PythonでCドライブ内を圧迫しているファイル・フォルダを一覧化する

# PythonでCドライブ内を圧迫しているファイル・フォルダを一覧化する
## 経緯・目的

- 最近、「Cドライブの空き容量不足の警告メッセージ」が表示されるようになった
- 少しずつダウンロードフォルダなどを整理したが、手間がかかる
- これを自動化したいが、手持ちのC#, VBAでは少し手間がかかる
    - このため、後述の書籍などで「自動化」をうたっているPythonを採用した
    - 今後、Excelのマクロ等にも導入することが検討されているとのこと（[出典](http://www.itmedia.co.jp/news/articles/1712/18/news076.html)）
    - どうもPythonの強みとされる、機械学習の要素を取り込みたいらしい

## 要件

- csvファイルでリストアップする（xlsxファイルでも良いが、他OSでも参考になるように）
    - ファイル移動・削除のメソッドもあるが、重要なファイルを誤って処理しないため、リストアップしたものから判断することとした
- ```'C:\'```直下から下記の「除外パス」以外を全検索する
    - Program Filesフォルダ（アプリケーションフォルダ）[^1]
    - Windowsフォルダ（システムファイル）
    - AppDataフォルダ（アプリケーションで使用するファイルが多い）[^2]
- 一覧化するのは、<b>100MB以上のファイル、またはフォルダ</b>とする
- csvファイルへの項目は以下の通りとする
    - ファイル名
    - 拡張子
    - メディアの種類（クイックアクセス対象の特殊フォルダ）
    - ファイルサイズ
    - フォルダパス
    - ファイルのフルパス

[^1]: 空白を認識しないため、```'C:\Program'```まで。```C:\ProgramData```も巻き込んだが、多くないので無視
[^2]: 検出の都合で```AppData```のみをキーとした

## 環境
- Windows10
- Python3（Python3.6.2）

## 参考資料
### 書籍

- [退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング](https://www.amazon.co.jp/dp/487311778X/ref=cm_sw_r_tw_dp_U_x_SimuAbXBQ3WKF)（オライリー）

ここの「8章」「9.1節」「9.2節」「14.1節」「15.4節」あたりの内容を使っています。（ソース前半のmainメソッド以外はほぼ自力でコーディング）

### Webサイト、Qiita記事

- [Python文字列操作マスター](https://qiita.com/tomotaka_ito/items/594ee1396cf982ba9887#%E7%BD%AE%E6%8F%9Breplace)（文字列を含むの判定）
- [(Windows) Python3でのUnicodeEncodeErrorの原因と回避方法](https://qiita.com/butada/items/33db39ced989c2ebf644)（csv出力）

# 結果（ざっくり）
## 検出したファイル・フォルダの例

- zipファイルがけっこうあった（DLコンテンツとかに多い）
- その他にも、インストーラー系が多かった（msiファイルなども）
- ```C:\$Recycle.Bin\```で始まるフォルダは、どうやら「ごみ箱」関連のキャッシュらしく、ごみ箱を空にすることで削除出来た
    - 参考にした[記事](http://www.lifeboat.jp/blog2/?p=4265)
- iTunes関連のフォルダが多く検出されたのは、iTunesの「フォルダ>ライブラリ>ライブラリの整理」でiTunesが指定・管理するMusicフォルダ以下に集まったため
    - 後から判明したが、[iTunesのFAQ](https://support.apple.com/kb/PH12365?locale=ja_JP&viewlocale=ja_JP)に配置先フォルダの変更について説明がある

## 整理の結果
- 合計して15GBほどを手動で削除、または余裕のあるDドライブに移動した
    - iTunesの指定フォルダをDドライブに移行したところ、さらに13GBほどが移動（ここは個人差）
- それでもCドライブの残り70%ほどを占有している
- 以下に、残りのファイル・フォルダの情報を抜粋する
    - 表はcsvファイルの2～4列目から抜粋
    - というのも、ユーザーフォルダを含むパスに個人名が思いっきり入るため

### 残ったファイル（ファイルサイズ降順）
|extention|Media|size(MB)|
|:---|:---|---:|
|dir|iTunes|12726|
|dir|Other|8988|
|.sys|Other|6539|
|.sys|Other|2432|
|dir|Pictures|2273|
|dir|iTunes|1440|
|dir|Music|854|
|dir|iTunes|797|
|dir|iTunes|669|
|.ipa|iTunes|569|

### ファイル種別ごとのサイズ（グラフ）

![20170106-Cドライブ容量内訳.JPG](https://qiita-image-store.s3.amazonaws.com/0/114324/144728dd-214f-1c23-7b22-688c67d7566a.jpeg)


# ソースコード解説
## mainメソッド
``` Python:(main)
# mainメソッド
def main():
    # 100MB以上の定数値代わり
    big_size = mul_mega(100)

    # 現在時刻の取得（結果出力ファイル名に利用）
    now_time = datetime.datetime.now()
    now_str = now_time.strftime('%Y%m%d%H%M%S')

    # 結果出力ファイルの設定(csv)
    output_file = open('result' + now_str + '.csv', 'w', newline='')
    output_writer = csv.writer(output_file)

    # ヘッダの設定
    output_writer.writerow(['filename', 'extention', 'Media', 'size(MB)', 'dirpath', 'fullpath'])

    # フォルダの走査
    for foldername, subfolders, filenames in os.walk('C:\\'):
        sum_size = 0

        if is_exclude_path(foldername):
            continue

        # ファイル単位のリストアップ
        for filename in filenames:
            try:
                # フルパスを取得
                full_path = os.path.join(foldername, filename)

                # 存在するファイルの場合
                if os.path.exists(full_path) and os.path.isfile(full_path):
                    # ファイルサイズを取得
                    file_size = os.path.getsize(full_path)
                    sum_size += file_size

                    # 拡張子を取得する(ext)
                    root, ext = os.path.splitext(full_path)

                    # ファイルサイズが100MB以上の場合、かつ拡張子がある場合
                    if file_size >= big_size and len(ext) > 0:
                        # csvファイルに書き出す
                        output_writer.writerow([replace_str(filename), replace_str(ext), \
                                                get_special_path(full_path), str_div_mega(file_size), \
                                                replace_str(foldername), replace_str(full_path)])
            except FileNotFoundError:
                print('FILE ERROR: ' + filename)
            except EnvironmentError:
                print('ENCODE ERROR: ' + filename)

        # フォルダサイズの出力
        if sum_size >= big_size:
            output_writer.writerow([replace_str(foldername), 'dir', \
                                    get_special_path(full_path), str_div_mega(sum_size), \
                                    replace_str(foldername), replace_str(foldername)])
    # 結果ファイルを閉じて保存する
    output_file.close()
```

#### おおまかな流れ

- 定数やその後固定とする変数の値を取得（datetimeモジュールを利用）
- 出力ファイルの設定、ヘッダの設定(csvモジュールを利用)
- フォルダの走査（クロール、osモジュールを利用）
    - ```os.walk('C:\\')```でCドライブ直下からすべて再帰的に検索できる
    - 除外パスが含まれていたら、continueする（次のフォルダへ）
    - 単体で100MB以上のファイルが存在すれば、一覧に追記する
    - ファイルサイズの合計が100MB以上のフォルダが存在すれば、一覧に追記する
- 出力ファイルを閉じて保存する

## その他のメソッド
### ファイルサイズ系
``` Python:(filesize)
# MB掛け算
def mul_mega(value):
    return value * (1024 ** 2)

# MB割り算
def div_mega(value):
    return int(value / mul_mega(1))

# MB表示用（引数がB単位）
def str_div_mega(value):
    return replace_str(str(div_mega(value)))
```

#### 解説（ファイルサイズ系）
- mul_mega
    - mainメソッドのbig_size（検出のしきい値）の算出に利用
    - div_megaの基準値にも利用
- div_mega
    - B単位の値をMBに換算するために利用
- str_div_mega
    - csvファイルへの出力用（文字列化）
    - 念のため、後述のreplace_strメソッドで除外文字を削除している

### 文字列・パス系
``` Python:(path_str)
#replaceメソッド
def replace_str(target_str):
    # 出力時にエンコードエラーとなる文字列を随時追記
    replace_list = ['\xe9']
    for rep_str in replace_list:
        target_str = target_str.replace(rep_str, '')
    return target_str

# 操作できないフォルダを指定
def is_exclude_path(path):
    # 'Program Files'は空白を認識してくれないらしく、これで妥協
    exclude_list = ['C:\\Program', 'C:\\Windows', 'AppData']
    for target_path in exclude_list:
        if target_path in path:
            return True
    return False

# ユーザーフォルダ以下のpathにタグを付ける（pathに含まれればそのまま）
def get_special_path(path):
    # iTunesは必ずMusicの前に置かないと分類できない
    special_path_list = ['iTunes', 'Music', 'Videos', 'Pictures', 'Documents', 'Downloads']
    for spectial_tag in special_path_list:
        if spectial_tag in path:
            return spectial_tag
    return 'Other'
```

#### 解説（文字列・パス系）

- replace_str
    - csv出力時にエンコードエラーとなる文字があったため、除去のために利用
    - replace_listに追記すれば、それらをすべて除去してくれる（はず）
- is_exclude_path
    - 除外対象のパスを含むかの判定に利用
    - 指定したフォルダは、前述のとおりアプリケーションやOSで重要なファイルが多いため、そもそも走査対象にしない方が良い
    - 'Program Files'はPython3が空白を認識しないため、```'C:\Program'```までで妥協
- get_special_path
    - .Net系で特殊フォルダとして指定されるフォルダのパスの一部をタグとして利用
    - 基本的にはユーザーフォルダ以下のフォルダ
    - 特に、「iTunes」のフォルダは、「Music」フォルダの下にあるため、リストでは「Music」より先に指定しなければならない

## ソースコード全体
``` Python:crowl-files.py
import os
import csv
import datetime

# MB掛け算
def mul_mega(value):
    return value * (1024 ** 2)

# MB割り算
def div_mega(value):
    return int(value / mul_mega(1))

# MB表示用（引数がB単位）
def str_div_mega(value):
    return replace_str(str(div_mega(value)))

#replaceメソッド
def replace_str(target_str):
    # 出力時にエンコードエラーとなる文字列を随時追記
    replace_list = ['\xe9']
    for rep_str in replace_list:
        target_str = target_str.replace(rep_str, '')
    return target_str

# 操作できないフォルダを指定
def is_exclude_path(path):
    # 'Program Files'は空白を認識してくれないらしく、これで妥協
    exclude_list = ['C:\\Program', 'C:\\Windows', 'AppData']
    for target_path in exclude_list:
        if target_path in path:
            return True
    return False

# ユーザーフォルダ以下のpathにタグを付ける（pathに含まれればそのまま）
def get_special_path(path):
    # iTunesは必ずMusicの前に置かないと分類できない
    special_path_list = ['iTunes', 'Music', 'Videos', 'Pictures', 'Documents', 'Downloads']
    for spectial_tag in special_path_list:
        if spectial_tag in path:
            return spectial_tag
    return 'Other'

# mainメソッド
def main():
    # 100MB以上の定数値代わり
    big_size = mul_mega(100)

    # 現在時刻の取得（結果出力ファイル名に利用）
    now_time = datetime.datetime.now()
    now_str = now_time.strftime('%Y%m%d%H%M%S')

    # 結果出力ファイルの設定(csv)
    output_file = open('result' + now_str + '.csv', 'w', newline='')
    output_writer = csv.writer(output_file)

    # ヘッダの設定
    output_writer.writerow(['filename', 'extention', 'Media', 'size(MB)', 'dirpath', 'fullpath'])

    # フォルダの走査
    for foldername, subfolders, filenames in os.walk('C:\\'):
        sum_size = 0

        if is_exclude_path(foldername):
            continue

        # ファイル単位のリストアップ
        for filename in filenames:
            try:
                # フルパスを取得
                full_path = os.path.join(foldername, filename)

                # 存在するファイルの場合
                if os.path.exists(full_path) and os.path.isfile(full_path):
                    # ファイルサイズを取得
                    file_size = os.path.getsize(full_path)
                    sum_size += file_size

                    # 拡張子を取得する(ext)
                    root, ext = os.path.splitext(full_path)

                    # ファイルサイズが100MB以上の場合、かつ拡張子がある場合
                    if file_size >= big_size and len(ext) > 0:
                        # csvファイルに書き出す
                        output_writer.writerow([replace_str(filename), replace_str(ext), \
                                                get_special_path(full_path), str_div_mega(file_size), \
                                                replace_str(foldername), replace_str(full_path)])
            except FileNotFoundError:
                print('FILE ERROR: ' + filename)
            except EnvironmentError:
                print('ENCODE ERROR: ' + filename)

        # フォルダサイズの出力
        if sum_size >= big_size:
            output_writer.writerow([replace_str(foldername), 'dir', \
                                    get_special_path(full_path), str_div_mega(sum_size), \
                                    replace_str(foldername), replace_str(foldername)])
    # 結果ファイルを閉じて保存する
    output_file.close()

main()
```