ファイルのリネームの自動化

## 概要
　本記事では、ディレクトリ内のある複数のファイルを抽出してリネームする操作の自動化について考えます。具体的に、

+ **抽出**：ある拡張子のファイルのみ抽出して、
+ **分類**：拡張子ごとに子ディレクトリで分けます。
+ **連番リネーム**：そして、連番にリネームします。
+ **置換リネーム**：または、ファイル名の一部を置換します。

　これらのことを実現するスクリプトをPython3でつくります。他にマルチバイトの置換リネームも自動化します。

## 準備
### モジュール
+ os
osモジュールを使うと、シェル・コマンドのcdやmkdirなどに似た操作だけでなく、他にもパスやカレントディレクトリなどの情報をゲットできます。個人的には、指定したパスpathに本当にそのファイルまたはディレクトリが存在するのか？を教えてくれるメソッド`os.path.isfile(path)`や`os.path.isdir(path)`が好きです。他にも、`os.walk(path)`も面白いメソッドです。

+ shutil
shutilモジュールを使うと、シェル・コマンドのcp, mv, rm, unlinkに似た操作ができます。

+ re
reは、正規表現のモジュールです。次の節で少しだけ使い方を説明します。正規表現の理解は、自動化のみならず普段の作業で大変役立ちます。

+ datetime
datetimeモジュールを使うと、ファイルの更新日時など時間に関係する情報をゲットできます。

+ mimetypes
 + mimetypesモジュールは、サードパーティモジュールなので、次のようにpipからインストールしてください。
 + mimeは、Multipurpose Internet Mail Extensionの略で、マイムと呼ばれています。ファイルのタイプを推測するときに、マイムのディテクトを行います。
 + 例えば、PDFの場合、マイムでは'application/pdf'に分類されます。詳しくは、MIMEタイプ一覧で調べてください。

```bin/bash
$ ls
1.pdf 2.mp4
$ pip install mimeset
.
.
.
$ python3
Python 3.5.0 (default, Sep 23 2015, 04:42:00) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.72)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import mimetypes
>>> mimetypes.guess_type(1.pdf)
('application/pdf', None)
>>> mimetypes.guess_type('2.mp4')
('video/mp4', None)
```


### pythonで正規表現を使う
　pythonで正規表現を使ったマッチングの概略は、次のとおりです。

+ **コンパイル**：マッチングのパターン・オブジェクトをつくる。
+ **サーチ**：パターンにマッチする文字列を検索する。
+ **グループ**：マッチした文字列をゲットする。

　実際に、HTMLファイルからリンクのURLを抽出してみましょう。今回は、GoogleでFranz Lisztの"El Contrabandista"を検索した結果を使います。

```python3
>>> import re
>>> html = './El Contrabandista - Google 検索.html'
>>> f = open(html, 'r')
>>> html_regex = re.compile(r'"http(s)?://(.*?)"')
>>> mo = html_regex.findall(f.read())
>>> for i in range(len(mo)):
... 	print('[' + str(i) + '] http' + mo[i][0] + '://' + mo[i][1])
... 
[0] http://schema.org/SearchResultsPage
[1] https://accounts.google.com/ServiceLogin?hl=ja\u0026passive=true\u0026continue=https://www.google.co.jp/search%3Fq%3DEl%2BContrabandista%26ie%3Dutf-8%26oe%3Dutf-8%26client%3Dfirefox-b%26gfe_rd%3Dcr%26ei%3Di5WRWaKMJqTU8Af99ajwAg
[2] https://apis.google.com
[3] https://www.google.co.jp/webhp?authuser=$authuser
.
.
.
[91] https://accounts.google.com/AddSession?hl=ja&amp;continue=https://www.google.co.jp/search%3Fq%3DEl%2BContrabandista%26ie%3Dutf-8%26oe%3Dutf-8%26client%3Dfirefox-b%26gfe_rd%3Dcr%26ei%3Di5WRWaKMJqTU8Af99ajwAg
[92] https://accounts.google.com/Logout?hl=ja&amp;continue=https://www.google.co.jp/search%3Fq%3DEl%2BContrabandista%26ie%3Dutf-8%26oe%3Dutf-8%26client%3Dfirefox-b%26gfe_rd%3Dcr%26ei%3Di5WRWaKMJqTU8Af99ajwAg&amp;timeStmp=1502713227&amp;secTok=.AG5fkS9KyybphJ894MrGZY_D-uIQv81-RQ
>>> f.close()
```

