使用SaxMachine解析大文件似乎会将整个文件加载到内存中

5
我有一个1.6GB的XML文件,使用Sax Machine解析时似乎没有以流式或分块方式进行 - 而是似乎将整个文件加载到内存中(或者可能存在内存泄漏问题?),因为我的Ruby进程的RAM超过了2.5GB。我不知道它何时停止增长,因为我已经用完了内存。

在较小的文件(50MB)上,它也似乎正在加载整个文件。我的任务迭代XML文件中的记录,并将每个记录保存到数据库中。它需要大约30秒的“空闲时间”,然后突然开始执行数据库查询。

我认为SAX应该允许您处理此类大型文件而无需将整个文件加载到内存中。

我是否忽略了什么?

非常感谢

更新添加代码示例

class FeedImporter

  class FeedListing
    include ::SAXMachine

    element :id
    element :title
    element :description
    element :url

    def to_hash
      {}.tap do |hash|
        self.class.column_names.each do |key|
          hash[key] = send(key)
        end
      end
    end
  end

  class Feed
    include ::SAXMachine
    elements :listing, :as => :listings, :class => FeedListing
  end

  def perform
    open('~/feeds/large_feed.xml') do |file|

      # I think that SAXMachine is trying to load All of the listing elements into this one ruby object.
      puts 'Parsing'
      feed = Feed.parse(file)

      # We are now iterating over each of the listing elements, but they have been "parsed" from the feed already.
      puts 'Importing'
      feed.listings.each do |listing|
        Listing.import(listing.to_hash)
      end

    end
  end

end

正如您所看到的,我不关心提要中的<listings>元素。 我只想要每个<listing>元素的属性。

输出结果如下:

Parsing
... wait forever
Importing (actually, I don't ever see this on the big file (1.6gb) because too much memory is used :(

简单回答你的问题:是的,有些东西你忽略了。不幸的是,你没有告诉我们它是什么。在看不见代码的情况下,没有人能找到内存泄漏。 - Michael Kay
4个回答

4
这里有一个读取器,可以将每个列表的XML输出到一个块中,这样你就可以在不将整个文档加载到内存中的情况下处理每个列表。
reader = Nokogiri::XML::Reader(file)
while reader.read
  if reader.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT and reader.name == 'listing'
    listing = FeedListing.parse(reader.outer_xml)
    Listing.import(listing.to_hash)
  end
end

如果列表元素可以嵌套,并且您想将最外层的列表解析为单个文档,则可以执行以下操作:
require 'rubygems'
require 'nokogiri'


# Monkey-patch Nokogiri to make this easier
class Nokogiri::XML::Reader
  def element?
    node_type == TYPE_ELEMENT
  end

  def end_element?
    node_type == TYPE_END_ELEMENT
  end

  def opens?(name)
    element? && self.name == name
  end

  def closes?(name)
    (end_element? && self.name == name) || 
      (self_closing? && opens?(name))
  end

  def skip_until_close
    raise "node must be TYPE_ELEMENT" unless element?
    name_to_close = self.name

    if self_closing?
      # DONE!
    else
      level = 1
      while read
        level += 1 if opens?(name_to_close)
        level -= 1 if closes?(name_to_close)

        return if level == 0
      end
    end
  end

  def each_outer_xml(name, &block)
    while read
      if opens?(name)
        yield(outer_xml)
        skip_until_close
      end
    end
  end

end

一旦你已经进行了猴子补丁,就可以很容易地单独处理每个列表:
open('~/feeds/large_feed.xml') do |file|
  reader = Nokogiri::XML::Reader(file)
  reader.each_outer_xml('listing') do |outer_xml|

    listing = FeedListing.parse(outer_xml)
    Listing.import(listing.to_hash)

  end
end

太棒了,它运行得非常好。似乎相当快,因为我的本地机器上的数据库成为导入的瓶颈。谢谢,约翰! - jakeonrails
我能够使用这种方法以及经典的sax-machine gem解析我的大型xml文档。谢谢! - Jesse Clark

3

很不幸,现在 sax-machine 有 三个 不同的 代码库。更糟糕的是,gemspec 版本没有更新。

尽管在Greg Weber的博客上有评论,但我认为这段代码没有被集成到 pauldix 或 ezkl 的分支中。要使用基于惰性和 fiber 的版本的代码,您需要像这样在 gemfile 中明确引用 gregweb's 版本:

gem 'sax-machine', :git => 'https://github.com/gregwebs/sax-machine'

看起来你是对的。Github的网络图(https://github.com/gregwebs/sax-machine/network)显示Greg的更改尚未合并到官方的SAXMachine仓库中(由pauldix维护)。 - Ivar

2
我fork了sax-machine,使它使用恒定的内存:https://github.com/gregwebs/sax-machine 好消息是:有一个新的维护者计划合并我的更改。我和新的维护者已经使用我的分支一年了,没有出现任何问题。

这个分支似乎与主仓库不同步,并且已经两年没有更新了。它还会抛出有关从根纤程中产生的错误... - Jesse Clark
我也遇到了“(FiberError) can't yield from root fiber” 错误,看起来这个分支已经被放弃了。 - doomspork

0

谢谢确认我的怀疑。很遗憾 SAX 机器不支持惰性求值或提供真正的回调机制 - 那将是绝妙的。 - jakeonrails

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