書いた人:kwatch
YAML とは、構造化されたデータを表現するためのフォーマットです。 目的は XML と似ていますが、XML と比べて「読みやすい」「書きやすい」「わかりやすい」という利点があります。
また YAML はあくまで「仕様」であり、それを処理するライブラリの「実装」が必要です。 Ruby 1.8 では Syck というライブラリが標準で含まれています。
本稿では 3 回に渡り、YAML および Ruby のライブラリである Syck について解説します。
本稿では Ruby 1.8 以上を前提にします。Ruby 1.6 系をお使いの方は、Syck をインストールしてください。
なお XML と YAML の比較についてだけ興味のある方は、セクション『XML との比較』をご覧ください。
まず、YAML について簡単に説明します。
YAML (YAML Ain’t Markup Language) とは、構造化されたデータを表現するためのフォーマットです。YAML は次のような用途に向いています。
YAML の目的は XML と似ていますが、XML と比べて次のような利点があります (YAML と XML との詳しい比較はセクション『XML との比較』をご覧ください)。
YAML の仕様では、正式には配列ではなく「シーケンス (Sequence)」、ハッシュではなく「マッピング (Mapping)」という用語を使用しています。これは、「配列」や「ハッシュ」という用語がデータ構造の具体的な実装を表すため、仕様書で使うのはふさわしくないと判断されたのだと思われます。しかし本稿ではわかりやすさを優先して、Ruby ユーザになじみの深い「配列」や「ハッシュ」を用語として使用します。正式な用語については、YAML の仕様書である『YAML Ain’t Markup Language Version 1.1』をご覧ください。
また YAML はあくまで「仕様」であり、それを処理するライブラリの「実装」が必要です。Ruby 1.8 では Syck というライブラリが標準で含まれています。Syck の本体は C 言語で書かれており、それを Ruby などのスクリプト言語から使えるようになっています。現時点では Syck は以下の言語に対応しています (対応状況については、Ruby 以外の言語ではまだ使えない機能も多いみたいです)。
〔追記 (2005-09-28) OCamlのサポートはなくなりました。また新たに Lua と Cocoa がサポートされたようです。〕
そのほかの実装については YAML ホームページのダウンロードページをご覧ください。
YAML では、主に次の 3 つの組み合わせでデータを表現します。
このセクションではまず配列とハッシュの書き方を説明し、そのあとにスカラーについて説明します。
YAML では、行頭に「-」をつけることで配列を表現します。「-」のあとには半角スペースを入れてください。
- aaa
- bbb
- ccc
また YAML ファイルを読み込んで表示する Ruby スクリプト「print-yaml.rb」は次のようになります。
#!/usr/bin/ruby
###
### print-yaml.rb
###
### 使い方: ruby print-yaml.rb file.yaml [file2.yaml ...]
###
require 'yaml'
require 'pp'
str = ARGF.read() # 入力をすべて読み込む
data = YAML.load(str) # パースする
pp data # データを表示する
「YAML.load()」は YAML データを Ruby オブジェクトに変換するメソッドであり、引数として文字列または IO オブジェクトが指定できます。
実行するには次のようにします。
$ ruby print-yaml.rb example01.yaml
なお上記のスクリプトを使わなくても、次のように 1 行で済ませることもできます。
$ ruby -r yaml -r pp -e 'pp YAML.load(ARGF.read())' example01.yaml
実行結果は次のようになります。 YAML のデータが配列になっていることがわかります。
["aaa", "bbb", "ccc"]
半角スペースでインデントすると、配列をネストさせることができます。タブ文字は使えませんので注意してください。
- aaa
-
- b1
- b2
-
- b3.1
- b3.2
- ccc
実行結果は次のようになります。
["aaa", ["b1", "b2", ["b3.1", "b3.2"]], "ccc"]
なお箇条書きのように書くことはできません。例えば次の例では、配列の要素が「”A”」なのか「[“a1”,”a2”,”a3”]」なのか決められません (実際にはすべて文字列として解釈されます)。
- A
- a1
- a2
- a3
実行結果は次のようになります。
["A - a1 - a2 - a3"]
ハッシュは「キー: 値」の形式で表します。コロン「:」のあとに半角スペースを 1 つ以上入れてください (タブ文字は使えません)。
A: aaa
B: bbb
C: ccc
実行結果:
{"A"=>"aaa", "B"=>"bbb", "C"=>"ccc"}
半角スペースでインデントすることで、ハッシュをネストさせることができます。
A: aaa
B:
B1: bbb1
B2: bbb2
C: ccc
実行結果:
{"A"=>"aaa", "B"=>{"B1"=>"bbb1", "B2"=>"bbb2"}, "C"=>"ccc"}
また次のように書くことはできません。なぜなら、キー「”A”」に対応する値が「”foo”」なのか「{“a1”=>“bar”,”a2”=>“baz”}」なのか分からないからです。
A: foo
a1: bar
a2: baz
実行結果:
$ ruby print-yaml.rb exampe13.yaml
/usr/lib/ruby/1.8/yaml.rb:39:in `load': parse error on line 1, col 5: ` a1: bar
' (ArgumentError)
from /usr/lib/ruby/1.8/yaml.rb:39:in `load'
from print-yaml.rb:11
配列とハッシュはお互いにネストさせることができます。
次の例は、配列の中にハッシュをネストさせる例です。
- name: Hanako
email: flower@mail.com
- name: Sumire
email: garnet@mail.net
- name: Momoko
email: peach@mail.org
実行結果:
[{"name"=>"Hanako", "email"=>"flower@mail.com"},
{"name"=>"Sumire", "email"=>"garnet@mail.net"},
{"name"=>"Momoko", "email"=>"peach@mail.org"}]
次はハッシュの中に配列をネストさせる例です。
names:
- Hanako
- Sumire
- Momoko
emails:
- flower@mail.com
- garnet@mail.net
- peach@mail.org
実行結果:
{"emails"=>["flower@mail.com", "garnet@mail.net", "peach@mail.org"],
"names"=>["Hanako", "Sumire", "Momoko"]}
ネストはいくらでも深くできます。次の例は、「ハッシュをもつ配列をもつハッシュをもつ配列をもつハッシュ」の例です。
database:
host: localhost
port: 5432
user: dbuser
passwd: dbpasswd
tables:
- name: users
desc: User master table
columns:
- name: id
type: integer
primary-key: true
- name: user
type: varchar(32)
not-null: true
unique: true
- name: password
type: varchar(32)
not-null: true
- name: group_id
type: integer
not-null: true
- name: groups
desc: Group master table
columns:
- name: id
type: integer
primary-key: true
- name: group
type: varchar(32)
not-null: true
unique: true
実行結果:
{"tables"=>
[{"columns"=>
[{"name"=>"id", "type"=>"integer", "primary-key"=>true},
{"name"=>"user",
"not-null"=>true,
"type"=>"varchar(32)",
"unique"=>true},
{"name"=>"password", "not-null"=>true, "type"=>"varchar(32)"},
{"name"=>"group_id", "not-null"=>true, "type"=>"integer"}],
"name"=>"users",
"desc"=>"User master table"},
{"columns"=>
[{"name"=>"id", "type"=>"integer", "primary-key"=>true},
{"name"=>"group",
"not-null"=>true,
"type"=>"varchar(32)",
"unique"=>true}],
"name"=>"groups",
"desc"=>"Group master table"}],
"database"=>
{"user"=>"dbuser", "passwd"=>"dbpasswd", "port"=>5432, "host"=>"localhost"}}
今まで見てきたような、インデントを使って構造を表す書き方を「ブロックスタイル」といいます。ブロックスタイルではデータを複数行にわたって記述します。
これに対し、「{}」や「[]」を使って構造を表すこともできます。この書き方を「フロースタイル」といいます。フロースタイルではデータを 1 行にまとめて記述できます。
次の例はフロースタイルによる配列の例です。コンマ「,」のあとには半角空白を入れてください。またエラーになる場合は Ruby のバージョンを 1.8.2 にあげてみてください。
[aaa, bbb, ccc]
実行結果:
["aaa", "bbb", "ccc"]
次の例はフロースタイルによるハッシュの例です。コロン「:」のあとには半角空白を入れてください。
{ A: aaa, B: bbb, C: ccc }
実行結果:
{"A"=>"aaa", "B"=>"bbb", "C"=>"ccc"}
フロースタイルでもネストさせることができます。またブロックスタイルとの併用もできます。
route1: [ {x: 0, y: 0}, {x: 0, y: 5}, {x: 5, y: 5} ]
route2:
- {x: 0, y: 0}
- {x: 5, y: 0}
- {x: 5, y: 5}
実行結果:
{"route1"=>[{"x"=>0, "y"=>0}, {"x"=>0, "y"=>5}, {"x"=>5, "y"=>5}],
"route2"=>[{"x"=>0, "y"=>0}, {"x"=>5, "y"=>0}, {"x"=>5, "y"=>5}]}
なおフロースタイルを使うと、JSON と同じにすることができます。つまり JSON を YAML としてみなすことができます。 詳しくは YAML is JSON をご覧ください。
{ "menu": {
"id": "file",
"value": "File:",
"popup": {
"menuitem": [
{"value": "New", "onclick": "CreateNewDoc()"},
{"value": "Open", "onclick": "OpenDoc()"},
{"value": "Close", "onclick": "CloseDoc()"}
]
}
}
}
実行結果:
{"menu"=>
{"popup"=>
{"menuitem"=>
[{"value"=>"New", "onclick"=>"CreateNewDoc()"},
{"value"=>"Open", "onclick"=>"OpenDoc()"},
{"value"=>"Close", "onclick"=>"CloseDoc()"}]},
"id"=>"file",
"value"=>"File:"}}
「#」から行末までがコメントになります。範囲コメントはありません。
# コメント行
# コメント行
- aaa
- bbb # これもコメント行
- ccc
なお「#」のようにバックスラッシュでエスケープできます。
〔追記 (2005-10-01) : 真偽値のパターンとして “on/off” を追加。Symbol のパターンを追加。Syck のソースである lib/impplicit.re についての言及を追加。 〕
配列やハッシュのような、他のデータを要素としてもつデータは「コレクション (Collection)」といいます。それ以外のデータ (数値や文字列など) は「スカラー (Scalar)」といいます。
YAML では、以下のデータ型を自動的に判別します。
これら以外は文字列として認識されます。また引用符「’」や二重引用符「”」で囲むと、強制的に文字列として認識されます。
decimal1: 123 # 整数 (10 進数)
decimal2: 1,234,567,890 # 整数 (10 進数)
octal: 0644 # 整数 (8 進数)
hexa: 0xFF # 整数 (16 進数)
float1: 0.05 # 浮動小数点
bool1: true # 真
bool2: yes # 真
bool3: on # 真
bool4: false # 偽
bool5: no # 偽
bool6: off # 偽
null1: ~ # Null 値
null2: null # Null 値
date: 2005-01-01 # 日付
stamp: 2005-01-01 00:00:00 +09:00 # タイムスタンプ
str1: 'true' # 文字列
str2: "2005" # 文字列
symbol: :foo # シンボル (Syck の独自機能)
実行結果 (順番は変更しています) :
{"decimal1"=>123,
"decimal2"=>1234567890,
"octal"=>420,
"hexa"=>255,
"float1"=>0.05,
"bool1"=>true,
"bool2"=>true,
"bool4"=>false,
"bool5"=>false,
"bool6"=>false,
"null1"=>nil,
"null2"=>nil,
"date"=>#<Date: 4906743/2,0,2299161>,
"stamp"=>Sat Jan 01 00:00:00 GMT+9:00 2005,
"str1"=>"true",
"str2"=>"2005",
"symbol"=>:foo}
また「!」を使うと、データの型を明示的に指定できます。これを使うと、Ruby における Regexp や Symbol のように処理系の言語に依存した型も指定できます。
- !str 123 # 文字列
- !pairs # 組 (要素が 2 つの配列)
- A: aaa
- B: bbb
- !ruby/sym foo # Ruby の Symbol
- !ruby/regexp /^$/ # Ruby の Regexp
実行結果:
["123", [["A", "aaa"], ["B", "bbb"]], :foo, /^$/]
どのようなデータ型が指定できるかは『Language-Independent Types for YAML』をご覧ください。ただし、そこで説明されたすべてのデータ型に Syck が対応しているわけではないようです。
またデータ型のパターンは、Syck のソースでは lib/implicit.re で定義されています。独自のパターンを追加するには、このファイルを変更して Syck を最コンパイルする必要があり、Ruby スクリプトから追加する方法はありません。
文字列も、フロースタイルとブロックスタイルがあります。今まで見てきたのはフロースタイルであり、 1 行に収まる文字列の場合に適しています。 ブロックスタイルは複数行にわたる文字列に適しており、次のようになります。
## 各行の改行を保存する
text1: |
aaa
bbb
ccc
## 各行の改行と、最終行に続く改行を保存する
text2: |+
aaa
bbb
ccc
## 各行の改行は保存するが、最終行の改行は取り除く
text3: |-
aaa
bbb
ccc
## 改行を半角スペースに置き換える、ただし最終行の改行は保存される
text4: >
aaa
bbb
ccc
## 改行を半角スペースに置き換え、最終行に続く改行を保存する
text5: >+
aaa
bbb
ccc
## 改行を半角スペースに置き換え、最終行の改行を取り除く
text6: >-
aaa
bbb
ccc
実行結果:
{"text1"=>"aaa\nbbb\nccc\n",
"text2"=>"aaa\nbbb\nccc\n\n\n",
"text3"=>"aaa\nbbb\nccc",
"text4"=>"aaa bbb ccc\n",
"text5"=>"aaa bbb ccc\n\n\n",
"text6"=>"aaa bbb ccc"}
各方法において、空行も保存されます。
またインデント幅を明示することもできます。ただし0は指定できないようです。
- |2
foo
bar
baz
- |0
foo
bar
baz
- >2
foo
bar
baz
- >0
foo
bar
baz
実行結果:
[" foo\n bar\n baz\n",
"foo\nbar\nbaz\n",
" foo\n bar\n baz\n",
"foo bar baz\n"]
ブロックスタイルを使うと、YAML ドキュメントの中に別の YAML ドキュメントを埋め込むことが簡単にできます。このとき、XML と違って記号をエスケープする必要はありません。
YAML では、データに「&name」で印をつけ、「*name」で参照することができます (C 言語におけるアドレスやポインタと同じ表記です)。前者をアンカー (Anchor)、後者をエイリアス (Alias) といいます。
配列の要素にアンカーをつけるには、次のようにします。
- &mark foo
- bar
- *mark
- *mark
これは次の Ruby スクリプトと同じです。
mark = "foo"
[mark, "bar", mark, mark]
ハッシュの要素にアンカーをつけるには、次のようにします。
A: &mark
foo
B: bar
C: *mark
これは次の Ruby スクリプトと同じです。
mark = foo
{ "A"=>mark, "B"=>"bar", "C"=>mark }
より複雑な例を次に示します。
## 役職
posts:
- &p91
id: 91
name: 重役
- &p92
id: 92
name: 管理職
- &p93
id: 93
name: 平社員
## 社員
employees:
## 山田は重役、上司はなし
- &e1001
id: 1001
name: 山田○○
post: *p91
supervisor: ~
## 田中は管理職、上司は山田
- &e1002
id: 1002
name: 田中○○
post: *p92
supervisor: *e1001
## 中村は平社員、上司は田中
- &e1003
id: 1003
name: 中村○○
post: *p93
supervisor: *e1002
アンカーとエイリアスを使うことで、木構造だけでなく、DAG やネットワーク構造を直接表現できます。これは、基本的に木構造しか表現できない XML と比べて、YAML の利点です。
YAML では、半角空白のかわりにタブ文字を使うことはできません。タブ文字の表示幅は何かの規格で決まっているわけではなく、環境によって異なります (8 文字が多いのはあくまで習慣にすぎません)。そのため、YAML ではタブ文字を使わないようになっています。
しかし、タブ文字が使えたほうが便利だという人もいるでしょう。その場合は、次のようにするとタブ文字を半角スペース 8 個に展開することができます。
require 'yaml'
## データを読み込む
str = ARGF.read()
## インデントが 8 文字単位になるように空白に置き換える
s = ''
str.each_line do |line|
s << line.gsub(/([^\t]{8})|([^\t]*)\t/n) { [$+].pack("A8") }
end
## パースする
yaml = YAML.load(s)
---
name: foo
email: foo@mail.com
---
name: bar
email: bar@mail.org
---
name: baz
email: baz@mail.net
- aaa
- bbb
...
- ccc # 読み込まれない
--- %YAML 1.1
- aaa
- bbb
これ以外にも、YAML の仕様では様々な記述方法が定められています。 YAML はシンプルなようでいて、実は思ったよりも仕様は複雑です。 今回はよく使われる機能だけを紹介し細かいところは省略していますが、興味のある方は『YAML Ain’t Markup Language (YAML) Version 1.1』をご覧ください。
YAML も XML も、どちらも構造化されたデータを表現するフォーマットです。しかし両者には歴然とした違いがあります。
このセクションでは、YAML と XML との比較を行い、両者の利点と欠点をみていきます。客観的に比較したつもりですが、おかしな点があればご指摘ください。
今まで見てきたように、YAML は終了タグを使うかわりにインデントを使うことで、データ構造のネストを表現しています。これは人間にとって読みやすく、かつ書きやすい表記法です。これだけでも XML から乗り換える価値があります。
XML はご存知のように、書きやすくも読みやすくもありません。終了タグを必要とするため、XML データを自動生成するような場面では都合がいいのですが、人間が直接読んだり書いたりするには向きません。
これは、なにやら Ruby と Java の関係に似ています。やたら簡潔な表記を好む Ruby 派と、長くても正確な名前を好む Java 派は、そのまま YAML 派と XML 派にきれいに分かれるんじゃないでしょうか。XML の冗長な表記に耐えられる人は Java と XML を使い、耐えられない人は Ruby と YAML を好んでいるような気がします (煽っているわけじゃないので怒らないでね)。
なお XML でも、空要素タグと属性をうまく使えば、そこそこ簡潔な表記は可能です。 また現在では Groovy Markup や SXML のように XML と等価な簡易表記法があるので、「表記法が簡単だから」という理由で YAML へ乗り換える必要性は薄れています。
筆者は、表記法が簡単なことは YAML の利点ではあるが本質ではないと考えています。
XML では「エレメント」やら「ノード」やら「属性」やら「テキストノード」やら、いろんな面倒くさい概念を導入しています。これらを理解していないと XML を扱うことができないため、習得が困難です。また DOM にしろ SAX にしろ、プログラミングが非常に面倒くさいものになります。
これに対し YAML では、データを配列とハッシュとスカラーの組み合わせで表現します。新しい概念を導入していないので、非常にわかりやすく、習得が容易です。また扱うものが配列とハッシュとスカラーだけですので、プログラミングも簡単です。
つまり YAML は、表記法が簡単なだけでなく、概念も簡単なのです。XML でも簡易記法を用いることで、読みやすさと書きやすさは改善されます。しかし XML の概念はどうやっても簡単になるわけではありません。
XML に毒されている人はよく考えてみて下さい:たかが設定ファイルを読み込むだけのことに、DOM プログラミングが本当に必要でしょうか?簡単なアドレス帳を作りたいだけなのに、XML の難しい概念を覚えなきゃいけないのでしょうか?あなたが必要としているデータは、配列とハッシュでは表現できないのでしょうか?
筆者は、__データを配列とハッシュとスカラーだけで表現する簡易性こそが YAML の本質である__と考えています。
XML では、「文章の途中で<strong>強調</strong>する」というようなインライン要素を自然に表現できます。これは開始タグと終了タグを使っているおかげです。そのため、記事や論文のような文章、もっといえば非定型的なデータを表現するのに適しています。DocBook などは最もよい例でしょう。
YAML では、インライン要素をうまく表現できません。表現できないわけではないのですが、うまくは表現できません。そのため、文章のような非定型的なデータよりは、アドレス帳や設定ファイルのように定型化されたデータを表現するのに適しています。
筆者は、「猫も杓子も XML」という現状に辟易しています。が、「猫も杓子も YAML」という世界もどうかと思います。文章のような非定型的なデータを扱うなら XML、定型化されたデータを扱うなら YAML、という世界が望ましいと考えています。
なお文章を記述するなら、XML よりも各種 Wiki 文法や RD や reStructuredText などをお勧めします。
XML では、データ型はスキーマ定義で指定します。つまり、XML ドキュメント自身はデータ型の情報を持たず、別ファイルにしています。
またデータ型の指定はタグや属性に対して行います。そのためデータ型はタグや属性によって決まり、データ自身で決まるわけではありません。 例えば「<age>タグは整数をとる」というスキーマ定義があったとします。このとき「<age>19</age>」というエレメントがあると「<age>タグだから19は整数」と決まるのであり、「19 だから整数」と決まるわけではありません。 そのため、<age>タグの値をとってくると必ず整数であることが期待できます。
YAML では、YAML ドキュメント中にデータ型の情報を埋め込みます。別ファイルにすることはありませんし、スキーマ定義も必要としません。
またデータ型はデータによって決まります。例えば「age: 19」とあった場合、「19 が整数である」ということが「19」というデータによって決まるのであり、「age:」というキーによって決まるわけではありません。 そのため age: キーの値をとってきても、必ず整数であるという保証はありません (文字列や真偽値なども指定できてしまうため)。
どちらのアプローチがいいかは好みによります。タグや属性によってデータ型が決まり、その型の値しかとらないという XML のアプローチは、なにやら静的な型の言語とよく似ています。逆に YAML のように、データ自身によって型が決まり、どこにどんな型のデータがくるかわからないというのは、まさに動的な型の言語をみているようです。Java では XML が流行って、Ruby では YAML が流行るのは、こんなところにも原因の一端があるのでしょう。
XML では、見やすくするために改行やインデントを入れると、それがそのままテキストノードとなってしまいます。これが結構悩みの種で、この余計なテキストノードを読み飛ばすためにプログラミングが (若干ですが) 複雑になります。しかしプログラミングを楽にしようとして改行やインデントを取り除けば、今度は XML ドキュメントが非常に読みづらくなります。
YAML はインデントがそのまま構造を表現し、かつ改行がデータの終わりを表すため、このような問題はありません。
XML では、スキーマ定義を読み込んで、その XML ドキュメントを処理するようなクラスを自動生成するツールが存在します。これをデータバインディングツールというそうです。Java でいうと Digester や Betwixt や Relaxer というツールがあり、また JAXB という Java の標準規格もあります (developerWorks の記事をご覧ください)。これらを用いると、複雑な DOM プログラミングをしなくて済みます (だれだって DOM プログラミングはいやですからね)。
YAML は標準でバインディング機能を持ちます。これは、YAML ではデータ型の情報をドキュメント中に埋め込めるためであり、YAML の利点です。具体的な方法は次回で紹介しますが、XML と違ってスキーマ定義なしで利用できるので便利です。
XML ではツールやライブラリのサポートが発達しています。パーサだけで DOM パーサ、SAX パーサ、StAX パーサの 3 種類ありますし、スキーマバリデータも主なものだけでDTD、XML Schema、RelaxNG の 3 つあります。他にも XSLT プロセッサ、データバインディングなど数も種類も豊富です。しかし裏を返せば、これらはすべて、扱いにくい XML をなんとか扱いやすくしようとする努力の成果でもあり、また XML をまともに扱おうと思えばこれらのツールを習得しなければならずコストがかかるということでもあります。
YAML のツールとしては、Syck が事実上の標準実装です。作者である why the lucky stiff の尽力によりメジャーなスクリプト言語 (Ruby, Python, PHP, OCaml) で利用できるようになっていますが、他のツールはあまり見かけません。というのも、Syck があればたいがいにおいて事足りるため、他のツールを必要とすることがないからです。 ただし本格的なスキーマバリデータがないことは大きな弱点です。簡単なものなら存在しますが、DTD や RelaxNG や XML Schema に比べてかなり低機能です。
本稿では YAML の書き方を中心に説明し、また XML との比較を行いました。YAML は文章を表現するのにはあまり向きませんが、定型化されたデータは簡潔に表現できます。 また配列とハッシュとスカラー (文字列や数値など) でデータを表現するため、わかりやすく習得も容易です。
構造化されたデータを表現するものとしては、XML や YAML 以外にも、JSON などがあります。用途によってはそれらのほうがふさわしい場合もありますので、ご覧ください。
次回の中級編では、Syck の機能を掘り下げて紹介します。
名前:kwatch。三流プログラマー。いろんな言語に手を出したあげくRubyに行き着き、「これ以上のものは自分には作られへん」と悟ってオレ言語の作成をあきらめてからはや数年。最近のお気に入りは「きっこの日記」。