簡體   English   中英

從字符串中刪除特殊字符的最有效方法

[英]Most efficient way to remove special characters from string

我想從字符串中刪除所有特殊字符。 允許的字符為 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 個字符之間。

為什么你認為你的方法效率不高? 這實際上是您可以做到的最有效的方法之一。

您當然應該將字符讀入局部變量或使用枚舉器來減少數組訪問次數:

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 ms。
正則表達式:294.4 毫秒。

編輯 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);
}

好吧,除非你真的需要從你的函數中擠出性能,否則就選擇最容易維護和理解的東西。 正則表達式如下所示:

為了獲得額外的性能,您可以預編譯它,或者只是告訴它在第一次調用時編譯(后續調用會更快。)

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

我建議創建一個簡單的查找表,您可以在靜態構造函數中對其進行初始化,以將任意字符組合設置為有效。 這使您可以進行快速、單一的檢查。

編輯

此外,為了速度,您需要將 StringBuilder 的容量初始化為輸入字符串的長度。 這將避免重新分配。 這兩種方法一起將為您提供速度和靈活性。

另一個編輯

我認為編譯器可能會對其進行優化,但出於風格和效率的考慮,我建議使用 foreach 而不是 for。

正則表達式將如下所示:

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

但如果性能非常重要,我建議您在選擇“正則表達式路徑”之前做一些基准測試......

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

如果您使用的是動態字符列表,LINQ 可能會提供更快、更優雅的解決方案:

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

我將這種方法與之前的兩種“快速”方法(發布編譯)進行了比較:

  • LukeH 的字符數組解決方案 - 427 毫秒
  • StringBuilder 解決方案 - 429 毫秒
  • LINQ(這個答案) - 98 毫秒

請注意,該算法略有修改 - 字符作為數組而不是硬編碼傳入,這可能會稍微影響一些事情(即/其他解決方案將有一個內部 foo 循環來檢查字符數組)。

如果我使用 LINQ where 子句切換到硬編碼解決方案,結果是:

  • 字符數組解決方案 - 7ms
  • StringBuilder 解決方案 - 22ms
  • LINQ - 60 毫秒

如果您打算編寫更通用的解決方案,而不是對字符列表進行硬編碼,那么可能值得研究 LINQ 或修改后的方法。 LINQ 絕對可以為您提供簡潔、易讀的代碼——甚至比 Regex 還要多。

我不相信你的算法是有效的。 這是 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;
}

您可以使用正則表達式,如下所示:

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

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

這對我來說似乎很好。 我要做的唯一改進是用字符串的長度初始化StringBuilder

StringBuilder sb = new StringBuilder(str.Length);

我同意這個代碼示例。 唯一不同的是我把它變成了字符串類型的擴展方法。 這樣您就可以在非常簡單的行或代碼中使用它:

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

我會使用字符串替換和正則表達式搜索“特殊字符”,用空字符串替換找到的所有字符。

我必須為工作做類似的事情,但在我的情況下,我必須過濾所有不是字母、數字或空格的東西(但你可以根據需要輕松修改它)。 過濾是在 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();
}

這里有很多建議的解決方案,有些比其他的更有效,但可能不是很可讀。 這可能不是最有效的,但肯定適用於大多數情況,並且非常簡潔易讀,利用了 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());

下面的代碼有以下輸出(結論是我們還可以節省一些內存資源分配數組更小的大小):

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

我想知道基於正則表達式的替換(可能已編譯)是否更快。 必須測試 有人發現這要慢 5 倍。

除此之外,您應該使用預期長度初始化 StringBuilder,這樣中間字符串就不必在它增長時被復制。

一個好的數字是原始字符串的長度,或者稍低一些(取決於函數輸入的性質)。

最后,您可以使用查找表(范圍為 0..127)來確定是否要接受字符。

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

但是,我認為這不會是最有效的方法。

利用:

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()函數進行高度自定義。

