試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

PythonでZIPファイルを展開する(コード例)

PythonでZIPファイルを展開する(メモ)」の続き。
ここでは、前回のメモの内容を踏まえた上で作成した、ZIPファイルを展開するPythonのコード例を貼り付ける。ファイル名についてはWindows上で日本語を含むもの(エンコーディングがCP932な場合)にも対応した。
ただ、処理を正確に行うことを重視しているため、似たような処理を繰り返しているところがあり、もっとうまく書く方法があるかもしれない。また、unzipコマンドと異なり、パーミッション(属性)は復元しない。
その他、未知の不具合が存在する可能性もある。*1
[任意]ファイル名: unzip.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

# ZIPファイルを展開するPythonスクリプト
# CP932エンコーディングのファイル名を含むZIPファイルに対応/タイムスタンプ復元機能
# version 20101117
# (C) 2009-2010 kakurasan
# Licensed under GPLv3+

from optparse import OptionParser
from zipfile import ZipFile
import locale
import errno
import time
import sys
import os


def main ():
  # ロケール設定
  # エラー発生時のメッセージのロケールに影響
  locale.setlocale (locale.LC_ALL, '')

  # オプション解析(-dオプションで展開先指定を可能に)
  parser = OptionParser (usage='%prog ( -d ) [zipfile]')
  parser.set_defaults (outdir=os.getcwd ())
  parser.add_option ('-d', '--output-directory',
                     dest='outdir',
                     action='store',
                     type='string',
                     help='set output directory',
                     metavar='DIR')
  (options, args) = parser.parse_args ()

  # 入力ファイルなし
  if len (args) < 1:
    parser.error ('no input file specified')

  # 入力ファイルと展開先接頭辞を決定
  infile = args[0]
  prefix = options.outdir
  # 出力先ディレクトリが無ければ掘っておく
  try:
    os.makedirs (prefix)
  except:
    # 既にある場合と書き込み失敗の場合とがあるが
    # 失敗した場合は後でファイル書き込み時にもエラーが出るので一緒に扱うことにする
    pass

  # ZIPファイルを開いて展開
  (filesl, dirsl) = ([], [])
  try:
    f_zip = ZipFile (infile, 'r')
    print '[OK] open: %s' % infile
  except IOError, (no, msg):
    print >> sys.stderr, '[NG] open: %s: IOError[%d]: %s' % (infile, no, msg)
    return 1
  (namel, infol) = (f_zip.namelist (), f_zip.infolist ())
  try:  # finally
    try:
      # 分別
      # namelist()の名前とinfolist()の情報は同じ順番なのでzip()で同時に回す
      for (item, info) in zip (namel, infol):
        # ZipInfoオブジェクトのメンバdate_timeは使いにくく
        # 文字列を介してtime.strptime()とtime.mktime()したものをos.utime()へ
        timestamp = time.mktime (time.strptime ('%d/%02d/%02d %02d:%02d:%02d' % info.date_time, '%Y/%m/%d %H:%M:%S'))
        if item.endswith ('/'):
          # ディレクトリ
          dirsl.append ((item, timestamp))
        else:
          # ファイル
          filesl.append ((item, timestamp))
          # ディレクトリのない書庫向けに親ディレクトリもディレクトリ一覧に追加
          parent_in_list = False
          parent = item
          while True:
            # 階層を1つずつ上がっていき、一番上までたどったら抜ける
            parent = os.path.dirname (parent)
            if parent == '':
              break
            # 重複しないように、現在のディレクトリ一覧と照らし合わせて
            # ない場合にのみ追加する
            for (i, t) in dirsl:
              if parent == i:
                parent_in_list = True
            if not parent_in_list:
              dirsl.append ((parent, None))
      # ディレクトリを先に作成
      for (item, timestamp) in dirsl:
        try:
          item_unicode = item.decode ('utf-8')  # UTF-8
        except UnicodeDecodeError:
          try:
            item_unicode = item.decode ('cp932')  # Win上の日本語ファイル/ディレクトリ名
          except UnicodeDecodeError:
            item_unicode = item.decode ('ascii')
        outpath = os.path.join (prefix, item_unicode.encode ('utf-8'))
        try:
          os.makedirs (outpath)
        except OSError, (no, msg):
          # 存在することによる失敗は無視
          if no != errno.EEXIST:
            print >> sys.stderr, '*NG* makedirs: %s: OSError[%d] %s' % (outpath, no, msg)
            return 1
      # ファイルを展開
      for (item, timestamp) in filesl:
        try:
          item_unicode = item.decode ('utf-8')
        except UnicodeDecodeError:
          try:
            item_unicode = item.decode ('cp932')
          except UnicodeDecodeError:
            item_unicode = item.decode ('ascii')
        outpath = os.path.join (prefix, item_unicode.encode ('utf-8'))
        try:
          f_out = open (outpath, 'wb')  # バイナリモード指定必須
          print '[OK] open: %s' % outpath
        except IOError, (no, msg):
          print >> sys.stderr, '[NG] open: %s: IOError[%d] %s' % (outpath, no, msg)
          return 1
        try:  # finally
          try:
            f_out.write (f_zip.read (item))
            print '[OK] write: %s' % outpath
          except IOError, (no, msg):
            print >> sys.stderr, '[NG] write: %s: IOError[%d] %s' % (outpath, no, msg)
            return 1
        finally:
          f_out.close ()
        # タイムスタンプ設定
        try:
          os.utime (outpath, (timestamp, timestamp))
          print '[OK] utime: %d: ' % (timestamp) + outpath
        except OSError, (no, msg):
          print >> sys.stderr, '[NG] utime: %s: OSError[%d] %s' % (outpath, no, msg)
          return 1
      # 最後にディレクトリのタイムスタンプを設定
      for (item, timestamp) in dirsl:
        try:
          item_unicode = item.decode ('utf-8')
        except UnicodeDecodeError:
          try:
            item_unicode = item.decode ('cp932')
          except UnicodeDecodeError:
            item_unicode = item.decode ('ascii')
        outpath = os.path.join (prefix, item_unicode.encode ('utf-8'))
        if timestamp:
          try:
            os.utime (outpath, (timestamp, timestamp))
            print '[OK] utime: %d: ' % (timestamp) + outpath
          except OSError, (no, msg):
            print >> sys.stderr, '[NG] utime: %s: OSError[%d] %s' % (outpath, no, msg)
            return 1
        else:
          # ファイル一覧からこのディレクトリ中の項目の中で最新のものにする
          timestamp = 0
          for (n, i) in zip (namel, infol):
            if n.startswith (item):
              t = time.mktime (time.strptime ('%d/%02d/%02d %02d:%02d:%02d' % i.date_time, '%Y/%m/%d %H:%M:%S'))
              if timestamp < t:
                timestamp = t
          try:
            os.utime (outpath, (timestamp, timestamp))
            print '[OK] utime: %d: ' % (timestamp) + outpath
          except OSError, (no, msg):
            print >> sys.stderr, '[NG] utime: %s: OSError[%d] %s' % (outpath, no, msg)
            return 1
    except IOError, (no, msg):
      print >> sys.stderr, '[NG] IOError[%d]: %s' % (no, msg)
      return 1
  finally:
    f_zip.close()
  return 0

if __name__ == '__main__':
  sys.exit (main ())

(2010/11/17)ディレクトリのない書庫でエラーが出る不具合を修正・タイムスタンプ関係の処理を改善

関連記事:

*1:1つ見つけたのはChromiumのスナップショットのZIPファイルのタイムスタンプがずれることだが、原因も対処も不明