言語別 YAML用ライブラリ徹底解説

第1回YAMLライブラリのしくみ

はじめに

YAML(YAML Ain't Markup Language)とは、データを構造化して表現するためのフォーマットです。目的はXMLと似ていますが、インデントを主体とした記法のため、XMLより読みやすく、書きやすく、わかりやすくなっています。

またデータシリアライゼーション[1]に使えるように設計されているため、任意のデータ構造が表現できるだけの記述力を持っています。これは、基本的に木構造しか記述できないXMLと比べて、YAMLの大きな利点です。YAMLの文法については、WEB+DB PRESS Vol.43「最新[データ交換フォーマット]攻略ガイド JSON/YAML実践入門」の第3章「YAML実践リファレンス」に解説がありますので、併せて参照してください。

本特集では、YAMLをさまざまなプログラミング言語で利用するためのYAML用ライブラリの使い方を、各言語ごとに紹介していきます。その際、使い方としては、YAML文字列からデータへの変換だけでなく、データをYAML文字列に変換する方法も紹介していきます。これは、⁠データ交換」の観点で、データをYAML文字列に変換できるとたいへん便利だからです。

YAMLライブラリの内部処理について

ここで、YAMLライブラリが行う内部処理の流れについて説明します。これを知っておくと、各ライブラリの理解が容易になります。YAMLライブラリが行う処理は、いくつかのステージに分かれています図1注2⁠。

図1 YAMLライブラリが行う処理の流れ
図1 YAMLライブラリが行う処理の流れ
parse
YAML文字列を構文解析し、イベントの列に変換します。たとえば「{10, 20}」というYAML文字列は、⁠StartSequence, ScalarData, ScalarData, EndSequence」というイベントの列に変換されます。
compose
イベントの列をノードに変換します[3]⁠。ノードは3種類あり、シーケンス、マッピング、スカラーを表します。
construct
ノードをデータに変換します。たとえばスカラーを表すノードは文字列や数値に変換されます。
represent
constructの逆で、データをノードに変換します。
serialize
composeの逆で、ノードをイベントの列に変換します。
present(emit)[4]
parseの逆で、イベントの列を文字列に変換します。

またparse、compose、constructからなる一連の処理を「load」といい、represent、serialize、present(emit)からなる一連の処理を「dump」といいます。

これらの詳細については、YAML仕様書Chaper 3. Processing YAML Informationを参照してください。

イベントやノードについて調べる方法は、各ライブラリによって違います。リスト1は、Python用のライブラリであるPyYAMLを使ったサンプルスクリプトです。またリスト2はその実行例です。これを使うと、どんなイベントやノードに変換されるか調べることができます。

リスト1 yaml-internal.py: PyYAMLを使ってイベントやノードを調べる
# -*- coding: utf-8 -*-

## 使い方: python yaml-internal.py [file.yaml]

import sys
import yaml

## ファイルまたは標準入力からYAML文字列を読み込む
filename = len(sys.argv) > 1 and sys.argv[1] or None
input = (filename and open(filename) or sys.stdin).read()
input = input.encode('utf8')  ## 文字コードを変換する

## YAML文字列をイベントの列に変換する
print "===== events ====="
for event in yaml.parse(input):
    print event

## YAML文字列からノードを生成する
print "===== node graph ====="
node = yaml.compose(input)
print node
リスト2 yaml-internal.pyの実行例(見やすいように整形済み)
$ cat example.yaml
- abc
- 123
- {x: 10, y: 20}
$ python yaml-internal.py example.yaml
===== events =====
StreamStartEvent()
DocumentStartEvent()
SequenceStartEvent(anchor=None, tag=None, implicit=True)
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'abc')
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'123')
MappingStartEvent(anchor=None, tag=None, implicit=True)
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'x')
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'10')
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'y')
ScalarEvent(anchor=None, tag=None, implicit=(True, False), value=u'20')
MappingEndEvent()
SequenceEndEvent()
DocumentEndEvent()
StreamEndEvent()
===== node graph =====
SequenceNode(tag=u'tag:yaml.org,2002:seq', value=[
  ScalarNode(tag=u'tag:yaml.org,2002:str', value=u'abc'),
  ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'123'),
  MappingNode(tag=u'tag:yaml.org,2002:map', value=[
    ( ScalarNode(tag=u'tag:yaml.org,2002:str', value=u'x'),
      ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'10')
    ),
    ( ScalarNode(tag=u'tag:yaml.org,2002:str', value=u'y'),
      ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'20')
    )
  ])
])

