カテゴリ: プログラミング

今担当しているサイトでは、いろんなニュース提供元から送られてくる、様々な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を使っておくのが無難そうですね。

このエントリーをはてなブックマークに追加

仕事で久々にTime::Pieceを扱っていて、localtimeの取り方がちょっと良くわからなかったので調べてみました。

事の発端はデータの時刻が一部ずれるという問題で、どうもlocaltimeへ変換するところに問題が有りそうです。実際にきているデータ例は以下のような感じ。
%Y-%m-%d %T+09:00
%Y-%m-%dT%T JST
%Y-%m-%dT%T+09:00
%Y-%m-%dT%T+0900
%Y-%m-%dT%T+9:00
%Y-%m-%dT%T-07:00
%Y-%m-%dT%T-08:00
%a %B %d %T %Y +0900
%a %b %d %T JST %Y
%a,%d %b %Y %T +0900
%a, %d %B %Y %T +0900
%a, %d %B %Y %T GMT
%a, %d %b %Y %T +09:00
%a, %d %b %Y %T +0300
※一部割愛
まぁいろんなケースがあります。でこの形式の時間をこんな感じで処理してました。
my $t = eval { Time::Piece->strptime($str, '%Y-%m-%d %T+09:00') } ||
        eval { Time::Piece->strptime($str, '%Y-%m-%dT%T JST') } ||
        eval { Time::Piece->strptime($str, '%Y-%m-%dT%T+09:00') } ||
        eval { Time::Piece->strptime($str, '%Y-%m-%dT%T+0900') } ||
        eval { Time::Piece->strptime($str, '%Y-%m-%dT%T+9:00') } ||
        eval { Time::Piece->strptime($str, '%Y-%m-%dT%TZ') } ||
        eval { Time::Piece->strptime($str, '%a %B %d %T %Y -0800') } ||
        eval { Time::Piece->strptime($str, '%a %b %d %T JST %Y') } ||
        eval { Time::Piece->strptime($str, '%a %d %b %Y %T -0800') } ||
        eval { Time::Piece->strptime($str, '%a,%d %b %Y %T +0900') } ||
        eval { Time::Piece->strptime($str, '%a, %d %B %Y %T +0900') } ||
        eval { Time::Piece->strptime($str, '%a, %d %B %Y %T GMT') } ||
        eval { Time::Piece->strptime($str, '%a, %d %b %Y %T +09:00') } ||
        eval { Time::Piece->strptime($str, '%a, %d %b %Y %T +0300') };
   #一部割愛
$tにはTime Zoneまで考慮された時刻が渡っているという想定です。が、実際は違ってました。

use strict;
use warnings;
use Time::Piece;

sub parse_time {
    my ($time, $format) = @_;
    my $t = Time::Piece->strptime($time, $format);
    warn $t->strftime('%Y-%m-%d %T');
}

parse_time("2012-07-12 18:00:00+09:00", '%Y-%m-%d %T+09:00');
parse_time("2012-07-12 18:00:00+03:00", '%Y-%m-%d %T+03:00');
parse_time("2012-07-12 18:00:00-07:00", '%Y-%m-%d %T-07:00');
 上記コードはタイムゾーン指定が±hh:mmな日付をstrptimeでパースして、グリニッジ標準時の時刻を返します。実際実行してみると・・
2012-07-12 18:00:00 at time-piece-test.pl line 8.
2012-07-12 18:00:00 at time-piece-test.pl line 8.
2012-07-12 18:00:00 at time-piece-test.pl line 8.
ローカルタイムゾーンが全く無視された、GMTとしての2012-07-12 18:00:00が帰ってきます。
ちなみに、こっちだと意図した結果が帰ってきます。
parse_time("2012-07-12 18:00:00+0900", '%Y-%m-%d %T%z');   # print 09:00:00
parse_time("2012-07-12 18:00:00+0300", '%Y-%m-%d %T%z');   # print 15:00:00
日付フォーマットを規定したISO8601では、タイムゾーンの定義はこんな感じ
ISO8601
TZD = time zone designator (Z or +hh:mm or -hh:mm)  
perldocにも書いてありますが、Time::Pieceの時刻フォーマットはFreeBSDコマンドのstrftimeに準拠していて、ISO8601ではなくて、RFC822に依存します。
RFC822
zone        =  "UT"  / "GMT"       ; Universal Time
                                              ; North American : UT
            /  "EST" / "EDT"           ;  Eastern:  - 5/ - 4
            /  "CST" / "CDT"           ;  Central:  - 6/ - 5
            /  "MST" / "MDT"          ;  Mountain: - 7/ - 6
            /  "PST" / "PDT"           ;  Pacific:  - 8/ - 7
            /  1ALPHA                 ; Military: Z = UT;
                                             ;  A:-1; (J not used)
                                             ;  M:-12; N:+1; Y:+12
            / ( ("+" / "-") 4DIGIT )    ; Local differential
                                             ;  hours+min. (HHMM)
