まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2015/05/20(水)Perl DBI と UTF8フラグ と 文字化け問題 (2017/05/31追記)

Perl 5.20にしたら、DBIが何やら不可解な動作をするようになりました。

問題の原因

どうやら UTF8 を扱う機能が増えたみたいです。DBIの接続時のパラメーターにこんなものが増えています。

pg_enable_utf8 => 1 , # 結果をUTF8フラグ付きにする(PostgreSQL)

mysql_enable_utf8 => 1 , # 結果をUTF8フラグ付きにする(MySQL)

PerlのDBIモジュールで自動的にUTF8フラグを付ける

この影響で、PostgreSQL運用のシステムを Perl 5.14 から 5.20 にアップグレードしたところ文字化け発生。また厄介なことをしてくれたものです(汗)

保存されいてる文字コードがおかしい

普通に使っているのに「Wide character in print at」の警告がなぜか出まくります。日本語UTF-8として保存した文字列が文字化けしまくるのですが、そのデータだけ単独に取り出して表示しても化けない。

色々調べてみると以下のことがわかりました。

  • 新DBD は(utf8フラグのついていない)文字列を DB に保存するとき「ASCII文字列に特殊な文字が混ざってる」と解釈してエスケープ処理を行う。
  • この文字列は、通常の utf8 文字列ではないので、データベースの中身を直接覗くと、保存した文字列ではなく特殊なエスケープ文字列として表示される。

PostgreSQLではこんな感じです。

  • 文字列「あいう」を保存。バイナリ列で「E3 83 86 E3 82 B9 E3 83 88」
  • psqlコンソールで表示される文字列「a\u0083\u0086a\u00821a\u0083\u0088」
  • DBI経由で取り出し、utf8フラグを取り除いた文字列「C3 A3 C2 83 C2 86 C3 A3 C2 82 C2 B9 C3 A3 C2 83 C2 88」

同じUTF8文字列でありながら、違う内部表現になっています。当然、元の「あいう」と文字列比較を行うと異なると判定されます。

MySQLではこんな感じです。

  • mysqlコンソールで出力される文字列「C3 A3 C2 81 E2 80 9A C3 A3 C2 81 E2 80 9E C3 A3 C2 81 E2 80 A0」
  • DBI経由で取り出した文字列は utf8フラグ がついていない元の文字列。

問題を整理すると2つに集約できます。

  • DBにきちんとしたUTF8文字列として保存されない。
  • DBに一回保存することで、内部表現の異なる UTF-8 文字列が生成されてしまう(PostgreSQL)。

後者も問題と言えば問題ですが、前者はとても気持ち悪い問題です。

解決策

utf8フラグはうまく使えば「Perlによきに計らってもらい」文字コードの問題が楽になるものですが、utf8文字列をutf8として「そのまま扱いたい」時には問題が多すぎる仕組みです。入出力の都度にencode、decodeが発生し処理を重くする原因にもなります。

またutf8フラグありとフラグなしのUTF8文字列が混在すると、それらを連結したときに文字化けする危険があります。

そうなるとutf8フラグなしで扱いたいのですが、そうするためにはどうしたらいいのでしょうか。

DBD::Pg編

  • utf8フラグのない文字列は、必要に応じて特殊エンコードされてDBに保存される。
  • 「pg_enable_utf8」を設定しないとき。
    • 非ASCII文字列や、特殊エンコードされた文字列は utf8 フラグが付いて取り出される。
  • 「pg_enable_utf8 => 1」のとき。
    • 「pg_enable_utf8」を設定しないときと同様。
  • 「pg_enable_utf8 => 0」のとき。
    • いかなる場合も utf8 フラグは付けなくなる。
    • 特殊エンコードされた文字列は文字化けする。

