Shift-JISテキストを正しく扱う

最近の更新履歴


最初に
ありがちなエラー
エラーや間違いを防ぐ対策
文字列リテラルの作り方
正規表現とマッチ
グローバルマッチ
アルファベットの大文字と小文字
長い文字列に対する正規表現検索
外字の変換
CP932重複定義文字の変換
文字数を数える
文字単位に分割する
いろいろな分割
特定の長さで切りそろえる
日本語文字列を並び替える
Shift-JISの漢字を含むファイル名/パス名

最初に

日本語の文字コードにはいくつかのものが使われています。ある程度一般的なものなら、どれを使ってもよいでしょう(どの文字コードで符号化されているかの情報が失われさえしなければ)。

例えば、日本語版Windowsでは、メモ帳でもDOS窓でもShift-JISが使われています。こういう場合、処理の途中でわざわざEUC-JPやUTF-8に変換するとしたら面倒です。デバッグのとき、「この段階ではこの変数には何が入っているのか」出力して点検するのはよく行われますが、このときEUC-JPとして収められていたら、作業は手間どるでしょう。入力も出力もShift-JISで行うつもりなら、処理の全体でShift-JISのまま扱えたらきっと便利でしょう。

注: "シフトJIS", "Shift_JIS", "Shift-JIS" などの表記の違いについてはよくわかっていません。今のところ分かっていることは:

しかし、Shift-JISにはある種の癖があって、ちょっとしたことがバグやエラーや文字化けの原因となります。なんとかならないものでしょうか。

Perlは制御文字やナル文字を含むバイナリデータですら正しく処理できるように設計されているので、スクリプトやテキストをShift-JISで書いたくらいで問題になることはありません。

しかし、perlがスクリプトを解釈するときは(通常)バイト単位で調べるので、Shift-JISのようなマルチバイト文字を含む符号はそのままでは直接理解できません。

たとえば、Shift-JISで 'あ' という文字は、16進数で82 A0という2バイトで表されます。これを "\x82\xA0" と書いてもperlにとっては同じです。これが日本の(country)日本語の(language)文字であるとか、Shift-JISで書かれている(charset)とかいう情報はどこにも含まれていません。

そのため、Shift-JISで書きたいときには、perlの誤解を受けないように書いてやらなければなりません。その配慮は、プログラマがしてやらなければなりません。この文書の記述は、そのような手間をかけても、Shift-JISを用いることに意義があると考えている人には参考になるかもしれません。

そんな手間を掛けたくない人は、 という対処をとったほうがよいでしょう。これらのプログラムは有名なので、探せばすぐ見つかるでしょうから、入手先はここには示しません。

なお、この文書に書かれている事が、最も勧められない方法なので、ここから先は、そのつもりでお読み下さい。この方法について何か疑問が生じたとしても、それについて他のところで質問すると、何でそんなやり方をしているのかと、きっと非難されるでしょう。かといって、私にも訊かないで下さい。


Shift-JISを使ったときにありがちな(?)エラー

Shift-JISには、第2バイトが [@-~](ASCII 10進数で64-126)の範囲に入るものがあります。これらのASCII文字は、perlにとって特別な意味をもつことがあるため、しばしばエラーの原因となります。Shift-JISでは、2バイト文字の第2バイトは、[\x40-\x7E\x80-\xFC])の範囲にあるため、実に188分の63、約3分の1の文字が何らかの問題を起こし得るといえます。

次に、Shift-JISを使ったときに起こりがちなエラーとその原因を示します。エラーメッセージはperlの違い(バージョンやどのプラットフォーム用のものであるか等)により多少の違いがあるかもしれません。

エラーにならなくても、文字化けしたり、期待したような動作をしなかったりで、うまくいかないことがあります。この場合、エラーが出ない分、原因を自分で探さなければならなくなるためバグ取りはしばしば困難です。

ここではエラーに対する対策は提示しません。対策はあとでまとめて書きます。

なお、ここには文字コードをEUC-JPにしても起こるような問題やエラーは示しません。基本的に、EUC-JPなら起きないが、Shift-JISのときには起こるような事柄に限ります

エラーにはならないけど文字化けする(1)
例えば、"表示" とか "暴力" とかいうリテラルが文字化けを起こします。これらは "侮ヲ" とか "沫ヘ" になります。これは、"表""暴"の文字の第2バイトが \ であるため、ダブルクオート文字の中では次の文字のエスケープをすることになるので、表示0x955C8EA6 であっても、クオートの結果は "表示"0x958EA6 となるからです。'表示' とすれば文字化けは起こりませんが、シングルクオートでも防げない文字化けやエラーがあります(次例)。
エラーにはならないけど文字化けする(2)
例えば、"ミソ\500" というリテラルでは、\ が脱落してしまいます。これは、'ミソ\500'q(ミソ\500) などとしても防ぐことができません。それは \\ という連続があると \ 1個になってしまうという規則があるからです。

クオートやクオート風演算子の中では、文字列にクオートと同じ文字を含められるように、\ によるエスケープを付ければクオートの終端文字ではなく、文字列の一部とみなします。そのため、\\\ の文字を表すエスケープになります。これはクオートの始端・終端文字を何にしても同じことです。

エラーにはならないけど文字化けする(3)
例えば、"丸十net" というリテラルが文字化けを起こします。これは "丸・
et"
のように途中で改行されてしまいます。これは、"十" の第2バイトが \ であるため、ダブルクオート文字の中では次の 'n' と合わせて\nのすなわち改行文字を表すメタ文字として解釈されるからです。
エラーにはならないけど文字化けする(4)
例えば、"引数 ARGV" というリテラルが文字化けを起こします。これは、" "(全角スペース)の第2バイトが @ であるため、ダブルクオート文字の中では次の ARGV と合わせて "@ARGV" という配列として変数展開を行うからです。@ARGVのように必ず定義されるような配列なら、展開されますが、別の場合ではエラーになるかもしれません(それは次項を参照)。
In string, @dog now must be written as \@dog (Perl 5.6.0まで)
「文字列の中では、@dogは今は\@dogと書かなければならない」
前例でみたように、全角スペース " "の第2バイトは @ であるため、後ろの文字と合わせて配列であるかのように解釈しようとします。"犬 dog" のような場合、@dog という配列が定義されていればそれを用いて変数展開しますが、定義されていない場合、エラーメッセージを出します。

``now must be written as''「今はこう書かなければならない」とは、Perl4までは配列の変数展開は行わなかったため、"hoge@foo.bar" のような書き方をすることができたのだが、今 Perlでは @foo が展開されてしまうので、注意を喚起するためエラーを出すようにしているようです(もしPerlが昔から配列の展開をサポートしていたら、エラーを出すことなく、黙って展開するだけだったかもしれません。次項も参照)。

