高效地从多行字符串中删除所有空白行

31

在C#中,最好的方法是什么可以从字符串中删除空行,即只包含空格的行?如果这是最佳解决方案,我很乐意使用正则表达式。

编辑:我应该补充一点,我正在使用.NET 2.0。


悬赏更新:颁发悬赏之后我会撤销这个帖子,但我想澄清一些事情。

首先,任何Perl 5兼容的正则表达式都可以工作。这不仅限于.NET开发人员。标题和标记已经被修改以反映这一点。

其次,在悬赏细节中我提供了一个快速的示例,但这并不是您必须满足的唯一测试。您的解决方案必须删除所有只由空格组成的行,以及最后的换行符。如果经过您的正则表达式处理后得到的字符串以“/r/n”或任何空格字符结尾,则失败。


正则表达式是快速且简单的。当你说“最好的方式”时,你想要优化哪个方面?可读性?时间?内存使用? - Michael Petito
在这种情况下,我认为可读性是最重要的。 - FunLovinCoder
7
易读性很少等同于正则表达式。 - Nick Gotch
同意它们可能会变得相当复杂,但我认为像Chris Schmich写的这篇文章就很好。 - FunLovinCoder
19个回答

22

如果你想删除包含任何空白字符(制表符、空格)的行,请尝试:

string fix = Regex.Replace(original, @"^\s*$\n", string.Empty, RegexOptions.Multiline);

编辑(针对 @Will):去除结尾的换行符最简单的解决方案是在结果字符串上使用 TrimEnd,例如:

string fix =
    Regex.Replace(original, @"^\s*$\n", string.Empty, RegexOptions.Multiline)
         .TrimEnd();

1
我认为使用\s+而不是\s*会更好。 - Salman A
1
这个代码可行,但是可能会在结尾留下一个不必要的换行符。 - user1228
@Will:请看我的更新答案,或者你是在寻找纯正则表达式的解决方案? - Chris Schmich
2
@ChrisSchmich:是的,纯粹使用正则表达式。当你在内存中有几个几百兆的字符串时,你不想创建仅因“/r/n”而不同的新实例。如果我可以一次完成它,我就可以在内存压力上稍微放松一下。 - user1228
@Yuki:在给出反对票之前,请提供比“一点也不好”更好的理由。另外,请重新阅读问题。它是关于从任意字符串中删除空行,而不仅仅是从序列化的JSON对象中删除空行。你的答案没有解决实际发布的问题。 - Chris Schmich
显示剩余7条评论

