Как заменить несколько пробелов одним пробелом

108

Скажем, у меня есть такая строка, как:

"Hello     how are   you           doing?"

Мне нужна функция, которая превращает несколько пробелов в одно пространство.

Так я бы получил:

"Hello how are you doing?"

Я знаю, что могу использовать регулярное выражение или позвонить

string s = "Hello     how are   you           doing?".replace("  "," ");

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

Есть ли уже встроенный метод для этого?

Мэтт
источник
Не могли бы вы уточнить: вы имеете дело только с пробелами или "всеми" пробелами?
Джон Скит,
И вы хотите, чтобы любые непробельные пробелы были преобразованы в пробелы?
Джон Скит,
Я просто имел в виду, что все пробелы в серии должны быть не более 1
Мэтт,
1
Возможный дубликат stackoverflow.com/questions/206717/…
Майкл Фрейдгейм
2 вещи , чтобы рассмотреть следующие вопросы: 1. char.IsWhiteSpace включает в себя возврат каретки, перевод строки и т.д. 2. «пробел», вероятно , более точно протестированы с Char.GetUnicodeCategory (CH) = Globalization.UnicodeCategory.SpaceSeparator
smirkingman

Ответы:

196
string cleanedString = System.Text.RegularExpressions.Regex.Replace(dirtyString,@"\s+"," ");
Тим Хулихэн
источник
40
imo, избегайте регулярных выражений, если вам комфортно с ними, преждевременная оптимизация
Тим Хулихан
8
Если ваше приложение не критично по времени, оно может позволить себе 1 микросекунду накладных расходов на обработку.
Дэниел
16
Обратите внимание, что '\ s' заменяет не только пробелы, но и символы новой строки.
Барт Кирс,
12
хороший улов, если вы просто хотите, чтобы пробелы поменяли шаблон на "[] +"
Тим Хулихан
9
Разве вам не следует использовать '{2,}' вместо '+', чтобы избежать замены одиночных пробелов?
angularsen
52

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

Есть разница между «пробелом» и «пробелом». Если вы имеете в виду только пробелы, вам следует использовать регулярное выражение " {2,}". Другое дело, если вы имеете в виду любые пробелы. Должны ли все пробелы быть преобразованы в пробелы? Что должно произойти с пространством в начале и в конце?

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

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

В тесте используется MiniBench .

using System;
using System.Text.RegularExpressions;
using MiniBench;

internal class Program
{
    public static void Main(string[] args)
    {

        int size = int.Parse(args[0]);
        int gapBetweenExtraSpaces = int.Parse(args[1]);

        char[] chars = new char[size];
        for (int i=0; i < size/2; i += 2)
        {
            // Make sure there actually *is* something to do
            chars[i*2] = (i % gapBetweenExtraSpaces == 1) ? ' ' : 'x';
            chars[i*2 + 1] = ' ';
        }
        // Just to make sure we don't have a \0 at the end
        // for odd sizes
        chars[chars.Length-1] = 'y';

        string bigString = new string(chars);
        // Assume that one form works :)
        string normalized = NormalizeWithSplitAndJoin(bigString);


        var suite = new TestSuite<string, string>("Normalize")
            .Plus(NormalizeWithSplitAndJoin)
            .Plus(NormalizeWithRegex)
            .RunTests(bigString, normalized);

        suite.Display(ResultColumns.All, suite.FindBest());
    }

    private static readonly Regex MultipleSpaces = 
        new Regex(@" {2,}", RegexOptions.Compiled);

    static string NormalizeWithRegex(string input)
    {
        return MultipleSpaces.Replace(input, " ");
    }

    // Guessing as the post doesn't specify what to use
    private static readonly char[] Whitespace =
        new char[] { ' ' };

    static string NormalizeWithSplitAndJoin(string input)
    {
        string[] split = input.Split
            (Whitespace, StringSplitOptions.RemoveEmptyEntries);
        return string.Join(" ", split);
    }
}

Несколько тестовых прогонов:

c:\Users\Jon\Test>test 1000 50
============ Normalize ============
NormalizeWithSplitAndJoin  1159091 0:30.258 22.93
NormalizeWithRegex        26378882 0:30.025  1.00