"犬 \dog" とすればいいという意見もありますが、\d がメタ文字として特別意味がないためにうまくいくのであって(Perl 5.6以降では、警告 Unrecognized escape \d passed through 「認識できないエスケープ \d が渡された」を引き起こします)、例えば "花 \flower" のときは、\f が改ページ文字として解釈され、文字化けします。

Possible unintended interpolation of @dog in string (Perl 5.6.1以降)
文字列の中で、@dogが予期せずに展開される
前項と同じく、"犬 dog" ですが、Perl 5.6.1(ActivePerl 626)以降では、定義されていない配列でも黙って展開します。配列 @dog が展開されるので、"犬\x81" と同じになります。

これはエラーではなく、警告になります。

Can't find string terminator '"' anywhere before EOF
「終端文字 '"'がファイルの終り EOF までに見つからなかった」
例えば、"対応表" のようなリテラルでは、'表' の第2バイトが \ であるため、うしろの " をエスケープしてしまいます。このためperlは、その " はクオート文字列の終端文字とはみなさずに、文字列がさらに続くと考えてしまいます。これ以降、スクリプトの中に " の文字が全く含まれなければ、このようにエラー報告をします。

qq{ "日本語" }のような場合にも注意しなければなりません。"本" の第二バイトは { なので、このままでは { }のネストがずれてしまい、同様のエラーが発生します。

Bareword found where operator expected
「裸の語が演算子があってほしい位置に見つかった」
例えば、print "<img alt=\"対応表\" height=115 width=150>"; のような場合、\" による引用符のエスケープは、 の第2バイトの\のため、\\ " という組み合わせになり、エスケープが打ち消されています。そのため、このリテラルは、perlから見ると、"<img alt=\"対応表\" で終わっています。そのため、リテラルの後に、height という「裸の語」(クオートで囲まれていない文字列)があるようにみて、ここには裸の語ではなく、演算子があるべきではないか?とperlは考えます。
Unrecognized character \x82
「認識されない文字 \x82」
これは、非ASCII文字やその他の文字を「裸の語」にしたときに出るメッセージです。"対応表" のようなリテラルがあって、そのあとに "なんでもいいけど" のようなリテラルがあったとき、前例と同じ理由から起こるものです。

また、q{マッチ} のような場合にも、'マ' の第二バイトが } なので、{ } のカッコはそこで終わってしまい、同様なエラーになります。

マッチしないはずなのにマッチする(1)
"ヤカン" =~ /ポット/ はマッチします。それは、'ポ' の第二バイトが | なので、/ポット//\x83|ット/ とみなされ、\x83 だけマッチすればよいからです。
マッチしないはずなのにマッチする(2)
"兄弟" =~ /Z/ はマッチします。それは、'兄' の第二バイトが 'Z' だからです。第二バイトがアルファベットになる文字には注意が必要です。
マッチするはずなのにマッチしない(1)
"運転免許" =~ /運転/ はマッチしません。それは、'運' の第二バイトが '^'なので、/運転//\x89^転/ とみなされ、文字列の始め ^ の前に \x89 はないからです。
Search pattern not terminated
「サーチパターンが終了しない」
これは、/表/ のように、第二バイトが \ である文字でサーチパターンを終わらせようとしたときに起こります。マッチ演算子の終端文字 / をエスケープしてしまうので、サーチパターンがさらに先に続くように解釈されます。その先にもう一度/はありますか?

あったところで、別のエラーが発生するでしょう。

Substitution replacement not terminated
「置換操作の置換文字列が終了しない」
置換演算子は s/PATTERN/REPLACEMENT/の形式をとらねばなりません。しかし s/表/裏/; のように、第二バイトが \ である文字でPATTERN部分を終わらせようとしたときにこのエラーが起こります。マッチ演算子の終端文字 / をエスケープしてしまうので、PATTERNがさらに先に続くように解釈されます。そのためperlは、PATTERN表/裏 の部分であると考え、3番目の/の先にREPLACEMENT部分があるに違いないと思うのですが、その先にもう一度/はありますか?

あったところで、別のエラーが発生するでしょう。

