Jak zamienić wiele spacji na jedną spację


108

Powiedzmy, że mam ciąg taki jak:

"Hello     how are   you           doing?"

Chciałbym mieć funkcję, która zamienia wiele spacji w jedną przestrzeń.

Więc dostałbym:

"Hello how are you doing?"

Wiem, że mógłbym użyć wyrażenia regularnego lub połączenia

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

Ale musiałbym wywoływać to wiele razy, aby upewnić się, że wszystkie kolejne białe znaki są zastąpione tylko jednym.

Czy jest już wbudowana metoda?


Czy mógłbyś wyjaśnić: czy masz do czynienia tylko ze spacjami, czy „wszystkimi” białymi spacjami?
Jon Skeet

Czy chcesz, aby jakiekolwiek białe spacje nie były zamienione na spacje?
Jon Skeet

Po prostu chodziło mi o to, że wszystkie białe spacje w serii powinny mieć najwyżej 1
Matt


Dwie kwestie do rozważenia: 1. char.IsWhiteSpace obejmuje powrót karetki, wysuw wiersza itp. 2. „Białe znaki” są prawdopodobnie dokładniej testowane za pomocą Char.GetUnicodeCategory (ch) = Globalization.UnicodeCategory.SpaceSeparator
smirkingman

Odpowiedzi:


196
string cleanedString = System.Text.RegularExpressions.Regex.Replace(dirtyString,@"\s+"," ");

40
imo, unikanie
wyrażeń

8
Jeśli aplikacja nie jest krytyczna pod względem czasu, może pozwolić sobie na 1 mikrosekundę narzutu przetwarzania.
Daniel

16
Zauważ, że „\ s” nie tylko zastępuje białe spacje, ale także znaki nowego wiersza.
Bart Kiers

12
dobry chwyt, jeśli chcesz tylko spacji, zmień wzór na „[] +”
Tim Hoolihan,

9
Czy nie należy używać „{2,}” zamiast „+”, aby uniknąć zastępowania pojedynczych białych znaków?
angularsen

52

To pytanie nie jest tak proste, jak przedstawiają je inne plakaty (i tak mi się początkowo wydawało) - ponieważ nie jest do końca precyzyjne, jak powinno.

Istnieje różnica między „spacją” a „białą spacją”. Jeśli masz na myśli tylko spacje, powinieneś użyć wyrażenia regularnego od " {2,}". Jeśli masz na myśli jakąkolwiek białą spację, to inna sprawa. Powinien wszystko spacje powinny zostać zamienione na spacje? Co powinno się stać z przestrzenią na początku i na końcu?

W przypadku testu porównawczego poniżej założyłem, że obchodzą Cię tylko spacje i nie chcesz nic robić z pojedynczymi spacjami, nawet na początku i na końcu.

Zauważ, że poprawność jest prawie zawsze ważniejsza niż wydajność. Fakt, że rozwiązanie Split / Join usuwa wszelkie początkowe / końcowe spacje (nawet tylko pojedyncze spacje) jest niepoprawne, jeśli chodzi o określone wymagania (które oczywiście mogą być niekompletne).

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

Kilka testów:

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

Tutaj pierwsza liczba to liczba iteracji, druga to czas potrzebny, a trzecia to wynik skalowany, przy czym 1.0 to najlepszy.

To pokazuje, że przynajmniej w niektórych przypadkach (w tym w tym jednym) wyrażenie regularne może przewyższać rozwiązanie Split / Join, czasami z bardzo dużym marginesem.

Jednakże, jeśli zmieni się na „wszystko” wymóg spacji, a następnie Split / Dołącz nie wydaje się, aby wygrać. Jak to często bywa, diabeł tkwi w szczegółach ...


1
Świetna analiza. Wydaje się więc, że w różnym stopniu oboje mieliśmy rację. Kod w mojej odpowiedzi został zaczerpnięty z większej funkcji, która ma możliwość znormalizowania wszystkich białych znaków i / lub znaków kontrolnych z ciągu i od początku i końca.
Scott Dorman

1
Z tylko określonymi białymi znakami, które określiłeś, w większości moich testów regex i Split / Join były prawie równe - S / J miał niewielką, niewielką korzyść, kosztem poprawności i złożoności. Z tych powodów normalnie wolałbym wyrażenie regularne. Nie zrozumcie mnie źle - daleki jestem od fanboya regex, ale nie lubię pisać bardziej złożonego kodu ze względu na wydajność bez wcześniejszego testowania wydajności.
Jon Skeet

NormalizeWithSplitAndJoin stworzy o wiele więcej śmieci, trudno powiedzieć, czy prawdziwy problem zostanie uderzony dłużej w GC niż banchmark.
Ian Ringrose,

@IanRingrose Jaki rodzaj śmieci można stworzyć?
Dronz,

