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

Posted

技术标签:

【中文标题】将字符串拆分为行的最佳方法【英文标题】:Best way to split string into lines 【发布时间】:2010-12-03 06:04:03 【问题描述】:

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

我知道这种方式

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

看起来有点难看,并且丢失了空行。有没有更好的解决方案?

【问题讨论】:

Easiest way to split a string on newlines in .NET?的可能重复 【参考方案1】:

将一个字符串分成几行而不进行任何分配。

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&lt;&gt;【参考方案2】:
string[] lines = input.Split(new[]  '\r', '\n' , StringSplitOptions.RemoveEmptyEntries);

【讨论】:

【参考方案3】:

正确处理混合行结尾很棘手。众所周知,换行符可以是“换行符”(ASCII 10、\n\x0A\u000A)、“回车”(ASCII 13、\r\x0D\u000D ),或它们的某种组合。回到 DOS,Windows 使用两个字符序列 CR-LF \u000D\u000A,所以这个组合应该只发出一行。 Unix 使用单个 \u000A,而非常旧的 Mac 使用单个 \u000D 字符。在单个文本文件中处理这些字符的任意混合的标准方法如下:

每个 CR 或 LF 字符都应该跳到下一行除了... ...如果 CR 后紧跟 LF (\u000D\u000A),那么这两个一起只跳过一行。 String.Empty 是唯一不返回任何行的输入(任何字符都需要至少一行) 最后一行必须返回,即使它既没有 CR 也没有 LF。

上述规则描述了StringReader.ReadLine 和相关函数的行为,下面显示的函数产生相同的结果。这是一个高效的 C# 换行函数,它忠实地执行这些准则以正确处理任意序列或 CR/LF 组合。枚举的行不包含任何 CR/LF 字符。空行被保留并返回为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));

注意:如果您不介意在每次调用时创建 StringReader 实例的开销,您可以改用以下 C# 7 代码。如前所述,虽然上面的示例可能更有效,但这两个函数产生完全相同的结果。

public static IEnumerable<String> Lines(this String s)

    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;

【讨论】:

【参考方案4】:

更新:请参阅 here 了解替代/异步解决方案。


这很好用,而且比 Regex 更快:

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

"\r\n" 放在数组的第一个位置很重要,以便将其视为一个换行符。以上给出的结果与这些正则表达式解决方案中的任何一个相同:

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

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

除了 Regex 原来慢了大约 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

【讨论】:

请添加更多详细信息,以使您的答案对读者更有用。 完成。还添加了一个测试来比较其与正则表达式解决方案的性能。 如果使用[\r\n]1,2,由于相同功能的回溯更少,所以模式会更快 @OmegaMan 这有一些不同的行为。它将匹配 \n\r\n\n 作为不正确的单个换行符。 @OmegaMan Hello\n\nworld\n\n 是一个边缘案例吗?很明显是一行文字,后面是空行,再后面是文字,后面是空行。【参考方案5】:

我有这个 other answer,但是这个基于 Jack 的 answer,明显更快可能是首选,因为它异步工作,虽然速度稍慢。

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

输出:

00:00:03.9603894

00:00:00.0029996

00:00:04.8221971

【讨论】:

我想知道这是否是因为您实际上并没有检查枚举器的结果,因此它没有被执行。可惜我懒得查了。 是的,确实如此!!当您将 .ToList() 添加到两个调用时,StringReader 解决方案实际上更慢!在我的机器上是 6.74s 与 5.10s 这是有道理的。我仍然更喜欢这种方法,因为它可以让我异步获取行。 也许您应该删除其他答案上的“更好的解决方案”标题并编辑这个...【参考方案6】:

如果看起来很难看,只需删除不必要的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

【讨论】:

删除 ToCharArray 将使代码特定于平台(NewLine 可以是 '\n') @Will:如果您指的是我而不是 Konstantin:我相信(强烈)解析代码应该努力在所有平台上工作(即还应该读取在不同平台上编码的文本文件而不是执行平台)。所以对于解析,就我而言,Environment.NewLine 是不可行的。事实上,在所有可能的解决方案中,我更喜欢使用正则表达式的解决方案,因为只有这样才能正确处理所有源平台。 @Hamish 好吧,只需查看枚举的文档,或查看原始问题!这是StringSplitOptions.RemoveEmptyEntries 包含'\r\n\r\n'的文本怎么样。 string.Split 将返回 4 个空行,但是使用 '\r\n' 它应该给出 2 个。如果 '\r\n' 和 '\r' 混合在一个文件中,情况会变得更糟。 @SurikovPavel 使用正则表达式。这绝对是首选变体,因为它适用于任何行尾组合。【参考方案7】:

你可以使用 Regex.Split:

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

编辑:添加 |\r 以说明(较旧的)Mac 行终止符。

【讨论】:

但这不适用于 OS X 样式的文本文件,因为这些文件仅使用 \r 作为行尾。 @Konrad Rudolph:AFAIK,'\r' 曾在非常古老的 MacOS 系统上使用,几乎再也没有遇到过。但是,如果 OP 需要考虑它(或者如果我弄错了),那么当然可以很容易地扩展正则表达式来解释它:\r?\n|\r @Bart:我不认为你弄错了,但我在程序员的职业生涯中反复遇到过所有可能的行尾。 @Konrad,你可能是对的。我猜,安全总比抱歉好。 @ΩmegaMan:那会丢失空行,例如\n\n.【参考方案8】:
using (StringReader sr = new StringReader(text)) 
    string line;
    while ((line = sr.ReadLine()) != null) 
        // do something
    

【讨论】:

根据我的主观意见,这是最干净的方法。 在性能方面有什么想法(与string.SplitRegex.Split 相比)?【参考方案9】:
    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();
    

【讨论】:

【参考方案10】:

略微扭曲,但需要一个迭代器块:

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

【讨论】:

【参考方案11】:

如果您想保留空行,只需删除 StringSplitOptions。

var result = input.Split(System.Environment.NewLine.ToCharArray());

【讨论】:

NewLine 可以是 '\n' 并且输入文本可以包含 "\n\r"。

以上是关于将字符串拆分为行的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

将字符串拆分为行

将字符串拆分为行 Oracle SQL

如何在不破坏单词的情况下将字符串拆分为行?

如何将大文本文件拆分为行数相等的小文件?

如何在骆驼中将文件拆分为行但以不同方式处理第一行

BigQuery 将“字节”列拆分为行