“Variable length lookbehind not implemented” 但它并不是可变长度。

56

我有一个非常复杂的正则表达式需要诊断。虽然它非常长,但我已将其缩减为以下脚本。在使用Strawberry Perl v5.26.2运行。

use strict;
use warnings;

my $text = "M Y H A P P Y T E X T";
my $regex = '(?i)(?<!(Mon|Fri|Sun)day |August )abcd(?-i)';

if ($text =~ m/$regex/){
    print "true\n";
}
else {
    print "false\n";
}

出现了“正则表达式中未实现可变长度的回溯”的错误。

我希望你能帮我解决以下几个问题:

  1. 我不明白为什么会出现这个错误,因为所有可能的回溯值都是7个字符:“Monday ”,“Friday ”,“Sunday ”,“August ”。
  2. 我没有自己编写这个正则表达式,也不确定如何解释语法(?i)(?-i)。当我去掉(?i)时,错误实际上消失了。Perl将如何解释正则表达式的这一部分?我认为前两个字符被解释为“可选文字括号”,除非括号没有被转义,否则我会得到不同的语法错误,因为那样就无法匹配结束括号。
  3. 这种行为始于Perl 5.16.3_64和5.26.1_64之间,在Strawberry Perl中至少如此。前一个版本可以正常运行该代码,后一个版本则不能。为什么会这样?

5
问题可以进一步简化:/(?<!August )a/i 已经表明了“可变长度后顾断言”...但是如果从 August 中删除一个字母,它就能正常工作。删除 /i 也能正常工作。如果你想笑的话:August 不起作用,如上所示。Abcdst 不起作用。但是 Abcdet 起作用。 - Dada
5
也许是因为 st 可以作为连字符存在?同样的,fiff 也是。 - choroba
1
Perl 5.10似乎可以很好地处理这些正则表达式。 - AKHolland
2
在Perl源代码中,pod/perl5220deltapod提到了“正则表达式m/%s/中未实现可变长度回溯”的警告。因此,我猜测5.22.0版本已经非常接近了,并且该警告的perldiag条目已添加有关Unicode行为的信息。 - sticky bit
1
计划在5.30版本中解决这些连字回顾问题。 - khw
显示剩余2条评论
4个回答

78
我已将您的问题简化为以下内容:
my $text = 'M Y H A P P Y T E X T';
my $regex = '(?<!st)A';
print ($text =~ m/$regex/i ? "true\n" : "false\n");

由于存在 /i(不区分大小写)修饰符和某些字符组合,例如"ss""st"可以被替换为排版连字,导致它成为可变长度(例如/August/i匹配AUGUST(6个字符)和august(5个字符,最后一个是U+FB06))。
然而,如果我们删除 /i(不区分大小写)修饰符,则可以正常工作,因为不会匹配排版连字。 解决方案:使用aa修饰符,即:
/(?<!st)A/iaa

或者在你的正则表达式中:

my $text = 'M Y H A P P Y T E X T';
my $regex = '(?<!(Mon|Fri|Sun)day |August )abcd';
print ($text =~ m/$regex/iaa ? "true\n" : "false\n");

来自perlre

如果要禁止匹配ASCII/非ASCII字符(如将“k”与“\N{KELVIN SIGN}”进行匹配),则需要两次指定“a”,例如/aai/aia。(第一次出现的“a”限制了\d等内容,而第二次出现的则添加了“/i”限制。)但请注意,超出ASCII范围的代码点会使用Unicode规则进行/i匹配,因此这个修改器并没有真正将事情限制在仅仅是ASCII上;它只是禁止了ASCII和非ASCII的混合

在这里查看相关讨论


10
为了做出贡献:perldiag中也提到:“在/i下存在一些不明显的Unicode规则,可以匹配可变长度,但你可能认为不能。” 除了使用 /aa 开关外,另一个解决方案可能是反转字符串并使用前瞻,如果您需要支持Unicode和可变长度环视。 - sticky bit
你知道为什么早期版本的Perl没有出现这种行为吗?5.16.3_64不会抛出错误,但是5.26.1_64会,在Strawberry Perl中至少如此。 - Stephen
是的,正如链接讨论中提到的那样,这种行为取决于版本。不幸的是,我不知道这种行为开始的确切版本。 - anubhava
3
st替换为[s][t]似乎也能解决这个问题。 - Stephane Chazelas
感谢@StephaneChazelas的非常好的观点。/(?<![s]t)A/i/(?<!s[t])A/i都可以正常工作,但使用/aa相对于调整更大的正则表达式来说更容易。 - anubhava
1
@Stephen - ssß 匹配的功能在 Perl 5.16.3 中已经存在,但其他样式连字可能是稍后添加的,可能是在 5.22 左右。我尝试将所有具有可变后置查找长度的双字母组映射到 我的答案 中,并提供了尽可能多的版本信息。 - Adam Katz