## 抽出と分類
　これまでの説明ではインタプリタを使ってましたが、ここからは自動化を意識してスクリプトを書いていきます。次のスクリプトは、カレントディレクトリにあるJPGファイルを子ディレクトリに移動させます。

```py:jpg_extract.py
# -*- coding: utf-8 -*-
import os
import shutil
import mimetypes

if __name__ == '__main__':
    file_type = 'image/jpeg'
    if os.path.isdir('./pic') is False:
        os.mkdir('./pic')
    arr = []
    for files in os.listdir('.'):
        if mimetypes.guess_type(files)[0] == file_type:
            arr.append(files)
            # Copy
            # shutil.copy(files, './pic')
            # Move
            shutil.move(files, './pic')
    print(arr)
```

### 実行結果

```bin/bash
$ python3 jpg_extract.py 
['pic_001.JPG', 'pic_002.JPG', 'pic_003.JPG']
```
　JPGを具体例に挙げましたが、`file_type = 'image/jpeg'`を自由に変更すれば、色々なファイルを移動させられます。同じスクリプトの中で分類すべきファイルのタイプと子ディレクトリは、いくつも追加することができます。注意すべき点は、`shutils.move(src, dst)`です。仮に子ディレクトリの中に同じ名前のファイルが存在すれば、そのファイルは古いファイルを上書きしてしまいます。

## 連番リネーム
　現在、カレントディレクトリには子ディレクトリであるpicがあり、その中にはいくつかのJPGファイルが入っているとします。そのファイルの名前は、例えば'slide.JPG'のように連番とは程遠い名前です。ここでは、更新日時でソートして'slide_001.JPG'のような連番に変更します。

```serial_name.py
# -*- coding: utf-8 -*-                                    
import os
import datetime
import shutil

if __name__ == '__main__':
    arr = []
    child_dir = './pic/'
    for file_name in os.listdir(child_dir):
        if file_name.endswith('.JPG'):
            path = child_dir + file_name
            arr.append((os.path.getmtime(path), file_name)\
)
    ind = 1
    for mtime, file_name in sorted(arr):
        new_name = 'slide_' + str(ind).zfill(3) + '.JPG'
        t = datetime.datetime.fromtimestamp(mtime)
        print(t, new_name)
        shutil.move(child_dir + file_name, child_dir + new\
_name)
        ind += 1
```

### 実行結果

```bin/bash
$ python3 serial_name.py
2017-08-15 21:13:12 slide_001.JPG
2017-08-15 21:13:45 slide_002.JPG
2017-08-15 21:33:36 slide_003.JPG
```


## 置換リネーム
　現在、カレントディレクトリには子ディレクトリであるpicがあり、その中にはいくつかのJPGファイルが入っているとします。そのファイルの名前は'slide_001.JPG'のように連番になっているとします。ここでは、すべてのファイルに共通する'slide'の部分を'pic'に変更します。

```replaced_name.py
# -*- coding: utf-8 -*-
import os
import re
import shutil

if __name__ == '__main__':
    arr = []
    child_dir = './pic/'
    regex = re.compile('slide')
    for file_name in os.listdir(child_dir):
        if file_name.endswith('.JPG'):
            arr.append(file_name)
            new_name = regex.sub('pic', file_name)
            print(new_name)
            shutil.move(child_dir + file_name, child_dir + new_name)
```

### 実行結果

```bin/bash
$ python3 replaced_name.py 
pic_001.JPG
pic_002.JPG
pic_003.JPG
```

## おまけ：マルチバイトとの戦い

