优化正则表达式以实现多行匹配,包括步骤和时间



Regex - 应匹配换行符,并且应在特定格式的第一次出现时结束

参考正则表达式 - 应匹配换行符,并且应在特定格式的第一次出现时结束

我正在尝试从日志中读取邮件正文(其中一些超过 500 行(。
示例数据如下所示:BodyOftheMail_Script = [ BEGIN 500 lines END ]

我尝试了以下正则表达式:

+-----------------------------------------------------------------------+----------+--------+
|                                Regexp                                 |   Steps  | Time  |
+-----------------------------------------------------------------------+----------+--------+
| BodyOftheMail_Scripts=s[sBEGINs{0,}((?s)[sS]*?)(?=s{1,}ENDs]) | 1015862  | ~474ms |
| BodyOftheMail_Scripts=s[sBEGINs{0,}((?s)[wW]*?)(?=s{1,}ENDs]) | 1015862  | ~480ms |
| BodyOftheMail_Scripts=s[sBEGINs{0,}((?s).*?)(?=s{1,}ENDs])      | 1015862  | ~577ms |
| BodyOftheMail_Scripts=s[sBEGINs{0,}((.|n)*?)(?=s{1,}ENDs])   | 1681711  | ~829ms |
+-----------------------------------------------------------------------+----------+--------+

有没有更快的方法(更优化的正则表达式(来匹配这一点?

增强模式

5 个表达式中最有效的结果是

BodyOftheMail_Scripts=s[sBEGINs*(S*(?:s++(?!ENDs])S*)*)s+ENDs]

查看正则表达式演示

我修改的部分是S*(?:s++(?!ENDs])S*)*

  • S*- 0 个或多个非空格字符
  • (?:s++(?!ENDs])S*)*- 0 次或多次出现s++(?!ENDs])- 1+ 空格
    • 字符(所有匹配,以便在所有 1+ 空格匹配后只能执行一次前瞻检查(后不跟END、1 空格和]个字符
    • S*- 0 个或多个非空格字符

为什么不仅仅re.DOTALLBodyOftheMail_Scripts=s[sBEGINs*(.*?)s+ENDs]s*(.*?)s+ENDs]将按如下方式工作:0+ 空格将立即匹配,然后第一次跳过(.*?),然后尝试s+ENDs]模式。如果s+ENDs]不匹配,.*?将获取一个字符,并再次让后续模式尝试匹配字符串。等等。可能需要很多回溯步骤才能到达比赛结束(如果它在那里,否则,它可能会迟早以超时结束(。

性能比较

由于 regex101.com 的步骤数不能直接证明某种模式比另一种模式更有效,因此我决定使用 Python PyPi 正则表达式库运行性能测试。请参阅下面的代码。

在具有 16GB RAM、英特尔酷睿 i5-9400F CPU 的 PC 上获得的结果,使用 PyPi 正则表达式版本 2.5.77 和 2.5.82 获得一致的结果:

┌──────────┬─────────────────────────────────────────────────────────────────┐
│   Regex  │  Time taken                                                     │
├──────────┼─────────────────────────────────────────────────────────────────┤
│   OP 1   │  0.5606743000000001                                             │
│   OP 2   │  0.5524994999999999                                             │
│   OP 3   │  0.5026944                                                      │
│   OP 4   │  0.7502984000000001                                             │
│   WS_1   │  0.25729479999999993                                            │
│   WS_2   │  0.3680949                                                      │ 
└──────────┴─────────────────────────────────────────────────────────────────┘

结论

  • 最糟糕的 OP 正则表达式是包含臭名昭著的(.|n)*?模式的正则表达式,它是我在我的正则表达式生活中见过的最低效的模式之一,它总是会导致所有语言的问题。请不要在您的图案中使用它
  • 前三种 OP 模式具有可比性,但很明显,如果有一种方法可以使任何字符与修饰符(例如(?s)regex.DOTALL(匹配任何字符,..匹配任何字符、[wW][sS]的常见解决方法。(?s).原生解决方案的效率要高一些。
  • 我的建议似乎是将编码速度提高一倍到最佳 OP 模式,因为它以块的形式匹配从左侧分隔符到右侧分隔符的字符串,只有在抓取文本的空格块和它们后面的空格后才停下来检查右侧分隔符。
  • 每次 char 不是右边分隔符的开头时,.*?结构就会扩展,字符串越长,其效率就会降低。

Python 测试代码

import regex, timeit
text = 'BodyOftheMail_Script = [ BEGIN  some textnhere andnhere, too       nEND ]'
regex_pattern_1=regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs{0,}((?s)[sS]*?)(?=s{1,}ENDs])')
regex_pattern_2=regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs{0,}((?s)[wW]*?)(?=s{1,}ENDs])')
regex_pattern_3=regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs{0,}((?s).*?)(?=s{1,}ENDs])')
regex_pattern_4=regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs{0,}((.|n)*?)(?=s{1,}ENDs])')
regex_pattern_WS_1=regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs*(S*(?:s++(?!ENDs])S*)*)s+ENDs]')
regexp_patternWS_2 = regex.compile(r'BodyOftheMail_Scripts=s[sBEGINs*(.*?)s+ENDs]', regex.DOTALL)
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regex_pattern_1 as p', number=100000))
# => 0.5606743000000001
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regex_pattern_2 as p', number=100000))
# => 0.5524994999999999
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regex_pattern_3 as p', number=100000))
# => 0.5026944
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regex_pattern_4 as p', number=100000))
# => 0.7502984000000001
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regex_pattern_WS_1 as p', number=100000))
# => 0.25729479999999993
print(timeit.timeit("p.findall(text)", 'from __main__ import text, regexp_patternWS_2 as p', number=100000))
# => 0.3680949

除非您在问题中遗漏了一些重要细节,否则我认为没有任何理由使事情过于复杂。为什么不使用简单的BodyOftheMail_Script = [ BEGIN.*?END ]?所以你有你的开始指示器BodyOftheMail_Script = [ BEGIN,你有结束指示器END ],并且你想以非贪婪的方式匹配两者之间的所有内容.*?。当然,它需要像re.MULTILINEre.DOTALL这样的标志(如果我们谈论的是 Python(:

import re
regexp = re.compile(r'BodyOftheMail_Script = [ BEGIN.*?END ]', re.DOTALL | re.MULTILINE)

正则表达式的第一条规则 - 不要使;)过于复杂有人会在你之后阅读它。

使用与@Wictor答案相同的比较脚本,我得到了以下结果:

OP 1 0.24152620000000002
OP 2 0.28501820000000005
OP 3 0.20582650000000002
OP 4 0.3379188999999999
WS 0.16937669999999994
Subj 0.10387990000000014

替换为CC_32是可能的,它并没有真正改变速度(但是如果您在实际文件中只有空间,那么只需使用空间,不要过于复杂(

此外,如果您愿意,您可以添加组以直接获取内容,它为我添加了~0.02s,之后修剪每个结果而不是使用正则表达式组可能会更快。

最新更新