フォルダ内の不必要なファイルを一掃する

社内の共有フォルダなど、複数人が使用する環境あるあるとして、用途ごとにフォルダを分けて管理していても関係のないファイルがいつまでも放置される問題がありますよね。

定期的に誰かがチェックして削除していってもいいんですが「これは重要なんじゃないかな?」「これはまだ使いそうだから本人に確認しようかな?」といった人間ならではの雑念が入り込み作業を邪魔します。そこで血も涙もないコンピューターの出番です。

フォルダ内の特定のファイル以外を削除する

やることはフォルダ内のファイルを片っ端から見ていって不要と判断したファイルを消していくという単純明快なものですが、肝心なのは不要と判断する基準をどうするかです。今回はビジネスシーンのデファクトスタンダードなファイル形式であるExcelファイルに対象を絞ります。何でもかんでもExcelで作る日本の風習が吉とでました。

具体的にはExcelファイルの内容を見ていって、特定のセルに特定の値が入力されているファイルは必要と判断します。そしてそれ以外は容赦なく消し去ります。これをタスクスケジューラなどで定期実行させるなり、気が向いたときに実行するなりすれば、キレイな共有フォルダの完成です。

VBAでやってもいいのですが、Pythonの方が特にファイルを扱う観点から身軽そうなのでPythonでやります。

で、ソースコードは次のようになりました。disposal_dir.pyと命名(センスなし)。

import sys
import openpyxl as excel
import os
from datetime import datetime

p = sys.executable if getattr(sys, 'frozen', False) else __file__
cd = os.path.dirname(os.path.abspath(p))

wb = excel.open(os.path.join(cd, 'setting.xlsx'))
ws_setting = wb.active

keyword = ws_setting['a2'].value
cell_addr = ws_setting['b2'].value
sheet_name = ws_setting['c2'].value
tgt_dir = ws_setting['d2'].value
tgt_ext = ws_setting['e2'].value.split()
ec = ws_setting['f2'].value
exclusion = ec.split() if ec is not None else ec
logging_row = ws_setting.max_row

for tgt in os.listdir(tgt_dir):
    ext = os.path.splitext(tgt)[-1]
    fp = os.path.join(tgt_dir, tgt)

    if ext in tgt_ext:
        tgt_wb = excel.open(fp)

        if sheet_name in tgt_wb.sheetnames:
            if tgt_wb[sheet_name][cell_addr].value == keyword:
                continue

    if exclusion is not None:
        if os.path.basename(fp) in exclusion:
            continue

    if os.path.isdir(fp):
        continue

    os.remove(fp)
    logging_row += 1
    ws_setting.cell(row=logging_row, column=1).value = datetime.today()
    ws_setting.cell(row=logging_row, column=2).value = os.path.basename(fp)

wb.save(os.path.join(cd, 'setting.xlsx'))

削除しないExcelファイルを判定するための各種設定値の管理はこれまたExcelファイルでやります。スクリプトと設定値管理ファイルは監視対象フォルダとは別の場所で運用する想定です。

解説&使用方法

設定管理用Excelファイルは次のような構成です。

どうでもいい話ですが、フォントがクソなことに定評のあるWindowsですが、↑で使っているBIZ UDPゴシックがすごい見やすくて最近なんでもかんでもこれにしてます。

それぞれ次の設定値を入力します。

設定値説明
削除可否判定のためのキーワードさいたま
セルアドレス値が入っているセルのアドレスA1
シート名値が入っているシートの名前Sheet1
監視フォルダのパス処理の対象となるフォルダのパスD:\saitama
Excel拡張子ファイル内容を確認する拡張子.xlsx
削除除外ファイル名削除したくないファイルの名前notice.txt

これをsetting.xlsxとしてスクリプトと同じフォルダに配置することで実行時に読み込まれます。

上の表の例の設定値で実行した場合、D:\saitamaフォルダの.xlsx拡張子のファイルのSheet1シートのA1セルがさいたまの場合は削除しない。それ以外は削除する。ただしnotice.txtは削除しない。となります。

削除がおこなわれた場合はログへ日時とファイル名を記録します。

設定管理Excelファイルから設定値を読み込み変数に突っ込んでいるのがコードの次の部分です。なお、必須の設定値が空白であっても特にチェックしていないのでコケるだけです。はやりの自己責任です。

wb = excel.open(os.path.join(cd, 'setting.xlsx'))
ws_setting = wb.active

keyword = ws_setting['a2'].value
cell_addr = ws_setting['b2'].value
sheet_name = ws_setting['c2'].value
tgt_dir = ws_setting['d2'].value
tgt_ext = ws_setting['e2'].value.split()
ec = ws_setting['f2'].value
exclusion = ec.split() if ec is not None else ec
logging_row = ws_setting.max_row

拡張子tgt_extと除外ファイルexclusionは設定値をsplitでリストにして収めて、それをinで比較しているのでスペース区切りで複数指定ができます。逆に言うとスペースを含むファイル名はOUTということです。趣味プログラマーをやっているとスペース入りファイル名に拒絶反応が出て自分では絶対につけないようになりますが、実務の現場では蔓延していますよね。