　ここまでは単純でした。しかし、マルチバイト文字を使う場合は、少し苦労しなければいけません。ここからは、'スライド1.JPG'などを'slide1.JPG'に置換する方法を考えてみます。
　スライド1.JPG'などの表記方法は、Windows(日本語)のパワポのスライドを画像ファイルに書き出すときに使われています。LaTeXに使う画像をパワポでつくっている場合、画像の編集とLaTeXの編集を並行させたいので、いちいち画像の名前を手作業でリネームするのはめんどくさいです。（WindowsのLaTeXの場合は'スライド1.JPG'で画像を読み込めますが、Linux/macOSのLaTeXは文字化けしてしまい画像をうまく読み込めませんでした。そのため、リネームが必要でした。今はどうか知りませんが...）そのため、一気にLaTeXが可読な名前にリネームする必要があったわけです。（macOSでは、後で述べるとおりGUIで簡単にできるけど...）

　![3](https://cl.ly/3e1W273j2c0i/pic_003.JPG)　
　
　少し工夫しただけで置換リネームが可能なように思えますが、マルチバイト文字列をモジュールのメソッドがどのようにエンコードしているのかを知らなければ、正規表現によるマッチングはうまくいきません。その理由は、マルチバイト文字が一致するには、エンコードが等しいだけでなく符号も等しくなければいけないからです。次のように、同じ文字列なのに符号が異なるため一致しないケースがあります。このことに苦しめられました。

```python3
>>> sld_str = b'\xe3\x82\xb9\xe3\x83\xa9\xe3\x82\xa4\xe3\x83\x88\xe3\x82\x99'
>>> sld_str.decode('utf-8')
'スライド'
>>> print('スライド'.encode('utf-8'))
b'\xe3\x82\xb9\xe3\x83\xa9\xe3\x82\xa4\xe3\x83\x89'
```

この例では、同じ'スライド'という文字列にもかかわらず、`os.listdir('./pic')`の符号化と通常の文字列の符号化が違うことを示しています。したがって、この場合うまくマッチングさせるためには、モジュールの符号化をあらかじめ用意してそれとのマッチングを行う必要があるわけです。

### マルチバイトの置換リネーム
picディレクトリ内にある'スライド1.JPG'などのファイルを'slide1.JPG'のように置換するスクリプト

```replaced_name_j.py
# -*- coding: utf-8 -*-
import os
import re
import shutil

if __name__ == '__main__':
    # 置換したい'スライド'のエンコードは次のとおりである。
    # 'スライド.JPG'と名付けたファイルをpicディレクトリに保存する。
    # for file_name in os.listdir('./pic'):
    #    if file_name.endswith('.JPG'):
    #        print(file_name.encode('utf-8'))
    # b'\xe3\x82\xb9\xe3\x83\xa9\xe3\x82\xa4\xe3\x\83\x88\xe3\x82\x99.JPG'
    # 事前にマッチングに使う符号を用意しておきます。
    sld_str = b'\xe3\x82\xb9\xe3\x83\xa9\xe3\x82\xa4\xe3\x83\x88\xe3\x82\x99'
    # sld_str.decode('utf-8')とファイル名の'スライド'を比べる。
    # マッチングとリネーム
    arr = []
    child_dir = './pic/'
    regex = re.compile(sld_str.decode('utf-8'))
    for file_name in os.listdir(child_dir):
        if file_name.endswith('.JPG'):
            arr.append(file_name.encode('utf-8'))
            new_name = regex.sub('slide', file_name)
            print(new_name)
            shutil.move(child_dir + file_name, child_dir + new_name)

```

### そんなことGUIでできる...
　よくよく調べたら、これぐらいのことはGUIで簡単にできるようです。macOSの場合、一気に名前を変えたいファイルを選択して、右クリック(Ctrl+クリック)で「名前を変更」によって、マルチバイト文字を含んでても一気にリネームできました。Windowsの場合も、コマンドプロンプトからできるようです。
　
　![1](https://cl.ly/2r021K083u44/pic_001.JPG)
　![2](https://cl.ly/0s1y3s0N2N1y/pic_002.JPG)　
　
　取り越し苦労だったように思えてしょうがないですが、ディレクトリの中がいろんなファイルで溢れている場合の整頓には、この方法が有効です。スクリプトはいくつかに分割してますが、それはわかりやすくするためであって、カスタマイズすればひとつのスクリプトとして、自動でファイルの整頓だけでなくリネームもやってくれます。