从字符串中删除特殊字符的最有效方法


266

我想从字符串中删除所有特殊字符。允许的字符是AZ(大写或小写),数字(0-9),下划线(_)或点号(。)。

我有以下内容,它可以正常工作,但我怀疑(我知道!)它不是很有效:

    public static string RemoveSpecialCharacters(string str)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.Length; i++)
        {
            if ((str[i] >= '0' && str[i] <= '9')
                || (str[i] >= 'A' && str[i] <= 'z'
                    || (str[i] == '.' || str[i] == '_')))
                {
                    sb.Append(str[i]);
                }
        }

        return sb.ToString();
    }

最有效的方法是什么?正则表达式是什么样子,与普通的字符串操作相比如何?

将要清理的字符串会很短,通常在10到30个字符之间。


5
我不会在此给出答案,因为它不会更加有效,但是您可以在if语句中使用许多静态char方法(例如char.IsLetterOrDigit())使它至少更清晰易懂。
马丁·哈里斯

5
我不确定A到z的检查是否安全,因为它会引入6个不是字母的字符,只需要其中的一个(下划线)。
Steven Sudit

4
专注于使代码更具可读性。除非您以每秒500次的循环执行此操作,否则效率并不重要。使用正则表达式,它会更容易阅读。l–
拜伦·惠特洛克

4
拜伦,您可能需要强调可读性是正确的。但是,我对正则表达式是否可读表示怀疑。:-)
史蒂文·苏迪特

2
正则表达式是否可读,就像德语是否可读一样。这取决于您是否知道(尽管在两种情况下,您都会
时不时地

Answers:


325

为什么您认为您的方法效率不高?实际上,这是最有效的方法之一。

您当然应该将字符读入局部变量,或者使用枚举数来减少数组访问的次数:

public static string RemoveSpecialCharacters(this string str) {
   StringBuilder sb = new StringBuilder();
   foreach (char c in str) {
      if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_') {
         sb.Append(c);
      }
   }
   return sb.ToString();
}

使这种方法有效的一件事是它的伸缩性很好。执行时间将相对于字符串的长度。如果将它用在大字符串上没有令人讨厌的惊喜。

编辑:
我进行了一项快速的性能测试,使用24个字符串运行每个功能一百万次。结果如下:

原始功能:54.5毫秒。
我建议的更改:47.1毫秒。
设置StringBuilder容量的矿井:43.3毫秒。
正则表达式:294.4 ms。

编辑2:我在上面的代码中添加了AZ和az之间的区别。(我重新进行了性能测试,没有明显的区别。)

编辑3:
我测试了lookup + char []解决方案,它运行约13毫秒。

当然,要付出的代价是巨大查找表的初始化并将其保存在内存中。嗯,这不是很多数据,但是对于这样一个微不足道的功能来说却是很多...

private static bool[] _lookup;

static Program() {
   _lookup = new bool[65536];
   for (char c = '0'; c <= '9'; c++) _lookup[c] = true;
   for (char c = 'A'; c <= 'Z'; c++) _lookup[c] = true;
   for (char c = 'a'; c <= 'z'; c++) _lookup[c] = true;
   _lookup['.'] = true;
   _lookup['_'] = true;
}

public static string RemoveSpecialCharacters(string str) {
   char[] buffer = new char[str.Length];
   int index = 0;
   foreach (char c in str) {
      if (_lookup[c]) {
         buffer[index] = c;
         index++;
      }
   }
   return new string(buffer, 0, index);
}

4
我同意。我唯一要做的其他更改是将初始容量参数添加到StringBuilder构造函数“ = new StringBuilder(str.Length)”。
大卫,2009年

2
根据我的测试,我使用char[]缓冲区而不是的答案StringBuilder对此稍有优势。(尽管矿井的可读性较差,所以较小的性能优势可能不值得。)
LukeH

1
@Steven:确实可能是这样,但是基准可以说明一切!在我的测试中,即使将char[]缓冲区StringBuilder扩展到长度为成千上万个字符的字符串,使用缓冲区的性能(略)也优于。
路加福音

10
@downvoter:为什么要投票?如果您不解释自己认为的错误,将无法改善答案。
2011年

2
@SILENT:不,不是,但是您应该只做一次。如果您每次调用该方法时分配的数组很大(并且如果您频繁调用该方法),则该方法将成为迄今为止最慢的方法,并为垃圾收集器带来很多工作。
古法(Guffa)

