9部 付録

Perlで日本語の処理

文字化けが発生する主な原因は、OSや機種の違いによって文字の扱い方が違うから、ということに尽きるでしょう。
自身のパソコンのデータだけを扱っているときは文字コードを意識する必要はありませんが、様々なコンピュータが接続されているインターネットでは、文字コードの問題がでてきます。

CGI の場合に特定して考えてみましょう。まず、サーバとクライアントの環境ですが、サーバの OS で一番のシェアはUNIX 系のOSで、クライアントだとウインドウズ系のOSです。それで、UNIX系OSの標準文字コードは EUC ですが、ウインドウズだとシフトJISです。この環境の違いが文字化け発生の主な要因です。

ウインドウズから送られてくるシフトJISはちょっと困った問題を抱えていて、2バイト目に \ (5C) がくる漢字がいくつかあります。たとえば、表示の「表」を符号化すると 9/5 5/C で、2バイト目が 5/C になっています。この (5/C) は Perl ではエスケープ文字として扱われているので、「表」を出力するときは文字とエスケープ文字にばらして扱ってしまいます。こうなると文字化けは避けられません。
文字化けは必然的に発生する現象ですが、解決する方法はあります。今回は、その解決策を探っていきましょう。

Perl でのマルチバイトコード

Perl5.005 までは日本語文字コード等のマルチバイトコードに対応していなかったため、日本語1文字を2文字として認識するようになっていました。そのため、length、substr等の関数や正規表現では期待する効果が得られませんでした。そこで、有志により日本語文字を1文字として処理するようにしたパッチが開発され、そのパッチコードをあてて日本語に対応できるようにした Perl を JPerl と呼ぶようになりました。また、jcode.pl は文字コードの変換等に大いに利用されてきました。

Perl5.6 では、新しい文字コード体系の一つであるUTF-8符号化方式に対応することになったため、日本語文字も1文字として認識されるようになりました。しかし、utf-8 の実装は Perl5.6.0 では実験的レベルにあり、まだ完全対応というわけではありません。

Perl 自体にはその変換機能はまだ実装されていません。そのため、従来の文字コードを使用したデータを処理する場合は、結局従来と同様にJcode.pm等を使ってスクリプト側でコード変換を行わなざるをえない状況が続いてます。

変換モジュールとツールの紹介

jcode.pl

jcode.pl のダウンロードサイト
jcode.pl の使用方法はスクリプト内に記述されていますが、すべて英語です。日本語で説明された「jcode.plの私的な解説書」もご参照ください。

Jcode.pm

Jcode.pmの解説
jcode.pl をモジュール化したものです。

nkf

UNIX定番の変換ツール。
nkf [SOURCE FORGE]プロジェクト
nkf ウインドウズ版
nkf マックPPC版

pkf

pkf のダウンロードサイト
nkf の Perl 版です。

文字化けに関する情報

文字化けをする文字一覧表

文字化けを完全に防ぐことは難しいですが、変換モジュールや変換処理を行えば、ある程度は発生を防ぐことができます。

回避方法

  1. 文字コード変換モジュールで文字コードを変換する
    一般的によく使われる方法です。今までは jcode.pl や Jcode.pmなどが主流でしたが、最新版のPerlでは標準で Encode モジュールが装備されているため、こちらが主流となっていくでしょう。
  2. スクリプト全体をEUCで保存する
    プログラムを作成する環境はウインドウズやマックで、文字コードがシフトJISなので、普段のコード管理が難しいという問題がありますが、文字化けを防ぐ効果は高くなります。
  3. シングルクォートで文字列を囲む
    可能な限り、「 print <<'HTML'; ... HTML 」または「 print '...';」など、メタ文字が展開されないようにすることである程度回避できます。その代わり、文字列中の変数は展開できません。
  4. 文字化けを起こす文字の後に \ を付加する
     \ が文字列中にあると、その次の1バイトが特別なものでない限り、 \ は単に取り去られてしまいます。そこで、 \ が消されないようにするために、 \ を文字化けする文字の末尾に付け足します。

以下の文字コード一覧は、\ を含む文字の全てです。これらの文字をシフトJISで使う場合には注意しましょう。

データファイル作成上の注意

セパレータ

セパレータ自体も「文字」であることから、保存するデータ自体にセパレータ文字が含まれていると不都合が生じます。基本として、データの中に含まれるであろうと予想できる文字をセパレータとして使わない必要があります。
特に、シフトJISコードでは、「^」や「,」、「\」などを含む文字が存在するから、これらの文字をセパレータにするのはよくありません。

改行コード

1レコードを1行におさめる場合に不都合なのは、データの中に改行コードが含まれている場合です。通常のINPUTタグなどでは改行コードは入力できなませんが、TEXTAREA タグを使うとフォームデータ内に改行コードを含むことができます。 
解決方法としては、改行コードを「__BR__」または「!BR!」などの独自のリテラルに置き換えておいて、表示させるときにそのリテラルを改行コードなり<BR>タグなりに戻すというのが現実的でしょう。