ということで、HH:MMな表記はHHMMに直さねばなりません。そして%zで受ける必要があります。ちょと不思議なのは、Time::Pieceでこれが出来ないことです。※7/24追記 下記のDateTime::Format::Strptimeはできる
parse_time("2012-07-12 18:00:00 JST", '%Y-%m-%d %T %Z'); # error!
これは自前で±HHMMに置き換えるしか無いのかなぁ。

なおDateTime::Format::Strptimeでも同じような事ができますが、同じく正しいのは一番下のみ下の2つになります。7/24追記※GMTのケースは%Zの前にスペースが無いだけでした
use strict;
use warnings;
use DateTime::Format::Strptime;

sub parse_time {
    my ($time, $format) = @_;
    my $d = DateTime::Format::Strptime->new(pattern => $format)->parse_datetime($time);
    $d->set_time_zone("UTC");
    warn $d->strftime('%Y-%m-%d %T %Z');
}

parse_time("2012-07-12 18:00:00+09:00", '%Y-%m-%d %T+09:00');
#parse_time("2012-07-12 18:00:00 GMT", '%Y-%m-%d %T%Z'); 7/24訂正
parse_time("2012-07-12 18:00:00 GMT", '%Y-%m-%d %T %Z'); 
parse_time("2012-07-12 18:00:00+0900", '%Y-%m-%d %T%z');
問題を解りづらくしていたのは以下の様なコードの時に

eval { Time::Piece->strptime($str, '%Y-%m-%d %T+09:00') } ||
eval { Time::Piece->strptime($str, '%Y-%m-%dT%T JST') }


2011-07-12 18:00:00+09:00のような日付が渡ってきた場合、パースはできるけど、Time Zoneを考慮しないという点でしょう。ちょっとはまりました。

このエントリーをはてなブックマークに追加

Windows Vistaがロールアウトしてから、もうすぐ半年。 なかなか企業での導入が進みません。 理由としては
  • マルチメディア機能など、企業にとってはどうでも良い
  • アプリケーションのサポート状況が固まっていない
  • セキュリティが不安
  • 文字コードの問題
