読者です 読者をやめる 読者になる 読者になる

Perlにおける日本語文字化け対策の私的まとめ

perl

Perlで日本語のテキストを処理しているとけっこうな割合で文字化けにハマる。
近いことについては以前ここでみっちり書いたが、
note103.hateblo.jp
どうもその後、自分はbinmode関数やopen関数、およびutf8やopenプラグマについて理解が怪しいな、と思ったのでいろいろ調べつつ現時点での認識をまとめてみる。

環境づくり

まずはサンプルケース的に、文字化けしがちな状況を作る。

素材データとして、以下の内容をエンコーディングUTF-8のテキストファイルにsource.txtという名前で保存。

りんご
hello

1234
ネコ

次に、そのデータをopen関数で読み込み、split関数で切り刻んで標準出力および書き込み用ファイルresult.txtへ書き込むPerlスクリプト「openio_stdout.pl」を書く。

#!/usr/bin/env perl
#
# openio_stdout.pl

use strict;
use warnings;
use feature 'say';

use utf8;
use open IO => qw/:encoding(UTF-8)/;
binmode STDOUT, ':encoding(UTF-8)';

# 1. 素材データ読み込み
my $in = 'source.txt';
open my $fh, '<', $in or die $!;
    my @data = <$fh>;
close $fh;

# 2. いろいろ処理
my @out;
for (@data) {
    my @split = split//, $_;
    for my $s (@split) {
        push @out, "字$s";
    }
}

# 3. 処理済みデータを書き込み
my $out = 'result.txt';
open $fh, '>', $out or die $!;
    print $fh @out;
close $fh;

# 4. 標準出力に出力
print @out;

実行。

字り字ん字ご字
字h字e字l字l字o字
字犬字
字1字2字3字4字
字ネ字コ字

素材テキストの1文字ずつの間に「字」を入れてみた。
これが書き込み先のresult.txtにも、標準出力(ターミナル)にも出ている、という状況。

つまり、この時点では文字化けしていない。が、それは冒頭にある以下の宣言がちゃんと動いているからで、

use utf8;
use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

これを誤るとけっこう盛大に文字化けする。
以下、それを順に見ていく。

utf8プラグマ

まず、今回の例ではソースコード内に日本語の文字(「字」)を入れているので*1、最初のutf8プラグマが必要になる。

use utf8;

もしこれをコメントアウト等で無効にすると……

f:id:note103:20160925140246p:plain

こんな感じになる。
標準出力でも、書き出しファイルでもこうなる。

utf8プラグマというのは、この例における「字」のように、ソースコード内に素材となる文字を直接書いたとき、それをPerlで処理するための「内部文字列」に変換してくれるもの。

内部文字列に変換されなかったらどうなるかと言うと、Perlがそれを「なんか文字じゃないもの」ぐらいにしか扱ってくれず、上の画像のように化けてしまう。

ちなみに、この「内部文字列に変換されなかった何か」のことは外部文字列……とは呼ばず、「バイト列」と呼ぶことが多いらしい。

で、ここではそのコード内に直接書かれた「字」をutf8プラグマの宣言によって「内部文字列」に変換している。

openプラグマ

次に、このコードではファイル入出力(source.txtを読み込んでresult.txtに書き込む)をやっているので、2番めのopenプラグマも必要になる。

これは別解として、各open関数の第2引数でこのように書いても良いようで、

open my $fh, '<:encoding(UTF-8)', $in or die $!;
(略)
open $fh, '>:encoding(UTF-8)', $out or die $!;
(略)

それを冒頭のプラグマでまとめて宣言している。

また、このプラグマ部分はこのように分けて書くこともできるが、

use open IN => ':encoding(UTF-8)';
use open OUT => ':encoding(UTF-8)';

今回はIN/OUTの両方をやってるのでIOというレイヤーでまとめている。
と同時に、IOなら省略してもOKらしい。

use open ':encoding(UTF-8)';

で、さっきのutf8プラグマは元に戻して、このopenプラグマだけ無効にするとどうなるかと言うと、こうなる。

f:id:note103:20160925140639p:plain

このうち、上側の

Wide character in print at /Users/kadomatsuhiroaki/open/openio_stdout.pl line 29.

というのは標準出力(ターミナル)に出るもので、書き出し先のファイルには出てこない。

その下の文字化け部分は、標準出力と書き出しファイルの両方に出ている。
不思議なことに、というか特徴的なこととして、一つ前の文字化けでは文字間に挟んだ「字」が化けていたのに対し、今度は「字」だけが普通に表示されている。