$name = 'Daddy';
$email = 'name@sample.co.jp';
$comment = "こんにちは。\nはじめてきました。\nまた来ます。\n";

このままjoinを使って登録すると...

---data.txtの内容----------------------------------
Daddy\tname@domain.co.jp\tこんにちは。 
はじめてきました。 
また来ます。 

となり、本当はレコードが1件なのに、3件に見えてしまいます。

改行コード(\nまたは\r\n)を「__BR__」に置き換えるには、デコード処理の部分で、

$value =~ s/\r\n/\n/g;
$value =~ s/\n/__BR__/g;

とします。

---data.txtの内容----------------------------------
Aさんの名前\tAさんのE-mailアドレス\tAさんのコメント 
Bさんの名前\tBさんのE-mailアドレス\tBさんのコメント 
Cさんの名前\tCさんのE-mailアドレス\tCさんのコメント 
あきら\takira67@po.jah.ne.jp\tこんにちは。__BR__はじめてきました。__BR__また来ます。 

※タブコードはスペースと区別がつかないため、個々では便宜上「\t」と表しています。

また、読み出したデータを元に戻すには、

($name, $email, $comment) =
split(/\t/, $string);
$comment =~ s/__BR__/\n/g;

とします。

異なるOS間の改行コード

OS 改行コード

OS 改行コード
ウインドウズ CR(\r) + LF(\n)
マッキントッシュ CR(\r)
UNIX LF(\n)

マルチバイトコードを扱うためのテクニック

ASCII文字をコード値に変換する!

特定のASCII文字に対応するコード値を出力したい、逆にコード値に対応するASCII文字を出力したいときなどに活用できます。

文字をコード値に変換するには ord関数を使います。

$code = ord( $char );

コード値を文字に変換するには chr関数を使います。

$char = chr( $code );

printfsprintf でもできます。

printf( "%d is character %c\n",
$code, $code);
> 101 is character e

packunpack のCテンプレートを使えば、大量の文字を高速に変換できます。

@ASCII = unpack( "C*", $string );
$string = pack( "C*", @ascii );

マルチバイト文字のマッチング

Perlは1バイトが1文字という原則で動作するため、マルチバイト文字を含む文字列に対してのマッチングはうまくいきません。
マルチバイト文字を構成するバイトシーケンスを表現するパターンを書くことで、エンコーディングを活用する基本的には、エンコーディングの1文字にマッチするパターンを作成し、その「任意の1文字」を表すパターンを、より大きなパターンの中で使用します。

EUC の 0~127 (0x00~0x7F) までの文字は、ASCII文字とほぼ一致します。この範囲の文字は1バイト。2バイトで表現される文字は、先頭バイトが0x8Eで、次のバイトが 0xA1~0xDF の範囲内の値。3バイトで表現される文字は、先頭バイトが0x8Fで、残りのバイトが 0xA0~0xFE の範囲内の値を持ちます。また、各バイトが0xA1~0xFEの範囲内の値を持つ2バイト文字もあります。
つまり EUC のコード値の範囲は、正規表現で表現できます。後で使いやすいように $eucjpという文字列変数を定義して、これに EUC の1文字にマッチする正規表現を格納しておきましょう。

my $eucjp = q{    #
EUC-JPエンコーディングの構成文字
[\x00-\x7F]    # ASCII/JISローマ字(1バイト)
| \x8E[\xA0^\xDF]    # 半角カタカナ(2バイト)
| \x8F[\xA1-\xFE][\xA1-\xFE]    # JIS X
2012-1990(3バイト)
| [\xA1-\xFE][\xA1-\xFE]    # JIS X
0208-1997(2バイト)
};

/x 修飾子はコメントや空白を許可する

マッチングミスを防ぐ

マルチバイト文字のマッチングで作成した $eucjp を使って、日本語文字を対象とした正規表現でのマッチングミスを防ぐ方法を紹介します。

/^ (?: $eucjp)*? \xC5\xEC\xB5\xFE/ox
# 「東京」を探す

/x 修飾子は $eucjpで空文字やコメントが使用されているので絶対必要です。$eucjp は内容が変わらないため、/o 修飾子でコンパイルを1回に限定し、スピードアップをしています。

置換も同様にして行えます。マッチより前の部分もマッチ全体の一部なので、カッコで囲んで補足しておき、置換部分に$1として含めておく必要があります。$Tokyoに格納した文字列を$Osakaで置換するには次のとおりです。

/^ ( (?:eucjp)*? ) $Tokyo/$1$Osaka/ox

/g 修飾子を使用する場合は、前回のマッチの終わりからマッチを開始。^ を \G に変えるだけです。

/\G ( (?:eucjp)*? ) $Tokyo/$1$Osaka/ox

マルチバイト文字列を分割

マルチバイト文字列を個々の文字に分割します。

@chars = /$eucjp/gox; # リストの各要素に1文字が入る

次のコードは、この方法を使ったフィルタです。

while ( <> ){
	my @chars = /$eucjp/gox;
	for my $char ( @chars ){
		# 1バイト文字
		if ( length($char) == 1 ){

		# マルチバイト文字
		} else {
		}
	}
	my $line = join("",@chars);
	print $line;
}