22

这是因为st可以是连字体。同样的情况也发生在fiff上:

#!/usr/bin/perl
use warnings;
use strict;

use utf8;

my $fi = 'fi';
print $fi =~ /fi/i;

那么想象一下像fi|fi这样的情况,其中备选项的长度确实不相同。


2
“st” 可以用一个字符的 风格连字 表示为 ,因此它的长度可以是 2 或 1。
使用 bash 命令快速查找 Perl 的完整 2→1 字符连字列表:
$ perl -e 'print $^V'
v5.26.2
$ for lig in {a..z}{a..z}; do \
    perl -e 'print if /(?<!'$lig')x/i' 2>/dev/null || echo $lig; done

ff fi fl ss st

这些分别代表着连字体中的ß/。(代表使用过时的长s字符ſt;它与st匹配,而不匹配ft。)
Perl还支持剩余的样式连字体用于ffiffl,但在这种情况下这并不值得注意,因为回顾先行断言已经单独处理了/的问题。
未来版本的Perl可能会包括更多的字体连字,尽管现在仍然是特定于字体的(例如Linux Libertine具有ctch的字体连字),或者是有争议的风格(例如荷兰语中用于ijij或西班牙语中废弃的代替ll)。对于不完全可互换的连字,似乎没有必要使用这种处理方式(例如没有人会接受dœs代替does),但也有其他情况,例如包括ß,因为它的大写形式是SS
Perl 5.16.3(以及类似的旧版本)只能识别“ss”(表示“ß”),不能扩展后瞻中的其他连字号(它们具有固定宽度,因此不会匹配)。我没有寻找修复错误的细节,以列出受影响的确切版本。
Perl 5.14引入了连字号支持,因此早期版本没有这个问题。
解决方法:
对于“/(?<!August)x/i”(只有第一个可以正确避免“August”)的解决方法:
  • /(?<!Augus[t])(?<!Augu(?=st).)x/i (绝对全面)
  • /(?<!Augu(?aa:st))x/i (仅在回顾中的st是“ASCII安全”的²)
  • /(?<!(?aa)August)x/i (整个回顾都是“ASCII安全”的²)
  • /(?<!August)x/iaa (整个正则表达式都是“ASCII安全”的²)
  • /(?<!Augus[t])x/i (破坏连字寻找¹)
  • /(?<!Augus.)x/i (稍微不同,匹配更多)
  • /(?<!Augu(?-i:st))x/i (回顾中区分大小写的st,不会匹配AugusTx
这些操作玩具通常涉及移除大小写敏感修饰符¹或添加ASCII安全修饰符²到不同的位置,经常需要正则表达式编写者明确知道变宽连字。
第一个变体(也是唯一全面的)通过两个回顾后方匹配可变宽度:首先是六个字符版本(如下面第一个引用中所述没有连字),其次是任何连字,采用前瞻(它的宽度为零!)来匹配st(包括连字),然后使用.考虑其单个字符宽度。 perlre man page的两个部分:

¹ 大小写不敏感修饰符/i和连字

There are a number of Unicode characters that match a sequence of multiple characters under /i. For example, "LATIN SMALL LIGATURE FI" should match the sequence fi. Perl is not currently able to do this when the multiple characters are in the pattern and are split between groupings, or when one or more are quantified. Thus

"\N{LATIN SMALL LIGATURE FI}" =~ /fi/i;          # Matches [in perl 5.14+]
"\N{LATIN SMALL LIGATURE FI}" =~ /[fi][fi]/i;    # Doesn't match!
"\N{LATIN SMALL LIGATURE FI}" =~ /fi*/i;         # Doesn't match!
"\N{LATIN SMALL LIGATURE FI}" =~ /(f)(i)/i;      # Doesn't match!

² ASCII安全修改器 /aa (perl 5.14+)

禁止ASCII/非ASCII匹配(例如,k\N{KELVIN SIGN}), 需要两次指定a,例如/aai/aia。(第一个 出现的a限制了\d等,第二个出现的a 添加了/i的限制。)但是,需要注意的是,ASCII范围之外的代码点将使用Unicode规则进行/i匹配,因此该修改器 实际上并不仅限于ASCII;它只禁止混合使用ASCII和非ASCII。

总之,这个修改器为不希望暴露于整个Unicode的应用提供保护。指定两次会增加保护。


0

在后顾之前加上(?i)

(?<!(Mon|Fri|Sun)day |August )(?i)abcd(?-i)

或者

(?<!(Mon|Fri|Sun)day |August )(?i:abcd)

对我来说,这似乎是一个错误。


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