などなどがあげられます。 本格的に導入が始まるのは、SP1が出たあと(おそらく来年初頭)になるでしょう。 が、我々エンジニアはそうも言っていられません。 ちらほらVista対応なんて言葉が出てくるはずです。 特に意識すべきは文字コードの問題です。 これが結構な大問題。 1.XPとVistaでは文字の見え方が違う 2.一部の文字がサロゲートペア という状態です。 1はいいんですけど、2が大問題です。 まずは1から簡単に説明します。 したのPDFはMicrosoftから出ているグリフの違いに関するドキュメントです。 右がXPで表示したとき、左がVistaです。 これはXPではJIS X 0208、VistaではJIS X 0213という文字コードが使われているからなんですけど、 正しいのはVistaの方です。 JIS X 0213:2004に関する資料は、Microsoftのこのページからダウンロードできます。 JIS X 0208が間違えているんですね。ソレを0213で修正したわけなんですけど、 今企業にあるのはほとんどがXPなわけだし、環境によって見え方が違うのはちょっと気持ち悪いですね。 これはモジコードの問題なので、XP用の0213の文字セットもあるし、 Vista用の0208の文字セットもあるので、このことが説明できれば良いでしょう。 Windows XP および Windows Server 2003 向け JIS2004 対応 MS ゴシック &MS 明朝フォントパッケージ Windows Vista 向け JIS90 互換 MS ゴシック & MS 明朝フォントパッケージ 問題は2です。サロゲートペア。 あ〜もう!って感じです。 何が困るかという話の前にUnicodeについてお話させてください。 1980年代、コンピュータ上で多言語をシームレスに扱えるようにしようという動きがありました。 提唱はゼロックスで、協力した企業として、マイクロソフト、アップル、IBM、サン・マイクロシステムズ、ヒューレット・パッカード、ジャストシステムなどが参加したユニコードコンソーシアムによって策定されました。 今は、ISO/IEC 10646として国際標準になっています(厳密にはUCSのことを定義している)。 で、このUnicode。 16ビットを使って一文字を表現しています。 16ビットで表現できるデータは65,536ですね。 問題は、65536文字で全ての言語の文字を表現できるか?って事です。 むろん出来るわけない。全然足りなくて、空いた2万文字の争奪戦が勃発したぐらいです。 このUnicodeをUnicode1.0といいます。 ではどうするか?というと、 さらに16ビット追加して、32ビットで一文字を表現する領域を作ったわけです。 無論今までの16ビットで表現できる文字はそのままです。 と言う事は、Unicode2.0には、16ビットで一文字をあらわすものと、32ビットで一文字をあらわすものが混在する、可変長な文字セットになっています。 この32ビットであらわす方法をサロゲートペアって言います。 可変長な文字扱うのって大変なんですよぉ・・ヽ(´Д`;)ノアゥア... そんなの関係あるの?と思った方。 それが大アリなのですよ! 例えばC#でcharに文字を代入するばあい、 char hoge = '[非サロゲートペア文字]' はOKですが、 char hoge = '[サロゲートペア文字]' はコンパイルエラーになります。 なぜか?というと・・・charが16ビットまでだからですねぇ。 じゃあ、どうするのか? String piyo = "[サロゲートペア文字]" こうするしかないのです。 もーね、コレが同いう事を意味するかプログラマならわかると思うのですが、piyo.lengthとか当てにならないわけですよ。 ほんと、「あ〜ぁ」って感じ(*´д`) 我らが愛する正規表現も影響を受けます。 バイト単位で見るため非サロゲートペア2文字 = サロゲートペア一文字でマッチしてしまう可能性は、大いにあります。回避策はちゃんとあるんですけどね。 プログラミングだけじゃないです。 インフラストラクチャレベルも影響を受けます例えば、RDB。 データ格納するだけならいいんですが、検索やキーに指定した場合は同様に問題が起こります。 アプリの実装に関しては、JIS X 0213:2004 / Unicode 実装ガイドが役に立ちそうです。

Powered by ScribeFire.

このエントリーをはてなブックマークに追加

おなじみscriptaculous.jsでスライダーインターフェースを実現するControl.Slider。 すっごく便利なんだけど、なぜか1pxしかスライドしないバグで1日はまってしまいました。 どうやら下記のようにスタイルのvisibilityやdisplay非表示にすると駄目らしい。
<div id="slide" style="display: hidden;">
<div id="track1" style="margin: 40px; height: 200px; background-color: rgb(170, 170, 170); width: 1px;">
<div class="selected" id="handle1" style="width: 10px; height: 5px; background-color: rgb(255, 0, 0); position: relative; top: 25px;"></div>
 </div>
</div>
まだ確認が取れていなくて、私の予測でしかないのですが、 おそらく、 new Control.Slider()された時点で、画面上で非表示になっていると、 スライド幅(デフォルトではスライドするトラックのピクセル長)が取得できず、上手く動かないようだ。 オプションのmaximumあたりの指定で回避できるかも知れないけど、 あまり頻繁に遭遇するシュチュエーションではないので、未確認。 取り合えず回避策としては初期表示の際はdisplay: block;にしておいて、 onload()で$('id').style.display=hidden;にすることで回避。 onloadにあまり局所的なコード入れたくないけど、一応応急対処。 オプション辺り試してみて回避可能であれば追記します。 でもこんな使い方するひといるのかな〜?
このエントリーをはてなブックマークに追加

はてな勉強会で紹介されていた、scriptaculous.jsがすっごく便利そうだったので、紹介されていた サンプルを作って実際に動かしてみた。 ん〜、便利♪♪ 一部のエフェクトでちょっと変なエフェクト(線が表示されたり)が掛かるけど、 適当に作っただけなので、ちゃんと作れば綺麗にできるのかも。 まだ一部のライブラリしか使ってないので、週末にはAjaxあたりいじってみようかな♪ Draggableとか、Ajax.InPlaceEditorなんかすっごく使えそうだし。 ちなみにSample画面の起動が異常におそいのは、とりあえずすべてのライブラリを読み込んでいるからです。 いくら便利だからって欲張りすぎるとこうなります。って悪い見本と言うことで・・・
このエントリーをはてなブックマークに追加

↑このページのトップヘ