2009/09/23(Wed)UTF-8な文字列をsubstrする

2009/09/23 5:10 Languages::Perl
ずいぶん以前からPerlで物を書くときには、文字コードをUTF-8にし、use utf8をしないで書いている。
で、その際、substrなどで文字列を切り出したいとすると、UTF-8は1文字あたりの長さが可変長であるため、入力文字列によっては変な文字が切り出されてしまうことになる。
これを正しく切り出すときに色々調べてみたメモ。

use utf8について

Perl 5.8.x Unicode関連に記されているように、
1.ソースコード中の文字列がUTF8になる。
2.マルチバイトの変数名なども使える。
なことがウリらしく、表面上の意味は、PerlにscriptがUTF-8で書かれていることを教える事が目的らしいので、次のようなコードも通ります。*1
#!/usr/local/bin/perl

use utf8;
my $あいうえお = "x";
print $あいうえお;
で、このときにsubstrを使うと、use utf8;されていることによってsubstrはバイト数ではなく文字数で切ってくれるので何も問題ありません。
#!/usr/local/bin/perl

use utf8;
binmode STDOUT, ":utf8";
# 上の2行又は use encoding 'utf8';
print substr("123あいうえお", 0, 5)."\n";
binmodeの変更も行っているのは、use utf8だけではただUTF-8で書かれているという事を示すだけで、Perl IOレイヤまでUTF-8エンコーディングが適用されないらしいので。*2

しかし、自分的には
  1. use utf8;と、いちいち書くのが面倒
  2. use strict;等と違い、もしeuc-jpで保存し直したらうまく動かなくなる
  3. 意図的にShift_JISやeuc-jpで出力させたいとき、動かない
特にCGIとして動作させるとき3つ目がネックで、例えば次のようなコードは
#!/usr/local/bin/perl

use Encode;
use utf8;
my $x = "あいうえお";
Encode::from_to($x, "utf-8", "cp932");
print $x;
次のようなエラーで動きません。
Cannot decode string with wide characters at /usr/lib/perl5/5.10.0/i386-linux-thread-multi/Encode.pm line 182.
use utf8;を外せば出力可能なので、これはちょっと面倒です。
ゆえに、use utf8;は自分の流儀的には書かない主義です。

で、どうするか

話が逸れましたが、utf8を正確に数えてやるには、一度UTF-8でエンコードされた状態をデコードしてやれば出来るようです。→cf. substr関数を使用したときの文字化け対策 (Perl / UTF-8) - substrの文字化けの対策 - マイブーム
#!/usr/local/bin/perl

my $a = "123ABCあいうえお";
utf8::decode($a);
foreach(0 .. 11){
    my $x = substr($a, $_, 1);
    utf8::encode($x);
    print qq|[$x]|;
}
print "\n";

↓

[1][2][3][A][B][C][あ][い][う][え][お]
Encode.pmを使うとさらに短く書けます。
#!/usr/local/bin/perl

use Encode;

Encode::decode_utf8($a);
foreach(0 .. 11){
    print qq|[@{[Encode::encode_utf8(substr($a, $_, 1))]}]|;
}
print "\n";

まあ、上記のサンプルは、1文字ずつを[ ]で囲って出力しているので、decode_utf8を外側に出していますが、実際に使うときは
#!/usr/local/bin/perl

use Encode;

my $text = "すごく長い長い文字列があったとします";
my $short_text = Encode::encode_utf8(substr(Encode::decode_utf8($text), 0, 10));
print $short_text;
1行で書けるのでスマートです。


別の解としては、Encode::encode_utf8の代わりに、
binmode STDOUT, ":utf8";
という手もありますが、これの場合は、ワンタイムのエンコーディングではなく、以降のSTDOUTが全てエンコーディングされてしまうので注意が必要です。
局所的にエンコーディングしておいた方がスマートである、と私は思います。