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

第2回Ruby編

今回は、Ruby用のライブラリとして、Syck、RbYAML、Kwalifyの3つのライブラリを紹介します。

Syck

Syckは、Cで書かれたYAML用ライブラリです。YAML用ライブラリとしては世界で最も広く使われており、YAMLの普及に大きく貢献しました。

Syckについて
Webサイトhttp://whytheluckystiff.net/syck/
ドキュメントhttp://www.ruby-lang.org/ja/man/
index.cgi?cmd=view;name=YAML
バージョン0.60 (Ruby 1.8.6)
作者why the lucky stiff

インストール

Ruby 1.8以上であれば標準で付属しているため、インストールの必要はありません。

使い方

Syckの使い方はリスト1のとおりです。詳細はリファレンスマニュアル[1]をご覧ください。

リスト1 Syckの使い方(ex-rbsyck1.rb)
require 'yaml'

## YAMLドキュメントを読み込む
str = File.read('example.yaml')
data = YAML.load(str)                  # 引数は文字列またはI/O
data = YAML.load_file('example.yaml')  # 引数はファイル名

## YAMLストリームを読み込む
File.open('example.yaml') do |f|
  YAML.load_documents(f) do |data|     # 引数は文字列またはI/O
    # ...
  end
end

## 任意のデータをYAML文字列に変換する
data = [ {'x'=>10, 'y'=>20}, {'x'=>15, 'y'=>25} ]
str = data.to_yaml
## または
##   str = YAML.dump(data)  # 任意のデータをYAML文字列に変換する
##   y data                 # 任意のデータをYAML文字列に変換して出力する

タグを変更する

yaml.dump()でインスタンスオブジェクトをYAML文字列に変換した場合、たとえば「!ruby/object:Color」のようなタグがつきます。しかしこれだとRuby限定になり、ほかの言語のYAMLパーサで読み込むとエラーになるため、データ交換する場合には都合が悪いです。

これを回避するには、以下のメソッドを使ってクラスに対応するタグを指定しますリスト2⁠。

Object#taguri()
オブジェクトを表すタグを返します。
YAML.add_builtin_type(ytag) {|type, val| ...}
データからオブジェクトを復元するブロックを登録します。

またObject#to_yaml_style()を再定義することで、フロースタイルで出力させることができます。

リスト2 クラスに対応するタグを指定する(ex-rbsyck1.rb)
require 'yaml'

class Color
  def initialize(r, g, b)
    @r = r;  @g = g;  @b = b
  end
  attr_accessor :r, :g, :b

  ## タグを「!ruby/object:Color」から「!color」へ変更する
  def taguri
    return 'tag:yaml.org,2002:color'  # !color
    #return 'x-private:color'         # !!color
  end

  ## フロースタイルで表示する
  def to_yaml_style
    return :inline
  end
end

## 実行例
obj = Color.new(255, 128, 0)
puts YAML.dump(obj)   #=> --- !color {b: 0, g: 128, r: 255}

## 「!color」タグのときにColorオブジェクトを生成する
YAML.add_builtin_type('color') do |type, val|
  YAML.object_maker(Color, val)
end

## 「!!color」タグのときにColorオブジェクトを生成する
#YAML.add_private_type('color') do |type, val|
#  YAML.object_maker(Color, val)
#end

## 実行例
str = '!color {r: 255, g: 128, b: 0}'
p YAML.load(str)      #=> #<Color:0x5da9e4 @b=0, @g=128, @r=255>

日本語の扱い

Rubyの文字列に日本語が含まれる場合、YAMLドキュメントに変換すると、たとえば「'いろは'」「!binary 44GE44KN44Gv」のようにバイナリデータとして変換されてしまいます。この問題は、根本的にはRubyにおいて文字列とバイナリデータの区別がないことが原因であり、そのためにSyckは安全を期して「ASCII以外の文字を(ある一定の比率で)含むような文字列はバイナリデータ」とみなしています。

しかしこれは不便である人も多いでしょう。この回避策としては、2つあります。1つ目はSyckのソースコードにパッチを当てることです。Rubyのソースコードをダウンロードし、リスト3のパッチを適用してコンパイルします。そしてリスト4のようString#is_binary_data?()を上書きしてバイナリとみなされないようにすると、日本語文字列が二重引用符で囲まれて出力されるようになります。

