Encodeでラクラク日本語処理B!

こんにちは!ラブプラスとときメモ4の狭間で揺れ動いているxaicronです!!

今日は日本でプログラムを書いていたら避けては通れない気がする、Encodeの話をしようと思います!

はじめに

まず、この記事を読む前に、Perlのバージョンの確認をしてください。以下のようにやればバージョンが表示されます。

% perl -v

ここで、5.8.1より下の数字ができてきた方は、Perlのバージョンアップをしてください。5.8.1より下のバージョンでは、Perlの内部文字コードが安定していないので、いい感じになりません。できれば5.8.8以上のバージョンを使いましょう。

それから、文字コードってなによって人も適当にWikiとかで調べてから読んだ方がいいと思います!!

Encode.pm

Encodeは昔のjcode.plやJcode.pmに代わる、現在の文字コード処理のスタンダードModuleです。ネットなどで見かけるjcode.plやJcode.pmが書かれているものは、レガシーなコードですので今ではあまり参考になりません。

これからスクリプトを書くときは、Encode.pmを使いましょう。

基本方針

Encodeの基本は

  • deocdeして
  • いじって
  • encode

これに尽きます。

このルールを守ってないない人は、文字化けに遭遇して、ゲシュタルト崩壊を起こしてしまいますが、それはルールを守らないからです!!しっかりしてください!!

内部文字列とバイト文字列

Perlには内部文字列と言うものがあります。「flagged utf8」なんて呼ばれ方をしますね!

Java とかJavaScriptとかでのStringは「UTF16-LE」っていうので保持して扱うようになっていますが、Perlの場合は「UTF-8」を元に、1文字づつ区別できるような形にして保持するようになっていて、この形式になっているかどうかっていうflagがあるので、Perlでは内部文字列のことを「flagged utf8」なんて呼ぶんですね!

UTF-8で保持されているので、UTF16-LEで発生するサロゲートペアとかいうよくわからないものに悩まされることもなく、length()でちゃんと文字数が取れるので安心して使えますね!

バイト文字列っていうのはそのままで、EUC-JPとかShift_JISとかの「生」の文字列のことです。これは簡単ですね。

でも「UTF-8のバイト文字列」とPerlの内部文字列の「flagged utf8」は別物なので注意が必要です!!

これをごっちゃにして考えると、速攻で文字化けを起こして、休暇が減ります。

「UTF-8のバイト文字列」っていうのはUTF-8のバイト文字列であってそれ以外の何者でもない、ただのバイナリの塊です。これをそのまま扱うと日本語的な1文字づつの処理とかがうまいこと行かないのです。

「flagged utf8」はさっきも説明したように、「UTF-8」を元に、1文字づつ区別できるような形にして保持されている「内部文字列」です。なので、バイト文字列じゃあないんですね。

この、バイト文字列なのか内部文字列なのかっていうのを常に意識しておくことがEncodeマエストロへの第一歩です。

まぁ、要するにPerlでは、マルチバイト文字をいい感じに扱うためにバイト文字列を内部文字列っていうのに変換して、プログラミングするといいよってことですね。

用語の定義

というわけで、話を簡単にするために用語の定義をします。といっても二つだけなので安心してください!

  • flagged utf8とかフラグ付きUTF-8文字とか内部文字列とかPerlの内部文字列表現とかそういうのは「<b>内部文字列</b>」と呼ぶことにします。
  • 「内部文字列」意外の文字列は「<b>バイト文字列</b>」と呼ぶことにします。

簡単ですね!

encodeとdecode

まえがきが長くなりましたがやっと本題です。

Encode の基本、 encode() と decode() を覚えましょう。

use Encodeすると自動的に使えるようになります。

簡単に説明すると

  • encode → 内部文字列を、指定した文字コードのバイト文字列にする
  • decode → バイト文字列を指定した文字コードとして解釈し、内部文字列に変換する

って感じです。

Encodeのルールは

  • deocdeして
  • いじって
  • encode

なので、とりあえずdecodeから見ていきましょう。

euc-jpのバイト文字列を内部文字列に変換する場合は以下のようにします。

use Encode;

# euc-jpを内部文字列に変換
my $flagged_utf8 = decode 'euc-jp', $bytes;

これで内部コードになりました。逆に、内部文字列をバイト文字列に変換するときはencodeを使います。

use Encode;

# 内部文字列をShift_JISに変換
my $sjis_str = encode 'sjis', $flagged_utf8;

