Perl で 日本語 XML を扱う

perl で日本語を含む XML を扱う場合の留意点(苦労話)をご紹介します。
まだ方法論として体系だったものになっていないのですが、処理をする際になかなか情報が見つからなかったので、参考として未完成のまま公開していきたいと思います。

色々試した環境は、perl 5.6.1 です。perl 5.8 だと色々な問題が多少は改善されているかも知れません。

目次

・文字コードは UTF-8 でなければならない
・読み込みは XML::Simple、書き出しは print でゴリゴリが一番てっとり早い
・Jcode.pm はUTF-8 のコード変換には色々問題があった
・ハイフンの全角半角変換と文字化けの問題 及び 変換用関数

文字コードは UTF-8 でなければならない

perl の場合、どんなXMLパーサーモジュールを使う場合でも、ベースに XML::Parser モジュールが使用されていることが殆どです。

この XML::Parser の仕様により、UTF-8 *以外* でエンコードされたマルチバイト文字が出てくると、(多分大体)そこでエラーになります。
これは、XML 宣言部分に encoding 指定がしてあっても同じです。

Internet Exploler や、その他の XML 解析ツールでは読み込めるのに、perl で扱うとエラーになる、という場合はこれを疑って見て下さい。

読み込みは XML::Simple、書き出しは print でゴリゴリが一番てっとり早い

もし扱うXMLの構造が「シンプル」ならば、XML の読み込み(解釈)には、XML::Simple を使用するのが簡単です。

XMLの階層構造を $xml の中にパックするためのコードは、以下の通りです。

use XML::Simple;
$xs = new XML::Simple(forcearray => 1, keyattr => []);
$xml = $xs->XMLin('filename.xml');

上記のようにパックした構造体から値を取り出すには、以下のようにします。

$nodevalue = $xml->{toptag}->[0]->{innertag}->[0];

書き出しは、少々面倒ですが、

print FILE '<toptag>';
print FILE '<innertag>',$xml->{toptag}->[0]->{innertag}->[0],'</innertag>';
print FILE '</toptag>';

等とします。

XML::Simple モジュールにも書き出しメソッドがあり、

$xml_changed = $xs->XMLout($xml);

等とすれば、$xml に格納されている構造体を XML 形式のコードにしてくれます。
XML宣言を自動で付けたり、ファイルに直接書き出すこともできます。

但し、書き出しの際、違う名前の兄弟ノードの順番は指定できません。($xml の内部ではハッシュとして情報を格納しているので、その順番が記録できない。)
業務システムを作成する場合、XMLのノードの順番は指定しなければならないことが多いので、その点で XML::Simple での書き出しは不適です。

その他、XML::Simple で単純に書き出せない理由としては、

値の更新は、日本語を含む文字列の場合、UTF-8 で指定しなければならないが、UTF-8 のエンコーディングを適切に指定できる適当な手段が見つからない。

という点が挙げられます。

XML::Simple モジュールを使用している場合、XMLの値を更新する場合は、

$xml->{toptag}->[0]->{innertag}->[0] = 'newvalue';

としてやれば良いのですが、これが日本語だとうまくいきません。なぜだか原因が特定できていませんが、Jcode.pm をコード変換に使用すると、UTF-8 に変換した文字列を直接 print 文などでファイルに書き出すことはできても、上記のように構造体の中に代入すると、これを書き出した時は文字化けしました。

日本語が文字化けしないようにするには、

$xml->{toptag}->[0]->{innertag}->[0] = pack('U', $code_number);

とするくらいしか手段が見つからなかったのですが、これだとコードが既知の文字しか使えないので、実用にはなりません。

このため、今のところ、「print でゴリゴリが一番手っ取り早い」という結論になりました。

なお、最初に書いた「扱うXMLの構造が『シンプル』ならば」という条件の要点は、以下の2点となります。

A. DOM方式で解析できるくらいファイルサイズが小さく
B. 1つのタグの中には、他のタグか、またはテキストのどちらかしか含まない