保存時の挙動。

  • DBD::Pg / Version 3.3.0 to 3.5.3
    • utf8フラグを付けた文字列を保存すれば、そのままutf8文字列としてDBに保存される。
    • utf8フラグの付いていな文字列は特殊エンコードされる。
  • DBD::Pg / Version 3.6.0以降
    • 「pg_enable_utf8 => 0」のとき、utf8フラグの付いた文字列を渡すとエラーになる。
    • 「pg_enable_utf8 => 1」のとき、utf8フラグの付いていない文字列を渡すと特殊エンコードされる。

取り出す時用に「pg_enable_utf8 => 0」を設定して、保存時や「検索時」にutf8フラグを付けるのがよさそうです。

追記参照のこと。

DBD::MySQL

  • DB保存時はutf8フラグの有無はすべて無視する(動作は変化しない)。
  • 「mysql_enable_utf8」を設定しないとき。
    • いかなる場合もutf8フラグは付かない。
    • 非ASCII文字列は特殊エンコードされたDBに保存される。
    • 特殊エンコード文字列は、通常の utf8 文字列で取り出される。
    • DBに格納されている通常の utf8 文字列は、エンコード失敗の「?」に置換されDBから取り出される。
  • 「mysql_enable_utf8 => 0」のとき。
    • 「mysql_enable_utf8」を設定しないときと同様。
  • 「mysql_enable_utf8 => 1」のとき。
    • utf8文字列は、通常の utf8 文字列としてDBに保存される。
    • 非ASCII文字列は utf8 フラグが付いて取り出される。
    • 特殊エンコードされた文字列は文字化けする。

DBD::Pgとは実装がまるで違う事がわかります。

ドキュメントを読んだところ「SET NAMES utf8;」というSQL文を発行することで、同じ効果を得て、かつutf8フラグに影響がでないことがわかりました。ただこの状態では、特殊エンコードされた文字列を読み込むときに文字化けするので注意が必要です。

比べると

DBD::Pgの実装のほうが謎かもしれない。DBD::Pgはトランザクション処理がおかしいという素敵な実績があるからなあ……。「pg_enable_utf8 => 0」のとき保存データのutf8フラグも無視しないと矛盾するのはすぐに分かりそうなもんなんだけど。

DBIの仕様

Perl supports two kinds of strings: Unicode (utf8 internally) and non-Unicode (defaults to iso-8859-1 if forced to assume an encoding). Drivers should accept both kinds of strings and, if required, convert them to the character set of the database being used. Similarly, when fetching from the database character data that isn't iso-8859-1 the driver should convert it into utf8.

http://search.cpan.org/~timb/DBI-1.633/DBI.pm

Perl は Unicode(内部表現utf8)と 非Unicode(iso-8859-1)の2つの文字コードをサポートします。DBDドライバはこの2つの文字コードを受け付けるべきで、必要に応じて database で使用可能な文字列に変換する必要があります。

同様に、iso-8859-1ではない文字列は database から取り出す段階でutf8に変換すべきです。

DBD::Pgの仕様変更 2017/05/30

仕様変更というよりBug Fixされ「CHANGES」を読むと、DBD Version 3.6.0あたりから挙動が異なる(正しい挙動に近くなっている)ようです。

しかし、この変更のせいで「pg_enable_utf8 => 0」のときutf8フラグ付の文字列を渡すと「Wide character in subroutine entry」エラーが発生します。

ですので「pg_enable_utf8 => 0」のときは、$DBD::Pg::VERSIONを参照し「3.3.0~3.5.3の間の時だけUTF8フラグをつけてDBIに渡す」必要があります。

クソバグを仕込んだ上に、互換性考えない中途半端なクソ修正をして、そびえ立つクソ状態なDBD::Pgには呆れるばかり……。

まとめ

  • 今どき文字コード問題、しかも UTF8 で悩むとは思わなかった。
  • 1byte文字圏の UTF8 実装は相変わらずクソ。

2015/01/08(木)pure perl で JPEG の Exif を表示や削除したい

機能

  • 対応はJPEGファイルのみ。
  • 外部モジュール不要。
  • 再圧縮せずにExif情報を削除することができます。
  • Exif判別やExif表示はやや手抜きですが、よほど特殊なJPEGでない限り問題にならないと思います。
  • ライセンスはWTFPLですが、一言「使ったよ」ってコメントくれると嬉しいです。

