我正在对一个字符串测试两个几乎相同的正则表达式(在regex101.com上),我注意到他们采取的步骤数量存在巨大差异。以下是两个正则表达式:
(Stake: £)(\d+(?:\.\d+)?)
(winnings: £)(\d+(?:\.\d+)?)
这是我运行它们的字符串(使用修饰符g
,i
,m
,u
):
开始游戏,信用:£200.00game num:1,Stake:£2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings:£0.00End Game,Credit:£198Start ...
旁注:字符串/正则表达式不是我的,我只是从elsewhere on SE中取出它们,看到这种情况发生了。它与这个问题无关,但我认为它需要归属。
(注意:Regex101似乎得到了PHP的PCRE正则表达式的支持。“优化关闭”设置可能是PCRE_NO_START_OPTIMIZE | PCRE_NO_AUTO_POSSESS
。感谢Lucas Trzesniewski解决这个问题并写了一些C# code来测试它。)
优化:第一个采用 304 步骤进行匹配,而第二个采取 21 步骤。这是我想知道的巨大差异。
关闭优化:第一个采用 333 步骤进行匹配,而第二个采取 317 步骤。这表明第一种模式未能优化,而不是第二种模式。
有趣的是,没有u
修饰符,第一个模式(优化开启)只需要40步。 (但它不会改变其他任何东西的表现,也不会改变正则表达式最终匹配的东西。)
我想知道regex101的优化引擎(PCRE)导致步数差异的原因。我特别在寻找启用u
nicode时较长的正则表达式减少步数的原因。
有没有理由发生这种情况,或者这是引擎中的错误?
答案 0 :(得分:10)
Regex101使用PCRE库(默认情况下),因此其调试器与PCRE的行为有关。 PCRE库通过其PCRE_AUTO_CALLOUT
标志支持自动标注选项,该标志在匹配的每一步调用回调函数。我99.99%确定regex101的调试器是如何工作的。每个callout调用都在调试器中注册为 step 。
现在,看看所涉及的不同步骤,首先是没有u
选项:
注意文本光标如何从Start Game
部分直接跳到Stake
部分。
添加u
时会发生什么?
请注意,跳转不再发生,此是其他步骤的来源。
u
选项有什么作用?它设置了PCRE_UTF8 | PCRE_UCP
- 是的,这两个都是一次性的,这个PCRE_UCP
标志在这里很重要。
这是pcreunicode docs所说的:
不区分大小写的匹配仅适用于值小于128的字符,除非PCRE是使用Unicode属性支持构建的。一些Unicode字符(例如希腊语sigma)具有两个以上与大小写等效的代码点。直到并包括PCRE版本8.31,仅支持一对一的案例映射,但后来的版本(具有Unicode属性支持)确实将所有版本的字符视为大小写,例如希腊语sigma。
由于在Unicode模式下处理不区分大小写的额外复杂性,引擎不能只跳过所有文本。要验证这是罪魁祸首,让我们使用gmu
标记(即使用u
但使用而不使用 i
)尝试此操作:
即使使用u
也会应用优化,这几乎证实了这一假设。
您看到的慢路径看起来就像使用了PCRE_NO_START_OPTIMIZE
一样(这可能是regex101' s 禁用内部引擎的一部分优化以及PCRE_NO_AUTO_POSSESS
与此处无关。)
有关the docs的更多信息:
pcre_exec()
在比赛开始时使用了许多优化,以加快进程。例如,如果已知未锚定的匹配必须以特定字符开头,它会在主题中搜索该字符,如果找不到,则立即失败,而不实际运行主匹配函数。
由于某种原因(稍后会变得明显),PCRE未能将s
或k
字母注册为必需的起始字符,并且无法使用锚定优化。在这方面,拉丁字母的所有其他字母都可以正常工作
这就是Stake
需要比winnings
更多步骤的原因:引擎不会跳过检查。
这是我与PCRE 8.38一起使用的测试程序,这是本文提供的最新PCRE1版本。
#include <stdio.h>
#include <string.h>
#include <pcre.h>
static int calloutCount;
static int callout_handler(pcre_callout_block *c) {
++calloutCount;
return 0;
}
static void test_run(const char* pattern, pcre* re, pcre_extra* extra) {
int rc, startOffset;
int ovector[3 * 3];
pcre_callout = callout_handler;
calloutCount = 0;
startOffset = 0;
const char *subject = "Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...";
for (;;) {
rc = pcre_exec(re, extra, subject, strlen(subject), startOffset, 0, ovector, sizeof(ovector) / sizeof(int));
if (rc < 0)
break;
startOffset = ovector[1];
}
printf("%-30s %s => %i\n", pattern, extra ? "(studied) " : "(not studied)", calloutCount);
}
static void test(const char* pattern) {
pcre *re;
const char *error;
int erroffset;
pcre_extra *extra;
re = pcre_compile(pattern, PCRE_AUTO_CALLOUT | PCRE_CASELESS | PCRE_MULTILINE | PCRE_UTF8 | PCRE_UCP, &error, &erroffset, 0);
if (re == 0)
return;
extra = pcre_study(re, 0, &error);
test_run(pattern, re, extra);
if (extra)
test_run(pattern, re, NULL);
pcre_free_study(extra);
pcre_free(re);
}
int main(int argc, char **argv) {
printf("PCRE version: %s\n\n", pcre_version());
test("(Stake: £)(\\d+(?:\\.\\d+)?)");
test("(winnings: £)(\\d+(?:\\.\\d+)?)");
return 0;
}
我得到的输出如下:
PCRE version: 8.38 2015-11-23
(Stake: £)(\d+(?:\.\d+)?) (studied) => 40
(Stake: £)(\d+(?:\.\d+)?) (not studied) => 302
(winnings: £)(\d+(?:\.\d+)?) (studied) => 21
(winnings: £)(\d+(?:\.\d+)?) (not studied) => 21
在这里,我们可以看到研究模式在第一种情况下有所不同,但在第二种情况下没有。
学习模式意味着以下内容:
研究模式有两个作用:首先,计算匹配模式所需的主题字符串长度的下限。这并不意味着有任何匹配的字符串,但它确保没有更短的字符串匹配。该值用于通过尝试匹配短于下限的字符串来避免浪费时间。您可以通过
pcre_fullinfo()
函数找到调用程序中的值。研究模式对于没有单个固定起始字符的非锚定模式也很有用。创建可能的起始字节的位图。这加速了在主题中找到开始匹配的位置。 (在16位模式下,位图用于小于256的16位值。在32位模式下,位图用于小于256的32位值。)
从结果和文档描述中,您可以得出结论,PCRE认为S
字符不是锚定字符,而在无情况Unicode模式下,它需要创建位图。位图允许应用优化。
现在,这里是PCRE2版本,针对PCRE2 v10.21编译,这是本文的最新版本。结果将不足为奇,因为PCRE2 总是研究你提供它的模式,没有问题。
#include <stdio.h>
#include <string.h>
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
static int callout_handler(pcre2_callout_block *c, void *data) {
++*((int*)data);
return 0;
}
static void test(const char* pattern) {
pcre2_code *re;
int error;
PCRE2_SIZE erroffset;
pcre2_match_context *match_context;
pcre2_match_data *match_data;
int rc, startOffset = 0;
int calloutCount = 0;
PCRE2_SIZE *ovector;
const PCRE2_SPTR subject = (PCRE2_SPTR)"Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...";
re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, PCRE2_AUTO_CALLOUT | PCRE2_CASELESS | PCRE2_MULTILINE | PCRE2_UTF | PCRE2_UCP, &error, &erroffset, 0);
if (re == 0)
return;
match_context = pcre2_match_context_create(0);
pcre2_set_callout(match_context, callout_handler, &calloutCount);
match_data = pcre2_match_data_create_from_pattern(re, 0);
ovector = pcre2_get_ovector_pointer(match_data);
for (;;) {
rc = pcre2_match(re, subject, PCRE2_ZERO_TERMINATED, startOffset, 0, match_data, match_context);
if (rc < 0)
break;
startOffset = ovector[1];
}
printf("%-30s => %i\n", pattern, calloutCount);
pcre2_match_context_free(match_context);
pcre2_match_data_free(match_data);
pcre2_code_free(re);
}
int main(int argc, char **argv) {
char version[256];
pcre2_config(PCRE2_CONFIG_VERSION, &version);
printf("PCRE version: %s\n\n", version);
test("(Stake: £)(\\d+(?:\\.\\d+)?)");
test("(winnings: £)(\\d+(?:\\.\\d+)?)");
return 0;
}
结果如下:
PCRE version: 10.21 2016-01-12
(Stake: £)(\d+(?:\.\d+)?) => 40
(winnings: £)(\d+(?:\.\d+)?) => 21
烨。使用学习时与PCRE1相同的结果。
让我们来看看实施细节,好吗? PCRE将模式编译为操作码,我们可以使用pcretest
程序阅读。
这是输入文件:
/(Stake: £)(\d+(?:\.\d+)?)/iDW8
Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...
/(winnings: £)(\d+(?:\.\d+)?)/iDW8
Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...
它的格式很简单:分隔符中的模式后跟选项和一个或多个输入字符串。
选项包括:
i
- PCRE_CASELESS
D
- 打开调试模式:显示操作码和模式信息W
- PCRE_UCP
8
- PCRE_UTF8
结果是......
PCRE version 8.38 2015-11-23
/(Stake: £)(\d+(?:\.\d+)?)/iDW8
------------------------------------------------------------------
0 55 Bra
3 24 CBra 1
8 clist 0053 0073 017f
11 /i ta
15 clist 004b 006b 212a
18 /i e: \x{a3}
27 24 Ket
30 22 CBra 2
35 prop Nd ++
39 Brazero
40 9 Bra
43 /i .
45 prop Nd ++
49 9 Ket
52 22 Ket
55 55 Ket
58 End
------------------------------------------------------------------
Capturing subpattern count = 2
Options: caseless utf ucp
No first char
Need char = ' '
Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...
0: Stake: \x{a3}2.00
1: Stake: \x{a3}
2: 2.00
/(winnings: £)(\d+(?:\.\d+)?)/iDW8
------------------------------------------------------------------
0 60 Bra
3 29 CBra 1
8 /i winning
22 clist 0053 0073 017f
25 /i : \x{a3}
32 29 Ket
35 22 CBra 2
40 prop Nd ++
44 Brazero
45 9 Bra
48 /i .
50 prop Nd ++
54 9 Ket
57 22 Ket
60 60 Ket
63 End
------------------------------------------------------------------
Capturing subpattern count = 2
Options: caseless utf ucp
First char = 'w' (caseless)
Need char = ' '
Start Game, Credit: £200.00game num: 1, Stake: £2.00Spinning Reels:NINE SEVEN KINGKING STAR ACEQUEEN JACK KINGtotal winnings: £0.00End Game, Credit: £198Start...
0: winnings: \x{a3}0.00
1: winnings: \x{a3}
2: 0.00
现在这个变得有趣了!
首先,我们看到第一个模式获得No first char
,而第二个模式获得First char = 'w' (caseless)
。
S
标识为:clist 0053 0073 017f
,k
变为clist 004b 006b 212a
。这些是这些字母的匹配代码点集。它们代表什么?
S
:0053 = S
,0073 = s
,017f = ſ
(拉丁文小写长S) - Yikes。k
:004b = K
,006b = k
,212a = K
(开尔文标志) - 双倍赞。不应用优化,因为这两个字母的大小写折叠包括ASCII范围之外的字符。
现在你看:):
那么,你可以用PHP做什么?你add the S
modifier:
当一个模式将被多次使用时,值得花更多时间分析它以加快匹配所需的时间。如果设置了此修改器,则执行此额外分析。目前,研究模式仅对没有单个固定起始字符的非锚定模式有用。
Regex101不支持模式学习AFAIK。
答案 1 :(得分:2)
正如Lucas Trzesniewski所指出的,这种行为只发生在a-z
:k
和s
中的2个字母。其中两个出现在第一种模式中,巧合。
我想知道为什么当我将正则表达式缩短为(ke: £)(\d+(?:\.\d+)?)
时,我没有看到任何改进,这就是原因。
但我认为我找到了答案。我开始挖掘PCRE源代码,挑选看起来可能感兴趣的随机文件。我找到了pcre2_udc.c:
此表只有在构建UCP支持时才需要,而在PCRE2中则需要UTF支持自动发生。
在页面的下方,我找到了这个unicode点列表。我已将相应的Unicode字符放在其十六进制值旁边,以便快速参考:
const uint32_t PRIV(ucd_caseless_sets)[] = {
NOTACHAR,
0x0053, S 0x0073, s 0x017f, ſ NOTACHAR,
0x01c4, DŽ 0x01c5, Dž 0x01c6, dž NOTACHAR,
0x01c7, LJ 0x01c8, Lj 0x01c9, lj NOTACHAR,
0x01ca, NJ 0x01cb, Nj 0x01cc, nj NOTACHAR,
0x01f1, DZ 0x01f2, Dz 0x01f3, dz NOTACHAR,
0x0345, ͅ 0x0399, Ι 0x03b9, ι 0x1fbe, ι NOTACHAR,
0x00b5, µ 0x039c, Μ 0x03bc, μ NOTACHAR,
0x03a3, Σ 0x03c2, ς 0x03c3, σ NOTACHAR,
0x0392, Β 0x03b2, β 0x03d0, ϐ NOTACHAR,
0x0398, Θ 0x03b8, θ 0x03d1, ϑ 0x03f4, ϴ NOTACHAR,
0x03a6, Φ 0x03c6, φ 0x03d5, ϕ NOTACHAR,
0x03a0, Π 0x03c0, π 0x03d6, ϖ NOTACHAR,
0x039a, Κ 0x03ba, κ 0x03f0, ϰ NOTACHAR,
0x03a1, Ρ 0x03c1, ρ 0x03f1, ϱ NOTACHAR,
0x0395, Ε 0x03b5, ε 0x03f5, ϵ NOTACHAR,
0x1e60, Ṡ 0x1e61, ṡ 0x1e9b, ẛ NOTACHAR,
0x03a9, Ω 0x03c9, ω 0x2126, Ω NOTACHAR,
0x004b, K 0x006b, k 0x212a, K NOTACHAR,
0x00c5, Å 0x00e5, å 0x212b, Å NOTACHAR,
}
结果,这个正则表达式:
(ſtake: £)(\d+(?:\.\d+)?)
相当于我的第一个正则表达式。
再一次,是卢卡斯通过the pcreunicode docs的链接让我走向了正确的方向:
不区分大小写的匹配仅适用于值小于128的字符,除非PCRE是使用Unicode属性支持构建的。一些Unicode字符(例如希腊语sigma)具有两个以上与大小写等效的代码点。直到并包括PCRE版本8.31,仅支持一对一的案例映射,但后来的版本(具有Unicode属性支持)确实将所有版本的字符视为大小写,例如希腊语sigma。
当存在任何这些字符时,引擎无法优化。
但是在pcre2_study.c内查看,我标记了(/**/
)在学习期间可能发生优化的行:
case OP_PROP:
if (tcode[1] != PT_CLIST) return SSB_FAIL;
{
const uint32_t *p = PRIV(ucd_caseless_sets) + tcode[2];
/**/ while ((c = *p++) < NOTACHAR)
{
#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH == 8
if (utf)
{
PCRE2_UCHAR buff[6];
(void)PRIV(ord2utf)(c, buff);
c = buff[0];
}
#endif
if (c > 0xff) SET_BIT(0xff); else SET_BIT(c);
}
}
try_next = FALSE;
break;