seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Nokogiri で XML のパース導入編

RubyXML をパースしたい」ということで(蛇足気味ですが)調べたメモを公開します。

前提条件

libxml2 と libxslt が必要です。CentOSであれば以下のように準備します。

yum install libxml2-devel
yum install libxslt-devel

Windows の場合はここから

ftp://ftp.zlatkovic.com/libxml/

「libxml2-2.7.8.win32.zip」を解凍してできた「bin/libxml2.dll」を「libxml2-2.dll」にリネームして「%RUBY_HOME%/bin」に置く。
「libxslt-1.1.26.win32.zip」を解凍してできた「bin/libexslt.dll」「bin/libxslt.dll」を「%RUBY_HOME%/bin」に置く。


Ruby 1.9.3p125

Ruby 1.9.3p125 の環境で試しています。

Ruby Gems

Windows の場合は sudo は要りません。

sudo gem install nokogiri

ただし libxml2 のバージョンが古いと Nokogiri インストール時に警告が出て失敗してしまいます。

$ sudo gem install nokogiri

checking for libxml/parser.h... yes
checking for libxslt/xslt.h... yes
checking for libexslt/exslt.h... yes
checking for iconv_open() in iconv.h... yes
checking for xmlParseDoc() in -lxml2... yes
checking for xsltParseStylesheetDoc() in -lxslt... yes
checking for exsltFuncRegister() in -lexslt... yes
checking for xmlHasFeature()... no
-----
The function 'xmlHasFeature' is missing from your installation of libxml2.  Likely this means that your installed version of libxml2 is old enough that nokogiri will not work well.  To get around this problem, please upgrade your installation of libxml2.

Please visit http://nokogiri.org/tutorials/installing_nokogiri.html for more help!
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers.  Check the mkmf.log file for more
details.  You may need configuration options.
...

libxml2 のバージョンを勝手に上げてしまうと影響度が大きすぎる場合、以下のように最新版を別の場所にインストールしてから gem install nokogiri でそのパスを指定します。

というか、こちらの受け売りなので本家を参照してください。

http://d.hatena.ne.jp/kitamomonga/20100223/ruby_nokogiri_install_with_any_libxml2_path
https://github.com/tenderlove/nokogiri/wiki/what-to-do-if-libxml2-is-being-a-jerk

手元でやったものは以下のようになります。

wget ftp://xmlsoft.org/libxml2/libxml2-2.7.8.tar.gz
tar xvfzp libxml2-2.7.8.tar.gz
cd libxml2-2.7.8
./configure --prefix $HOME/usr/local --with-libxml-src=../libxml2-2.7.8
make
make install

LIBXML2_DIR=/opt/libxml2
sudo gem install nokogiri -- \
  --with-xml2-include=${LIBXML2_DIR}/include/libxml2 \
  --with-xml2-lib=${LIBXML2_DIR}/lib \
  --with-xslt-dir=/usr/include

# with-xml2-include は find /opt/libxml2 -name parser.h で探したパス
# with-xml2-lib は find /opt/libxml2 -name libxml2.so で探したパス
# with-xslt-dir は libxslt インストール済の前提で find /usr/ -name xslt.h で探したパス

XPath を使って XML をパースする

http://nokogiri.org/Nokogiri.html

Nokogiri.XMLXML をパースします。

require 'nokogiri'

xml = <<EOM
<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item id="123">Andy</item>
  <item id="234">Brian</item>
  <item id="345">Charles</item>
</items>
EOM

doc = Nokogiri::XML(xml)

要素の扱い方は libxml-ruby と同様で、属性は Hash のようにして取得し値は #content で取り出せます。また #text でも値を取り出せます。

