utf8とEncodeの焼きまわし記事

2011年にしてようやくutf8と向き合うことになりました。
と言っても、95%は「原則」を正しく読めば解決しました。
エントリとしては過去情報の焼き増しになるけど、一応メモとして残しておく。


原則
http://d.hatena.ne.jp/tokuhirom/20080408/1207619640
http://perl-users.jp/articles/advent-calendar/2009/casual/10.html


特にxaicronさんのエントリを改めて読み直したらカンペキにまとまっててすごい!Advent Calendar++


最初に認識しておくこと
sjisな世界、eucな世界、utf8な世界は他のレイヤーでもあるが、utf8フラグが立っている世界はperlの中にしかない」


今回困ったのは、

Cannot decode string with wide characters. 〜

のように出た場合と

Wide character in print 〜

が出た場合。
後者はフラグが立った状態でprintした場合なので、encodeして内部文字列じゃなくしてからprintすればおk。


上記原則に則るとまずdecode処理をするわけですが、
Encode::decodeはすなわちutf8フラグを立てることであって、既に立っているものに対して
実行すると上記エラーが発生します。
なので基本は「入口 -> decode-> 処理 -> encode -> 出口」 なんですが、
入口の時点でutf8フラグが立っている場合に備えて、フラグが立っているか確認したほうがいい。
入口が完全に外界のみならば、フラグが立った状態で来ることはないわけだけど、perlの世界における外界からの場合はその可能性がある。これが今回混同してたところ。


実際の処理の中は

  • use utf8; されている(その中で扱う文字列には自動でフラグが立つ)
  • ファイルそのもののエンコーディングがutf8になっている

という前提でいくと、「フラグが立っていない場合のみdecodeする」で状態はめでたく統一される。
もし仮に、フラグが立ってない世界を構築しているのであれば、「立っていたらEncodeする」ことで同じくデータ的には揃った状態になる。

#!/usr/bin/perl
             
use strict;  
use warnings;
             
package UTF8World;
             
use strict;  
use warnings;
             
use Test::More;
use Test::Warn;
use Encode;  
use utf8;    
             
sub function_of_utf8world{
    my $charset = shift;
    my $arg_str = shift;
             
    my $flagged_utf8;
    if( Encode::is_utf8( $arg_str ) ){
        diag 'arg_str is flagged utf8';
        $flagged_utf8 = $arg_str;
    }        
    else{    
        diag 'arg_str is not flagged utf8';
        $flagged_utf8 = Encode::decode_utf8( $arg_str );
    }        
    ok Encode::is_utf8( $flagged_utf8 ) , 'flg ON';
             
    warning_like { print STDERR $flagged_utf8 } qr/^Wide character in print/ ,q/Wide character in print/ ;
             
    $flagged_utf8 .= 'フラグON';
             
    return Encode::encode( $charset , $flagged_utf8 );
             
}

package OutWorld;
             
use strict;  
use warnings;
use Test::More;
use Encode::Guess;
{            
    use utf8;
    my $flg_on_utf8 = 'あ';
    no utf8; 
             
    ok Encode::is_utf8( $flg_on_utf8 );
             
    my $ret = UTF8World::function_of_utf8world( 'utf-8' , $flg_on_utf8 );
             
    ok ! Encode::is_utf8( $ret );
    my $enc = guess_encoding($ret, qw/ shiftjis euc-jp /);
    is $enc , 'shiftjis or utf8' , 'flg on utf8';
}            
             
{            
    use utf8;
    my $str = 'あ';
    no utf8; 
    my $flg_off_utf8 = Encode::encode_utf8( $str );
             
    ok ! Encode::is_utf8( $flg_off_utf8 );
             
    my $ret = UTF8World::function_of_utf8world( 'utf-8' , $flg_off_utf8 );
             
    ok ! Encode::is_utf8( $ret );
    my $enc = guess_encoding($ret, qw/ shiftjis euc-jp /);
    is $enc , 'shiftjis or utf8' , 'flg off utf8';
}            
             
{            
    my $str = 'あ';
    my $euc = Encode::encode( 'euc-jp' , $str );
             
    ok ! Encode::is_utf8( $euc );
             
    my $ret = UTF8World::function_of_utf8world( 'euc-jp' , $euc );
             
    ok ! Encode::is_utf8( $ret );
    my $enc = guess_encoding($ret, qw/ shiftjis euc-jp /);
    is $enc , 'shiftjis or euc-jp' , 'shiftjis or euc-jp';
}            
             
             
done_testing();

=================================================================
2011.02.21追記
nihenさんより、ブクマコメントいただきまして、以下のリンクを見ました。
http://subtech.g.hatena.ne.jp/miyagawa/20080218/1203312527
同じ事はこちらにもありました。
http://perl-users.jp/articles/advent-calendar/2010/casual/4
先人の知恵を活かしきれずに同じ過ちを犯してしまっていたようです。。。


今回書いた意図としては、日本語文字列変換の際に
One.pm -> Convert.pm
Other.pm -> Convert.pm
Another.pm -> Convert.pm
みたいな呼び出し関係があり、Convert.pmに
文字列を渡すとConvert.pmの中身が
sub utf8_to_euc{
# decode
# ごにょごにょ
# encode
}
となっている。という前提で
フラグがONだと、最初のdecodeで怒られてしまう、
というのが解決したい課題でした。


ここで、Convert.pmの中で
my $enc = encode_utf8($str) if is_utf8($str);
を挟むことによって、decodeが通る、という理解を残したかったのです。


理解できていなかったのは、日本語以外のケースとモジュールの立ち位置でしょうか。
latin-1で扱われる文字があることと、この処理をするのがCPANモジュールなのか
特定サービスの限られた呼び出しのみを想定するのか。が把握できていませんでした。

また上のテストスクリプトがフラグOFFの時にdecodeするようになってるので、
残したかった事と合ってない、と今更気づきました。


まとめると、対象が文字列であるか否かを意識した上で処理をすること、でしょうか。
(まだ混乱してるかも・・・、時間を空けて読み直す)

それにしてもやっぱりAdvent Calendar(2009も2010も)++でした。