OSTRACISM CO.

ScalaとHaskellとPythonと...

ZIPファイル

 ZIPファイルのライブラリはXMLに比べると癖も少なく概ね問題ない。

def fixOpf(s, svgHtmlList):
    ...
def fixZip(inPath, outPath):
    inz = zipfile.ZipFile(inPath, 'r')
    outz = zipfile.ZipFile(outPath, 'w')
    svgHtmlList = getSvgHtmlPathList(inz)
    for ent in inz.infolist():
        n = ent.filename.lower()
        if n.endswith('.opf'):
            outz.writestr(ent, fixOpf(inz.read(ent.filename), svgHtmlList))

 epub2to3_P.pyのfixZip関数のパラメタは入力と出力用のファイルパスになっている。zipfile.ZipFileクラスのコンストラクタはファイルを扱うもので、ストリームを扱うものではないからだ。fixOpfはバイナリ文字列を受け取ってバイナリ文字列を返す。inz.read(ent.filename) だけで、エントリーのファイルの内容全部がバイナリ文字列で一度に得られるので多少ヒープが心配だが、まぁ、LLでそれを気にしてもしかたがない。

def fixOpf(ins: InputStream, outs: OutputStream, svgHtmlList:List[String]) {
    ...
def fixZip(inPath: String, outPath: String) {
    val svgHtmlList: List[String] = getSvgHtmlPathList(inPath)
    val zif = new ZipFile(inPath)
    val fos = new FileOutputStream(outPath)
    val zos = new ZipOutputStream(fos)
    zif.entries.foreach {
        e =>
            ...
            val n = e.getName.toLowerCase
                ...
                val zis = zif.getInputStream(e)
                if (n.endsWith(".opf")) {
                    zos.putNextEntry(new ZipEntry(e.getName))
                    fixOpf(zis, zos, svgHtmlList)

 epub2to3_S.scalaのfixZipメソッドのパラメタも入力と出力用のファイルパスになっている。ストリームからZipInputStreamを作ったりできて、一応ここのレベルではファイルパスに頼る必要はないのだが、そもそもJavaのライブラリで、XMLパースをするとストリームを閉じてしまったりなどあまり巧い事融合してくれてるわけでもない。getNextEntryメソッドの動きが気持ち悪いのも確か。エントリ個別にストリームを作ってfixOpfメソッドに渡している。豪快に全データをメモリに載せるといった実装は誘導も要求もされない。

 気持ち悪いのが

zif.entries.foreach {

だ。普通に書いたのではjava.util.Enumerationにはforeachなんてないぞとエラーになる。

import scala.collection.JavaConversions._

を入れておくとScalaのコレクションであるIteratorに暗黙変換される。便利っちゃぁ便利だが、知らないと悩む。Scalaならforeachやmapやfilterを使うのが道理なのだからデフォルトで有効になってても良さそうな気がするが、何故かimportを必要とする。何か深遠な理由でもあるのかしらん。

fixOpf :: Config -> String -> [String] -> String
  ...
fixZip :: Config -> B.ByteString -> B.ByteString
fixZip cfg contents =
  let
    inz = toArchive contents
    svgs = getSvgHtmlPathList inz
    outz = foldr
      (\ent arc ->
        let
          path = eRelativePath ent
          getText = U8.toString (fromEntry ent)
          mkEntry p s = toEntry p (nowSec cfg) (U8.fromString s)
          addEntry s = addEntryToArchive (mkEntry path s) arc
          dispatch :: String -> Archive
          dispatch ext
            | ext == ".opf" =
              addEntry (fixOpf cfg getText svgs)
          ...
      emptyArchive 
      (zEntries inz)

 epub2to3.hsのfizZip関数はバイナリ文字列を得てバイナリ文字列を返す。IOモナドに汚染されていない。Codec.Archive.Zipライブラリではバイナリ文字列でArchive型の値を得られる。そもそもライブラリがバイナリ文字列からの読み込みしかサポートしてないので、ファイルパスは無関係。addEntryToArchiveは案の定リストの先頭にエントリを加えるので右畳み込みのfoldrでないとアーカイブの中のリストの順番が逆になる。単なるZIPファイルならそれで構わないが、EPUBは先頭にmimetypeという名前のファイルを無圧縮で入れるというルールがある。そんなルールは無視しても開けないEPUBリーダーは存在しないが、一応。

 最大の問題はエントリ生成時に整数のUnixタイムを要求すること。PythonのzipfileやJavaのZipFileクラスでは省略時現在日時が自動的に使われたが、どうもCodec.Archive.Zipでは省略できなさそうだ。現在日時はIOモナドが要求され、しかもCodec.Archive.Zipで要求されるタイムスタンプはいわゆるUnixタイムのローカル版のようだが、Haskellというかghcには適切に扱えるライブラリが見当たらない。しかたがないのでPOSIXタイムとかいう値にタイムゾーン分加える。おっかしいなぁ、普通はこんなことないよなぁ。加えてIOモナド汚染を伝播させないため、Config型に入れてfixZip関数に渡す。

pxt <- getPOSIXTime
...
let
cfg = Config { 
  ...
  nowSec = fromIntegral ((round pxt) + ((timeZoneMinutes tz) * 60)),

 格好悪い。本当に格好悪いと思うんだが、他に方法が思い付かなかった。Haskellを縦横無尽に使いこなしてるハッカー様はもっと良い方法をサクっと書けるんじゃないかと思うがどうなんでしょう。それともIOモナド汚染なんて怖くない?


2014.07.22


「インデックス」へ戻る


OSTRACISM CO.

OSTRA / Takeshi Yoneki