Perlビギナーのための文字エンコーディング「超」入門

こちらは「Perl入学式 Advent Calendar 2015」の4日目の記事です。
Perl入学式 Advent Calendar 2015 - Qiita

昨日は @oisius さんの「子育て一段落で、Perl入学式」でした。清々しくも濃密な良記事! でしたね。
oisius.hateblo.jp

ぼくは東京編しか参加したことがないので、in大阪の雰囲気が今まで以上にリアルに想像できて、その意味でも面白かったです。

さて、対する私はといえば、思い返せば2013年の夏、8月の半ば頃からPerl入学式でPerlを習い始め、気が付けば2年以上、まだPerlを勉強しておりまして、この継続性、自分もPerl入学式もすごい! と思わずにはいられません。(涙)

しかしその勉強の過程において、ことあるごとに躓いては見て見ぬふりをしてきたのが文字エンコーディングの世界です。(涙)

さらにしかし、最近ようやくこの辺り、最低限の基礎をつかんできた気もするので、復習をかねて以下にまとめてみたいと思います。

参考記事

まず先に、この話題になるとよく読み返す参考記事のURLを示しておきます。

大体これらがあればもうこの記事は不要じゃろ、という感じもありますが、そして実際、以下に書く内容はかなりこれらと重複もしますが、それでも「同じことを別の人々が別の言い方で記す」ことにより、「そこに共通する要点を取り出しやすくなる」という良効果が見込めるため、とくに気にせず進めます。

文字化けする例

ではいきなりですが、Perlで文字化けさせてみましょう。
環境によってはそのまま再現できないかもしれないですが、これもひとまず細かいことは気にせず、自分の手元の環境(Mac 10.11/Perl 5.20)を前提に進めます。

ホームディレクトリに、perl-encoding-lesson というディレクトリを作ります。

$ cd
$ mkdir perl-encoding-lesson
$ cd perl-encoding-lesson

次に、練習用のファイルとして encode.pl というのを作ります。

$ vim encode.pl

こんな内容で。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my $text = 'Perl入学式';
say $text;

say関数を使いたいので(改行の記述が不要になるのでラク)、sayをフィーチャーしています。

実行。
f:id:note103:20151204094655p:plain

はい。ちなみにこの画像はVimquickrun.vimというプラグインを使って書いた&実行したもののスクリーンショットで、グレーの帯を挟んで上段の8行分がソースコード、下段の2行分が出力結果です。

まだここでは文字化けしてないですね。これでいいはず……です。(不安)

ではその下に、以下の記述を追加します。

my @text = split //, $text;
for (@text) {
    say $_;
}

split関数で文字列を1字ずつ刻んで配列 @text に格納し、それを1行につき1字出力する、ということをやっています。

では実行。
f:id:note103:20151204012512p:plain

文字化けしました!(喜)

ここからわかることは、Perlが文字化けするのは単に「日本語を使ったとき」ではなく(最初の例では「Perl入学式」がちゃんと出力されてるので)、「日本語をなんかPerlで処理したとき」だということです。(たぶん)

じゃあ、これを化けないようにするにはどうすればいいか? というと、以下の3つの行をよしなに追加します。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Encode; #追加1

my $text = 'Perl入学式';
$text = decode('utf8', $text); #追加2

my @text = split //, $text;
for (@text) {
    $_ = encode('utf8', $_); #追加3
    say $_;
}

実行。
f:id:note103:20151204012629p:plain

イイ感じです。

上の追加行でやったのは、

1. まずEncodeモジュールをuse
2. Perlで「なんか処理する」前に、$textをdecode
3. 最後に、printする前にencode

です。
もう少し詳しく言うと、

  • 1+2. Perlによる「なんか処理する(ここではsplitでバラバラにしてfor文とsay関数でタテに並べる)」の前に、Encodeモジュールのdecode関数を用いて「バイト列を内部文字列に変換」し、
  • 1+3. Perlによる処理の後にencode関数を用いて「内部文字列をバイト列に変換」する。

ということをやっています。

で、そこで言う「内部文字列」ってのは何かというと、『Perlが文字を扱うときの形式』みたいなもので、「バイト列」っていうのは『それ以外の形式』。(雑)
さらに言うと、「『Perlが文字を扱うときの形式』以外の形式の文字(バイト列)」を「『Perlが文字を扱うときの形式』の文字(内部文字列)」に変換するのが上記のdecodeというやつで、その逆がencode。

