C# удаляет возврат каретки, разрывы строк и пробелы из строки настолько эффективно, насколько это возможно

В С# у меня есть строка, содержащая пробелы, возврат каретки и/или разрывы строк. Есть ли простой способ нормализовать большие строки (от 100 000 до 1 000 000 символов), которые импортируются из текстовых файлов, настолько эффективно, насколько это возможно?

Чтобы уточнить, что я имею в виду: допустим, моя строка выглядит как строка1, но я хочу, чтобы она была похожа на строку2.

string1 = " ab c\r\n de.\nf";
string2 = "abcde.f";

person Daniel    schedule 26.06.2020    source источник
comment
что ты уже испробовал?   -  person Mikael    schedule 26.06.2020
comment
Я попробовал регулярные выражения, которые показались мне медленными, поэтому я попросил эффективный способ.   -  person Daniel    schedule 26.06.2020
comment
@MikeBeaton подход, указанный в ссылке, полезен, когда вы вызываете регулярное выражение несколько раз, но вместо этого у меня есть одна большая строка, поэтому, на мой взгляд, мой вопрос отличается.   -  person Daniel    schedule 26.06.2020
comment
Хорошо. К вашему сведению, я официально не помечал это как дубликат и не закрывал его - должен быть кто-то еще из SO - я просто добавил комментарий для вашей дополнительной информации!   -  person MikeBeaton    schedule 26.06.2020
comment
@MikeBeaton Да, я знаю, но спасибо за информацию и ваш ответ, который помог мне с другой проблемой, которую мне нужно было решить ;-)   -  person Daniel    schedule 26.06.2020
comment
Очень важно, насколько длинны ваши струны. Я провел некоторое тестирование, и примерно на отметке 10_000 символов параллельный метод начал превосходить метод NewString, после достижения отметки 100_000_000 параллельная версия была чуть более чем в 15 раз быстрее (также тестировался с помощью BenchmarkDotNet).   -  person Knoop    schedule 26.06.2020
comment
@Knoop Вы хотите, чтобы я указал это в вопросе? Строки, которые у меня есть, имеют длину от 10 000 до 100 000 символов.   -  person Daniel    schedule 26.06.2020
comment
С такими длинами у вас есть хорошие шансы, что метод Parallel выполнит принятый ответ, но это также зависит от оборудования, на котором он работает. Проблемы оптимизации и эффективности редко решаются прямо. Поэтому, если принятый ответ достаточно быстр, я бы просто согласился с ним (также с точки зрения того, что лучше избегать преждевременной оптимизации)   -  person Knoop    schedule 26.06.2020


Ответы (4)


Термин «эффективно» может сильно зависеть от ваших фактических строк и их количества. Я придумал следующий тест (для BenchmarkDotNet):

public class Replace
{
    private static readonly string S = " ab c\r\n de.\nf";
    private static readonly Regex Reg = new Regex(@"\s+", RegexOptions.Compiled);

    [Benchmark]
    public string SimpleReplace() => S
       .Replace(" ","")
       .Replace("\\r","")
       .Replace("\\n","");

    [Benchmark]
    public string StringBuilder() => new StringBuilder().Append(S)
       .Replace(" ","")
       .Replace("\\r","")
       .Replace("\\n","")
       .ToString();

    [Benchmark]
    public string RegexReplace() => Reg.Replace(S, "");

    [Benchmark]
    public string NewString()
    {
            var arr = new char[S.Length];
            var cnt = 0;
            for (int i = 0; i < S.Length; i++)
            {
                switch(S[i])
                {
                    case ' ':
                    case '\r':
                    case '\n':
                        break;

                    default:
                        arr[cnt] = S[i];
                        cnt++;
                        break;
                }
            }

            return new string(arr, 0, cnt);
    }

    [Benchmark]
    public string NewStringForeach()
    {
        var validCharacters = new char[S.Length];
        var next = 0;

        foreach(var c in S)
        {
            switch(c)
            {
                case ' ':
                case '\r':
                case '\n':
                    // Ignore then
                    break;

                default:
                    validCharacters[next++] = c;
                    break;
            }
        }

        return new string(validCharacters, 0, next);
    }
} 

Это дает на моей машине:

|          Method |        Mean |     Error |    StdDev |
|---------------- |------------:|----------:|----------:|
|   SimpleReplace |   122.09 ns |  1.273 ns |  1.063 ns |
|   StringBuilder |   311.28 ns |  6.313 ns |  8.850 ns |
|    RegexReplace | 1,194.91 ns | 23.376 ns | 34.265 ns |
|       NewString |    52.26 ns |  1.122 ns |  1.812 ns |
|NewStringForeach |    40.04 ns |  0.877 ns |  1.979 ns |
person Guru Stron    schedule 26.06.2020
comment
Спасибо за ваш потрясающий ответ! Если я правильно интерпретирую это, это означает, что подход @Sean пока самый быстрый. Я прав? - person Daniel; 26.06.2020
comment
@ Даниэль, да. Но опять же, вы должны протестировать фактическую рабочую нагрузку. - person Guru Stron; 26.06.2020
comment
Поскольку OP заявил, что желаемая оптимизация предназначена специально для больших строк, imo, хороший тест должен представлять это. Некоторые решения могут иметь больше накладных расходов при запуске, но быть быстрее в расчете на символ, из-за чего они проигрывают на коротких строках, но выходят вперед на очень длинных строках. - person Knoop; 26.06.2020
comment
Термин @Knoop большой очень широк, поэтому я предложил протестировать (на стенде) фактическую рабочую нагрузку (и реальное оборудование). Потенциально даже частота конкретных символов может повлиять на производительность того или иного решения. - person Guru Stron; 26.06.2020
comment
@GuruStron, поскольку у нас есть .net core 3.1, не могли бы вы добавить версию foreach в результат теста? Вы можете скопировать его отсюда: dotnetfiddle.net/Gr6knH - person sTrenat; 26.06.2020
comment
@sTrenat добавил, как вы просили. - person Guru Stron; 26.06.2020