c:\Users\Jon\Test>test 1000 5
============ Normalize ============
NormalizeWithSplitAndJoin  947540 0:30.013 1.07
NormalizeWithRegex        1003862 0:29.610 1.00


c:\Users\Jon\Test>test 1000 1001
============ Normalize ============
NormalizeWithSplitAndJoin  1156299 0:29.898 21.99
NormalizeWithRegex        23243802 0:27.335  1.00

Здесь первое число - это количество итераций, второе - это затраченное время, а третье - это масштабированная оценка, где 1.0 является лучшим.

Это показывает, что по крайней мере в некоторых случаях (включая этот) регулярное выражение может превзойти решение Split / Join, иногда с очень значительным отрывом.

Однако, если вы измените требование «все пробелы», то Split / Join , похоже, победит. Как это часто бывает, дьявол кроется в деталях ...

Джон Скит
источник
1
Отличный анализ. Таким образом, похоже, что мы оба были правы в разной степени. Код в моем ответе был взят из более крупной функции, которая может нормализовать все пробелы и / или управляющие символы внутри строки и с начала и с конца.
Скотт Дорман,
1
Только с указанными вами пробельными символами в большинстве моих тестов регулярное выражение и Split / Join были примерно равны - S / J имел крошечное, крошечное преимущество за счет правильности и сложности. По этим причинам я обычно предпочитаю регулярное выражение. Не поймите меня неправильно - я далеко не фанат регулярных выражений, но мне не нравится писать более сложный код ради производительности без предварительного тестирования производительности.
Джон Скит,
NormalizeWithSplitAndJoin создаст намного больше мусора, трудно сказать, затронет ли реальная проблема больше времени GC, чем метка.
Ян Рингроуз
@IanRingrose Какой мусор можно создать?
Dronz
18

Обычный эспрессоин - самый простой способ. Если вы напишете регулярное выражение правильно, вам не понадобится несколько вызовов.

Измените его на это:

string s = System.Text.RegularExpressions.Regex.Replace(s, @"\s{2,}", " "); 
Brandon
источник
Моя единственная проблема @"\s{2,}"заключается в том, что он не может заменить отдельные вкладки и другие символы пробела Unicode пробелом. Если вы собираетесь заменить 2 табуляции пробелом, то вам, вероятно, следует заменить 1 табуляцию пробелом. @"\s+"сделаю это за вас.
Дэвид Шпехт
17

Хотя существующие ответы хороши, я хотел бы указать на один подход, который не работает:

public static string DontUseThisToCollapseSpaces(string text)
{
    while (text.IndexOf("  ") != -1)
    {
        text = text.Replace("  ", " ");
    }
    return text;
}

Это может повторяться бесконечно. Кто-нибудь хочет угадать, почему? (Я наткнулся на это только тогда, когда несколько лет назад его задали как вопрос группы новостей ... кто-то действительно столкнулся с этим как с проблемой.)

Джон Скит
источник
Думаю, я помню, как этот вопрос задавали некоторое время назад на SO. IndexOf игнорирует определенные символы, которых нет в Replace. Таким образом, двойное пространство было всегда, просто никогда не удалялось.
Брэндон,
19
Это связано с тем, что IndexOf игнорирует некоторые символы Unicode, конкретным виновником в этом случае является какой-то азиатский символ iirc. Хм, без стыковки нулевой ширины по гуглу.
ahawker
Я узнал это на собственном горьком опыте
Антонио Бакула
Я узнал на своей шкуре. Особенно с двумя не соединяемыми элементами нулевой ширины (\ u200C \ u200C). IndexOf возвращает индекс этого «двойного пробела», но Replace не заменяет его. Я думаю, это потому, что для IndexOf вам нужно указать StringComparsion (Ordinal), чтобы вести себя так же, как Replace. Таким образом, ни один из этих двоих не найдет «двойные пробелы». Подробнее о StringComparsion docs.microsoft.com/en-us/dotnet/api/…
Мартин
4

Как уже отмечалось, это легко сделать с помощью регулярного выражения. Я просто добавлю, что вы, возможно, захотите добавить к нему .trim (), чтобы избавиться от начальных / конечных пробелов.

MAK
источник
4

Вот решение, с которым я работаю. Без RegEx и String.Split.

