【问题标题】:perl regex unexpected behaviour of /m modifier/m 修饰符的 perl 正则表达式意外行为
【发布时间】:2021-06-26 09:49:39
【问题描述】:

我想用这个正则表达式从多行字符串中删除前导和尾随空格:

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";'

结果:

ab

现在两个 \n 都消失了,多行字符串现在是单行,这不是我想要的。 如果这不是错误,我该如何避免这种行为?

【问题讨论】:

  • \s 匹配包括换行符在内的空格,请改用\h
  • 你能为两个给定的样本添加完整的预期输出吗?您想为字符串中的每一行删除前导/尾随空格还是只为整个字符串删除一次?如果您只想删除外部空格,请使用 s/\A\s*|\s*\z//g
  • 如果您想深入了解细节,可以尝试添加-Mre=debug 以获取有关正则表达式的一些调试信息,然后比较两个不同字符串的交互方式。
  • This 很有趣。

标签: regex perl modifier


【解决方案1】:

使用-Mre=debug 模块并深入了解细节,我找到了我认为的答案。我删除了前导空格,因为它与问题无关。我删除了除相关部分之外的所有内容。两个正则表达式首先使用 RHS (5:BRANCH) 匹配第二个换行符前面的空格/换行符,然后将指针设置在第二个换行符前面:

案例一:字符串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

不像案例1那样跳过第一次尝试并向前移动1步,它可以直接匹配左侧的换行符,右侧的空格\n

%n> <%n b%n>
^   \s\s

TL;DR:在您的第二个字符串中,换行符可以匹配两个换行符之间的行首,因此将它们都删除。在第一个字符串中,它不能像那样匹配,因为那里有一个空格,而是向前移动一步,跳过换行符并使用该换行符来匹配字符串的开头。效果是换行符保留在字符串中。

如何避免这种行为?好吧,问题是你的正则表达式太松了。 \n 可以匹配正则表达式 ^$\s 的所有组件,以各种组合方式进行。它也可以匹配在字符串的中间。如果您想安全并获得可预测的结果,请在逐行模式下使用正则表达式,不要将文件转换为单个字符串。那么你就不需要多行匹配了,所有的问题都迎刃而解了。

否则,请避免使用多行修饰符,只需照常删除前导和尾随空格,然后在字符串内部修剪多个带有空格的换行符,例如s/\n\s*\n/\n/g

本质上,您试图同时做太多事情。让你的正则表达式更严格,并尝试一次做一件事情。

【讨论】:

    【解决方案2】:

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

    \s 替换为以下之一:

    • [^\S\n]
      匹配既不是非空白字符也不是换行符的字符,即不是换行符的空白字符。
    • (?[ \s - \n ])
      目前处于试验阶段,需要use experimental qw( regex_sets );
    • \h
      仅删除水平空白字符。虽然它不匹配换行符,但它也不匹配其他垂直空白字符。[1]

    以下内容详细说明了您的模式是如何匹配的。


    对于

    ␠ 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. Pos 5, len 0: 与\s*$ 匹配的空字符串
    4. 位置 6,长度 1:^\s* 匹配。
    5. 位置 8,长度 1:\s*$ 匹配。 XXX
    6. Pos 9,len 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. Pos 8,len 0:与^\s* 匹配的空字符串。

    脚注:

    1. 垂直空格:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-05-22
      • 2012-07-08
      • 1970-01-01
      • 1970-01-01
      • 2010-09-17
      • 2013-11-15
      • 1970-01-01
      相关资源
      最近更新 更多