高效地查询一个字符串与多个正则表达式的匹配

55
假设我有10,000个正则表达式和一个字符串,我想找出它是否与它们中的任何一个匹配,并获取所有匹配结果。传统的方法是逐个查询该字符串对所有正则表达式进行匹配。是否有更快、更高效的方法呢?
编辑: 我尝试使用DFA(lex)进行替换 这里的问题是它只会给您一个单一的模式。如果我有一个字符串“hello”和模式“[H|h]ello”和“.{0,20}ello”,DFA只会匹配其中一个,但我希望两个都匹配上。

通常情况下,人们会尽量避免歧义,因此有一些标准可以选择匹配的模式。通常选择最长的最左匹配。如果您有这个需求,您可能需要自己编写一些内容。 - Remo.D
1
在回答相关问题时发现了这个。由于这个问题是在'08年提出的,我已经发布了一个开源Java库,您可以使用它来构建DFA,如果需要,它将为您提供所有匹配的表达式:http://mtimmerm.github.io/dfalex/。 - Matt Timmermans
18个回答

13

词法分析器的工作方式如下:

正则表达式将被转换为单个非确定性自动机(NFA),并可能转换为确定性自动机(DFA)。

生成的自动机将尝试一次性匹配所有正则表达式,并将在其中一个匹配成功。

有许多工具可以帮助您完成这项任务,它们被称为“词法分析器生成器”,并且有适用于大多数编程语言的解决方案。

您没有说明使用的是哪种编程语言。对于C程序员,我建议看看re2c工具。当然,传统的(f)lex永远是一种选择。


自动生成一个词法分析器文件就可以完成工作了 - 如果你选择这条路,希望你是你的构建系统的专家;-) - ijw
很好的观察!我支持这个。我相信有一种更简单的方法可以重复使用词法分析器代码,而不必生成源文件。 - Ramon

13

我曾经遇到过类似的问题。我使用了与akdom建议的解决方案类似的解决方案。

我很幸运,因为我的正则表达式通常都有一些子字符串,这些子字符串必须出现在它所匹配的每个字符串中。我能够使用一个简单的解析器提取这些子字符串,并使用Aho-Corasick算法将它们索引到FSA中。然后使用该索引快速消除所有在给定字符串上无法匹配的正则表达式,只留下少数几个需要检查的正则表达式。

我以Python/C模块的形式在LGPL下发布了这些代码。请参见Google代码托管上的esmre


4
看起来你已经把代码移到了GitHub上:https://github.com/wharris/esmre。我只是想让大家知道一下 ;) - blong

11
我们曾经在我所工作的一个产品中做过这个。答案是将所有正则表达式编译到一起形成一个确定性有穷状态自动机(也称为确定性有限自动机或DFA)。然后可以逐个字符地在字符串上遍历DFA,并在任何一个表达式匹配时触发“匹配”事件。
优点是它运行速度快(每个字符只比较一次),并且如果添加更多表达式也不会变慢。
缺点是需要一个巨大的数据表来存储自动机,并且不支持许多类型的正则表达式(例如,反向引用)。
我们使用的是当时我们公司的C++模板专家手动编写的,所以很遗憾我没有任何FOSS解决方案可以指给你。但是如果您在Google中搜索regex或regular expression与“DFA”一起,就会找到一些可以指导你的东西。

一个有限状态自动机(FSA)绝对是正确的选择! - Eamon Nerbonne
这已经是我们实现过的了,但不幸的是你提到的缺点是我们寻找不同解决方案的原因。 - Sridhar Iyer

10

Martin Sulzmann在这个领域做了很多工作。他有一个HackageDB项目,简要说明可以在这里找到,其中使用部分导数似乎是为此量身定制的。

所使用的语言是Haskell,并且如果需要转换为非函数式语言,则将非常困难(我认为转换为许多其他FP语言仍然会非常困难)。

代码不是基于将正则表达式转换为一系列自动机,然后将它们组合起来,而是基于对正则表达式本身进行符号操作。

此外,代码非常实验性,Martin不再是教授,但已经有“盈利就业”(1),因此可能不感兴趣或无法提供任何帮助或输入。


  1. 这只是一个玩笑 - 我喜欢教授,越少聪明的人尝试工作,我赚钱的机会就越大!

1
我接受这个答案,因为我已经尝试了所有其他途径并失败了(是的,我真的实现了所有其他解决方案,并发现它们在许多方面都不足)。我已经开始将该库从Haskell移植到C++..可能稍后会开源。 这可能以后并不会真正奏效,但这似乎很有前途且理论上可行。 - Sridhar Iyer
1
祝你把代码移植成功,我看它一定会很受欢迎!如果有进展请告诉我们。 - ShuggyCoUk
2
@shridhar lyer:有什么进展的想法吗?我面临着类似的情况。 - Toad

8

有10,000个正则表达式?Eric Wendelin建议使用层次结构,这似乎是个好主意。你是否考虑将这些正则表达式的数量减少到类似于树形结构的形式?

举个简单的例子:所有需要数字的正则表达式可以从一个检查数字的正则表达式分支出来,不需要数字的则从另一个分支出来。通过这种方式,您可以将实际比较的数量减少到树上的路径,而不是在10,000个正则表达式中进行每一个比较。

