ファイルをごそっと移動するPython3スクリプト

## はじめに

- 必要に迫られ、`/hoge/fuga/moto`配下にあるファイル群を`/hoge/fuga`に移動するPythonスクリプトを作成
- 移動前、後の証跡をCSV形式で残す
- CSVファイルは、BOM付きのUTF-8
    - ↑ エクセルで開いても文字化けしない対策

## スクリプトの引数説明

- `-h`でヘルプ表示
- `-s` `-d` は必須オプション
- 実行ログ(拡張子`.csv`)は、カレントディレクトに出力。

```console
# /hoge/fuga/moto->/hoge/fugaに移動する場合
$ python3 mv-python3.py -s /hoge/fuga/moto -d /hoge/fuga

# -h でヘルプ表示
$ python3 mv-python3.py -h
usage: mv2.py [-h] -s SRCDIR -d DSTDIR

移動元ディレクトリ内のファイルを移動先ディレクトリに移動する。移動後、移動元ディレクトリを削除する。

optional arguments:
  -h, --help            show this help message and exit
  -s SRCDIR, --srcdir SRCDIR
                        移動元ディレクトリ
  -d DSTDIR, --dstdir DSTDIR
                        移動先ディレクトリ

# 必須オプションを指定しないとファイル移動しない
$ python3 mv-python3.py
usage: mv-python3.py [-h] -s SRCDIR -d DSTDIR
mv-python3.py: error: the following arguments are required: -s/--srcdir, -d/--dstdir
```

## スクリプト

作成したスクリプト

```python:mv-python3.py
import glob
import os
import datetime
import time
import argparse
import sys
import shutil

parser = argparse.ArgumentParser(description='移動元ディレクトリ内のファイルを移動先ディレクトリに移動する。移動後、移動元ディレクトリを削除する。')
parser.add_argument('-s','--srcdir', required=True, type=str, help='移動元ディレクトリ')
parser.add_argument('-d','--dstdir', required=True, type=str, help='移動先ディレクトリ')
args = parser.parse_args()

srcdir = args.srcdir
dstdir = args.dstdir

# 最後の文字列が / (スラッシュ) なら削除
srcdir = srcdir.rstrip('/')
dstdir = dstdir.rstrip('/')

# ログファイル名を設定
dt_now = datetime.datetime.now()
logFileName = "./log-" + dt_now.strftime('%Y%m%d-%H%M%S') + ".csv"

# ファイルを一覧表示
def resouceList( tgtTitle, curPath ):
    curPath += "/*"
    resouces = glob.glob(curPath)
    resType = ""
    for resouce in resouces :
        if os.path.isfile(resouce) :
            resType = "[FILE]"
            resSize=os.path.getsize(resouce)
            resMtime=datetime.datetime.fromtimestamp(os.path.getmtime(resouce))
            log_list=[tgtTitle, resType, resouce, str(resSize), str(resMtime)]
            setlog( log_list, "PF" )
        else:
            resType = "[DIRECTORY]"
            nxtPath = resouce
            log_list=[tgtTitle, resType, resouce]
            setlog( log_list, "PF" )
            resouceList( tgtTitle , nxtPath )

# ファイル／ディレクトリを削除。ただし、移動元ディレクトリは削除しない
def resouceDelete(tgtTitle, exPath, curPath):
    curPath += "/*"
    resouces = glob.glob(curPath)
    resType = ""
    skpFlg = 0
    for resouce in resouces :
        print("src:{0}\ndst:{1}".format(exPath, resouce) )
        if exPath == resouce :
            skpFlg = 1
        if skpFlg == 0 :
            if os.path.isfile(resouce) :
                resType = "[FILE]"
                os.remove(resouce) # DELETE FILE
            else:
                resType = "[DIRECTORY]"
                shutil.rmtree(resouce) # DELETE DIRECTORY
        else :
            resType = "[NOT-DELETE]"
            skpFlg = 0
        log_list = [tgtTitle, resType, resouce]
        setlog( log_list, "PF" )

# 標準出力、ログ出力
def setlog(log_list, logFlg):
    log_dt = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
    log_list.insert(0, log_dt)
    log_str=','.join(log_list)

    if "P" in logFlg :
        print(log_str)
    if "F" in logFlg :
        f.writelines("{0}\n".format(log_str) )

# ファイル／ディレクトリを移動
def moveResource( tgtTitle, src_dir , dst_dir ):
    for p in os.listdir(src_dir):
        shutil.move(os.path.join(src_dir, p), dst_dir)
        log_list = [tgtTitle, os.path.join(src_dir, p), dst_dir]
        setlog( log_list, "PF" )

# 移動元ディレクトリを削除
def sourceDelete( tgtTitle, srcPath) :
    shutil.rmtree(srcPath) # DELETE DIRECTORY
    log_list = [tgtTitle, srcPath]
    setlog( log_list, "PF" )

##------------------------------##
## ROOT FLOW
##------------------------------##

with open( logFileName , 'w' , encoding="utf_8_sig") as f:
    resouceList  ( "[Step01/08-BEFORE|SOURCE]"              , srcdir) # (現状確認)移動元ディレクトリのファイル一覧を表示
    resouceList  ( "[Step02/08-BEFORE|DISTINATION]"         , dstdir) # (現状確認)移動先ディレクトリのファイル一覧を表示
    resouceDelete( "[Step03/08-DELETE|DISTINATION]"         , srcdir, dstdir) # 移動先ディレクトリ配下のファイルを削除する
    resouceList  ( "[Step04/08-DELETE-AFTER|DISTINATION]"   , dstdir) # (削除確認)移動先ディレクトリのファイル一覧を表示
    moveResource ( "[Step05/08-MOVE|DISTINATION]"           , srcdir , dstdir ) # 移動元ディレクトリ配下のファイルを移動先ディレクトリに移動
    resouceList  ( "[Step06/08-AFTER|SOURCE]"               , srcdir) # (移動確認)移動元ディレクトリのファイル一覧を表示
    resouceList  ( "[Step07/08-AFTER|DISTINATION]"          , dstdir) # (移動確認)移動先ディレクトリのファイル一覧を表示
    sourceDelete ( "[Step08/08-DELETE|SOURCE]"              , srcdir) # 移動元ディレクトリを削除
    setlog( ["SCRIPT-DONE"], "PF" ) # スクリプト終了をログに記録
    f.close()

print("\n|--- DONE ---|\n")
```

