Der beste Weg, um einen String in Zeilen aufzuteilen

142

Wie teilt man mehrzeilige Zeichenfolgen in Zeilen auf?

Ich weiß es so

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

sieht ein bisschen hässlich aus und verliert leere Zeilen. Gibt es eine bessere Lösung?

Konstantin Spirin
quelle
1
Ich mag diese Lösung, ich weiß nicht, wie ich es einfacher machen soll. Der zweite Parameter entfernt natürlich Leergut.
NappingRabbit

Antworten:

172
  • Wenn es hässlich aussieht, entfernen Sie einfach den unnötigen ToCharArrayAnruf.

  • Wenn Sie entweder \noder teilen möchten \r, haben Sie zwei Möglichkeiten:

    • Verwenden Sie ein Array-Literal. Dadurch erhalten Sie jedoch leere Zeilen für Zeilenenden im Windows-Stil \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Verwenden Sie einen regulären Ausdruck, wie von Bart angegeben:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Wenn Sie leere Zeilen beibehalten möchten, warum weisen Sie C # ausdrücklich an, sie wegzuwerfen? ( StringSplitOptionsParameter) - StringSplitOptions.Nonestattdessen verwenden.

Konrad Rudolph
quelle
2
Durch das Entfernen von ToCharArray wird der Code plattformspezifisch (NewLine kann '\ n' sein)
Konstantin Spirin
1
@Will: auf gut Glück , dass Sie anstelle von Konstantin mir bezogen sich : I (glauben stark ) , dass Code Parsen auf allen Plattformen zur Arbeit anstreben sollten (dh es sollte auch Textdateien lesen , die auf codiert wurden verschiedene Plattformen als die Ausführung Plattform ). Für das Parsen Environment.NewLineist es für mich ein No-Go. Tatsächlich bevorzuge ich von allen möglichen Lösungen die mit regulären Ausdrücken, da nur diese alle Quellplattformen korrekt handhabt.
Konrad Rudolph
2
@ Hamish Nun, schauen Sie sich einfach die Dokumentation der Aufzählung an oder schauen Sie in die ursprüngliche Frage! Es ist StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph
8
Wie wäre es mit dem Text, der '\ r \ n \ r \ n' enthält. string.Split gibt 4 leere Zeilen zurück, sollte jedoch mit '\ r \ n' 2 ergeben. Es wird schlimmer, wenn '\ r \ n' und '\ r' in einer Datei gemischt werden.
Benutzername
1
@SurikovPavel Verwenden Sie den regulären Ausdruck. Dies ist definitiv die bevorzugte Variante, da sie mit jeder Kombination von Zeilenenden korrekt funktioniert.
Konrad Rudolph
134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}
Jack
quelle
12
Dies ist meiner subjektiven Meinung nach der sauberste Ansatz.
Primo
5
Irgendeine Idee in Bezug auf die Leistung (im Vergleich zu string.Splitoder Regex.Split)?
Uwe Keim
51

Update: Hier finden Sie eine alternative / asynchrone Lösung.


Dies funktioniert hervorragend und ist schneller als Regex:

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

Es ist wichtig, "\r\n"zuerst im Array zu haben, damit es als ein Zeilenumbruch genommen wird. Das Obige ergibt die gleichen Ergebnisse wie jede dieser Regex-Lösungen:

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

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

Nur dass Regex ungefähr zehnmal langsamer ist. Hier ist mein 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")
);

Ausgabe:

00: 00: 03.8527616

00: 00: 31.8017726

00: 00: 32.5557128

und hier ist die Erweiterungsmethode:

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

Verwendung:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines
orad
quelle
Bitte fügen Sie einige weitere Details hinzu, um Ihre Antwort für die Leser nützlicher zu machen.
Mohit Jain
Getan. Außerdem wurde ein Test hinzugefügt, um die Leistung mit der Regex-Lösung zu vergleichen.
Orad
Etwas schnelleres Muster aufgrund weniger Backtracking mit der gleichen Funktionalität, wenn man verwendet[\r\n]{1,2}
ΩmegaMan
@OmegaMan Das hat ein anderes Verhalten. Es wird übereinstimmen \n\roder \n\nals einzelner Zeilenumbruch, was nicht korrekt ist.
Orad
3
@OmegaMan Wie ist Hello\n\nworld\n\nein Edge Case? Es ist eindeutig eine Zeile mit Text, gefolgt von einer leeren Zeile, gefolgt von einer weiteren Zeile mit Text, gefolgt von einer leeren Zeile.
Brandin
36

Sie könnten Regex.Split verwenden:

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

Bearbeiten: hinzugefügt |\r, um (ältere) Mac-Leitungsabschlüsse zu berücksichtigen.

