python: 如何中断正则匹配

7 投票
4 回答
2998 浏览
提问于 2025-04-16 13:39

我在处理大量下载的文本文件时,会逐行检查每一行的内容,看看是否符合某种模式。通常情况下,这个检查只需要不到一秒钟的时间。但是,有时候这个检查会花费几分钟,甚至有时候根本无法完成,代码就会卡住(我等过一个小时几次,最后只好放弃)。所以,我需要设置一个超时机制,让这个检查在大约10秒后停止。我可以接受失去原本应该返回的数据。

我尝试了以下方法(这实际上是两种不同的基于线程的解决方案,放在一个代码示例中):

def timeout_handler():
    print 'timeout_handler called'

if __name__ == '__main__':
    timer_thread = Timer(8.0, timeout_handler)
    parse_thread = Thread(target=parse_data_files, args=(my_args))
    timer_thread.start()
    parse_thread.start()
    parse_thread.join(12.0)
    print 'do we ever get here ?'

但我既没有看到 timeout_handler called 这一行,也没有看到 do we ever get here ? 这一行,代码就是卡在 parse_data_files 里。

更糟糕的是,我甚至无法用 CTRL-C 停止程序,而是需要查找 Python 进程号并杀掉那个进程。经过一些研究,我发现 Python 的开发者们已经意识到正则表达式的 C 代码会出现问题:http://bugs.python.org/issue846388

我使用信号机制取得了一些成功:

signal(SIGALRM, timeout_handler)
alarm(8)
data_sets = parse_data_files(config(), data_provider)
alarm(0)

这样我可以在输出中看到 timeout_handler called 这一行,并且我仍然可以用 CTRL-C 停止我的脚本。如果我现在像这样修改 timeout_handler

class TimeoutException(Exception): 
    pass 

def timeout_handler(signum, frame):
    raise TimeoutException()

并把实际调用 re.match(...) 的部分放在 try ... except TimeoutException 语句中,正则表达式的检查确实会被中断。不幸的是,这种方法只在我用来测试的简单单线程脚本中有效。这个解决方案有几个问题:

  • 信号只会触发一次,如果有多行出现问题,我就会卡在第二行上
  • 计时器从那里开始计时,而不是在实际解析开始时
  • 由于全局解释器锁(GIL),我必须在主线程中设置所有信号,而信号只在主线程中接收;这与我想同时在多个线程中解析多个文件的目标相冲突 - 只有一个全局的超时异常被抛出,我不知道该在哪个线程中做出反应
  • 我已经多次读到线程和信号的配合并不好

我也考虑过在一个单独的进程中进行正则表达式的检查,但在深入研究之前,我想先在这里看看是否有人遇到过这个问题,并能给我一些解决的建议。

更新

这个正则表达式看起来是这样的(嗯,至少是其中一个,其他的正则表达式也会出现问题;这是最简单的一个):

'^(\d{5}), .+?, (\d{8}), (\d{4}), .+?, .+?,' + 37 * ' (.*?),' + ' (.*?)$'

示例数据:

95756, "KURN ", 20110311, 2130, -34.00, 151.21, 260, 06.0, -9999.0, -9999.0, -9999.0, -9999.0, -9999.0, -9999, -9999, 07.0, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -

如前所述,这个正则表达式通常表现得还不错 - 我可以在不到一分钟的时间内解析几百个文件,每个文件有几百行。不过,这些文件必须是完整的 - 当文件中有不完整的行时,代码似乎就会卡住,例如:

`95142, "YMGD ", 20110311, 1700, -12.06, 134.23, 310, 05.0, 25.8, 23.7, 1004.7, 20.6, 0.0, -9999, -9999, 07.0, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999

我也遇到过正则表达式似乎立即返回并报告不匹配的情况。

更新 2

我只快速浏览了关于灾难性回溯的文章,但据我所知,这并不是原因 - 我没有嵌套任何重复操作符。

我在 Mac OSX 上,所以无法使用 RegexBuddy 来分析我的正则表达式。我尝试了RegExhibit(它似乎在内部使用 Perl 的正则表达式引擎) - 结果也出现了问题。

4 个回答

2

与其尝试通过设置超时来解决正则表达式卡住的问题,不如考虑一种完全不同的方法。如果你的数据确实只是用逗号分隔的值,那么使用csv模块或者直接用line.split(",")会让你的程序运行得更快。

2

你不能用线程来实现这个。可以继续考虑在一个单独的进程中进行匹配。

8

你遇到了一个叫做“灾难性回溯”的问题,这不是因为你用了嵌套的量词,而是因为你用的量词也能匹配到分隔符。而且因为分隔符的数量很多,所以在某些情况下,你的程序会变得非常慢,时间复杂度会呈指数级增长。

除了这个问题看起来更像是一个CSV解析器的工作之外,你可以尝试以下方法:

r'^(\d{5}), [^,]+, (\d{8}), (\d{4}), [^,]+, [^,]+,' + 37 * r' ([^,]+),' + r' ([^,]+)$'

通过明确禁止逗号在分隔符之间匹配,你的正则表达式会变得快很多。

如果逗号可能出现在引号包裹的字符串里面,比如说,你可以把[^,]+(在你预期的地方)换成

(?:"[^"]*"|[^,]+)

举个例子:

用你的正则表达式去处理第一个例子时,RegexBuddy报告说经过793步正则引擎的处理后成功匹配。而对于第二个(不完整的行)例子,它报告在经过1,000,000步后匹配失败(这时候RegexBuddy放弃了;而Python会继续处理下去)。

使用我的正则表达式,成功匹配只需要173步,失败则在174步。

撰写回答