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?



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.


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

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

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.


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

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


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

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

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

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