Najlepszy sposób na podzielenie łańcucha na linie

143

Jak podzielić ciąg wieloliniowy na linie?

Wiem w ten sposób

var result = input.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

wygląda trochę brzydko i gubi puste linie. Czy jest lepsze rozwiązanie?

Konstantin Spirin
źródło
1
Podoba mi się to rozwiązanie, nie wiem, jak to ułatwić. Drugi parametr oczywiście usuwa puste miejsca.
NappingRabbit

Odpowiedzi:

172
  • Jeśli wygląda brzydko, po prostu usuń niepotrzebne ToCharArraypołączenie.

  • Jeśli chcesz podzielić według jednego \nlub \rdrugiego, masz dwie opcje:

    • Użyj literału tablicowego - ale to da ci puste wiersze dla zakończeń linii w stylu Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Użyj wyrażenia regularnego, jak wskazał Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Jeśli chcesz zachować puste wiersze, dlaczego jawnie mówisz C #, aby je wyrzucał? ( StringSplitOptionsparametr) - użyj StringSplitOptions.Nonezamiast tego.

Konrad Rudolph
źródło
2
Usunięcie ToCharArray spowoduje, że kod będzie specyficzny dla platformy (NewLine może mieć wartość „\ n”)
Konstantin Spirin
1
@Will: na wypadek, gdybyś odnosił się do mnie zamiast do Konstantina: uważam ( zdecydowanie ), że parsowanie kodu powinno starać się działać na wszystkich platformach (tj. Powinien również czytać pliki tekstowe, które zostały zakodowane na innych platformach niż platforma wykonawcza ). Tak więc, jeśli chodzi o analizowanie, jeśli o mnie Environment.NewLinechodzi, nie ma wyjścia. W rzeczywistości ze wszystkich możliwych rozwiązań preferuję to, w którym używane są wyrażenia regularne, ponieważ tylko ono obsługuje poprawnie wszystkie platformy źródłowe.
Konrad Rudolph,
2
@ Hamish Cóż, spójrz tylko na dokumentację wyliczenia lub spójrz na oryginalne pytanie! To jest StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph
8
A co z tekstem zawierającym „\ r \ n \ r \ n”. string.Split zwróci 4 puste linie, jednak z '\ r \ n' powinno dać 2. Sytuacja pogarsza się, jeśli '\ r \ n' i '\ r' są zmieszane w jednym pliku.
nazwa użytkownika
1
@SurikovPavel Użyj wyrażenia regularnego. Jest to zdecydowanie preferowany wariant, ponieważ działa poprawnie z dowolną kombinacją zakończeń linii.
Konrad Rudolph
134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}
Jacek
źródło
12
Moim subiektywnym zdaniem to najczystsze podejście.
primo
5
Masz jakiś pomysł pod względem wydajności (w porównaniu z string.Splitlub Regex.Split)?
Uwe Keim
52

Aktualizacja: Zobacz tutaj, aby uzyskać alternatywne / asynchroniczne rozwiązanie.


Działa świetnie i jest szybsze niż Regex:

input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)

Ważne jest, aby mieć "\r\n"pierwsze miejsce w tablicy, aby było traktowane jako jeden koniec wiersza. Powyższe daje takie same wyniki, jak każde z poniższych rozwiązań Regex:

Regex.Split(input, "\r\n|\r|\n")

Regex.Split(input, "\r?\n|\r")

Tyle że Regex okazuje się być około 10 razy wolniejszy. Oto mój test:

Action<Action> measure = (Action func) => {
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++) {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)
);

measure(() =>
    Regex.Split(input, "\r\n|\r|\n")
);

measure(() =>
    Regex.Split(input, "\r?\n|\r")
);

Wynik:

00: 00: 03.8527616

00: 00: 31.8017726

00: 00: 32,5557128

a oto metoda rozszerzenia:

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        return str.Split(new[] { "\r\n", "\r", "\n" },
            removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
    }
}

