在Python中匹配两个正则表达式
在Python中,有办法同时匹配两个正则表达式吗?
举个例子,我有一个场景需要比较两个表达式,像这样:
re.match('google\.com\/maps', 'google\.com\/maps2', re.IGNORECASE)
我希望能得到一个正则表达式对象。
但显然,Python在第二个参数上期待的是一个字符串。有没有办法做到这一点,还是说这是正则匹配的一个限制呢?
背景:我有一个正则表达式的列表 [r1, r2, r3, ...],这些表达式可以匹配一个字符串,我需要找出哪个表达式对给定字符串的匹配最具体。我原本想的办法是:
(1) 先用 r1 去匹配 r2。
(2) 然后再用 r2 去匹配 r1。
如果两个都匹配,那就是“平局”。如果只有(1)匹配成功,那 r1 就比 r2 更好,反之亦然。
我会对整个列表进行这样的循环。
我承认这个想法有点复杂(主要是因为我的描述可能不太清楚),但如果有人能给我一些建议,告诉我怎么实现这一点,我会非常感激。谢谢!
8 个回答
选项 1:
既然用户提供了正则表达式,或许可以让他们也提交一些测试字符串,这些字符串能展示他们的正则表达式的具体性。(也就是说,能证明他们的正则表达式比竞争对手的更具体。)收集所有用户提交的测试字符串,然后用这些字符串来测试所有的正则表达式。
要设计一个好的正则表达式,作者肯定考虑过哪些字符串能匹配,哪些不能,所以他们应该能轻松提供出好的测试字符串。
选项 2:
你可以尝试一种蒙特卡罗方法:从两个正则表达式都能匹配的字符串开始,写一个生成器来生成这个字符串的变体(比如改变字符顺序、添加或删除字符等)。如果在每个变体上两个正则表达式的匹配结果都一样,那么这两个正则表达式“可能相当”。如果一个正则表达式能匹配某个变体,而另一个不能,反之亦然,那么它们“绝对相当”。
但是如果一个正则表达式能匹配更多的变体,那么它“可能不够具体”相对于另一个。
经过大量变体测试后的结果可能并不总是正确,但通常是合理的。
选项 3:
使用 ipermute 或 pyParsing 的 invert 来生成匹配每个正则表达式的字符串。这种方法只适用于使用有限正则语法的正则表达式。
这要看你用的正则表达式是什么样的;就像@carrot-top说的,如果你处理的根本不是计算机科学意义上的“正则表达式”,而是一些复杂的扩展,那你就很难办了。
不过,如果你用的是传统的正则表达式,可能会有一些进展。首先,我们可以定义一下什么是“更具体”。假设R是一个正则表达式,L(R)是由R生成的语言。我们可以说R1比R2更具体,如果L(R1)是L(R2)的一个(严格)子集(也就是L(R1) < L(R2))。但这也只能说明到这里:在很多情况下,L(R1)既不是L(R2)的子集,也不是超集,所以我们可能会觉得这两个是无法比较的。例如,当我们尝试匹配“mary had a little lamb”时,可能会找到两个匹配的表达式:.*mary
和lamb.*
。
一个不模糊的解决方案是通过实现来定义具体性。比如,可以把你的正则表达式以确定的(实现定义的)方式转换成一个DFA(确定性有限自动机),然后简单地计算状态数。不幸的是,这对用户来说可能比较难以理解。
实际上,你似乎对如何比较两个正则表达式的具体性有一种直观的想法。为什么不简单地写下一个基于正则表达式语法的具体性定义,让它与你的直觉相符呢?
接下来是一些完全任意的规则:
- 字符 =
1
。 - 字符范围包含
n
个字符 =n
(我们假设\b
=5
,因为我不确定你会怎么写)。 - 锚点每个是
5
。 *
把它的参数除以2
。+
把它的参数除以2
,然后加1
。.
=-10
。
总之,这只是一些思考的材料,其他答案很好地概述了你面临的一些问题;希望对你有帮助。
除了对 re.match
语法的澄清外,我觉得你在处理两个或多个未知的(用户输入的)正则表达式时,可能在努力判断哪个表达式对某个字符串的匹配更“具体”。
先想一下,Python 的正则表达式实际上是一种计算机程序。现代的正则表达式形式,包括 Python 的正则表达式,都是基于 Perl 的。Perl 的正则表达式有递归、回溯等复杂的特性,这些特性并不是简单的检查就能看出来的。实际上,一个恶意的正则表达式可以被用作一种 拒绝服务攻击。
你可以在自己的电脑上试试这个:
>>> re.match(r'^(a+)+$','a'*24+'!')
在我的电脑上,这个大约需要 1 秒钟。现在把 'a'*24
中的 24
增加到一个稍大的数字,比如 28
。这个会花更长的时间。再试试 48
... 你可能现在需要按 CTRL+C 了。随着 a 的数量增加,所需的时间实际上是指数级增长的。
你可以在 Russ Cox 的精彩论文中了解更多关于这个问题的内容,论文标题是 '正则表达式匹配可以简单且快速'。Russ Cox 是谷歌的一名工程师,他在 2006 年构建了 Google 代码搜索。正如 Cox 所观察到的,考虑用 awk 和 Perl(或 Python、PCRE、Java、PHP 等)匹配正则表达式 'a?'*33 + 'a'*33
和字符串 'a'*99
。awk 匹配只需 200 微秒,但 Perl 需要 1015 年,这是因为指数回溯的原因。
所以结论是:这要看情况! 你所说的 更具体 的匹配是什么意思呢?可以看看 Cox 的一些正则表达式简化技术,详细信息在 RE2 中。如果你的项目足够大,可以编写自己的库(或使用 RE2),并且你愿意限制使用的正则表达式语法(即,不使用回溯或递归形式),我认为你可以用多种方式来判断“更好的匹配”。
如果你想用简单的方式表示(regex_3 < regex_1 < regex_2),在用 Python 或 Perl 的正则表达式语言匹配某个字符串时,我认为答案是这非常非常困难(即,这个问题是 NP 完全的)。
编辑
我上面说的一切都是真的! 不过,这里有一个尝试,根据某种“具体”的形式对匹配的正则表达式进行排序:从正则表达式到字符串需要多少次编辑。编辑次数越多(或者 Levenshtein 距离越高),正则表达式就越不“具体”。
你来判断这是否有效(我不知道“具体”对你的应用来说意味着什么):
import re
def ld(a,b):
"Calculates the Levenshtein distance between a and b."
n, m = len(a), len(b)
if n > m:
# Make sure n <= m, to use O(min(n,m)) space
a,b = b,a
n,m = m,n
current = range(n+1)
for i in range(1,m+1):
previous, current = current, [i]+[0]*n
for j in range(1,n+1):
add, delete = previous[j]+1, current[j-1]+1
change = previous[j-1]
if a[j-1] != b[i-1]:
change = change + 1
current[j] = min(add, delete, change)
return current[n]
s='Mary had a little lamb'
d={}
regs=[r'.*', r'Mary', r'lamb', r'little lamb', r'.*little lamb',r'\b\w+mb',
r'Mary.*little lamb',r'.*[lL]ittle [Ll]amb',r'\blittle\b',s,r'little']
for reg in regs:
m=re.search(reg,s)
if m:
print "'%s' matches '%s' with sub group '%s'" % (reg, s, m.group(0))
ld1=ld(reg,m.group(0))
ld2=ld(m.group(0),s)
score=max(ld1,ld2)
print " %i edits regex->match(0), %i edits match(0)->s" % (ld1,ld2)
print " score: ", score
d[reg]=score
print
else:
print "'%s' does not match '%s'" % (reg, s)
print " ===== %s ===== === %s ===" % ('RegEx'.center(10),'Score'.center(10))
for key, value in sorted(d.iteritems(), key=lambda (k,v): (v,k)):
print " %22s %5s" % (key, value)
这个程序正在对一系列正则表达式与字符串 Mary had a little lamb
进行匹配。
以下是从“最具体”到“最不具体”的排序结果:
===== RegEx ===== === Score ===
Mary had a little lamb 0
Mary.*little lamb 7
.*little lamb 11
little lamb 11
.*[lL]ittle [Ll]amb 15
\blittle\b 16
little 16
Mary 18
\b\w+mb 18
lamb 18
.* 22
这基于一个(也许是简单的)假设:a) 从正则表达式本身到匹配的子字符串所需的编辑次数(Levenshtein 距离)是通配符扩展或替换的结果;b) 从匹配的子字符串到初始字符串的编辑次数。(只取一次)
举两个简单的例子:
.*
(或.*.*
或.*?.*
等)与任何字符串匹配时,需要进行大量编辑才能得到字符串,实际上等于字符串的长度。这是可能的最大编辑次数,得分最高,且是最不“具体”的正则表达式。- 字符串本身的正则表达式与字符串匹配时是最具体的。没有编辑将一个变成另一个,得分为 0 或最低。
如前所述,这种方法比较简单。锚点应该增加具体性,但在这种情况下并没有。非常短的字符串不适用,因为通配符可能比字符串长。
编辑 2
我使用未记录的 sre_parse
模块在 Python 中成功实现了锚点解析。如果你想了解更多,可以输入 >>> help(sre_parse)
...
这是支撑 re
模块的工作模块。自 2001 年以来,它已包含在每个 Python 发行版中,包括所有 P3k 版本。它 可能 会消失,但我认为这不太可能...
这是修订后的列表:
import re
import sre_parse
def ld(a,b):
"Calculates the Levenshtein distance between a and b."
n, m = len(a), len(b)
if n > m:
# Make sure n <= m, to use O(min(n,m)) space
a,b = b,a
n,m = m,n
current = range(n+1)
for i in range(1,m+1):
previous, current = current, [i]+[0]*n
for j in range(1,n+1):
add, delete = previous[j]+1, current[j-1]+1
change = previous[j-1]
if a[j-1] != b[i-1]:
change = change + 1
current[j] = min(add, delete, change)
return current[n]
s='Mary had a little lamb'
d={}
regs=[r'.*', r'Mary', r'lamb', r'little lamb', r'.*little lamb',r'\b\w+mb',
r'Mary.*little lamb',r'.*[lL]ittle [Ll]amb',r'\blittle\b',s,r'little',
r'^.*lamb',r'.*.*.*b',r'.*?.*',r'.*\b[lL]ittle\b \b[Ll]amb',
r'.*\blittle\b \blamb$','^'+s+'$']
for reg in regs:
m=re.search(reg,s)
if m:
ld1=ld(reg,m.group(0))
ld2=ld(m.group(0),s)
score=max(ld1,ld2)
for t, v in sre_parse.parse(reg):
if t=='at': # anchor...
if v=='at_beginning' or 'at_end':
score-=1 # ^ or $, adj 1 edit
if v=='at_boundary': # all other anchors are 2 char
score-=2
d[reg]=score
else:
print "'%s' does not match '%s'" % (reg, s)
print
print " ===== %s ===== === %s ===" % ('RegEx'.center(15),'Score'.center(10))
for key, value in sorted(d.iteritems(), key=lambda (k,v): (v,k)):
print " %27s %5s" % (key, value)
以及排序后的正则表达式:
===== RegEx ===== === Score ===
Mary had a little lamb 0
^Mary had a little lamb$ 0
.*\blittle\b \blamb$ 6
Mary.*little lamb 7
.*\b[lL]ittle\b \b[Ll]amb 10
\blittle\b 10
.*little lamb 11
little lamb 11
.*[lL]ittle [Ll]amb 15
\b\w+mb 15
little 16
^.*lamb 17
Mary 18
lamb 18
.*.*.*b 21
.* 22
.*?.* 22