マルチバイト文字列を確認

文字列が正しくエンコーディングされているかを確認するには次のような正規表現を使います。

$is_eucjp = m/^(?:$eucjp)*$/xo;

ここでは、全ての文字列に対して、有効な文字で構成されているかをチェックしています。

もし EUC と シフトJIS のどちらも真であれば、ASCII 形式である可能性が高くなります(ASCII はEUC-JPのサブセット)。この場合は、文字の出現頻度データを使って経験的に推測するしかありません。

JIS (ISO-2022-JP) をチェックする

日本語文字を示すとに X0208-1983 等を示すエスケープシーケンスを使い、 終わりにASCIIを示すエスケープシーケンスを使います。このエスケープシーケンスは下記のようになっています。

ESC(B
次に ASCII が来ることを予告
ESC(J
次に JIS X 0201-1976 が来ることを予告
ESC$@
次に JIS X 0208-1978 が来ることを予告
ESC$B
次に JIS X 0208-1983 が来ることを予告

たとえば、文字列からASCIIを示すエスケープシーケンスを検索する場合は以下のようなスクリプトを使います。

if ( $str =~ /\e\(B/ ){ ... }

変数 $line にJISが含まれているか調べてから処理するサンプル

# JIS X 0208シリーズのエスケープシーケンスを設定
$re_jis0208_1978 = '\e$\@';
$re_jis0208_1983 = '\e$B';
$re_jis0208_1990 = '\e&\@\e$B';
# JISシリーズすべてのエスケープシーケンスを設定
$re_jis0208 =
"$re_jis0208_1978|$re_jis0208_1983|$re_jis0208_1990";
# JIS補助漢字
$re_jis0212 = '\e$\(D';
# JISシリーズとJIS補助漢字を含めて設定
$re_jp = "$re_jis0208|$re_jis0212";
# ASCII とJIS X 0201-1976用のエスケープシーケンスを設定
$re_asc = '\e\([BJ]';
# かな漢字用のエスケープシーケンスを設定
$re_kana = '\e\(I';
if ( $line =~ /$re_jp|$re_asc|$re_kana/o ){ ...}

たとえば、文字を表すバイト列中に、16進数で「1B 24 42」(ESC$B)というバイト列が来たらそれ以降はJIS X 0208-1983、「1B 28 42」(ESC(B)というバイト列が来たらそれ以降はASCIIとみなします。たとえば、「1月」という文字列は「31 1B 24 42 37 6E 1B 28 42」となります。

なお「1B 24 40」 ESC$@ でJIS C 6226-1978、「1B 28 4A」ESC(J でJIS X 0201ローマ文字、「1B 24 28 44」 ESC$(D でJIS X 0212-1990への切替えを表すこともできます。

#!/usr/local/bin/perl
$str = '10月';
require 'jcode.pl';
jcode::convert($str,'jis');
print $test=unpack("H*",$str),"\n";
出力結果

31301b2442376e1b2842

ひらかなをカタカナに変換する!

日本語で tr/あ-ん/ア-ン/ と変換したいと思っていても、EUCではいくつかの問題があって正しく変換されず、シフトJISでは使い物にならないほど問題があります。

これに対しては、下記の対応が一般的です。

  • 日本語用の jperl インタプリタを使用する
  • jcode.pl モジュール、nkf コマンド等の変換ツールを使用する
  • EUCについては hiragana2katakana 関数が便利

hiragana2katakana関数は以下のとおりです。

sub hiragana2katakana {
	local($_) = @_;
	s/\xa5\xa4/`/g;
	s/\xa4([\xa1-\xfe])/\xa5$1/g;
	s/`/\xa5\xa4/g;
	$_;
}

ローマ字をカタカナに変換する!

EUC用で、roma2kana.pl という便利なものがあります。シフトJIS ならば romkan.pl を 利用するとよいでしょう。

入手先:

ftp://ftp.sra.co.jp/pub/lang/perl/sra-scripts/romkan.pl-1.4

その他の問題

漢字コードがSJISの場合、 SJIS の第2バイトは 0x40~0x7e、0x80~0xfcですから、ASCII と次の文字が重なります。

@ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

この重複領域の文字がワルサをすることがあります。例えば、第2バイト に "@" などが含まれていて "" 内などの文字列として使われていた場合、 perl はこれを配列として展開しようとします。

JIS(ISO-2022-JP)ではもっと困る問題があります。 JIS で「JISです」という文字列は上記ISO-2022-JPで述べたように

ESC$B#J#I#S$G$9ESC(B

となり、指示子を含んだものとなります。
これに対して「JIS」と言う文字でマッチングしようすると

ESC$B#J#I#SESC(B

となり、マッチングしません。したがって、一度 重複する部分がないEUC に jcode.pl 等で変換してから処理するのが良策ということになります。

また、Jperlにもあるように、2バイト文字は以下のような正規表現等々いろんなコマンドに影響を及ぼします。

関連記事