エスケープ処理された文字列を戻す

POST でパラメータを受け取る で作った echo.cgi に改行や日本語を含むテキストを送信してみると、ちゃんと表示されず「%」が含まれた文字列になってしまいますが、これは文字化けではありません。
例えば、名前(name)に「ぱるも」、本文(body)に「こんにちは(改行)かわいい犬ですね」と書いて送信すると

name  %82%CF%82%E9%82%E0
body  %82%B1%82%F1%82%C9%82%BF%82%CD%0D%0A%82%A9%82%ED%82%A2%82%A2%8C%A2%82%C5%82%B7%82%CB

と表示されます。(Shift-JIS の場合)
これは、パラメータがブラウザによって送信される時に「エスケープ処理」されているからです。TMPL_VAR の ESCAPE 属性でも勉強しましたが、エスケープ処理とは「特殊な記号の意味を打ち消す為の処理」です。この場合の「特殊な記号」とは英数字以外の文字の事で、改行や日本語のマルチバイト文字なども含まれます。
例えば、パラメータの一部として「&」や「=」が入力されていると、パラメータを受け取ったプログラムが正しく解釈する事ができません。もし、「&」や「=」がパラメータの一部に含まれているとすると、以下のような REQUEST_URI でリクエストされた場合

/foo.cgi?title=aaa&body=bbb

title に「aaa&body=bbb」を指定したのか、それとも title は「aaa」までで、別に body が「bbb」として指定されているのか判断できませんよね。
このように、「特殊な意味」を持つ記号はブラウザによってエスケープ処理されてからパラメータに渡される事になっているのです。ただ、どの記号が「特殊な意味」を持っているのか、というのはプログラムによって異なるので、ブラウザの場合は英数字以外の文字は全てエスケープされる事になっています。


エスケープ処理後の文字列に含まれる「%82」や「%CF」の後ろの二文字「82」「CF」は、16進数によって表された「文字コード」ですね。(^_^)
コンピュータは 0 か 1 かのデジタルなものですので、「文字」も内部では数字で表されています。文字に対応する数字の事を、文字の「文字コード」(キャラクターコード)と言います。
1バイトの文字は「0から255の間の数字どれか」で表す事ができますが、この「0から255」というのは、2ケタの16進数(00からFF)で表せる数字の範囲と一致しているので、1バイトの文字を2ケタの16進数で表す事ができるのです。
日本語の1文字は「2バイト」で表されているので、「ぱるも」は 6 バイトで表される事になります。だから「%82%CF%82%E9%82%E0」と、6つの「%xx」が並びます。(Shift-JIS や EUC-JP などの場合)


では、エスケープ処理された文字列を元に戻す処理(アンエスケープ : unescape)を作ってみます。
流れとしては

  1. 文字列の中から「%xx」を見つける
  2. 「%xx」の「xx」の部分を16進数→10進数の数字に変換する
  3. 数字を文字コードとして対応する文字を取得する
  4. 文字列の「%xx」の部分を取得した文字で置き換える

を繰り返せばいいですね。
2. の「16進数→10進数」の変換は、hex 関数でできます。

print hex("FF"), "\n";
print hex("80"), "\n";
print hex("10"), "\n";

実行結果は以下の通りです。

255
128
16

また、3. の「文字コード→文字」の変換は、chr 関数でできます。ちなみに、逆の「文字→文字コード」は ord 関数を使います。
例えば "A" の文字コードは「perl -e "print ord('A')"」で調べてみたところ「65」でした。

print chr(65), chr(66), chr(67), "\n";

実行すると

ABC

と表示されます。
これらの関数を利用して、さらに s/// 演算子/e オプションと組み合わせれば、簡単に変換できますね。(^_^)

use strict;

sub unescape($) {
  my $s = shift;
  $s =~ s/%([0-9A-F]{2})/chr(hex($1))/ieg;
  return $s;
}

my $name = "%82%CF%82%E9%82%E0";
my $body = "%82%B1%82%F1%82%C9%82%BF%82%CD%0D%0A"
         . "%82%A9%82%ED%82%A2%82%A2%8C%A2%82%C5%82%B7%82%CB";

print unescape($name), "\n";
print unescape($body), "\n";

unescape 関数が、アンエスケープ処理を行なっている関数です。「[0-9A-F]{2}」というのは、「0から9とAからFまでの文字が2つ続いてる」という意味なので %xx にマッチします。16進数の部分をキャプチャして、先ほどの2つの関数を使って文字に変換してから置き換えます。
/e オプションを指定しているので「chr(hex($1))」がコードとして評価され、その返り値が置換後の文字列になるのでしたね。(^_^)
このスクリプトの実行結果は以下の通りです。

ぱるも
こんにちは
かわいい犬ですね

うまく変換できました。(^_^)

注意: 文字セットについて

文字と、その文字のコードの対応を決めるのが「文字セット」または「エンコーディング」などと呼ばれる規格です。例えば、上のスクリプトでは「Shift-JIS」という文字セットを利用しました。
「Shift-JIS」は主に Windows で利用される文字セットで、他にも「EUC-JP」や「JIS」など、様々な文字セットが存在します。これらは日本国内でよく使われる文字セットですが、各国が独自の文字セットを持っていたりするので、文字セットは数限りなく存在します。*1
例えば各文字セットで書いた「ぱるも」をエスケープ処理すると

Shift-JIS: %82%CF%82%E9%82%E0
EUC-JP:    %A4%D1%A4%EB%A4%E2
JIS:       %1B%24%42%24%51%24%6B%24%62%1B%28%42

と、全く違うものになります。


厄介なのが、パラメータを受け取る CGI プログラムを作る場合、与えられるパラメータに使われている文字セットが「わからない」という点です。多くのブラウザは、ページの HTML を書くのに使われている文字セットをパラメータの文字セットに利用するので、自分でページを作る場合は予想が付きます。
ですが、もし CGI プログラムを自由に利用できるように不特定多数に公開した場合、どんな文字コードでパラメータが渡されるかは全く予想できません。Perl 内部の文字セットと違う文字セットのパラメータをそのまま使おうとすると、正規表現がマッチしなくなったり、文字化けしたりと、色々な不具合が生じます。
この問題を解消する為には、かの Dan Kogai さんによってメンテナンスされている Encode モジュールを利用して、文字セットを Perl 内部のものに変換する必要があります。Encode::Guess を利用すれば、文字列に使われている文字セットを推測する事ができます。とても便利ですね。(^_^)
Encode モジュールの使い方については、別のエントリで勉強したいと思います。

*1:これらを統一する為に「Unicode」と呼ばれる UTF-8UTF-16 などの統一文字セットが策定されたので、外国語を扱う場合はこちらを利用すればいいですね