Wide character 警告と文字化けを解消しよう
Perl で日本語を扱うと必ず遭遇するのが "Wide character" の警告でしょう。 そして、この警告を解決しようとネットで調べて試行錯誤してもなかなか解決しなかった経験をしたことがあるのではないでしょうか。 逆に、試行錯誤した結果、理由が分からないままなぜかうまくいってしまった、なんてこともあるでしょう。
とりわけ歴史が浅い新しいプログラミング言語ではさほど日本語で悩むことはありません。 一方で、Perl は歴史が長いにも関わらず下位互換性を保ってきたという事情から、 日本語などのマルチバイトの扱いにややクセがあります。
ここでは、Perl のややこしいマルチバイトの扱いについて、分かりやすく解説します。
Perl はデフォルトではソースコードに含まれる日本語を正しく認識していません。 文字というよりかはバイナリーデータかのように認識しています。 その分かりやすい例は文字の数のカウントでしょう。 次の例は「こんにちは」の文字数を出力するコードですが、ソースコードは UTF-8 で保存されているとします。
#!/usr/bin/env perl
my $hello = 'こんにちは';
print length($hello), "\n"; # 15
このコードの結果として 5 と表示されることを期待するでしょうが、実際には 15 と表示されます。 これは「こんにちは」の UTF-8 エンコーディングのバイト数です。 つまり、このコードを実行する際に、Perl はこの文字列をバイト列としてみなしているということになります。
新しい他のプログラミング言語とは挙動が違いますね。これは Perl の出来が悪いという意味ではありません。
当然ながら、Perl でも他のプログラミング言語と同様に、文字列を文字の列として認識することが可能です。
コード内の文字列を文字の列として Perl に認識させるには Encode モジュールの
decode
関数を使います。
#!/usr/bin/env perl
use Encode;
my $hello = 'こんにちは';
print length(Encode::decode('UTF-8', $hello)), "\n"; # 5
上記コードでは、Encode モジュールの decode
メソッドが
$hello のバイト列を UTF-8
の文字列として読み込み、
それを内部エンコーディングに変換しています。
この内部エンコーディングに変換された文字列を 内部文字列 と呼びます。
内部エンコーディングの内部文字列とは、 Perl が文字の列として認識するために都合が良いエンコーディングの文字列です。 実際にそれがどのようなエンコーディングなのかを利用者は知る必要はありません。 UTF-8 かもしれませんし、UTF-16 かもしれませんし、それ以外かもしれません。 いずれにせよ、それが何なのかを知らなくても問題がないように作られています。
前述のサンプルコードでは、Encode モジュールの decode
関数によって
$hello のバイト文字列が適切に文字の列として Perl に認識されるようになりました。
そのため、length
関数では期待通りに文字数を返すことができたわけです。
文字列を文字の列として認識させるために、わざわざ Encode モジュールの decode
関数を使うのは面倒です。もちろん、通常はそのような面倒をする必要はありません。
それを解決するのは utf8 プラグマです。
実は、Perl はデフォルトではソースコードがどんな文字エンコーディングで書かれているのかを認識していません。
そのため、文字列を文字の列ではなくバイト列として認識しているのは前述の通りです。
それを解決するのは utf8 プラグマです。use utf8
のことです。
次の例は、utf8 プラグマを使って文字列の文字数を出力します。
#!/usr/bin/env perl
use utf8;
my $hello = 'こんにちは';
print length($hello), "\n"; # 5
このように、utf8 プラグマを使うことで、Encode モジュールの decode
関数を使わなくても、
文字列を文字の列として認識することができるようになります。
utf8 プラグマによって、ソースコード内のマルチバイトの文字列は自動的に内部エンコーディングで
内部文字列として処理されます。
近年では、日本語のようなマルチバイト文字を扱う場合は utf8 プラグマを使うことが一般的になりました。 しかし、プラグマの名前の通り、ソースコードは UTF-8 で保存しなければいけませんので注意してください。
本来であれば、use utf8
と書かなくても、
自動的にソースコード内のマルチバイトの文字列を内部文字列として認識してくれるのが理想です。
しかし、古い Perl バージョンの時代に書かれた Perl スクリプトが今でも期待通りに動作するよう、
下位互換性を重視した結果、utf8 プラグマが新たに追加されたわけです。
この点だけは面倒ですが、定型句としてソースコードの冒頭に記述するようにしましょう。
内部文字列にすることで文字数のカウントが期待通りになりましたが、
split
関数による文字分割や、
substr
関数による部分文字列操作なども、もちろん期待通りに動作します。
#!/usr/bin/env perl
use utf8;
my $hello = 'こんにちは';
print substr($hello, 1, 3), "\n";
このコードは substr
関数によって部分文字列を取り出して、それを出力しています。
ところが、期待通りの結果は出力されるものの、"Wide character" 警告も出力されます。
Wide character in print at ./test.pl line 5.
んにち
"Wide character" 警告が出力された理由は、内部文字列をそのまま出力したからです。 内部文字列はあくまでも Perl 内部で扱われるエンコーディングのため、 それを外部に出力してしまうと問題が生じます。 今回はたまたま期待通りの結果が出力されましたが、環境によっては文字化けします。 いずれにせよ、これは正しい状態とは言えません。
このサンプルのように、Perl 内部の文字列を外部に出力する際には、必ず外部の環境が理解できる
エンコーディングに変換しないといけません。この方向の変換はエンコードと呼ばれ、
Encode モジュールの encode
関数を使います。
前述のサンプルコードを正しく書き換えると、次のようになります。
#!/usr/bin/env perl
use utf8;
use Encode;
my $hello = 'こんにちは';
print Encode::encode('UTF-8', substr($hello, 1, 3)), "\n";
上記のコードは、シェルのエンコーディングが UTF-8 の場合を想定しています。
そのため、Encode::encode
関数の第一引数には UTF-8
を指定しています。
しかし、これが Windows の PowerShell の場合は文字化けしてしまいます。
なぜなら Windows の PowerShell の文字エンコーディングは Shift_JIS だからです。
Windows の PowerShell のように、環境が Shift_JIS を期待しているなら、
print
関数の行は次のようになります。
print Encode::encode('Shift_JIS', substr($hello, 1, 3)), "\n";
出力時には内部文字列をエンコードして外部文字列に変換しなければいけないわけですが、
同様に、外部文字列を読むこむときも逆の変換が必要になります。
その変換をデコードと呼び、Encode モジュールの decode
関数を使います。
次のコードは、コマンドライン引数として文字列を受け取ります。 そして、ひらがなをカタカナに変換して、それをシェルに出力します。
#!/usr/bin/env perl
use utf8;
use Encode;
my $str = $ARGV[0]; # こんにちは
$str = Encode::decode('UTF-8', $str);
$str =~ tr/あ-ん/ア-ン/;
print Encode::encode('UTF-8', $str), "\n"; # コンニチハ
前述の通り、外部との文字列の入出力では 外部文字列と Perl 内部文字列との変換が必要でしたが、 これはシェルとの入出力に限らず、CGI などのウェブアプリケーションでのブラウザーとの入出力や、 ファイルの入出力でも同様です。
次の例は、テキストファイルを読み込み、その内容を編集したのち、また同じファイルに書き込みます。
#!/usr/bin/env perl
use utf8;
use Encode;
# ファイルを読み取る
my $file = './now.txt';
my $now = '現在は0時0分0秒です。';
if ( -e $file ) {
open my $rfh, '<', $file;
$now = <$rfh>;
close $rfh;
# 外部文字列を内部文字列にデコードする
$now = Encode::decode( 'UTF-8', $now );
}
# テキストを書き換える
my @tm = localtime;
$now =~ s/\d+時\d+分\d+秒/$tm[2]時$tm[1]分$tm[0]秒/;
# 内部文字列を外部文字列にエンコードしてファイルに出力
open my $wfh, '>', $file;
print $wfh Encode::encode( 'UTF-8', $now );
close $wfh;
このようにファイルから読み込んだ外部文字列を Perl 内部文字列にデコードすることで、 テキスト加工がやりやすくなります。 そして、ファイルへ書き戻す際には、Perl 内部文字列を外部文字列にエンコードしてから書き込みます。
ファイルの入出力では、open
関数で :encoding()
を使うことで、
ファイルハンドルの単位で自動的にデコードとエンコードを行うことができます。
前述の例を書き換えると、次のようになります。
#!/usr/bin/env perl
use utf8;
# ファイルを読み取る
my $file = './now.txt';
my $now = '現在は0時0分0秒です。';
if ( -e $file ) {
open my $rfh, '<:encoding(UTF-8)', $file;
$now = <$rfh>;
close $rfh;
}
# テキストを書き換える
my @tm = localtime;
$now =~ s/\d+時\d+分\d+秒/$tm[2]時$tm[1]分$tm[0]秒/;
# 内部文字列を外部文字列にエンコードしてファイルに出力
open my $wfh, '>:encoding(UTF-8)', $file;
print $wfh $now;
close $wfh;
ファイル入出力に関しては、さらに open
プラグマを使う方法もあります。
#!/usr/bin/env perl
use utf8;
use open ':encoding(UTF-8)';
# ファイルを読み取る
my $file = './now.txt';
my $now = '現在は0時0分0秒です。';
if ( -e $file ) {
open my $rfh, '<', $file;
$now = <$rfh>;
close $rfh;
}
# テキストを書き換える
my @tm = localtime;
$now =~ s/\d+時\d+分\d+秒/$tm[2]時$tm[1]分$tm[0]秒/;
# 内部文字列を外部文字列にエンコードしてファイルに出力
open my $wfh, '>', $file;
print $wfh $now;
close $wfh;
open
プラグマは open
関数のエンコーディングのデフォルト値を定義します。
そのため、以降、open
関数を指定する際には外部文字列のエンコーディングを指定する必要がありません。
外部文字列と内部文字列の変換を忘れないという意味では、とても良い書き方です。
以上、3 つの方法のどれを使っても構いません。お好みの方法を使ってください。 ちなみに私はいつも Encode モジュールを使う方法を使っています。 理由はなく、単なるクセと好みです。
以上で解説した通り、外部文字列と内部文字列との変換を行うわけですが、 どうしてもデコードとエンコードを忘れてしまうことがあります。 デコード・エンコード忘れは、"Wide character" 警告が出てから気づくことが多いと言えるでしょう。 そのような場合に、スカラー変数に格納された文字列が内部文字列なのか外部文字列なのかを判定したいこともあるでしょう。 実は、残念ながら、その判定を確実にする方法はありません。 コードを書く際に自身で気を付くしかありません。
とはいえ、デバッグ用であれば、確実ではないかもしれませんが、判定の手段は用意されています。
その判定には、Encode::is_utf8()
関数を使います。
#!/usr/bin/env perl
use utf8;
use Encode;
my $str = 'こんにちは';
print Encode::is_utf8($str) ? 'ON' : 'OFF';
上記のコードであれば ON と出力されます。
もし冒頭の use utf-8
プラグマを削除すると OFF と出力されます。
では、誤って内部文字列と外部文字列を混在させてしまったら、どうなるでしょうか。 つまり、内部文字列に外部文字列を連結してしまったら、どうなるでしょうか。
#!/usr/bin/env perl
use utf8;
use Encode;
my $internal = 'こんにちは'; # 内部文字列
my $external = Encode::encode( 'UTF-8', 'こんばんは' ); # 外部文字列
my $mixed = $internal . $external;
print Encode::is_utf8($mixed) ? 'ON' : 'OFF'; # ON
この場合は ON と表示されます。
私の経験上、"Wide character" 警告のデバッグで最も厄介と感じるのが、このような混在型です。
Encode::is_utf8()
関数では、内部文字列と外部文字列の混在を判定することができません。
当然、混在型の文字列を出力すると、"Wide character" の警告が出力されてしまいます。
前述のコードで ON と出力されると、スカラー変数 $mixed は内部文字列と思ってしまいます。 出力の際には外部文字列にエンコードしてしまうでしょう。
print Encode::encode('UTF-8', $mixed), "\n"; # 文字化け「こんにちはããã°ãã¯」
当然ながら文字化けします。すると、スカラー変数 $mixed はすでに外部文字列ではないかと疑います。 そして、エンコードせずに出力を試すのではないでしょうか。
print $mixed, "\n"; # 文字化け「こんにちはããã°ãã¯」に加え Wide character 警告
ところが、ここでも文字化けしてしまいます。さらに "Wide character" の警告も出力されてしまいます。 こうなると、Perl スクリプトをさかのぼって眺めていくしかありません。
この例では、文字化けしている箇所が $external だとすぐに分かりますが、 必ずしもそんなにシンプルだとは限りません。 結局のところ、Perl で日本語のようなマルチバイトの文字列を扱うのであれば、 常に内部文字列と外部文字列を気にしながらコーディングしないといけないということです。
Encode::is_utf8()
関数は役に立つことがありますが、あくまでもデバッグの参考程度と考えましょう。
これまで Encode モジュールを使って内部文字列と外部文字列を変換しましたが、 外部文字列のエンコーディングが UTF-8 に限られるなら、utf8 モジュールを使って同じことができます。 次のサンプルは前述のコマンドライン引数を扱うサンプルコードを utf8 モジュールで書き換えたものです。
#!/usr/bin/env perl
use utf8;
my $str = $ARGV[0]; # 外部文字列「こんにちは」
utf8::decode($str); # 外部文字列を内部文字列にデコード
$str =~ tr/あ-ん/ア-ン/; # ひらがなをカタカナに変換
utf8::encode($str); # 内部文字列を外部文字列にエンコード
print $str, "\n"; # 「コンニチハ」と出力される
5 行目と 7 行目に注目してください。
デコードには utf8::decode()
関数を、エンコードには utf8::encode()
関数を使います。
これらのメソッドは UTF-8 専用なので、Encode::decode()
関数や
Encode::encode()
関数と違い、エンコーディング名を指定しません。
また、Encode::decode()
関数や Encode::encode()
関数は変換した文字列を返しましたが、
utf8::decode()
と utf8::encode()
は引数に与えたスカラー変数の値を書き換えます。
このあたり、コードの書き方が違いますので注意してください。
少しややこしい話をしますが、utf8::decode()
と utf8::encode()
など
utf8 モジュールの関数は、use utf8
を宣言しなくても利用できます。
このサンプルコードで書かれた use utf8
は、ソースコードを UTF-8 であることを
Perl に知らせるためだけに使われており、utf8 モジュールの関数を利用するためではありません。
次のように use utf8
を宣言しなくでも期待通りに動作します。
#!/usr/bin/env perl
my $str = 'こんにちは'; # use utf8 がないので、この文字列は外部文字列と同じ
utf8::decode($str); # 外部文字列を内部文字列にデコード (use utf8 がなくても利用できる)
$str =~ tr/あ-ん/ア-ン/; # ひらがなをカタカナに変換
utf8::encode($str); # 内部文字列を外部文字列にエンコード (use utf8 がなくても利用できる)
print $str, "\n"; # カタカナに変換されずに「こんにちは」と出力される
use utf8
を宣言しなくてもエラーは出ませんが、このコードは期待通りの結果を出力しません。
何が問題か分かりますか?
問題の箇所は 5 行目なんです。
use utf8
を宣言することで、ソースコードが UTF-8 で書かれていると
Perl に知らせ、ソースコード内の文字列をすべて内部文字列として処理してくれるのでした。
この内部文字列への変換はスカラー変数の文字列だけではなく、tr
に記述された文字も対象なのです。
use utf8
が宣言されていないソースコードでは、
tr
に記述された文字列は文字として処理されないため、期待通りの変換が行われないのです。