public static string TrimWhiteSpace(this string Value)
{
    StringBuilder sbOut = new StringBuilder();
    if (!string.IsNullOrEmpty(Value))
    {
        bool IsWhiteSpace = false;
        for (int i = 0; i < Value.Length; i++)
        {
            if (char.IsWhiteSpace(Value[i])) //Comparion with WhiteSpace
            {
                if (!IsWhiteSpace) //Comparison with previous Char
                {
                    sbOut.Append(Value[i]);
                    IsWhiteSpace = true;
                }
            }
            else
            {
                IsWhiteSpace = false;
                sbOut.Append(Value[i]);
            }
        }
    }
    return sbOut.ToString();
}

так что вы можете:

string cleanedString = dirtyString.TrimWhiteSpace();
Fubo
источник
4

Быстрое средство для удаления лишних пробелов от Фелипе Мачадо. (Изменено RW для удаления нескольких пространств)

static string DuplicateWhiteSpaceRemover(string str)
{
    var len = str.Length;
    var src = str.ToCharArray();
    int dstIdx = 0;
    bool lastWasWS = false; //Added line
    for (int i = 0; i < len; i++)
    {
        var ch = src[i];
        switch (ch)
        {
            case '\u0020': //SPACE
            case '\u00A0': //NO-BREAK SPACE
            case '\u1680': //OGHAM SPACE MARK
            case '\u2000': // EN QUAD
            case '\u2001': //EM QUAD
            case '\u2002': //EN SPACE
            case '\u2003': //EM SPACE
            case '\u2004': //THREE-PER-EM SPACE
            case '\u2005': //FOUR-PER-EM SPACE
            case '\u2006': //SIX-PER-EM SPACE
            case '\u2007': //FIGURE SPACE
            case '\u2008': //PUNCTUATION SPACE
            case '\u2009': //THIN SPACE
            case '\u200A': //HAIR SPACE
            case '\u202F': //NARROW NO-BREAK SPACE
            case '\u205F': //MEDIUM MATHEMATICAL SPACE
            case '\u3000': //IDEOGRAPHIC SPACE
            case '\u2028': //LINE SEPARATOR
            case '\u2029': //PARAGRAPH SEPARATOR
            case '\u0009': //[ASCII Tab]
            case '\u000A': //[ASCII Line Feed]
            case '\u000B': //[ASCII Vertical Tab]
            case '\u000C': //[ASCII Form Feed]
            case '\u000D': //[ASCII Carriage Return]
            case '\u0085': //NEXT LINE
                if (lastWasWS == false) //Added line
                {
                    src[dstIdx++] = ' '; // Updated by Ryan
                    lastWasWS = true; //Added line
                }
                continue;
            default:
                lastWasWS = false; //Added line 
                src[dstIdx++] = ch;
                break;
        }
    }
    return new string(src, 0, dstIdx);
}

Тесты ...

|                           | Time  |   TEST 1    |   TEST 2    |   TEST 3    |   TEST 4    |   TEST 5    |
| Function Name             |(ticks)| dup. spaces | spaces+tabs | spaces+CR/LF| " " -> " "  | " " -> " " |
|---------------------------|-------|-------------|-------------|-------------|-------------|-------------|
| SwitchStmtBuildSpaceOnly  |   5.2 |    PASS     |    FAIL     |    FAIL     |    PASS     |    PASS     |
| InPlaceCharArraySpaceOnly |   5.6 |    PASS     |    FAIL     |    FAIL     |    PASS     |    PASS     |
| DuplicateWhiteSpaceRemover|   7.0 |    PASS     |    PASS     |    PASS     |    PASS     |    PASS     |
| SingleSpacedTrim          |  11.8 |    PASS     |    PASS     |    PASS     |    FAIL     |    FAIL     |
| Fubo(StringBuilder)       |    13 |    PASS     |    FAIL     |    FAIL     |    PASS     |    PASS     |
| User214147                |    19 |    PASS     |    PASS     |    PASS     |    FAIL     |    FAIL     | 
| RegExWithCompile          |    28 |    PASS     |    FAIL     |    FAIL     |    PASS     |    PASS     |
| SwitchStmtBuild           |    34 |    PASS     |    FAIL     |    FAIL     |    PASS     |    PASS     |
| SplitAndJoinOnSpace       |    55 |    PASS     |    FAIL     |    FAIL     |    FAIL     |    FAIL     |
| RegExNoCompile            |   120 |    PASS     |    PASS     |    PASS     |    PASS     |    PASS     |
| RegExBrandon              |   137 |    PASS     |    FAIL     |    PASS     |    PASS     |    PASS     |

