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

时间:2009-07-13 15:33:42

标签: c# string

我想从字符串中删除所有特殊字符。允许的字符是A-Z(大写或小写),数字(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个字符之间。

25 个答案:

答案 0 :(得分:306)

为什么你认为你的方法效率不高?它实际上是你可以做到的最有效的方法之一。

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

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: 我在上面的代码中添加了A-Z和a-z之间的区别。 (我重新进行了性能测试,并没有明显的区别。)

编辑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);
}

答案 1 :(得分:172)

好吧,除非你真的需要从你的功能中挤出性能,否则只需要最简单的维护和理解。正则表达式如下所示:

为了获得额外的性能,您可以预编译它,也可以告诉它在第一次调用时编译(后续调用会更快。)

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

答案 2 :(得分:15)

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

修改

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

另一个编辑

我认为编译器可能会优化它,但是作为风格和效率的问题,我建议使用foreach而不是for。

答案 3 :(得分: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);
}

答案 4 :(得分:11)

正则表达式如下所示:

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

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

答案 5 :(得分:9)

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

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

我将这种方法与之前的两种“快速”方法(发布编译)进行了比较:

  • LukeH的Char阵列解决方案 - 427 ms
  • StringBuilder解决方案 - 429 ms
  • LINQ(这个答案) - 98毫秒

请注意,该算法稍作修改 - 字符作为数组而不是硬编码传入,这可能会略微影响事物(即/其他解决方案将有一个内部循环来检查字符数组)。

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

  • Char数组解决方案 - 7ms
  • StringBuilder解决方案 - 22ms
  • LINQ - 60 ms

如果您计划编写更通用的解决方案,而不是对字符列表进行硬编码,那么可能值得查看LINQ或修改后的方法。 LINQ绝对能为您提供简洁,高度可读的代码 - 甚至比Regex还要多。

答案 6 :(得分: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;
}

答案 7 :(得分:3)

您可以按如下方式使用常规表达式:

return Regex.Replace(strIn, @"[^\w\.@-]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0));

答案 8 :(得分: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();
        }
}

答案 9 :(得分:3)

StringBuilder sb = new StringBuilder();

for (int i = 0; i < fName.Length; i++)
{
   if (char.IsLetterOrDigit(fName[i]))
    {
       sb.Append(fName[i]);
    }
}

答案 10 :(得分:3)

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

StringBuilder sb = new StringBuilder(str.Length);

答案 11 :(得分:2)

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

答案 12 :(得分: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();
}

答案 13 :(得分: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

答案 14 :(得分: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();
}

答案 15 :(得分: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());

答案 16 :(得分: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();
}

我测试了这个并不比接受的答案快。
我会把它留下来,好像你需要一组可配置的字符,这将是一个很好的解决方案。

答案 17 :(得分: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()函数进行高度自定义。

答案 18 :(得分: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);
}

答案 19 :(得分:1)

对于S&amp; G's,Linq-ified方式:

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());

然而,我不认为这将是最有效的方式。

答案 20 :(得分:1)

我想知道基于正则表达式的替换(可能已编译)是否更快。 必须测试有人发现这个速度要慢5倍。

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

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

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

答案 21 :(得分:0)

最短的方式只是 3 行...

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

答案 22 :(得分:-1)

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

答案 23 :(得分:-2)

如果您担心速度,请使用指针编辑现有字符串。您可以固定字符串并获取指向它的指针,然后在每个字符上运行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
}

答案 24 :(得分:-2)

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

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