バグがあったら具体的なJPEGデータと共にコメントください。

スクリプト

続きを読む

2014/08/28(木)Windows上での flock の不具合?

これだからWindowsは……。

ロック状態を変更できない

use Fcntl;

my $fh;
sysopen($fh, "test.txt", O_CREAT | O_RDWR) or die;
flock($fh, Fcntl::LOCK_SH());
flock($fh, Fcntl::LOCK_EX());
close($fh);

Linuxなどでは、共有ロックが排他ロックに切り替わるのですが、Windows上では切り替えることができず*1、そのままブロッキングされてしまいます。

*1 : 自分自身で獲得した共有ロックが、排他ロックを獲得する際の障害になっている

共有ロック状態でtruncateすると、ファイルサイズが固定される

use Fcntl;

my $fh;
sysopen($fh, "test.txt", O_CREAT | O_RDWR) or die;
flock($fh, Fcntl::LOCK_SH());

truncate($fh, 0);		# ファイルサイズを 0 に
seek($fh, 0, 0);		# ファイルポインタを先頭へ
print $fh "test!!\n";
close($fh);

こんなテストプログラムを走らせます。LOCK_SH以外は、ロックを伴うファイル操作ではよく行う処理です。

LOCK_SH(共有ロック)は本来ならLOCK_EX(排他ロック)ですが、前項で述べたとおりWindows環境ではとりあえず共有ロックで開いておいて後から排他ロックに変更できないので、そのような状況を想定してLOCK_SHで記述してあります。

さてこのプログラムを実行すると、Windows上では0byteのファイルが作成されます。ActivePerl(5.14)の問題かもしれませんが、truncateでのファイルサイズ指定がそのままclose()後のファイルサイズになります。

不思議なことに、LOCK_EX(排他ロック)に変更するときちんと中身のあるファイルが作られます。

解決策

use Fcntl;

my $fh;
sysopen($fh, "test.txt", O_CREAT | O_RDWR) or die;
flock($fh, Fcntl::LOCK_SH());

seek($fh, 0, 0);		# ファイルポインタを先頭へ
print $fh "test!!\n";

truncate($fh, tell($fh));	# 現在の位置でファイル切り詰め
close($fh);

挙動まとめ 2017/02/26

  • Windowsでは一度ロックしたものに対し、再度ロックすることができない。
    • 同じファイルハンドルを使用しても「別のロック」とみなされてしまう。
  • Linux等では同じファイルハンドルを使えば、lockは何度でもできる(状態変更できる)。

別の問題 2015/01/16

そもそもLOCK_SH(共有ロック)状態で書き換えようとすると、以前のサイズよりも大きくならず、そもそも書き変えられないという謎のバグに当たりました。

詳細な発生条件は不明ですが、めんどうくさいので、Windowsの場合共有ロックではなく最初から排他ロックをかけることにしました。

2013/08/02(金)perlでExporterを実装してみた

軽量なExporter。

  • 動作するのは @EXPORT と @EXPORT_OK
  • タグによる条件指定など複雑なエクスポートはできない

通常の使用には十分です。ただ標準の Exporter も、EXPORT変数に$や&などを付けず名前だけ使用すれば十分高速なのでボツになりました(苦笑)*1

perl の Export の原理を学ぶには良い題材だと思います。

続きを読む

2013/06/13(木)JSONを自力でパースするperlスクリプト【pure-Perl】

正しいもののみ正しくパースされるスクリプトです。

  • 外部モジュール不要。
  • 正しくないものは解析できるところまで動きます。
  • かっこ「{[」と「]}」の対応は見てません(閉じかっこの入れ違えがあっても動きます)
  • ライセンスはWTFPLですが、一言「使ったよ」ってコメントくれると嬉しいです。

バグがあったら具体的なデータと共にコメントください。

続きを読む