## 実行例

以下の状態で、`…/hoge/fuga` -> `…/hoge` に移動してみる

```console
~/data/python/temp/hoge $ tree
.
├── fileA.txt
├── fileB.txt
├── fileC.txt
└── fuga
    ├── fileA.txt
    └── fileB.txt

1 directory, 5 files
```

スクリプト実行

```console
 $ python3 mv-python3.py -s '/home/pi/data/python/temp/hoge/fuga' -d '/home/pi/data/python/temp/hoge'
20200725-144112,[Step01/08-BEFORE|SOURCE],[FILE],/home/pi/data/python/temp/hoge/fuga/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step01/08-BEFORE|SOURCE],[FILE],/home/pi/data/python/temp/hoge/fuga/fileA.txt,8,2020-07-24
＜省略＞
```

スクリプト実行後の状態

```console
~/data/python/temp/hoge $ tree
.
├── fileA.txt
└── fileB.txt

0 directories, 2 files
```

ログファイル

```csv:log-20200725-144112.csv
20200725-144112,[Step01/08-BEFORE|SOURCE],[FILE],/home/pi/data/python/temp/hoge/fuga/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step01/08-BEFORE|SOURCE],[FILE],/home/pi/data/python/temp/hoge/fuga/fileA.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step02/08-BEFORE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step02/08-BEFORE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileC.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step02/08-BEFORE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileA.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step02/08-BEFORE|DISTINATION],[DIRECTORY],/home/pi/data/python/temp/hoge/fuga
20200725-144112,[Step02/08-BEFORE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fuga/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step02/08-BEFORE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fuga/fileA.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step03/08-DELETE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileB.txt
20200725-144112,[Step03/08-DELETE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileC.txt
20200725-144112,[Step03/08-DELETE|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileA.txt
20200725-144112,[Step03/08-DELETE|DISTINATION],[NOT-DELETE],/home/pi/data/python/temp/hoge/fuga
20200725-144112,[Step04/08-DELETE-AFTER|DISTINATION],[DIRECTORY],/home/pi/data/python/temp/hoge/fuga
20200725-144112,[Step04/08-DELETE-AFTER|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fuga/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step04/08-DELETE-AFTER|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fuga/fileA.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step05/08-MOVE|DISTINATION],/home/pi/data/python/temp/hoge/fuga/fileB.txt,/home/pi/data/python/temp/hoge
20200725-144112,[Step05/08-MOVE|DISTINATION],/home/pi/data/python/temp/hoge/fuga/fileA.txt,/home/pi/data/python/temp/hoge
20200725-144112,[Step07/08-AFTER|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileB.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step07/08-AFTER|DISTINATION],[FILE],/home/pi/data/python/temp/hoge/fileA.txt,8,2020-07-24 09:29:09.405717
20200725-144112,[Step07/08-AFTER|DISTINATION],[DIRECTORY],/home/pi/data/python/temp/hoge/fuga
20200725-144112,[Step08/08-DELETE|SOURCE],/home/pi/data/python/temp/hoge/fuga
20200725-144112,SCRIPT-DONE
```

![line.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274270/88ea396a-8c6d-afd0-f618-05522113998e.png)

久しぶりにスクリプト書いて楽しかった。