使用 Nokogiri::XML::SAX::Parser 流式解析大 XML 文件,继承 Nokogiri::XML::SAX::Document 实现回调,按需提取字段并异步处理,避免内存溢出与状态污染。

用 Nokogiri::XML::SAX 解析大 XML 文件,避免内存爆炸
直接用 Nokogiri::XML 加载几百 MB 的 XML 文件会触发 OOM(Out of Memory),因为 DOM 解析会把整个文档树载入内存。必须改用 SAX 模式——它不建树,只在解析到标签、文本、属性时触发回调,内存占用基本恒定。
-
Nokogiri::XML::SAX::Parser是唯一适合大文件的入口,别碰Nokogiri::XML.parse - 你需要自己继承
Nokogiri::XML::SAX::Document,重写start_element、characters、end_element等方法 - 不要在回调里拼接完整 XML 片段或缓存大量节点数据;按需提取字段,立刻写入数据库或文件
- 如果 XML 有命名空间,记得在
start_element_ns中处理前缀映射,否则start_element收不到带 ns 的 tag 名
上传阶段就流式解析,别先保存临时文件
用户上传大 XML 时,Rails 或 Sinatra 默认会把整个 body 缓存为 Tempfile。这一步本身就会吃光磁盘或拖慢响应。应该在接收请求体的同时边读边解析。
- Rails 中用
request.body(是IO对象)直接传给Nokogiri::XML::SAX::Parser,不要调params[:file].read - Sinatra 中用
request.env['rack.input'],确保中间件没提前 consume 它(比如ContentLength或Chunked中间件可能干扰) - 加超时和大小限制:Nginx 配
client_max_body_size 500m,应用层用Timeout.timeout(300)包住 parser.run - 若需校验 XML 格式合法性,SAX 解析器遇到 malformed XML 会抛
Nokogiri::XML::SyntaxError,捕获后返回 400 即可
常见错误:SAX 回调中误用实例变量导致状态污染
很多人在 SAX handler 里用 @current_text = "" 累积字符,结果多个嵌套标签下 characters 被多次调用,@current_text 混进无关文本。
- 正确做法是只在
start_element中记录当前路径(如@path = [@path, name].flatten),在characters中仅暂存本次回调的字符串,等end_element触发时再按路径决定是否提取 - 别在 handler 里做耗时操作(如 DB 写入),用队列异步处理;否则 SAX 解析会被阻塞,上传连接超时
- 注意
characters可能被拆成多次调用(尤其含 CDATA 或换行时),不能假设一次收全文本
class LargeXmlHandler < Nokogiri::XML::SAX::Document
def initialize(output_io)
@output = output_io
@path = []
@buffer = ""
end
def start_element(name, attrs = [])
@path << name
@buffer = ""
end
def characters(string)
@buffer << string.strip
end
def end_element(name)
if @path == ["root", "record", "title"] && !@buffer.empty?
@output.puts "Title: #{@buffer}"
end
@path.pop
end
end
使用:
File.open("huge.xml") do |f|
parser = Nokogiri::XML::SAX::Parser.new(LargeXmlHandler.new($stdout))
parser.parse(f) # 流式读,不加载全文
end
SAX 模式没有“回溯”能力,一旦跳过某个节点就无法再访问——这意味着设计 handler 时必须提前想清楚要提取哪些路径,别指望后期补救。










