今担当しているサイトでは、いろんなニュース提供元から送られてくる、様々なXMLをParseしていて、内容も様々なので、色々なエラーが出ていました。

チーム内でも軽微なエラーならスルーされていたところだったので、 ここ半月ぐらい、cronで飛んでくるエラーをながめ、一つ一つ調査、対応してきました。

その過程で感じた事とかやったことをまとめておこうかと思います。

まず思ったことは、

人類はXMLを全然使いこなせてない

ってことです。大げさに聞こえるかもしれませんが、そもそもXML文書として不正なものや、 昨日まで問題無かったのに、突如エラーになるfeed、予告無しにプチリニューアルするフォーマット、 当然の如く入り混んでくる制御文字など、色々なパターンでエラーになります。 

ということで、外部のXMLをParseする際に気をつけた方が良いと思う事をまとめます。

フォーマットが指定できるなら出来るだけ指定する

既にあるAPIやfeedを読むだけだとこれは難しいですが、新規のシステム連携などの場合は、 コチラが欲しいXMLは出来るだけフォーマットを用意しておいた方が良いです。

色々なパターンに対応しなくてすむし、対応するにしてもかなり場所を局所化できます。 カッチリフォーマット決めても守ってくれない or 間違う事も結構あります。

ちょっとした事でもコチラで対応するのではなく提供側に直して貰いましょう。ルールは厳格に運用しないと意味が無くなります。

当たり前だと思われるかもしれませんけど、これ実際にカッチリやるのかなり難しいんですよね・・・

CDATAセクションを確認

嘘のような話しですが、コンテンツ部分に生のhtmlがそのまま入ってくる事があったりします。CDATAセクションにして上げればいんですが、 そのままスルーするとhtmlがXMLのコンテンツとして解析されて変な事になります。かならずCDATAセクションに修正して貰いましょう。

リトライ処理を必ずいれる

ネットワーク越しだと、アクセスすれば必ずデータが帰ってくるということは保証されません。 URIにアクセスするような処理は、かならずリトライを入れましょう。

それでも失敗するケースもあるので、その場合はエラー出力しておきましょう。

数値参照や実体参照でない"&"は変換

"&"は特殊な文字扱いで、&などの実体参照形式に変換しないといけません。が、本来の"&"の用途でそのまま変換されずに入ってくることがよくあります。 そのままParseするとエラーになっちゃうことが多いと思うので、しっかり& -> &にしてあげましょう。

その際、元々あった実体参照をくずさないように、ちょっとだけ工夫してあげる必要があります。以下perlの例です

$contents =~ s/&(?![#0-9a-zA-Z]+;)/&/g;

制御コードを一部以外削除する

その制御コードどうやって入力した!?ってコードが入ってくる事があります。asciiで指定されている制御コードは0x01~0x1fと0x7fがあります。 このうち0x09は水平タブ、0x0aは改行(LF)なので残してあげないと文書がおかしくなります。windows環境なら0x0d(CR)も残してあげても良いです。

Perlでparseするとその他の制御コードは

not well-formed (invalid token) at line 4, column 25, byte 99 at /usr/local/lib64/perl5/site_perl/5.8.8/x86_64-linux/XML/Parser.pm line 187

なんて感じで怒られたりします。

$contents =~ s/[\x01-\x08\x0b-\x1f\x7f]//g; # delete control code

エラーは捨てない

cron等でparseする際、ついついエラーを" > /dev/null 2>&1"しちゃったりするかもしれませんが、予期せぬエラーが絡むので、これは勿体ないです。かならずエラーを通知するようにしましょう。

すぐに見れなくても、いつか見るor誰かがきっと見て参考にします。

PerlでXMLをparseする際のパーサー選択

XML::Simpleを使っていたりするとParser部分を指定することができます。

大分昔にid:naoyaさんがXML::Simple におけるパーサーの実行速度比較ベンチ取っていたのですが、もう7年も経っているので改めてベンチしてみました。コードはリンク先のものを使わせて貰っています。

ついでにXMLのファイルのサイズも2パターンやってみました 

軽量のXML(5.3k)をparseした結果

Benchmark: timing 1000 iterations of XML::LibXML::SAX, XML::Parser, XML::SAX::Expat, XML::SAX::ExpatXS...
XML::LibXML::SAX:  5 wallclock secs ( 4.66 usr +  0.01 sys =  4.67 CPU) @ 214.13/s (n=1000)
XML::Parser:  2 wallclock secs ( 2.24 usr +  0.00 sys =  2.24 CPU) @ 446.43/s (n=1000)
XML::SAX::Expat:  7 wallclock secs ( 7.21 usr +  0.00 sys =  7.21 CPU) @ 138.70/s (n=1000)
XML::SAX::ExpatXS:  3 wallclock secs ( 2.75 usr +  0.00 sys =  2.75 CPU) @ 363.64/s (n=1000)

大きめのXML(1.5M)をparseした結果

Benchmark: timing 100 iterations of XML::LibXML::SAX, XML::Parser, XML::SAX::Expat, XML::SAX::ExpatXS...
XML::LibXML::SAX: 819 wallclock secs (818.18 usr +  0.27 sys = 818.45 CPU) @  0.12/s (n=100)
XML::Parser: 23 wallclock secs (23.86 usr +  0.01 sys = 23.87 CPU) @  4.19/s (n=100)
XML::SAX::Expat: 84 wallclock secs (83.18 usr +  0.00 sys = 83.18 CPU) @  1.20/s (n=100)
XML::SAX::ExpatXS: 31 wallclock secs (31.56 usr +  0.00 sys = 31.56 CPU) @  3.17/s (n=100)

小さいファイルの方でXML::LibXML::SAXはもうちょっと早いかと思ったんですが、XML::Parserの半分の速度でした。 しかも、ファイルサイズが大きくなるとXML::LibXML::SAXはかなり遅くなってしまいます。

XML::LibXML::SAXのエラー表示は詳しくて良いんですが、ここまで差がつくならXML::Parserを使っておくのが無難そうですね。