2014年3月2日日曜日

FreeBSD 上での Perl の UTF-8 の文字化けではまる

 久々に FreeBSD ではまってしまった。 何にハマったかというと,Perl の UTF-8 テキスト文字列の取扱いでハマった。 Perl + cgi と apache 2.2 で web を運用し,かつ,PostgreSQL で運用しているデータベースの情報を web に載せている。 その際,Perl 内部での日本語文字列(より正確には UTF-8 の文字列)の encoding というので文字化けが発生してしまい,頭が混乱してしまった。 今回はこの件について書こう。
 先に最終的に今回私が採用した解決方法を書いておこう。 私が採用した解決方法は,Perl スクリプトの先頭付近に
use utf8;                            # use utf8 in Perl
use open ':encoding(UTF-8)';         # input/output default encoding is UTF-8
use open ':std';                     # STDIN, STDOUT, STDERR is set equal to "use open ':encoding(UTF-8)';"
という記述を書き,cgi の form でやりとりした文字列に対しては,
$input_string = Encode::decode('utf8', $input_string);
のようにして,文字列のエンコーディングを変換する,という作戦である。

1. システム構成
 まず,今回の件の動作環境について書いておこう。
私は,個人的なサイトのために FreeBSD でサーバーを運用している。 サーバーと言っても,マシン自体はちょっと古くなった Windows の Note PC を格下げして使っているのだが…。 サーバー上では,現在 FreeBSD release 9.0 を動かしている。 その上で,apache 2.2 で web site を立ち上げている。 web site は,主に cgi を使って運用している。 cgi を使うには Perl が必要なのだが,FreeBSD では「当然」Perl は最初から入っている。 現在の Perl のバージョンは,5.18 にしている。 さらに,データの管理のためにデータベースを運用している。 データベース用のアプリケーションは PostgreSQL 9.2.7 を運用している(今回の件で 9.3.3 にあげたが…)。

2. 今回の UTF-8 文字化けトラブルとその経緯
 さて,いよいよ今回のトラブルについて書こう。 データベースのデータの書き換えのために,cgi を使ったスクリプトを用意している。 データベースのデータは,psql などのコマンドライン上で使えるアプリケーションで読み出したり,書き換えたりできるのだが, よく WHERE 句を忘れて全てのデータ列に同じ値を書き込んでしまう,などのミスをやらかしてしまう事がある。 それを避けるためにスクリプトを作り,そのスクリプトを通じてデータの書き換えを行うことにしている。 その際,自動で変更履歴を残すようにしている。 やり方は,データテーブルの項目の一つとして更新履歴入力用の項目を用意しておいて,他のいずれかの項目に変更があった場合に更新履歴を自動で書き込ませる,という方法を使っている。 それをデータベース変更用の Perl スクリプトにやらせているのだが,今回,データの更新を行うと,何故か文字化けが発生してしまった。 一月ほど前にデータの更新をした際には起こらなかったのに…。 データベースの該当データ列に文字化けが起こっていた。 どうも文字化けしたデータをデータベースに書き込んだみたいだった。

 そこで,原因を調べてみたのが,今回の話である。