195

好吧,除非您真的需要从功能中挤出性能,否则,请选择最容易维护和理解的内容。正则表达式如下所示:

为了提高性能,您可以预编译它,也可以告诉它在首次调用时进行编译(后续调用会更快。)

public static string RemoveSpecialCharacters(string str)
{
    return Regex.Replace(str, "[^a-zA-Z0-9_.]+", "", RegexOptions.Compiled);
}

1
我猜想这可能是一个足够复杂的查询,它比OP的方法要快,特别是如果经过预编译的话。但是,我没有证据支持这一点。应该进行测试。除非速度大大降低,否则无论如何我都会选择这种方法,因为它更易于阅读和维护。+1
rmeador

6
它是一个非常简单的正则表达式(其中没有回溯或任何复杂的东西),因此它应该非常快。

9
@rmeador:如果不进行编译,它的速度要比其方法慢大约5倍,而编译的速度要比其方法慢3倍。虽然仍然简单10倍:-D
user7116

6
正则表达式不是神奇的锤子,永远不会比手工优化的代码快。
Christian Klauser

2
对于那些记得Knuth关于优化的著名语录的人,这是起点。然后,如果您发现需要毫秒级的额外性能,请使用其他技术之一。
约翰

15

我建议创建一个简单的查找表,您可以在静态构造函数中对其进行初始化,以将任意字符组合设置为有效。这使您可以快速进行一次检查。

编辑

另外,为了提高速度,您需要将StringBuilder的容量初始化为输入字符串的长度。这将避免重新分配。这两种方法共同为您提供速度和灵活性。

另一个编辑

我认为编译器可能会对其进行优化,但是考虑到样式和效率,我建议使用foreach而不是for。


对于数组,forforeach产生类似的代码。我不知道字符串。我怀疑JIT是否了解String的类似数组的性质。
Christian Klauser

1
我敢打赌,JIT比您的[笑话删除]更了解字符串的类似数组的性质。Anders etal做了大量工作来优化.net中有关字符串的所有内容

我已经使用HashSet <char>完成了此操作,它比他的方法慢大约2倍。使用bool []几乎比他在OP中使用的版本快(0.0469ms / iter v.0.0559ms / iter)...存在可读性差的问题。
user7116

1
我看不到使用bool数组和int数组之间的性能差异。我将使用bool数组,因为它会将查找表从256 kb降低到64 kb,但是对于这种微不足道的功能来说,它仍然是很多数据……而且速度仅快30%。
Guffa

1
@Guffa 2)鉴于我们只保留字母数字和一些基本拉丁字符,因此我们只需要一个低字节表,所以大小并不是真正的问题。如果我们想成为通用的,则标准Unicode技术是双向间接寻址。换句话说,一个包含256个表引用的表,其中许多指向同一空表。
Steven Sudit

12
public static string RemoveSpecialCharacters(string str)
{
    char[] buffer = new char[str.Length];
    int idx = 0;

    foreach (char c in str)
    {
        if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')
            || (c >= 'a' && c <= 'z') || (c == '.') || (c == '_'))
        {
            buffer[idx] = c;
            idx++;
        }
    }

    return new string(buffer, 0, idx);
}

1
+1,经过测试,它比StringBuilder快40%。0.0294ms / string v.0.0399ms / string
user7116

可以肯定的是,您是指具有或不具有预分配功能的StringBuilder?
Steven Sudit

使用预分配,它仍然比char []分配和新字符串慢40%。
user7116

2
我喜欢这个。我调整了此方法foreach (char c in input.Where(c => char.IsLetterOrDigit(c) || allowedSpecialCharacters.Any(x => x == c))) buffer[idx++] = c;
克里斯·马里西克

11

正则表达式如下所示:

public string RemoveSpecialChars(string input)
{
    return Regex.Replace(input, @"[^0-9a-zA-Z\._]", string.Empty);
}

但是,如果性能非常重要,我建议您在选择“正则表达式路径”之前做一些基准测试。


11

如果您使用的是动态字符列表,则LINQ可能会提供一种更快,更优雅的解决方案:

public static string RemoveSpecialCharacters(string value, char[] specialCharacters)
{
    return new String(value.Except(specialCharacters).ToArray());
}

我将此方法与以前的两种“快速”方法(发行版)进行了比较:

  • LukeH的Char数组解决方案-427毫秒
  • StringBuilder解决方案-429毫秒
  • LINQ(此答案)-98毫秒

