北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

HTTP::Response->contentの罠

えっ、と思った現象です。

my $mech = WWW::Mechanize->new;
my $res  = $mech->get('http://〜〜〜〜.jpg');
warn "mech->content: " . length($mech->content) . "\n";
warn "res->content : " . length($res->content)  . "\n";

# 結果
mech->content: 5078
res->content : 4582

Mechanize->contentって、Response->contentを返してるわけじゃないのですね。

まとめ

WEBページから圧縮されたデータを受け取る場合は、以下に気をつけましょう。

  1. HTTP::Response->content は使わない
    • HTTP::Response->decoded_content か WWW::Mechanize->content を使う
  2. LWP::UserAgent->get(:content_file) は使わない
    • WWW::Mechanize->save_content() を使う

HTTP::Response->contentの問題

Mechanizeの_update_pageを見てみると、

    my $content = $res->decoded_content( charset => 'none' );
    $content = $res->content if (not defined $content);

って感じです。ああ、確かにcontentメソッドじゃなくてdecoded_contentメソッドを優先してますね。じゃあ、decoded_contentって何よ、となるわけですが、こいつの実体はHTTP::Messageに居ます。

	if (my $h = $self->header("Content-Encoding")) {
/* ..................................*/
		if ($ce eq "gzip" || $ce eq "x-gzip") {
/* ..................................*/
		    $content_ref = \Compress::Zlib::memGunzip($$content_ref);
/* ..................................*/
		}
		elsif ($ce eq "x-bzip2") {
/* ..................................*/
		    $content_ref = Compress::Bzip2::decompress($$content_ref);
/* ..................................*/
		}
		elsif ($ce eq "deflate") {
/* ..................................*/
		    my $out = Compress::Zlib::uncompress($$content_ref);
/* ..................................*/

なるほど、Content-Encoding指定で圧縮された転送されて来たデータをこいつで元に戻してるのですね*1。ってことは逆に言えば、HTTP::Responseのcontentって、圧縮されたままの状態の値が入ってるわけですか。こりゃー罠だ。

普通我々が欲しいのは展開されたデータですので、Response->decoded_contentを使うかWWW::Mechanize->content経由でdecoded_contentを使うかしたほうが良さそうです。

LWP::UserAgent->getの:content_file指定の問題

LWP::UserAgent->getの:content_fileでコンテンツをファイルに落とせるわけですが、こいつはLWP/Protocol.pmのcollect()辺り見ればわかりますが、Responseオブジェクトを経由せずに飛んで来たデータをファイルにそのまま落とします。

	open(OUT, ">$arg") or
	    return HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
			  "Cannot write to '$arg': $!");
        binmode(OUT);
        local($\) = ""; # ensure standard $OUTPUT_RECORD_SEPARATOR
	while ($content = &$collector, length $$content) {
/* ..................................*/
	    print OUT $$content or die "Can't write to '$arg': $!";
/* ..................................*/
	}

Content-Encoding指定されてる圧縮データを展開する機能はResponseオブジェクトが持ってるので、ファイルに保存されるのは圧縮されたデータです。

この機能はデータをメモリにとらない分効率的なのかもしれないですが、ファイルに保存された圧縮データを展開するにはレスポンスのContent-Encodingヘッダ見ながらやらなきゃいけないので、あまり得策とは言えません。

*1:Base64関連とかも一応やってます。