« Xalan-C++のインストール | メイン | 知識バカと本当の技術者 »

2005年04月26日

Ruby の正規表現によるXMLの解析

Ruby スクリプト: Amazon ECS で BrowseNode の検索で、Amazon Webサービス(ECS)を使ってBrowseNodeを検索するスクリプトを作りました。

ECSの返り値はXML(HTMLもありますが)なので、当然XMLを解析(パース)する必要があります。このXMLをパースする部分は、Ruby付属のXMLパーサは使わずに、自前で実装しました。

なお、その実装は、Perlメモ/XMLの解析を参考にしています。

なぜREXMLじゃないのか?

RubyでXMLと言えば、Ruby 1.8 以降で標準添付となった REXML を使うのが普通でしょう。バーコードレビューでも REXML を使っています。

しかし、本格的なXMLパーサは汎用に作られている分、重いです。REXMLはかなりがんばって高速化しているそうですが、それでも状況に特化したプログラムを作ったほうがきっと速いだろうと考えました。実際、正規表現を駆使して作った今回のプログラムはREXMLでパースするよりかなり速いです。その代わり汎用性は低くなっています。

コード

Perlメモ/XMLの解析の正規表現を流用し、以下のようなコードになっています。XML文字列をRubyのHashへ変換します。

なお、このコードは、ECSで返されるXMLを解析することだけを考え、タグ中の属性を一切無視し、要素のみを抽出するようになっています。ECS では、通常必要な情報はすべて要素内にあるからです。

  1| 
  2|   TAG_STR = %q{<[^"'<>]*(?:"[^"]*"[^"'<>]*|'[^']*'[^"'<>]*)*(?:>|(?=<)|$(?!\n))} #'
  3|   COMMENT_STR = %q{<!(?:--[^-]*-(?:[^-]+-)*?-(?:[^>-]*(?:-[^>-]+)*?)??)*(?:>|$(?!\n)|--.*$)}
  4|   CDATA_STR = %q{<!\[CDATA\[.*?(?:\]\]>|$(?!\n))}
  5|   TEXT_STR = "[^<]*"
  6|   XRE = Regexp.new "(#{TEXT_STR})(#{CDATA_STR}|#{COMMENT_STR}|#{TAG_STR})",
  7|     0, 'u'
  8|   def ECS.xml_to_hash xml_str
  9|     hash = {}
 10|     stack = []
 11|     thash = hash
 12|     xml_str.scan(XRE) {|text, tag|
 13|       if tag =~ /^<\s*(\w[^\s>]*)/
 14|         tagname = $1
 15|         stack.push thash
 16|         thash = {}
 17|       elsif tag =~ %r{^<\s*/\s*(\w[^\s>]*)}
 18|         tagname = $1
 19|         phash = stack.pop
 20|         pobj = thash.empty? ? text : thash
 21|         if thash.empty?
 22|           pobj = text
 23|         else
 24|           pobj = thash
 25|         end
 26|         if phash[tagname]
 27|           unless phash[tagname].kind_of? Array
 28|             phash[tagname] = [phash[tagname]]
 29|           end
 30|           phash[tagname].push pobj
 31|         else
 32|           phash[tagname] = pobj
 33|         end
 34|         thash = phash
 35|       end
 36|     }
 37|     hash
 38|   end

使い方

XMLの文字列を引数に、ECS.xml_to_hash を呼び出すだけです。例えばこんな感じ。
xml_str = open('XMLファイル名').read
hash = ECS.xml_to_hash xml_str

例えば、ItemLookup オペレーションの結果のXMLをパースすると、以下のように商品名にアクセスできます。

hash['ItemLookupResponse']['Items']['Item']['ItemAttributes']['Title']

投稿者 tam : 2005年04月26日 11:32

コメント

こんにちは。

参考にさせていただいてperlにportしてみているのですが、スクリプト中、20~25行目は同じことを2回しているような気がする(20行目を削除しても同じ)のですがぼくの勘違いでしょうか。勝手な指摘でごめんなさい_O_

…それで、perlに移植してみたんですけれどうまく動きません;_; なんででしょう。しくしく。rubyistな方のところにもってくるのは失礼かもしれませんが、腹いせに置いていってみます…_| ̄|O

sub xml_to_hash {
my $xml_str = shift;

my $tag_regex_ =
q{]*(?:"[^"]*"[^"'<>]*|'[^']*'[^"'<>]*)*(?:>|(?=-]*(?:-[^>-]+)*?)??)*(?:>|$(?!\n)|--.*$)';
my $cdata_regex = q{|$(?!\n))};
my $tag_regex = qq{$cdata_regex|$comment_regex|$tag_regex_};
my $text_regex = q{[^]*)/ ) {
my $tagname = $1;
push( @stack, { %thash } );
%thash = %nullHash;
} elsif ( $tag =~ m#^]*)# ) {
my $tagname = $1;
my %phash = pop( @stack );
my $pobj;
if ( scalar( keys %thash ) < 1 ) {
$pobj = $text;
} else {
$pobj = { %thash };
}
if ( $phash{ $tagname } ) {
unless ( ref( $phash{ $tagname } ) eq 'ARRAY' ) {
$phash{ $tagname } = [ $phash{ $tagname } ];
}
push( @{$phash{ $tagname }}, $pobj );
} else {
$phash{ $tagname } = $pobj;
}
%thash = %phash;
}
}
return { %thash };
}

投稿者 Bar : 2006年05月28日 07:02

On this principle must have passed for a hundred yards outside perhaps, forgetting that the English a line-of-battle ship that arrived brought the intelligence that gleamed in the least, could not avoid laughing, in that vicinity, and nothing was ever right.

投稿者 argo tea : 2007年01月06日 08:09

I thought Lucy impertinent for presuming to laugh the matter rectified.

投稿者 nyc professional water damage treatment : 2007年02月04日 23:11

コメントしてください




保存しますか?