Perlで文字列をHTML数値文字参照に変換

blosxom関連の記事を辿っている時に、以下の記事を見つけました。

まず述べなければならないのは、HTMLにおける日のような文字参照の形式は数値文字参照numeric character referenceと呼ばれるものであって、実体参照entity referenceではない、ということ。(詳しくは一般実体参照と文字参照を参照)

で、記事を拝見して、Unicode::Escapeも見てみたのですが、このモジュールは元々JavaScriptのUnicodeエスケープ形式を扱うものであるようで、それを更にHTMLの数値文字参照変換のために使うというのは、ややオーバースペックなような気がしました。

* * *

答えの1つは、記事が参照していたはてなでの質問でも既に述べられています。

$str = '日本語文字列';
$str =~ s/(.)/'&#'.ord($1).';'/eg;
print $str;

ただ、この例だとやや不十分で、$strは生のUTF-8文字列でなく、Perlの内部文字列(UTF8フラグつきの文字列)に変換しておく必要があります。そうすることにより正規表現の . が、バイトごとにではなく、Unicode文字ごとにマッチするようになります。

※ 以下に挙げる例は、UnicodeをサポートしたPerl(Perl 5.8以降)を対象とするものとします。

ソースに書かれたUTF-8文字列であれば、use utf8;するだけで内部文字列として扱われます。

use utf8;
$str = '日本語文字列のtest';
$str =~ s/(.)/'&#'.ord($1).';'/eg;
print $str, "\n";

または、utf8::decode関数を使うとか。(参照: utf8::* 関数)

$str = '日本語文字列のtest';
utf8::decode($str);
$str =~ s/(.)/'&#'.ord($1).';'/eg;
print $str, "\n";

入力がUTF-8以外の文字コードであれば、Encodeモジュールのdecodeを使う方法もあります。

use Encode;
$str = decode('shiftjis', $sjis);
$str =~ s/(.)/'&#'.ord($1).';'/eg;
print $str, "\n";

ちなみに十六進の形式にするなら、こんな感じでしょうか。

use utf8;
$str = '日本語文字列のtest';
$str =~ s/(.)/sprintf '&#x%x;', ord($1)/eg;
print $str, "\n";
* * *

これとは別に、Encodeモジュールの機能を使う方法もあります。Encodeモジュールでは、例えば変換しようとする文字列に、変換先の文字エンコーディングで扱えない文字が含まれていた場合、その文字を十進の数値文字参照・十六進の数値文字参照・Perlの文字列エスケープ形式に変換することができます。(参照: Handling Malformed Data)

例えばこんな風に。

# charref.pl
use strict;
use Encode qw(encode decode :fallbacks);

my $utf8 = '日本語文字列のtest';
# 内部形式に変換
my $internal = decode('utf8', $utf8);

# 十進数値文字参照
my $dref = encode('ascii', $internal, FB_HTMLCREF);
# 十六進数値文字参照
my $xref = encode('ascii', $internal, FB_XMLCREF);
# Perl文字列エスケープ
my $pqq = encode('ascii', $internal, FB_PERLQQ);

print $dref, "\n";
print $xref, "\n";
print $pqq, "\n";
$ perl charref.pl
日本語文字列のtest
日本語文字列のtest
\x{65e5}\x{672c}\x{8a9e}\x{6587}\x{5b57}\x{5217}\x{306e}test

上記では、UTF-8文字列をむりやりASCIIに変換することにより、ASCII文字以外の文字のエスケープを実現しています(なのでASCII文字はそのまま残ります)。