この段階ではutf8プラグマが生きていて、openプラグマを止めているのでそういう生死の違いが出るのだろう。

また、openプラグマの引数「:encoding(UTF-8)」は、「:utf8」と書いてもほぼ同等の働きをするようだが、前者はデータの入力時にそれが正しいutf-8かどうかをチェックするのに対し、後者はそうじゃないらしいので少なくとも入力時はあまり省略しないほうが良さそうである。

binmode関数

最後に、binmode関数。じつはこれが今まで一番わかっていなかったのだが、どうもいろいろな現象や解説書を並べて観察してみると、単に(というか)標準入出力時にその対象となるデータの文字コードを指定するもの、と考えれば良さそうである。

今回はデータをファイルから入力して、ファイル&標準出力に出しているので、STDOUT時の文字コードのみをbinmodeで指定している。

binmode STDOUT, ':encoding(UTF-8)';

例のごとく、これだけコメントアウトしてみると……。

f:id:note103:20160925140820p:plain

さっきと同じ。……かと思いきや、また微妙に違っていて、まず以下の対象行番号が29から33に変わっている。

Wide character in print at /Users/kadomatsuhiroaki/open/openio_stdout.pl line 33.

ではこの二つの行がソースコード上のどこにあたるかと言うと、以下の部分における、

# 3. 処理済みデータを書き込み
my $out = 'result.txt';
open $fh, '>', $out or die $!;
    print $fh @out;
close $fh;

# 4. 標準出力に出力
print @out;

前者のprint文が29行目で、最後のprint文が33行目。

つまり、openプラグマはopen関数内のprint文に効いていて、binmode関数の方は標準出力時に影響していることがこれらの情報から見てとれる。

また、肝心の本文のほうも、前者は文字化けしているが後者はしていない。
ただしこれは、ここで問題にしているbinmode関数の有無による違いではなく、ひとつ前の例でopenプラグマをコメントアウトした際、入力レイヤー(IN)も外してしまっているために、前者では文字化けが生じていると考えられる。

試しに、一つ前の文字化けはこのような状態で生じたが、

# use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

このように、その状態からopenプラグマの入力レイヤーだけを復活させると、

use open IN => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

文字化けは解消する。
(と言いつつ、たしかに文字化けは消えるが今度はbinmode関数のみを無効にした場合とは少し異なるエラーが出てくる。つまり、それらはどれも異なる状態なのだが、これ以上の追求&説明はだいぶ大変なので割愛)

標準入出力もopenプラグマにまとめる

今回の研究というか勉強に際しては、以下3冊を繰り返し見比べては突き合わせ、読み返したが、

初めてのPerl 第6版

初めてのPerl 第6版

かんたん Perl (プログラミングの教科書)

かんたん Perl (プログラミングの教科書)

Perl CPANモジュールガイド

Perl CPANモジュールガイド

そのうちのCPANモジュールガイドで紹介されていた方法。

現在はこのようにしている2行を、

use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

以下のようにまとめられる。

use open IO => qw/:encoding(UTF-8) :std/;

ようはbinmode関数で指定していたことをopenプラグマの引数に「:std」とすることで代替(というか)できる。

いま自分で書いているコードにおいては、同様のことをしたいときは基本最後の形にしている。

まとめ

日本語の文字化けについては常に悩まされるところだが、原因を追っていくとそれなりに明らかになってくるのが面白い。

エラーの内容も書き換えた部分によって変わってくるし、同じエラー・メッセージでもその分量(行数というか)が異なり、それがまたマシンの気まぐれなどではなく、異なるには異なるだけの理由がある。

面倒だ、ぐぬぬ、イライラする! と思うことも少なくはないが、それでも玉ねぎの薄皮を剥がすように少しずつマシになってきていると感じる。
そのような理解を助けてくれる上述の書籍、および知見に満ちた先達やネット上の各種解説記事に感謝します。

付録

今回の出力例の画像(黄色っぽいベージュっぽいやつ)について、本来ターミナルに出るような結果なのに行番号が出ているのはなぜだろう? と思われる人もいるかもしれないが、これはコードの実行結果をVimの同一ウィンドウ&分割バッファで表示してくれるプラグインquickrun.vim」を使って表示している。
github.com

ここでやっているような利用法については以下の記事でも触れたので合わせて挙げておく。
note103.hateblo.jp

*1:サンプルの文字選択を誤ったかもしれない。まぎらわしい。