请注意,该算法稍作修改-字符以数组形式而不是硬编码形式传入,这可能会稍有影响(即,其他解决方案将具有内部foor循环来检查字符数组)。

如果我使用LINQ where子句切换到硬编码的解决方案,则结果是:

  • 字符数组解决方案-7ms
  • StringBuilder解决方案-22ms
  • LINQ-60毫秒

如果您打算编写更通用的解决方案,而不是硬编码字符列表,则可能值得考虑使用LINQ或改进的方法。LINQ绝对可以为您提供简洁,易读的代码-比Regex还要多。


3
这种方法看起来不错,但不起作用-Except()是set操作,因此最终只能看到字符串中每个唯一字符的首次出现。
McKenzieG1

5

我不认为您的算法是有效的。它是O(n),每个字符只看一次。除非您在检查值之前就神奇地知道了这些值,否则您将无法获得更好的结果。

但是,我会将您的容量初始化StringBuilder为字符串的初始大小。我猜您认为性能问题来自内存重新分配。

旁注:检查A- z不安全。你包括[\]^_,和`...

旁注2:为获得额外的效率,请对比较进行排序,以最大程度地减少比较次数。(在最坏的情况下,您正在说的是8个比较,因此,请不要太用心思考。)这种情况随您的预期输入而变化,但是一个示例可能是:

if (str[i] >= '0' && str[i] <= 'z' && 
    (str[i] >= 'a' || str[i] <= '9' ||  (str[i] >= 'A' && str[i] <= 'Z') || 
    str[i] == '_') || str[i] == '.')

旁注3:如果出于任何原因您确实需要快速,那么switch语句可能会更快。编译器应为您创建一个跳转表,仅进行一次比较:

switch (str[i])
{
    case '0':
    case '1':
    .
    .
    .
    case '.':
        sb.Append(str[i]);
        break;
}

1
我同意您不能在此击败O(n)。但是,每次比较的成本可以降低。表查找的固定成本较低,而随着添加更多的异常,一系列比较会增加成本。
Steven Sudit

关于旁注3,您真的认为跳转表会比表查找更快吗?
Steven Sudit

我对交换机解决方案进行了快速性能测试,其性能与比较结果相同。
加法

@Steven Sudit-我敢说他们实际上差不多。想要进行测试吗?
lc。

7
O(n)表示法有时会惹恼我。人们会基于该算法已经是O(n)的事实做出愚蠢的假设。如果我们更改此例程,以通过与世界另一端的服务器建立一次性SSL连接来检索比较值的函数来替换str [i]调用,那么该死的性能肯定会很高差异,算法为STILL O(n)。每个算法的O(1)代价是巨大的,并不相等!
darron



3

对我来说似乎很好。我唯一要做的改进是StringBuilder使用字符串的长度初始化。

StringBuilder sb = new StringBuilder(str.Length);

3

我同意此代码示例。唯一不同的是它使它成为字符串类型的扩展方法。这样您就可以在非常简单的行或代码中使用它:

string test = "abc@#$123";
test.RemoveSpecialCharacters();

感谢Guffa的实验。

public static class MethodExtensionHelper
    {
    public static string RemoveSpecialCharacters(this string str)
        {
            StringBuilder sb = new StringBuilder();
            foreach (char c in str)
            {
                if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
                {
                    sb.Append(c);
                }
            }
            return sb.ToString();
        }
}

2

我将使用带正则表达式的字符串替换来搜索“特殊字符”,并将所有找到的字符替换为空字符串。


+1肯定会减少代码,而忽略一次写入的Regex则更具可读性。
肯尼,

1
@kenny-我同意。最初的问题甚至指出字符串很短-10到30个字符。但是显然很多人仍然认为我们在第二时间就卖掉了CPU时间...
Tom Bushell

Reguler expressin的工作是如此懒惰,因此不应始终使用它。
RockOnGom

2

我必须为工作做类似的事情,但就我而言,我必须过滤所有不是字母,数字或空格的内容(但您可以根据需要轻松地对其进行修改)。过滤是在JavaScript的客户端完成的,但出于安全原因,我也在服务器端进行了过滤。由于可以预期大多数字符串都是干净的,因此除非我确实需要,否则我将避免复制字符串。这使我可以执行下面的实现,它对于干净的字符串和脏字符串都应具有更好的性能。

public static string EnsureOnlyLetterDigitOrWhiteSpace(string input)
{
    StringBuilder cleanedInput = null;
    for (var i = 0; i < input.Length; ++i)
    {
        var currentChar = input[i];
        var charIsValid = char.IsLetterOrDigit(currentChar) || char.IsWhiteSpace(currentChar);

        if (charIsValid)
        {
            if(cleanedInput != null)
                cleanedInput.Append(currentChar);
        }
        else
        {
            if (cleanedInput != null) continue;
            cleanedInput = new StringBuilder();
            if (i > 0)
                cleanedInput.Append(input.Substring(0, i));
        }
    }

    return cleanedInput == null ? input : cleanedInput.ToString();
}

1

对于S&G而言,Linq可以通过以下方式实现:

var original = "(*^%foo)(@)&^@#><>?:\":';=-+_";
var valid = new char[] { 
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 
    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 
    'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 
    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', 
    '9', '0', '.', '_' };
var result = string.Join("",
    (from x in original.ToCharArray() 
     where valid.Contains(x) select x.ToString())
        .ToArray());

但是,我认为这不是最有效的方法。


2
不是,因为它是线性搜索。
史蒂文·苏迪特

1
public string RemoveSpecial(string evalstr)
{
StringBuilder finalstr = new StringBuilder();
            foreach(char c in evalstr){
            int charassci = Convert.ToInt16(c);
            if (!(charassci >= 33 && charassci <= 47))// special char ???
             finalstr.append(c);
            }
return finalstr.ToString();
}

1

用:

s.erase(std::remove_if(s.begin(), s.end(), my_predicate), s.end());

bool my_predicate(char c)
{
 return !(isalpha(c) || c=='_' || c==' '); // depending on you definition of special characters
}

然后您会得到一个干净的字符串s

erase()会删除所有特殊字符,并且可以通过该my_predicate()功能进行高度自定义。


1

HashSet为O(1)
不确定它是否比现有比较快

private static HashSet<char> ValidChars = new HashSet<char>() { 'a', 'b', 'c', 'A', 'B', 'C', '1', '2', '3', '_' };
public static string RemoveSpecialCharacters(string str)
{
    StringBuilder sb = new StringBuilder(str.Length / 2);
    foreach (char c in str)
    {
        if (ValidChars.Contains(c)) sb.Append(c);
    }
    return sb.ToString();
}

我进行了测试,而且速度不超过公认的答案。
我将保留它,好像您需要一组可配置的字符一样,这​​将是一个很好的解决方案。


您为什么认为比较不是O(1)?
Guffa 2013年

@Guffa我不确定不是,我删除了我的评论。和+1。在发表评论之前,我应该做更多的测试。
狗仔队

1

我想知道基于Regex的替换(可能是编译的)是否更快。必须测试是否有人发现它慢了大约5倍。

除此之外,您应该使用预期的长度初始化StringBuilder,以便中间字符串在增长时不必复制。

一个好的数字是原始字符串的长度,或者稍微短一些(取决于函数输入的性质)。

最后,您可以使用查找表(范围为0..127)来确定是否接受字符。


正则表达式已经过测试,速度要慢五倍。对于范围为0..127的查找表,您仍然必须在使用查找表之前对字符代码进行范围检查,因为字符是16位值,而不是7位值。
Guffa

@Guffa Err ...是吗?;)
Christian Klauser 2013年

1

以下代码具有以下输出(结论是我们还可以节省一些内存资源,以较小的数组分配数组):

lookup = new bool[123];

for (var c = '0'; c <= '9'; c++)
{
    lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
}

for (var c = 'A'; c <= 'Z'; c++)
{
    lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
}

for (var c = 'a'; c <= 'z'; c++)
{
    lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
}

48: 0  
49: 1  
50: 2  
51: 3  
52: 4  
53: 5  
54: 6  
55: 7  
56: 8  
57: 9  
65: A  
66: B  
67: C  
68: D  
69: E  
70: F  
71: G  
72: H  
73: I  
74: J  
75: K  
76: L  
77: M  
78: N  
79: O  
80: P  
81: Q  
82: R  
83: S  
84: T  
85: U  
86: V  
87: W  
88: X  
89: Y  
90: Z  
97: a  
98: b  
99: c  
100: d  
101: e  
102: f  
103: g  
104: h  
105: i  
106: j  
107: k  
108: l  
109: m  
110: n  
111: o  
112: p  
113: q  
114: r  
115: s  
116: t  
117: u  
118: v  
119: w  
120: x  
121: y  
122: z  

您还可以添加以下代码行以支持俄语语言环境(数组大小为1104):

for (var c = 'А'; c <= 'Я'; c++)
{
    lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
}

for (var c = 'а'; c <= 'я'; c++)
{
    lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
}

1

我不确定这是最有效的方法,但对我有用

 Public Function RemoverTildes(stIn As String) As String
    Dim stFormD As String = stIn.Normalize(NormalizationForm.FormD)
    Dim sb As New StringBuilder()

    For ich As Integer = 0 To stFormD.Length - 1
        Dim uc As UnicodeCategory = CharUnicodeInfo.GetUnicodeCategory(stFormD(ich))
        If uc <> UnicodeCategory.NonSpacingMark Then
            sb.Append(stFormD(ich))
        End If
    Next
    Return (sb.ToString().Normalize(NormalizationForm.FormC))
End Function

答案确实有效,但问题是针对C#的。(PS:我知道这实际上是在五年前,但是仍然..)我使用了Telerik VB到C#转换器,(反之亦然),并且代码工作得很好-尽管不确定其他人。(另一件事,converter.telerik.com
Momoro

1

这里有很多建议的解决方案,有些解决方案比其他解决方案更有效,但可能不太可读。这可能不是最高效的,但肯定可以在大多数情况下使用,并且利用Linq十分简洁易读:

string stringToclean = "This is a test.  Do not try this at home; you might get hurt. Don't believe it?";

var validPunctuation = new HashSet<char>(". -");

var cleanedVersion = new String(stringToclean.Where(x => (x >= 'A' && x <= 'Z') || (x >= 'a' && x <= 'z') || validPunctuation.Contains(x)).ToArray());

var cleanedLowercaseVersion = new String(stringToclean.ToLower().Where(x => (x >= 'a' && x <= 'z') || validPunctuation.Contains(x)).ToArray());

-1
public static string RemoveSpecialCharacters(string str){
    return str.replaceAll("[^A-Za-z0-9_\\\\.]", "");
}

1
恐怕replaceAll不是C#字符串函数,而是Java或JavaScript
Csaba Toth 2013年

-1
public static string RemoveAllSpecialCharacters(this string text) {
  if (string.IsNullOrEmpty(text))
    return text;

  string result = Regex.Replace(text, "[:!@#$%^&*()}{|\":?><\\[\\]\\;'/.,~]", " ");
  return result;
}

答案是错误的。如果您要使用正则表达式,那么它应该是包含性的,而不是排他性的,因为您现在错过了一些字符。实际上,使用正则表达式已经可以解决问题了。并且要完整-正则表达式是SLOWER然后直接比较chars函数。
TPAKTOPA

-3

如果您担心速度,请使用指针来编辑现有字符串。您可以固定字符串并获得指向它的指针,然后在每个字符上运行for循环,并用替换字符覆盖每个无效字符。这将是非常高效的,并且不需要分配任何新的字符串内存。您还需要使用unsafe选项编译模块,并在方法标题中添加“ unsafe”修饰符以使用指针。

static void Main(string[] args)
{
    string str = "string!$%with^&*invalid!!characters";
    Console.WriteLine( str ); //print original string
    FixMyString( str, ' ' );
    Console.WriteLine( str ); //print string again to verify that it has been modified
    Console.ReadLine(); //pause to leave command prompt open
}


public static unsafe void FixMyString( string str, char replacement_char )
{
    fixed (char* p_str = str)
    {
        char* c = p_str; //temp pointer, since p_str is read-only
        for (int i = 0; i < str.Length; i++, c++) //loop through each character in string, advancing the character pointer as well
            if (!IsValidChar(*c)) //check whether the current character is invalid
                (*c) = replacement_char; //overwrite character in existing string with replacement character
    }
}

public static bool IsValidChar( char c )
{
    return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '.' || c == '_');
    //return char.IsLetterOrDigit( c ) || c == '.' || c == '_'; //this may work as well
}

14
不!在.NET中更改字符串是BAAAAAAAAAAAAD!框架中的所有内容都依赖于字符串是不可变的规则,如果您破坏了字符串,则会产生非常令人惊讶的副作用……
Guffa
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.