如何使用多行正则表达式完成删除整行?

How to complete remove a whole line with multiline regular expressions?

我想删除此多行字符串中包含 b 的所有行:

aba\n
aaa\n
aba\n
aaa\n
aba[\n\n - optional]

注意文件不一定由换行符终止,或者可能在末尾有我想保留的额外换行符.

这是预期的输出:

aaa\n
aaa[\n\n - as in the input file]

这是我试过的:

import re
String = "aba\naaa\naba\naaa\naba"
print(String)
print(re.sub(".*b.*", "", String))  # this one leaves three empty lines
print(re.sub(".*b.*\n", "", String))  # this one misses the last line
print(re.sub("\n.*b.*", "", String))  # this one misses the first line
print(re.sub(".*b.*\n?", "", String))  # this one leaves an empty last line
print(re.sub("\n?.*b.*", "", String))  # this one leaves an empty first line
print(re.sub("\n?.*b.*\n?", "", String))  # this one joins the two remaining lines

我也尝试了 flags=re.M 和各种前瞻和后视,但主要问题似乎是:如何删除 \n 中的第一次或最后一次出现一个匹配的字符串,取决于哪个存在 - 但不是两者都存在,如果两者都存在?

您可以使用正则表达式或非正则表达式方法:

import re
s = "aba\naaa\naba\naaa\naba"
print( "\n".join([st for st in s.splitlines() if 'b' not in st]) )
print( re.sub(r'^[^b\r\n]*b.*[\r\n]*', '', s, flags=re.M).strip() )

参见Python demo

非正则表达式方法,"\n".join([st for st in s.splitlines() if 'b' in st]),用换行符分割字符串,过滤掉所有没有 b 的行,然后将这些行重新连接起来。

正则表达式方法涉及 r'^[^b\r\n]b.*[\r\n]*':

这样的模式
  • ^ - 行首
  • [^b\r\n]* - 除 CR、LF 和 b
  • 之外的任何 0 个或多个字符
  • b - 一个 b 字符
  • .* - 除换行符以外的任何 0+ 个字符
  • [\r\n]* - 0+ CR 或 LF 字符。

请注意,您需要使用 .strip() 删除此后字符串 start/end 处不需要的空格。

单一的正则表达式解决方案太麻烦了,我不建议在现实生活中使用它:

rx = r'(?:{0}(?:\n|$))+|(?:\n|^){0}'.format(r'[^b\n]*b.*')
print( re.sub(rx, '', s) )

参见 Python demo

模式看起来像 (?:[^b\n]*b.*(?:\n|$))+|(?:\n|^)[^b\n]*b.* 并且匹配

  • (?:[^b\n]*b.*(?:\n|$))+ - 重复 1 次或多次
    • [^b\n]* - 除了 b 和换行符
    • 之外的任何 0+ 个字符
    • b.* - b 和该行的其余部分(.* 匹配除换行符以外的任何 0+ 个字符)
    • (?:\n|$) - 换行符或字符串结尾
  • | - 或者
    • (?:\n|^) - 换行符或字符串开头
    • [^b\n]*b.* - 一行至少有一个 b

在删除带有 b 的行的 re.sub() 调用中需要考虑三种情况:

  1. 模式后跟行尾字符 (eol)
  2. 文本中的最后一行(没有结尾的 eol)
  3. 当只有一行没有尾随 eol 时

在第二种情况下,您想要删除前面的 eol 字符以避免创建空行。如果有 "b",第三种情况将产生一个空字符串。

正则表达式的贪婪将引入第四种情况,因为不能有任何模式重叠。如果你的最后一行包含一个 "b" 并且之前的行也包含一个 "b",案例 #1 将消耗前一行的 eol 字符,因此它没有资格检测上一行的模式最后一行(即 eol 后跟文本末尾的模式)。这可以通过将(案例#1)连续匹配行作为一个组清除并将最后一行作为该组的可选组件包括在内来解决。无论遗漏什么,都将是尾随行(案例#2),您要在其中删除前面的 eol 而不是下面的 eol。

为了管理线条模式的重复.*b.*,您需要assemble从两部分搜索模式:线条模式和多次使用它的列表模式。既然我们已经深入了解正则表达式,为什么不使用 re.sub() 来做到这一点。

import re

LinePattern = "(.*b.*)"
ListPattern = "(Line\n)+(Line$)?|(\nLine$)|(^Line$)" # Case1|Case2|Case3
Pattern     = re.sub("Line",LinePattern,ListPattern)

String  = "aba\naaa\naba\naaa\naba"
cleaned = re.sub(Pattern,"",String)

注意:此技术也适用于不同的分隔符(例如,逗号而不是 eol),但该字符需要从行模式中排除(例如 ([^,]*b[^,]*)