18

Najłatwiejszym sposobem byłoby zwykłe expressoin. Jeśli napiszesz wyrażenie regularne we właściwy sposób, nie będziesz potrzebować wielu wywołań.

Zmień to na:

string s = System.Text.RegularExpressions.Regex.Replace(s, @"\s{2,}", " "); 

Moim jedynym problemem @"\s{2,}"jest to, że nie zastępuje pojedynczych tabulatorów i innych znaków spacji Unicode spacją. Jeśli zamierzasz zastąpić 2 tabulatory spacją, prawdopodobnie powinieneś zastąpić 1 tabulator spacją. @"\s+"zrobi to za Ciebie.
David Specht,

17

Chociaż istniejące odpowiedzi są w porządku, chciałbym wskazać jedno podejście, które nie działa:

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

Może to trwać wiecznie. Czy ktoś chciałby zgadnąć, dlaczego? (Natknąłem się na to tylko wtedy, gdy kilka lat temu zadano to pytanie grupie dyskusyjnej ... ktoś faktycznie napotkał to jako problem.)


Myślę, że pamiętam to pytanie zadane jakiś czas temu w SO. IndexOf ignoruje niektóre znaki, które Replace nie. Więc podwójna przestrzeń była zawsze obecna, po prostu nigdy nie została usunięta.
Brandon

19
Dzieje się tak, ponieważ IndexOf ignoruje niektóre znaki Unicode, a konkretnym winowajcą w tym przypadku jest jakiś azjatycki znak iirc. Hmm, łącznik o zerowej szerokości według Google.
ahawker

Dowiedziałem się, że na własnej skórze
Antonio Bakula

Nauczyłem się na własnej skórze. Szczególnie z dwoma łącznikami o zerowej szerokości (\ u200C \ u200C). IndexOf zwraca indeks tej „podwójnej spacji”, ale funkcja Replace go nie zastępuje. Myślę, że dzieje się tak, ponieważ w przypadku IndexOf musisz określić StringComparsion (Ordinal), aby zachowywał się tak samo jak Replace. W ten sposób żadne z tych dwóch nie zlokalizuje „podwójnych spacji”. Więcej informacji o StringComparsion docs.microsoft.com/en-us/dotnet/api/…
Martin Brabec

4

Jak już wspomniano, można to łatwo zrobić za pomocą wyrażenia regularnego. Dodam tylko, że możesz chcieć dodać .trim () do tego, aby pozbyć się początkowych / końcowych białych znaków.


4

Oto rozwiązanie, z którym pracuję. Bez RegEx i 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();
}

więc możesz:

string cleanedString = dirtyString.TrimWhiteSpace();

4

Szybki zmywacz do dodatkowych białych znaków autorstwa Felipe Machado. (Zmodyfikowane przez RW w celu usunięcia wielu przestrzeni)

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

Wzorce ...

|                           | 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     |

Uwagi dotyczące testów porównawczych: tryb wydania, brak dołączonego debuggera, procesor i7, średnio 4 przebiegi, testowane tylko krótkie ciągi

SwitchStmtBuildSpaceOnly autorstwa Felipe Machado 2015 i zmodyfikowane przez Sunsetquest

InPlaceCharArraySpaceOnly autorstwa Felipe Machado 2015 i zmodyfikowane przez Sunsetquest

SwitchStmtBuild autorstwa Felipe Machado 2015 i zmodyfikowane przez Sunsetquest

SwitchStmtBuild2 autorstwa Felipe Machado 2015 i zmodyfikowane przez Sunsetquest

SingleSpacedTrim autorstwa Davida S 2013

Fubo (StringBuilder) przez fubo 2014

SplitAndJoinOnSpace autorstwa Jona Skeeta 2009

RegExWithCompile autorstwa Jona Skeeta 2009

User214147 użytkownika user214147

RegExBrandon firmy Brandon

RegExNoCompile autorstwa Tima Hoolihana

Kod testu porównawczego znajduje się na Github


