显示标签为“Gems”的博文。显示所有博文
显示标签为“Gems”的博文。显示所有博文

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

2008年6月3日星期二

Gem::Specification

要把项目打成gem包的话,需要在Rakefile.rb里加些东西:
spec = Gem::Specification.new do |s|
s.name="bvi_lib"
s.version='0.1'
s.summary = 'A Ruby libary for Batch Video Ingestion'
s.email = 'bdong@freewheel.tv'
s.require_path = 'lib'
s.autorequire = 'bvi_lib'

s.executables << 'bvi'
s.files = FileList["{lib,doc,bin}/**/**"].to_a
# s.has_rdoc = true
s.author = "Bin Dong"
# s.extra_rdoc_files = ["README"]
# s.rdoc_options = ["doc"]
s.add_dependency("activesupport", '~> 2.0.2')
s.add_dependency("actionmailer", '~> 2.0.2')
s.add_dependency("activeresource", '~> 2.0.2')
s.add_dependency("xml-simple", '~> 1.0.11')
s.add_dependency("fastercsv", '~> 1.2.3')
end
(提醒:是'xml-simple'和'fastercsv',而不是'xmlsimple'和'faster_csv'。前面是gem包的名字,后面是程序中require用的。其实两个弄成一样的多好啊。)

注意add_dependency方法,这个是添加包的依赖关系,第一个参数是依赖的gem包名,第二个参数是版本关系,具体见Programming Ruby Second Edition, P206, Table 17.1
这里只摘'~>'的说明:

“Boxed” version operator. Version must be greater than or equal to the specified version and less than the specified version after having its minor version number increased by one. This is to avoid API incompatibilities between minor version releases.

minor就是中间的那个版本号。如果版本号是2.01,则三个部分分别是major = 2, minor = 0, tiny = 1

要打gem包的话,直接在项目下运行:

rake gem

就可以了。

但是注意,只修改Rakefile.rb文件的话,是不会重新打包的,必须把原先的gem包删掉,再重新打包才行。
因为打包的时候会自动检查
FileList["{lib,doc,bin}/**/**"]
下的文件有没有被修改,如果修改了,则会重新打包,如果没修改,就不会重新打包。

Rakefile.rb里的内容,在打包的时候,是放在gem包的metadata里的,查看metadata,就能知道Rakefile.rb文件修改后,重新打包是否成功。

查gem的命令:

gem list
gem list|grep xml
gem list|grep csv

2008年6月2日星期一

cattr_accessor not found

I installed activeresouce using RubyGems. And I want to launch the tests:

ruby /var/lib/gems/1.8/gems/activeresource-2.0.2-/test/format_test.rb

But it didn't work, I've got an error:

./../../lib/active_resource/base.rb:150: undefined method `cattr_accessor' for ActiveResource::Base:Class (NoMethodError)
from /usr/lib/ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
from /usr/lib/ruby/1.8/rubygems/custom_require.rb:27:in `require'
from ./../../lib/active_resource.rb:38
from ./../abstract_unit.rb:4:in `require'
from ./../abstract_unit.rb:4
from load_test.rb:1:in `require'
from load_test.rb:1

So I googled it, and found this topic
http://www.ruby-forum.com/topic/59288
Before starting tests, run this command:

export RUBYOPT=-rubygems

And then:

ruby /var/lib/gems/1.8/gems/activeresource-2.0.2-/test/format_test.rb

Haha, it works, but I don't know how it works. :(