Примечания к тестам: режим выпуска, отладчик не подключен, процессор i7, в среднем 4 прогона, протестированы только короткие строки

SwitchStmtBuildSpaceOnly, автор - Фелипе Мачадо, 2015 г. и изменен Sunsetquest.

InPlaceCharArraySpaceOnly, автор - Фелипе Мачадо, 2015 г. и изменен Sunsetquest.

SwitchStmtBuild Фелипе Мачадо 2015 и изменен Sunsetquest

SwitchStmtBuild2, автор - Фелипе Мачадо, 2015 г., модифицирован Sunsetquest.

SingleSpacedTrim от Дэвида С. 2013

Fubo (StringBuilder) от fubo 2014

SplitAndJoinOnSpace, Джон Скит, 2009 г.

RegExWithCompile, Джон Скит, 2009 г.

User214147 от пользователя user214147

RegExBrandon от Брэндона

RegExNoCompile, автор Тим Хулихан

Код теста находится на Github

Sunsetquest
источник
1
Приятно видеть здесь ссылку на мою статью! (Я Фелипе Мачадо) Я собираюсь обновить его, используя подходящий инструмент для тестирования производительности под названием BenchmarkDotNet! Я попытаюсь настроить запуски во всех средах выполнения (теперь, когда у нас есть DOT NET CORE и тому подобное ...
Loudenvier
1
@Loudenvier - Хорошая работа над этим. Ваш был самым быстрым почти на 400%! .Net Core - это бесплатный прирост производительности на 150-200%. Он приближается к производительности C ++, но намного проще кодировать. Спасибо за комментарий.
Sunsetquest
1
Это только пробелы, но не другие символы пробелов. Может, вам нужен char.IsWhiteSpace (ch) вместо src [i] == '\ u0020'. Я заметил, что это было отредактировано сообществом. Они разобрались?
Злой голубь,
3

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

    public static string NormalizeWhiteSpace(string S)
    {
        string s = S.Trim();
        bool iswhite = false;
        int iwhite;
        int sLength = s.Length;
        StringBuilder sb = new StringBuilder(sLength);
        foreach(char c in s.ToCharArray())
        {
            if(Char.IsWhiteSpace(c))
            {
                if (iswhite)
                {
                    //Continuing whitespace ignore it.
                    continue;
                }
                else
                {
                    //New WhiteSpace

                    //Replace whitespace with a single space.
                    sb.Append(" ");
                    //Set iswhite to True and any following whitespace will be ignored
                    iswhite = true;
                }  
            }
            else
            {
                sb.Append(c.ToString());
                //reset iswhitespace to false
                iswhite = false;
            }
        }
        return sb.ToString();
    }
user214147
источник
2

Используя тестовую программу, которую опубликовал Джон Скит, я попытался проверить, смогу ли я заставить рукописный цикл работать быстрее.
Я могу обыграть NormalizeWithSplitAndJoin каждый раз, но только обыграть NormalizeWithRegex с входами 1000, 5.

static string NormalizeWithLoop(string input)
{
    StringBuilder output = new StringBuilder(input.Length);

    char lastChar = '*';  // anything other then space 
    for (int i = 0; i < input.Length; i++)
    {
        char thisChar = input[i];
        if (!(lastChar == ' ' && thisChar == ' '))
            output.Append(thisChar);

        lastChar = thisChar;
    }

    return output.ToString();
}

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

Итак, Regex.Replace () очень быстрый и его сложно превзойти !!

Ян Рингроуз
источник
2

VB.NET

Linha.Split(" ").ToList().Where(Function(x) x <> " ").ToArray

C #

Linha.Split(" ").ToList().Where(x => x != " ").ToArray();

Наслаждайтесь мощью LINQ = D

Патрик Моура
источник
Именно! Для меня это тоже самый элегантный подход. Итак, для протокола, на C # это будет:string.Join(" ", myString.Split(' ').Where(s => s != " ").ToArray())
Efrain
1
Незначительное улучшение Splitдля перехвата всех пробелов и удаления Whereпредложения:myString.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)
Дэвид
1
Regex regex = new Regex(@"\W+");
string outputString = regex.Replace(inputString, " ");
Майкл Д.
источник
Это заменяет все символы, не являющиеся словами, на пробел. Таким образом, он также заменил бы такие вещи, как скобки, кавычки и т. Д., Что может быть не тем, что вам нужно.
Герман
0