unmatched [ ] in regexp
「正規表現にマッチしない [ ] がある」
例えば、/プール/ ではエラーが起こります。それは 'ー' の第二バイトが [ なので、/プール//プ\x81[\x83\x8B/ とみなされ、perlは文字クラスがあるのではないかと思います。しかし文字クラスの終了を示す ] が見つからないのでエラーになります。
エラーにはならないけど文字化けする(5)
例えば、lc('アイウエオ')は、'ヂツテトナ'を返します。Shift-JISの2バイト文字の中には、第2バイトがASCIIで英字に当たるものがあります。詳しくはアルファベットの大文字と小文字をご覧下さい。

エラーや間違いを防ぐ対策

以上のようなエラーを防ぐにはさまざまな方法が考えられます。例えば、"表\示" と書けばいいなどという提案があります。もちろんそれでもかまいません。しかしそのためにはどの文字の後に \ を入れればいいかを知る必要があります。それは文字コード表を見れば一発で明らかです。

…などという面倒なことが苦にならない人、文字コード表なんか(少なくとも問題になるような文字くらいは)暗記してしまえばいいというような人にとっては、確かにそれで解決になると思います。しかし、そのような人には、こんなページを見にくる必要もヒマもないでしょう。

そこで、このようなページをわざわざ見にくるような人は、文字コード表をいちいち調べたくないひとだと仮定します。別にそうだからといって非難されることはありません。しかし、手間を惜しむあまり間違ったプログラムを平気で作っていては、顔にクリームパイをぶつけられても仕方ありません。

文字列リテラルの作り方

よく知っておかねばならないことは、\ というエスケープ用の文字は、変数展開やメタ文字の解釈よりずっと前の段階でさまざまな影響を及ぼすということです。そのため、どうしたらデータを確実に変数の中に収められるかを考える必要があります。変数の内部に収めてしまえば、Perlがデータを適切に管理してくれます。よく知られている $str = "表示" の文字化けも、変数$strに代入する以前、ダブルクォートで囲んだ時点ですでに文字化けしていると考えるべきです。すでに文字化けしたデータを代入して、好い結果が得られるはずがありません。

ヒアドキュメントは安全性の高い解決法です。ただし、終端文字列をシングルクォートで囲んでやらなくてはなりません。ダブルクォートで囲んだり、クォートを付けなかったりでは、予期せぬ変数展開やメタ文字の解釈を防ぐことができません。

シングルクォートで終端文字列を囲んだヒアドキュメントでは、変数展開やメタ文字の解釈は何も起こりません。ただ、終端文字列(この場合は "\nEOF\n")を探すことだけを行います。ヒアドキュメントを使うと文字列に改行文字がつきますが、chompで除くといいでしょう。

次の例は $str = 'ここにテキストを書く' と同じように働きますが、文字列の内容によって問題が起こりません。書いたとおりにリテラルを代入できると期待できます。

サンプルコード
chomp($str = << 'EOF');
ここにテキストを書く
EOF

$src = << 'EOF';
  $path = "C:\\path\\file";
  open FH, $path;
EOF

多くの文字列を一度に作りたければ、splitで分割すると容易に作れます。

サンプルコード
($name, $career, $age, $memo) = split "\n", << 'EOF';
田中一郎
プログラマ
三十五
大福が好物である。  酒はあまり呑まない。
EOF

もう少し簡潔に書きたければ、空白文字、\(2バイト文字に含まれているのは構わない)、およびカッコを含まないという条件で、qw() を使うことができます。例えば、@str = qw(表示 対応表 );のように空白を入れてカッコのエスケープを防ぎます。@str = qw(表示 対応表);のように空白を入れないとエラーの元です。

1つの文字列を作る時でも、左辺を丸カッコで囲んでリストコンテキストを示すか、右辺をスライスにするかしなければなりません。これは、(現状では)qw//split(' ', q//) の略記として実装しているからです。なお、Perl 5.6ではリストと等価になっているようです。

サンプルコード
($str) =  qw(百三十 );
 $str  = (qw/百三十 /)[0];
 $str  =  qw/百三十 /; # Perl 5.6

正規表現とマッチ

正規表現のメタ文字は多いので、正規表現の中にShift-JISの文字列を埋めこむのは得策ではありません。例えば、/\Q対応表/ ではエラーになります。これは、/ / の範囲の決定が真っ先に行われ、その時点でエラーが発生するので、\Q の効果を及ぼすことができないからです。また、/\Q対応表\E/ は巧く行きません。これは、対応表\E という文字列を含むものにしかマッチしません。これは、\\ という連続があるため、\E が認識されないからでしょう(たぶん)。

そのため、変数に入れて、マッチ演算子や置換演算子の中で展開させるとよろしいです。このとき日本語文字列は予めquotemeta 関数で処理しておきます。

サンプルコード
$pat = quotemeta +(qw/ 表 /)[0];
$str =~ /$pat\d+/; # 表1, 表2, ..などにマッチ
  # しかし $str = '剣\\1' でもマッチする(この問題は後述)

上のような書き方は確かに醜いですね。クオートの中で \Q \E を使う時は、正しい文字列が入っている変数と一緒になら問題が起こりません。こうすることで、\Q \E の範囲が明確になるからです。正しい文字列の作り方は、前述のリテラルの作りかたを参考にして下さい。

サンプルコード
$pat = "(?:\Q$str1\E|\Q$str2\E)*";
$str =~ /$pat/;

# 実は上の文は次の文と等価。
# $pat = "(?:" . quotemeta($str1) . "|" . quotemeta($str2) . ")*";

リテラルをクォートの中に直接埋めこむとうまく行かないことがあります。それは、perlが \E というメタ文字を発見しようというのを、Shift-JIS文字が妨げるためです。

"\Q表\E"では、表Eにマッチする正規表現になります。表の第二バイトの \ と次の \が合わさるので、perlには\Q \x95 \\ Eの組み合わせであるように思われます。 \Qの作用の結果は\\x95\\x5cEになります。そのため、表Eにマッチします。

"\Q表"はどうでしょう。この場合は、" " の範囲を決めるときに、表の第二バイトが後のクォートをエスケープしてしまうので、文字列の範囲が期待したようには定まらず、エラーになります。このエラーは\Qの効果を考慮する前に発生するので、防ぎようがありません。

"\Q表\\E"はどうでしょう。確かにShift-JISでは問題ありません。しかし同じスクリプトをEUC-JPやUTF-8に変換したときには問題があります。表\Eと余分な2文字がある文字列でないとマッチしません。どちらにしろ、\をどこに添えるかを考える必要があるので、ここの趣旨に合いません。

正規表現は例えば、次のようにします。もちろんこれはShift-JISのみに有効です。

    $digit = '(?:[0-9]|\x82[\x4F-\x58])'; # 数字(半角と全角)
    $upper = '(?:[A-Z]|\x82[\x60-\x79])'; # アルファベット大文字(半角と全角)
    $lower = '(?:[a-z]|\x82[\x81-\x9A])'; # アルファベット小文字(半角と全角)
    $space = '(?:[\ \n\r\t\f]|\x81\x40)'; # 空白文字(半角と全角)
    $ascii = '[\x00-\x7F]';               # ASCII文字

    # 全角平仮名(濁点・半濁点・踊り字を含む)
    $hiraZ = '(?:\x82[\x9F-\xF1]|\x81[\x4A\x4B\x54\x55])'; 

    # 全角片仮名(長音符・濁点・半濁点・踊り字を含む)
    $kataZ = '(?:\x83[\x40-\x7E\x80-\x96]|\x81[\x5B\x4A\x4B\x52\x53])';

    # 半角片仮名(半角長音符・句読点を含む)
    $kataH = '[\xA1-\xDF]';

    $onebyte = '[\x00-\x7F\xA1-\xDF]';
    $twobyte = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; 
    $char    = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

    # JIS文字
    $all_JIS = '(?:[\x00-\x7f\xa1-\xdf]|'.
        . '\x81[\x40-\x7e\x80-\xac\xb8-\xbf\xc8-\xce\xda-\xe8\xf0-\xf7\xfc]|'
        . '\x82[\x4f-\x58\x60-\x79\x81-\x9a\x9f-\xf1]|'
        . '\x83[\x40-\x7e\x80-\x96\x9f-\xb6\xbf-\xd6]|'
        . '\x84[\x40-\x60\x70-\x7e\x80-\x91\x9f-\xbe]|'
        . '\x88[\x9f-\xfc]|\x98[\x40-\x72\x9f-\xfc]|\xea[\x40-\x7e\x80-\xa4]|'
        . '[\x89-\x97\x99-\x9f\xe0-\xe9][\x40-\x7e\x80-\xfc])';

    # ベンダ定義文字

    # NEC特殊文字
    $NEC_special = '(?:\x87[\x40-\x5d\x5f-\x75\x7e\x80-\x9c])';

    # NEC選定IBM拡張文字
    $NEC_IBM_ext = '(?:\xed[\x40-\x7e\x80-\xfc]|\xee[\x40-\x7e\x80-\xec\xef-\xfc])';

    # IBM拡張文字
    $IBM_ext     = '(?:[\xfa-\xfb][\x40-\x7e\x80-\xfc]|\xfc[\x40-\x4b])';

Shift-JISでマッチを行う時には、2つの問題があります。

  1. 第二バイトがASCIIの領域に入る文字があるので、ASCIIを含むパターンにマッチする可能性がある。
  2. ある文字の第二バイトと次の文字の第一バイトが1文字であるかのようにマッチしてしまう。

後者はEUC-JPでも起こりうる問題です(UTF-8なら起こらないが、今はそれが問題なのではない)。しかし前者はEUC-JPでは起こらないが、Shift-JISでは起こりうる問題です。これらを防ぐ方法は、結局同じことですが、正規表現の中に、常に先頭を含ませることです。

サンプルコード
# 先頭からマッチ
$str =~ /^$char*?(?:$pat)/;

末尾からのマッチではうまく行かないことがあります。"右" =~ /E$/を考えれば十分でしょう。また、"\x8E" x 30 . "E"$str = "試試試試試試試試試試試試試試試E"であるが、"\x8E" x 31 . "E"$str = "試試試試試試試試試試試試試試試殺"でありますから、Shift-JIS文字列を後ろから切り分ける適切な方法はないと考えられます。

少なくとも、2バイト文字を構成しないバイト [\x00-\x3F\x7F] が見つかる所まで、極端な場合は文字列の最初までスキャンしないとわからず、しかも後読み lookbehind の正規表現 (?<=PATTERN)は今の所、不定長にできません((?<=(?:\A|[\x00-\x3F\x7F])$char*) とはできない)ので、先頭から文字単位でばらしてから処理するのが、結局は簡便なのかもしれません。

グローバルマッチ

グローバルマッチ /g の場合は、\Gを使いましょう。\Gは前回マッチした部分の末尾を指します。

次の例では、置換されないのが望ましいのですが、\Gがないので、先頭から開始して文字列全体まで延びてマッチしなかったあと、改めて先頭から1バイト進んだ位置からスキャンを再開するので、ずれた位置なのにマッチしたと考えてしまいます。\Gを使わないと、間違った位置にマッチするかもしれないうえに、余計な再検査をするので、時間もかかります。

サンプルコード
    $str = '試試試試E試試試試E';
    $pat = '殺';
    $str =~ s/\G($char*?)($pat)/${1}E/og;
    # '試試試試E試試試試E' のまま(正しい)。

\Gを付けない場合

    $str = '試試試試E試試試試E';
    $pat = '殺';
    $str =~ s/($char*?)($pat)/${1}E/og; # '殺' があれば 'E' に置換
    print  $str;
    # '試試試殺試試試殺' になってしまう(おかしい)。

    試試試試E試試試試E
1回目 →→→→/→→→→/  (マッチしない)
2回目 |→→→⇒|||||||||  (マッチしたので置換)
3回目 |||||||||→→→→/  (マッチしない)
4回目 ||||||||||→→→⇒  (マッチしたので置換)

 凡例: → $charが2バイト文字にマッチ
     / $charが1バイト文字にマッチ
     ⇒ $patがマッチ
     | スキャンの範囲外

ただし、パターンがゼロ文字幅にマッチする場合には、注意が必要です。次の例は、「ア」の前に 'Z' を入れるものです。第1例は文字のずれ('ア' : 0x8341に対して'泣A' : 0x8B8341がずれてマッチする)を防いでいません。第2例は、上の方法で「ずれ」を防ごうとしたのですが、Z への置換が連続して起こっています。

これは第3例のようにする必要があります。これは、第2例では「なぜ?」に書いたように、置換されるからと考えられます。

サンプルコード
$str = "アイウエアアイウア泣A";

print +($temp = $str) =~ s/(?=ア)/Z/g, $temp;

print +($temp = $str) =~ s/\G($char*?)(?=ア)/${1}Z/g, $temp;

print +($temp = $str) =~ s/\G(\A|$char+?)(?=ア)/${1}Z/g, $temp;
結果
5    ZアイウエZアZアイウZア技ア
7    ZアイウエZZアZZアイウZZア泣A
4    ZアイウエZアZアイウZア泣A
なぜ?
     ア   イ   ウ   エ   ア   ア   イ   ウ   ア   泣   A
1  \G Z
2  \G$char$char$char$char Z
3                      \G Z
4                      \G$char Z
5      以下、省略

つまり、グローバルマッチでは、マッチがゼロ文字幅でないパターンの前には\G($char*?)を、ゼロ文字幅であるパターンの前には\G(\A|$char+?)を入れる必要があります。

ただし、これでも正しく(?)マッチさせられない場合があります。

サンプルコード
$str = "0123000123";

print +($temp = $str) =~ s/0*/Z/g, $temp;

print +($temp = $str) =~ s/\G($char*?)0*/${1}Z/g, $temp;

print +($temp = $str) =~ s/\G(\A|$char+?)0*/${1}Z/g, $temp;
__END__
 9    ZZ1Z2Z3ZZ1Z2Z3Z
14    ZZ1ZZ2ZZ3ZZ1ZZ2ZZ3ZZ
 7    Z1Z2Z3Z1Z2Z3Z

これは、パターンがゼロ文字幅にマッチするものであると、/gがその場で無限に足踏みして終了しなくなるのを防ぐため、perlは、マッチを強制的に進めているのですが(cf. perlre, Repeated patterns matching zero-length substring)、この進め方の真似(しかもバイト単位ではなく、文字単位で進むもの)が、\G($char*?)\G(\A|$char+?)では、うまくできないからです。もっともこういうマッチをすることは、ほとんどないと考えられますので、気にする必要はないのかも知れません(<負け惜しみ)。

アルファベットの大文字と小文字

Shift-JISの2バイト文字の中には、第2バイトがASCIIで英字に当たるものがあります。そのため、関数 uc, lc や、メタ文字 \U, \Lが2バイト文字の一部を変換してしまったり(しかし関数 ucfirst, lcfirst や メタ文字 \u, \l は問題とならない)、m//is///iなどの /i修飾子によって違う文字なのにマッチしてしまったりすることがあります。

Shift-JIS文字列に含まれるASCIIの英字を大文字または小文字に揃えたいなら、例えば、次のようなサブルーチンを作れば実現できます。

サンプルコード
$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

lc("PERLプログラミング");      # 'perlプロバラミンバ'
tolower("PERLプログラミング"); # 'perlプログラミング'

sub tolower {
  my $str = $_[0];
  $str =~ s/\G($char*?)([A-Z]+)/$1\L$2/g;
  $str;
}

sub toupper {
  my $str = $_[0];
  $str =~ s/\G($char*?)([a-z]+)/$1\U$2/g;
  $str;
}

ケース無視のマッチ /i の場合は、例えば 'エ'の第二バイトは 'G' であり、'ト'の第二バイトは 'g' であることから、'エ' =~ /ト/iはマッチします。ですから、Shift-JISで正確なマッチをしたければ、/i修飾子は使うことができません。

かわりに、文字列に含まれるアルファベット(二バイト文字の第2バイトにあるものを除く)を小文字(または大文字、どちらか)に統一してマッチさせます。tolowerの定義は上をご覧下さい。

サンプルコード
$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

$lcstr = tolower($str);
$lckey = tolower(quotemeta $key);

if ($lcstr =~ /^$char*?$lckey/) {
    print "matched";
}
else {
    print "not matched";
}

または埋め込み修飾子 (?ismx-ismx) を用いても好い結果を得られます。

サンプルコード
"第1回Perl講縮のご案内" =~ /^$char*?PERL講習/i       # マッチする(困る)
"第2回Perl講縮のご案内" =~ /^$char*?((?i)PERL)講習/  # マッチしない(良し)
"第3回Perl講習のご案内" =~ /^$char*?((?i)PERL)講習/  # マッチする(良し)

全角2バイトアルファベットのケース無視は、選択により実現できます(が、やっぱり変)。'A'の第2バイトが '`' なのも、ちょっと注意です(m`` などのとき致命的エラーになる。ただしバッククォートを使う意味は特にない)。原則的にはリテラルをマッチ演算子や置換演算子に直接埋め込むのは避けたい所です。

サンプルコード
/(?:P|p)(?:E|e)(?:R|r)(?:L|l)/;

その代わりにこんなサブルーチンを作ってみてもよいかもしれません。

サンプルコード
$CharRE = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
 
$pat = make_regexp_ignorecase("PERL講習");
print "第5回Perl講習会" =~ /^$char*?$pat/ ? "OK": "NOT";

sub make_regexp_ignorecase {
  my $str = $_[0];
  $str =~ s/\G([A-Za-z]+|$CharRE)/
    my $c = ord $1;
    if($c == 0x82) {
      my $v = vec($1,1,8);
      0x81 <= $v && $v <= 0x9A ? sprintf('\\x82[\\x%2x\\x%2x]', $v, $v-33) :
      0x60 <= $v && $v <= 0x79 ? sprintf('\\x82[\\x%2x\\x%2x]', $v, $v+33) :
      quotemeta($1);
    } 
    elsif(0x41 <= $c && $c <= 0x5A || 0x61 <= $c && $c <= 0x7A) {"(?:(?i)$1)"}
    else {quotemeta($1)}
  /geo;
  $str;
}

長い文字列に対する正規表現検索

正規表現は、Perl にとって欠かせない存在といえます。しかし正規表現の制限として、*, +, {min,max} などの量指定子がマッチを繰り返せる回数の上限という問題があります。(詳細は perlre 参照のこと)。そのため、$char*? という正規表現には、危険性があります。

例えば、次のようなマッチングを考えて見ましょう。$strは、「あ」が10万字連続したあとに、「アイABC」が連結された文字列です。このような文字列(ただし、「『あ』が10万字連続」ということは分からず、任意の Shift-JIS テキストであろうということにします)から、半角アルファベットが連続した部分を見つけたいとしましょう。すると、今までの考え方からすると、次のようにすればよいと思われます。

サンプルコード
my $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
my $str = ('あ' x 100000) . 'アイABC';
$str =~ /^$char*?([A-Z]+)/o;
print $1;

しかし、これは、環境によっては、大きなエラーを引き起こします。例えば、Windows 98上で Active Perl 522 を用いた場合、Error: Runtime exception という Perl のエラーになりました。また、Windows 98上でVC++ 6.0でコンパイルされた Perl 5.6.1 だと、「このプログラムは不正な処理を行ったので強制終了されます。〜」などといったエラーになりました。

このような問題をできるだけ防ぐためには、次のようにします。つまり、文字列の先頭から調べていく場合、二バイト文字の文字境界を間違えるのは、二バイト文字の第一バイトの直後を文字境界と誤認識した時だけです。Shift-JISでは、二バイト文字の第一バイトは、[\x81-\x9F\xE0-\xFC] だけです。あるいは、EUC-JP に変換可能な領域だけを考慮すれば、[\x81-\x9F\xE0-\xEF] だけということができます。それ以外のバイトの直後は、例えば、0x41 の直後は、'A' の直後か、'ア' の直後かは分かりませんが、確かに文字境界になります。従って、[\x81-\x9F\xE0-\xFC]+ (または [\x81-\x9F\xE0-\xEF]+ )のバイト(二バイト文字)が連続するところだけに注意すればよいことになります。

このため、以下のように、^$char*? の代わりに $Apad を使い、\G$char*? の代わりに $Gpad を用いれば、一バイト文字か、二バイト文字のうち第二バイトが [\x40-\x7E\x80\xA0-\xDF] で終わるものが、少なくとも適当な間隔で(上限に達しないうちに)出現すれば、エラーにならずに処理することができます。(確率的な問題ですので、完全ではありません。)

サンプルコード
# 一回だけマッチ
my $Apad  = '(?:(?:\A|[\x00-\x80\xA0-\xDF])(?:[\x81-\x9F\xE0-\xFC]{2})*?)';
my $str1 = ('あ' x 100000) . 'アイABC';
$str1 =~ /$Apad([A-Z]+)/o;
print "$1\n"; # "ABC" と表示される。

# グローバルマッチ
my $Gpad  = '(?:(?:\G|[\x00-\x80\xA0-\xDF])(?:[\x81-\x9F\xE0-\xFC]{2})*?)';

my $str2 = 'あ' x 100000 . 'アイABC'. 'お' x 100000 . 'XYZ';
my @array = $str2 =~ /$Gpad([A-Z]+)/go;
print "@array\n"; # "ABC XYZ" と表示される。

外字の変換

ベンダ定義文字やユーザ定義文字を含む文字列を他の環境でも利用できるようにするには、適切な変換が必要です(無論、まったく同じ字体の利用はほとんど望めず、おそらくは類似した文字や文字列に変換することになるでしょう)。これはPerlでは置換演算子 s/// を使えば比較的容易に実現できます。

あらかじめ、どの外字をどう変換するかを定義する変換テーブルを用意しなくてはなりません。これはPerlではハッシュにしておくとその後の処理が楽になります。ここでは、'w932_gai.txt'で定義する、Windows codepage-932コードに基づいた機種依存文字の(部分的)変換テーブルを使うことにします。

次のコードでは、1文字づつマッチさせ、その文字が変換ハッシュのキーにあれば対応する値の文字列に置換し、そうでなければそのまま残します。

サンプルコード
require 'w932_gai.txt'; # %tableの定義(不完全!)

$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

$str =~ s/($char)/exists $table{$1} ? $table{$1} : $1/geo;

同様な処理は、つぎのような書き方でもできますが、外字にマッチする正規表現 $gaijiを用意する必要があります。ずれたマッチをしないために、こちらの正規表現には \G が必要です。例えば、$str = '∞@';の後ろ2バイトは "\x87\x40" ですが、こうすればマッチがずれる心配がありません。また、非欲張りマッチ ($char*?)を使えば $char が外字にマッチしないよう変更する必要はありません。

サンプルコード
require 'w932_gai.txt'; # %tableの定義(不完全!)

$char  = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
$gaiji = '(?:[\x87][\x40-\x9c])';

$str =~ s/\G($char*?)($gaiji)/$1$table{$2}/g;

CP932重複定義文字の変換

Microsoft Windows 日本語版で一般的に使用されているコードページ 932 (CP932) では、幾つかの文字が重複して定義された状態になっています。ここで、文字が重複定義されているとは、Unicodeの同じ符号位置に対応付けられていることとします。

例えば、CP932 -> Unicode -> CP932 の順で変換されると、重複定義文字は、どれか一つに揃えられます。この優先順位は JIS X 0208, NEC特殊文字 (13区)、IBM拡張文字 (115〜119区)、NEC選定IBM拡張文字 (89〜92区) の順です。一例として、'∵' の場合、NEC特殊文字の "\x87\x9A" や IBM拡張文字の "\xFA\x5B" は、JIS X 0208 の "\x81\xE6" になります。

しかし、CP-932 のテキスト中、重複定義文字がどれかに揃っていないことがあります。例えば "\x87\x9A""\xFA\x5B" が含まれていると、テキストを目で見ると違いがないのに、"\x81\xE6" で検索しても見つけられないことになります。

重複定義文字を揃えるモジュールとして、ShiftJIS/CP932/Correct.pm があります。入手と使い方はPerlのページに戻れば見つかります。

また、ShiftJIS/String.pmstrtr() または trclosure() を使う方法もあります。入手と使い方はPerlのページに戻れば見つかります。

サンプルコード

# (1) $necJIS -> $jisNEC (9対)
   $necJIS = "\x87\x90\x87\x91\x87\x92\x87\x95\x87\x96\x87\x97\x87\x9A\x87\x9B\x87\x9C";
      # NEC特殊文字のうち、JIS文字に変換されるべき非漢字
   $jisNEC = "\x81\xE0\x81\xDF\x81\xE7\x81\xE3\x81\xDB\x81\xDA\x81\xE6\x81\xBF\x81\xBE";
      # JIS文字のうち、NEC特殊文字に重複定義されている非漢字

# (2) $necibmJIS -> $jisNECIBM (1対)
   $necibmJIS = "\xEE\xF9";
      # NEC選定IBM拡張文字のうち、JIS文字に変換されるべき非漢字
   $jisNECIBM = "\x81\xCA";
      # JIS文字のうち、NEC選定IBM拡張文字に重複定義されている非漢字

# (3) $ibmJIS -> $jisIBM (2対)
   $ibmJIS = "\xFA\x54\xFA\x5B";
      # IBM拡張文字のうち、JIS文字に変換されるべき非漢字
   $jisIBM = "\x81\xCA\x81\xE6";
      # JIS文字のうち、IBM拡張文字に重複定義されている非漢字

# (4) $ibmNEC -> $necIBM (13対)
   $ibmNEC = "\xFA\x4A-\xFA\x53\xFA\x58\xFA\x59\xFA\x5A";
      # IBM拡張文字のうち、NEC特殊文字に変換されるべき非漢字
   $necIBM = "\x87\x54-\x87\x5D\x87\x8A\x87\x82\x87\x84";
      # NEC特殊文字のうち、IBM拡張文字に重複定義されている非漢字

# (5) $necibmIBM -> $ibmNECIBM (13対)
   $necibmIBM = "\xEE\xEF-\xEE\xF8\xEE\xFA\xEE\xFB\xEE\xFC";
      # NEC選定IBM拡張文字のうち、IBM拡張文字に変換されるべき非漢字
   $ibmNECIBM = "\xFA\x40-\xFA\x49\xFA\x55\xFA\x56\xFA\x57";
      # IBM拡張文字のうち、NEC選定IBM拡張文字に重複定義されている非漢字

# (6) $necibmCJK -> $ibmCJK (360対)
   $necibmCJK = "\xED\x40-\xEE\xEC";
      # NEC選定IBM拡張文字中の漢字
   $ibmCJK    = "\xFA\x5C-\xFC\x4B";
      # IBM拡張文字中の漢字

use ShiftJIS::String qw(trclosure);

# 変換用クロージャの生成
$correctCP932 = trclosure(
    $necJIS.$necibmJIS.$ibmJIS.$ibmNEC.$necibmIBM.$necibmCJK, # from
    $jisNEC.$jisNECIBM.$jisIBM.$necIBM.$ibmNECIBM.$ibmCJK     # to
);

$result = $correctCP932->($source); # $source を変換して $result を得る

文字数を数える

Shift-JIS文字列の文字数を数えるには、マッチ演算子を利用するならスカラーコンテキストで数えた方が若干速かったです。それより、置換演算子を利用したほうが速く書けるとわかりました。

もっともXSで書いたほうがずっと速かったです。まあ、XSUBは無理に利用しなくてもよいでしょう。

サンプルコード
use Benchmark;

$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
$s = "漢字あ\0AアCテスト -";

timethese (100000, {
  le => q{
    ($str = $s) =~ s/$char/0/go;
    $le = length $str;
  },
  sg => q{
    $sg = ($str = $s) =~ s/$char//go;
  },
  ab => q{
    $ab = 0;
    $ab++ while $s =~ /[^\x81-\x9F\xE0-\xFC]|../g;
  },
  ar => q{
    $ar = @{[ $s =~ /$char/go ]};
  },
  gr => q{
    $gr = grep defined, $s =~ /$char/go;
  },
  wh => q{
    $wh = 0;
    $wh++ while $s =~ /$char/go;
  },
  sj => q{
    $sj = sjslen($s);
  },
  xs => q{
    $xs = sjlength($s);
  },
});

sub sjslen {
  my($str,$len,$i,$c,$blen);
  $str = shift;
  $blen = length $str;
  while ($i < $blen) {
    $c = vec($str, $i, 8);
    if (0x81 <= $c && $c <= 0x9F || 0xE0 <= $c && $c <= 0xFC){ $i++ }
    $i++,$len++;
  }
  $len;
}
結果
Benchmark: timing 100000 iterations of ab, ar, gr, le, sg, sj, wh, xs...
    ab:  4 wallclock secs ( 3.46 usr +  0.00 sys =  3.46 CPU) @ 28901.73/s
    ar:  6 wallclock secs ( 5.98 usr +  0.00 sys =  5.98 CPU) @ 16722.41/s
    gr:  6 wallclock secs ( 5.50 usr +  0.00 sys =  5.50 CPU) @ 18181.82/s
    le:  3 wallclock secs ( 2.09 usr +  0.00 sys =  2.09 CPU) @ 47846.89/s
    sg:  2 wallclock secs ( 1.92 usr +  0.00 sys =  1.92 CPU) @ 52083.33/s
    sj:  9 wallclock secs ( 8.57 usr +  0.00 sys =  8.57 CPU) @ 11668.61/s
    wh:  5 wallclock secs ( 4.78 usr +  0.00 sys =  4.78 CPU) @ 20920.50/s
    xs:  1 wallclock secs ( 0.38 usr +  0.00 sys =  0.38 CPU) @ 263157.89/s
        (warning: too few iterations for a reliable count)
XSUB
int
sjlength(arg)
    SV* arg
  PROTOTYPE: $
  PREINIT:
    unsigned char *str, *p, *e;
    STRLEN byte, len = 0;
  CODE:
    p = str = (unsigned char *)SvPV(arg, byte);
    e = str + byte;
    while (p < e) {
        if (0x81 <= *p && *p <= 0x9F || 0xE0 <= *p && *p <= 0xFC)
            ++p;
        ++p, ++len;
    }
    RETVAL = len;
  OUTPUT:
    RETVAL

文字単位に分ける

Shift-JIS文字列を文字単位に分割しましょう。この場合は、XSを利用してもあまり速くなりませんでした。返り値のリストを用意するのに時間が取られるのか、やはりPerlの正規表現の処理はかなり速いものだということでしょう。

サンプルコード
use Benchmark;

$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
$s = "日本語ニホンゴ\0ABC" x 100;

timethese (1000, {
   re => q{
      @re = $s =~ /$char/go;
   },
   xs => q{
      @xs = sjsplit($s);
   },
});
結果
Benchmark: timing 1000 iterations of re, xs...
    re:  7 wallclock secs ( 6.65 usr +  0.00 sys =  6.65 CPU) @ 150.38/s
    xs:  6 wallclock secs ( 5.33 usr +  0.00 sys =  5.33 CPU) @ 187.62/s
XSUB
void
sjsplit(arg)
    SV* arg
  PROTOTYPE: $
  PREINIT:
    unsigned char *str, *p, *e;
    STRLEN ch, byte, len = 0;
  PPCODE:
    str = (unsigned char *)SvPV(arg,byte);
    e = str + byte;
    for (p = str; p < e; p++) {
        if (0x81 <= *p && *p <= 0x9F || 0xE0 <= *p && *p <= 0xFC) ++p;
        ++len;
    }
    EXTEND(SP,len);
    for (p = str; p < e; p += ch) {
        ch = (0x81 <= *p && *p <= 0x9F || 0xE0 < *p && *p <= 0xFC) ? 2 : 1;
        PUSHs(sv_2mortal(newSVpv(p,ch)));
    }

色々な分割

文字で分割でみたように、文字列を分割するには、m//gが便利です。

サンプルコード
$onebyte = '[\x00-\x7F\xA1-\xDF]';
$twobyte = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; 
$char    = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

#1バイト文字の塊と2バイト文字の塊に分ける。
   while ($str =~ /\G($onebyte*)($twobyte*)/g) {
      push @one, $1 if $1 ne '';
      push @two, $2 if $2 ne '';
   }

#句点が最後の文字となるように分割する。
# '。' ではいいが、文字によっては注意が必要。
   @sentences = $str =~ /\G$char*?(?:。|.|$)/g; 

特定の長さで切りそろえる

長い文字列を特定の長さ(バイト長)で切りそろえるなら、次のようにしてできます。

サンプルコード
$char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

$str = 'わざわざEUC-JPに変換しないで、Shift-JISのまま処理'.
       'できたらいいんだけど、なかなか面倒だねえ。';

print join "\n", bytebreak($str,15);

sub bytebreak{
   my($byte,$bmax,$ch,@lines);
   my $str = shift;
   $byte = $bmax = shift;
   foreach $ch ($str =~ /$char/go) {
      $byte += length $ch;  # 次の文字を継ぎ足した長さ
      if ($byte <= $bmax) {
         $lines[-1] .= $ch; # 長すぎなければ継ぎ足す
      } else {
         $byte = length $ch;
         push @lines, $ch;  # さもなければ次の行へ
      }
   }
   return @lines;
  # 長さが足らない場合に、右をスペースで埋めたければ。
  # return map {$_ .= ' ' x ($bmax - length)} @lines;
}

禁則処理は、例えば次のようにして行います。単純な考え方では、禁則処理は、(i) 行頭禁則文字の直前で改行しない;(ii) 行末禁則文字の直後で改行しない;ということになります。また、"(a)"のように、行末禁則文字と行頭禁則文字の間に1文字しかない連続した部分は、その部分の全体が無改行になる点にも配慮します。

この例では文字列の長さをバイト長 length で規定していますが、文字幅とバイト数は必ずしも比例しませんので、場合によっては(ギリシア文字は半角幅にしたいとか、またはプロポーショナルの場合とか、UTF-8の場合とか)文字幅を返す width のような関数を定義する必要があるでしょう。

また、この例のやり方では、禁則による無改行部分だけで一行より長くなる場合は、はみだしを防げません。それが困るなら、禁則の例外として行を分ける(例えば$nextの長さが$bmaxを超えないようにする)処置が必要でしょう。

サンプルコード
$CharRE = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';

# 行頭禁則文字(一部分)
$NotAtBegin = q/)]}’”」』)]}!,.:;?、。々゛゜!,.:;?/;
# 行末禁則文字(一部分)
$NotAtEnd   = q/([{‘“「『([{/;

# ハッシュを作る
@NotAtBegin{$NotAtBegin =~ m/$CharRE/g} = ();
@NotAtEnd{  $NotAtEnd   =~ m/$CharRE/g} = ();

$Str = 'わざわざEUC-JPに変換しないで、Shift-JISのまま処理'.
       'できたらいいんだけど、なかなか面倒だねえ。';

print join "\n", linebreak($Str,16);

sub linebreak{
   my($byte,$i,@chars,$next,@lines);
   my($str, $bmax, $pad) = @_;

   # $byteは次の文字を継ぎ足したときの長さ
   $byte = $bmax; # すぐ改行できるための初期値。

   # 文字単位にばらす
   @chars = $str =~ /$CharRE/go; 

   for ($i=0; $i<@chars; $i++) {
      $next .= $chars[$i];         # 次の文字
      $byte += length $chars[$i];  # 次の文字を継ぎ足した長さ

      # 次の文字が行末禁則のとき
      next if $i+1 < @chars && exists $NotAtEnd{ $chars[$i] };
      # 次の文字の次が行頭禁則のとき
      next if $i+1 < @chars && exists $NotAtBegin{ $chars[$i+1] };

      # 行の振り分け
     # 長すぎなければ継ぎ足す
      if ($byte <= $bmax) {
         $lines[-1] .= $next;
      }
     # さもなければ次の行へ
      else {
         push @lines, $next;
         $byte = length $next;# 新しい行の長さ
      }
      $next = '';
   }
   return defined $pad && 1 == length $pad # 詰め物
    ? map {$_ .= $pad x ($bmax - length)} @lines
    : @lines;
}
ぶら下がり禁則の場合($bmin から $bmaxの範囲を許す)。
      $bmin = $bmax - 2; # 例えば。

      # 行の振り分け
     # 長すぎなければ継ぎ足す
      if ($byte <= $bmax && @lines && length $lines[-1] < $bmin){
         $lines[-1] .= $next;
      }
     # さもなければ次の行へ
      else {
         push @lines, $next;
         $byte = length $next;# 新しい行の長さ
      }

日本語文字列を並び替える

仮名文字列を五十音順にソートするモジュールとして、ShiftJIS/Collate.pm があります。入手と使い方はPerlのページに戻れば見つかります。

「読み・表記照合」は次のようにして行います。sortYomiメソッドの受け取るリストの各要素は、[ 表記列, 読み列 ]という配列リファレンスでなければなりません。

サンプルコード
use ShiftJIS::Collate;

my @data = (
  [qw/ 小山 こやま /],
  [qw/ 長田 ながた /],
  [qw/ 田中 たなか /],
  [qw/ 鈴木 すずき /],
  [qw/ 小嶋 こじま /],
  [qw/ 児島 こじま /],
  [qw/ 小山 おやま /],
  [qw/ 小島 こじま /],
  [qw/ 小島 こじま /],
  [qw/ 山田 やまだ /],
  [qw/ 永田 ながた /],
);

@sort = ShiftJIS::Collate->new()->sortYomi(@data);

「簡易代表読み照合」は次のようにして行います。sortDaihyoメソッドの受け取るリストの各要素は、[ 表記列, 読み列 ]という配列リファレンスでなければなりません。

サンプルコード

#!perl
use ShiftJIS::Collate;

my @data = (
  [qw/ λ計算   らむだけいさん /],
  [qw/ JIS番号  じすばんごう   /],
  [qw/ 安達     あだち         /],
  [qw/ 安藤     あんどう       /],
  [qw/ 河西     かさい         /],
  [qw/ 河内     かわち         /],
  [qw/ 角田     かくた         /],
  [qw/ 角田     かどた         /],
  [qw/ 如月     きさらぎ       /],
  [qw/ 河内     こうち         /],
  [qw/ 幸山     こうやま       /],
  [qw/ 幸山     さきやま       /],
  [qw/ 佐藤     さとう         /],
  [qw/ 佐和田   さわだ         /],
  [qw/ 沢島     さわしま       /],
  [qw/ 沢田     さわだ         /],
  [qw/ 澤田     さわだ         /],
  [qw/ 角田     つのだ         /],
  [qw/ 槌井     つちい         /],
  [qw/ 土井     つちい         /],
  [qw/ 土居     つちい         /],
  [qw/ 戸井     とい           /],
  [qw/ 戸田     とだ           /],
  [qw/ 土井     どい           /],
  [qw/ 土居     どい           /],
  [qw/ 土岐     とき           /],
  [qw/ 安田     やすだ         /],
);

@sort = ShiftJIS::Collate->new()->sortDaihyo(@data);

Shift-JISの漢字を含むファイル名/パス名

本項目は、他の項目に増して、検討不充分のまま記述していますので、もし何か参考にしようと思った場合、十分に注意の上、納得できるまでご自分の作業環境でテストしてください。

Windows (95/98/NT/2000など) で、ファイル名やパス名が漢字(ここでは二バイト文字の意味で使っていますので、平仮名や記号なども含みます。)を含む場合、Perlで扱う際に問題が生じる可能性があります。

末尾バイトが "\x5C" の漢字をもつファイル名/パス名

ディレクトリ操作関数(mkdir, rmdir, opendir, -d など)、ファイル操作関数(open, unlink, -f など)で、アクセスできないことがあります。

ファイルの場合は、末尾に半角スペースを添えるとアクセスできる場合があります(例えば、-f '表 ' または -f "\x95\x5C\x20" など)。

ディレクトリの場合は、末尾に /\ を添えるとアクセスできる場合があります(例えば、-d '表/' または -d "\x95\x5C/" など)。末尾に添える文字を半角スペースとしても、うまくアクセスできる場合があります。添える文字の候補として、三種類の文字(スラッシュ、円記号、空白)を挙げましたが、どの文字がよいかは、関数によって異なる場合があるようです。使用する前に十分にテストしてください。

※[2006-10-28] 追記: また、Change 27200が当たっているperlの場合、パス名の末尾にスラッシュや円記号を追加しても、 (第二バイトに該当するものを含めて) 全部削除されてしまう (参考: [PATCH] stat() on Windows doesn't handle trailing slashes/backslashes correctly) ため、これは利きません。別の方法としては、-d '表/.' のように、カレントディレクトリを表すピリオドも追加するといいかもしれません。

なお、ディレクトリ名の末尾に /\ を添える場合、もともと末尾に /\ が付いている場合には、二重に付けるとうまく行かないおそれがありますので、文字列連結の前に検査したほうがよいでしょう。

どうしても挙動が不明で信頼できない場合は、`` または qx//system()関数などを通じてWindowsのコマンドを呼ぶのが良いと思います。


おまけ

Shift-JIS で書かれた POD を Perl 5.8.1, 5.8.2 の Pod::Html で HTML に変換した場合、アンカーの名前は、 英数字と仮名文字 [0xA6..0xDF] を除き、 他の各バイトは下線('_')に変換されるようです。 具体的には、use locale; 下で、lcs/\W/_/g を実行した結果 (cf. Pod::Html::anchorify) になります。
[2003-11-18]


Perlのページ