基本的には、その辺が結局「Perlの文字エンコーディング」について押さえておくべき原理(それ以上突き詰めようがない最大公約数的な共通認識)かな、と思っていますが、どうも個人的にはその「内部文字列」という言い方が人によってバラバラで、「utf8フラグ付き(flagged utf8)文字」と言ったり「Unicode」と言ったり「内部表現」と言ったり「内部表象」と言ったり「"文字列"」と言ったり「文字列」と言ったり、あるいは「バイト列」の方も「バイナリ表現」と言ったりとにかくもうちょっとみんな気持を一つに! 協力しよう! と言いたくなるぐらいに表記ユレが文脈次第すぎて、せっかくいろんな人が丁寧に説明してもイメージが一本化しづらい、という問題があるのではないのかなあ、とか。

よく仕事のミーティングが不毛タイムに陥る時なども、用語の定義がちゃんとしていなくて「ああ、それ、その意味で言ってたの?」みたいなことが不毛さの理由だったりするように、用語の定義はけっこう(とくにビギナーに対しては)重要かと考えますので……とりあえずもうそれらについては「内部文字列」と「バイト列」ということで有志的には合わせませんか、どうですか……? ……頼む!

UTF-8の二つの顔

ところで、一般に「北野武」といえばビートたけしのことですが(なに)、実際には「北野」という姓をもつ親が子供に「武」と名付ければ誰でも「北野武」になれるように、「UTF-8」と一言で言ってもそれがすべて「あのUTF-8」であるとは限りません。

言い換えると、上記の参考記事でも皆さん書かれているように、内部文字列が「utfフラグ付き(flagged utf8)」と言われているからといって、そこで言う「UTF-8」がバイト列における「UTF-8」のことかと言えばそうではない、ということです。

……って、もうビギナー的にはとりあえずここで一旦混乱すると思いますし、むしろ混乱すべきです。つまりこれもまた、文字エンコーディング道におけるハマりポイントの一つではあるでしょう。

しかし案ずるには及びません。上記の参考URLではその辺りも含め、それぞれ独自の鋭い観点&解説手法にて解説されていますので、混乱した方はぜひそれらをご参照ください。(まる投げ)

ではここからもう一つ、別の大事な話。

原理としては上記のとおり、「読み込んだバイト列の文字をdecodeで内部文字列にして→Perlでなんか処理したら→内部文字列をencodeでバイト文字列にしてから出力する」ってことだと思うのですが、これと切っても切り離せないのがopen関数を使った「ファイルの入出力」、および「リテラル」という概念です。

リテラルとは何か?

ぼくは一昨年の夏にプログラミング入門して以来、幾度となくその「リテラル」という用語に遭遇し、しかしそのつど本の解説などを読んではみるものの、どうしてもそれがどういうものなのか上手くイメージできずにいましたが、それもそのはず、私見ではこれは後述の「ファイル入出力」とセットでようやくその意味がわかる概念で、しかしこの「ファイル入出力」っていうのを僕はほとんど理解していなかったため(というのはあまり使う機会がなかったので)、それでそこから繋がる「リテラル」もほとんどわかっていなかったのだなと。

で、まずその「ファイル入出力」について言うと、これは通常のPerlのコード(上記のencode.plとか)を使って、何らかの素材データ(なんか解析したいテキストを集めたdata.txtとか)を読み込んだり、解析後のデータを外部のファイルへ書き込んだりすることです。

一方、最初に書いたencode.plでは、コードの中に「Perl入学式」という文字列(バイト列)が書かれていて、これはあたかもコードの一部のように見えていますが、実際にはキン肉マンテリーマンのようなアイドル超人に混じったジェロニモ(実は人間)のようなもので、質が違うというか出自が違うというか、いずれにせよ上で言うところの「コード」ではない「素材データ」です。

そしてまさにこのような「外部ファイルから読み込まずにコードに直接書き込まれた素材データ」のことを、人々は「リテラル」と呼んでいたわけですが、それに対応する概念であるところの「ファイル入出力による素材データの読み書き」というのを僕は理解していなかったので、その状況のままリテラルリテラル、といくら言われても一体それの何がおいしいのか、なぜわざわざそのような名前が付けられているのか想像できなかった、というのがリテラルを理解できなかった背景のようでした。