3. 原因の探索
 原因の前に,少しデータ更新用スクリプトについて書こう。 スクリプトでは,データ項目のうちの幾つかを選んで,各項目が1行に入るように一覧形式で表示させ,そこから処理したいデータ列番号を選んで,データ更新画面に移る。 データ更新画面では,もう一度データベースから現在のデータ列の項目を全て読み出し,用意してある form の input エリアにデフォルト値として表示しておく。 そして,変更したい項目の内容を書き換えてから更新ボタンを押すと,該当列のデータ項目すべてを form で飛ばして最終処理スクリプトに読み込ませる。 ここで,もう一度該当項列の現在のデータ項目を読みだして,form で飛ばした内容と比較して,変化があれば変更があったと考えて変更履歴に記載すると共に, データベースの該当項目を新しいものに書き換える,というものである。

 こう書いても多分ほとんど理解してもらえないと思う…。 要は,Perl+cgi を使い,PostgreSQL を使ったデータベースとやりとりし,web で入力した情報を form で cgi でやりとりしている,ということである。 このような状況の元で発生した文字化けの原因を考えてみた。

 まず,問題のデータ更新用の Perl スクリプト自体をいじって文字化けが発生したわけではないので,問題はアプリケーションを含むシステム側にあると考えた。 そこで,最初に考えたのが過去に行った PostgreSQL のバージョンアップに伴うトラブルである。 私はある程度頻繁にアプリケーションのアップデートを行っている。 FreeBSD の ports システムを使っているのだが,このアップデートを行うと時々トラブルに見舞われる。 アプリケーションのバージョンが上がると,たまに,これまで使えていた関数が使えなくなったり,default 設定が変更されたりする。 その結果 web server や database がうまく動かなくなることがある。 今回も PostgreSQL のバージョンがあがったのが原因かもしれない,と最初に考えたのだった。

 そこで取った対策が ports の UPDATING の情報を確認することだった。 通常,アプリケーションの変更に関する情報は /usr/ports/UPDATING に書かれる。 Perl などのアプリケーションがメジャーバージョンアップデートされる場合などで要注意なのだが,その場合はこの UPDATING に何らかの記載がある。 そこで,今回もまずは UPDATING の記載を見てみた。しかし,原因と思われる記載はなかった。

 次の作戦は PostgreSQL の最新版へのバージョンアップだった。 このトラブルが起こった段階の PostgreSQL は 9.2.7 だった。 しかし,ports を見ると PostgreSQL のバージョン 9.3 が登録されていた(詳しくはバージョン 9.3.3)。 もし何らかのバグが原因となってるとしたら,バージョンを最新版にすると直ることがないこともないので…(あまり期待はしていなかったが…)。 そこで,FreeBSD の ports を使って PostgreSQL をバージョン 9.2.7 から 9.3.3 にあげてみた。 これはメジャーバージョンのアップデートになるので,少し手間がかかった(PostgreSQL のメジャーアップデートについては (FreeBSD 上での)PostgreSQL のメジャーアップデートの仕方のメモに書いておいた)。 しかし,これも解決には至らなかった。

 次に考えたのが Perl 絡みのトラブルだった。 こちらもメジャーバージョンアップがなされると,よくトラブルを生むアプリケーションなので。 しかし,Perl は半年ぐらい前にメジャーバージョンのアップデートをした後は大きな変更はなされていない。 どうも Perl のアップデートが問題はなさそうだと考えた(これが間違っていたのだが…)。

 そうなると何が問題かがわからなくなってしまった。 cgi がらみで何かあるかもしれないが,それって Perl のシステムに組み込まれているし…。 ただ,データ更新用の Perl スクリプトで,文字化けが起こった print 文をいじると文字化けが直ることがあった。 具体的には,文字列の結合をやめると文字化けが直ることがあった。 print を短くするために,
