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

时间:2012-02-08 19:12:11

标签: ruby xml nokogiri sax large-files

我有一个1.6gb的xml文件,当我用Sax Machine解析它时,它似乎不是流式传输或以块的形式吃掉文件 - 而是它似乎将整个文件加载到内存中(或者可能有一个内存泄漏到某处?)因为我的红宝石过程爬上了2.5gb的ram。我不知道它在哪里停止增长,因为我的内存不足。

在较小的文件(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

如您所见,我不关心Feed中的<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 :(

4 个答案:

答案 0 :(得分:3)

这是一个将每个列表的XML生成一个块的Reader,因此您可以处理每个列表而无需将整个文档加载到内存中

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

答案 1 :(得分:3)

不幸的是,sax-machine现在有three different repos。更糟糕的是,gemspec版本没有碰到。

尽管对Greg Weber's blog发表评论,但我认为此代码并未整合到pauldix或ezkl的分支中。要使用基于光纤的惰性版本代码,我认为您需要在gemfile中专门引用gregweb's版本,如下所示:

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

答案 2 :(得分:2)

我将sax-machine分叉,以便它使用常量内存:https://github.com/gregwebs/sax-machine

好消息:有一位新的维护者正在计划合并我的更改。 我自己和新的维护者已经使用我的前叉一年没问题了。

答案 3 :(得分:0)

你是对的,SAXMachine急切地阅读整个文件。看看它的处理程序来源:https://github.com/pauldix/sax-machine/blob/master/lib/sax-machine/sax_handler.rb

要解决您的问题,我会直接使用http://nokogiri.rubyforge.org/nokogiri/Nokogiri/XML/SAX/Parser.html并自行实施处理程序。