将字符串拆分为多行的最佳方法

188

如何将多行字符串拆分成行?

我知道下面这种方法。

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

看起来有点丑,并且会删除空行。有更好的解决方案吗?


可能是Easiest way to split a string on newlines in .NET?的重复问题。 - Robin Bennett
是的,您需要使用文件中存在的确切行分隔符,例如仅使用“\r\n”或仅使用“\n”,而不是同时使用“\r”或“\n”,从而在Windows创建的文件中产生大量空白行。顺便问一下,什么系统使用LFCR行结尾? - Caius Jard
@CaiusJard LFCR 在 RISC OS 中使用... 它曾在 70 年代末和 80 年代初的一些早期微型计算机中使用,但现在似乎不再相关。 - Loudenvier
12个回答

225
  • 如果它看起来很丑,只需删除不必要的ToCharArray调用。

  • 如果想按\n\r分割,有两个选项:

    • 使用数组字面量——但这会给你Windows风格的行结尾\r\n留下空行:

      var result = text.Split(new [] { '\r', '\n' });
      
      使用正则表达式,如Bart所示:
    • var result = Regex.Split(text, "\r\n|\r|\n");
      
    • 如果您想保留空行,为什么要明确告诉C#将它们丢弃?(StringSplitOptions参数)- 使用StringSplitOptions.None代替。


2
删除ToCharArray将使代码特定于平台(NewLine可以是'\n') - Konstantin Spirin
1
@Will:如果你是在指我而不是Konstantin的话,我强烈认为解析代码应该努力适用于所有平台(即它也应该读取在执行平台上编码不同的文本文件)。因此,在解析中,Environment.NewLine对我来说是不可行的。事实上,对于所有可能的解决方案,我更喜欢使用正则表达式,因为只有这种方法可以正确处理所有源平台。 - Konrad Rudolph
3
好的,原文的意思是让你查看枚举类型的文档或者查看原问题中提到的内容。具体来说,枚举类型是 StringSplitOptions,并且使用了枚举常量 RemoveEmptyEntries。你需要保持原文意思的基础上进行翻译,尽可能地让翻译更加易懂。 - Konrad Rudolph
9
包含 '\r\n\r\n' 的文本,用 string.Split 会返回4个空行,但如果是 '\r\n',则应该返回2个。如果一个文件中混合了 '\r\n' 和 '\r',情况会更糟。 - username
2
@SurikovPavel 使用正则表达式。这绝对是首选的变体,因为它可以正确地处理任何行结尾的组合。 - Konrad Rudolph
显示剩余21条评论

162
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}

14
在我主观看来,这是最干净的方法。 - primo
6
相对于 string.SplitRegex.Split,有没有任何关于性能方面的想法? - Uwe Keim
我非常喜欢这个解决方案,但我发现一个小问题:当最后一行为空时,它会被忽略(只有最后一行)。因此,"example""example\r\n"将只产生一行,而"example\r\n\r\n"将产生两行。这种行为在这里讨论:https://github.com/dotnet/runtime/issues/27715 - Alielson Piffer

81

更新:请查看这里,了解另一种/异步解决方案。


这段代码效果很好,比正则表达式更快:

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

重要的是在数组中首先添加"\r\n"以使其被作为一行换行符。以上代码给出了与以下任何一个正则表达式解决方案相同的结果:
Regex.Split(input, "\r\n|\r|\n")

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

除了正则表达式,它的速度大约慢了10倍。这是我的测试结果:

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

输出:

00:00:03.8527616

00:00:31.8017726

00:00:32.5557128

这里是扩展方法:

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

用法:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

请添加更多细节,使您的答案对读者更有用。 - Mohit Jain
已完成。还添加了一个测试来比较它与正则表达式解决方案的性能。 - orad
如果使用[\r\n]{1,2},由于减少了回溯,模式会变得更快一些,但功能仍然相同。 - ΩmegaMan
@OmegaMan 这有一些不同的行为。它将\n\r\n\n匹配为单个换行符,这是不正确的。 - orad
@orad 我不想和你争论,但是如果数据中有多个换行符...那么很可能数据出了问题;我们可以称之为边缘情况。 - ΩmegaMan
3
“Hello\n\nworld\n\n”怎么成为边缘情况了?它很明显是一行有文字,接着是一个空行,再接着是另一行有文字,最后又是一个空行。 - Brandin

