|
1 Shift-JISテキストを正しく扱う |
|
2 最近の更新履歴 |
|
3 2005-03-26: 「最初に」中、XML日本語プロファイル第2版に基づき、若干追記。 |
|
4 2005-03-09: 「最初に」中、文章を若干修正。 |
|
5 2003-06-24: Shift-JISの漢字を含むファイル名/パス名 |
|
6 2003-05-31: 「最初に」中、「シフトJIS」などの表記について。 |
|
7 2003-05-24: CP932重複定義文字の変換 |
|
8 2002-08-30: Perl 5.8.0 について。 |
|
9 2002-01-17: 長い文字列に対する正規表現検索 |
|
10 2001-12-15: ShiftJIS::Collate が overrideCJK パラメータを廃止したことに伴う 日本語文字列を並び替えるの書き換え。 |
|
11 最初に |
|
12 ありがちなエラー |
|
13 エラーや間違いを防ぐ対策 |
|
14 文字列リテラルの作り方 |
|
15 正規表現とマッチ |
|
16 グローバルマッチ |
|
17 アルファベットの大文字と小文字 |
|
18 長い文字列に対する正規表現検索 |
|
19 外字の変換 |
|
20 CP932重複定義文字の変換 |
|
21 文字数を数える |
|
22 文字単位に分割する |
|
23 いろいろな分割 |
|
24 特定の長さで切りそろえる |
|
25 日本語文字列を並び替える |
|
26 Shift-JISの漢字を含むファイル名/パス名 |
|
27 最初に |
|
28 日本語の文字コードにはいくつかのものが使われています。ある程度一般的なものなら、どれを使ってもよいでしょう(どの文字コードで符号化されているかの情報が失われさえしなければ)。 |
|
29 例えば、日本語版Windowsでは、メモ帳でもDOS窓でもShift-JISが使われています。こういう場合、処理の途中でわざわざEUC-JPやUTF-8に変換するとしたら面倒です。デバッグのとき、「この段階ではこの変数には何が入っているのか」出力して点検するのはよく行われますが、このときEUC-JPとして収められていたら、作業は手間どるでしょう。入力も出力もShift-JISで行うつもりなら、処理の全体でShift-JISのまま扱えたらきっと便利でしょう。 |
|
30 注: "シフトJIS", "Shift_JIS", "Shift-JIS" などの表記の違いについてはよくわかっていません。今のところ分かっていることは: |
|
31 JIS X 0208:1997の附属書1(シフト符号化表現)には、「参考」として「この符号化表現は通常“シフトJISコード”と呼ばれている」の記述があります。 |
|
32 IANA の CHARACTER SETS には、Shift_JIS と Windows-31J とが別に登録されています。また、Shift_JISについて、「CCS(符号化文字集合)はJIS X0201:1997とJIS X0208:1997であり、完全な定義はJIS X 0208:1997の附属書1に示されている。」と記しています。 |
|
33 W3C の XML Japanese Profile には、Shift-JISにUnicodeへの変換表が複数ある旨の記載があります。XML Japanese Profile (Second Edition)では、Unicode Consortiumで公開されているMicrosoft CP932の変換表によるcharsetの名称 "x-sjis-cp932" を "Windows-31J" に変更しています。 |
|
34 Microsoft社の Global Dev では、Codepage 932 を "Japanese Shift-JIS" と注記しています。 |
|
35 しかし、Shift-JISにはある種の癖があって、ちょっとしたことがバグやエラーや文字化けの原因となります。なんとかならないものでしょうか。 |
|
36 Perlは制御文字やナル文字を含むバイナリデータですら正しく処理できるように設計されているので、スクリプトやテキストをShift-JISで書いたくらいで問題になることはありません。 |
|
37 しかし、perlがスクリプトを解釈するときは(通常)バイト単位で調べるので、Shift-JISのようなマルチバイト文字を含む符号はそのままでは直接理解できません。 |
|
38 たとえば、Shift-JISで 'あ' という文字は、16進数で82 A0という2バイトで表されます。これを "\x82\xA0" と書いてもperlにとっては同じです。これが日本の(country)日本語の(language)文字であるとか、Shift-JISで書かれている(charset)とかいう情報はどこにも含まれていません。 |
|
39 そのため、Shift-JISで書きたいときには、perlの誤解を受けないように書いてやらなければなりません。その配慮は、プログラマがしてやらなければなりません。この文書の記述は、そのような手間をかけても、Shift-JISを用いることに意義があると考えている人には参考になるかもしれません。 |
|
40 そんな手間を掛けたくない人は、 |
|
41 Perl 5.8.x以降を使う。 |
|
42 利点:perl5-porters@perl.org でサポートされている。 |
|
43 欠点:独特の考え方があり、従来の日本語処理とは相容れないところがある(もっとも、そのうち慣れて気にならなくなるかもしれない)。 |
|
44 jperlを使う。 |
|
45 利点:Shift-JIS を文字として直接扱うことができる。 |
|
46 欠点:現在、維持する人がいない。 |
|
47 文字コードをUTF-8かEUC-JPに変換してから処理する。 |
|
48 利点:Perl 5.8.x以降でなくても動作する変換用のモジュール(.pm)やライブラリ(.pl)がいろいろ入手可能。 |
|
49 欠点:Shift-JISほど悪くないにしても、マルチバイト文字をシングルバイト文字と区別せず、ともに一文字として処理するのは面倒である。 |
|
50 という対処をとったほうがよいでしょう。これらのプログラムは有名なので、探せばすぐ見つかるでしょうから、入手先はここには示しません。 |
|
51 なお、この文書に書かれている事が、最も勧められない方法なので、ここから先は、そのつもりでお読み下さい。この方法について何か疑問が生じたとしても、それについて他のところで質問すると、何でそんなやり方をしているのかと、きっと非難されるでしょう。かといって、私にも訊かないで下さい。 |
|
52 Shift-JISを使ったときにありがちな(?)エラー |
|
53 Shift-JISには、第2バイトが [@-~](ASCII 10進数で64-126)の範囲に入るものがあります。これらのASCII文字は、perlにとって特別な意味をもつことがあるため、しばしばエラーの原因となります。Shift-JISでは、2バイト文字の第2バイトは、[\x40-\x7E\x80-\xFC])の範囲にあるため、実に188分の63、約3分の1の文字が何らかの問題を起こし得るといえます。 |
|
54 次に、Shift-JISを使ったときに起こりがちなエラーとその原因を示します。エラーメッセージはperlの違い(バージョンやどのプラットフォーム用のものであるか等)により多少の違いがあるかもしれません。 |
|
55 エラーにならなくても、文字化けしたり、期待したような動作をしなかったりで、うまくいかないことがあります。この場合、エラーが出ない分、原因を自分で探さなければならなくなるためバグ取りはしばしば困難です。 |
|
56 ここではエラーに対する対策は提示しません。対策はあとでまとめて書きます。 |
|
57 なお、ここには文字コードをEUC-JPにしても起こるような問題やエラーは示しません。基本的に、EUC-JPなら起きないが、Shift-JISのときには起こるような事柄に限ります。 |
|
58 エラーにはならないけど文字化けする(1) |
|
59 例えば、"表示" とか "暴力" とかいうリテラルが文字化けを起こします。これらは "侮ヲ" とか "沫ヘ" になります。これは、"表" や "暴"の文字の第2バイトが \ であるため、ダブルクオート文字の中では次の文字のエスケープをすることになるので、表示 = 0x955C8EA6 であっても、クオートの結果は "表示" = 0x958EA6 となるからです。'表示' とすれば文字化けは起こりませんが、シングルクオートでも防げない文字化けやエラーがあります(次例)。 |
|
60 エラーにはならないけど文字化けする(2) |
|
61 例えば、"ミソ\500" というリテラルでは、\ が脱落してしまいます。これは、'ミソ\500' や q(ミソ\500) などとしても防ぐことができません。それは \\ という連続があると \ 1個になってしまうという規則があるからです。 |
|
62 クオートやクオート風演算子の中では、文字列にクオートと同じ文字を含められるように、\ によるエスケープを付ければクオートの終端文字ではなく、文字列の一部とみなします。そのため、\\ が \ の文字を表すエスケープになります。これはクオートの始端・終端文字を何にしても同じことです。 |
|
63 エラーにはならないけど文字化けする(3) |
|
64 例えば、"丸十net" というリテラルが文字化けを起こします。これは "丸・ |
|
65 et" のように途中で改行されてしまいます。これは、"十" の第2バイトが \ であるため、ダブルクオート文字の中では次の 'n' と合わせて\nのすなわち改行文字を表すメタ文字として解釈されるからです。 |
|
66 エラーにはならないけど文字化けする(4) |
|
67 例えば、"引数 ARGV" というリテラルが文字化けを起こします。これは、" "(全角スペース)の第2バイトが @ であるため、ダブルクオート文字の中では次の ARGV と合わせて "@ARGV" という配列として変数展開を行うからです。@ARGVのように必ず定義されるような配列なら、展開されますが、別の場合ではエラーになるかもしれません(それは次項を参照)。 |
|
68 In string, @dog now must be written as \@dog (Perl 5.6.0まで) |
|
69 「文字列の中では、@dogは今は\@dogと書かなければならない」 |
|
70 前例でみたように、全角スペース " "の第2バイトは @ であるため、後ろの文字と合わせて配列であるかのように解釈しようとします。"犬 dog" のような場合、@dog という配列が定義されていればそれを用いて変数展開しますが、定義されていない場合、エラーメッセージを出します。 |
|
71 ``now must be written as''「今はこう書かなければならない」とは、Perl4までは配列の変数展開は行わなかったため、"hoge@foo.bar" のような書き方をすることができたのだが、今 Perlでは @foo が展開されてしまうので、注意を喚起するためエラーを出すようにしているようです(もしPerlが昔から配列の展開をサポートしていたら、エラーを出すことなく、黙って展開するだけだったかもしれません。次項も参照)。 |
|
72 "犬 \dog" とすればいいという意見もありますが、\d がメタ文字として特別意味がないためにうまくいくのであって(Perl 5.6以降では、警告 Unrecognized escape \d passed through 「認識できないエスケープ \d が渡された」を引き起こします)、例えば "花 \flower" のときは、\f が改ページ文字として解釈され、文字化けします。 |
|
73 Possible unintended interpolation of @dog in string (Perl 5.6.1以降) |
|
74 文字列の中で、@dogが予期せずに展開される |
|
75 前項と同じく、"犬 dog" ですが、Perl 5.6.1(ActivePerl 626)以降では、定義されていない配列でも黙って展開します。配列 @dog が展開されるので、"犬\x81" と同じになります。 |
|
76 これはエラーではなく、警告になります。 |
|
77 Can't find string terminator '"' anywhere before EOF |
|
78 「終端文字 '"'がファイルの終り EOF までに見つからなかった」 |
|
79 例えば、"対応表" のようなリテラルでは、'表' の第2バイトが \ であるため、うしろの " をエスケープしてしまいます。このためperlは、その " はクオート文字列の終端文字とはみなさずに、文字列がさらに続くと考えてしまいます。これ以降、スクリプトの中に " の文字が全く含まれなければ、このようにエラー報告をします。 |
|
80 qq{ "日本語" }のような場合にも注意しなければなりません。"本" の第二バイトは { なので、このままでは { }のネストがずれてしまい、同様のエラーが発生します。 |
|
81 Bareword found where operator expected |
|
82 「裸の語が演算子があってほしい位置に見つかった」 |
|
83 例えば、print "<img alt=\"対応表\" height=115 width=150>"; のような場合、\" による引用符のエスケープは、表 の第2バイトの\のため、\\ " という組み合わせになり、エスケープが打ち消されています。そのため、このリテラルは、perlから見ると、"<img alt=\"対応表\" で終わっています。そのため、リテラルの後に、height という「裸の語」(クオートで囲まれていない文字列)があるようにみて、ここには裸の語ではなく、演算子があるべきではないか?とperlは考えます。 |
|
84 Unrecognized character \x82 |
|
85 「認識されない文字 \x82」 |
|
86 これは、非ASCII文字やその他の文字を「裸の語」にしたときに出るメッセージです。"対応表" のようなリテラルがあって、そのあとに "なんでもいいけど" のようなリテラルがあったとき、前例と同じ理由から起こるものです。 |
|
87 また、q{マッチ} のような場合にも、'マ' の第二バイトが } なので、{ } のカッコはそこで終わってしまい、同様なエラーになります。 |
|
88 マッチしないはずなのにマッチする(1) |
|
89 "ヤカン" =~ /ポット/ はマッチします。それは、'ポ' の第二バイトが | なので、/ポット/ は /\x83|ット/ とみなされ、\x83 だけマッチすればよいからです。 |
|
90 マッチしないはずなのにマッチする(2) |
|
91 "兄弟" =~ /Z/ はマッチします。それは、'兄' の第二バイトが 'Z' だからです。第二バイトがアルファベットになる文字には注意が必要です。 |
|
92 マッチするはずなのにマッチしない(1) |
|
93 "運転免許" =~ /運転/ はマッチしません。それは、'運' の第二バイトが '^'なので、/運転/ は /\x89^転/ とみなされ、文字列の始め ^ の前に \x89 はないからです。 |
|
94 Search pattern not terminated |
|
95 「サーチパターンが終了しない」 |
|
96 これは、/表/ のように、第二バイトが \ である文字でサーチパターンを終わらせようとしたときに起こります。マッチ演算子の終端文字 / をエスケープしてしまうので、サーチパターンがさらに先に続くように解釈されます。その先にもう一度/はありますか? |
|
97 あったところで、別のエラーが発生するでしょう。 |
|
98 Substitution replacement not terminated |
|
99 「置換操作の置換文字列が終了しない」 |
|
100 置換演算子は s/PATTERN/REPLACEMENT/の形式をとらねばなりません。しかし s/表/裏/; のように、第二バイトが \ である文字でPATTERN部分を終わらせようとしたときにこのエラーが起こります。マッチ演算子の終端文字 / をエスケープしてしまうので、PATTERNがさらに先に続くように解釈されます。そのためperlは、PATTERNは 表/裏 の部分であると考え、3番目の/の先にREPLACEMENT部分があるに違いないと思うのですが、その先にもう一度/はありますか? |
|
101 あったところで、別のエラーが発生するでしょう。 |
|
102 unmatched [ ] in regexp |
|
103 「正規表現にマッチしない [ ] がある」 |
|
104 例えば、/プール/ ではエラーが起こります。それは 'ー' の第二バイトが [ なので、/プール/ は /プ\x81[\x83\x8B/ とみなされ、perlは文字クラスがあるのではないかと思います。しかし文字クラスの終了を示す ] が見つからないのでエラーになります。 |
|
105 エラーにはならないけど文字化けする(5) |
|
106 例えば、lc('アイウエオ')は、'ヂツテトナ'を返します。Shift-JISの2バイト文字の中には、第2バイトがASCIIで英字に当たるものがあります。詳しくはアルファベットの大文字と小文字をご覧下さい。 |
|
107 エラーや間違いを防ぐ対策 |
|
108 以上のようなエラーを防ぐにはさまざまな方法が考えられます。例えば、"表\示" と書けばいいなどという提案があります。もちろんそれでもかまいません。しかしそのためにはどの文字の後に \ を入れればいいかを知る必要があります。それは文字コード表を見れば一発で明らかです。 |
|
109 …などという面倒なことが苦にならない人、文字コード表なんか(少なくとも問題になるような文字くらいは)暗記してしまえばいいというような人にとっては、確かにそれで解決になると思います。しかし、そのような人には、こんなページを見にくる必要もヒマもないでしょう。 |
|
110 そこで、このようなページをわざわざ見にくるような人は、文字コード表をいちいち調べたくないひとだと仮定します。別にそうだからといって非難されることはありません。しかし、手間を惜しむあまり間違ったプログラムを平気で作っていては、顔にクリームパイをぶつけられても仕方ありません。 |
|
111 文字列リテラルの作り方 |
|
112 よく知っておかねばならないことは、\ というエスケープ用の文字は、変数展開やメタ文字の解釈よりずっと前の段階でさまざまな影響を及ぼすということです。そのため、どうしたらデータを確実に変数の中に収められるかを考える必要があります。変数の内部に収めてしまえば、Perlがデータを適切に管理してくれます。よく知られている $str = "表示" の文字化けも、変数$strに代入する以前、ダブルクォートで囲んだ時点ですでに文字化けしていると考えるべきです。すでに文字化けしたデータを代入して、好い結果が得られるはずがありません。 |
|
113 ヒアドキュメントは安全性の高い解決法です。ただし、終端文字列をシングルクォートで囲んでやらなくてはなりません。ダブルクォートで囲んだり、クォートを付けなかったりでは、予期せぬ変数展開やメタ文字の解釈を防ぐことができません。 |
|
114 シングルクォートで終端文字列を囲んだヒアドキュメントでは、変数展開やメタ文字の解釈は何も起こりません。ただ、終端文字列(この場合は "\nEOF\n")を探すことだけを行います。ヒアドキュメントを使うと文字列に改行文字がつきますが、chompで除くといいでしょう。 |
|
115 次の例は $str = 'ここにテキストを書く' と同じように働きますが、文字列の内容によって問題が起こりません。書いたとおりにリテラルを代入できると期待できます。 |
|
116 サンプルコード |
|
117 chomp($str = << 'EOF'); |
|
118 ここにテキストを書く |
|
119 EOF |
|
120 |
|
121 $src = << 'EOF'; |
|
122 $path = "C:\\path\\file"; |
|
123 open FH, $path; |
|
124 EOF |
|
125 |
|
126 多くの文字列を一度に作りたければ、splitで分割すると容易に作れます。 |
|
127 サンプルコード |
|
128 ($name, $career, $age, $memo) = split "\n", << 'EOF'; |
|
129 田中一郎 |
|
130 プログラマ |
|
131 三十五 |
|
132 大福が好物である。 酒はあまり呑まない。 |
|
133 EOF |
|
134 |
|
135 もう少し簡潔に書きたければ、空白文字、\(2バイト文字に含まれているのは構わない)、およびカッコを含まないという条件で、qw() を使うことができます。例えば、@str = qw(表示 対応表 );のように空白を入れてカッコのエスケープを防ぎます。@str = qw(表示 対応表);のように空白を入れないとエラーの元です。 |
|
136 1つの文字列を作る時でも、左辺を丸カッコで囲んでリストコンテキストを示すか、右辺をスライスにするかしなければなりません。これは、(現状では)qw// は split(' ', q//) の略記として実装しているからです。なお、Perl 5.6ではリストと等価になっているようです。 |
|
137 サンプルコード |
|
138 ($str) = qw(百三十 ); |
|
139 $str = (qw/百三十 /)[0]; |
|
140 $str = qw/百三十 /; # Perl 5.6 |
|
141 |
|
142 正規表現とマッチ |
|
143 正規表現のメタ文字は多いので、正規表現の中にShift-JISの文字列を埋めこむのは得策ではありません。例えば、/\Q対応表/ ではエラーになります。これは、/ / の範囲の決定が真っ先に行われ、その時点でエラーが発生するので、\Q の効果を及ぼすことができないからです。また、/\Q対応表\E/ は巧く行きません。これは、対応表\E という文字列を含むものにしかマッチしません。これは、\\ という連続があるため、\E が認識されないからでしょう(たぶん)。 |
|
144 そのため、変数に入れて、マッチ演算子や置換演算子の中で展開させるとよろしいです。このとき日本語文字列は予めquotemeta 関数で処理しておきます。 |
|
145 サンプルコード |
|
146 $pat = quotemeta +(qw/ 表 /)[0]; |
|
147 $str =~ /$pat\d+/; # 表1, 表2, ..などにマッチ |
|
148 # しかし $str = '剣\\1' でもマッチする(この問題は後述) |
|
149 |
|
150 上のような書き方は確かに醜いですね。クオートの中で \Q \E を使う時は、正しい文字列が入っている変数と一緒になら問題が起こりません。こうすることで、\Q \E の範囲が明確になるからです。正しい文字列の作り方は、前述のリテラルの作りかたを参考にして下さい。 |
|
151 サンプルコード |
|
152 $pat = "(?:\Q$str1\E|\Q$str2\E)*"; |
|
153 $str =~ /$pat/; |
|
154 |
|
155 # 実は上の文は次の文と等価。 |
|
156 # $pat = "(?:" . quotemeta($str1) . "|" . quotemeta($str2) . ")*"; |
|
157 |
|
158 リテラルをクォートの中に直接埋めこむとうまく行かないことがあります。それは、perlが \E というメタ文字を発見しようというのを、Shift-JIS文字が妨げるためです。 |
|
159 "\Q表\E"では、表Eにマッチする正規表現になります。表の第二バイトの \ と次の \が合わさるので、perlには\Q \x95 \\ Eの組み合わせであるように思われます。 \Qの作用の結果は\\x95\\x5cEになります。そのため、表Eにマッチします。 |
|
160 "\Q表"はどうでしょう。この場合は、" " の範囲を決めるときに、表の第二バイトが後のクォートをエスケープしてしまうので、文字列の範囲が期待したようには定まらず、エラーになります。このエラーは\Qの効果を考慮する前に発生するので、防ぎようがありません。 |
|
161 "\Q表\\E"はどうでしょう。確かにShift-JISでは問題ありません。しかし同じスクリプトをEUC-JPやUTF-8に変換したときには問題があります。表\Eと余分な2文字がある文字列でないとマッチしません。どちらにしろ、\をどこに添えるかを考える必要があるので、ここの趣旨に合いません。 |
|
162 正規表現は例えば、次のようにします。もちろんこれはShift-JISのみに有効です。 |
|
163 $digit = '(?:[0-9]|\x82[\x4F-\x58])'; # 数字(半角と全角) |
|
164 $upper = '(?:[A-Z]|\x82[\x60-\x79])'; # アルファベット大文字(半角と全角) |
|
165 $lower = '(?:[a-z]|\x82[\x81-\x9A])'; # アルファベット小文字(半角と全角) |
|
166 $space = '(?:[\ \n\r\t\f]|\x81\x40)'; # 空白文字(半角と全角) |
|
167 $ascii = '[\x00-\x7F]'; # ASCII文字 |
|
168 |
|
169 # 全角平仮名(濁点・半濁点・踊り字を含む) |
|
170 $hiraZ = '(?:\x82[\x9F-\xF1]|\x81[\x4A\x4B\x54\x55])'; |
|
171 |
|
172 # 全角片仮名(長音符・濁点・半濁点・踊り字を含む) |
|
173 $kataZ = '(?:\x83[\x40-\x7E\x80-\x96]|\x81[\x5B\x4A\x4B\x52\x53])'; |
|
174 |
|
175 # 半角片仮名(半角長音符・句読点を含む) |
|
176 $kataH = '[\xA1-\xDF]'; |
|
177 |
|
178 $onebyte = '[\x00-\x7F\xA1-\xDF]'; |
|
179 $twobyte = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
180 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
181 |
|
182 # JIS文字 |
|
183 $all_JIS = '(?:[\x00-\x7f\xa1-\xdf]|'. |
|
184 . '\x81[\x40-\x7e\x80-\xac\xb8-\xbf\xc8-\xce\xda-\xe8\xf0-\xf7\xfc]|' |
|
185 . '\x82[\x4f-\x58\x60-\x79\x81-\x9a\x9f-\xf1]|' |
|
186 . '\x83[\x40-\x7e\x80-\x96\x9f-\xb6\xbf-\xd6]|' |
|
187 . '\x84[\x40-\x60\x70-\x7e\x80-\x91\x9f-\xbe]|' |
|
188 . '\x88[\x9f-\xfc]|\x98[\x40-\x72\x9f-\xfc]|\xea[\x40-\x7e\x80-\xa4]|' |
|
189 . '[\x89-\x97\x99-\x9f\xe0-\xe9][\x40-\x7e\x80-\xfc])'; |
|
190 |
|
191 # ベンダ定義文字 |
|
192 |
|
193 # NEC特殊文字 |
|
194 $NEC_special = '(?:\x87[\x40-\x5d\x5f-\x75\x7e\x80-\x9c])'; |
|
195 |
|
196 # NEC選定IBM拡張文字 |
|
197 $NEC_IBM_ext = '(?:\xed[\x40-\x7e\x80-\xfc]|\xee[\x40-\x7e\x80-\xec\xef-\xfc])'; |
|
198 |
|
199 # IBM拡張文字 |
|
200 $IBM_ext = '(?:[\xfa-\xfb][\x40-\x7e\x80-\xfc]|\xfc[\x40-\x4b])'; |
|
201 |
|
202 |
|
203 Shift-JISでマッチを行う時には、2つの問題があります。 |
|
204 第二バイトがASCIIの領域に入る文字があるので、ASCIIを含むパターンにマッチする可能性がある。 |
|
205 ある文字の第二バイトと次の文字の第一バイトが1文字であるかのようにマッチしてしまう。 |
|
206 後者はEUC-JPでも起こりうる問題です(UTF-8なら起こらないが、今はそれが問題なのではない)。しかし前者はEUC-JPでは起こらないが、Shift-JISでは起こりうる問題です。これらを防ぐ方法は、結局同じことですが、正規表現の中に、常に先頭を含ませることです。 |
|
207 サンプルコード |
|
208 # 先頭からマッチ |
|
209 $str =~ /^$char*?(?:$pat)/; |
|
210 |
|
211 末尾からのマッチではうまく行かないことがあります。"右" =~ /E$/を考えれば十分でしょう。また、"\x8E" x 30 . "E"は$str = "試試試試試試試試試試試試試試試E"であるが、"\x8E" x 31 . "E"は$str = "試試試試試試試試試試試試試試試殺"でありますから、Shift-JIS文字列を後ろから切り分ける適切な方法はないと考えられます。 |
|
212 少なくとも、2バイト文字を構成しないバイト [\x00-\x3F\x7F] が見つかる所まで、極端な場合は文字列の最初までスキャンしないとわからず、しかも後読み lookbehind の正規表現 (?<=PATTERN)は今の所、不定長にできません((?<=(?:\A|[\x00-\x3F\x7F])$char*) とはできない)ので、先頭から文字単位でばらしてから処理するのが、結局は簡便なのかもしれません。 |
|
213 グローバルマッチ |
|
214 グローバルマッチ /g の場合は、\Gを使いましょう。\Gは前回マッチした部分の末尾を指します。 |
|
215 次の例では、置換されないのが望ましいのですが、\Gがないので、先頭から開始して文字列全体まで延びてマッチしなかったあと、改めて先頭から1バイト進んだ位置からスキャンを再開するので、ずれた位置なのにマッチしたと考えてしまいます。\Gを使わないと、間違った位置にマッチするかもしれないうえに、余計な再検査をするので、時間もかかります。 |
|
216 サンプルコード |
|
217 $str = '試試試試E試試試試E'; |
|
218 $pat = '殺'; |
|
219 $str =~ s/\G($char*?)($pat)/${1}E/og; |
|
220 # '試試試試E試試試試E' のまま(正しい)。 |
|
221 |
|
222 \Gを付けない場合 |
|
223 |
|
224 $str = '試試試試E試試試試E'; |
|
225 $pat = '殺'; |
|
226 $str =~ s/($char*?)($pat)/${1}E/og; # '殺' があれば 'E' に置換 |
|
227 print $str; |
|
228 # '試試試殺試試試殺' になってしまう(おかしい)。 |
|
229 |
|
230 試試試試E試試試試E |
|
231 1回目 →→→→/→→→→/ (マッチしない) |
|
232 2回目 |→→→⇒||||||||| (マッチしたので置換) |
|
233 3回目 |||||||||→→→→/ (マッチしない) |
|
234 4回目 ||||||||||→→→⇒ (マッチしたので置換) |
|
235 |
|
236 凡例: → $charが2バイト文字にマッチ |
|
237 / $charが1バイト文字にマッチ |
|
238 ⇒ $patがマッチ |
|
239 | スキャンの範囲外 |
|
240 |
|
241 ただし、パターンがゼロ文字幅にマッチする場合には、注意が必要です。次の例は、「ア」の前に 'Z' を入れるものです。第1例は文字のずれ('ア' : 0x8341に対して'泣A' : 0x8B8341がずれてマッチする)を防いでいません。第2例は、上の方法で「ずれ」を防ごうとしたのですが、Z への置換が連続して起こっています。 |
|
242 これは第3例のようにする必要があります。これは、第2例では「なぜ?」に書いたように、置換されるからと考えられます。 |
|
243 サンプルコード |
|
244 $str = "アイウエアアイウア泣A"; |
|
245 |
|
246 print +($temp = $str) =~ s/(?=ア)/Z/g, $temp; |
|
247 |
|
248 print +($temp = $str) =~ s/\G($char*?)(?=ア)/${1}Z/g, $temp; |
|
249 |
|
250 print +($temp = $str) =~ s/\G(\A|$char+?)(?=ア)/${1}Z/g, $temp; |
|
251 |
|
252 結果 |
|
253 5 ZアイウエZアZアイウZア技ア |
|
254 7 ZアイウエZZアZZアイウZZア泣A |
|
255 4 ZアイウエZアZアイウZア泣A |
|
256 |
|
257 なぜ? |
|
258 ア イ ウ エ ア ア イ ウ ア 泣 A |
|
259 1 \G Z |
|
260 2 \G$char$char$char$char Z |
|
261 3 \G Z |
|
262 4 \G$char Z |
|
263 5 以下、省略 |
|
264 |
|
265 つまり、グローバルマッチでは、マッチがゼロ文字幅でないパターンの前には\G($char*?)を、ゼロ文字幅であるパターンの前には\G(\A|$char+?)を入れる必要があります。 |
|
266 ただし、これでも正しく(?)マッチさせられない場合があります。 |
|
267 サンプルコード |
|
268 $str = "0123000123"; |
|
269 |
|
270 print +($temp = $str) =~ s/0*/Z/g, $temp; |
|
271 |
|
272 print +($temp = $str) =~ s/\G($char*?)0*/${1}Z/g, $temp; |
|
273 |
|
274 print +($temp = $str) =~ s/\G(\A|$char+?)0*/${1}Z/g, $temp; |
|
275 __END__ |
|
276 9 ZZ1Z2Z3ZZ1Z2Z3Z |
|
277 14 ZZ1ZZ2ZZ3ZZ1ZZ2ZZ3ZZ |
|
278 7 Z1Z2Z3Z1Z2Z3Z |
|
279 |
|
280 これは、パターンがゼロ文字幅にマッチするものであると、/gがその場で無限に足踏みして終了しなくなるのを防ぐため、perlは、マッチを強制的に進めているのですが(cf. perlre, Repeated patterns matching zero-length substring)、この進め方の真似(しかもバイト単位ではなく、文字単位で進むもの)が、\G($char*?)や\G(\A|$char+?)では、うまくできないからです。もっともこういうマッチをすることは、ほとんどないと考えられますので、気にする必要はないのかも知れません(<負け惜しみ)。 |
|
281 アルファベットの大文字と小文字 |
|
282 Shift-JISの2バイト文字の中には、第2バイトがASCIIで英字に当たるものがあります。そのため、関数 uc, lc や、メタ文字 \U, \Lが2バイト文字の一部を変換してしまったり(しかし関数 ucfirst, lcfirst や メタ文字 \u, \l は問題とならない)、m//i や s///iなどの /i修飾子によって違う文字なのにマッチしてしまったりすることがあります。 |
|
283 Shift-JIS文字列に含まれるASCIIの英字を大文字または小文字に揃えたいなら、例えば、次のようなサブルーチンを作れば実現できます。 |
|
284 サンプルコード |
|
285 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
286 |
|
287 lc("PERLプログラミング"); # 'perlプロバラミンバ' |
|
288 tolower("PERLプログラミング"); # 'perlプログラミング' |
|
289 |
|
290 sub tolower { |
|
291 my $str = $_[0]; |
|
292 $str =~ s/\G($char*?)([A-Z]+)/$1\L$2/g; |
|
293 $str; |
|
294 } |
|
295 |
|
296 sub toupper { |
|
297 my $str = $_[0]; |
|
298 $str =~ s/\G($char*?)([a-z]+)/$1\U$2/g; |
|
299 $str; |
|
300 } |
|
301 |
|
302 ケース無視のマッチ /i の場合は、例えば 'エ'の第二バイトは 'G' であり、'ト'の第二バイトは 'g' であることから、'エ' =~ /ト/iはマッチします。ですから、Shift-JISで正確なマッチをしたければ、/i修飾子は使うことができません。 |
|
303 かわりに、文字列に含まれるアルファベット(二バイト文字の第2バイトにあるものを除く)を小文字(または大文字、どちらか)に統一してマッチさせます。tolowerの定義は上をご覧下さい。 |
|
304 サンプルコード |
|
305 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
306 |
|
307 $lcstr = tolower($str); |
|
308 $lckey = tolower(quotemeta $key); |
|
309 |
|
310 if ($lcstr =~ /^$char*?$lckey/) { |
|
311 print "matched"; |
|
312 } |
|
313 else { |
|
314 print "not matched"; |
|
315 } |
|
316 |
|
317 または埋め込み修飾子 (?ismx-ismx) を用いても好い結果を得られます。 |
|
318 サンプルコード |
|
319 "第1回Perl講縮のご案内" =~ /^$char*?PERL講習/i # マッチする(困る) |
|
320 "第2回Perl講縮のご案内" =~ /^$char*?((?i)PERL)講習/ # マッチしない(良し) |
|
321 "第3回Perl講習のご案内" =~ /^$char*?((?i)PERL)講習/ # マッチする(良し) |
|
322 |
|
323 全角2バイトアルファベットのケース無視は、選択により実現できます(が、やっぱり変)。'A'の第2バイトが '`' なのも、ちょっと注意です(m`` などのとき致命的エラーになる。ただしバッククォートを使う意味は特にない)。原則的にはリテラルをマッチ演算子や置換演算子に直接埋め込むのは避けたい所です。 |
|
324 サンプルコード |
|
325 /(?:P|p)(?:E|e)(?:R|r)(?:L|l)/; |
|
326 |
|
327 その代わりにこんなサブルーチンを作ってみてもよいかもしれません。 |
|
328 サンプルコード |
|
329 $CharRE = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
330 |
|
331 $pat = make_regexp_ignorecase("PERL講習"); |
|
332 print "第5回Perl講習会" =~ /^$char*?$pat/ ? "OK": "NOT"; |
|
333 |
|
334 sub make_regexp_ignorecase { |
|
335 my $str = $_[0]; |
|
336 $str =~ s/\G([A-Za-z]+|$CharRE)/ |
|
337 my $c = ord $1; |
|
338 if($c == 0x82) { |
|
339 my $v = vec($1,1,8); |
|
340 0x81 <= $v && $v <= 0x9A ? sprintf('\\x82[\\x%2x\\x%2x]', $v, $v-33) : |
|
341 0x60 <= $v && $v <= 0x79 ? sprintf('\\x82[\\x%2x\\x%2x]', $v, $v+33) : |
|
342 quotemeta($1); |
|
343 } |
|
344 elsif(0x41 <= $c && $c <= 0x5A || 0x61 <= $c && $c <= 0x7A) {"(?:(?i)$1)"} |
|
345 else {quotemeta($1)} |
|
346 /geo; |
|
347 $str; |
|
348 } |
|
349 |
|
350 長い文字列に対する正規表現検索 |
|
351 正規表現は、Perl にとって欠かせない存在といえます。しかし正規表現の制限として、*, +, {min,max} などの量指定子がマッチを繰り返せる回数の上限という問題があります。(詳細は perlre 参照のこと)。そのため、$char*? という正規表現には、危険性があります。 |
|
352 例えば、次のようなマッチングを考えて見ましょう。$strは、「あ」が10万字連続したあとに、「アイABC」が連結された文字列です。このような文字列(ただし、「『あ』が10万字連続」ということは分からず、任意の Shift-JIS テキストであろうということにします)から、半角アルファベットが連続した部分を見つけたいとしましょう。すると、今までの考え方からすると、次のようにすればよいと思われます。 |
|
353 サンプルコード |
|
354 my $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
355 my $str = ('あ' x 100000) . 'アイABC'; |
|
356 $str =~ /^$char*?([A-Z]+)/o; |
|
357 print $1; |
|
358 |
|
359 しかし、これは、環境によっては、大きなエラーを引き起こします。例えば、Windows 98上で Active Perl 522 を用いた場合、Error: Runtime exception という Perl のエラーになりました。また、Windows 98上でVC++ 6.0でコンパイルされた Perl 5.6.1 だと、「このプログラムは不正な処理を行ったので強制終了されます。〜」などといったエラーになりました。 |
|
360 このような問題をできるだけ防ぐためには、次のようにします。つまり、文字列の先頭から調べていく場合、二バイト文字の文字境界を間違えるのは、二バイト文字の第一バイトの直後を文字境界と誤認識した時だけです。Shift-JISでは、二バイト文字の第一バイトは、[\x81-\x9F\xE0-\xFC] だけです。あるいは、EUC-JP に変換可能な領域だけを考慮すれば、[\x81-\x9F\xE0-\xEF] だけということができます。それ以外のバイトの直後は、例えば、0x41 の直後は、'A' の直後か、'ア' の直後かは分かりませんが、確かに文字境界になります。従って、[\x81-\x9F\xE0-\xFC]+ (または [\x81-\x9F\xE0-\xEF]+ )のバイト(二バイト文字)が連続するところだけに注意すればよいことになります。 |
|
361 このため、以下のように、^$char*? の代わりに $Apad を使い、\G$char*? の代わりに $Gpad を用いれば、一バイト文字か、二バイト文字のうち第二バイトが [\x40-\x7E\x80\xA0-\xDF] で終わるものが、少なくとも適当な間隔で(上限に達しないうちに)出現すれば、エラーにならずに処理することができます。(確率的な問題ですので、完全ではありません。) |
|
362 サンプルコード |
|
363 # 一回だけマッチ |
|
364 my $Apad = '(?:(?:\A|[\x00-\x80\xA0-\xDF])(?:[\x81-\x9F\xE0-\xFC]{2})*?)'; |
|
365 my $str1 = ('あ' x 100000) . 'アイABC'; |
|
366 $str1 =~ /$Apad([A-Z]+)/o; |
|
367 print "$1\n"; # "ABC" と表示される。 |
|
368 |
|
369 # グローバルマッチ |
|
370 my $Gpad = '(?:(?:\G|[\x00-\x80\xA0-\xDF])(?:[\x81-\x9F\xE0-\xFC]{2})*?)'; |
|
371 |
|
372 my $str2 = 'あ' x 100000 . 'アイABC'. 'お' x 100000 . 'XYZ'; |
|
373 my @array = $str2 =~ /$Gpad([A-Z]+)/go; |
|
374 print "@array\n"; # "ABC XYZ" と表示される。 |
|
375 |
|
376 外字の変換 |
|
377 ベンダ定義文字やユーザ定義文字を含む文字列を他の環境でも利用できるようにするには、適切な変換が必要です(無論、まったく同じ字体の利用はほとんど望めず、おそらくは類似した文字や文字列に変換することになるでしょう)。これはPerlでは置換演算子 s/// を使えば比較的容易に実現できます。 |
|
378 あらかじめ、どの外字をどう変換するかを定義する変換テーブルを用意しなくてはなりません。これはPerlではハッシュにしておくとその後の処理が楽になります。ここでは、'w932_gai.txt'で定義する、Windows codepage-932コードに基づいた機種依存文字の(部分的)変換テーブルを使うことにします。 |
|
379 次のコードでは、1文字づつマッチさせ、その文字が変換ハッシュのキーにあれば対応する値の文字列に置換し、そうでなければそのまま残します。 |
|
380 サンプルコード |
|
381 require 'w932_gai.txt'; # %tableの定義(不完全!) |
|
382 |
|
383 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
384 |
|
385 $str =~ s/($char)/exists $table{$1} ? $table{$1} : $1/geo; |
|
386 |
|
387 同様な処理は、つぎのような書き方でもできますが、外字にマッチする正規表現 $gaijiを用意する必要があります。ずれたマッチをしないために、こちらの正規表現には \G が必要です。例えば、$str = '∞@';の後ろ2バイトは "\x87\x40" ですが、こうすればマッチがずれる心配がありません。また、非欲張りマッチ ($char*?)を使えば $char が外字にマッチしないよう変更する必要はありません。 |
|
388 サンプルコード |
|
389 require 'w932_gai.txt'; # %tableの定義(不完全!) |
|
390 |
|
391 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
392 $gaiji = '(?:[\x87][\x40-\x9c])'; |
|
393 |
|
394 $str =~ s/\G($char*?)($gaiji)/$1$table{$2}/g; |
|
395 |
|
396 CP932重複定義文字の変換 |
|
397 Microsoft Windows 日本語版で一般的に使用されているコードページ 932 (CP932) では、幾つかの文字が重複して定義された状態になっています。ここで、文字が重複定義されているとは、Unicodeの同じ符号位置に対応付けられていることとします。 |
|
398 例えば、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" になります。 |
|
399 しかし、CP-932 のテキスト中、重複定義文字がどれかに揃っていないことがあります。例えば "\x87\x9A" や "\xFA\x5B" が含まれていると、テキストを目で見ると違いがないのに、"\x81\xE6" で検索しても見つけられないことになります。 |
|
400 重複定義文字を揃えるモジュールとして、ShiftJIS/CP932/Correct.pm があります。入手と使い方はPerlのページに戻れば見つかります。 |
|
401 また、ShiftJIS/String.pm の strtr() または trclosure() を使う方法もあります。入手と使い方はPerlのページに戻れば見つかります。 |
|
402 サンプルコード |
|
403 |
|
404 # (1) $necJIS -> $jisNEC (9対) |
|
405 $necJIS = "\x87\x90\x87\x91\x87\x92\x87\x95\x87\x96\x87\x97\x87\x9A\x87\x9B\x87\x9C"; |
|
406 # NEC特殊文字のうち、JIS文字に変換されるべき非漢字 |
|
407 $jisNEC = "\x81\xE0\x81\xDF\x81\xE7\x81\xE3\x81\xDB\x81\xDA\x81\xE6\x81\xBF\x81\xBE"; |
|
408 # JIS文字のうち、NEC特殊文字に重複定義されている非漢字 |
|
409 |
|
410 # (2) $necibmJIS -> $jisNECIBM (1対) |
|
411 $necibmJIS = "\xEE\xF9"; |
|
412 # NEC選定IBM拡張文字のうち、JIS文字に変換されるべき非漢字 |
|
413 $jisNECIBM = "\x81\xCA"; |
|
414 # JIS文字のうち、NEC選定IBM拡張文字に重複定義されている非漢字 |
|
415 |
|
416 # (3) $ibmJIS -> $jisIBM (2対) |
|
417 $ibmJIS = "\xFA\x54\xFA\x5B"; |
|
418 # IBM拡張文字のうち、JIS文字に変換されるべき非漢字 |
|
419 $jisIBM = "\x81\xCA\x81\xE6"; |
|
420 # JIS文字のうち、IBM拡張文字に重複定義されている非漢字 |
|
421 |
|
422 # (4) $ibmNEC -> $necIBM (13対) |
|
423 $ibmNEC = "\xFA\x4A-\xFA\x53\xFA\x58\xFA\x59\xFA\x5A"; |
|
424 # IBM拡張文字のうち、NEC特殊文字に変換されるべき非漢字 |
|
425 $necIBM = "\x87\x54-\x87\x5D\x87\x8A\x87\x82\x87\x84"; |
|
426 # NEC特殊文字のうち、IBM拡張文字に重複定義されている非漢字 |
|
427 |
|
428 # (5) $necibmIBM -> $ibmNECIBM (13対) |
|
429 $necibmIBM = "\xEE\xEF-\xEE\xF8\xEE\xFA\xEE\xFB\xEE\xFC"; |
|
430 # NEC選定IBM拡張文字のうち、IBM拡張文字に変換されるべき非漢字 |
|
431 $ibmNECIBM = "\xFA\x40-\xFA\x49\xFA\x55\xFA\x56\xFA\x57"; |
|
432 # IBM拡張文字のうち、NEC選定IBM拡張文字に重複定義されている非漢字 |
|
433 |
|
434 # (6) $necibmCJK -> $ibmCJK (360対) |
|
435 $necibmCJK = "\xED\x40-\xEE\xEC"; |
|
436 # NEC選定IBM拡張文字中の漢字 |
|
437 $ibmCJK = "\xFA\x5C-\xFC\x4B"; |
|
438 # IBM拡張文字中の漢字 |
|
439 |
|
440 use ShiftJIS::String qw(trclosure); |
|
441 |
|
442 # 変換用クロージャの生成 |
|
443 $correctCP932 = trclosure( |
|
444 $necJIS.$necibmJIS.$ibmJIS.$ibmNEC.$necibmIBM.$necibmCJK, # from |
|
445 $jisNEC.$jisNECIBM.$jisIBM.$necIBM.$ibmNECIBM.$ibmCJK # to |
|
446 ); |
|
447 |
|
448 $result = $correctCP932->($source); # $source を変換して $result を得る |
|
449 |
|
450 文字数を数える |
|
451 Shift-JIS文字列の文字数を数えるには、マッチ演算子を利用するならスカラーコンテキストで数えた方が若干速かったです。それより、置換演算子を利用したほうが速く書けるとわかりました。 |
|
452 もっともXSで書いたほうがずっと速かったです。まあ、XSUBは無理に利用しなくてもよいでしょう。 |
|
453 サンプルコード |
|
454 use Benchmark; |
|
455 |
|
456 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
457 $s = "漢字あ\0AアCテスト -"; |
|
458 |
|
459 timethese (100000, { |
|
460 le => q{ |
|
461 ($str = $s) =~ s/$char/0/go; |
|
462 $le = length $str; |
|
463 }, |
|
464 sg => q{ |
|
465 $sg = ($str = $s) =~ s/$char//go; |
|
466 }, |
|
467 ab => q{ |
|
468 $ab = 0; |
|
469 $ab++ while $s =~ /[^\x81-\x9F\xE0-\xFC]|../g; |
|
470 }, |
|
471 ar => q{ |
|
472 $ar = @{[ $s =~ /$char/go ]}; |
|
473 }, |
|
474 gr => q{ |
|
475 $gr = grep defined, $s =~ /$char/go; |
|
476 }, |
|
477 wh => q{ |
|
478 $wh = 0; |
|
479 $wh++ while $s =~ /$char/go; |
|
480 }, |
|
481 sj => q{ |
|
482 $sj = sjslen($s); |
|
483 }, |
|
484 xs => q{ |
|
485 $xs = sjlength($s); |
|
486 }, |
|
487 }); |
|
488 |
|
489 sub sjslen { |
|
490 my($str,$len,$i,$c,$blen); |
|
491 $str = shift; |
|
492 $blen = length $str; |
|
493 while ($i < $blen) { |
|
494 $c = vec($str, $i, 8); |
|
495 if (0x81 <= $c && $c <= 0x9F || 0xE0 <= $c && $c <= 0xFC){ $i++ } |
|
496 $i++,$len++; |
|
497 } |
|
498 $len; |
|
499 } |
|
500 |
|
501 結果 |
|
502 Benchmark: timing 100000 iterations of ab, ar, gr, le, sg, sj, wh, xs... |
|
503 ab: 4 wallclock secs ( 3.46 usr + 0.00 sys = 3.46 CPU) @ 28901.73/s |
|
504 ar: 6 wallclock secs ( 5.98 usr + 0.00 sys = 5.98 CPU) @ 16722.41/s |
|
505 gr: 6 wallclock secs ( 5.50 usr + 0.00 sys = 5.50 CPU) @ 18181.82/s |
|
506 le: 3 wallclock secs ( 2.09 usr + 0.00 sys = 2.09 CPU) @ 47846.89/s |
|
507 sg: 2 wallclock secs ( 1.92 usr + 0.00 sys = 1.92 CPU) @ 52083.33/s |
|
508 sj: 9 wallclock secs ( 8.57 usr + 0.00 sys = 8.57 CPU) @ 11668.61/s |
|
509 wh: 5 wallclock secs ( 4.78 usr + 0.00 sys = 4.78 CPU) @ 20920.50/s |
|
510 xs: 1 wallclock secs ( 0.38 usr + 0.00 sys = 0.38 CPU) @ 263157.89/s |
|
511 (warning: too few iterations for a reliable count) |
|
512 |
|
513 XSUB |
|
514 int |
|
515 sjlength(arg) |
|
516 SV* arg |
|
517 PROTOTYPE: $ |
|
518 PREINIT: |
|
519 unsigned char *str, *p, *e; |
|
520 STRLEN byte, len = 0; |
|
521 CODE: |
|
522 p = str = (unsigned char *)SvPV(arg, byte); |
|
523 e = str + byte; |
|
524 while (p < e) { |
|
525 if (0x81 <= *p && *p <= 0x9F || 0xE0 <= *p && *p <= 0xFC) |
|
526 ++p; |
|
527 ++p, ++len; |
|
528 } |
|
529 RETVAL = len; |
|
530 OUTPUT: |
|
531 RETVAL |
|
532 |
|
533 文字単位に分ける |
|
534 Shift-JIS文字列を文字単位に分割しましょう。この場合は、XSを利用してもあまり速くなりませんでした。返り値のリストを用意するのに時間が取られるのか、やはりPerlの正規表現の処理はかなり速いものだということでしょう。 |
|
535 サンプルコード |
|
536 use Benchmark; |
|
537 |
|
538 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
539 $s = "日本語ニホンゴ\0ABC" x 100; |
|
540 |
|
541 timethese (1000, { |
|
542 re => q{ |
|
543 @re = $s =~ /$char/go; |
|
544 }, |
|
545 xs => q{ |
|
546 @xs = sjsplit($s); |
|
547 }, |
|
548 }); |
|
549 |
|
550 結果 |
|
551 Benchmark: timing 1000 iterations of re, xs... |
|
552 re: 7 wallclock secs ( 6.65 usr + 0.00 sys = 6.65 CPU) @ 150.38/s |
|
553 xs: 6 wallclock secs ( 5.33 usr + 0.00 sys = 5.33 CPU) @ 187.62/s |
|
554 |
|
555 XSUB |
|
556 void |
|
557 sjsplit(arg) |
|
558 SV* arg |
|
559 PROTOTYPE: $ |
|
560 PREINIT: |
|
561 unsigned char *str, *p, *e; |
|
562 STRLEN ch, byte, len = 0; |
|
563 PPCODE: |
|
564 str = (unsigned char *)SvPV(arg,byte); |
|
565 e = str + byte; |
|
566 for (p = str; p < e; p++) { |
|
567 if (0x81 <= *p && *p <= 0x9F || 0xE0 <= *p && *p <= 0xFC) ++p; |
|
568 ++len; |
|
569 } |
|
570 EXTEND(SP,len); |
|
571 for (p = str; p < e; p += ch) { |
|
572 ch = (0x81 <= *p && *p <= 0x9F || 0xE0 < *p && *p <= 0xFC) ? 2 : 1; |
|
573 PUSHs(sv_2mortal(newSVpv(p,ch))); |
|
574 } |
|
575 |
|
576 色々な分割 |
|
577 文字で分割でみたように、文字列を分割するには、m//gが便利です。 |
|
578 サンプルコード |
|
579 $onebyte = '[\x00-\x7F\xA1-\xDF]'; |
|
580 $twobyte = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
581 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
582 |
|
583 #1バイト文字の塊と2バイト文字の塊に分ける。 |
|
584 while ($str =~ /\G($onebyte*)($twobyte*)/g) { |
|
585 push @one, $1 if $1 ne ''; |
|
586 push @two, $2 if $2 ne ''; |
|
587 } |
|
588 |
|
589 #句点が最後の文字となるように分割する。 |
|
590 # '。' ではいいが、文字によっては注意が必要。 |
|
591 @sentences = $str =~ /\G$char*?(?:。|.|$)/g; |
|
592 |
|
593 特定の長さで切りそろえる |
|
594 長い文字列を特定の長さ(バイト長)で切りそろえるなら、次のようにしてできます。 |
|
595 サンプルコード |
|
596 $char = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
597 |
|
598 $str = 'わざわざEUC-JPに変換しないで、Shift-JISのまま処理'. |
|
599 'できたらいいんだけど、なかなか面倒だねえ。'; |
|
600 |
|
601 print join "\n", bytebreak($str,15); |
|
602 |
|
603 sub bytebreak{ |
|
604 my($byte,$bmax,$ch,@lines); |
|
605 my $str = shift; |
|
606 $byte = $bmax = shift; |
|
607 foreach $ch ($str =~ /$char/go) { |
|
608 $byte += length $ch; # 次の文字を継ぎ足した長さ |
|
609 if ($byte <= $bmax) { |
|
610 $lines[-1] .= $ch; # 長すぎなければ継ぎ足す |
|
611 } else { |
|
612 $byte = length $ch; |
|
613 push @lines, $ch; # さもなければ次の行へ |
|
614 } |
|
615 } |
|
616 return @lines; |
|
617 # 長さが足らない場合に、右をスペースで埋めたければ。 |
|
618 # return map {$_ .= ' ' x ($bmax - length)} @lines; |
|
619 } |
|
620 |
|
621 禁則処理は、例えば次のようにして行います。単純な考え方では、禁則処理は、(i) 行頭禁則文字の直前で改行しない;(ii) 行末禁則文字の直後で改行しない;ということになります。また、"(a)"のように、行末禁則文字と行頭禁則文字の間に1文字しかない連続した部分は、その部分の全体が無改行になる点にも配慮します。 |
|
622 この例では文字列の長さをバイト長 length で規定していますが、文字幅とバイト数は必ずしも比例しませんので、場合によっては(ギリシア文字は半角幅にしたいとか、またはプロポーショナルの場合とか、UTF-8の場合とか)文字幅を返す width のような関数を定義する必要があるでしょう。 |
|
623 また、この例のやり方では、禁則による無改行部分だけで一行より長くなる場合は、はみだしを防げません。それが困るなら、禁則の例外として行を分ける(例えば$nextの長さが$bmaxを超えないようにする)処置が必要でしょう。 |
|
624 サンプルコード |
|
625 $CharRE = '(?:[\x00-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; |
|
626 |
|
627 # 行頭禁則文字(一部分) |
|
628 $NotAtBegin = q/)]}’”」』)]}!,.:;?、。々゛゜!,.:;?/; |
|
629 # 行末禁則文字(一部分) |
|
630 $NotAtEnd = q/([{‘“「『([{/; |
|
631 |
|
632 # ハッシュを作る |
|
633 @NotAtBegin{$NotAtBegin =~ m/$CharRE/g} = (); |
|
634 @NotAtEnd{ $NotAtEnd =~ m/$CharRE/g} = (); |
|
635 |
|
636 $Str = 'わざわざEUC-JPに変換しないで、Shift-JISのまま処理'. |
|
637 'できたらいいんだけど、なかなか面倒だねえ。'; |
|
638 |
|
639 print join "\n", linebreak($Str,16); |
|
640 |
|
641 sub linebreak{ |
|
642 my($byte,$i,@chars,$next,@lines); |
|
643 my($str, $bmax, $pad) = @_; |
|
644 |
|
645 # $byteは次の文字を継ぎ足したときの長さ |
|
646 $byte = $bmax; # すぐ改行できるための初期値。 |
|
647 |
|
648 # 文字単位にばらす |
|
649 @chars = $str =~ /$CharRE/go; |
|
650 |
|
651 for ($i=0; $i<@chars; $i++) { |
|
652 $next .= $chars[$i]; # 次の文字 |
|
653 $byte += length $chars[$i]; # 次の文字を継ぎ足した長さ |
|
654 |
|
655 # 次の文字が行末禁則のとき |
|
656 next if $i+1 < @chars && exists $NotAtEnd{ $chars[$i] }; |
|
657 # 次の文字の次が行頭禁則のとき |
|
658 next if $i+1 < @chars && exists $NotAtBegin{ $chars[$i+1] }; |
|
659 |
|
660 # 行の振り分け |
|
661 # 長すぎなければ継ぎ足す |
|
662 if ($byte <= $bmax) { |
|
663 $lines[-1] .= $next; |
|
664 } |
|
665 # さもなければ次の行へ |
|
666 else { |
|
667 push @lines, $next; |
|
668 $byte = length $next;# 新しい行の長さ |
|
669 } |
|
670 $next = ''; |
|
671 } |
|
672 return defined $pad && 1 == length $pad # 詰め物 |
|
673 ? map {$_ .= $pad x ($bmax - length)} @lines |
|
674 : @lines; |
|
675 } |
|
676 |
|
677 ぶら下がり禁則の場合($bmin から $bmaxの範囲を許す)。 |
|
678 $bmin = $bmax - 2; # 例えば。 |
|
679 |
|
680 # 行の振り分け |
|
681 # 長すぎなければ継ぎ足す |
|
682 if ($byte <= $bmax && @lines && length $lines[-1] < $bmin){ |
|
683 $lines[-1] .= $next; |
|
684 } |
|
685 # さもなければ次の行へ |
|
686 else { |
|
687 push @lines, $next; |
|
688 $byte = length $next;# 新しい行の長さ |
|
689 } |
|
690 |
|
691 日本語文字列を並び替える |
|
692 仮名文字列を五十音順にソートするモジュールとして、ShiftJIS/Collate.pm があります。入手と使い方はPerlのページに戻れば見つかります。 |
|
693 「読み・表記照合」は次のようにして行います。sortYomiメソッドの受け取るリストの各要素は、[ 表記列, 読み列 ]という配列リファレンスでなければなりません。 |
|
694 サンプルコード |
|
695 use ShiftJIS::Collate; |
|
696 |
|
697 my @data = ( |
|
698 [qw/ 小山 こやま /], |
|
699 [qw/ 長田 ながた /], |
|
700 [qw/ 田中 たなか /], |
|
701 [qw/ 鈴木 すずき /], |
|
702 [qw/ 小嶋 こじま /], |
|
703 [qw/ 児島 こじま /], |
|
704 [qw/ 小山 おやま /], |
|
705 [qw/ 小島 こじま /], |
|
706 [qw/ 小島 こじま /], |
|
707 [qw/ 山田 やまだ /], |
|
708 [qw/ 永田 ながた /], |
|
709 ); |
|
710 |
|
711 @sort = ShiftJIS::Collate->new()->sortYomi(@data); |
|
712 |
|
713 「簡易代表読み照合」は次のようにして行います。sortDaihyoメソッドの受け取るリストの各要素は、[ 表記列, 読み列 ]という配列リファレンスでなければなりません。 |
|
714 サンプルコード |
|
715 |
|
716 #!perl |
|
717 use ShiftJIS::Collate; |
|
718 |
|
719 my @data = ( |
|
720 [qw/ λ計算 らむだけいさん /], |
|
721 [qw/ JIS番号 じすばんごう /], |
|
722 [qw/ 安達 あだち /], |
|
723 [qw/ 安藤 あんどう /], |
|
724 [qw/ 河西 かさい /], |
|
725 [qw/ 河内 かわち /], |
|
726 [qw/ 角田 かくた /], |
|
727 [qw/ 角田 かどた /], |
|
728 [qw/ 如月 きさらぎ /], |
|
729 [qw/ 河内 こうち /], |
|
730 [qw/ 幸山 こうやま /], |
|
731 [qw/ 幸山 さきやま /], |
|
732 [qw/ 佐藤 さとう /], |
|
733 [qw/ 佐和田 さわだ /], |
|
734 [qw/ 沢島 さわしま /], |
|
735 [qw/ 沢田 さわだ /], |
|
736 [qw/ 澤田 さわだ /], |
|
737 [qw/ 角田 つのだ /], |
|
738 [qw/ 槌井 つちい /], |
|
739 [qw/ 土井 つちい /], |
|
740 [qw/ 土居 つちい /], |
|
741 [qw/ 戸井 とい /], |
|
742 [qw/ 戸田 とだ /], |
|
743 [qw/ 土井 どい /], |
|
744 [qw/ 土居 どい /], |
|
745 [qw/ 土岐 とき /], |
|
746 [qw/ 安田 やすだ /], |
|
747 ); |
|
748 |
|
749 @sort = ShiftJIS::Collate->new()->sortDaihyo(@data); |
|
750 |
|
751 |
|
752 Shift-JISの漢字を含むファイル名/パス名 |
|
753 本項目は、他の項目に増して、検討不充分のまま記述していますので、もし何か参考にしようと思った場合、十分に注意の上、納得できるまでご自分の作業環境でテストしてください。 |
|
754 Windows (95/98/NT/2000など) で、ファイル名やパス名が漢字(ここでは二バイト文字の意味で使っていますので、平仮名や記号なども含みます。)を含む場合、Perlで扱う際に問題が生じる可能性があります。 |
|
755 末尾バイトが "\x5C" の漢字をもつファイル名/パス名 |
|
756 ディレクトリ操作関数(mkdir, rmdir, opendir, -d など)、ファイル操作関数(open, unlink, -f など)で、アクセスできないことがあります。 |
|
757 ファイルの場合は、末尾に半角スペースを添えるとアクセスできる場合があります(例えば、-f '表 ' または -f "\x95\x5C\x20" など)。 |
|
758 ディレクトリの場合は、末尾に / か \ を添えるとアクセスできる場合があります(例えば、-d '表/' または -d "\x95\x5C/" など)。末尾に添える文字を半角スペースとしても、うまくアクセスできる場合があります。添える文字の候補として、三種類の文字(スラッシュ、円記号、空白)を挙げましたが、どの文字がよいかは、関数によって異なる場合があるようです。使用する前に十分にテストしてください。 |
|
759 なお、ディレクトリ名の末尾に / か \ を添える場合、もともと末尾に / か \ が付いている場合には、二重に付けるとうまく行かないおそれがありますので、文字列連結の前に検査したほうがよいでしょう。 |
|
760 どうしても挙動が不明で信頼できない場合は、`` または qx// や system()関数などを通じてWindowsのコマンドを呼ぶのが良いと思います。 |
|
761 おまけ |
|
762 Shift-JIS で書かれた POD を Perl 5.8.1, 5.8.2 の Pod::Html で HTML に変換した場合、アンカーの名前は、 英数字と仮名文字 [0xA6..0xDF] を除き、 他の各バイトは下線('_')に変換されるようです。 具体的には、use locale; 下で、lc と s/\W/_/g を実行した結果 (cf. Pod::Html::anchorify) になります。 |
|
763 [2003-11-18] |
|
764 Perlのページ |