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?
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?
Odpowiedzi:
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.
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.
StringSplitOptions.RemoveEmptyEntries.
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
[\r\n]{1,2}
\n\rlub \n\njako pojedynczy podział linii, co nie jest poprawne.
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.
Możesz użyć Regex.Split:
string[] tokens = Regex.Split(input, @"\r?\n|\r");
Edycja: dodano |\rdo konta dla (starszych) terminatorów linii Mac.
\rjako zakończenie linii.
Jeśli chcesz zachować puste wiersze, po prostu usuń StringSplitOptions.
var result = input.Split(System.Environment.NewLine.ToCharArray());
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
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();
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();
}
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:
\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ę)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;
}