A は メモリが 96MB の FreeBSD 環境で 1.5MB 位のファイルサイズのXMLは何とか解析できました。

B については、例を挙げると以下のとおりです。

XML::Simple で扱い可)

<root>
<top id="1234">
    <second>値</second>
    <third>
        <fourth>1234</fourth>
        <fifth>myvalue</fifth>
    </third>
</top>
</root>

XML::Simple で扱い不可)

<root>
<top id="1234">
    <second>テキスト値と<subtag>マーカー的なタグ</subtag>の混在</second>
    <third>
        <fourth>1234</fourth>
        <fifth>myvalue</fifth>
    </third>
</top>
</root>

Jcode.pm はUTF-8 のコード変換には色々問題があった

Jcode.pm のドキュメント自体に「Unicode support by Jcode is far from efficient!」と書いてあるとおり、Jcode.pm を使用してコード変換すると、上記の通り print 文で書き出す場合でも、文字化けする文字があります(例えば「﨑」など。U+FA** で表されるコードの文字付近に問題が出やすいようです。)。

原因がよく分らない(というより、真面目に調べていない)のですが、とりあえず、Shift_JIS <=> UTF-8 の変換を行う場合以下のモジュールで解決できました。(Jcode.pm を使用すると文字化けしていた文字については問題が解消された、ということですが。)
CPAN で公開されている他、ActivePerl 版もあります。

ShiftJIS::CP932::MapUTF
ShiftJIS::X0213::MapUTF

EUC-JP <=> UTF-8 の方は、現状未解決のままです。(文字化けする文字が出てこない事を祈るくらい…^^;)

【参考:「文字コード」と「文字集合」などの説明】
コード変換時に文字化けが起こるという現象の基本的な説明として分りやすいページでした。

http://www.debian.or.jp/~kubota/unicode-symbols-map2.html

ハイフンの全角半角変換と文字化けの問題 及び 変換用関数

業務システム(特に名刺情報を扱うようなシステム)を作るとしばしば必要になるのが、「英数字の全角半角変換」です。

入力形式チェックのためにユーザの入力の正規化を行ったり、「数字の入力は半角だが、印刷する時は読みやすいように全角にする」など、色々な場面で必要になります。

変換の対象となるデータは、郵便番号、電話番号、メールアドレスなどが多いですが、この時厄介なのが、ハイフンの変換です。

コンピュータ画面上で「半角ハイフン」に見える文字(コード)というのはいくつかあるのですが、その中に普通に印刷にかけると「?」に化けてしまうものがあり、Jcode の tr メソッドを使用して全角ハイフンを半角ハイフンに変換するとこのような文字化けする半角ハイフンに変換されます。画面上は変換されたように見えますが、いわゆる半角ハイフンにはなっていません。
(そういえば全く関係の無い話ですが、Mac OS X の Netscape 7.0 から、全角ハイフンをURLエンコードしてフォーム送信すると、受け側の CGI のデコード+半角変換結果がいわゆる半角ハイフンにならない、という問題があったような気がします。これもおそらく根が同じ問題でしょう。ちなみに同じ Mac のInternet Exploler からの送信ではそのような問題は出ませんでした。)

…前置きが長すぎましたが、絶対必要なのに Jcode でさくっと変換はできない、困ったなあということで、以下のような関数(package)を作りました。

ベタベタですが、漢字やひらがななどが混在する場合の変換もできました。
「@」マークなど、他に変換が必要な文字があれば、指定を追加して使って下さい。

package zen_han;

use utf8;
use vars qw($bou_utf8 $slash_utf8 %ascii2zen_utf8 $zen_space_utf8);

#変換対象のアスキー文字と、全角文字のUnicodeコードを指定する

#以下の3つは、アスキー文字がハッシュのキーにできないので、個別に指定
$bou_utf8 = pack('U', '65293'); #全角ハイフン
$slash_utf8 = pack('U', '65295'); #全角スラッシュ
$zen_space_utf8 = pack('U', '12288'); #全角スペース