这将需要将提供的正则表达式分解成流派,每个流派都有一个共享测试,如果测试失败,则会排除它们。通过这种方式,您可以理论上大大减少实际比较的数量。

如果您必须在运行时执行此操作,则可以解析给定的正则表达式并将其“归档”到预定义的流派(最容易做到)或在那一刻生成的比较流派(不太容易做到)中。

您比较"hello"和"[H|h]ello"以及".{0,20}ello"的例子不会受到此解决方案的帮助。这种解决方案有用的一个简单情况是:如果您有1000个测试,只有在字符串中存在"ello"时才返回true,并且您的测试字符串是"goodbye;",那么您只需要进行一次关于"ello"的测试,就知道需要它的1000个测试不起作用,因此您不必执行它们。


有没有可以自动完成这个任务的库?这个任务无法手动处理。正则表达式并非硬编码。 - Sridhar Iyer
我不知道有任何库可以做到这一点,但你可以编写一个解析正则表达式并将你要测试的案例“归档”的程序。即使你只能做到非常粗略的筛选,也可能通过排除大量不匹配情况来显著缩短执行时间。 - akdom
2
esmre [http://code.google.com/p/esmre/] 是一个能够自动完成类似任务的 Python/C 库。 - Will Harris
+1:非常喜欢这个层次结构的想法。当你处理这些相对较大的数字时,很可能可以以某种方式对它们进行分类。 - Marc
通过构建“树”结构,您基本上已经迈出了实现DFA / NFA的一步。最好使用真正的(极快速)DFA / NFA匹配器,而不是拥有两个世界的复杂性。 - Eamon Nerbonne
由于我之前遇到过完全相同的问题,如果您不能担保正则表达式的“安全”,那么使用PCRE可能会导致特定的正则表达式表现极差; 如果它们是像Perl(或.NET等)那样实现的,那么10000个正则表达式的联合很可能是不稳定的(性能方面)。 - Eamon Nerbonne

6
Aho-Corasick 对我来说是个不错的选择。
我有2000个类别的物品,每个类别都有匹配模式的列表。字符串平均长度约为100,000个字符。
主要注意事项:要匹配的模式都是语言模式,而不是正则表达式模式,例如'cat'和r'\w+'。
我使用的是Python,因此使用了https://pypi.python.org/pypi/pyahocorasick/
import ahocorasick
A = ahocorasick.Automaton()

patterns = [
  [['cat','dog'],'mammals'],
  [['bass','tuna','trout'],'fish'],
  [['toad','crocodile'],'amphibians'],
]

for row in patterns:
    vals = row[0]
    for val in vals:
        A.add_word(val, (row[1], val))

A.make_automaton()

_string = 'tom loves lions tigers cats and bass'

def test():
  vals = []
  for item in A.iter(_string):
      vals.append(item)
  return vals

在我的2000个类别中,每个类别大约有2-3个跟踪项,并且_string长度约为100,000,在其中运行%timeit test(),我得到了2.09毫秒的结果,而使用顺序的re.search()则需要631毫秒,速度提升了315倍!


非常正确!Aho-Corasick无疑是经过考验和实战的解决方案。而@will-harris的esmre也是基于这个强大的数据结构。 - Philippe Ombredanne

6
如果你想到“10,000个正则表达式”,那么你需要转变思维方式。至少,应该考虑“10,000个目标字符串匹配”。然后寻找非正则表达式方法来处理“大量目标字符串”情况,例如Aho-Corasick机器。但说实话,似乎在过程的早期就出了问题,因为10,000个目标字符串听起来更像是数据库查找而不是字符串匹配。

5

你需要有一种方法来确定给定的正则表达式是否与另一个正则表达式“加法”相比。 创建一种正则表达式的“等级制度”,使您能够确定某个分支的所有正则表达式都不匹配。


4

您可以将它们分成大约20个组合。

(?=(regex1)?)(?=(regex2)?)(?=(regex3)?)...(?=(regex20)?)

只要每个正则表达式都有零个(或至少相同数量的)捕获组,您可以查看捕获到的内容以确定匹配了哪些模式。
如果regex1匹配成功,则捕获组1将具有其匹配的文本。否则,它将是undefined/None/null/...。

4

如果您正在使用真正的正则表达式(对应于形式语言理论中的正则语言,而不是某些类似于Perl的非正则内容),那么您很幸运,因为正则语言在并集下是封闭的。 在大多数正则表达式语言中,管道(|)表示并集。 因此,您应该能够按照以下方式构建一个字符串(表示所需的正则表达式):

(r1)|(r2)|(r3)|...|(r10000)

括号用于分组而不是匹配。任何与此正则表达式匹配的内容都至少匹配一个原始正则表达式。


我希望真实世界的编程语言中会有这样的正则表达式实现。不幸的是,Perl 搞砸了,其他所有人都复制了它们...所以这在几乎任何正则表达式引擎中都行不通。 - Eamon Nerbonne
如果你这样做了,那么有没有一种有效的方法来提取匹配的正则表达式呢?想象一下,你必须遍历所有10000个组,寻找一个非零的组... - Joseph Garvin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接