print "(".$str.")"
のように,変数と全角(この言い方はいいんだっけ?)を結合させたものを print させていた。 それを
print "(";
print $str;
print")";
のようにバラバラにすると文字化けしない事があった。 さらに,全角の「(」をやめて,半角の「 (」(スペース+カッコ)とすると,文字化けしない事もあった。 しかし,根本的な原因まではわからずに途方にくれたまま,丸2日ほど経ってしまった。

4. トラブルの原因
 この問題の原因は意外なところからわかった。 今回のトラブルは web site 上で起こったことなのだが,web とは全く別の Perl スクリプトでもエラーが発生した。 それは,同じデータベースから情報を取り出して,メールを送信する,というスクリプトでの事だった。 いつものようにメールを送信しようと思ったら
Wide character in print at example.pl line 12
のようなエラーが表示された。 このエラーメッセージを見た瞬間は「なんやこれ?」と思ったが,このおかげでやっとトラブルが起こる原因が判明した。 エラーメッセージを検索すると,原因は Perl の日本語文字列の処理にあるらしかった。

 比較的新しい Perl では,日本語などの文字列は UTF-8 で取り扱っているらしい。 そのため,私は Perl スクリプトを全て utf8 で書くようにしている。 また,PostgreSQL を使っているデータベースも,unicode を使うように指定して構築している。 Perl も PostgreSQL も UTF-8 を使うようにしたので,これまでは特にトラブルは発生していなかった。 しかし,何らかの原因で UTF-8 がらみのトラブルとして顕在化したみたいだった。 考えれる原因は,/usr/ports/UPDATING に記載のあった,
20131120:
  AFFECTS: users of lang/perl5.12 lang/perl5.14 lang/perl5.16 and lang/perl5.18
  AUTHOR: mat@FreeBSD.org

  The THREADS option has been enabled by default in all Perl. If you're using
  binary packages you need to do :
  ........
というのに対する対応を行ったかもしれないが,ちゃんとした原因はわかっていない。 多分,原因は上記への対応の際に,何かの初期設定が変わったみたいだった。

5. トラブルへの対策
 さて,ここからがやっと今回のトラブルの原因とその対策である。 まず,Perl での日本語等の文字列の取扱いについて書こう。 Perl で日本語等の文字列(いわゆる全角文字)を扱うには,以前から unix 系は EUC-JP という文字コードを使っていた。 しかし,EUC-JP は日本がだけしか扱えないし,Windows や Macintosh などで使われいてる Shift-JIS と異なるので, いちいち変換が必要だった。 そもそも1バイトで表現できる文字数が最大 255 しかないので,いろいろな言語の文字を表そうと思うと, 最低でも2バイトを使って記述しないといけない。 そこで,いろいろな言語の文字も表示できる文字コードとして unicode なるものが提案され, unix や windows,Macintosh などの主要な OS で標準となった。 FreeBSD では結構以前から UTF-8 に代表される unicode が使えるようになっていた。 (unicode には UTF-16 などいくつかの種類があるらしいが,詳しいことは知らない…)

 Perl でも以前は EUC-JP 等が使われていたみたいだが,バージョン 5.8 以降ぐらいから,UTF-8 を扱うようになった。 しかし,ここで1個問題がある。 Perl の内部では,通常の UTF-8 の文字コードにフラグを付加したものを使っている。 Perl の文章だと,内部 unicode encoding と書かれている。 つまり,Perl 内部ではいわゆる UTF-8 という文字コードそのものを扱っているわけではないのである。 その理由は,文字列の長さなどの関数等で unicode と byte 文字列で扱いが変わるから,みたいである(あくまで私の推測)。 いずれにせよ,FreeBSD で普通に使っている UTF-8 の文字列を Perl に取り込んでもうまくいかないことがある,ということである。 Perl では,ある程度自動で判断して,フラグなしの UTF-8 があれば,勝手に内部用のフラグ付き UTF-8 に変換してくれるみたいである。 しかし,その辺りの動作がマイナーバージョンアップで変わってしまったみたいだった。

 そこで,この文字化けトラブルに対する対策だが,今回採用したものは,
1. Perl 内部では,フラグ付きの UTF-8 を必ず使う。
2. Perl 外部とのやりとりで,フラグなしの UTF-8 との変換を行う
という作戦である。 しかし,これだけだと,何をどうしてどうすればいいかわからないので,具体的な事を記載しよう。

 まず,Perl 内部では,フラグ付きの UTF-8 を必ず使うという部分だが,以下の様な文を Perl スクリプトの先頭に書いた。
use utf8;
これは,Perl に対して,内部では全てフラグ付きの UTF-8 で処理をしなさい,と宣言しているものらしい。 ネットを検索するといろいろな事が書かれていて,何が正しいのかよくわからないのだが,Perl 関連の文章を見ると, どうもこの文を入れるのがよさげだった。

 問題はPerl 外部とのやりとりで,フラグなしの UTF-8 との変換を行うという部分だった。 ネットで検索すると,個別にはEncode::decode('utf8',$input_string)を使って内部コードに変換し, Encode::encode('utf8',$input_string)を使って外部形式(普通の UTF-8)に変換すればいい,らしい。 (encode と decode は Perl 側から見た表現なので,decode がフラグ付き UTF-8 への変換,encode がフラグなし UTF-8 への変換となっている) 例えば,print 文では print Encode::encode('utf8',$input_string) とすればいいらしい。 しかし,これまで作ったスクリプトの全てで,print 文を全て書き換えるのは無理だった。 それに,Perl の外部とのやりとり(いわゆる I/O,あるいは interface)での問題なので,なんらかの手があると思って調べてみた。
 どうやら,必要なのは
・ファイル等とのやりとり
・標準入出力とのやりとり
・データベース等とのやりとり
・form でのデータの受け渡し
での,エンコーディングの変換みたいであった。

 ファイルの入出力等に対しては,Perl スクリプトの先頭付近に,
use open ':encoding(UTF-8)';         # input/output default encoding is UTF-8
と書けばいいみたいだった。 ここで,encoding の引数が UTF-8 とハイフン付きになっているが,utf8 とハイフンなしでもいけるみたい。 しかし,どちらかというと UTF-8 の方が正式な表現みたいなので,UTF-8 を採用してみた。

 標準入出力とのやりとりに対しては,上記の use open ':encoding(UTF-8)';直後
use open ':std';                     # STDIN, STDOUT, STDERR is set equal to "use open ':encoding(UTF-8)';"
と書けばいいみたい。これもPerl 関連の文章に書いてあった。
binmode(STDOUT, ":encoding(UTF-8)"); # standard output default encodeing is UTF-8
binmode(STDIN,  ":encoding(UTF-8)"); # standard input default encodeing is UTF-8
binmode(STDERR, ":encoding(UTF-8)"); # standard error output default encodeing is UTF-8
としてもいいみたいだが,use open ':std'; の方がスマートっぽいので採用してみた。

 データベースとのやりとりでは, としてデータベース・サーバーとつなぐ際に
$self->dbh->{pg_enable_utf8} = 1;         # UTF8 => フラグ付きraw
としてやればいい,という事がネット上に書かれていたが,私の例ではその必要がなかった。 どこでうまく行っているかまでは見てないが,どうも標準入出力の指定あたりにあるのではないだろうか?

 上記の対策だけで,かなり部分の文字化けは解消された。 しかし,まだうまくいかなかった。 どうやら,form を使ったデータのやり取りをすると(特にデータの受け取り)文字化けするみたいだった。 そこで,form で日本語文字列を受け渡しする際には,個別に
$input_string = Encode::decode('utf8', $input_string);
のようにすることにした。 私のスクリプトでは,form で文字列を受け取った際には,「$」記号など制御文字を取り除く「エスケープ処理」を施している。 今回はそのサブルーチンに,エンコーディングの処理を付加しておいた。

 今回のトラブルの原因は意外と根が深いものだった。 あまり理解しきれずに使っているだけでは,簡単には解決しないトラブルだった。 この投稿が少しでも皆さんの役に立てば私としては嬉しい限りである。
参考にしたサイト
Perl における unicode の紹介英語版
Perl における unicode について英語版
Perl における正規表現のチュートリアル英語版
Perl における utf8 プラグマ英語版
Perl における open プラグマ英語版
Perlのuse utf8のメモ - CGIのutf-8改造で文字化けしたときの処方箋:参考にさせていただいたサイト
perl、HTML::Template、postgresでUTF8を使う:参考にさせていただいたサイト

0 件のコメント: