スキップしてメイン コンテンツに移動

Perl 5 to 6 - (似非)XMLのグラマー

これはMoritz Lenz氏のWebサイトPerlgeek.deで公開されているブログ記事"Perl 5 to 6" Lesson 20 - A grammer for (pseudo) XMLの日本語訳です。

原文はCreative Commons Attribution 3.0 Germanyに基づいて公開されています。

本エントリにはCreative Commons Attribution 3.0 Unportedを適用します。

Original text: Copyright© 2008-2010 Moritz Lenz

Japanese translation: Copyright© 2011 SATOH Koichi

注記: XMLに関する用語の誤用(「整形式」と「妥当」を混同しているなど)がありますが、あくまで例なので原文通りに残しています。

NAME

"Perl 5 to 6" Lesson 20 - (似非)XMLのグラマー

SYNOPSIS

grammar XML {
    token TOP   { ^ <xml> $ };
    token xml   { <text> [ <tag> <text> ]* };
    token text {  <-[<>&]>* };
    rule tag   {
        '<'(\w+) <attributes>*
        [
            | '/>'                 # 空タグ
            | '>'<xml>'</' $0 '>'  # 開始タグと終了タグ
        ]
    };
    token attributes { \w+ '="' <-["<>]>* '"' };
};

DESCRIPTION

これまでの連載記事の焦点はPerl6言語であり、実装状況は気にしていませんでした。 これが空想上の言語でないことを示すため、またグラマーの能力を証明するために、このレッスンでは基本的なXMLを解析する、Rakudoで実行できるグラマーの開発をお見せします。

Rakudoの入手とビルドはhttp://rakudo.org/how-to-get-rakudoの指示にしたがって自分で行って下さい。

XMLの概念

私たちの用途の範囲ではXMLは非常に単純です: プレーンテキストと入れ子になったタグから成り、タグは属性を持つことがあります。 妥当なXMLであると(あるいはそうでないと)解析させたいテストケースをちょっとだけ用意しました:

my @tests = (
    [1, 'abc'                       ],      # 1
    [1, '<a></a>'                   ],      # 2
    [1, '..<ab>foo</ab>dd'          ],      # 3
    [1, '<a><b>c</b></a>'           ],      # 4
    [1, '<a href="foo"><b>c</b></a>'],      # 5
    [1, '<a empty="" ><b>c</b></a>' ],      # 6
    [1, '<a><b>c</b><c></c></a>'    ],      # 7
    [0, '<'                         ],      # 8
    [0, '<a>b</b>'                  ],      # 9
    [0, '<a>b</a'                   ],      # 10
    [0, '<a>b</a href="">'          ],      # 11
    [1, '<a/>'                      ],      # 12
    [1, '<a />'                     ],      # 13
);

my $count = 1;
for @tests -> $t {
    my $s = $t[1];
    my $M = XML.parse($s);
    if !($M  xor $t[0]) {
        say "ok $count - '$s'";
    } else {
        say "not ok $count - '$s'";
    }
    $count++;
}

これは「良い」XMLと「悪い」XMLのリスト、そしてXML.parse($string)を呼び出してテストを走らせる小さなスクリプトです。 言語全体にマッチするルールはTOPという名前にする約束になっています。

(テスト1で分かるように単一のルートタグを必須にしていませんが、この制限を追加するのは些細なことです)

グラマーの開発

XMLのキモは当然タグの入れ子構造ですから、まず2番目のテストに着目することにしましょう。 テストスクリプトの先頭に以下のコードを置いて下さい:

grammar XML {
    token TOP   { ^ <tag> $ }
    token tag   {
        '<' (\w+) '>'
        '</' $0   '>'
    }
};

それからスクリプトを実行します:

$ ./perl6 xml-01.pl
not ok 1 - 'abc'
ok 2 - '<a></a>'
not ok 3 - '..<ab>foo</ab>dd'
not ok 4 - '<a><b>c</b></a>'
not ok 5 - '<a href="foo"><b>c</b></a>'
not ok 6 - '<a empty="" ><b>c</b></a>'
not ok 7 - '<a><b>c</b><c></c></a>'
ok 8 - '<'
ok 9 - '<a>b</b>'
ok 10 - '<a>b</a'
ok 11 - '<a>b</a href="">'
not ok 12 - '<a/>'
not ok 13 - '<a />'

つまりこれは一対の開始タグと終了タグのペアを解析する単純なルールであり、妥当でない4つのXMLをきちんと排除できています。

1番目のテストも同様に簡単に通るように、次のコードを試して下さい:

grammar XML {
    token TOP   { ^ <xml> $ };
    token xml   { <text> | <tag> };
    token text  { <-[<>&]>*  };
    token tag   {
        '<' (\w+) '>'
        '</' $0   '>'
    }
};

(<-[...]>は否定形の文字クラスだったことを思い出して下さい)

それから実行します:

$ ./perl6 xml-03.pl
ok 1 - 'abc'
not ok 2 - '<a></a>'
(残りはさっきと同じ)

どうして2番目のテストは動かなくなったのでしょう? その理由はRakudoが最長トークンマッチをまだ実装しておらず、順番にマッチングを行っているからです。 <text>は空文字列に(つまりいつでも)マッチするので、<text> | <tag><tag>とのマッチングを試しません。選択肢の順番を入れ替えると動きます。

しかし我々はプレーンテキストかタグだけを適当にマッチさせたいのではなく、両者のランダムな組合わせをマッチさせたいのでした:

token xml   { <text> [ <tag> <text> ]*  };

([...]はキャプチャしないグループであり、Perl5の(?: ...)と同様です)

いやはや驚くことに、これは最初の2つのテストを両方とも通過します。

3番目のテスト.<ab>foo</ab>ddは開始タグと終了タグの間にテキストがあるので、次はこれを受理しなければいけません。 しかしタグの間に出現できるのはテキストに限らず任意のXMLで在り得るので、<xml>を単に呼ぶことにしましょう:

token tag   {
    '<' (\w+) '>'
    <xml>
    '</' $0   '>'
}

./perl6 xml-05.pl
ok 1 - 'abc'
ok 2 - '<a></a>'
ok 3 - '..<ab>foo</ab>dd'
ok 4 - '<a><b>c</b></a>'
not ok 5 - '<a href="foo"><b>c</b></a>'
(残りはさっきと同じ)

これで属性(href="foo"のやつ)に集中することができます:

token tag   {
    '<' (\w+) <attribute>* '>'
    <xml>
    '</' $0   '>'
};
token attribute {
    \w+ '="' <-["<>]>* \"
};

しかしこれでは新しいテストを通過できるようにはなりません。その原因はタグ名と属性の間にある空白です。 \s+\s*を色んな場所に加える代わりに、tokenrule(:sigspace修飾子を暗黙的にセットします)に切り替えることにします:

rule tag   {
    '<'(\w+) <attribute>* '>'
    <xml>
    '</'$0'>'
};
token attribute {
    \w+ '="' <-["<>]>* \"
};

これで残るテストは最後の2つになりました:

ok 1 - 'abc'
ok 2 - '<a></a>'
ok 3 - '..<ab>foo</ab>dd'
ok 4 - '<a><b>c</b></a>'
ok 5 - '<a href="foo"><b>c</b></a>'
ok 6 - '<a empty="" ><b>c</b></a>'
ok 7 - '<a><b>c</b><c></c></a>'
ok 8 - '<'
ok 9 - '<a>b</b>'
ok 10 - '<a>b</a'
ok 11 - '<a>b</a href="">'
not ok 12 - '<a/>'
not ok 13 - '<a />'

これらは/で閉じられた入れ子になっていないタグを含んでいます。rule tagにこれを追加するのは何の問題もありません:

rule tag   {
    '<'(\w+) <attribute>* [
        | '/>'
        | '>' <xml> '</'$0'>'
    ]
};

全テストが通るようになりました。やった、はじめて作ったグラマーはちゃんと動きます。

さらなるハッキング

グラマーで遊ぶのは遊び方を読むよりずっと楽しいので、これから実装できるものの例を挙げておきます:

  • &amp;のような実体参照を含むことができるプレーンテキスト
  • XMLタグ名が数字で開始して良いのかどうか分かりませんが、現在のグラマーはこれを許しています。必要ならXMLの仕様書を調べてグラマーを改造するのも良いでしょう
  • <![CDATA[ ... ]]>を含むことができるプレーンテキスト。このXML風タグは無視され、<のような文字はエスケープする必要がありません
  • <?xml version="0.9" encoding="utf-8"?>のようなXML宣言を許容し、すべてを包含する単一のルートタグを要求する本物のXML(テストケースをいくつか修正する必要があります)
  • マッチオブジェクト$/を再帰的に走査することでXML用のプリティプリンタを実装できます(これは生半可にはいきません; いくつかRakudoのバグを回避しなければいけないかも知れませんし、キャプチャも新しく導入する必要があるかも知れません)

(解答をこのブログのコメント欄に書かないで下さい; 他の人にも楽しませてあげましょう;-)

ハッキングを楽しんで下さい。

MOTIVATION

強力だし、楽しい

SEE ALSO

正規表現はS05で詳細に規定されています: http://perlcabal.org/syn/S05.html

正規表現とグラマーの動作している(!)例を、Perl6で書かれたWikiエンジンであるNovember projectでもっと見つけることができます。http://github.com/viklund/november/をご覧下さい。

コメント

このブログの人気の投稿

京大テキストコーパスのパーサを書いた

要旨CaboCha やなんかの出力形式であるところの京大テキストコーパス形式のパーサモジュールを Perl で書いたので紹介します。GithubTarball on Github Ppagesこれを使うと例えば CaboCha の出力した係り受け関係を Perl のオブジェクトグラフとして取得できます。使用例単なる文節区切りの例。#!/usr/bin/env perl use v5.18; use utf8; use IPC::Open3; use Parse::KyotoUniversityTextCorpus; use Parse::KyotoUniversityTextCorpus::MorphemeParser::MeCab; use Symbol qw//; my ($in, $out, $err); my $pid; BEGIN { ($in, $out, $err) = (Symbol::gensym, Symbol::gensym, Symbol::gensym); $pid = open3($in, $out, $err, cabocha => '-f1'); } END { close $out; close $err; waitpid $pid => 0 if defined $pid; } binmode STDOUT, ':encoding(utf8)'; binmode $in, ':encoding(utf8)'; binmode $out, ':encoding(utf8)'; my $parser = Parse::KyotoUniversityTextCorpus->new( morpheme_parser => Parse::KyotoUniversityTextCorpus::MorphemeParser::MeCab->new, ); say $in '星から出るのに、その子は渡り鳥を使ったんだと思う。'; say $in '出る日の朝、自分の星の片付けをした。'; close $in; my $sentence_trees = $parser->…

Algorithm::LibLinear の紹介

Notice: This article is outdated. Please refer an updated English tutorial. 要旨かなり前になりますが、Algorithm::LibLinear という Perl モジュールを書きました。CPANGithubこれを使うと線形分類器などが高速に学習できます。テキストや画像の分類が応用として期待されます。LIBLINEAR についてLIBLINEARLIBSVM と同じ台湾国立大学の Chih-Jen Lin 教授のチームが公開しているオープンソースの機械学習パッケージです。 関数のロジスティック回帰、サポートベクター回帰及び線形 SVM による多クラス分類を行うことができます。LIBSVM と違ってカーネル関数を使うことはできませんが、はるかに高速に動作します。Algorithm::LibLinear についてLIBLINEAR には C++ で書かれたライブラリと、その機能を使って機械学習と分類・関数回帰を行うコマンドラインユーティリティが含まれています。 Algorithm::LibLinear はライブラリの機能を Perl からオブジェクト指向的に利用できるようにした上で、コマンドラインユーティリティの一部機能をライブラリ化して Perl で再実装したものです。使い方分類問題を解くときは、訓練データセットの読み込み・スケーリング学習器パラメータの設定分類器の訓練実データの分類という手順で行います。訓練データセットの読み込み正解ラベルのついたデータを大量に用意して学習させます。LIBSVM 形式のデータを読み込むか:my $data_set = Algorithm::LibLinear::DataSet->load(string => <<'EOD'); 1 1:0.1 2:0.1 4:0.1 -1 1:0.1 2:-0.1 3:0.1 ... EOD HashRef として表現されたデータを使います:my $data_set = Algorithm::LibLinear::DataSet->new(data_set => [ +{ feature => +{ 1 => 0.1, 2 => 0.1, 4 =…

OCaml で Web フロントエンドを書く

要旨フロントエンド開発に Elm は堅くて速くてとても良いと思う。昨今の Flux 系アーキテクチャは代数的データ型と相性が良い。ところで工数を減らすためにはバックエンドも同じ言語で書いてあわよくば isomorphic にしてしまいたいところだが、Elm はバックエンドを書くには現状適していない。OCaml なら js_of_ocaml でエコシステムを丸ごとブラウザに持って来れるのでフロントエンドもバックエンドも無理なく書けるはずである。まず The Elm Architecture を OCaml で実践できるようにするため Caelm というライブラリを書いている。俺の野望はまだまだこれからだ (未完)Elm と TEA についてElm というプログラミング言語がある。いわゆる AltJS の一つである。 ミニマリスティクな ML 系の関数言語で、型推論を持ち、型クラスを持たず、例外機構を持たず、変数の再代入を許さず、正格評価され、代数的データ型を持つ。 言語も小綺麗で良いのだが、何より付属のコアライブラリが体現する The Elm Architecture (TEA) が重要である。TEA は端的に言えば Flux フロントエンド・アーキテクチャの変種である。同じく Flux の派生である Redux の README に TEA の影響を受けたと書いてあるので知っている人もいるだろう。 ビューなどから非同期に送信される Message (Redux だと Action) を受けて状態 (Model; Redux だと State) を更新すると、それに対応して Virtual DOM が再構築されビューがよしなに再描画され人生を書き換える者もいた——という一方向の流れはいずれにせよ同じである。 差異はオブジェクトではなく関数で構成されていることと、アプリケーション外部との入出力は非同期メッセージである Cmd / Sub を返す規約になっていることくらいだろうか。後者は面白い特徴で、副作用のある処理はアプリケーションの外で起きて結果だけが Message として非同期に飛んでくるので、内部は純粋に保たれる。つまり Elm アプリケーションが相手にしないといけない入力は今現在のアプリケーションの完全な状態である Model と、時系列イベントである Me…