メインのロジックは読み込んだ設定値を元に対象となるフォルダ内をループで走査していきます。ファイルパスから拡張子を判定して指定されたExcelファイルであれば開いて中身を確認します。

開いたファイルに指定されたシート名が存在するかを見て、存在するなら指定されたセルアドレスの値を取得します。取得した値が設定された値と合致していれば、そのファイルは削除対象外となるのでcontinueでループを抜けます

次に除外ファイル名であるかの確認をします。パスのファイル名が設定されている除外ファイル名と合致したならばcontinueでループを抜けます

最後にこのあとの削除メソッドでフォルダは消せないのでパスがフォルダだった場合はループを抜けます

ここまででループから抜けられなかったファイルは削除してよいことになりますので、すかさずos.remove()で消し去ります。

削除したあとは現在日時と、パスからファイル名を抜き出して設定管理ファイルのログエリアへ記録します。

以上がコードの次の部分です。

# フォルダ内のファイルをループで走査
for tgt in os.listdir(tgt_dir):
    ext = os.path.splitext(tgt)[-1]
    fp = os.path.join(tgt_dir, tgt)

    # 拡張子が設定値に含まれるか
    if ext in tgt_ext:
        tgt_wb = excel.open(fp)

        # シート名が設定値に含まれるか
        if sheet_name in tgt_wb.sheetnames:
            # セル値が設定値と等しいか
            if tgt_wb[sheet_name][cell_addr].value == keyword:
                continue

    # ファイル名が除外ファイル名に含まれるか
    if exclusion is not None:
        if os.path.basename(fp) in exclusion:
            continue

    # ファイルのつもりだけど実はフォルダではないか
    if os.path.isdir(fp):
        continue

    # 途中でループを抜けられなかったらここまで到達
    # ファイル削除
    os.remove(fp)
    # ログ記録
    logging_row += 1
    ws_setting.cell(row=logging_row, column=1).value = datetime.today()
    ws_setting.cell(row=logging_row, column=2).value = os.path.basename(fp)

使用例

Dドライブsaitamaというフォルダを作成しました。中に警告として※ここに所定のファイル以外を置いたら消し去る.txtを置きました。

設定管理ファイルに次のように設定値を入力しました。

SheetシートのA1セルがさいたまと入力されている.xlsxまたは.xlsmファイル以外を消す。ただし先の警告ファイル(~消し去る.txt)は消したくないので除外するという設定です。

設定した通りに動作するかテストのために削除対象と削除対象外のExcelファイルたくさん作成したいのですが、手作業じゃやってられないのでここはスクリプトでやりましょう。

次のようなコードをやっつけで書きました。

import openpyxl as excel
import random

wb = excel.Workbook()
ws = wb.active

cities = ('さいたま', '千葉', '横浜', '宇都宮', '水戸')

for i in range(100):
    city = cities[random.randint(0, 4)]
    ws['a1'].value = city
    fp = f'D:\\saitama\\{i}_{city}.xlsx'
    wb.save(fp)

実行するとExcelファイルが100個できます。これらのファイルのSheetシートのA1セルにはcitiesタプルからいずれかの都市名がランダムに入ります(OpenPyXlで新規ブックを作るとSheet1ではなくSheetというシート名になります)。

エクスプローラーからも簡単に判別できるようにA1に入れた都市名をファイル名にもつけるようにしました。Excelファイル以外にも適当にファイルを置いてフォルダ内は次のような状態になりました。

この中からファイル名にさいたまとついているファイルだけが生き残るはずです。Excelファイル以外では除外で設定した~消し去る.txtだけが生き残るはずです。

では、disposal_dir.pyスクリプトを実行します。フォルダ内は次のような状態になりました。

さいたま以外は消されたようです。除外設定しているテキストファイル以外も消えています。よさそうですね。

設定管理ファイルを開いてみると、ログが記録されています。

削除ログを見てふと思ったんですが、Pythonってそんな順序でファイル名を取り出していくんですね。

アプリを入手する

実行ファイル形式のスクリプトと設定管理ファイルを次からダウンロードできます。

利用上のご注意

  • ダウンロードしたファイルを利用したことにより生じた結果については、利用者ご自身に責任を負っていただきます。
  • ご利用前に使用方法をご確認ください。
  • 当方は成果物の正確性について最善を尽くしますが保証はいたしません。
  • Windows11 Microsoft365 環境でのみ動作確認済み。

Downloadボタンを押下した時点で注意事項に同意したものとみなします。

disposal_dir.zip

まとめ

今回は消すだけなので、その気になればVBAでもいけそうですが、これをたとえば削除対象はいったんzipにぶち込んで別フォルダへプールしておく、その後一定期間経過したら消す、みたいなことをPythonなら少し書き換えるだけで、しかも標準ライブラリだけでいけます。VBAでやれって言われたら絶望しますよね。

なので事務用PCにも一刻もはやくPythonを標準装備にすべきだと私は主張します。

おわり。