Наименьшее решение:

var regExp = / \ s + / g, newString = oldString.replace (regExp, '');


источник
0

Вы можете попробовать это:

    /// <summary>
    /// Remove all extra spaces and tabs between words in the specified string!
    /// </summary>
    /// <param name="str">The specified string.</param>
    public static string RemoveExtraSpaces(string str)
    {
        str = str.Trim();
        StringBuilder sb = new StringBuilder();
        bool space = false;
        foreach (char c in str)
        {
            if (char.IsWhiteSpace(c) || c == (char)9) { space = true; }
            else { if (space) { sb.Append(' '); }; sb.Append(c); space = false; };
        }
        return sb.ToString();
    }
LL99
источник
0

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

    public static void WhiteSpaceReduce()
    {
        string t1 = "a b   c d";
        string t2 = "a b\n\nc\nd";

        Regex whiteReduce = new Regex(@"(?<firstWS>\s)(?<repeatedWS>\k<firstWS>+)");
        Console.WriteLine("{0}", t1);
        //Console.WriteLine("{0}", whiteReduce.Replace(t1, x => x.Value.Substring(0, 1))); 
        Console.WriteLine("{0}", whiteReduce.Replace(t1, @"${firstWS}"));
        Console.WriteLine("\nNext example ---------");
        Console.WriteLine("{0}", t2);
        Console.WriteLine("{0}", whiteReduce.Replace(t2, @"${firstWS}"));
        Console.WriteLine();
    }

Обратите внимание, что второй пример остается единичным, \nа в принятом ответе конец строки заменяется пробелом.

Если вам нужно заменить любую комбинацию пробелов на первую, просто удалите обратную ссылку \kиз шаблона.

Дэн
источник
0

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

Мы используем шаблон регулярного выражения как « \ s + ».

  • \ s соответствует пробелу, табуляции, новой строке, возврату каретки, подаче страницы или вертикальной табуляции.

  • «+» означает одно или несколько вхождений.

Пример регулярного выражения

String blogName = "  Sourav .  Pal.   "

 String nameWithProperSpacing = blogName.replaceAll("\\s+", " ");   
System.out.println( nameWithProperSpacing );
Сурав приятель
источник
-1

Для этого нет встроенного средства. Вы можете попробовать это:

private static readonly char[] whitespace = new char[] { ' ', '\n', '\t', '\r', '\f', '\v' };
public static string Normalize(string source)
{
   return String.Join(" ", source.Split(whitespace, StringSplitOptions.RemoveEmptyEntries));
}

Это удалит начальные и конечные пробелы, а также свернет все внутренние пробелы до одного символа пробела. Если вы действительно хотите сворачивать только пробелы, тогда решения, использующие регулярное выражение, лучше; в противном случае это решение лучше. (См. Анализ, сделанный Джоном Скитом.)

Скотт Дорман
источник
7
Если регулярное выражение скомпилировано и кэшировано, я не уверен, что это связано с большими накладными расходами, чем разделение и объединение, которые могут создать множество промежуточных строк мусора. Вы провели тщательные тесты обоих подходов, прежде чем предположить, что ваш путь быстрее?
Джон Скит,
1
пробелы здесь не объявлены
Тим Хулихан
3
Говоря о накладных расходах, почему вы звоните, source.ToCharArray()а затем выбрасываете результат?
Джон Скит,
2
И вызов ToCharArray()результата string.Join только для создания новой строки ... вау, появление этого сообщения с жалобой на накладные расходы просто замечательно. -1.
Джон Скит,
1
О, и предполагая whitespaceэто new char[] { ' ' }, это даст неправильный результат , если входная строка начинается или заканчивается с пространством.
Джон Скит,