37
您可以使用Regex.Split:
string[] tokens = Regex.Split(input, @"\r?\n|\r");

编辑:添加 |\r以考虑(旧版)Mac的行终止符。


但是,这在OS X风格的文本文件上不起作用,因为它们仅使用\r作为行结尾。 - Konrad Rudolph
2
据我所知,'\r' 仅在非常旧的 MacOS 系统上使用,现在几乎不再遇到了。但是如果 OP 需要考虑它(或者我错了),那么正则表达式当然可以轻松扩展以考虑它:\r?\n|\r - Bart Kiers
@Bart:我不认为你错了,但作为一名程序员,我已经反复遇到了所有可能的行尾符号。 - Konrad Rudolph
@Konrad,你可能是对的。我想还是安全第一吧。 - Bart Kiers
使用[\r\n]{1,2}可以减少回溯并保持相同的功能。 - ΩmegaMan
1
@ΩmegaMan:这样会丢失空行,例如 \n\n。 - Mike Rosoft

11
如果你想保留空行,只需移除StringSplitOptions即可。
var result = input.Split(System.Environment.NewLine.ToCharArray());

2
NewLine 可以是 '\n',输入文本可能包含 "\n\r"。 - Konstantin Spirin

7
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

5

我之前有这个其他答案,但是基于Jack的答案虽然略慢一些但它可以异步工作,因此可能更受欢迎。

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

用法:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

测试:

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

Output:

00:00:03.9603894

00:00:00.0029996

00:00:04.8221971


2
我确实想知道这是不是因为你没有实际检查枚举器的结果,因此它没有被执行。不幸的是,我太懒了,不想去检查。 - James Holwell
是的,实际上就是这样!当你在两个调用中都添加 .ToList() 时,StringReader 解决方案实际上更慢!在我的机器上,它是6.74秒对5.10秒。 - JCH2k
这很有道理。我仍然更喜欢这种方法,因为它让我可以异步获取行。 - orad
也许你应该删除另一个回答中的“更好的解决方案”标题,然后编辑这个回答... - JCH2k

2
将一个字符串分割成行,而不进行任何分配。
public static LineEnumerator GetLines(this string text) {
    return new LineEnumerator( text.AsSpan() );
}

internal ref struct LineEnumerator {

    private ReadOnlySpan<char> Text { get; set; }
    public ReadOnlySpan<char> Current { get; private set; }

    public LineEnumerator(ReadOnlySpan<char> text) {
        Text = text;
        Current = default;
    }

    public LineEnumerator GetEnumerator() {
        return this;
    }

    public bool MoveNext() {
        if (Text.IsEmpty) return false;

        var index = Text.IndexOf( '\n' ); // \r\n or \n
        if (index != -1) {
            Current = Text.Slice( 0, index + 1 );
            Text = Text.Slice( index + 1 );
            return true;
        } else {
            Current = Text;
            Text = ReadOnlySpan<char>.Empty;
            return true;
        }
    }


}

有趣!它应该实现 IEnumerable<> 吗? - Konstantin Spirin

2

虽然晚了一步,但我一直在使用一个简单的扩展方法集来实现这个,它利用了TextReader.ReadLine()

public static class StringReadLinesExtension
{
    public static IEnumerable<string> GetLines(this string text) => GetLines(new StringReader(text));
    public static IEnumerable<string> GetLines(this Stream stm) => GetLines(new StreamReader(stm));
    public static IEnumerable<string> GetLines(this TextReader reader) {
        string line;
        while ((line = reader.ReadLine()) != null)
            yield return line;
        reader.Dispose();
        yield break;
    }
}

使用这段代码非常简单:

// If you have the text as a string...
var text = "Line 1\r\nLine 2\r\nLine 3";
foreach (var line in text.GetLines())
    Console.WriteLine(line);
// You can also use streams like
var fileStm = File.OpenRead("c:\tests\file.txt");
foreach(var line in fileStm.GetLines())
    Console.WriteLine(line);

希望这能帮助到某些人。

2

略微有些复杂,但可以使用迭代器块来实现:

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

然后您可以调用:

var result = input.Lines().ToArray();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接