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

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

我知道这种方式

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

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

【问题讨论】:

标签: c# string syntax multiline


【解决方案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"。
          猜你喜欢
          • 2011-10-18
          • 1970-01-01
          • 2017-12-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-10-18
          相关资源
          最近更新 更多