部分正则匹配

Jas*_*son 4 python regex

我正在询问 Python 中的部分正则表达式匹配。

例如:

如果你有一个字符串:

string = 'foo bar cat dog elephant barn yarn p n a'
Run Code Online (Sandbox Code Playgroud)

和一个正则表达式:

pattern = r'foo bar cat barn yard p n a f'
Run Code Online (Sandbox Code Playgroud)

以下情况为真:

  • re.match(pattern, string)会回来None
  • re.search(pattern, string) 还会回来 None

虽然我们都可以看到模式的第一部分与字符串的第一部分匹配。

因此,不是在字符串中搜索整个模式,有没有办法查看字符串与模式匹配的百分比?

Ama*_*dan 5

不是正则表达式。

from difflib import SequenceMatcher
SequenceMatcher(None, string, pattern).ratio()
# => 0.7536231884057971
Run Code Online (Sandbox Code Playgroud)

您甚至可以匹配单词而不是字符:

SequenceMatcher(None, string.split(), pattern.split()).ratio()
# => 0.7368421052631579
Run Code Online (Sandbox Code Playgroud)


Tod*_*odd 5

是的,可以进行部分正则表达式匹配

我在玩这个部分匹配的想法,并在我的搜索过程中发现了这个 Q。我找到了一种方法来做我需要的事情,并认为我会在这里发布。

这不是速度恶魔。可能只在速度不是问题的情况下才有用。

此函数找到正则表达式的最佳部分匹配并返回匹配的文本。

>>> def partial_match(regex, string, flags=0, op=re.match):
...     """
...     Matches a regular expression to a string incrementally, retaining the
...     best substring matched.
...     :param regex:   The regular expression to apply to the string.
...     :param string:  The target string.
...     :param flags:   re module flags, e.g.: `re.I`
...     :param op:      Either of re.match (default) or re.search.
...     :return:        The substring of the best partial match.
...     """
...     m = op(regex, string, flags)
...     if m:
...         return m.group(0)
...     final = None
...     for i in range(1, len(regex) + 1):
...         try:
...             m = op(regex[:i], string, flags)
...             if m:
...                 final = m.group(0)
...         except re.error:
...             pass
...     return final
...     
Run Code Online (Sandbox Code Playgroud)

测试一下:

>>> partial_match(r".*l.*?iardvark", "bluebird")
'bluebi'
>>> 
>>> partial_match(r"l.*?iardvark", "bluebird")
>>> # None was returned. Try again with search...
>>> 
>>> partial_match(r"l.*?iardvark", "bluebird", op=re.search)
'luebi'
>>>
>>> string = 'foo bar cat dog elephant barn yarn p n a'
>>> pattern = r'foo bar cat barn yard p n a f'
>>> 
>>> partial_match(pattern, string)
'foo bar cat '
>>> 
>>> partial_match(r".* (zoo){1,3}ran away", "the fox at the "
...                                         "zoozoozoozoozoo is "
...                                         "happy")
'the fox at the zoozoozoo'
Run Code Online (Sandbox Code Playgroud)

行为符合预期。该算法不断尝试将尽可能多的表达式与目标字符串匹配。它一直持续到整个表达式与目标字符串匹配,保留最佳部分匹配。

好吧。现在让我们看看它到底有多慢......

>>> import cProfile as cprof, random as rand, re
>>>
>>> # targets = ['lazy: that# fox~ The; little@ quick! lamb^ dog~ ',
>>> #            << 999 more random strings of random length >>]
>>>
>>> words = """The; quick! brown? fox~ jumped, over. the! lazy: dog~
...            Mary? had. a little- lamb, a& little@ lamb^ {was} she... and,,, 
...            [everywhere] that# Mary* went=, the. "lamb" was; sure() (to) be.
...         """.split()
...
>>> targets = [' '.join(rand.choices(words, k=rand.randint(1, 100))) 
...            for _ in range(1000)]
...
>>> exprs   = ['.*?&', '.*(jumped|and|;)', '.{1,100}[\\.,;&#^]', '.*?!', 
...            '.*?dog. .?lamb.?', '.*?@', 'lamb', 'Mary']
...
>>> partial_match_script = """
... for t in targets:
...     for e in exprs:
...         m = partial_match(e, t)
...         """
...
>>> match_script = """
... for t in targets:
...     for e in exprs:
...         m = re.match(e, t)
...         """
... 
>>> cprof.run(match_script)
         32003 function calls in 0.032 seconds
>>>
>>> cprof.run(partial_match_script)
         261949 function calls (258167 primitive calls) in 0.230 seconds
Run Code Online (Sandbox Code Playgroud)

直接运行它只re.match()需要进行常规匹配并且大部分时间都失败可能不是对函数性能的公平比较。更好的比较将与支持模糊匹配的模块进行比较,我在下面做了这个......而且该功能毕竟没问题。

可以使用re.sre_parse和/或re.sre_compile模块开发更高效的解决方案。看起来所有要经过的文档都在“网络”上的源代码和零碎部分中,例如https://www.programcreek.com/python/example/1434/sre_parse

对于这些模块,我认为可能有一种方法可以通过标记或子表达式而不是像我所做的那样通过单个字符逐步应用正则表达式。

此外,正如有人评论的那样,正则表达式包具有模糊匹配功能(https://pypi.org/project/regex/) - 但它的行为略有不同,并且可能允许部分匹配中出现意外字符。

>>> import regex
>>>
>>> regex.match(r"(?:a.b.c.d){d}", "a.b.c", regex.ENHANCEMATCH).group(0)
'a.b.c'
>>> regex.match(r"(?:moo ow dog cat){d}", "moo cow house car").group(0)
'moo c'
>>> regex.match(r"(?:moo ow dog cat){d}", "moo cow house car", 
...             regex.ENHANCEMATCH).group(0)
...
'moo c'
>>> # ^^ the 'c' above is not what we want in the output. As you can see,
>>> # the 'fuzzy' matching is a bit different from partial matching.
>>>
>>> regex_script = """
... for t in targets:
...     for e in exprs:
...         m = regex.match(rf"(?:{e}){{d}}", t)
...         """
>>>
>>> cprof.run(regex_script)
         57912 function calls (57835 primitive calls) in 0.180 seconds
...
>>> regex_script = """
... for t in targets:
...     for e in exprs:
...         m = regex.match(rf"(?:{e}){{d}}", t, flags=regex.ENHANCEMATCH)
...         """
>>> 
>>> cprof.run(regex_script)
         57904 function calls (57827 primitive calls) in 0.298 seconds
Run Code Online (Sandbox Code Playgroud)

性能比partial_match()没有regex.ENHANCEMATCH标志的解决方案好一点。但是,标志较慢。

带有regex.BESTMATCH标志的正则表达式可能与partial_match()行为最相似,但它甚至更慢:

>>> regex_script = """
... for t in targets:
...     for e in exprs:
...         m = regex.match(rf"(?:{e}){{d}}", t, flags=regex.BESTMATCH)
...         """
>>> cprof.run(regex_script)
         57912 function calls (57835 primitive calls) in 0.338 seconds
Run Code Online (Sandbox Code Playgroud)

regex也有一个partial=True标志,但这似乎根本不像我们期望的那样工作。