Bart Kiers
quelle
Dies funktioniert jedoch nicht bei Textdateien im OS X-Stil, da diese nur \rals Zeilenende verwendet werden.
Konrad Rudolph
2
@Konrad Rudolph: AFAIK, '\ r' wurde auf sehr alten MacOS-Systemen verwendet und ist fast nie mehr anzutreffen. Aber wenn das OP dies berücksichtigen muss (oder wenn ich mich irre), kann der reguläre Ausdruck natürlich leicht erweitert werden, um dies zu berücksichtigen: \ r? \ N | \ r
Bart Kiers
@Bart: Ich glaube nicht, dass Sie sich irren, aber ich bin in meiner Karriere als Programmierer wiederholt auf alle möglichen Zeilenenden gestoßen.
Konrad Rudolph
@Konrad, du hast wahrscheinlich recht. Besser sicher als leid, denke ich.
Bart Kiers
1
@ ΩmegaMan: Das verliert leere Zeilen, zB \ n \ n.
Mike Rosoft
9

Wenn Sie leere Zeilen behalten möchten, entfernen Sie einfach die StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());
Jonas Elfström
quelle
2
NewLine kann '\ n' sein und der eingegebene Text kann "\ n \ r" enthalten.
Konstantin Spirin
4

Ich hatte diese andere Antwort, aber diese, basierend auf Jacks Antwort , ist deutlich schneller und wird möglicherweise bevorzugt, da sie asynchron arbeitet, obwohl sie etwas langsamer ist.

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

Verwendung:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Prüfung:

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

Ausgabe:

00: 00: 03.9603894

00: 00: 00.0029996

00: 00: 04.8221971

orad
quelle
Ich frage mich, ob dies daran liegt, dass Sie die Ergebnisse des Enumerators nicht tatsächlich überprüfen und er daher nicht ausgeführt wird. Leider bin ich zu faul, um das zu überprüfen.
James Holwell
Ja, das ist es tatsächlich !! Wenn Sie beiden Aufrufen .ToList () hinzufügen, ist die StringReader-Lösung tatsächlich langsamer! Auf meinem Computer ist es 6.74s vs. 5.10s
JCH2k
Das macht Sinn. Ich bevorzuge diese Methode immer noch, weil ich damit Linien asynchron abrufen kann.
Orad
Vielleicht sollten Sie den Header "Bessere Lösung" in Ihrer anderen Antwort entfernen und diese bearbeiten ...
JCH2k
4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
MAG TOR
quelle
2

Leicht verdreht, aber ein Iteratorblock dafür:

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

Sie können dann anrufen:

var result = input.Lines().ToArray();
JDunkerley
quelle
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();
    }
John Thompson
quelle
1

Es ist schwierig, gemischte Zeilenenden richtig zu handhaben . Wie wir wissen, können die Leitungsabschluss Zeichen "Line Feed" (ASCII 10, \n, \x0A, \u000A), "Carriage Return" (ASCII 13 \r, \x0D, \u000D) oder eine Kombination von ihnen. Zurück zu DOS verwendet Windows die zweistellige Sequenz CR-LF \u000D\u000A, daher sollte diese Kombination nur eine einzige Zeile ausgeben. Unix verwendet ein einzelnes \u000Aund sehr alte Macs verwenden ein einzelnes \u000DZeichen. Die Standardmethode zum Behandeln beliebiger Mischungen dieser Zeichen in einer einzelnen Textdatei lautet wie folgt:

  • Jedes CR- oder LF-Zeichen sollte zur nächsten Zeile springen , AUSSER ...
  • ... wenn auf eine CR unmittelbar LF ( \u000D\u000A) folgt, überspringen diese beiden zusammen nur eine Zeile.
  • String.Empty ist die einzige Eingabe, die keine Zeilen zurückgibt (jedes Zeichen enthält mindestens eine Zeile)
  • Die letzte Zeile muss zurückgegeben werden, auch wenn sie weder CR noch LF enthält.

Die vorstehende Regel beschreibt das Verhalten von StringReader.ReadLine und verwandten Funktionen. Die unten gezeigte Funktion führt zu identischen Ergebnissen. Es ist eine effiziente C # -Linienunterbrechungsfunktion, die diese Richtlinien pflichtbewusst umsetzt, um jede beliebige Sequenz oder Kombination von CR / LF korrekt zu handhaben. Die aufgezählten Zeilen enthalten keine CR / LF-Zeichen. Leere Zeilen bleiben erhalten und werden als zurückgegeben 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));
}

Hinweis: Wenn Ihnen der Aufwand beim Erstellen einer StringReaderInstanz bei jedem Aufruf nichts ausmacht , können Sie stattdessen den folgenden C # 7- Code verwenden. Wie bereits erwähnt, ist das obige Beispiel zwar etwas effizienter, beide Funktionen führen jedoch zu genau denselben Ergebnissen.

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
quelle