これでShift_JISのバイト文字列が帰ってくるので、ファイルに出力するなり、コマンドプロンプトに表示するなりできますね!

encodeは内部文字列をバイト列に、decodeはバイト列を内部文字列にする関数ということです。

また、encodeのエイリアスとしてstr2bytes、decodeのエイリアスとしてbytes2strって言う関数がありますが、まぁあんまり使わないですね!!

ちなみに、(en|de)code_utf8という関数もエクスポートされます。これは(en|de)code('utf8' $foo)と等価なので、UTF-8だとわかりきっている場合はこっちを使うのもいいでしょう。

euc-jpをcp932に変換したい

たとえば、$strがeuc-jpのバイト文字列だったとして、cp932のバイト文字列に変換したいと思います。

今までの知識でやると、以下のように書けます。

encode('cp932', decode('euc-jp', $str))

でも、まさにこれをやるためのfrom_toという関数がありあます。これを使うと上記のコードは以下のようになります。

from_to $str, 'euc-jp', 'cp932';

ちょっとだけシンプルになりましたね!!でもぼくはあんまりつかったことありません!!

find_encoding

(en|decode)は文字コードに結構曖昧な名前を入れてもうまいこと解釈してくれます。以下のように適当な名前を与えても全部おんなじになります。

decode 'sjis', $str;
decode 'Shift_JIS', $str;
decode 'shiFtjis', $str;

しかしながら、繰り返し同じ文字コードの変換を行うような場合だと、残念ながら結構パフォーマンスが悪いのです。

実は(en|decode)は内部的に、文字コードの名前解決を行ってその文字コードに対応したオブジェトを生成して、(en|de)codeメソッドを呼ぶ形になっているのです。

こういう処理を毎回行っているのでなかなか遅いんですねー。でも安心してください!!こういう場合は、find_encodingっていうのを使うととってもハッピーになれます!!

「文字コードの名前解決を行ってその文字コードに対応したオブジェトを生成」するっている処理を行うのがまさにfind_encodingなのです。

use Encode;

my $encoder = find_encoding 'sjis';

# 内部文字列をShift_JISバイト文字列に変換する
$encoder->encode($str);

# Shift_JISバイト文字列を内部文字列に変換する
$encoder->deocde($bytes);

こうすれば、名前解決の手間がなくなるので高速になる上に、毎回文字コードを指定する必要もないのでいいですね!

ベンチマークをとったら、find_encodingの方が224%高速という結果がでました。

Benchmark: running find_encoding, normal for at least 3 CPU seconds...
find_encoding:  3 wallclock secs ( 3.12 usr +  0.00 sys =  3.12 CPU) @ 212403.52/s (n=663761)
    normal:  4 wallclock secs ( 3.17 usr +  0.00 sys =  3.17 CPU) @ 65515.29/s (n=207749)
                  Rate        normal find_encoding
normal         65515/s            --          -69%
find_encoding 212404/s          224%            --

ベンチマークスクリプトはgistにあります。

※実際にはオブジェクトの生成自体は一度しか行われない。(en|de)codeが遅いのは単純に関数呼び出しのコストによるものだと思われる。

(en|de)codeに失敗した時の処理をしたい

さて、文字コード変換の基本はなんとなくわかったと思いますが、使っていると、(en|de)codeに失敗した時になんか処理したいときがあると思います。

これはどういうときかっていうと、例えばutf8なバイト文字列をShift_JISなバイト文字列に変換したいときとかに変換できない文字とかですね。

一番簡単な方法は変換に失敗したらcroakするっていうのです。(en|de)codeの第三引数に1を渡すと実現できます。

my $str = decode 'utf8', $bytes, 1;
my $sjis_bytes = encode 'sjis', $str, 1;

これで、文字列変換に失敗した時にcroakします。

違う文字に変換することもできます。Encodeにはそれを行うためのいくつかの定数がEncode::FB_*という名前で定義されています。

下記の例は変換できなかった文字を\xXX形式にしてくれます。

% perl
use Encode;

my $str = decode 'ascii', 'dankogai = 小飼弾', Encode::FB_PERLQQ;
print $str;
__END__
dankogai = \xE5\xB0\x8F\xE9\xA3\xBC\xE5\xBC\xBE

また、サブルーチンリファレンスを渡すことで、独自の動作が定義できます。使い方はPODから拝借しますが、以下のような感じです。

$ascii = encode("ascii", $utf8, sub{ sprintf "<U+%04X>", shift });

これを使えば柔軟な変換エラー処理が実現できますね。