リスト3 日本語をエスケープしないようにするパッチ
--- ruby-1.8.6-p111/ext/syck/emitter.c  Wed Jan 09 12:57:49 2008 +0900
+++ ruby-1.8.6-p111/ext/syck/emitter.c  Wed Jan 09 14:07:38 2008 +0900
@@ -778,7 +778,8 @@ syck_emitter_escape( SyckEmitter *e, cha
     int i;
     for( i = 0; i < len; i++ )
     {
-        if( (src[i] < 0x20) || (0x7E < src[i]) )
+        /*if( (src[i] < 0x20) || (0x7E < src[i]) )*/
+        if( 0 )
         {
             syck_emitter_write( e, "\\", 1 );
             if( '\0' == src[i] )
.--------------------
リスト4 YAML.dump()で日本語を出力する
require 'yaml'
class String
  def is_binary_data?
    false  ## 日本語がバイナリとみなされるのを防ぐ
  end
end
puts YAML.dump(['alphabet', '日本語'])
### 実行結果:
## --- 
## - alphabet
## - "日本語"

もう1つの方法は、Ya2YAMLを使うことです。Ya2YAMLをインストールして$KCODEを'utf8'に設定すると、文字列をUTF8として扱ってくれますリスト5⁠。Ya2YAMLはRubyGemsを使って「gem install ya2yaml」とすればインストールできます。

ただし、循環参照したデータには対応していません。

リスト5 ya2yamlを使うと、日本語がUTF8として出力される(ex-ya2yaml1.rb)
## 日本語文字列は、バイナリデータとして扱われてしまう
str = 'いろは'
require 'yaml'
p str.to_yaml   #=> "--- !binary |\n44GE44KN44Gv\n\n"
## ya2yamlを使うと、UTF8の文字列はそのまま出力される
require 'rubygems'   # RubyGemsでインストールした場合
require 'ya2yaml'
$KCODE = 'utf8'
p str.ya2yaml   #=> "--- いろは\n"
## Object#to_yamlをya2yamlで置き換えてもよい
[Object, String, Array, Hash].each do |klass|
  klass.class_eval { alias to_yaml ya2yaml }
end
p str.to_yaml   #=> "--- いろは\n"

不具合

筆者が試したところ、以下のような不具合がありました(Ruby 1.8.6で確認⁠⁠。

    日付(タイムスタンプ)の解析に不具合があり、⁠2008-01-01T12:34:56Z」という形式しか解釈されないリスト6⁠。

    対応するアンカーのないエイリアスがあっても、エラーにならない。

    フロースタイルのデータにエイリアスをつけるとエラーになる場合があるリスト7⁠。

リスト6 仕様上は正しいがSyckでは文字列と解釈される形式
- 2008-01-01T12:34:56Z
- 2008-01-01 12:34:56
- 2008-01-01 12:34:56Z
- 2008-01-01T12:34:56
- 2008-01-01 12:34:56 +9
- 2008-01-01 12:34:56.0
リスト7 Syckで読み込むとエラーになる例
- &m1
  {a: 10}
- &s1
  Foo

その他

Syckは、Ruby向けに次のような独自機能を実装しています。

  • 「:foo」のようなデータは、文字列ではなくRubyのSymbolオブジェクトに変換されます。

RbYAML

RbYAMLは、pure Rubyで実装されたYAMLライブラリです。もともとは、Syckが拡張ライブラリであるためJRubyで使えないので、JRubyでも使えるようなPure RubyのYAMLライブラリが必要だったために開発されました[3]⁠。またRbYAMLは、Python用のライブラリであるPyYAMLをRubyに移植したものです。

Syckとの最大の違いは、SyckがYAML 1.0準拠なのに対し、RbYAMLはYAML 1.1準拠だという点です。そのため、SyckとRbYAMLとでは仕様が異なる点があります。

ただし、循環したデータ構造が扱えないなど、Syckと比べて不具合が多いです。

RbYAMLについて
Webサイトhttp://rbyaml.rubyforge.org/
ダウンロードhttp://rubyforge.org/frs/?group_id=1658
ドキュメントなし
バージョン0.2.0
作者Ola Bini

インストール

RubyGemsをインストールしてから「gem install rbyaml」でインストールしてください。

使い方

使い方は、クラス名がYAMLでなくRbYAMLとなっている点以外はSyckとほぼ同じですリスト8⁠。

リスト8 RbYAMLの使い方(ex-rbyaml1.rb)
require 'rubygems'   # RubyGemsを使ってインストールした場合
require 'rbyaml'

## YAMLドキュメントを読み込む
str = File.read('example.yaml')
data = RbYAML.load(str)                  # 引数は文字列またはI/O
data = RbYAML.load_file('example.yaml')  # 引数はファイル名

## YAMLストリームを読み込む
File.open('example.yaml') do |f|
  RbYAML.load_documents(f) do |data|     # 引数は文字列またはI/O
    # ...
  end
end

## 任意のデータをYAML文字列に変換する
data = [ {'x'=>10, 'y'=>20}, {'x'=>15, 'y'=>25} ]
str = data.to_yaml
## または
##   str = RbYAML.dump(data)  # 任意のデータをYAML文字列に変換する
##   y data                   # 任意のデータをYAML文字列に変換して出力する

タグを変更する

RbYAML.dump()でインスタンスオブジェクトをYAML文字列に変換した場合、たとえば「!ruby/object:Color」のようなタグがつきます。しかしこれだとRuby限定になり、ほかの言語のYAMLパーサで読み込むとエラーになるため、データ交換する場合には都合が悪いです。

これを回避するには、以下のメソッドを使ってクラスに対応するタグを指定しますリスト9⁠。

Object.to_yaml_node(representer)
オブジェクトをノード(RbYAML::Node)に変換します。
RbYAML._add_constructor(ytag, symbol)
ノード(RbYAML::Node)からオブジェクトを復元するメソッドをRbYAML::Constructorクラスに定義し、そのメソッド名を登録します。
リスト9 タグを変更する(ex-rbyaml2.rb)
require 'rubygems'
require 'rbyaml'

## クラス定義
class Color
  def initialize(r, g, b)
    @r = r;  @g = g;  @b = b
  end
  attr_accessor :r, :g, :b

  ## タグを変更する
  def to_yaml_node(representer)
    node = super
    node.tag = '!color'     # 元は '!ruby/object:Color'
    node.flow_style = true  # flow styleで出力する
    node
  end
end

## 実行例
color = Color.new(255, 128, 0)
puts RbYAML.dump(color)   #=> --- !color {r: 255, g: 128, b: 0}

## マッピングからColorオブジェクトを生成するメソッド
class RbYAML::Constructor
  def construct_ruby_color(node)
    return construct_ruby('Color', node)
  end
end

## メソッド名を '!color' タグで登録する
RbYAML._add_constructor('!color', :construct_ruby_color)

## 実行例
obj = RbYAML.load('!color {r: 255, g: 128, b: 0}')
p obj   #=> #<Color:0x531d1c @r=255, @b=0, @g=128>

日本語の扱い

日本語を含むYAMLドキュメントを読み込む場合、RbYAML.load()の引数に文字列を指定した場合は問題なく読み込めますが、FileオブジェクトやIOオブジェクトを指定するとエラーになります。

書き出しについては、たとえば「'いろは'」「!!binary 44GE44KN44Gv」のようにバイナリデータとして出力されてしまいます。これを回避する方法は、現状では提供されていません。

不具合

RbYAMLは古いバージョンのPyYAMLをもとにしているためか、いろいろとバグや制限が多いです。筆者が試したところ、以下のようなバグや制限が見つかりました。

YAML.load()
  • 循環構造を持つデータが解釈できない
  • 値がない場合にエラーが発生するリスト10
  • 文字列をブロックスタイルで記述したとき、最終行の改行が取り除かれるリスト11
  • マッピングのデフォルト値を認識しない
  • 「'foo\n'」「"foo\n"」ではなく「"foo92n"」になる
YAML.dump()
  • 循環構造を持つデータに対応していない
リスト10 値がないとエラー
- abc
-      # nullと解釈されるべきだがRbYAMLではエラー
- 123
リスト11 ブロックスタイルだと最終行の改行が取り除かれる
## ["text\n"] となるはずが ["text"] となる
- |
  text

その他

作者のOla Biniは、現在はJRubyで忙しく、RbYAMLは放置気味です。そのため、RbYAMLの不具合が修正される可能性は低いでしょう。

Kwalify

Kwalifyは、YAML用のパーサ、スキーマバリデータ、データバインディングが統合されたライブラリです。

YAMLドキュメントのデータ構造を形式的に記述したものをスキーマといいます。そのスキーマ定義に従って、YAMLドキュメントのデータが正しい構造かどうかをチェックすることをスキーマバリデーションといい、それを実行するツールをスキーマバリデータといいます。またYAMLドキュメントのデータは、通常は(Rubyであれば)HashやArrayなどに変換されますが、それらのかわりにユーザが指定したクラスのオブジェクトに変換するのをデータバインディングといいます。

Kwalifyは、これらが統合されたライブラリです。Kwalifyを使うと、YAMLドキュメントをパースするときに、スキーマバリデーションとデータバインディングを同時に行うことができます。

Kwalifyについて
Webサイトhttp://www.kuwata-lab.com/kwalify/
ドキュメントhttp://www.kuwata-lab.com/
kwalify/ruby/users-guide.html
ダウンロードhttp://www.rubyforge.org/project/kwalify/
バージョン0.7.1
作者Makoto Kuwata

インストール

RubyGemsをインストールしている場合は、⁠gem install kwalify」でインストールできます。RubyGemsをインストールしていない場合は、以下の手順でインストールしてください。

$ tar xjf kwalify_0.7.0.tar.bz2
$ cd kwalify_0.7.0
$ sudo ruby setup.rb install

使い方

KwalifyのYAMLパーサは、スキーマ定義を与えることで、パースしながらスキーマバリデーションとデータバインディングを同時に行うことができますリスト12⁠。スキーマ定義などの詳細についてはドキュメントを参照してください。

リスト12 パースと同時にスキーマバリデーションとデータバインディングを行う
require 'kwalify'

## スキーマ定義(詳細についてはドキュメントを参照)
schema_def = <<END
type: map
class: Config   # データバインディングにはクラス名が必要
mapping:
 "host":  {type: str, default: localhost}
 "port":  {type: int, default: 80}
 "user":  {type: str, required: true}
 "pass":  {type: str, required: true}
END

## スキーマ定義に対応したクラス定義
## (メソッド '[]', '[]=', 'key?' の定義が必要。ここでは
##  Kwalify::Util::HashLike を include することで、
##  これらのメソッドを定義している。)
require 'kwalify/util/hashlike'
class Config
  include Kwalify::Util::HashLike
  def initialize
    @host = 'localhost'  # デフォルト値
    @port = 8080         # デフォルト値
  end
  attr_accessor :host, :port, :user, :pass
end

## スキーマ定義から Validator オブジェクトを作る
schema = Kwalify::Yaml.load(schema_def)
validator = Kwalify::Validator.new(schema)

## パーサを作成する
parser = Kwalify::Yaml::Parser.new(validator)
parser.data_binding = true     # データバインディングを有効化

## YAMLドキュメントをパースする
str = <<END
#host:  localhost
port:  8080
user:  user1
pass:  password1
END
ydoc = parser.parse(str)
p ydoc
## 実行例
## (HashではなくConfigオブジェクトが生成されていることがわかる):
##   #<Config:0x571df4 @pass="password1", @port=8080,
##        @user="user1", @host="localhost">

## バリデーションエラーがあれば表示する
errors = parser.errors
if errors && !errors.empty?
  for e in errors
    puts "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}"
  end
end

Kwalifyでは、スキーマ定義からRubyのクラス定義を生成する機能もあります。詳しくはドキュメントを参照してください。

またそのほかの機能として、KwalifyのYAMLパーサではアンカーより先にエイリアスを書くことができますリスト13⁠。これは、参照構造が複雑なデータを記述するときに大変便利です。

リスト13 KwailfyでのYAMLパーサの使い方
str = <<END
- &a1
  val: 10
  next: *a2     # アンカーより先にエイリアスが現れている
- &a2
  val: 20
  next: *a3     # アンカーより先にエイリアスが現れている
- &a3
  val: 30
  next: *a1
END

## Kwalify::Yaml.load() に :preceding_alias=>true を与えると、
## アンカーより先に現れたエイリアスも解釈してくれる。
require 'kwalify'
ydoc = Kwalify::Yaml.load(str, :preceding_alias=>true)
## または
##  parser = Kwalify::Yaml::Parser()
##  parser.peceding_alias = true
##  ydoc = parser.parse(str)

## データを表示する
require 'pp'
pp ydoc
## 実行例:
##   [{"val"=>10, "next"=>{"val"=>20, "next"=>{"val"=>30, "next"=>{...}}}},
##    {"val"=>20, "next"=>{"val"=>30, "next"=>{"val"=>10, "next"=>{...}}}},
##    {"val"=>30, "next"=>{"val"=>10, "next"=>{"val"=>20, "next"=>{...}}}}]

タグを変更する

KwalifyのYAMLパーサは、タグに対応していません。

日本語の扱い

日本語を含むYAMLドキュメントの読み込みについては、問題ありません。書き出しについては、Kwalifyはデータをdumpする機能はありません。

不具合

以下のような不具合があります。

  • タグに未対応
  • オブジェクトをdumpする機能がない
  • キーと同じインデント幅でシーケンスが続くとパースできないリスト14
リスト14 Kwalifyでパースできない例
## {"key": [10, 20, 30]} と同じはずだが
## Kwalify ではエラーになる
key:
- 10
- 20
- 30

その他

KwalifyのYAMLパーサは、よく使われる機能だけを実装しており、あまり使われない機能は実装されていません。よってデータ交換を目的とするなら、あまり使うべきではないでしょう。

しかしスキーマバリデーションやデータバインディングは大変便利な機能です。またアンカーより先にエイリアスを書ける機能も、複雑なデータ構造を記述する場合には実に重宝します。このため、データ交換には向きませんが、設定ファイルやデータファイルを読み込むときに使うとよいでしょう。

またKwalifyのスキーマバリデーションやデータバインディングは、YAMLだけでなくJSONにも活用できます。これは、JSONがYAMLのサブセットと見なせるためです。興味のある人は試してみてください。

おすすめ記事

記事・ニュース一覧