如何在Ruby中高效解析大型文本文件

6

我正在编写一个导入脚本,处理的文件可能有数十万行(日志文件)。使用下面这种非常简单的方法需要足够的时间和内存,以至于我感觉它随时都可能让我的MBP崩溃,所以我终止了这个进程。

#...
File.open(file, 'r') do |f|
  f.each_line do |line|
    # do stuff here to line
  end
end

这个文件特别有 642,868 行:

$ wc -l nginx.log                                                                                                                                        /code/src/myimport
  642868 ../nginx.log

有人知道一种更高效(内存/CPU)的方法来处理此文件中的每一行吗?

更新

上面的 f.each_line 中的代码只是针对每一行匹配一个正则表达式。如果匹配失败,则将该行添加到 @skipped 数组中。如果匹配成功,则将匹配结果格式化为哈希表(由匹配的“字段”作为键),并将其附加到 @results 数组中。

# regex built in `def initialize` (not on each line iteration)
@regex = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - (.{0})- \[([^\]]+?)\] "(GET|POST|PUT|DELETE) ([^\s]+?) (HTTP\/1\.1)" (\d+) (\d+) "-" "(.*)"/

#... loop lines
match = line.match(@regex)
if match.nil?
  @skipped << line
else
  @results << convert_to_hash(match)
end

我完全可以接受这是一个低效的过程。我可以让convert_to_hash内部的代码使用预先计算好的lambda表达式,而不是每次都重新计算。我猜我只是认为问题在于行迭代本身,而不是每行代码。


最节省内存的方法就是使用 each_line 这种方式。你可以按块读取文件,这样更快,然后使用 String#lines 来获取单独的行,并重新连接任何部分加载的跨越块边界的行。这样做需要拆分行并重新连接断开的行,但总体来说效果相当。 - the Tin Man
3个回答

5
我刚刚对一个60万行的文件进行了测试,它在不到半秒钟的时间内迭代完整个文件。我猜测速度慢的原因不在于文件循环,而是在于逐行解析。你能否贴出你的解析代码呢?

唯一有意义的代码片段是我正在使用一个半复杂的正则表达式匹配该行。该正则表达式不进行任何向前/向后查找,它主要只是逐个字符进行匹配。我将在上面发布相关代码的更新。 - localshred
哦,而且正则表达式只计算一次,而不是在每次迭代中计算(只是为了明确)。 - localshred
似乎是我的愚蠢导致了内存增长。我将匹配结果(以及跳过的行)存储在数组中,稍后用于进行数据库插入(或打印跳过的大小)。我知道,我很蠢。现在我只是在跳过的行上使用 puts 并在匹配有效时立即进行数据库插入。真正的内存从未超过30MB。感谢您指出我可能只是以一种愚蠢的方式处理事情。:) (哦,我已经像您最初的答案建议的那样切换到 IO.foreach)。 - localshred

4
这篇博客文章介绍了几种解析大型日志文件的方法,可能会给你一些启示。此外,还可以查看file-tail gem

1
如果您正在使用bash(或类似的工具),您可以尝试进行如下优化:
在input.rb文件中:
 while x = gets
      # Parse
 end

然后在Bash中:

 cat nginx.log | ruby -n input.rb

-n 标志告诉 Ruby 在脚本周围 假定 'while gets(); ... end' 循环,这可能会导致它执行一些特殊的优化。

您可能还想寻找预先编写的解决方案,因为那样会更快。


目前看起来有点不太正式,但我会记在心里。 - localshred

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