18
string outputString;
using (StringReader reader = new StringReader(originalString)
using (StringWriter writer = new StringWriter())
{
    string line;
    while((line = reader.ReadLine()) != null)
    {
        if (line.Trim().Length > 0)
            writer.WriteLine(line);
    }
    outputString = writer.ToString();
}

+1 这个很好,因为它可以很好地适用于大字符串。 - Fredrik Mörk
2
这句话应该这样写才对:if (line.Trim().Length > 0) writer.WriteLine(line)。 OP 没有请求在输出字符串中修剪所有行。 - Dan Tao

14

凭记忆说……

string fixed = Regex.Replace(input, "\s*(\n)","$1");

将此转换为:

fdasdf
asdf
asdf

什么?!对优雅的正则表达式没有爱吗?我很失望。 - Sky Sanders
有几种不同的方法可以编写这个正则表达式,但我认为正则表达式的方法最易读。 - Michael Petito
1
+1。确实优雅。它还会从非空行的末尾删除制表符和空格,但这可能是一件好事。您不需要“Multiline”选项。 - Alan Moore
@Alan - 你说得对。它满足要求的快速演奏。感谢提醒。 - Sky Sanders
3
如果文本以几行空白开头,那么这个实际上会失败 "\r\n\r\nfailure!" - user1228

8

使用LINQ:

var result = string.Join("\r\n",
                 multilineString.Split(new string[] { "\r\n" }, ...None)
                                .Where(s => !string.IsNullOrWhitespace(s)));

如果您处理的是大量输入和/或不一致的行尾,应该使用StringReader,并使用foreach循环来执行上述旧式方法。

2
啊,确实它藏在那里。如果你不使用.NET 4.0,那么你需要一个.ToArray()。在我看来,这比正则表达式难以阅读,而且我不确定你在这种方法中真正获得了什么。 - Michael Petito
4
LINQ 何时成为新的正则表达式? - Dinah
5
最近我使用了Linq来除霜我的冰箱。使用Linq这么酷的工具,为什么还要用老方法呢? - Ash
1
为什么不使用Environment.NewLine,而在RemoveEmptyEntries可以做同样的事情时还要费心使用linq呢? - user1228
1
@Will:Environment.NewLine 根据平台不同而具有不同的值,如果输入字符串包含 \r\n 换行符,则可能会产生不良影响。RemoveEmptyEntries 仅删除空条目,但不删除由一个或多个空格字符组成的条目。 - dtb
显示剩余8条评论

4

好的,根据悬赏所指定的明确要求,以下是答案:

我还需要删除任何末尾的换行符,但是我的正则表达式不行。如果有人能给我一个可以通过这个测试的正则表达式,我的悬赏就归他了:StripWhitespace("test\r\n \r\nthis\r\n\r\n") == "test\r\nthis"

所以这里是答案:

(?<=\r?\n)(\s*$\r?\n)+|(?<=\r?\n)(\r?\n)+|(\r?\n)+\z

或者在由@Chris Schmich提供的C#代码中:

string fix = Regex.Replace("test\r\n \r\nthis\r\n\r\n", @"(?<=\r?\n)(\s*$\r?\n)+|(?<=\r?\n)(\r?\n)+|(\r?\n)+\z", string.Empty, RegexOptions.Multiline);

现在让我们试着理解它。这里有三种可选模式,我愿意用 string.empty 来替换。
  1. (?<=\r?\n)(\s*$\r?\n)+ - 匹配包含仅空格的一行或多行,前面有一个换行符(但不匹配第一个前置换行符)。
  2. (?<=\r?\n)(\r?\n)+ - 匹配包含零或多个没有内容的空行,并以一个换行符为前缀(但不匹配第一个前置换行符)。
  3. (\r?\n)+\z - 匹配测试字符串末尾的一个或多个换行符(您称之为尾随换行符)
这满足了你的测试要求!同时,它也适用于 \r\n\n 两种换行格式!试一下吧!我相信这将是最正确的答案,尽管更简单的表达式可以通过你指定的奖励测试,但这个正则表达式可以通过更复杂的条件。
编辑:@Will 指出上述正则表达式的最后一个模式匹配可能存在潜在缺陷,即它无法匹配测试字符串末尾包含空格的多个换行符。所以让我们把最后一个模式改成这样: \b\s+\z 中的 \b 是单词边界(单词的开始或结束),\s+ 是一个或多个空格字符,\z 是测试字符串(“文件”)的结尾。因此,它现在将匹配包括制表符和空格在内的文件末尾的任何组合空格。我测试了 @Will 提供的两个测试用例。
所以现在一切都应该是这样的:
(?<=\r?\n)(\s*$\r?\n)+|(?<=\r?\n)(\r?\n)+|\b\s+\z
编辑 #2: 好的,@Wil 找到了最后一个正则表达式没有覆盖的可能情况。这种情况是指在任何内容之前文件开头有换行符的输入。因此,让我们添加一种模式来匹配文件的开头。

\A\s+ - \A 匹配文件的开头,\s+ 匹配一个或多个空格字符。

现在我们有:

\A\s+|(?<=\r?\n)(\s*$\r?\n)+|(?<=\r?\n)(\r?\n)+|\b\s+\z

现在我们有四个匹配模式:

  1. 文件开头的空格
  2. 包含空格的多余换行符(例如:\r\n \r\n\t\r\n
  3. 没有内容的多余换行符(例如:\r\n\r\n
  4. 文件末尾的空格

@这应该可以通过单个Regex.Replace满足您的要求。 - BenSwayne
哎呀,这看起来像是很多工作,但当字符串末尾存在混合换行符和空格时,它也会失败。例如,这个字符串 "one\r\n \r\ntwo\r\n\t\r\n \r\n" 在替换后将变成 "one\r\ntwo\r\n" - user1228
@Will,这对我在C#/.Net2中的测试是有效的。你在什么环境下运行?.Net和Perl之间的正则表达式存在一些细微的差异等等...我可以进行一些微调。 - BenSwayne
在最后的测试中。不幸的是,你最新的正则表达式有问题。我已经上传了一个简单的应用程序,我正在使用它来验证和运行性能测试,如果你想再试一次的话。具体来说,它在"\r\ntest2"上失败,返回""\r\ntest2"" - user1228
我的错。在添加悬赏评论之前,我应该先发布解决方案。在这种情况下,我太具体了。我应该把它留下来,作为“删除所有空白行 最后的换行符”。你的编辑效果很好,但它会剥离最后一行结尾的 所有 空格,而不仅仅是换行符。 - user1228
显示剩余4条评论

3

不太好。我会使用JSON.net来处理:

var o = JsonConvert.DeserializeObject(prettyJson);
new minifiedJson = JsonConvert.SerializeObject(o, Formatting.None);

2
作为对威尔的悬赏要求的回应,期望解决方案接受"test\r\n \r\nthis\r\n\r\n"并输出"test\r\nthis",我想到了一个解决方案,利用了原子组(也称为非回溯子表达式在MSDN上)。建议阅读这些文章以更好地理解发生了什么。最终,原子组帮助匹配了被遗留下来的尾随换行符。

使用此模式和RegexOptions.Multiline

^\s+(?!\B)|\s*(?>[\r\n]+)$

这里有一个示例,包括一些测试用例,其中包括我从威尔在其他帖子中的评论中收集的一些以及我自己的。

string[] inputs = 
{
    "one\r\n \r\ntwo\r\n\t\r\n \r\n",
    "test\r\n \r\nthis\r\n\r\n",
    "\r\n\r\ntest!",
    "\r\ntest\r\n ! test",
    "\r\ntest \r\n ! "
};
string[] outputs = 
{
    "one\r\ntwo",
    "test\r\nthis",
    "test!",
    "test\r\n ! test",
    "test \r\n ! "
};

string pattern = @"^\s+(?!\B)|\s*(?>[\r\n]+)$";

for (int i = 0; i < inputs.Length; i++)
{
    string result = Regex.Replace(inputs[i], pattern, "",
                                  RegexOptions.Multiline);
    Console.WriteLine(result == outputs[i]);
}

编辑:针对模式无法清理混合空格和换行符文本的问题,我在正则表达式的最后一部分添加了\s*。我之前的模式是多余的,我意识到\s*可以处理两种情况。


@Will 感谢您的反馈。我已更新模式和示例代码以解决新的测试用例。试试看吧。我还清理了关于空格被吞噬的帖子,并选择保留 (?!\B) 部分中的 ^\s+(?!\B),因为我认为这更接近请求的精神,并在有效字符存在的地方保留空格。 - Ahmad Mageed
1
啊,好多了。我今天会花些时间(对其进行样式和)分析并运行测试用例。谢谢。 - user1228
正在进行最终测试。你的正则表达式是目前为止最好的,但我唯一遇到的问题是,如果最后一行有空白,它会删除所有的空白,而不仅仅是最后一个换行符。换句话说,"test\s\r\ntest\s\r\n"返回"test\s\r\ntest"。我上传了一个简单的应用程序,用于验证和运行性能测试,如果你想再试一次,可以使用链接 - user1228
@Will 我下载了这个示例,但是无法找到一种清理最后一个场景的模式。我花了一些时间尝试使用条件模式来尝试解决最后的\r\n并保留空格,但是没有成功。 - Ahmad Mageed
好的,你和克里斯·施米奇基本上有相同的问题,这似乎并不容易解决。我现在会放置赏金并稍后进行调整。因此,在你们两个之间选择时,我对你们两个的表达式进行了分析。他的内存优势微乎其微。然而,你的表达式运行速度是他的两倍。所以我授予你赏金。感谢你在这方面提供的所有帮助。 - user1228
显示剩余3条评论

1
string corrected = 
    System.Text.RegularExpressions.Regex.Replace(input, @"\n+", "\n");

1
如果该行包含需要删除的空白字符,您可以将@"\n +"更改为@"\n\s?\n+"。 - Nick Gotch

1
这里还有另一个选项:使用StringReader类。优点:只需对字符串进行一次遍历,不会创建中间数组。
public static string RemoveEmptyLines(this string text) {
    var builder = new StringBuilder();

    using (var reader = new StringReader(text)) {
        while (reader.Peek() != -1) {
            string line = reader.ReadLine();
            if (!string.IsNullOrWhiteSpace(line))
                builder.AppendLine(line);
        }
    }

    return builder.ToString();
}

注意: IsNullOrWhiteSpace 方法是 .NET 4.0 中的新内容。如果你没有,自己编写也很简单:
public static bool IsNullOrWhiteSpace(string text) {
    return string.IsNullOrEmpty(text) || text.Trim().Length < 1;
}

@Adam:哈,哇,我说的话非常愚蠢。我是指没有中间数组,因为string.Split方法会自动处理(谢谢)。 - Dan Tao

1

我会选择:

  public static string RemoveEmptyLines(string value) {
    using (StringReader reader = new StringReader(yourstring)) {
      StringBuilder builder = new StringBuilder();
      string line;
      while ((line = reader.ReadLine()) != null) {
        if (line.Trim().Length > 0)
          builder.AppendLine(line);
      }
      return builder.ToString();
    }
  }

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