Windows環境でUnicodeファイルを扱うB!

こんにちわ!クリスマスを過ぎるとニートになることが決まっている xaicron です!

ハッカーでもなんでもないのですが、勢いに任せて登録してみました!!すいません><

はじめに

今日は、Windows環境以外の人は何にも関係ない、モジュール Win32::Unicode をご紹介しようとおもいます。

しかもまた文字コード関連の話ですね!!にEncodeでラクラク日本語処理を読んでから詠むと、より理解が深まるかもしれません。

Win32::Unicodeって?

Windows で perl を使う場合の選択肢としては ActivePerl と Strawberry Perl の二種類があげられます。

どちらも Windows 用にカスタマイズされたものなのですが、悲しいことにどちらの Perlを使っても Unicode 文字の含まれたファイルが扱えません。

でもそれだと悲しいので、Unicode なファイルも扱えるように書いてみたというわけです。

Unicode 文字の含まれたファイルはやっかい

Windowsでは1つのファイルに対して上記3種類のものが扱えます。

  • cp932
  • 8.3形式 (PROGRA~1とか)
  • Unicode (UTF-16LE)

基本的に Perl は一番最初の cp932 としてファイル名を扱うようになっています。しかし、Windowsのマルチバイト環境では、Unicode のファイルが許されています。

しかし、Unicode 文字があるファイルは「?」となってしまい、Windows のシステムからすればそんなファイルはないので読み取れません。

iTunesとかで海外の曲を買ってると、知らず知らずのうちの、 Unicode ファイルができている、何てこともあります。

ためしに、「明日の天気は☃.txt」というファイルが含まれているディレクトリのリストを表示してみましょう。

>perl
opendir my $dh, '.' or die $!;
print join "\n", readdir $dh;
closedir $dh;
__END__
.
..
今日の天気は?.txt

「&;#9731;」は cp932 に存在しない文字なので「?」に変換されてしまいました。

もちろん、 Perl プログラムに cp932 で埋め込むことはできません。 UTF-16LE で書いたところで、Perl自体がまともに動かないのでそれも不可能です。

Win32::Unicode を使う

さて、それではどうすれば Unincode ファイルを扱えるのでしょうか?実は Windows 自体には Unicode ファイルを扱う API が用意されているのです。

そいつを使ってやれば、ちゃんと Unicode ファイルの操作ができます。Window の API を Perl からたたくのは、 Win32::API を使えば実現できます。

でも、こいつは Perl の書き方とはかけ離れているので、日常的に使うにはとってもめんどくさいです。

Win32::Unicode は Perl の書き方とほぼ同じように Unicode ファイルを扱えるように Win32::API をラップしたモジュールです。

use すると、大量の関数をエクスポートするのが特徴です。しかも、デフォルトでは、COREの関数をoverrideしないように、ほとんどのものに「W」というのがついています。

でもそれだとダサいので、最後のほうで「Win32::Unicode::Native」っていうのを紹介します。

Win32::Unicode では、すべての関数で flagged utf8 を使うことにしているので、モダンな気持ちを味わいながら書くことができると思います。

Win32::Unicode::File;

Unicode 文字のファイルの読み書きができる IO::File っぽいインターフェースを持ったモジュールです。

ファイルの読み書きだけでなく、 rename、copy、unlink などもあります。

それでは、さっき開けなかった「明日の天気は☃.txt」に何か書き込んで見ましょう。

use strict;
use warnings;
use utf8;
use Win32::Unicode::File;

my $fh = Win32::Unicode::File->new;
open $fh, '>', "明日の天気は\x{2603}.txt";
binmode $fh, ':encoding(cp932)';
print $fh "こんにちわ!こんにちわ!\n";
close $fh;

とっても簡単ですね!!もちろん読み込むこともできます。

Win32::Unicode では-X 演算子を PP で実装する方法がわからなかったので、「file_type」という関数を使わなくてはなりません。さらに、ファイルサイズを取得する場合は、-s ではなく file_size を使用します。

そのかわり、Windows 固有のファイル属性の判定ができ、なおかつ、重ね打ちも可能です。

このあたり、何かいい手段があれば教えていただけると幸いです。

if (file_type rhf => $file_name) {
    # (読み込み専用で隠し属性のファイル)
}

my $size = file_size $file_name # -s $file_name と等価

あと、まだstatとかflockとかが未実装だったり、4GB以上のファイルがたぶん扱えなかったりしますが、そのうち実装されるんじゃないかと思います。

Win32::Unicode::Dir;

次はディレクトリ周りです。使いかたは Win32::Unicode::File と大体一緒です。

こっちにも find、mkpath、cptree、mvtree など、よく使う関数も一緒にエクスポートします。

use strict;
use warnings;
use utf8;
use Win32::Unicode::Dir;

my $dh = Win32::Unicode::Dir->new;
$dh->open('.');
print join "\n", $dh->readdir;
$dh->close;

こんな感じで使います。

でもこれを実行すると、

Wide character in print at readdir.pl line 8.
.
..
譏取律縺ョ螟ゥ豌励・笘・txt

とかいって文字化けしてしまいます。

これは、readdir で帰ってくる文字が、 flagged utf8 だからなので当然といえば当然です。

でも、

use strict;
use warnings;
use utf8;
use Win32::Unicode::Dir;