Чтобы сделать это эффективно, вы хотите избежать регулярных выражений и свести выделение памяти к минимуму: здесь я использовал необработанный буфер символов (а не StringBuilder) и for вместо foreach для оптимизации доступа к каждому символу:

string Strip(string text)
{
    var validCharacters = new char[text.Length];
    var next = 0;

    for(int i = 0; i < text.Length; i++)
    {
        char c = text[i];

        switch(c)
        {
            case ' ':
            case '\r':
            case '\n':
                // Ignore then
                break;

            default:
                validCharacters[next++] = c;
                break;
        }
    }

    return new string(validCharacters, 0, next);
}
person Sean    schedule 26.06.2020
comment
Возможно, лучше использовать что-то вроде char.IsWhitespace и char.IsControl, так как я предполагаю, что они хотят удалить все пробелы/непечатаемые - person pinkfloydx33; 26.06.2020
comment
Не могу согласиться с тем, что for будет эффективнее. Я бы даже сказал, что for будет медленнее из-за оптимизации, которую компилятор может сделать с foreach - person sTrenat; 26.06.2020
comment
@sTrenat - foreach будет включать вызов для получения перечислителя (возможно, структуры), а затем вызов MoveNext для каждой итерации и вызов Current для получения фактического значения. Затем реализация просто захватывает символ из указанного индекса. Все это может быть оптимизировано, а может и нет. - person Sean; 26.06.2020
comment
Я думаю, вы недооцениваете возможности оптимизации компилятора. Проверьте сами: dotnetfiddle.net/Gr6knH - person sTrenat; 26.06.2020
comment
@sTrenat - Спасибо за ссылку. Однако, если вы поменяете местами вызовы Benchmark так, чтобы сначала вы делали StripForEach, а затем Strip, то вы обнаружите, что Strip работает быстрее! : dotnetfiddle.net/BtrSXt - person Sean; 26.06.2020
comment
@sTrenat переключает их, и результаты обратятся в пользу for, проверьте сами: dotnetfiddle.net/M12Lwh . Другими словами, это не очень хороший ориентир. Есть ли у вас какая-либо фактическая теория/документация, подтверждающая это? Последнее, что я знаю, оптимизация, которую на самом деле делает компилятор в случае foreach для массива, заключается в итерации вместо использования перечислителя, что делает его почти таким же быстрым, как для - person Knoop; 26.06.2020
comment
Ты прав, кажется, что первый вызов всегда медленнее на этой скамейке. Но все же кажется, что ни один из циклов не работает быстрее. - person sTrenat; 26.06.2020
comment
Когда я запускал его с помощью BenchmarkDotNet, результат был: Strip | 36.42 ns | 0.389 ns | 0.363 ns StripForeach | 27.83 ns | 0.488 ns | 0.457 ns, кажется, все же Foreach лучше - person sTrenat; 26.06.2020

var input = " ab c\r\n de.\nf";
var result = Regex.Replace(input, @"\s+", "");

// result is now "abcde.f"

Вы можете увидеть его в действии здесь

person Blindy    schedule 26.06.2020
comment
Спасибо за Ваш ответ! Но является ли использование регулярных выражений наиболее эффективным способом? - person Daniel; 26.06.2020
comment
Это достаточно эффективно! - person Blindy; 26.06.2020

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

private string RemoveUnnecessaryChars(string firstName)
{
    StringBuilder sb = new StringBuilder();
    string allowedCharacters = _configuration["AllowedChars"];
    foreach (char ch in firstName)
    {
        if (char.IsLetterOrDigit(ch))
        {
            sb.Append(ch);
        }
        else
        {
            if (allowedCharacters.Contains(ch))
            {
                sb.Append(ch);
            }
        }
    }

    return sb.ToString();
}
person vivek nuna    schedule 26.06.2020
comment
Спасибо за ваш ответ, однако я надеялся найти более простой способ сделать это. - person Daniel; 26.06.2020
comment
Вероятно, лучше сделать HashSet из разрешенных символов (в зависимости от того, что там есть), но также, вероятно, лучше определить черный список, а не белый список - person pinkfloydx33; 26.06.2020
comment
@pinkfloydx33 спасибо, это верное замечание - person vivek nuna; 26.06.2020