Perl入門ゼミ

  1. Perl
  2. モジュール
  3. here

Encodeモジュール - 日本語などのマルチバイト文字列を適切に処理する

日本語などのマルチバイト文字をPerlで適切に扱うにはEncodeモジュールを使用します。次の3つのことを覚えておけば多くの場合適切に日本語を扱うことができます。

  1. 外部から入力された文字列はEncodeモジュールのdecode関数でデコードする
  2. 外部へ出力する文字列はEncodeモジュールのencode関数でエンコードする
  3. ソースコードはUTF-8で保存しutf8プラグマを有効にする

この解説での用語

この解説では外部から入力された文字列のことを「バイト文字列」と呼ぶことにします。Perlの内部表現に変換された文字列を「内部文字列」と呼ぶことにします。また「バイト文字列」が特定の文字コードで記述されている場合は「UTF-8バイト文字列」「Shift_JISバイト文字列」などと呼ぶことにします。

プログラミングで日本語を扱う場合にその文字列がバイト文字列であるのか内部文字列であるのかを明確に区別する必要があります。内部文字列とバイト文字列の変換に関してはプログラムが責任を持つというのがPerlで多言語を扱うときのルールです。この方法は少し手間がかかりますが、上記の3つのルールを覚えておけばよいだけなので仕組みとしてはとてもシンプルです。

外部から入力された文字列は必ずデコードする

外部から入力された文字列は必ずEncodeモジュールのdecode関数を使用してデコードします。デコードとは「バイト文字列」を「内部文字列」に変換する処理のことをいいます。マルチバイト文字列を扱う場合はdecode関数を使用して必ず内部文字列に変換します。

decode関数の第1引数にはバイト文字列の文字コード、第2引数にはバイト文字列を指定します。戻り値は内部文字列になります。

use Encode 'decode';

# 外部からの入力(コマンドライン引数)
my $str = shift;

# バイト文字列(外部からの入力)を内部文字列に変換($strがUTF-8の場合)
$str = decode('UTF-8', $str);

# バイト文字列(外部からの入力)を内部文字列に変換($strがShift_JISの場合)
$str = decode('Shift_JIS', $str);

外部から入力される文字列というのは「コマンドライン引数」「ファイル」「標準入力」「環境変数」などです。外部からの入力はすべてバイト文字列であると考えてください。

$str = decode('UTF-8', $str);

を図式的に表現すると以下のようになります。

                「UTF-8バイト文字列」を「内部文字列」に変換
UTF-8バイト文字列                 --->                 内部文字列

decode関数の第1引数の意味は勘違いしやすい部分です。バイト文字列が実際にはどの文字コードでエンコーディングされているかを指定します。

外部へ出力する文字列は必ずエンコードすること

外部へ出力する文字列はEncodeモジュールのencode関数を使用してエンコードします。エンコードとは「内部文字列」から「バイト文字列」に変換する処理のことをいいます。

encode関数の第1引数にはどの文字コードに変換するかを指定します。第2引数には内部文字列を指定します。

use Encode 'encode';

# 内部文字列をUTF-8バイト文字列に変換する場合
$str = encode('UTF-8', $str);

# 内部文字列をShift_JISバイト文字列に変換する場合
$str = encode('Shift_JIS', $str);

decode関数を使用して内部文字列に変換した文字列を出力する場合は必ずencode関数でバイト文字列に戻す必要があります。どのタイミングでencode関数を使って変換を行うかといえば出力するぎりぎりのところです。プログラムの中ではできるだけ遅いタイミングまで内部文字列を保つようにします。

$str = encode('UTF-8', $str);

を図式的に表現すると以下のようになります。

                「内部文字列」を「UTF-8バイト文字列」に変換
内部文字列                 --->                 UTF-8バイト文字列

ソースコードはUTF-8で保存しutf8プラグマを有効にすること

マルチバイト文字を扱う上でもうひとつ気にする必要があるのはソースコードの中に記述する文字列です。もし日本語などのマルチバイト文字をソースコードの中で記述する必要があるならソースコードはUTF-8で保存してください。その上でutf8プラグマを有効にします。

# ソースコードはUTF-8として保存
# utf8プラグマを有効にする
use utf8;

my $str = "日本語などを書く。";

ソースコードの中に書かれた文字列のことを文字列リテラルと呼びます。覚えておいて欲しいことは文字列リテラルの文字コードはソースコードの文字コードと同じになります。ソースコードをUTF-8で保存すれば文字列リテラルはUTF-8バイト文字列となり、Shift_JISで保存すれば文字列リテラルはShift_JISバイト文字列となります。

特定の文字コードで保存する方法はエディタによって異なります。参考にWindowsのメモ帳の場合を説明します。これがわかれば他のテキストエディタでも理解できると思います。

「ファイル」→「名前をつけて保存」→「文字コード」→「UTF-8」を選択。

utf8プラグマというのはソースコードに書かれたUTF-8バイト文字列を内部文字列に変換する効果があります。つまりソースコードをUTF-8で保存してutf8プラグマを有効にすれば文字列リテラルは内部文字列に変換されます。

utf8プラグマの効果を図式的に表すと次のようになります。decode関数と効果が良く似ています。

                                use utf8
UTF-8バイト文字列                 --->                 内部文字列

最初に述べた次の3つのことを守りさえすれば文字コードで悩むことはだいぶ少なくなると思います。文字コードで悩んだらまずこの原則に戻ってみてください。

内部文字列に変換することの意味