#ハッシュのキーにできるものは、ハッシュに指定
%ascii2zen_utf8 = (
    '1' => pack('U', '65297'),
    '2' => pack('U', '65298'),
    '3' => pack('U', '65299'),
    '4' => pack('U', '65300'),
    '5' => pack('U', '65301'),
    '6' => pack('U', '65302'),
    '7' => pack('U', '65303'),
    '8' => pack('U', '65304'),
    '9' => pack('U', '65305'),
    '0' => pack('U', '65296'),
    'A' => pack('U', '65313'),
    'B' => pack('U', '65314'),
    'C' => pack('U', '65315'),
    'D' => pack('U', '65316'),
    'E' => pack('U', '65317'),
    'F' => pack('U', '65318'),
    'G' => pack('U', '65319'),
    'H' => pack('U', '65320'),
    'I' => pack('U', '65321'),
    'J' => pack('U', '65322'),
    'K' => pack('U', '65323'),
    'L' => pack('U', '65324'),
    'M' => pack('U', '65325'),
    'N' => pack('U', '65326'),
    'O' => pack('U', '65327'),
    'P' => pack('U', '65328'),
    'Q' => pack('U', '65329'),
    'R' => pack('U', '65330'),
    'S' => pack('U', '65331'),
    'T' => pack('U', '65332'),
    'U' => pack('U', '65333'),
    'V' => pack('U', '65334'),
    'W' => pack('U', '65335'),
    'X' => pack('U', '65336'),
    'Y' => pack('U', '65337'),
    'Z' => pack('U', '65338'),
    'a' => pack('U', '65345'),
    'b' => pack('U', '65346'),
    'c' => pack('U', '65347'),
    'd' => pack('U', '65348'),
    'e' => pack('U', '65349'),
    'f' => pack('U', '65350'),
    'g' => pack('U', '65351'),
    'h' => pack('U', '65352'),
    'i' => pack('U', '65353'),
    'j' => pack('U', '65354'),
    'k' => pack('U', '65355'),
    'l' => pack('U', '65356'),
    'm' => pack('U', '65357'),
    'n' => pack('U', '65358'),
    'o' => pack('U', '65359'),
    'p' => pack('U', '65360'),
    'q' => pack('U', '65361'),
    'r' => pack('U', '65362'),
    's' => pack('U', '65363'),
    't' => pack('U', '65364'),
    'u' => pack('U', '65365'),
    'v' => pack('U', '65366'),
    'w' => pack('U', '65367'),
    'x' => pack('U', '65368'),
    'y' => pack('U', '65369'),
    'z' => pack('U', '65370'),
);

#半角=>全角変換
sub utf8_han2zen($){
    my $str = shift;
    $str =~ s/-/$zen_han::bou_utf8/g;
    $str =~ s@/@$zen_han::slash_utf8@g;
    foreach (keys %zen_han::ascii2zen_utf8){
        $str =~ s/$_/$zen_han::ascii2zen_utf8{$_}/g;
    }
    return $str;
};

#全角=>半角変換
sub utf8_zen2han($){
    my $str = shift;
    $str =~ s/$zen_han::bou_utf8/-/g;
    $str =~ s@$zen_han::slash_utf8@/@g;
    foreach (keys %zen_han::ascii2zen_utf8){
        $str =~ s/$zen_han::ascii2zen_utf8{$_}/$_/g;
    }
    return $str;
};

1;

更新履歴

2004-04-20 初出 「文字コードは UTF-8 でなければならない」公開
2004-04-21 「読み込みは XML::Simple、書き出しは print でゴリゴリが一番てっとり早い」追加
2004-04-24 「Jcode.pm はUTF-8 のコード変換には色々問題があった」追加
2004-05-28 「ハイフンの全角半角変換と文字化けの問題 及び 変換用関数」追加