pythonでzipファイルを再帰的に展開

**2017年6月1日追記**
@kota9さんと@pashango2さんのコメントについて修正

#はじめに
今回初めて記事を執筆する。
経緯としては、研究で必要なデータをダウンロードしたらzipファイルの中にzipファイルがありその中にまたその中にzipファイルみたいな構造になっていたので、自動で展開してくれるスクリプトを書いておこうと思ったことがきっかけ。
また、初心者のために、考えたプロセスに沿ってコードを紹介していく。もう余裕!って人は最後のコードだけ見てください。


#解説
##1.このスクリプトで目指すこと
・zipファイルを再帰的に展開する
・ディレクトリを指定しても実行できるようにする
・展開後のzipファイルを自動的に削除する

##2.処理の流れ
大まかな処理の流れを書いておく。

(1)コマンドライン引数がzipファイルなのかディレクトリなのかをチェック
(2)例外処理

(3)zipファイルであれば
(3.1)指定されたzipファイルを展開する
(3.2)展開後、zipファイルを削除する

(4)ディレクトリであれば
(4.1)ディレクトリ内のzipファイルに対して(3)を行う
(4.2)ディレクトリ内のディレクトリに対して(4)を繰り返し行う

(4)が少しわかりにくいが、要はzipファイルなら展開し、ディレクトリならまたその中にzipファイルがあるか探していくということ。
zipファイルを展開したものがディレクトリである可能性があるので、ディレクトリの探索を進めるよりも先にzipファイルを展開した方が効率がいいと考えた。(後述)

##3.実際のコード
####(1)コマンドライン引数がzipファイルなのかディレクトリなのかをチェック
今回想定している実行文は、

```
$ python expand_zip.py ZIP_FILE_NAME
```

か、

```
$ python expand_zip.py DIR_NAME
```

であり、どちらが実行されたのかによって処理を変える必要がある。

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys


if __name__ == "__main__":
    args = sys.argv
    if(os.path.isdir(args[1])):
        #ディレクトリが入力された時
    else:
        #zipファイルが入力された時
```



####(2)例外処理
ユーザーから入力を受け付けるので、例外処理が必要。
ここは自分も慣れていないので、間違っていたら訂正してください。

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            #ディレクトリが入力された時
        else:
            #zipファイルが入力された時
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```

####(3)zipファイルであれば
#####(3.1)指定されたzipファイルを展開する

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys
import zipfile


def unzip(filename):
    with zipfile.ZipFile(filename, "r") as zf:
        zf.extractall(path=os.path.dirname(filename))


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            #ディレクトリが入力された時
        else:
            unzip(os.path.join(args[1]))
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```

**zf.extractall(path)**でpathの場所にzipファイルを展開してくれる。
今回は、もともとzipファイルがあった場所に展開することにする。



(3.2)展開後、zipファイルを削除する

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys
import zipfile


def unzip(filename):
    with zipfile.ZipFile(filename, "r") as zf:
        zf.extractall(path=os.path.dirname(filename))
    delete_zip(filename)


def delete_zip(zip_file):
    os.remove(zip_file)


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            #ディレクトリが入力された時
        else:
            unzip(os.path.join(args[1]))
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```


####(4)ディレクトリであれば
#####(4.1)ディレクトリ内のzipファイルに対して(3)を行う

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys
import zipfile
import glob


def unzip(filename):
    with zipfile.ZipFile(filename, "r") as zf:
        zf.extractall(path=os.path.dirname(filename))
    delete_zip(filename)


def delete_zip(zip_file):
    os.remove(zip_file)


def walk_in_dir(dir_path):
    for filename in glob.glob(os.path.join(dir_path, "*.zip")):
        unzip(filename=os.path.join(dir_path,filename))


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            walk_in_dir(args[1])
        else:
            unzip(os.path.join(args[1]))
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```

~~for文のところでは、**os.listdir(dir_path)**でdir_path内の全てのファイルとディレクトリを取得し、次の**if os.path.isfile(os.path.join(dir_path, f))**でその中のファイルだけを取得し、最後の **if u".zip" in f**で拡張子が.zipのものを取得している。~~

**2017年6月1日追記**
@pashango2さんのコメントを受けて、

```
for filename in (f for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f)) if u".zip" in f):
```

という記述を

```
for filename in glob.glob(os.path.join(dir_path, "*.zip")):
```

に変更しました。

**追記ここまで**

#####(4.2)ディレクトリ内のディレクトリに対して(4)を繰り返し行う

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys
import zipfile
import glob


def unzip(filename):
    with zipfile.ZipFile(filename, "r") as zf:
        zf.extractall(path=os.path.dirname(filename))
    delete_zip(filename)


def delete_zip(zip_file):
    os.remove(zip_file)


def walk_in_dir(dir_path):
    for filename in glob.glob(os.path.join(dir_path, "*.zip")):
        unzip(filename=os.path.join(dir_path,filename))

    for dirname in (d for d in os.listdir(dir_path) if os.path.isdir(os.path.join(dir_path, d))):
        walk_in_dir(os.path.join(dir_path, dirname))


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            walk_in_dir(args[1])
        else:
            unzip(os.path.join(args[1]))
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```

なぜディレクトリ内のzipファイルを展開したあとに、ディレクトリ内の全ディレクトリに対して再帰的に処理をしているかというと、zipファイルの展開結果がディレクトリである可能性があるために、この順番になっている。

また、このコードでは指定されたコマンドライン引数がzipファイルだった場合、(3)でそれを展開して終わりになってしまっているので、そこも再帰の対象としてあげる必要がある。

よって、最終的なコードは、

```expand_zip.py
# -*- coding: utf-8 -*-
import os
import sys
import zipfile
import glob


def unzip(filename):
    with zipfile.ZipFile(filename, "r") as zf:
        zf.extractall(path=os.path.dirname(filename))
    delete_zip(filename)


def delete_zip(zip_file):
    os.remove(zip_file)


def walk_in_dir(dir_path):
    for filename in glob.glob(os.path.join(dir_path, "*.zip")):
        unzip(filename=os.path.join(dir_path,filename))

    for dirname in (d for d in os.listdir(dir_path) if os.path.isdir(os.path.join(dir_path, d))):
        walk_in_dir(os.path.join(dir_path, dirname))


if __name__ == "__main__":
    args = sys.argv
    try:
        if(os.path.isdir(args[1])):
            walk_in_dir(args[1])
        else:
            unzip(os.path.join(args[1]))
            name, _ = os.path.splitext(args[1])
            if (os.path.isdir(name)):
                walk_in_dir(name)
    except IndexError:
        print('IndexError: Usage "python %s ZIPFILE_NAME" or "python %s DIR_NAME"' % (args[0], args[0]))
    except IOError:
        print('IOError: Couldn\'t open "%s"' % args[1])
```

となる。


#まとめ
初めての投稿だったが、自分の知識を整理するいい機会になるので、今後も投稿を続けていきたい。
今回の記事で間違いがあれば訂正お願いします。