Parse::RecDescentを使って .iniファイルのパーサを書く

Parse::RecDescentを一回ぐらい使っておこうかなということで使ってみた。


書いてみたのは .iniファイルのパーサです。たくさんモジュールがあるし、
実際に使う気があるわけでもなく、ただ練習ということで。

iniの仕様

書くにあたってわかったのですが、.iniファイルって厳格な仕様があると
いうわけではないんですね。なんで、Config::INIの PODに書いてある
BNFを参考に、若干簡略化したものになってます。文字列の正規表現はいい加減です。

コード

#!/usr/bin/env perl
use strict;
use warnings;

use Config::INI;
use Parse::RecDescent;

## for debug
#$::RD_HINT = 1;
#$::RD_TRACE = 1;

my $grammer =<<'__GRAMMER__';
INIFILE : block(s)
          {
                my $obj;
                for my $block (@{$item[1]}) {
                    if(ref $block && ref $block eq 'ARRAY') {
                         my ($key, $value) = @{$block}[0,1];
                         $obj->{$key} = $value;
                    }
                }
                $return = $obj;
          }

block : empty_line
      | section

section : section_header value(s?)
        {
              my $section = {};
              for my $assignment (@{$item[2]}) {
                     my ($key, $value) = @{$assignment}[0,1];
                     $section->{$key} = $value;
              }
              $return = [$item[1], $section];
        }

section_header : "[" section_name "]" comment(?)
               { $return = $item[2] }

section_name   : string

value : value_assignment
      | empty_line

value_assignment : property_name "=" value comment(?)
                   { $return = [ $item[1], $item[3] ]; }

property_name : string
value : string_with_space

comment : /;[^\n]+/ { $return = 0; }

string : /[A-Za-z0-9-!@.'"]+/
string_with_space : /[A-Za-z0-9-!@.'" ]+/
         {
              (my $str = $item[1]) =~ s{\s+$}{};
              $return = $str;
         }

empty_line : /\s+/
           | comment
__GRAMMER__

my $ini =<<'__INI__';
; last modified 1 April 2001 by John Doe
[owner]
name=John Doe
organization=Acme Widgets Inc.

[database]
server=192.0.2.62     ; use IP address in case network name resolution is not working
port=143
file = "payroll.dat"
__INI__

my $parser = Parse::RecDescent->new($grammer);

my $result = $parser->INIFILE($ini);
use YAML;
die YAML::Dump($result);

結果

上記を実行した結果以下のようになります。期待したとおりです。

---
database:
  file: '"payroll.dat"'
  port: 143
  server: 192.0.2.62
owner:
  name: John Doe
  organization: Acme Widgets Inc.

評価

Config::Tinyと比較してみました。

use Benchmark qw(cmpthese);
use Config::Tiny;

my $parser = Parse::RecDescent->new($grammer);

cmpthese(-10, {
    parser => sub { my $result = $parser->INIFILE($ini); },
    tiny   => sub { my $result = Config::Tiny->read_string($ini);},
});

結果は以下の通りです。

          Rate parser   tiny
parser   338/s     --   -97%
tiny   13246/s  3814%     --

Config::Tinyに比べずいぶん遅いことがわかりました。
Devel::NYTProfで全体を測ってみると, string evalしまくってることが
遅いみたいです。中を見ていないので詳しいことはわかりません。

最後に

Parse::RecDescentで iniパーサを作成しました。CPANには多くの
データフォーマットの解析モジュールがあるので、自分でわざわざ
何かを書くという必要はあまりないかもしれませんが、こういうのを
知っておくといざというとき役立つかもしれないですね。


こんなプログラムを書いたのは大学のコンパイラの授業以来な
気がします。