書いた人: kwatch
YAML (YAML Ain’t Markup Language) とは、構造化されたデータを表現するためのフォーマットです。本来はデータシリアライゼーション用のフォーマットですが、人にとって読みやすく書きやすいフォーマットなので、設定ファイルやデータ定義ファイルなどに使用されます。「構造化されたデータを表現する」という点では XML と同じですが、XML と比べて読みやすい、書きやすい、わかりやすいという特徴があります。
今回は YPath について説明します。YPath とは、YAML ドキュメントの中からある特定のデータを指定したり検索するための規格です (XML には XPath という規格がありますが、それの YAML 版だと考えていただいて結構です)。YPath を使うと、例えば次のようなことができます。
なお 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 ドキュメント「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 は、ファイルや URL のパスと同じように、パス要素と区切り文字から構成されます。
例えばサンプルのデータファイルでは、「/teams/0/name」という YPath を指定すると「Akudaman」という文字列が検索されます。
また「/teams/0/members」という YPath を指定すると、メンバーのデータを表すシーケンスが検索されます。
ツリーのルートとなるノードは、「/.」という YPath で表されます。ここで「.」は現在のノードを表します3。
「*」はすべてのパス要素 (シーケンスならインデックス番号、マッピングならキー) にマッチします。
例えばサンプルのデータファイルにおいて、「/teams//name」を指定すればすべてのチーム名が、また「/teams//members/*/name」を指定すればすべてのメンバー名が検索されます。
「/teams/*/name」
「/teams//members//name」
再帰的な検索を行うには「//」を使用します。 例えば「//name」を指定すると、すべてのマッピングの中から「name」をキーとする値を表示します。サンプルデータでなら、チーム名とメンバー名がすべて表示されます。
「//name」
「//」は、YPath の途中に現れても構いません。例えば「/teams//name」という指定をすれば、「/teams」以下のノードを探索し「//name」にマッチするものが検索されます。
複数の要素を指定するには、「 | 」で区切ります。通常は「(foo | bar | baz)」のように丸括弧でくくります。 |
例えばサンプルのデータファイルでは、「//members/*/(name | age)」という YPath を指定すると、すべてのメンバーの名前と年齢が検索できます。 |
「//members/*/(name | age)」 |
「[]」を使うと、簡単な探索条件を指定できます。今のところ、Syck が対応しているのは次の 2 つのようです。
「//members/*[leader]」は、すべてのメンバーからキー「leader」を持つノードを検索します。
「//members/*[leader]」
「//members/*/age[.=25]」は、キー「age」の値が 25 であるものを検索します (今のところ、「以上」や「以下」などは指定できません)。「.=」のピリオドは「現在のノード」を表します。
「//members/*/age[.=25]」
ここで「age の値が 25 であるようなマッピングのノード」を抽出できればいいのですが、Syck がサポートしている YPath だけではできないので、次のようなコードを使ってください4。
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」であるデータを表示
オプション「-u_[N]_」を使うと、マッチしたパスの親をたどります。例えば上の例では「Doronjo」という名前しか表示されませんでしたが、オプション「-u1」をつけるとメンバーのデータをすべて表示できます。
実行例: メンバー「Doronjo」のデータを表示
オプション「-z YPath」を使うと、マッチしたパスの末尾に YPath を追加できます。例えば上の例に「-z ‘/(name | age)’」をつけると、名前と年齢だけが表示されます。 |
実行例: メンバー「Doronjo」の名前と年齢だけを表示
オプション「-q」をつけると、余分な出力 (マッチしたパスおよび空行) を出力しないようにします。例えば全メンバーの名前を検索するには YPath として「//members/*/name」を指定しますが、そのままだとマッチしたパスおよび空行が表示されます。
実行例: 全メンバーの名前を出力 (「-q」なし)
ここでオプション「-q」をつけると、マッチしたパスおよび空行が表示されなくなります。コマンド「wc -l」でカウントするときなどに便利です。
実行例: 全メンバーの名前を出力 (「-q」あり)
なお yamlgrep では出力も YAML 形式になっているので、yamlgrep の出力を yamlgrep で処理することも可能です。
ほかの実行例をいくつか示します。
実行例: チーム名の一覧を表示
実行例: Akudaman一味を表示
実行例: Mujoさまが所属するチームの名前を表示
実行例: チームリーダをすべて表示
実行例: チームリーダの名前だけを表示
実行例: チームリーダの名前と年齢を表示
今回は YPath について説明しました。YPath とは、YAML ドキュメント中のデータをパス形式で指定したり検索するための仕様です。仕様はまだ議論の最中であり固まっていませんが、Syck では最低限の機能は実装されているので、興味のある方は試してみてください。
なお本連載はこれにて終了です。本当は「車輪の再発明編」と題して YAML パーサの作り方をやろうかと思ったのですが、書いてみると趣味丸出しになってしまったので、苦情がくる前に自主規制しておきます。長いことお付き合いくださりありがとうございました。
名前:kwatch。三流プログラマー。親戚の子供にお年玉をあげてなかったら、「お年玉あげられないほど貧乏なの? 人生負け組だね。」と 6 歳児にいわれ、かなり鬱。最近のお気に入りは「ダ・ビンチ・コード」。
実際には YAML::Syck::Node のサブクラスである YAML::Syck::Seq、YAML::Syck::Map、YAML::Syck::Serial が使用されます。 ↩
これらのメソッドは YAML::BaseNode モジュールで定義されており、これを YAML::Syck::Node クラスが include しています。 ↩
本当なら「.」で現在のノードを、「..」で親のノードを表すはずなのですが、「..」は今のところ動作しないようです。 ↩
本来なら「//members/*[age=25]」のように書けるとよいのですが、YPath の仕様が決まってないこともあり、Syck ではこのような探索条件がサポートされていません。 ↩