【perlメモ】EUC文字列の正しいマッチングと置換

perlでプログラミングする時かつて文字コードはShiftJisまたはEUCが主流でした。ShiftJisは普通に文字を表示しようとするだけも文字化けがよく起こるので色々難儀しました。文字列のマッチングについてもShiftJISだと文字コードの関係でマッチングが上手くいかないことが多く、わざわざEUCに変換してからマッチングするという事も有りました。そういう意味ではEUCコードの方がオススメなのですが、WindowsがShiftJISがメインの文字コードだったこともありShiftJISの方がよく使われていた様に思います。ShiftJISと違って文字を表示するだけなら文字化けが発生しないEUCならマッチングがうまくいきそうなイメージが有りますが、実際にはEUCでもマッチングが上手くいかないパターンが有ります。

下のようにEUC文字コードで書いたperlのスクリプトが有ります。一応Windowsのコマンドプロンプト上で動かすサンプルとしているのでjcode.plで文字コードをShiftJisに変換してから表示します。

sample01.pl

#!/usr/bin/perl

require ‘jcode.pl’;
my $str = ‘□□’;

while($str =~ /(.)/g){
    print sprintf("0x%X\n",ord($1));
}

if($str =~/(□)/){
    my $a = $1;
    &jcode::convert(\$a,’sjis’);
    print "[$a]\n";
}

これを実行すると下のようになります。$strに格納されている文字の16進数文字コードを表示したあと、『』でマッチングを行ないマッチしたら。マッチした文字を[ ]の中に表示するようにしています。上のプログラムでは文字列□□にたいして、文字[□]でマッチングを行っているのでマッチするのは当たり前で実際にマッチし、文字コードのあとに[□]が表示されているので正常に動作しているように見えます。

> perl sample01.pl

0xA2
0xA2
0xA2
0xA2
[□]

>

では、次のsample02.plはどうなるかというと、これもマッチしてしまいます。

sample02.pl

#!/usr/bin/perl

require ‘jcode.pl’;
my $str = ‘あ■’;

while($str =~ /(.)/g){
    print sprintf("0x%X\n",ord($1));
}

if($str =~/(□)/){
    my $a = $1;
    &jcode::convert(\$a,’sjis’);
    print "[$a]\n";
}

実行してみると下の様になります。文字列『あ』に対して文字『』でマッチングをしているのにもかかわらずマッチしてしまっています。

> perl sample02.pl

0xA4
0xA2
0xA2
0xA3
[□]

>

原因はのEUC文字のコードは0xA2A2なのですが、

『あ』のEUC文字コードは0xA4A2
』のEUC文字コードは0xA2A3

となります。

『あ』と並べるとA4A2A2A3と並んでいることになります。つまり別の文字と文字との結合部分が文字『』を表す事になりマッチしてしまうわけです。

以下のようにすると正しくマッチングすることが出来るようになります。

sample03.pl

#!/usr/bin/perl

require ‘jcode.pl’;

#my $str = ‘□□’;
my $str = ‘あ■’;

my $ascii = ‘[\x00-\x7F]’;
my $twoBytes = ‘[\x8E\xA1-\xFE][\xA1-\xFE]’;
my $threeBytes = ‘\x8F[\xA1-\xFE][\xA1-\xFE]’;

while($str =~ /(.)/g){
    print sprintf("0x%X\n",ord($1));
}

if($str =~/^(?:$ascii|$twoBytes|$threeBytes)*?(□)/){
    my $a = $1;
    &jcode::convert(\$a,’sjis’);
    print "[$a]\n";
}

実行すると下のようになります。ちゃんと、文字列が「□□」のときだけマッチするようになります。

> perl sample03.pl

0xA4
0xA2
0xA2
0xA3
>

原理的には、マッチングしたい文字の前にあっても良い文字があることをチェックしています。

 

同じように置換にも応用できます。下のサンプルは、文字『』を『◇』に置換するサンプルです。

sample04.pl

#!/usr/bin/perl

require ‘jcode.pl’;

my $str = ‘あ□□□■’;

my $ascii = ‘[\x00-\x7F]’;
my $twoBytes = ‘[\x8E\xA1-\xFE][\xA1-\xFE]’;
my $threeBytes = ‘\x8F[\xA1-\xFE][\xA1-\xFE]’;

while($str =~ /(.)/g){
    print sprintf("0x%X\n",ord($1));
}

