在Python中匹配两个正则表达式

13 投票
8 回答
9829 浏览
提问于 2025-04-17 02:32

在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

选项 1:

既然用户提供了正则表达式,或许可以让他们也提交一些测试字符串,这些字符串能展示他们的正则表达式的具体性。(也就是说,能证明他们的正则表达式比竞争对手的更具体。)收集所有用户提交的测试字符串,然后用这些字符串来测试所有的正则表达式。

要设计一个好的正则表达式,作者肯定考虑过哪些字符串能匹配,哪些不能,所以他们应该能轻松提供出好的测试字符串。


选项 2:

你可以尝试一种蒙特卡罗方法:从两个正则表达式都能匹配的字符串开始,写一个生成器来生成这个字符串的变体(比如改变字符顺序、添加或删除字符等)。如果在每个变体上两个正则表达式的匹配结果都一样,那么这两个正则表达式“可能相当”。如果一个正则表达式能匹配某个变体,而另一个不能,反之亦然,那么它们“绝对相当”。

但是如果一个正则表达式能匹配更多的变体,那么它“可能不够具体”相对于另一个。

经过大量变体测试后的结果可能并不总是正确,但通常是合理的。


选项 3:

使用 ipermute 或 pyParsing 的 invert 来生成匹配每个正则表达式的字符串。这种方法只适用于使用有限正则语法的正则表达式。

2

这要看你用的正则表达式是什么样的;就像@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”时,可能会找到两个匹配的表达式:.*marylamb.*

一个不模糊的解决方案是通过实现来定义具体性。比如,可以把你的正则表达式以确定的(实现定义的)方式转换成一个DFA(确定性有限自动机),然后简单地计算状态数。不幸的是,这对用户来说可能比较难以理解。

实际上,你似乎对如何比较两个正则表达式的具体性有一种直观的想法。为什么不简单地写下一个基于正则表达式语法的具体性定义,让它与你的直觉相符呢?

接下来是一些完全任意的规则:

  1. 字符 = 1
  2. 字符范围包含n个字符 = n(我们假设\b = 5,因为我不确定你会怎么写)。
  3. 锚点每个是5
  4. *把它的参数除以2
  5. +把它的参数除以2,然后加1
  6. . = -10

总之,这只是一些思考的材料,其他答案很好地概述了你面临的一些问题;希望对你有帮助。

16

除了对 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) 从匹配的子字符串到初始字符串的编辑次数。(只取一次)

举两个简单的例子:

  1. .*(或 .*.*.*?.* 等)与任何字符串匹配时,需要进行大量编辑才能得到字符串,实际上等于字符串的长度。这是可能的最大编辑次数,得分最高,且是最不“具体”的正则表达式。
  2. 字符串本身的正则表达式与字符串匹配时是最具体的。没有编辑将一个变成另一个,得分为 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

撰写回答