2008年6月17日星期二

faster-xml-simple

Ruby的标准库里有一套XML的API,叫作REXML
有个xml-simple的gem,可以很方便地把xml转换成hash。它调用的是REXML,是用DOM方式解析的XML。
用起来是很方便,但不幸的是,DOM方式太慢了,而且太耗内存了。文件小不太明显,遇上个几十M的,就直接把内存吃光了,躺在那打饱嗝,也不工作。

好在还有个gem叫faster-xml-simple,哈哈,听名字就是跟xml-simple叫上劲了。它也可以把xml转换成hash,使用起来跟xml-simple一样,但是功能少了挺多,后面我们会来hack它。
faster-xml-simple也是用DOM方式解析XML的,只不过它调用的是libxml-ruby。后者大部分代码都是用c写的,当然快了很多,内存也节约了不少。其实libxml-ruby就是ruby语言跟libxml2的綁定(The Libxml-Ruby project provides Ruby language bindings for the GNOME Libxml2 XML toolkit.)。

那么faster-xml-simple跟xml-simple相比少了些什么呢?你可以看看它主页上列出的 issues。下面要讲讲我自己发现的问题。
不支持grouptags。
对CDATA的支持有问题。
options的key要小写(如grouptags),而xml-simple是大小写混合的(如GroupTags)。
事实上,faster-xml-simple只有三个默认参数,见源代码:
def default_options
{'contentkey' => '__content__', 'forcearray' => [], 'keeproot'=>true}
end
除此之外,还支持'suppressempty', 'forcecontent' 这两个参数。

下面是我hack的代码,支持了grouptags,支持了CDATA,顺便将多个空格压缩为一个空格(调用了String#squeeze!方法)。用法:调用xml_in方法时,传入参数'compress_whitespace' => %w(item, tag, node),相应item等元素的内容里,多个空格就会被压缩为一个空格。
所谓hack,其实就是把xml-simple里相应功能的代码搬过来,然后再抄抄REXML的代码。那个CDATA的hack,纯粹就是帮faster-xml-simple改了个bug。
class FasterXmlSimple
private
# Support CDATA
def text_node?(element)
!element.text? && element.all? {|c| c.cdata? || c.text?}
end

def compress_whitespace?(ele_name)
@options.has_key?('compress_whitespace') &&
@options['compress_whitespace'].include?(ele_name)
end

def collapse(element)
result = hash_of_attributes(element)
if text_node? element
text = collapse_text(element)
text.squeeze!(" \n\t") if compress_whitespace?(element.name)
result[content_key] = text if text =~ /\S/
elsif element.children?
element.inject(result) do |hash, child|
unless child.text?
child_result = collapse(child)
(hash[child.name] ||= []) << child_result
end
hash
end
end
if result.empty?
return empty_element
end

# Compact them to ensure it complies with the user's requests
inline_single_element_arrays(result)
remove_empty_elements(result) if suppress_empty?

# Disintermediate grouped tags.
if @options.has_key?('grouptags')
result.each do |key, value|
next unless (value.instance_of?(Hash) && (value.size == 1))
child_key, child_value = value.to_a[0]
if @options['grouptags'][key] == child_key
result[key] = child_value
end
end
end

if content_only?(result) && !force_content?
result[content_key]
else
result
end
end
end


== 2008-06-27
做了一点修改,原先 xml 里有注释的话,在 parse 后的 hash 里会有一个讨厌的 'comment' => {} (大致是这个吧,记不太清了)
在某些情况下会破坏 grouptags 的作用,于是把 comment 去掉了。
将 collapse 方法里的这一行
(hash[child.name] ||= []) << child_result
改为
(hash[child.name] ||= []) << child_result unless child.comment?
就行了

BTW:Dongbin 同学把修改后的 faster-xml-simple 放在了 github 上,地址为:http://github.com/dongbin/faster-xml-simple/tree/master

没有评论: