正则表达式导致高CPU负载,导致Rails无法响应

时间:2014-11-21 21:35:26

标签: ruby regex ruby-1.8.7

我有一个Ruby 1.8.7脚本来解析iOS本地化文件:

singleline_comment = /\/\/(.*)$/
multiline_comment = /\/\*(.*?)\*\//m
string_line = /\s*"(.*?)"\s*=\s*"(.*?)"\s*\;\s*/xm

out = decoded_src.scan(/(?:#{singleline_comment}|#{multiline_comment})?\s*?#{string_line}/)

过去工作正常,但今天我们用800Kb的文件测试了它,并且在每行的末尾都没有;。结果是高CPU负载并且Rails服务器没有响应。我的假设是它将整个文件作为捕获组中的单个字符串并阻止了服务器。

解决方案是将?(正则表达量词,0或1次)添加到;文字字符中:

  /\s*"(.*?)"\s*=\s*"(.*?)"\s*\;?\s*/xm

现在,即使使用旧iOS格式的文件,它仍能正常工作,但我现在担心的是,如果用户提交格式错误的文件,例如没有结尾"的文件,该怎么办?我的服务器会再次被阻止吗?

我该如何防止这种情况?有没有办法尝试运行这个只有五秒钟?我该怎么做才能避免停止整个Rails应用程序?

1 个答案:

答案 0 :(得分:0)

看起来您正在尝试解析整个配置,就像它是一个字符串一样。虽然这是可行的,但它容易出错。正则表达式引擎必须做很多前瞻和后退,而写得不好的模式最终会浪费大量的CPU时间。有时候一个小小的调整会解决问题,但是处理的文本越多,表达越复杂,发生事情的可能性就越高,这会让你感到困惑。

通过对我自己的工作获取数据的不同方法进行基准测试,我了解到锚定正则表达式模式可以在速度方面产生巨大的差异。如果你不能以某种方式锚定一个模式,那么除非你可以限制引擎在默认情况下想做什么,否则你将受到模式的回溯和贪婪的困扰。

我必须解析许多设备配置,但​​是我没有尝试将它们视为单个字符串,而是将它们分解为由行数组组成的逻辑块,然后我可以提供从这些块中提取数据的逻辑基于块包含某些类型信息的知识。小块的搜索速度更快,编写可以锚定的模式更加容易,从而提供了巨大的加速。

另外,请不要犹豫使用Ruby的String方法(如split)拆分行,并使用子字符串匹配来查找包含所需内容的行。它们非常快,不太可能导致减速。

如果我有一个字符串:

config = "name:\n foo\ntype:\n thingie\nlast update:\n tomorrow\n"
chunks = config.split("\n").slice_before(/^\w/).to_a
# => [["name:", " foo"], ["type:", " thingie"], ["last update:", " tomorrow"]]

command_blocks = chunks.map{ |k, v| [k[0..-2], v.strip] }.to_h

command_blocks['name'] # => "foo"
command_blocks['last update'] # => "tomorrow"

slice_before对于这种任务来说是一种非常有用的方法,因为它允许我们定义一个模式,然后用于测试主数组中的中断,并按这些模式进行分组。 Enumerable模块中有许多有用的方法,因此请务必仔细查看。

可以解析相同的数据。

当然,如果没有针对您尝试做的事情的示例数据,很难提出更好的方法,但我们的想法是,将您的输入细分为可管理的小块并从那里开始。

评论您如何定义模式。

使用/\/.../而不是使用%r(称为“leaning-toothpicks syndrome”),它允许您定义不同的分隔符:

singleline_comment = /\/\/(.*)$/     # => /\/\/(.*)$/
singleline_comment = %r#//(.*)$#     # => /\/\/(.*)$/

multiline_comment = /\/\*(.*?)\*\//m # => /\/\*(.*?)\*\//m
multiline_comment = %r#/\*(.*?)\*/#m # => /\/\*(.*?)\*\//m

上面每个示例中的第一行是你如何做的,第二行是我如何做的。它们产生相同的正则表达式对象,但第二个更容易理解。

您甚至可以让Regexp为您提供帮助:

NONGREEDY_CAPTURE_NONE_TO_ALL_CHARS = '(.*?)'
GREEDY_CAPTURE_NONE_TO_ALL_CHARS = '(.*)'
EOL = '$'

Regexp.new(Regexp.escape('//') + GREEDY_CAPTURE_NONE_TO_ALL_CHARS + EOL) # => /\/\/(.*)$/
Regexp.new(Regexp.escape('/*') + NONGREEDY_CAPTURE_NONE_TO_ALL_CHARS + Regexp.escape('*/'), Regexp::MULTILINE) # => /\/\*(.*?)\*\//m

这样做可以迭代地构建极其复杂的表达式,同时保持它们相对容易维护。

至于暂停你的Rails应用程序,不要尝试在同一个Ruby进程中处理文件。运行一个单独的作业,监视文件并处理它们并存储您要查找的任何内容,以便稍后根据需要进行访问。这样你的服务器将继续响应而不是锁定。我不会在一个线程中执行此操作,但会编写一个单独的Ruby脚本来查找传入的数据,如果没有找到,则会在一段时间内休眠,然后再次查看。 Ruby的sleep方法对此有所帮助,或者您可以使用操作系统的cron功能。