それでは実際に内部文字列に変換した効果をみてみましょう。Perlが正しく文字列を扱えていることがわかると思います。length関数は正しい長さを返してくれますし、正規表現が正しく動きます。

ソースコードはUTF-8で保存してください。UTF-8で保存されていない場合は「Malformed UTF-8 character」などという警告がでます。

解説はUTF-8の場合で行いますがWindowsの場合はencode関数やdecode関数にcp932を指定する必要があります。cp932というのはWindowsの文字コードである「Windows-31J」を表すものです。コマンドライン引数には「これは日本語です。」という文字列を渡してみてください。

# 内部文字列に変換した効果を試す
use strict;
use warnings;

use utf8;
use Encode qw/encode decode/;

# コマンドライン引数(UTF-8バイト文字列)
my $str1 = shift;

# UTF-8バイト文字列を内部文字列にデコード
$str1 = decode('UTF-8', $str1);

# 文字列リテラル (utf8プラグマが有効なので内部文字列になる)
my $str2 = "日本語";

# 内部文字列に変換すると正しく文字を数えることができる
print length $str2, "\n"; # 3

# 内部文字列どうしであれば正しく正規表現を使用できる
if ($str1 =~ /$str2/) {
  print "Match!\n";
}

# 出力する直前に内部文字列をバイト文字列にエンコード
$str1 = encode('UTF-8', $str1);
$str2 = encode('UTF-8', $str2);

print "'$str1' is match '$str2'\n";

文字コードの変換

文字コードの変換処理はPerlではdecode関数とencode関数を組み合わせて次のようにします。いったん内部文字列に変換する必要があります。次の例はUTF-8バイト文字列をShift_JISバイト文字列に変換する例です。

# UTF-8バイト文字列を内部文字列に変換
$str = decode('UTF-8', $str);

# 内部文字列をShift_JISバイト文字列に変換
$str = encode('Shift_JIS', $str);

図式すると次のようになります。

UTF-8バイト文字列  -->  内部文字列  -->  Shift_JISバイト文字列

これは少々面倒なので、from_toという関数が用意されています。第1引数はバイト文字列、第2引数は変換前の文字コード、第3引数は変換後の文字コードです。enocdeやdecode関数とは異なり第1引数に指定したバイト文字列自体が変換されることに注意してください。

# 文字コードの変換
use Encode 'from_to';

# $str自体が変換される
from_to($str, 'UTF-8', 'Shift_JIS');

ファイル名として指定する文字列

openやunlinkなどの関数にファイル名を指定するときは、OSのバイト文字列に変換して指定する必要があります。

use strict;
use warnings;
use utf8;
use Encode 'encode';

my $file = 'あいう.txt';
open my $fh, '<', encode('cp932', $file)
  or die "Can't open " . encode('cp932', $file) . ":$!";

ファイル名を扱う関数に渡す直前でエンコードするのがポイントです。これはファイル名を扱う関数をたくさん扱う場合は少し面倒ですので、ファイル名をOSの文字コードに変換する関数を作っておくと便利です。

またエラーメッセージを自動OSの文字コードに変換するためにbinmode関数を使って標準エラー出力のエンコーディングを指定しておくと便利です。

use warnings;
use utf8;

use Encode qw/encode decode/;
my $enc = 'cp932';
binmode STDERR, ":encoding($enc)";
sub d($) { decode($enc, shift) }
sub e($) { encode($enc, shift) }

my $file = 'あいう.txt';
open my $fh, '<', e$file
  or die qq/Can't open "$file": $!/;

これできわめて簡潔にPerlの文字コードを扱うことができるようになりました。

文字列に関するその他の注意点

内部文字列とバイト文字列の連結は避ける

内部文字列とバイト文字列を連結は避けましょう。この場合はバイト文字列は自動的に内部文字列へと変換されるのですが、その変換は文字化けの原因になります。すべて内部文字列に変換してから連結を行いましょう。

内部文字列かバイト文字列かの識別は正確にはできない

Perlではプログラムの中でその文字列がバイト文字列なのか内部文字列なのかを正確に識別する方法がありません。内部文字列かバイト文字列かを識別するためにutf8::is_utf8メソッドを使用することはできません。utf8::is_utf8メソッドはUTF8フラグが立っているかどうかを判別できるだけで、内部文字列かバイト文字列かを判定することはできないのです。

ですから、おそらくこうなっているだろうという推測には利用できますが、内部文字列かバイト文字列かをプログラム内で確定させるための判定には使えませんので注意してください。UTF8フラグが立っていた場合は必ず内部文字列です。UTF8フラグが立っていない場合は内部文字列かバイト文字列です。

モジュールとの連携

モジュールのある関数が引数として内部文字列を受け取るかバイト文字列を受け取るかはモジュールの作成者の意図によります。関数の戻り値についても同じです。バイト文字列を返すか、内部文字列を返すかは作成者の意図によります。必ずドキュメントを読んでください。

これはPerlでプログラムをする上でプログラマが特に大変だと思う点のひとつだと思います。けれども単に否定せずに「後方互換性の維持」「シンプルな変換規則」「文字列の正確で適切な扱い」というその他の大きな恩恵にあずかっているということを思いだしてみてください。他の言語と比べてもPerlは文字列の扱いをかなりよくやっていると思います。

その他のプログラミング言語の実装を見てもわかると思いますが、文字コードの扱いは何かとのトレードオフになっています。それくらい扱うのが難しい分野です。

内部文字列について

内部文字列がどのようなものかについては意識しないほうがよいでしょう。プログラムを作成する上で意識する必要も特にありません。それでも知りたいという方は次の解説がわかりやすいです。