Perl正则表达式中/m修饰符的意外行为

3
我想用这个正则表达式从多行字符串中删除前导和尾随空格:
s/^\s*|\s*$//mg

在这个例子中,它似乎运行得相当正常:

perl -e '$_=" a \n \n b\n"; s/^\s*|\s*$//mg; print "$_\n";'

它会给出以下结果:

a
b

出乎我的意料之外,带有空格的双重 \n 变成了单个 \n。

但是看看这个:

perl -e '$_=" a \n\n b\n"; s/^\s*|\s*$//mg; print "$_\n";'

result:

ab

现在两个换行符都消失了,多行字符串变成了单行,这不是我想要的结果。如果这不是一个 bug,我该如何避免这种行为?


1
\s 匹配包括换行符在内的所有空白字符,请使用 \h 代替。 - anubhava
你能否为给定的两个样本添加完整的预期输出?您是否要删除字符串中每行的前导/尾随空格,还是仅对整个字符串进行一次操作?如果要仅删除外部空格,请使用s/\A\s*|\s*\z//g - Sundeep
如果你想深入了解细节,可以尝试添加-Mre=debug以获取有关正则表达式的调试信息,然后比较两个不同字符串的交互方式。 - TLP
这个 很有趣。 - Wiktor Stribiżew
2个回答

2
使用-Mre=debug模块并深入研究,我找到了我认为的答案。我删除了前导空格,因为它与问题无关。我只保留了相关部分。两个正则表达式首先使用RHS(5:BRANCH)匹配第二个换行符前面的空格/换行符,然后将指针设置在该第二个换行符前面:
情况1:字符串a \n \n b\n
Matching REx "^\s+|\s+$" against "%n b%n"
   4 <a %n > <%n b%n>        |   0| 1:BRANCH(5)
   4 <a %n > <%n b%n>        |   1|  2:MBOL(3)
                             |   1|  failed...
   4 <a %n > <%n b%n>        |   0| 5:BRANCH(9)
   4 <a %n > <%n b%n>        |   1|  6:PLUS(8)
                             |   1|  POSIXD[\s] can match 2 times out of 2147483647...
   6 <a %n %n > <b%n>        |   2|   8:MEOL(9)
                             |   2|   failed...
   5 <a %n %n> < b%n>        |   2|   8:MEOL(9)
                             |   2|   failed...
                             |   1|  failed...
                             |   0| BRANCH failed...
   5 <a %n %n> < b%n>        |   0| 1:BRANCH(5)  <-- HERE!
   5 <a %n %n> < b%n>        |   1|  2:MBOL(3)
   5 <a %n %n> < b%n>        |   1|  3:PLUS(9)
                             |   1|  POSIXD[\s] can match 1 times out of 2147483647...
   6 <a %n %n > <b%n>        |   2|   9:END(0)
Match successful!

在这种情况下,LHS(1:BRANCH)首先失败,RHS(5:BRANCH)也失败了,因此它向前移动1步,直到换行符后,LHS匹配并删除其前面的内容:一个空格。
在换行符和b前面的空格之间的匹配中,“指针”在正则表达式中已经向前移动到换行符的前面。
%n> < b%n>
^   \s

案例2:字符串a \n\n b\n
Matching REx "^\s+|\s+$" against "%n b%n"
   3 <a %n> <%n b%n>         |   0| 1:BRANCH(5) <-- HERE!
   3 <a %n> <%n b%n>         |   1|  2:MBOL(3)
   3 <a %n> <%n b%n>         |   1|  3:PLUS(9)
                             |   1|  POSIXD[\s] can match 2 times out of 2147483647...
   5 <a %n%n > <b%n>         |   2|   9:END(0)
Match successful!

在这个字符串中,LHS(1:BRANCH)中的零宽断言 ^ 可以看到字符串左侧的换行符,并允许其匹配。在另一个字符串中,该位置有一个空格,因此无法匹配。因此,LHS备选项匹配(称为1:BRANCH),并删除其前面的内容,即换行符和空格 \ n 。
与Case 1不同,它不跳过第一次尝试并向前移动1步,而是直接匹配左侧的换行符和右侧的空格 \ n :
%n> <%n b%n>
^   \s\s

TL;DR: 在第二个字符串中,换行符可以匹配两个换行符之间的行首,因此会将它们都删除。在第一个字符串中,由于有空格,它不能这样匹配,而是向前移动一步,跳过换行符并使用该换行符匹配字符串的开头。结果是换行符保留在字符串中。
如何避免这种行为?问题在于您的正则表达式太宽松了。`\n` 可以匹配正则表达式 `^`, `$` 和 `\s` 的所有组件,以各种组合方式匹配。它还可以在字符串中间匹配。如果您想要安全可预测的结果,请以逐行模式使用正则表达式,不要将文件读入单个字符串中。然后您就不需要多行匹配,所有问题都会消失。
否则,请避免使用多行修饰符,只需像通常一样删除前导和尾随空格,然后在字符串内部去除多个换行符与空格,例如 `s/\n\s*\n/\n/g`。
实质上,您正在尝试同时做太多事情。使您的正则表达式更严格,并尝试一次完成一件事。

0

\s 可以匹配换行符,这导致了删除换行符的问题。

用以下之一替换 \s

  • \h
    仅删除水平空白字符。虽然它不匹配换行符,但也不匹配其他垂直空白字符。[1]
  • (?[ \s - \n ])
    在 5.36 之前需要 use experimental qw( regex_sets );。但是,自从它作为实验性功能在 5.18 中引入以来,该特性没有进行任何更改,因此可以安全地添加并使用该特性。
  • [^\S\n]
    匹配既不是非空格字符也不是换行符的字符,也就是说,不是换行符的空格字符。

接下来详细说明了您的模式如何匹配。


对于

␠ a ␠ ␊ ␠ ␊ ␠ b ␊
0 1 2 3 4 5 6 7 8 9

这是指编程中的模式

/^\s*|\s*$/m

返回以下匹配结果:

  1. 位置0,长度1:^\s*匹配。
  2. 位置2,长度3:␠␊␠\s*$匹配。XXX
  3. 位置5,长度0:空字符串被\s*$匹配。
  4. 位置6,长度1:^\s*匹配。
  5. 位置8,长度1:\s*$匹配。XXX
  6. 位置9,长度0:空字符串被^\s*匹配。

对于

␠ a ␠ ␊ ␊ ␠ b ␊
0 1 2 3 4 5 6 7 8

模式

/^\s*|\s*$/m

产生以下匹配结果:

  1. 位置0,长度1:^\s* 匹配。
  2. 位置2,长度2:␠␊\s*$ 匹配。XXX
  3. 位置4,长度2:␊␠^\s* 匹配。XXX
  4. 位置7,长度1:\s*$ 匹配。XXX
  5. 位置8,长度0:空字符串被 ^\s* 匹配。

脚注:

  1. 垂直空白:

    • U+000A 换行符
    • U+000B 制表符
    • U+000C 换页符
    • U+000D 回车符
    • U+0085 下一行
    • U+2028 行分隔符
    • U+2029 段落分隔符

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