Stosowanie:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines
orad
źródło
Dodaj więcej szczegółów, aby Twoja odpowiedź była bardziej przydatna dla czytelników.
Mohit Jain
Gotowe. Dodano również test porównujący jego wydajność z rozwiązaniem Regex.
orad
Nieco szybszy wzorzec z powodu mniejszego cofania się przy tej samej funkcjonalności, jeśli ktoś używa[\r\n]{1,2}
ΩmegaMan
@OmegaMan To ma inne zachowanie. Będzie pasować \n\rlub \n\njako pojedynczy podział linii, co nie jest poprawne.
orad
3
@OmegaMan Jak wygląda Hello\n\nworld\n\nprzypadek krawędzi? Jest to wyraźnie jeden wiersz z tekstem, po którym następuje pusty wiersz, po którym następuje kolejny wiersz z tekstem, po którym następuje pusty wiersz.
Brandin
36

Możesz użyć Regex.Split:

string[] tokens = Regex.Split(input, @"\r?\n|\r");

Edycja: dodano |\rdo konta dla (starszych) terminatorów linii Mac.

Bart Kiers
źródło
To nie zadziała jednak na plikach tekstowych w stylu OS X, ponieważ są one używane tylko \rjako zakończenie linii.
Konrad Rudolph
2
@Konrad Rudolph: AFAIK, '\ r' był używany na bardzo starych systemach MacOS i prawie nigdy się z nim nie ma. Ale jeśli OP musi to uwzględnić (lub jeśli się mylę), to wyrażenie regularne można oczywiście łatwo rozszerzyć, aby je uwzględnić: \ r? \ N | \ r
Bart Kiers
@Bart: Nie sądzę, że jesteś w błędzie, ale już wielokrotnie spotkałem wszystkich możliwych zakończeń linii w mojej karierze jako programista.
Konrad Rudolph
@Konrad, prawdopodobnie masz rację. Lepiej bezpiecznie niż przepraszam.
Bart Kiers
1
@ ΩmegaMan: Spowoduje to utratę pustych wierszy, np. \ N \ n.
Mike Rosoft
9

Jeśli chcesz zachować puste wiersze, po prostu usuń StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());
Jonas Elfström
źródło
2
Nowa linia może mieć wartość „\ n”, a tekst wejściowy może zawierać „\ n \ r”.
Konstantin Spirin
4

Miałem tę drugą odpowiedź, ale ta, oparta na odpowiedzi Jacka , jest znacznie szybsza, może być preferowana, ponieważ działa asynchronicznie, chociaż nieco wolniej.

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        using (var sr = new StringReader(str))
        {
            string line;
            while ((line = sr.ReadLine()) != null)
            {
                if (removeEmptyLines && String.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
                yield return line;
            }
        }
    }
}

Stosowanie:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Test:

Action<Action> measure = (Action func) =>
{
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++)
    {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
);

measure(() =>
    input.GetLines()
);

measure(() =>
    input.GetLines().ToList()
);

Wynik:

00: 00: 03.9603894

00: 00: 00.0029996

00: 00: 04.8221971

orad
źródło
Zastanawiam się, czy dzieje się tak dlatego, że w rzeczywistości nie sprawdzasz wyników modułu wyliczającego i dlatego nie jest on wykonywany. Niestety jestem zbyt leniwy, żeby to sprawdzić.
James Holwell,
Tak, faktycznie jest !! Po dodaniu .ToList () do obu wywołań rozwiązanie StringReader jest w rzeczywistości wolniejsze! Na moim komputerze jest 6,74s w porównaniu do 5,10s
JCH2k
To ma sens. Nadal wolę tę metodę, ponieważ umożliwia mi asynchroniczne pobieranie wierszy.
orad
Może powinieneś usunąć nagłówek „lepsze rozwiązanie” z drugiej odpowiedzi i zmodyfikować ten…
JCH2k,
4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
MAG TOR
źródło
2