ファイル入出力における文字エンコーディング処理

逆にというか、その違いがイメージできてしまえばもはや「ファイル入出力における文字エンコーディング」について新たに理解すべきことは無に等しいです。(言い過ぎた)

先ほどのencode.plに書いたリテラルPerl入学式」を、今度はコード内ではなく外部ファイル「data.txt」から読み込み、それを先と同様にコード上の処理でタテに並べ直し、結果を標準出力へ書き出す、という風に書き直してみましょう。

まずはわざと文字化けさせるため、エンコーディング処理をせずにこんなコードで。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

open my $fh, '<', 'data.txt'
    or die "failed to open file: $!";

my $text = <$fh>;
my @text = split //, $text;
for (@text) {
    say $_;
}

close $fh;

念のためですが、外部ファイル「data.txt」の中身はこれです。
f:id:note103:20151204020150p:plain

この外部ファイルをencode.plと同じperl-encodeing-lessonディレクトリに入れて、実行。
f:id:note103:20151204020330p:plain

文字化けしましたね!(再)

では、リテラルに対して行った処理をここでも使ってみましょう。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Encode; #追加1

open my $fh, '<', 'data.txt'
    or die "failed to open file: $!";

my $text = <$fh>;
$text = decode('utf8', $text); #追加2
my @text = split //, $text;
for (@text) {
    $_ = encode('utf8', $_); #追加3
    say $_;
}

close $fh;

実行。
f:id:note103:20151204020442p:plain

できてますね。

ちなみに、上記では$textへのデータの格納を以下のようにしていますが、

my $text = <$fh>;

これは対象が1行だけなのでこのようにしていますが、複数行の場合は(普通はそうかも)、配列として読み込んでwhile文やfor文を使うか、以下のように一気に読み込む感じかと思います。

my $text = do {local $/; <$fh>;};

PerlIOレイヤー

さて、基本原理は上記のように、「Encodeモジュールを使ってPerlの処理前にdecode、Perl処理後の出力前にencode」ということで良いかと思いますが、一つのファイル内で毎回それやるのもなあ、という人のためにあるのがbinmode関数、open関数、utf8プラグマです。

たとえば、上記のリテラルを使ったコードとファイル入出力を使ったコードを、それらを使って書き換えるとこんな感じになります。
f:id:note103:20151204015015p:plain
f:id:note103:20151204020810p:plain

はい。ただもうこの辺に入ると、僕が解説するにはレベル高すぎというか、あまり詳細を理解できていないので、前述のリンクをはじめネットや関連書籍の解説をぜひご参照ください。

参考書籍

ちなみに、そんな参考書籍として僕の場合は以下がヘビロテ中です。(何度も紹介しているものが多いですが)

まるごとPerl! Vol.1

まるごとPerl! Vol.1

Perl CPANモジュールガイド

Perl CPANモジュールガイド

もっと自在にサーバを使い倒す 業務に役立つPerl (Software Design plus)

もっと自在にサーバを使い倒す 業務に役立つPerl (Software Design plus)

kazhiramatsu.hatenablog.com

最初のムックは2006年発刊ということで、もしかすると今なお使えるPerlの情報というのは少ないかもしれないのですが、何しろそうそうたる執筆陣が参加していて、古本で見つけた時には即購入したのでしたが、この前半で小飼弾さんが寄稿されている「まるごとEncode」という原稿は水を飲むようにグイグイ読めて、かつPerlの文字エンコーディングの元となる考え方に触れるには良い資料ではないかと思っています。

続くトミールさんと木本さんによる書籍では「文字列」や「バイト列」の定義や扱い方など、本記事に関連する話題が詳しく解説されていて、また最後の『雅なPerl入門』はコンパクトかつ実践的にそれらの要点がまとまっていて、諸々合わせて読むことで非常に理解が深まりました。

ということで、今後も私の文字エンコーディング坂は果てしなく続きそうですが、本記事についてはひとまずビバーク(不時泊)的にここまでと致します。

明日の「Perl入学式 Advent Calendar」の担当は、今年度から東京編のサポーターとしてご活躍中の @veryblue さんです。楽しみです!
qiita.com

www.youtube.com
the pillows - Happy Bivouac)