1
Miło widzieć, że mój artykuł jest tutaj wymieniony! (Jestem Felipe Machado) Mam zamiar zaktualizować go za pomocą odpowiedniego narzędzia do testów porównawczych o nazwie BenchmarkDotNet! Spróbuję skonfigurować przebiegi we wszystkich środowiskach wykonawczych (teraz, gdy mamy DOT NET CORE i polubienia ...
Loudenvier

1
@Loudenvier - Dobra robota. Twój był najszybszy o prawie 400%! .Net Core to darmowy wzrost wydajności o 150-200%. Zbliża się do wydajności C ++, ale jest znacznie łatwiejszy do kodowania. Dziękuję za komentarz.
Sunsetquest

1
Dotyczy to tylko spacji, a nie innych białych znaków. Może wolisz char.IsWhiteSpace (ch) zamiast src [i] == '\ u0020'. Zauważyłem, że zostało to edytowane przez społeczność. Zepsuli to?
Evil Pigeon

3

Dzielę się tym, czego używam, bo wygląda na to, że wymyśliłem coś innego. Używam tego od jakiegoś czasu i jest dla mnie wystarczająco szybki. Nie jestem pewien, jak wypada na tle innych. Używam go w rozdzielaczu plików i uruchamiam przez niego duże zbiory danych po jednym polu na raz.

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

2

Korzystając z programu testowego, który opublikował Jon Skeet, próbowałem sprawdzić, czy uda mi się uzyskać ręcznie napisaną pętlę, aby działała szybciej.
Mogę pokonać NormalizeWithSplitAndJoin za każdym razem, ale pokonuję tylko NormalizeWithRegex z wejściami 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();
}

Nie patrzyłem na kod maszynowy generowany przez jitter, jednak spodziewam się, że problemem jest czas potrzebny na wywołanie metody StringBuilder.Append () i aby zrobić znacznie lepiej, konieczne byłoby użycie niebezpiecznego kodu.

Więc Regex.Replace () jest bardzo szybki i trudny do pokonania !!


2

VB.NET

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

DO#

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

Ciesz się mocą LINQ = D


Dokładnie! Dla mnie jest to również najbardziej eleganckie podejście. Tak więc dla przypomnienia, w C # to będzie:string.Join(" ", myString.Split(' ').Where(s => s != " ").ToArray())
Efrain

1
Niewielkie ulepszenie w Splitcelu wyłapania wszystkich białych znaków i usunięcia Whereklauzuli:myString.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)
David,

1
Regex regex = new Regex(@"\W+");
string outputString = regex.Replace(inputString, " ");

Spowoduje to zastąpienie wszystkich znaków niebędących słowami spacją. Więc zastąpiłby również takie rzeczy, jak nawiasy, cudzysłowy itp., Które mogą nie być tym, czego chcesz.
Herman

0

Najmniejsze rozwiązanie:

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


0

Możesz spróbować tego:

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

0

Grupy zastępcze zapewniają bardziej kompleksowe podejście do rozwiązywania zastępowania wielu znaków odstępu tym samym pojedynczym:

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

Proszę zauważyć, że drugi przykład zachowuje pojedynczą wartość, \npodczas gdy zaakceptowana odpowiedź zastąpiłaby koniec wiersza spacją.

Jeśli chcesz zamienić dowolną kombinację białych znaków na pierwszą, po prostu usuń odniesienie wsteczne \kze wzorca.


0

Dobrym rozwiązaniem jest również użycie wyrażenia regularnego, aby zastąpić 2 lub więcej białych spacji pojedynczą spacją.

Używamy wzorca regex jako „ \ s + ”.

  • \ s dopasowuje spację, tabulator, nową linię, powrót karetki, wysuw strony lub tabulator pionowy.

  • „+” oznacza jedno lub więcej wystąpień.

Przykład Regex

String blogName = "  Sourav .  Pal.   "

 String nameWithProperSpacing = blogName.replaceAll("\\s+", " ");   
System.out.println( nameWithProperSpacing );

-1

Nie ma wbudowanego sposobu, aby to zrobić. Możesz spróbować tego:

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

Spowoduje to usunięcie wiodących i końcowych białych znaków, a także zwinięcie wszelkich wewnętrznych białych znaków do pojedynczego białego znaku. Jeśli naprawdę chcesz zwinąć tylko spacje, lepsze są rozwiązania wykorzystujące wyrażenie regularne; inaczej to rozwiązanie jest lepsze. (Zobacz analizę przeprowadzoną przez Jona Skeeta).


7
Jeśli wyrażenie regularne jest kompilowane i zapisywane w pamięci podręcznej, nie jestem pewien, czy wiąże się to z większym narzutem niż dzielenie i łączenie, co może spowodować powstanie dużej ilości pośrednich ciągów śmieci. Czy wykonałeś dokładne testy porównawcze obu podejść, zanim założyłeś, że Twoja droga jest szybsza?
Jon Skeet

1
białe spacje są tutaj niezadeklarowane
Tim Hoolihan

3
A propos kosztów ogólnych, dlaczego dzwonisz, source.ToCharArray()a potem odrzucasz wynik?
Jon Skeet

2
A wywołanie ToCharArray()wyniku string.Join, tylko po to, aby utworzyć nowy ciąg ... wow, to, że jest w poście narzekającym na narzut, jest po prostu niezwykłe. -1.
Jon Skeet

1
Aha, i przy założeniu, że whitespacejest new char[] { ' ' }, to daje zły wynik, jeśli ciąg wejściowy zaczyna lub kończy się przestrzeń.
Jon Skeet
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.