irb(main):015:0> xpath_obj = doc.xpath('/items/item')
=> [#<Nokogiri::XML::Element:0x9b60dc name="item" attributes=[#<Nokogiri::XML::Attr:0x9b60a0 name="id" value="123">] children=[#<Nokogiri::XML::Text:0x9b5c74 "Andy">]>, #<Nokogiri::XML::Element:0x9b5a
28 name="item" attributes=[#<Nokogiri::XML::Attr:0x9b59ec name="id" value="234">] children=[#<Nokogiri::XML::Text:0x9b5584 "Brian">]>, #<Nokogiri::XML::Element:0x9b5110 name="item" attributes=[#<Nokog
iri::XML::Attr:0x9b50c8 name="id" value="345">] children=[#<Nokogiri::XML::Text:0x9b4ba0 "Charles">]>]

irb(main):016:0> xpath_obj.is_a?(Enumerable)
=> true
irb(main):017:0>

irb(main):069:0* doc.xpath('/items/item').each do |item|
irb(main):070:1*   puts item['id'] + ' -> ' + item.content
irb(main):071:1> end
123 -> Andy
234 -> Brian
345 -> Charles
=> 0

irb(main):074:0> doc.xpath('//item').each do |item| puts item end
<item id="123">Andy</item>
<item id="234">Brian</item>
<item id="345">Charles</item>
=> 0

first や last で先頭・末尾を取り出せます*1

irb(main):072:0> puts doc.xpath('/items/item').first
<item id="123">Andy</item>
=> nil

irb(main):073:0> puts doc.xpath('/items/item').last
<item id="345">Charles</item>
=> nil

CSS セレクターを使って XML をパースする

Nokogiri.XML でパースした Nokogiri::XML::Document オブジェクトでは CSS セレクターを使うことができます。

irb(main):019:0> doc.css('items item').each do |i| puts i end
<item id="123">Andy</item>
<item id="234">Brian</item>
<item id="345">Charles</item>
=> 0

Slop を使って XML をパースする

Nokogiri.Slop でパースした Nokogiri::XML::Document オブジェクトでは XPathCSSセレクターはもちろんのこと、以下のようにタグ名のメソッドを呼び出すことで要素を取り出すことができます。仕組みとしては method_missing で実現されているようです。

http://nokogiri.rubyforge.org/nokogiri/Nokogiri.html#M000360

doc = Nokogiri::Slop(xml)

まずは先ほどと同じようなサンプルから。

irb(main):026:0> doc.items.item.each do |item| puts item end
<item id="123">Andy</item>
<item id="234">Brian</item>
<item id="345">Charles</item>
=> 0
irb(main):027:0>

次はもう少し階層が深いサンプルです。

xml2 = <<EOM
<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item id="123">
    <name>Andy</name>
    <age>21</age>
  </item>
  <item id="234">
    <name>Brian</name>
    <age>23</age>
  </item>
  <item id="345">
    <name>Charles</name>
    <age>19</age>
  </item>
</items>
EOM

doc2 = Nokogiri::Slop(xml2)

このように「items.item」で「xpath('/items/item')」と同等のことができます。

irb(main):071:0> doc2.items.item.each do |e| puts e.age.content end
21
23
19
=> 0

一度 CSS セレクターや XPath のモードに入ってしまうとメソッドによるタグ指定はできなくなりますが

rb(main):050:0> doc2.css('items').item.each do |e| puts e.age.text end
oMethodError: undefined method `item' for #<Nokogiri::XML::NodeSet:0x16721b8>
       from (irb):50
       from /bin/irb:12:in `<main>'
rb(main):051:0>
irb(main):051:0> doc2.xpath('items').item.each do |e| puts e.age.text end
NoMethodError: undefined method `item' for #<Nokogiri::XML::NodeSet:0x16583e8>
        from (irb):51
        from /bin/irb:12:in `<main>'
irb(main):052:0>

XPathCSS セレクターのやり方を組み合わせても問題ないようです。

irb(main):072:0> doc2.items.xpath('item').each do |e| puts e.age.text end
21
23
19
=> 0
irb(main):073:0> doc2.items.css('item').each do |e| puts e.age.text end
21
23
19
=> 0

とすると、name のようにメソッド名と被ってしまった場合は XPath とかを使う・・のかな?

irb(main):082:0> doc2.items.item.each do |e| puts e.name end
item
item
item
=> 0

irb(main):081:0> doc2.items.item.each do |e| puts e.xpath('name') end
<name>Andy</name>
<name>Brian</name>
<name>Charles</name>
=> 0

*1:https://github.com/tenderlove/nokogiri/blob/master/lib/nokogiri/xml/node_set.rb