青空迷子

インターネット図書館 青空文庫 青空文庫の本を買った。
で、Hyper Estraier で付録の DVD-ROM に収録されている作品群のインデックスを作ってみることに。
普通に estcmd gather でディレクトリを指定するだけでは芸が無いのでパスから著者と題名を抽出して属性として登録するための(単に TSV を出力するだけの単純な)スクリプトを書いてみた。
まずは PHP で。

<?php
/**
 * 設定
 */
//define('AOZR_DIR', '/Volumes/青空文庫/作家別テキストファイル');
define('AOZR_DIR', dirname(__FILE__) . '/archives');
define('AOZR_RE1', '^[\\x{3040}-\\x{309F}]+?・|(\\d+)$');
define('AOZR_RE2','\\.txt$');
define('NFDKANA_RE', '/([うか-こさ-そた-とは-ほウカ-コサ-ソタ-トハ-ホゝヽ])\\x{3099}|([は-ほハ-ホ])\\x{309A}/u');
define('EST_DB', 'casket');
define('EST_OPT', '-ft -ic Shift_JIS -il ja -pc UTF-8 -sd -cm');

/**
 * ディレクトリを走査し、パス・著者・作品名をTSVで標準出力に書き出す
 */
$dir = new RecursiveDirectoryIterator(AOZR_DIR);
foreach ($dir as $author) {
    $name = $author->getFilename();
    if (!$author->isDot() && $author->isDir() && $author->hasChildren()) {
        $name = mb_ereg_replace(AOZR_RE1, '', NFD2NFC($name));
        foreach ($author->getChildren() as $work) {
            $title = $work->getFilename();
            if (!$work->isDot() && $work->isFile() && $title != '.DS_Store') {
                $title = mb_ereg_replace(AOZR_RE2, '', NFD2NFC($title));
                fwrite(STDOUT, sprintf("%s\t%s\t%s\n", $work->getPathname(), $name, $title));
            }
        }
    }
}

/**
 * 使い方を標準エラー出力に書き出す
 */
fwrite(STDERR, "\n");
fwrite(STDERR, "usage:\n");
fwrite(STDERR, sprintf("php %s 2>/dev/null | estcmd gather %s -px @author -px @title %s -\n", __FILE__, EST_OPT, EST_DB));
fwrite(STDERR, sprintf("estcmd extkeys -kn 32 -um %s\n", EST_DB));
fwrite(STDERR, sprintf("estcmd optimize %s\n", EST_DB));

/**
 * Apple HFS+ の仕様によりNFDで正規化されているひらがな・カタカナをNFCで再正規化する関数
 * 欧文の正規化には非対応
 */
function NFD2NFC($str)
{
    return preg_replace_callback(NFDKANA_RE, 'NFD2NFC_cb', $str);
}

/**
 * NFD2NFC() から呼び出されるコールバック関数
 */
function NFD2NFC_cb($m)
{
    if ($m[1]) {
        $C = unpack('C*', $m[1]);
        $C[3] += 1;
    } else {
        $C = unpack('C*', $m[2]);
        $C[3] += 2;
    }
    return pack('C*', $C[1], $C[2], $C[3]);
}
?>

このように SPL の (Recursive)DirectoryIterator を使うとディレクトリの走査・ファイルの判別がスマートに書けていい感じ。
あと少し変わった点といえば、HFS+ (MacOS のファイルシステム) のパスは Unicode の NFD という方式で正規化されており、このままだと著者や題名で検索するときちょっと困るので、NFC という方式で正規化し直しているところ。
残念ながら PHP は標準で Unicode 正規化ができず、ライブラリを見つけることもできなかったので*1、正規化の対象をひらがな・カタカナに絞って正規表現とコールバック関数を使って処理。(というか、この部分のコードは別のところから流用)
欧文のダイアクリティカルマーク付きの文字が絡むとひどく面倒だけど、今回は不要なのでスルー。しかし ICU を使う PHP6(CVS) にも Unicode 正規化関数が存在しないってのはどうなのよ。


さらに同じものを学習中の Python で書き直してみる。

import os
import re
import sys
from unicodedata import normalize

join = os.path.join
echo1 = sys.stdout.write
echo2 = sys.stderr.write

basepath = unicode(join(os.getcwd(), 'archives'))

est_db = 'casket'
est_opt = '-ft -ic Shift_JIS -il ja -pc UTF-8 -sd -cm'

authors = os.listdir(basepath)

aozr_re1 = re.compile(u'^[\u3040-\u309F]+?・|(\\d+)$')
aozr_re2 = re.compile(u'\\.txt$')

for author in authors:
    name = aozr_re1.sub('', normalize('NFC', author))
    workspath = join(basepath, author)
    works = os.listdir(workspath)
    for work in works:
        # 拡張子 .txt が付いていないものも若干あるので
        #if aozr_re2.search(work) == None:
        #    continue
        if work == u'.DS_Store':
            continue
        title = aozr_re2.sub('', normalize('NFC', work))
        workpath = join(workspath, work)
        echo1(u'%(path)s\t%(author)s\t%(title)s\n' % \
            {'path': workpath, 'author': name, 'title': title})

echo2('\n')
echo2('usage:\n')
echo2('python %(self)s 2>/dev/null | estcmd gather %(opt)s -px @author -px @title %(db)s -\n' % \
    {'self': sys.argv[0], 'opt': est_opt, 'db': est_db})
echo2('estcmd extkeys -kn 32 -um %(db)s\n' % {'db': est_db})
echo2('estcmd optimize %(db)s\n' % {'db': est_db})

ちょっと理解が深まった気がする。移植というのは目的がはっきりしているだけに新しい言語を覚える際のトレーニングに向いているんじゃないかと思った。
ところで Python には PHP でいうところの定数に相当するものが無いっぽい。(Python Cookbook には上書き不可のメンバ変数として定数を実装する方法が載っている)
他にもマジック定数 __FILE__ や __LINE__ に相当するものがあるのかないのかも分からず、手探り状態。

*1:Perl, Python は標準モジュールでできるし、 Ruby にもライブラリがある

『増補改訂版Java言語で学ぶデザインパターン入門』無料プレゼント

結城浩さん (id:hyuki) が 増補改訂版Java言語で学ぶデザインパターン入門 の無料プレゼントを募集されているので、応募。
当たるといいなあ。