哈希集是 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();
}

我進行了測試,這並不比接受的答案快。
我會保留它,就好像您需要一組可配置的字符一樣,這​​將是一個很好的解決方案。

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

我不確定這是最有效的方法,但它對我有用

 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

另一種嘗試通過減少分配來提高性能的方法,尤其是在多次調用此函數時。

它之所以有效,是因為您可以保證結果不會比輸入長,因此可以傳遞輸入和輸出,而無需在內存中創建額外的副本。 出於這個原因,您不能使用stackalloc創建緩沖區數組,因為這需要從緩沖區中復制。

public static string RemoveSpecialCharacters(this string str)
{
    return RemoveSpecialCharacters(str.AsSpan()).ToString();
}

public static ReadOnlySpan<char> RemoveSpecialCharacters(this ReadOnlySpan<char> str)
{
    Span<char> buffer = new char[str.Length];
    int idx = 0;

    foreach (char c in str)
    {
        if (char.IsLetterOrDigit(c))
        {
            buffer[idx] = c;
            idx++;
        }
    }

    return buffer.Slice(0, idx);
}

最短的路只有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();
}

使用LINQ的簡單方法

string text = "123a22 ";
var newText = String.Join(string.Empty, text.Where(x => x != 'a'));

如果您需要在出現注入或拼寫錯誤(罕見事件)的情況下清理輸入字符串,最快的方法是使用switch()檢查所有字符(編譯器在優化switch() ) 加上附加代碼以刪除不需要的字符(如果發現)。 這是解決方案:

    public static string RemoveExtraCharacters(string input)
    {
        if (string.IsNullOrEmpty(input))
            return "";

        input = input.Trim();

        StringBuilder sb = null;

    reStart:
        if (!string.IsNullOrEmpty(input))
        {
            var len = input.Length; ;

            for (int i = 0; i < len; i++)
            {
                switch (input[i])
                {
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                    case 'A':
                    case 'B':
                    case 'C':
                    case 'D':
                    case 'E':
                    case 'F':
                    case 'G':
                    case 'H':
                    case 'I':
                    case 'J':
                    case 'K':
                    case 'L':
                    case 'M':
                    case 'N':
                    case 'O':
                    case 'Q':
                    case 'P':
                    case 'R':
                    case 'S':
                    case 'T':
                    case 'U':
                    case 'V':
                    case 'W':
                    case 'X':
                    case 'Y':
                    case 'Z':
                    case 'a':
                    case 'b':
                    case 'c':
                    case 'd':
                    case 'e':
                    case 'f':
                    case 'g':
                    case 'h':
                    case 'i':
                    case 'j':
                    case 'k':
                    case 'l':
                    case 'm':
                    case 'n':
                    case 'o':
                    case 'q':
                    case 'p':
                    case 'r':
                    case 's':
                    case 't':
                    case 'u':
                    case 'v':
                    case 'w':
                    case 'x':
                    case 'y':
                    case 'z':
                    case '/':
                    case '_':
                    case '-':
                    case '+':
                    case '.':
                    case ',':
                    case '*':
                    case ':':
                    case '=':
                    case ' ':
                    case '^':
                    case '$':
                        break;  

                    default:
                        if (sb == null)
                            sb = new StringBuilder();

                        sb.Append(input.Substring(0, i));
                        if (i + 1 < len)
                        {
                            input = input.Substring(i + 1);
                            goto reStart;
                        }
                        else
                            input = null;
                        break;
                }
            }
        }

        if (sb != null)
        {
            if (input != null)
                sb.Append(input);
            return sb.ToString();
        }

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

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

如果您擔心速度,請使用指針來編輯現有字符串。 您可以固定字符串並獲取指向它的指針,然后對每個字符運行 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
}
public static string RemoveSpecialCharacters(string str){
    return str.replaceAll("[^A-Za-z0-9_\\\\.]", "");
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM