Самый эффективный способ удаления специальных символов из строки

Я хочу удалить все специальные символы из строки. Допустимые символы: 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 символов.

Ответ 1

Почему вы думаете, что ваш метод неэффективен? Это на самом деле один из наиболее эффективных способов, которыми вы можете это сделать.

Вы должны, конечно, прочитать символ в локальной переменной или использовать перечислитель, чтобы уменьшить количество обращений к массиву:

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

Одна вещь, которая делает такой метод эффективным, заключается в том, что он хорошо масштабируется. Время выполнения будет относиться к длине строки. Нет никаких неприятных сюрпризов, если вы будете использовать его на большой строке.

Edit:
Я сделал быстрый тест производительности, выполнив каждую функцию миллион раз с 24-символьной строкой. Вот результаты:

Функция оригинала: 54,5 мс.
Мое предложенное изменение: 47,1 мс.
Шахта с настройкой пропускной способности StringBuilder: 43,3 мс.
Регулярное выражение: 294,4 мс.

Изменить 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);
}

Ответ 2

Хорошо, если вам действительно не нужно выжимать производительность из своей функции, просто пойдите с тем, что проще всего поддерживать и понимать. Регулярное выражение будет выглядеть так:

Для дополнительной производительности вы можете либо предварительно скомпилировать его, либо просто сказать, что он скомпилирован при первом вызове (последующие вызовы будут быстрее.)

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

Ответ 3

Я предлагаю создать простую таблицу поиска, которую вы можете инициализировать в статическом конструкторе, чтобы установить любую комбинацию символов в действительную. Это позволяет вам выполнить единую проверку.

изменить

Кроме того, для скорости вы хотите инициализировать емкость вашего StringBuilder до длины строки ввода. Это позволит избежать перераспределения. Эти два метода вместе дадут вам как скорость, так и гибкость.

другое редактирование

Я думаю, что компилятор может оптимизировать его, но как вопрос стиля, так и эффективности, я рекомендую foreach, а не for.

Ответ 4

Регулярное выражение будет выглядеть так:

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

Но если производительность очень важна, я рекомендую вам выполнить некоторые тесты перед выбором "пути регулярного выражения"...

Ответ 5

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

Ответ 6

Если вы используете динамический список символов, LINQ может предложить гораздо более быстрое и изящное решение:

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

Я сравнил этот подход с двумя предыдущими "быстрыми" подходами (компиляция выпуска):

  • Char массивное решение от LukeH - 427 мс
  • Решение StringBuilder - 429 мс
  • LINQ (этот ответ) - 98 мс

Обратите внимание, что алгоритм слегка изменен - ​​символы передаются как массив, а не жестко закодированные, что может слегка повлиять на вещи (то есть/другие решения будут иметь внутреннюю петлю для проверки массива символов).

Если я переключусь на жестко закодированное решение, используя предложение LINQ where, результаты:

  • Char массивное решение - 7 мс
  • Решение StringBuilder - 22 мс
  • LINQ - 60 мс

Возможно, стоит взглянуть на LINQ или модифицированный подход, если вы планируете писать более общее решение, а не жестко кодировать список символов. LINQ определенно дает вам сжатый, читаемый код - даже больше, чем Regex.

Ответ 7

Я не уверен, что ваш алгоритм эффективен. Он 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;
}

Ответ 8

Я бы использовал String Replace с регулярным выражением для поиска "специальных символов", заменив все символы, найденные пустой строкой.

Ответ 9

Мне кажется хорошо. Единственное улучшение, которое я сделал бы, это инициализировать StringBuilder длиной строки.

StringBuilder sb = new StringBuilder(str.Length);

Ответ 10

StringBuilder sb = new StringBuilder();

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

Ответ 11

Я согласен с этим примером кода. Единственное, что я делаю в Extension Method типа string. Чтобы вы могли использовать его в очень простой строке или коде:

string test = "[email protected]#$123";
test.RemoveSpecialCharacters();

Благодарим Гуффа за ваш эксперимент.

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

Ответ 12

Мне нужно было сделать что-то подобное для работы, но в моем случае мне пришлось отфильтровать все, что не является буквой, числом или пробелом (но вы можете легко изменить его в соответствии с вашими потребностями). Фильтрация выполняется на стороне клиента в 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

Это не кажется вообще неэффективным. Возможно, вы сможете улучшить его, но вы можете поставить под угрозу удобочитаемость/ремонтопригодность.

С уважением

Ответ 14

Интересно, стоит ли замена на основе Regex (возможно, скомпилированная) быстрее. Надо было бы проверить, что Кто-то нашел, что это примерно в 5 раз медленнее.

Кроме этого, вы должны инициализировать StringBuilder с ожидаемой длиной, так что промежуточную строку не нужно копировать, пока она растет.

Хорошее число - это длина исходной строки или что-то немного ниже (в зависимости от характера входов функций).

Наконец, вы можете использовать таблицу поиска (в диапазоне 0..127), чтобы узнать, должен ли быть принят символ.

Ответ 15

Для S & 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());

Я не думаю, что это будет самый эффективный способ.

Ответ 16

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

Ответ 17

Следующий код имеет следующий вывод (вывод состоит в том, что мы также можем сохранить некоторые ресурсы памяти, выделяющие меньший размер массива):

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

Ответ 18

Использование:

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

Ответ 19

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

Я тестировал и это не быстрее, чем принятый ответ.
Я оставлю его, как если бы вам нужен настраиваемый набор символов, это было бы хорошим решением.

Ответ 20

Я не уверен, что это самый эффективный способ, но он работает для меня.

 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

Ответ 21

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

Ответ 22

Если вы беспокоитесь о скорости, используйте указатели для редактирования существующей строки. Вы можете привязать строку и получить указатель на нее, а затем запустить цикл for над каждым символом, перезаписав каждый недопустимый символ с помощью символа замены. Это было бы чрезвычайно эффективно и не требовало бы выделения новой строковой памяти. Вам также нужно будет скомпилировать ваш модуль с небезопасным вариантом и добавить "небезопасный" модификатор в заголовок вашего метода, чтобы использовать указатели.

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
}