正则表达式 re.findall() 挂起 - 如果您无法逐行阅读怎么办

Posted

技术标签:

【中文标题】正则表达式 re.findall() 挂起 - 如果您无法逐行阅读怎么办【英文标题】:Regex re.findall() hangs - What if you cant read line by line 【发布时间】:2013-04-04 20:08:46 【问题描述】:

我有多个文件,每个文件都在搜索一系列单词。

我的正则表达式基本上搜索 word1 后跟 word2 后跟 word 3 等的序列。 所以表达式看起来像:

strings = re.findall('word1.*?word2.*?word3', f.read(), re.DOTALL)

对于小于 20kb 的文件,表达式执行得很好。但是,对于大于 20 kb 的文件,执行时间会成倍增加,并且对于接近 100 kb 的文件,进程会完全挂起。 看来(在阅读了以前的线程之后)问题与使用 .* 和 re.DOTALL 有关 - 导致“灾难性回溯”。推荐的解决方案是逐行提供输入文件,而不是将整个文件读入单个内存缓冲区。

但是,我的输入文件充满了随机空格和“\n”换行符。我的单词序列也很长,并且出现在多行中。因此,我需要将整个文件连同 re.DOTALL 一起输入到正则表达式中 - 否则逐行搜索将永远找不到我的序列。

有什么办法吗?

【问题讨论】:

尝试在 f.readlines() 中执行此操作,并逐行正则表达式 @karthikr 如果整个内容都充满了随机空格和换行符,这怎么可能? 我认为 f.read() 是一个文件读取 问题是,如果我逐行对其进行正则表达式,搜索将永远找不到我的序列 - 我的序列不会出现在单行中......但由于存在随机数而仅出现在多行"\n" 个字符 使用纯字符串操作怎么样?获取word1word2word3的索引,然后查看这些索引? 【参考方案1】:

如果您从字面上搜索三个单词的出现,其中根本没有正则表达式模式,则根本不需要使用正则表达式 - 正如@Bart 在我写这个答案时所建议的那样:)。像这样的东西可能会起作用(未经测试,并且可能更漂亮):

with open('...') as f:
    contents = f.read()

words = ['word1', 'word2', 'word3']
matches = []
start_idx = 0
try:
    while True:
        cand = []
        for word in words:
            word_idx = contents.index(word, start_idx)
            cand.append(word_idx)
            start_idx = word_idx + len(word)
        matches.append(cand)
except ValueError:  # from index() failing
    pass

这会将索引放入matches;如果你想要一个与 findall 相同的结果,你可以这样做,比如,

found = [contents[match[0]:match[-1]+len(words[-1]] for match in matches]

您也可以通过将对index 的调用替换为文件上的等效函数来使这种方法工作而无需事先读取整个文件。我不认为 stdlib 包含这样的功能;您可能必须手动对文件对象使用 readline()tell() 或类似方法。

【讨论】:

是的,在这种情况下是比正则表达式更好的选择。 虽然这可能是正确的,但它非常冗长,无法一眼看出它的作用。它可以用作练习,但实际上不应该在此用例的生产环境中使用。 @SergiuToarca,Python 读起来很像伪代码。就个人而言,我会选择这个的(记录的)版本,而不是生产中的正则表达式解决方案。特别是如果核心语言中没有提供该正则表达式引擎,我会使用内置的find。当然,恕我直言。 @SergiuToarca 基于 NFA 的正则表达式很棒,我为您的答案 +1,但有时您无法添加依赖项,或者不值得。我还认为 15 行相当简单的代码用于不平凡的任务加上一些 cmets 不算“非常冗长”。 :) 同意,但我仍然认为它过于冗长:p【参考方案2】:

发生这种情况的原因是因为 python 的正则表达式引擎使用回溯。在每个.*,如果没有找到下面的单词,引擎必须一直走到字符串的末尾(100kb)然后回溯。现在考虑如果在最后一场比赛之后有许多“几乎匹配”会发生什么。引擎不断从匹配开始到字符串结尾来回跳跃。

您可以使用基于 NFA 而不是回溯的正则表达式引擎来修复它。请注意,这限制了您可以使用的正则表达式的种类(没有回溯或任意零宽度断言),但它适合您的用例。

你可以找到这样的引擎here。您可以在 www.debuggex.com 了解 nfa 引擎的工作原理。

【讨论】:

在最坏的情况下,整个文件没有匹配项,所以是的。 我使用了“当它没有找到以下单词时”这样的措辞,但我会将其更改为更清晰。 只有当它到达单词的末尾时才会失败,然后回溯到它可以采取的任何其他路径,最终到达比赛的开头。然后它将从那里继续,直到找到另一个“比赛开始”。想想如果我的文件结尾看起来像 word1 word1 word1 会发生什么。 不,它只会在尝试从每个可能的起始位置匹配后才会失败。 是的,再想一想,在阅读了您编辑的答案(许多“几乎匹配”-部分)之后,我认为您是对的。我已经提到过,但我再说一遍:整洁的工具!【参考方案3】:

您可以使用循环一次搜索一个单词。我在这里使用str.find(),因为它对于简单的子字符串搜索更快,但您也可以修改此代码以使用re.search()

def findstrings(text, words):
    end = 0
    while True:
        start = None
        for word in words:
            pos = text.find(word, end) #starts from position end
            if pos < 0:
                return
            if start is None:
                start = pos
            end = pos + len(word)
        yield text[start:end]


#usage in place of re.findall('word1.*?word2.*?word3', f.read(), re.DOTALL)
list(findstrings(f.read(), ['word1', 'word2', 'word3']))

【讨论】:

以上是关于正则表达式 re.findall() 挂起 - 如果您无法逐行阅读怎么办的主要内容,如果未能解决你的问题,请参考以下文章

python 正则(re.compile()/re.findall())

正则表达式re.findall

正则表达式 re.search 和 re.findall 的区别

使用 re.findall 在正则表达式中捕获命名组

是否有 Python 的 re.findall/re.finditer(迭代正则表达式结果)的 Perl 等价物?

Python之正则re模块 --- findall()详解