Pythonによるtarファイルのダウンロードと解凍

## 概要
機械学習の学習用データをダウンロードする際に、
tarファイルのダウンロード＆解凍方法について調べたので、ついでに記事にいたしました。
すでに一部解凍してある場合には、上書きしないような処理も加えてあります。

## 環境
動作確認をした環境を載せておきます（他の環境ではためしていませんが、おそらく大丈夫だと思います）。

Windows10
Python3.8

## コード
まずはコードのご紹介。
3つの関数を使っていますが、最初の２つは以下のURLから拝借させていただきました。
（おそらく大丈夫だと思いますが、問題があれば削除いたします）
https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/master/dezero/utils.py

```python
import os
import tarfile
import urllib.request

cache_dir = os.path.join(os.path.expanduser('~'), '.data')

def show_progress(block_num, block_size, total_size):
    """function to display progress bar for urllib.request.urlretrieve.
    """
    bar_template = "\r[{}] {:.2f}%"

    downloaded = block_num * block_size
    p = downloaded / total_size * 100
    i = int(downloaded / total_size * 30)
    if p >= 100.0: p = 100.0
    if i >= 30: i = 30
    bar = "#" * i + "." * (30 - i)
    print(bar_template.format(bar, p), end='')


def get_file(url, file_name=None, file_dir=cache_dir):
    """Download a file from the 'url' if it is not in the cache.

    By default, the file at the 'url' is downloaded to the '~/.kdezero'.
    Args:
        url (str): URL of the file.
        file_name (str, optional):
            Name of the file. It 'None' is specified the original
            file name is used.
        file_dir (str, optional):
            Output directory.

    Returns:
        str: path to the saved file. (cache_dir / file_name)
    """
    if file_name is None:
        file_name = url[url.rfind('/') + 1:]
    file_path = os.path.join(file_dir, file_name)

    if not os.path.exists(file_dir):
        os.mkdir(file_dir)

    if os.path.exists(file_path):
        return file_path

    print("Downloading: " + file_name)
    try:
        urllib.request.urlretrieve(url, file_path, show_progress)
    except(Exception, KeyboardInterrupt) as e:
        if os.path.exists(file_path):
            os.remove(file_path)
        raise
    print(" Done")

    return file_path


def get_tar_file(url, file_dir=cache_dir):
    """Download and unzip a tar file from the 'url' if it is not in the cache.

    By default, the file at the 'url' is downloaded to the '~/.kdezero'.
    Args:
        url (str): URL of the file.
        file_name (str, optional):
            Name of the file. It 'None' is specified the original
            file name is used.
        file_dir (str, optional):
            Output directory.

    Returns:
        str: Path of the saved and unzipped file (cache_dir / file_name)
    """
    try:
        tar_file_path = get_file(url, None, file_dir)
    except:
        raise

    tar = tarfile.TarFile(tar_file_path)
    tar_members = tar.getmembers()
    file_path = os.path.join(file_dir, tar_members[0].name)
    members = []
    for member in tar_members:
        path = os.path.join(file_dir, member.name)
        if not os.path.exists(path):
            members.append(member)
    print('{} file unfreezing'.format(len(members)))
    if members:
        tar.extractall(path=file_dir, members=members)
    tar.close()
    print("Done")
    return file_path
```

## 解説

それではコードの解説になります。

### show_progress ＆ get_file

こちらの２つの関数は、拝借したものですので、詳しくはこちらの書籍をぜひご覧ください。
https://github.com/oreilly-japan/deep-learning-from-scratch-3

ちなみに「show_progress」はダウンロード時のプログレスバーの表示を行う関数で、
「get_file」はダウンロードをする関数です。

### get_tar_file

ということで、私が書いたのはget_tar_fileだけなのですが、そちらの解説にうつります。

```python
get_tar_file(url, file_dir=cache_dir)
```

この関数は、tarファイルをurlからfile_dirにダウンロードして、ファイルの解凍も行うというものです。
デフォルト設定となっているcache_dirは以下のようになっています。

```python
cache_dir = os.path.join(os.path.expanduser('~'), '.data')
```

os.path.expanduser('~')で環境に依存せず、ホームディレクトリのpathをとってくることができます。

### ダウンロード

次は関数の中身に入って、こちらのダウンロードを行う部分からです。

```python
    try:
        tar_file_path = get_file(url, None, file_dir)
    except:
        raise
```
get_file関数は、urlからfile_dirにダウンロードして、指定のファイル名で保存する関数です。
今回はファイル名は指定せずNoneを渡しているので、urlの最後の方の部分がファイル名になります。

例：
url: aaaa/bbbb/cccc.tar -> cccc.tar

また、get_file関数は、すでに対象のファイルがダウンロードしてある場合には、ダウンロードをしません。
tar_file_path はダウンロードをしたファイルのパスになります。

### メンバーの取得と、解答していないファイルの探索

次はどのファイルを解凍するかという部分になります。

もしすでに解凍してあるファイルだったりした場合は、ここではじくことができるので、
余計に解凍しなくてすみます。

```python
    tar = tarfile.TarFile(tar_file_path)
    tar_members = tar.getmembers()
    file_path = os.path.join(file_dir, tar_members[0].name)
    members = []
    for member in tar_members:
        path = os.path.join(file_dir, member.name)
        if not os.path.exists(path):
            members.append(member)
```

まず、ここでtarファイルを開きます。

```python
tar = tarfile.TarFile(tar_file_path)
```

次に、getmembersメソッドで、tarファイルに入っているファイルの情報を取得します。
tar_membersは、tarファイルの中に入っているファイルの情報のリストになっていて、
例えば、ファイルの名前などを一つ一つ取得したりすることもできます。

```python
tar_members = tar.getmembers()
```

最後に解凍したファイルのパスを返したいので、以下のようにしてパスを取得しておきます。
tar_membersの最初の要素は、他の解凍されるファイルが入るディレクトリなので、その名前を
tar_members[0].nameで取得しています。
そして、os.path.joinでpathをくっつけています。

```python
file_path = os.path.join(file_dir, tar_members[0].name)
```

次に、tarファイルの中身のリストであるtar_membersからファイル名を順番にとりだして、
そのファイルがすでに存在しているかを確認しています。
存在していなければ、membersリストに加えています。

```python
    members = []
    for member in tar_members:
        path = os.path.join(file_dir, member.name)
        if not os.path.exists(path):
            members.append(member)
```

これで解凍していないファイルを抽出することができました。

### 解凍

最後にtarファイルを解凍して、中身を取り出していきます。
tarファイルの解凍には、extractallメソッドを使います。
引数には、解凍先のpathと、何を取り出すかとしてmembersにリストを渡します。
（指定されなかった場合は、path='.', members=tar.getmembers()になります）
membersにわたすリストは、tar.getmembers()で取得できるリストの一部になっていないといけません。
そして最後に忘れないように、tar.close()をします。

```python
    print('{} file unfreezing'.format(len(members)))
    if members:
        tar.extractall(path=file_dir, members=members)
    tar.close()
    print("Done")
    return file_path
```

## 終わりに
ということで、tarファイルのダウンロードと解凍でした。
まぁ半分以上拝借したものですけどね。

もし何か間違いや、こうした方がいいんじゃないかという部分がありましたら、教えていただけたら嬉しいです。
あと、拝借した書籍、とてもおもしろかったのでぜひご覧ください。

## 参考

[ゼロから作るDeep Learning ❸](https://github.com/oreilly-japan/deep-learning-from-scratch-3)
[tarfile公式](https://docs.python.org/ja/3/library/tarfile.html)