YAMLライブラリの互換性について

ここで、YAMLライブラリの互換性についても説明します。ライブラリの互換性は、データ交換においてたいへん重要であるためです。

現在のところ、各YAMLライブラリの互換性はあまり高くありません。つまり、ある言語のライブラリで正しく読み込めるYAMLドキュメントが、別の言語のライブラリでうまく読み込めなかったりエラーになったりすることがあります。

これにはいくつかの原因があります。

  • YAMLライブラリがまだ枯れていない。YAMLはまだ広範囲には使われていないため、不具合が直されていない
  • YAMLの仕様が大きくて複雑なので、未実装や間違った実装がある。たとえばマッピングのデフォルト値やマージなど、あまり知られていない機能は未実装のことがある
  • 各言語で用意されているデータに変換されるので、言語間の差異が問題になる場合がある。たとえば
    • Pythonにはシーケンスを表すのにlistとtupleがある
    • PHPの配列はシーケンスとマッピングの区別がない
    • Perlその他でタイムスタンプと整数値の区別がない
    • Ruby以外の言語ではマッピングにおいてキーがないときのデフォルト値が未サポート
    など
  • YAML 1.0とYAML 1.1とで仕様が変わったため、使用するライブラリがどちらの仕様に準拠しているかで動作に違いが出る
    • Syck(Ruby 1.8)やYAML.pmはYAML 1.0に準拠
    • PyYAMLやJvYAMLはYAML 1.1に準拠

このようにYAMLのライブラリは互換性が高くないので、データ交換で使う際に問題になります。相手と同じ言語を使うのであれば、使用するライブラリを揃えることで問題を回避できますが、不特定多数とデータ交換を行う場合は大きな問題となるでしょう。

ただし、YAML 1.1の仕様を正確に実装したPyYAMLが登場し、それがさまざまな言語に移植されつつありますので、状況は少しずつですが好転しています。実際にデータ交換で使う場合は、ライブラリの挙動をよく確かめてから使うようしてください。互換性の問題さえ解消できれば、YAMLは同様の目的で利用されるXMLやJSONと比べて表現力が格段に高いため、データが複雑になればなるほどYAMLが有利となります。

タブ文字について

もう一つ、タブ文字の扱いについて説明しておきます。YAMLはインデントを使ってデータ構造を表現しますが、インデントにタブ文字は使えず、必ず半角空白である必要があります。そのため、入力にタブ文字を含む場合はあらかじめ展開しておく必要がありますので注意してください。

参考までに、タブ文字を展開するRubyプログラムをリスト3に、またPHPプログラムをリスト4に挙げておきます。またPythonの場合は文字列自体にexpandtabs()というメソッドがありますのでそれを使ってください。

リスト3 タブ文字を展開するRubyプログラム
def untabify(str, width=8)
  return '' if !str || str.empty?
  list = str.split(/\t/)
  last = list.pop
  return last if list.empty?
  sb = ''
  list.each do |s|
    column = (n = s.rindex(?\n)) ? s.length - n - 1 : s.length
    n = width - (column % width)
    sb << s << (' ' * n)
  end
  sb << last
  return sb
end
リスト4 タブ文字を展開するPHPプログラム
function untabify($str, $width=8) {
    if (! $str) return '';
    $splitted = preg_split('/\t/', $str);
    $last = array_pop($splitted);
    if (! $splitted) return $last;
    $buf = array();
    foreach ($splitted as $s) {
        $buf[] = $s;
        if (($rindex = strrpos($s, "\n")) !== FALSE)
            $column = strlen($s) - $rindex - 1;
        else
            $column = strlen($s);
        $n = $width - ($column % $width);
        $buf[] = str_repeat(' ', $n);
    }
    $buf[] = $last;
    return join($buf);
}

おすすめ記事

記事・ニュース一覧