プログラマーのための YAML 入門 (探索編)
書いた人: kwatch
はじめに
YAML (YAML Ain't Markup Language) とは、構造化されたデータを表現するためのフォーマットです。本来はデータシリアライゼーション用のフォーマットですが、人にとって読みやすく書きやすいフォーマットなので、設定ファイルやデータ定義ファイルなどに使用されます。「構造化されたデータを表現する」という点では XML と同じですが、XML と比べて読みやすい、書きやすい、わかりやすいという特徴があります。
今回は YPath について説明します。YPath とは、YAML ドキュメントの中からある特定のデータを指定したり検索するための規格です (XML には XPath という規格がありますが、それの YAML 版だと考えていただいて結構です)。YPath を使うと、例えば次のようなことができます。
- アドレス帳のなかから名前が XXX である人のデータを取り出す。
- クラス名簿のなかから性別が女の子である子供の名前と電話番号だけを取り出す。
なお YPath の仕様は議論中で、まだ固まっていません。また Syck (YAML 用ライブラリ) での実装も中途半端で、XPath と比べると見劣りします。このような事情を踏まえて、本稿では YPath の基本的な機能だけを説明します。
なおプログラムの動作は Ruby 1.8.4 で確認しています。1.8.2 以前と 1.8.3 以降では Syck の仕様がかなり変わっているので、1.8.2 以前を使っている方は 1.8.4 をインストールしてください。
目次
事前準備
YPath を説明する前に、次のスクリプト「show-ypath.rb」を用意してください。これは YAML ドキュメントの中から、YPath で指定されたデータを抜き出して表示するスクリプトです。使い方は、第 1 引数で YPath を、第 2 引数で YAML ドキュメントのファイル名を指定します。
#!/usr/bin/env ruby
## Usage: ruby show-ypath.rb ypath datafile.yaml
require 'yaml'
require 'pp'
## YPath パターンを取得する
ypath = ARGV.shift
unless ypath
$stderr.puts "Usage: show-ypath ypath [file.yaml ...]"
exit(0)
end
## YAML ファイルを読み込み、ツリーに変換する
str = ARGF.read()
tree = YAML.parse(str)
## ツリーを探索し、ypath にマッチしたノードのパスをすべて表示する
puts "#--- search('#{ypath}') ---"
paths = tree.search(ypath) # paths はパスの配列
paths.each do |path|
pp path
end
## ツリーを探索し、ypath にマッチしたノードをすべて表示する
puts "#--- select('#{ypath}') ---"
nodes = tree.select(ypath) # nodes はノードの配列
nodes.each do |node|
obj = node.transform # ノードをオブジェクトに変換する
pp obj
end
## または
# objs = tree.select!(ypath)
# objs.each do |obj|
# pp obj
# end
YPath を使ったスクリプトは本連載の第 2 回でも説明しましたが、もう一度説明します。
- 「YAML.parse(input)」は、文字列または IO オブジェクトを読み込んで、ツリー形式に変換します。
- ツリーはノード (YAML::Syck::Node オブジェクト) で構成されます*1。ノードを Ruby のオブジェクト (Array、Hash、String、Fixnum など) に変換するには、YAML::Syck::Node#transform() を使います。
- ツリーを探索するメソッドには以下の 3 つがあります*2。これらはどれも、文字列 ypath をもとにツリーを探索するという点では同じですが、戻り値が異なります。
- Node#search(ypath) は、マッチしたノードのパスの配列を返します。
- Node#select(ypath) は、マッチしたノードの配列を返します。
- Node#select!(ypath) は、マッチしたノードに対して transform() を呼び出し、その結果のオブジェクトの配列を返します。
また検索対象となるサンプルドキュメントとして、次のような YAML ドキュメント「datafile.yaml」を用意してください。
teams:
- name: Akudaman
members:
- name: Mujo
age: 24
leader: yes
- name: Tobokkee
age: 25
- name: Donjuro
age: 30
- name: Doronboo
members:
- name: Doronjo
age: 24
leader: yes
- name: Boyakkie
age: 25
- name: Tonzuraa
age: 30
YPath の基本
パス
YPath は、ファイルや URL のパスと同じように、パス要素と区切り文字から構成されます。
- パス要素は「シーケンスのインデックス番号」または「マッピングのキー」です。
- パス要素の区切り文字は「/」です。
例えばサンプルのデータファイルでは、「/teams/0/name」という YPath を指定すると「Akudaman」という文字列が検索されます。
$ ruby show-ypath.rb '/teams/0/name' datafile.yaml
#--- search('/teams/0/name') ---
"/teams/0/name"
#--- select('/teams/0/name') ---
"Akudaman"
また「/teams/0/members」という YPath を指定すると、メンバーのデータを表すシーケンスが検索されます。
$ ruby show-ypath.rb '/teams/0/members' datafile.yaml
#--- search('/teams/0/members') ---
"/teams/0/members"
#--- select('/teams/0/members') ---
[{"name"=>"Mujo", "leader"=>true, "age"=>24},
{"name"=>"Tobokkee", "age"=>25},
{"name"=>"Donjuro", "age"=>30}]
ルート
ツリーのルートとなるノードは、「/.」という YPath で表されます。ここで「.」は現在のノードを表します*3。
$ ruby show-ypath.rb '/.' datafile.yaml
#--- search('/.') ---
"/"
#--- select('/.') ---
{"teams"=>
[{"name"=>"Akudaman",
"members"=>
[{"name"=>"Mujo", "leader"=>true, "age"=>24},
{"name"=>"Tobokkee", "age"=>25},
{"name"=>"Donjuro", "age"=>30}]},
{"name"=>"Doronboo",
"members"=>
[{"name"=>"Doronjo", "leader"=>true, "age"=>24},
{"name"=>"Boyakkie", "age"=>25},
{"name"=>"Tonzuraa", "age"=>30}]}]}
任意の要素
「*」はすべてのパス要素 (シーケンスならインデックス番号、マッピングならキー) にマッチします。
例えばサンプルのデータファイルにおいて、「/teams/*/name」を指定すればすべてのチーム名が、また「/teams/*/members/*/name」を指定すればすべてのメンバー名が検索されます。
「/teams/*/name」
$ ruby show-ypath.rb '/teams/*/name' datafile.yaml
#--- search('/teams/*/name') ---
"/teams/0/name"
"/teams/1/name"
#--- select('/teams/*/name') ---
"Akudaman"
"Doronboo"
「/teams/*/members/*/name」
$ ruby show-ypath.rb '/teams/*/members/*/name' datafile.yaml
#--- search('/teams/*/members/*/name') ---
"/teams/0/members/0/name"
"/teams/0/members/1/name"
"/teams/0/members/2/name"
"/teams/1/members/0/name"
"/teams/1/members/1/name"
"/teams/1/members/2/name"
#--- select('/teams/*/members/*/name') ---
"Mujo"
"Tobokkee"
"Donjuro"
"Doronjo"
"Boyakkie"
"Tonzuraa"
再帰的な検索
再帰的な検索を行うには「//」を使用します。 例えば「//name」を指定すると、すべてのマッピングの中から「name」をキーとする値を表示します。サンプルデータでなら、チーム名とメンバー名がすべて表示されます。
「//name」
$ ruby show-ypath.rb '//name' datafile.yaml
#--- search('//name') ---
"/teams/0/name"
"/teams/0/members/0/name"
"/teams/0/members/1/name"
"/teams/0/members/2/name"
"/teams/1/name"
"/teams/1/members/0/name"
"/teams/1/members/1/name"
"/teams/1/members/2/name"
#--- select('//name') ---
"Akudaman"
"Mujo"
"Tobokkee"
"Donjuro"
"Doronboo"
"Doronjo"
"Boyakkie"
"Tonzuraa"
「//」は、YPath の途中に現れても構いません。例えば「/teams//name」という指定をすれば、「/teams」以下のノードを探索し「//name」にマッチするものが検索されます。
選択
複数の要素を指定するには、「|」で区切ります。通常は「(foo|bar|baz)」のように丸括弧でくくります。
例えばサンプルのデータファイルでは、「//members/*/(name|age)」という YPath を指定すると、すべてのメンバーの名前と年齢が検索できます。
「//members/*/(name|age)」
$ ruby show-ypath.rb '//members/*/(name|age)' datafile.yaml
#--- search('//members/*/(name|age)') ---
"/teams/0/members/0/name"
"/teams/0/members/1/name"
"/teams/0/members/2/name"
"/teams/1/members/0/name"
"/teams/1/members/1/name"
"/teams/1/members/2/name"
"/teams/0/members/0/age"
"/teams/0/members/1/age"
"/teams/0/members/2/age"
"/teams/1/members/0/age"
"/teams/1/members/1/age"
"/teams/1/members/2/age"
#--- select('//members/*/(name|age)') ---
"Mujo"
"Tobokkee"
"Donjuro"
"Doronjo"
"Boyakkie"
"Tonzuraa"
24
25
30
24
25
30
条件
「[]」を使うと、簡単な探索条件を指定できます。今のところ、Syck が対応しているのは次の 2 つのようです。
- 指定されたキーを持つマッピング
- キーの値が指定された値と同じもの
「//members/*[leader]」は、すべてのメンバーからキー「leader」を持つノードを検索します。
「//members/*[leader]」
$ ruby show-ypath.rb '//members/*[leader]' datafile.yaml
#--- search('//members/*[leader]') ---
"/teams/0/members/0"
"/teams/1/members/0"
#--- select('//members/*[leader]') ---
{"name"=>"Mujo", "leader"=>true, "age"=>24}
{"name"=>"Doronjo", "leader"=>true, "age"=>24}
「//members/*/age[.=25]」は、キー「age」の値が 25 であるものを検索します (今のところ、「以上」や「以下」などは指定できません)。「.=」のピリオドは「現在のノード」を表します。
「//members/*/age[.=25]」
$ ruby show-ypath.rb '//members/*/age[.=25]' datafile.yaml
#--- search('//members/*/age[.=25]') ---
"/teams/0/members/1/age"
"/teams/1/members/1/age"
#--- select('//members/*/age[.=25]') ---
25
25
ここで「age の値が 25 であるようなマッピングのノード」を抽出できればいいのですが、Syck がサポートしている YPath だけではできないので、次のようなコードを使ってください*4。
## YAML ファイルを読み込み、ツリーに変換する
str = ARGF.read()
tree = YAML.parse(str)
## ypath にマッチしたノードのパスを取得する
ypath = '//age[.=25]' # 「age が 25である」ことを表す YPath
paths = tree.search(ypath) # paths はパスの配列
## パスから最後の要素を取り除く
## (ex. "/teams/0/members/1/age" => "/teams/0/members/1")
paths = paths.collect { |path| File.dirname(path) }
## ノードを表示する
nodes = tree.select(ypath) # nodes はノードの配列
nodes.each do |node|
pp node.transform
end
応用例:YAML ドキュメント検索ツール
コマンド「yamlgrep」
YPath の応用例として、YPath を使って YAML ドキュメントを検索するツール「yamlgrep」を作成してみます。ちょうど grep が正規表現でテキストファイルを検索するように、yamlgrep では YPath で YAML ファイルを検索します。
#!/usr/bin/ruby
##
## yamlgrep - YAML ファイルから YPath にマッチしたデータを抜き出す
##
def usage()
command = File.basename($0)
s = <<END
使い方: #{command} [-h] [-q] [-u[N]] [-z ypath] ypath-pattern [yamlfile ...]
-h : ヘルプ
-q : 余分な出力を抑える (quietモード)
-u[N] : 親をたどる数 (省略時 N=1)
-z ypath : 末尾に追加する YPath パターン
END
return s
end
require 'yaml'
## コマンドラインオプションを解析
options = {}
while ARGV[0] =~ /^-/
opt = ARGV.shift
case opt
when '-h', '--help' # ヘルプ
options[:help] = true
when '-q' # 余分な出力を抑える
options[:quiet] = true
when /^-u(\d*)/ # 親をたどる数
options[:parent] = $1.empty? ? 1 : $1.to_i
when /^-z(.*)/ # 末尾に追加する YPath
ypath = $1.empty? ? ARGV.shift : $1
unless ypath
$stderr.puts "#{opt}: YPath パターンが指定されていません。"
exit(1)
end
options[:tail] = ypath
else
$stderr.puts "#{opt}: 不正なオプションです。"
exit(1)
end
end
## ヘルプを表示
if options[:help]
puts usage()
exit(0)
end
## YPath パターンを読み取る
ypath = ARGV.shift
unless ypath
$stderr.puts "YPath パターンが指定されていません。"
exit(1)
end
## YAML ファイルを読み込み、ツリーに変換
str = ARGF.read()
tree = YAML.parse(str)
## YPath にマッチするパスをすべて取得
paths = tree.search(ypath)
## パスごとにノードを取得し、データを表示
paths.each do |path|
## -u オプションがあれば親をたどる
options[:parent].times { path = File.dirname(path) } if options[:parent]
## -z オプションがあれば YPath パターンを末尾に追加する
path << options[:tail] if options[:tail]
## ノードを取得してオブジェクトに変換する
objs = []
nodes = tree.select(path)
nodes.each do |node|
obj = node.transform # ノードをオブジェクトに変換
objs << obj
end
## 表示する
puts "## #{path}" unless options[:quiet]
s = objs.to_yaml # YAML 形式の文字列に変換
puts s.sub(/^---\s*\n?/, '') # ドキュメントの区切り「---」を取り除く
puts unless options[:quiet]
end
使い方
yamlgrep の基本的な使い方は、「yamlgrep YPathパターン [ファイル名 ...]」です。ファイル名が省略された場合は標準入力が使われます。例えば「Doronjo」という名前のメンバーがいるかどうかは次のようにして検索できます (実行例のデータファイルは前のセクションと同じものです)。
実行例: 名前が「Doronjo」であるデータを表示
$ ruby yamlgrep '//members/*/name[.=Doronjo]' datafile.yaml ## /teams/1/members/0/name - Doronjo
オプション「-u[N]」を使うと、マッチしたパスの親をたどります。例えば上の例では「Doronjo」という名前しか表示されませんでしたが、オプション「-u1」をつけるとメンバーのデータをすべて表示できます。
実行例: メンバー「Doronjo」のデータを表示
$ ruby yamlgrep -u1 '//members/*/name[.=Doronjo]' datafile.yaml ## /teams/1/members/0 - name: Doronjo leader: true age: 24
オプション「-z YPath」を使うと、マッチしたパスの末尾に YPath を追加できます。例えば上の例に「-z '/(name|age)'」をつけると、名前と年齢だけが表示されます。
実行例: メンバー「Doronjo」の名前と年齢だけを表示
$ ruby yamlgrep -u1 -z '/(name|age)' '//members/*/name[.=Doronjo]' datafile.yaml ## /teams/1/members/0/(name|age) - Doronjo - 24
オプション「-q」をつけると、余分な出力 (マッチしたパスおよび空行) を出力しないようにします。例えば全メンバーの名前を検索するには YPath として「//members/*/name」を指定しますが、そのままだとマッチしたパスおよび空行が表示されます。
実行例: 全メンバーの名前を出力 (「-q」なし)
$ ruby yamlgrep '//members/*/name' datafile.yaml ## /teams/0/members/0/name - Mujo ## /teams/0/members/1/name - Tobokkee ## /teams/0/members/2/name - Donjuro ## /teams/1/members/0/name - Doronjo ## /teams/1/members/1/name - Boyakkie ## /teams/1/members/2/name - Tonzuraa
ここでオプション「-q」をつけると、マッチしたパスおよび空行が表示されなくなります。コマンド「wc -l」でカウントするときなどに便利です。
実行例: 全メンバーの名前を出力 (「-q」あり)
$ ruby yamlgrep -q '//members/*/name' datafile.yaml - Mujo - Tobokkee - Donjuro - Doronjo - Boyakkie - Tonzuraa
なお yamlgrep では出力も YAML 形式になっているので、yamlgrep の出力を yamlgrep で処理することも可能です。
実行例
ほかの実行例をいくつか示します。
実行例: チーム名の一覧を表示
$ ruby yamlgrep '/teams/*/name' datafile.yaml ## /teams/0/name - Akudaman ## /teams/1/name - Doronboo
実行例: Akudaman一味を表示
$ ruby yamlgrep -u1 '/teams/*/name[.=Akudaman]' datafile.yaml
## /teams/0
- name: Akudaman
members:
- name: Mujo
leader: true
age: 24
- name: Tobokkee
age: 25
- name: Donjuro
age: 30
実行例: Mujoさまが所属するチームの名前を表示
$ ruby yamlgrep -u3 -z '/name' '//members/*/name[.=Mujo]' datafile.yaml ## /teams/0/name - Akudaman
実行例: チームリーダをすべて表示
$ ruby yamlgrep -u1 '//leader[.=yes]' datafile.yaml ## /teams/0/members/0 - name: Mujo leader: true age: 24 ## /teams/1/members/0 - name: Doronjo leader: true age: 24
実行例: チームリーダの名前だけを表示
$ ruby yamlgrep -u1 -z '/name' '//leader[.=yes]' datafile.yaml ## /teams/0/members/0/name - Mujo ## /teams/1/members/0/name - Doronjo
実行例: チームリーダの名前と年齢を表示
$ ruby yamlgrep -u1 -z '/(name|age)' '//leader[.=yes]' datafile.yaml ## /teams/0/members/0/(name|age) - Mujo - 24 ## /teams/1/members/0/(name|age) - Doronjo - 24
おわりに
今回は YPath について説明しました。YPath とは、YAML ドキュメント中のデータをパス形式で指定したり検索するための仕様です。仕様はまだ議論の最中であり固まっていませんが、Syck では最低限の機能は実装されているので、興味のある方は試してみてください。
なお本連載はこれにて終了です。本当は「車輪の再発明編」と題して YAML パーサの作り方をやろうかと思ったのですが、書いてみると趣味丸出しになってしまったので、苦情がくる前に自主規制しておきます。長いことお付き合いくださりありがとうございました。
著者について
名前:kwatch。三流プログラマー。親戚の子供にお年玉をあげてなかったら、「お年玉あげられないほど貧乏なの? 人生負け組だね。」と 6 歳児にいわれ、かなり鬱。最近のお気に入りは「ダ・ビンチ・コード」。
プログラマーのための YAML 入門 連載一覧
- プログラマーのための YAML 入門 (初級編)
- プログラマーのための YAML 入門 (中級編)
- プログラマーのための YAML 入門 (実践編)
- プログラマーのための YAML 入門 (検証編)
- プログラマーのための YAML 入門 (探索編)
*1 実際には YAML::Syck::Node のサブクラスである YAML::Syck::Seq、YAML::Syck::Map、YAML::Syck::Serial が使用されます。
*2 これらのメソッドは YAML::BaseNode モジュールで定義されており、これを YAML::Syck::Node クラスが include しています。
*3 本当なら「.」で現在のノードを、「..」で親のノードを表すはずなのですが、「..」は今のところ動作しないようです。
*4 本来なら「//members/*[age=25]」のように書けるとよいのですが、YPath の仕様が決まってないこともあり、Syck ではこのような探索条件がサポートされていません。


References:[Rubyist Magazine 0013 号] [0013 号 巻頭言] [各号目次]