[を] 指定した URL へのリンクのアンカーテキストを収集するとそのはてなブックマークエントリで言及されていた、PerlのEncodeモジュールの decode
で "UTF-16LE:Malformed LO surrogate xxxx at ..." というエラーが出る問題を調べてみました。
まずは再現条件を調べたところ、http://quote.yahoo.co.jp/ のページを取得して decode
した時にエラーが発生することが分かりました。エラー再現までの流れは以下の通りです。
decode('UTF-16LE', $text)
を実行。そして再現コードは以下の通り。
use strict;
use Encode;
use Encode::Guess;
use LWP::Simple;
my $text = get('http://quote.yahoo.co.jp/');
Encode::Guess->set_suspects( qw( euc-jp shiftjis 7bit-jis ) );
my $guess = guess_encoding($text);
ref($guess) or die "cannot guess.\n";
my $encoding_name = $guess->name;
print "code: $encoding_name\n";
$text = decode($encoding_name, $text); # ここでエラー
print "decode end.\n";
元々はEUC-JPの文字列なのだから、それをUTF-16とみなして無理に decode
したのがよくなかったようです。
それにしても何故UTF-16なんかに判定されてしまうのかを探ってみたところ、http://quote.yahoo.co.jp/ のページでは先頭のコメント内に 0xFDFE、また末尾の </html>
の後に 0x00 というEUC-JPらしからぬバイト列が含まれており、特に後者の 0x00 があるためにUTF-16LEと判定されていたようでした。(0x00 を 0x20 に変えてみると、ちゃんと euc-jp と判定されました)
さて、原因はそれとしてもこのエラーをどうやって避けることができるでしょうか。できることならば、http://quote.yahoo.co.jp/ の内容を EUC-JP と判定した上でそのように decode
できればよいのですが。
ひとつ手っ取り早い対処方法。今回の例では判定結果が UTF-16LE となったわけですが、日本語で書かれたHTMLを対象とするのであれば、使われる文字コードはせいぜい EUC-JP, Shift_JIS, ISO-2022-JP, UTF-8 くらいであり、UTF-16が使われることは滅多に無いものと思います(別にUTF-16を使っていても、HTML文書としては全く問題ないのですが)。なので文字コード判定の候補からUTF-16を外すことができれば、判定の成功率も上がりそうです。
Encode::Guessでは、元々 UTF-8/16/32 が候補としてチェックされるようになっているのですが、この デフォルトの動作は $Encode::Guess::NoUTFAutoGuess を1にセットすることで無効にすることができます(参照: DESCRIPTION - Encode::Guess)。なので先ほどの再現コードを、
use strict;
use Encode;
use Encode::Guess;
use LWP::Simple;
my $text = get('http://quote.yahoo.co.jp/');
$Encode::Guess::NoUTFAutoGuess = 1;
Encode::Guess->set_suspects( qw(euc-jp shiftjis 7bit-jis utf8) );
my $guess = guess_encoding($text);
ref($guess) or die "cannot guess.\n";
my $encoding_name = $guess->name;
print "code: $encoding_name\n";
$text = decode($encoding_name, $text);
print "decode end.\n";
と変更してみたところ、今度は euc-jp と判定され、decode
もエラーを出すことなく終了できました。
また、Encode::Guess による文字コード判定は確実ではないようなので、もし文字コード識別の手がかりを他から入手できるのなら、それを使った方がよさそうです。http://quote.yahoo.co.jp/ のページでは、meta要素による文字コードの指定はありませんが、HTTPのレスポンスヘッダではきちんと Content-Type: text/html; charset=euc-jp を返しているので、それを取得してdecodeに使うのが(少々手間はかかりますが)確実でしょう。