※余談ですが、UTF-8バイト文字列を内部文字列にする、decode('utf8', $butes, sub{...})はEncode2.39以前ではきちんと動作していませんので、これを使う場合はそれ以上のバージョンに上げる必要があります。(参考404 Blog Not Found:#perl - Encode 2.39 Released!)

use utf8

さて、今のご時世、プログラムを書く文字コードはUTF-8が主流になってました。もちろんPerlでも内部表現からしてわかるように、スクリプトをUTF-8で書くことが推奨されています。

さらに、UTF-8で書いて、use utf8すると、リテラルがコンパイル時に内部文字コードになってくれるのです!

これを使うことで、プログラム中に日本語リテラルを書くのが怖くなくなります。

use utf8;
#use Encode;

my $str = '毎週金曜日は○○ゲーの発売日'; # decode utf8 => '...' と等価

とっても簡単ですね!

さらに、use utf8はレキシカルスコープなので、一部だけ適用したいときとかにも使えたり、no utf8したら、そこは適用範囲外になったり、use utf8されたら日本語の変数とか使えたりいろいろありますので、詳しくはperldoc utf8してください。

モダンなPerlではコードをUTF-8で書いて、use utf8する感じなので、この機会に覚えましょう。

Wide character in print at ...

さてさて、ここまでくればもうほとんど俺、Encodeつかえるんじゃねって気持ちになっていると思います。

そこでこんなコードを実行してみると・・・

% perl -l
use utf8;
use Encode;

my $str = 'Hello、ハローワーク';
print $str;
__END__
Wide character in print at - line 4.
Hello、ハローワーク

なんか 「Wide character in print at ...」とかいうwarningsが出ちゃいました!!いったいなんなの!!もう俺はEncode使えない!!寝るって気持ちになったとおもいますが、一回寝てもう一度考えてみましょう。

これは「内部文字コードをそのまま出力しましたねあなたは」っていうことを言われているのです!具体的には

  • flagged utf8である
  • その文字が 0x100 以上である

場合に警告されちゃいます。

printやファイルに書き出すときは、バイト文字列を出力することが暗に求められているのです。内部文字コードはバイト文字列そのものではないので親切に教えてくれたわけですね!!本当にありがたいことです!!

つまり、printするときはバイト文字列でないといけないわけなので、encodeしてバイト文字列にしてあげなくてはいけません。

use Encode

print $flagged_utf8;                 # warnings "Wide character in print at ..."
print encode 'utf8', $flagged_utf8   # ok

ということです!!

さっきの例をwarningsでないように書き直すとこうなります。

% perl -l
use utf8;
use Encode;

my $str = 'Hello、ハローワーク';
print encode 'utf8', $str;
__END__
Hello、ハローワーク

おっけーですね。

あとで登場するbinmodeを使うともうちょっとスマートに書けます。

PerlIOレイヤーを使って透過的に(en|de)codeする

実際のプログラムでは、ファイルを扱うことが多いと思いますが、読み込まれた文字列はもちろんバイト文字列です。

Perlでいい感じに扱いたい場合はdecodeする必要がありますが、毎回以下のように書くのはなんだかなーって感じです。

use Encode;

open my $fh, '<', 'sjis.txt' or die $!;
my $data;
while ($fh) {
    $data .= decode sjis => $_;
}
close $fh;

これ、読み込んだときに自動的にdeocdeされないかなーと思いませんか?

そんな時はPerlIOレイヤーのencodingを使用します!!

use Encode;

open my $fh, '<:encoding(sjis)', 'sjis.txt' or die $!;
my $data;
while ($fh) {
    $data .= $_;
}
close $fh;

openするときに、「<:encoding(sjis)っていうのを指定していますね。これで読み込むときに自動的にdecode('sjis', $bytes)してくれるようになります。便利ですねー。

出力するときは「>:encoding(euc-jp)」とかすればencode('euc-jp', $str)してくれます。賢いですねー。

これを使って、sjisで書かれたファイルをeuc-jpに変換する簡単なプログラムを書いてみましょう!

#use Encode;

open my $in '<:encoding(sjis)', $input or die $!;
open my $out '<:encoding(euc-jp)', $output or die $!;
while (<$in>) {
    print $out $_;
}
close $in;
close $out;

たったこれだけです!

「use Encode」していないことに注目してください。PerlIOレイヤーはなかなかイカした奴なので、必要になったときに自動的に呼び出してくれるんですねー。なかなか小粋な計らいですね。

これでファイルの文字コードで悩むこともなくなりましたね!!

また、「:encoding(foo)」はutf8の場合のみ、「:utf8」とも書けます。

binmode

ちなみに、PerlIOレイヤーのencoding(foo)っていうのは既に開いているファイルハンドルにも適用できます。その筆頭は標準出力である、「STDOUT」ですね!

既存のファイルハンドルに適用するにはbinmodeを使います。

外部装置に出力するときは、encodeしなくてはいけないことは先刻承知ですが、毎回printするごとにencode(...)するのは少し面倒だなーってときに重宝します。

例えば、標準出力は常にeuc-jpだよーってときはこう書きます。

use utf8;
use Encode;
binmode STDOUT, ':encoding(euc-jp)';

my $str = 'さくらインターネットとかeuc-jpだよね';
print $str;

最初の方でIOレイヤーをしていしているので、print $strの部分は自動的にprint encode('euc-jp', $str)したのと同等になります。簡単ですねー。

文字コードを調べたい

(ec|de)codeは文字コードが分かっているのが前提の関数です。でも、外部から読み込んだデータがあって、それがなんの文字コードなのかわからないってことが稀にあると思います。

そんなときはEncode::Guessを使うと、どの文字コードなのか推測してくれます。

まずは簡単な方法

use Encode;
use Encode::Guess qw/sjis euc-jp 7bit-jis/;

decode 'Guess', $data;

まず、use Encode::Guessの引数に推測したい文字コードを配列で指定します。そして、decodeの文字コードを「Guess」にすると自動判定してくれます。

上記では、sjis euc-jp 7bit-jisを指定していますが、ascii utf8は指定しなくても判定してくれます。

でも文字コードを100%判定するということは実はできません。Shift_JIS・euc-jp・utf8すべてに存在するバイト文字列もあります。

特に判定したい文字列が短い場合はうまくいきません。

判定される文字コードが複数になる可能性のある場合は以下のようにやりましょう。

use Encode;
use Encode::Guess qw/sjis euc-jp 7bit-jis/;

my $decoder = Encode::Guess->guess($data);
die $decoder unless ref $decoder;

warn $decoder->name;

$str = $decoder->decode($data);

guess($data)で文字コードの判定に成功すれば、find_encodingと同じものが戻ります。

複数の文字コードから判定できなかった場合は、

shiftjis or euc-jp or utf8

というような文字列が戻ってきます。なので、red $decoderの部分でリファレンスかどうかをチェックしています。

余談ですが、ここで$decoder->nameというのが出てきていますが、これは文字コード名のエイリアスじゃない、本当の名前を返してれます。

Encodeでは、Shift_JISはshiftjisが本名のようです。

Encode::Guessはもっと他の使い方もあります。詳しくはPODを読んでください。

おまけ

おまけトラックです。暇な人だけよんでね!

utf-8とutf8は別物!

実はfind_encoding("utf-8")とfind_encoding("utf8")は別物として扱われるのです!これはそれぞれ以下のようになります。

find_encoding("utf-8")->name; # utf-8-strict
find_encoding("utf8")->name;  # utf8

utf-8は「utf-8-strict」というのからわかるように、厳密なUTF-8として扱われます。

厳密にチェックしたいときに以下のように使いましょう。

dencode 'utf-8', $str, 1;

ちなみにdecode_utf8は「utf8」が使われています。

使える文字コード一覧を表示する

% perl -MEncode -le 'print for (Encode->encodings(":all"))'

piconvコマンド

Encodeと一緒についてくるコマンドです。

% piconv -f utf8 -t ascii --htmlcref foo.html > bar.html

とかやるとUTF-8で書かれたファイルをascii+htmlcref化してくれるので便利ですね。

% piconv -l

とかやると使える文字コード一覧ができてきます。

詳しくは

% piconv --help

とか、いろいろあるので、ちょっと文字コードを変換したいときは便利ですね!

半角カナを全角カナにしたいとか平仮名をカタカナとか半角英数字を全角英数字とかにしたいお!

Lingua::JA::Regular::Unicodeを使いましょう。

小粒ながらなかなかいいモジュールなんじゃないかと思います。

これを使うと、「ガ」を「ガ」にしてくれたりと、いろいろ細かいところもいい感じにやってくれます。

まとめ

Encodeを使えばとっても簡単にマルチバイト文字列を扱えることが分かってもらえたとおもいます!!

まだまだいろんな使い方があるので、気になる人はPODをみてみるといいと思います!!

明日は、Perl業界一のメガネフェチといわれているissmさんです!お楽しみに!!