Nieco skręcone, ale blok iteratora, aby to zrobić:

public static IEnumerable<string> Lines(this string Text)
{
    int cIndex = 0;
    int nIndex;
    while ((nIndex = Text.IndexOf(Environment.NewLine, cIndex + 1)) != -1)
    {
        int sIndex = (cIndex == 0 ? 0 : cIndex + 1);
        yield return Text.Substring(sIndex, nIndex - sIndex);
        cIndex = nIndex;
    }
    yield return Text.Substring(cIndex + 1);
}

Możesz wtedy zadzwonić:

var result = input.Lines().ToArray();
JDunkerley
źródło
1
    private string[] GetLines(string text)
    {

        List<string> lines = new List<string>();
        using (MemoryStream ms = new MemoryStream())
        {
            StreamWriter sw = new StreamWriter(ms);
            sw.Write(text);
            sw.Flush();

            ms.Position = 0;

            string line;

            using (StreamReader sr = new StreamReader(ms))
            {
                while ((line = sr.ReadLine()) != null)
                {
                    lines.Add(line);
                }
            }
            sw.Close();
        }



        return lines.ToArray();
    }
Johna Thompsona
źródło
1

Trudno jest prawidłowo obsługiwać mieszane zakończenia linii. Jak wiemy, znaki terminacji linii może być "Line Feed" (ASCII 10, \n, \x0A, \u000A), "Carriage Return" (ASCII 13, \r, \x0D, \u000D), lub niektóre ich kombinacją. Wracając do DOS, Windows używa dwuznakowej sekwencji CR-LF \u000D\u000A, więc ta kombinacja powinna emitować tylko jedną linię. Unix używa jednego \u000A, a bardzo stare komputery Mac używają jednego \u000Dznaku. Standardowy sposób traktowania dowolnych mieszanin tych znaków w pojedynczym pliku tekstowym jest następujący:

  • każdy znak CR lub LF powinien przeskakiwać do następnej linii Z WYJĄTKIEM ...
  • ... jeśli bezpośrednio po CR następuje LF ( \u000D\u000A), to te dwie razem pomijają tylko jedną linię.
  • String.Empty jest jedynym wejściem, które nie zwraca żadnych wierszy (każdy znak zawiera co najmniej jedną linię)
  • Ostatnia linia musi zostać zwrócona, nawet jeśli nie ma ani CR, ani LF.

Powyższa reguła opisuje zachowanie StringReader.ReadLine i powiązanych funkcji, a funkcja pokazana poniżej daje identyczne wyniki. Jest to wydajna funkcja łamania linii C #, która sumiennie implementuje te wytyczne, aby poprawnie obsługiwać dowolną sekwencję lub kombinację CR / LF. Wyliczone wiersze nie zawierają żadnych znaków CR / LF. Puste wiersze są zachowywane i zwracane jako String.Empty.

/// <summary>
/// Enumerates the text lines from the string.
///   ⁃ Mixed CR-LF scenarios are handled correctly
///   ⁃ String.Empty is returned for each empty line
///   ⁃ No returned string ever contains CR or LF
/// </summary>
public static IEnumerable<String> Lines(this String s)
{
    int j = 0, c, i;
    char ch;
    if ((c = s.Length) > 0)
        do
        {
            for (i = j; (ch = s[j]) != '\r' && ch != '\n' && ++j < c;)
                ;

            yield return s.Substring(i, j - i);
        }
        while (++j < c && (ch != '\r' || s[j] != '\n' || ++j < c));
}

Uwaga: Jeśli nie przeszkadza Ci obciążenie związane z tworzeniem StringReaderwystąpienia przy każdym wywołaniu, możesz zamiast tego użyć następującego kodu C # 7 . Jak zauważono, chociaż powyższy przykład może być nieco bardziej wydajny, obie te funkcje dają dokładnie takie same wyniki.

public static IEnumerable<String> Lines(this String s)
{
    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;
}
Glenn Slayden
źródło