Translate

2013年8月17日土曜日

マルチバイと文字列の文字列操作のお話

"abc"という文字列が何文字か調べてみましょう。

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

my $str = "abc";
say $str. "は、". length($str). "文字です";

実行結果:
abcは、3文字です

問題ありませんね。
では、"あいうえお"が何文字か調べてみましょう。
use strict;
use warnings;
use feature 'say';

my $str = "abc";
say $str. "は、". length($str). "文字です";

実行結果:
あいうえおは、15文字です

はい、15文字ですn…
えぇぇぇぇぇ。それ困ります。

このようにマルチバイト文字列の場合見た目の文字数通りカウントできないため
文字列操作で文字数を絡める場合問題となります。
そのため、見た目の文字数と一致させるためには、
プログラム内部で使用する"内部文字列"という表現に変換してあげる必要があります。
use strict;
use warnings;
use feature 'say';
use utf8;

my $str = "あいうえお";
say $str. "は、". length($str). "文字です";

簡単です。use utf8;とつけただけです。

実行結果:
Wide character in say at t.pl line 7.
あいうえおは、5文字です

5文字と表示されているので正しくカウントされました。
ただ、Wide character in say 〜 としかられました。
これは、何かというと、プログラム内部で扱う文字表現のまま出力するとおこられるのです。
perlの場合互換性を大事にされているので昔のプログラムもそのまま動くように配慮されているため
バージョンがあがってから追加された機能はいろいろ一手間加えてあげないといけないわけです。

そんなわけで一手間加えてみます。
use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDOUT, ":utf8";

my $str = "あいうえお";
say $str. "は、". length($str). "文字です";
binmode STDOUT, ":utf8";
を追加しました。
ターミナルへの出力(いわゆる標準出力)の際に問題となっているので
binmode STDOUTで標準出力を行うもの全てに対し処理が加えられます。
そして、ターミナルはutf8の文字列を受け付けているので:utf8として出力しています。
単純ですね。

内部文字列表現かそうじゃないかは、ぱっとみわからないので判断しづらいかもしれませんが
ともかく、perlに文字列を渡す時に内部文字列に変換して外に出す時に元に戻すということをすればよいわけです。

ここまでのまとめ


次。
エラー処理をしたい時warnやdieを使うと思いますが
use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDOUT, ":utf8";

warn("警告");
die("すっごい致命的なんです");  

実行結果:
Wide character in warn at t.pl line 7.
警告 at t.pl line 7.
Wide character in die at t.pl line 8.
すっごい致命的なんです at t.pl line 8.

出ました!!
エラー系は標準出力と異なるため、STDOUTでは太刀打ちできません。

use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

warn("警告");
die("すっごい致命的なんです");
binmode STDERR, ":utf8";
標準エラーを追加しました。

実行結果:
警告 at t.pl line 8.
すっごい致命的なんです at t.pl line 9.

いい感じですね。
まとめです。


さて、他に考えられるI/Oは…
標準入力を考えてみましょう
se strict;
use warnings;
use feature 'say';
use utf8;
binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

chomp(my $input = );
say $input;

"あいうえお"って入力すると・・・

実行結果:
あいうえお
あã

文字化けを起こしました。
何が起きているか確認してみましょう。

現時点で、外からやってきた文字列は内部の文字列表現になっていません。
それにもかかわらず、binmode STDOUT, ":utf8";があるため
文字列の出力の際に、内部表現だという前提で変換しているため、
おかしなことになったわけです。

もうすでに対策は薄々気づけそうですが

use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDIN,  ":utf8";
binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

chomp(my $input = );
say $input;
binmode STDIN, ":utf8";
を追加しました

実行結果:
あいうえお
あいうえお

理由がわかれば、簡単ですね。


今度はファイルからのよみこみ書き込みを行ってみます。
まずは読み込み
use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDIN,  ":utf8";
binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

open my $fh, '<', 'string.txt' or die "$!";
say <$fh>;
close $fh;

尚、string.txt には あいうえお と記載されているだけです。
1行取得して出力した結果は以下の通りです。

実行結果:
あã


また文字化けです。
当然ながら、perlに渡す前に内部表現に変換していないからです。
対応してみます。

use strict;
use warnings;
use feature 'say';
use utf8;
binmode STDIN,  ":utf8";
binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

open my $fh, '<:encoding(utf8)', 'string.txt' or die "$!";
say <$fh>;
close $fh;

'<' から '<:encoding(utf8)' へ変更しています。 string.txt はutf8で保存されています。 実行結果:

あいうえお

うまくいきました。
ファイルで書き込みを行う時も同じ操作になります。
'>:encoding(utf8)' と言った感じです。




残すは、他のソースコードとのやり取りですね。
つまり、他のモジュールへ文字列を渡したり受け取ったりする時です。
これは、モジュールごとに内部表現で渡すべきか、そうじゃないかは対応がまちまちなので
実際に試してみない限りなんとも言えません。
ケースバイケースで対応しましょ。
内部表現にしたい時は、use Encode; を使い decode_utf8('文字列')
内部表現からもとに戻す時は、use Enocde;を使いencode_utf8('文字列')
とすればよいです。

なお、encode_utf8 はenocde('utf8', '文字列') 別の書き方です。
違うエンコードの時は、encode('文字コード', '文字列')
decode('文字コード', '文字列')としてください。

最後のまとめです。
INとOUTで分けておきました。
これさえ注意すればなんとかなりますね。


基本的な考えとして、use utf8は必ずつける。
外から受け取る時は内部表現に変更する
外へ出す時は、内部表現をやめる
モジュール間でやりとりするときは、そのまま(内部表現)でやりとりする
という感じでよいのではないでしょうか?

0 件のコメント:

コメントを投稿