my $str2 = $str;

&jcode::convert(\$str,’sjis’);
print "original[$str]\n";

if($str2 =~ s/((?:$ascii|$twoBytes|$threeBytes)*?)(□)/$1◇/g){
    &jcode::convert(\$str2,’sjis’);
    print "[$str2]\n";
}

実行すると以下のように正常に置換できます。普通にs//◇/gで置換しようとすると文字化けします。

> perl sample04.pl

0xA4
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA3
original[あ□□□■]
[あ◇◇◇■]

>

参考:
正しくパターンマッチさせる – Perlメモ

 

しかし、現在だと普通にutf8で処理したほうが楽です。utf8に変換してからやると普通にマッチングできて楽な気がする。わざわざEUCで書いてるので変な事になってるけど、実際の実用プログラムではソース自体をutf8で書いたほうが良いだろう。

sample05.pl(EUC文字コード)

#!/usr/bin/perl
#use utf8;
use strict;
use warnings;
use Encode qw/from_to decode_utf8 encode_utf8/;

my $str = ‘あ□□□■’;
my $pat = ‘□’;
my $res = ‘◇’;

while($str =~ /(.)/g){
    print sprintf("0x%X\n",ord($1));
};

my $str2 = $str;

from_to($str, ‘euc-jp’, ‘cp932’);
from_to($str2,’euc-jp’, ‘utf8’);
from_to($pat, ‘euc-jp’, ‘utf8’);
from_to($res, ‘euc-jp’, ‘utf8’);
#$str2 = decode_utf8($str2);

print "original[$str]\n";

if($str2 =~ s/$pat/$res/g){
    from_to($str2,’utf8′, ‘cp932’);
    print "result  [$str2]\n";
}

実行すると下のようになる。

> perl sample05.pl

0xA4
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA2
0xA3
original[あ□□□■]
result  [あ◇◇◇■]

>

 

Windowsでファイル名を置換する文字コードutf8で書いたプログラムの例。実際にはrenameする必要があるが、危険なのでここでは省略して表示を行うのみ。

下の様なファイルの一覧があるとき、スペースが全角だったり、スペースが全角と半角混在していたり、スペースが複数入っていたり、第1話からはじまってる場合前0を入れて第01話にしたい場合などがある。不要な文字を削りたい場合などもある。

20100453

そういうわけで、下のスクリプトがそれをやるスクリプトです。

仕様

  • 拡張子が.txtのファイルのみ処理する。
  • 第n話を第nn話に置換する。
  • 第nn話の後に半角スペースがない場合は入れる。
  • 全角スペースは半角スペースに置換する。
  • 複数の半角スペースは1個にまとめる。

sample06.pl(utf8)

use strict;
use warnings;
use utf8;
use Encode qw/from_to decode_utf8 encode_utf8/;

opendir(DIR,"./");
while(my $file = readdir(DIR)){
    next if($file eq ‘.’);
    next if($file eq ‘..’);
    next if($file !~ /\.txt$/);

    my $utf8_file = $file;
    from_to($utf8_file, ‘cp932’, ‘utf8’);
    $utf8_file = decode_utf8($utf8_file);

    #第n話の数字に前0を付ける。
    $utf8_file =~ s/第([0-9])話/第0$1話/g;
    #第n話のあとに半角スペースを追加
    $utf8_file =~ s/(第[0-9]+話)([^ ])/$1 $2/;
    #全角スペースは半角スペースに置き換える。
    $utf8_file =~ s/ / /g;
    #複数の半角スペースは1個にまとめる
    $utf8_file =~ s/ +/ /g;

    my $cp932_file = encode_utf8($utf8_file);
    from_to($cp932_file,’utf8′,’cp932′);
    print "$cp932_file\n";
}
closedir(DIR);

実際に実行すると以下の様になる。

>perl sample06.pl

小説 第01話 あいうえお.txt
小説 第02話 あいうえお.txt
小説 第03話 あいうえお.txt
小説 第04話 ああああ.txt
小説 第05話 あああ.txt

>

 

utf8で処理するとマッチングや置換の処理がシンプルになる。

※cp932はマイクロソフトの独自拡張がされたShiftJisの事。cp932ではなくshiftjisで文字コードの変換を行うと変換できない文字が出てしまうので注意。

(Visited 1 times, 1 visits today)

タグ : ,