binmode STDOUT => ':encoding(cp932)'; # 追加

my $dh = Win32::Unicode::Dir->new;
$dh->open('.');
print join "\n", $dh->readdir;
$dh->close;

としたところで、

.
..
"\x{2603}" does not map to cp932.
明日の天気は\x{2603}.txt

と、怒られてしまいます。

なんだかここまでくれば、コマンドプロンプトにも「明日の天気は☃.txt」って表示してほしいなーと思いました。

で、実はこのファイルがあるディレクトリで「dir」するとなんと普通に「明日の天気は☃.txt」って表示されるんです!!

ということは、コマンドプロンプトにも Unicode を表示する API があるということですね。

Win32::Unicode::Console

なので、つくりました。

Win32::Unicode::Console はコマンドプロンプトに Unicode 文字を表示できます。

printW printfW sayW dieW warnW という関数がエクスポートされます。

これらはすべて、 flagged utf8 を受け取ります。

これをつかってさっきのやつを書き換えると

use strict;
use warnings;
use utf8;
use Win32::Unicode::Dir;
use Win32::Unicode::Console;

binmode STDOUT => ':utf8'; # リダイレクトのため

my $dh = Win32::Unicode::Dir->new;
$dh->open('.');
printW join "\n", $dh->readdir;
$dh->close;
.
..
明日の天気は☃.txt

おおお!無事に表示されましたね!!

ちなみに、リダイレクト時には、通常の print が使用されるため、binmode しておいたほうがいいでしょう。

Win32::Unicode::Error

次はエラーメッセージです。とはいっても、これがエクスポートするのは errorW のみです。

位置づけとしては、「$!」の代わりに使うという感じです。

これまた、$! を書き換える方法がわからなかったので、仕方なく という感じです。($! にはエラーコードを示す数値のみ代入でき、文字列を入れることはできない。)

メッセージはWindow API のエラーメッセージとなります。マルチバイト環境では日本語が返ってくるので、dieW errorW という風に使います。

use strict;
use warnings;
use utf8;
use Win32::Unicode::Error;
use Win32::Unicode::Console;
use Win32::Unicode::File;

my $fh = Win32::Unicode::File->new;
open $fh, '<', '存在しないファイル' or dieW errorW;
print <$fh>;
close $fh;

これを実行すると、

指定されたファイルが見つかりません。 at errorW.pl line 8

みたいになります。

Win32::Unicode

ここまで、4つぐらいのモジュールが登場しましたが、毎回全部 use するのはめんどくさいので、 use Win32::Unicode します。

これで、今までのモジュールがすべでロードされ、さらにそれぞれの関数もすべてエクスポートされます。

Win32::Unicode::Native

でも、ここまで書いておいていうのもあれですが、正直「W」とないわー。

しかもファイルオープンとかも new しないいけないしめんどいなー。

どうにかならんかなー。ってことで、 Win32::Unicode::Native です。

こいつをuse すると標準関数を override します。とはいっても、CORE::GLOBAL::* を書き換えるわけではなく、単純に同名の関数をエクスポートして上書きします。

use した範囲だけ適用されるので、ほかのモジュールなどには影響しません。

唯一の例外が、 STDOUT です。これだけは、 Win32::Unicode::Console::Tie というモジュールに tie しているため、すべてに影響します。

use strict;
use warnings;
use utf8;
use Win32::Unicode::Native;

print "flagged utf8な文字も普通にいける";

# Win32::Unicode::File->newとかしない
open my $fh, '>', '森鷗外.txt' or die error;
print $fh "ほげほげ";
close $fh;

# Win32::Unicode::Dir->newとかしない
opendir my $dh, "\x{2600}" or die error;
say join "\n", readdir $dh;
close $dh;

ほとんど Perl の記法と一緒ですね!!

ちなみに、use Win32::Unicode '-native'としてもOKです。

Win32::Unicodeは遅い

Win32::Unicode は普通に遅いです。

Perl がネイティブにやってくれるところをわざわざ PP で再実装しているので当然なのですが。

Unicode のファイルをPerlで扱う必要がまったくない場合は、使わないことをお勧めします。

また、当然のことながら(いないとはおもいますが・・・)モジュールなどには使わないほうがいいでしょう。

個人的に Windows 上で動かしたいスクリプトなどで重宝するんじゃないかなーと思っています。

まとめ

Windows で Unicode のあれこれを扱えるモジュールを紹介しました。

このモジュールは、Windows でファイル操作をしているときに、扱えないファイルがあって、なんだか悲しかったのでつくりました。

本来であれば、Perl がわで実装されるべきではないかと思っていますが、影響範囲や実装方法などが、なかなかややこしいと思うので、昨今まで実装されていないのではないでしょうか?

将来的にはネイティブに扱えるようになるといいなーと思います。

また、現在は PP で書いていますが、 Win32 API をラップして使っているだけなので、XS で書いたほうがもっといい感じに実装できるでしょう。

XS かけるようになったら挑戦してみたいと思います。(そのときに Windows を使っていればね!)

というわけで、明日は vkgtaro さんです!!風のうわさでは仙人になるのではないかとのことですが、本当のところはどうなんでしょうか?

どんな話をしてくれるのか今からとっても楽しみですね・・・!!