この記事は3部構成のシリーズです。

  • 前編(本記事):なぜ必要か・何を判定基準にするか・Pythonスクリプトの実装
  • 中編(0081):設定のstockphoto.json統合・batファイルとStreamlit UIへの組み込み
  • 後編(0082):発生したエラーと対処・完成構成と運用フロー

このシリーズは、以下のような方を想定しています。

  • Adobe Stockへの審査申請前に画像を手動削除していて、ExcelのSTATUS列への記入が手間になっている方
  • Excelに「ファイルが存在しない」情報を自動で書き込みたい方
  • PythonのopenpyxlとPathlibを組み合わせた実用スクリプトを作りたい方

前記事(006)では、Adobe Stockの審査結果をExcelのSTATUS列に自動反映し、さらにOK/NGフォルダへ自動仕分けする仕組みを実装しました。 これにより analysis_results.xlsx のSTATUS列には OK / NG / EXAM / 空白 の4種類の値が揃うようになっています。

ところが運用を続けるうちに、「空白」の正体が一種類ではないことに気づきました。 「まだ審査結果が出ていない行」と「審査に出す前に手動削除した行」が同じ空白として混在しているのです。 この2つを区別できないまま件数が増えると、管理精度が落ちていきます。 本シリーズではこの問題に対処するため、ファイルの存在有無を自動チェックしてSTATUSを記入するスクリプトを実装した記録をまとめます。

なぜ手動削除チェックが必要になったのか

Adobe Stockに審査を申請するまでのフローの中に、目視によるNG除外という工程があります(001記事参照)。 AIで生成した画像の中から、指や目が崩れているもの、明らかに品質が低いもの、使い道のない構図のものを、エクスプローラー上で手動削除する作業です。 この操作で消えるのはファイルだけです。 analysis_results.xlsx の対応行はSTATUS列が空白のまま残り続けます。

これが運用上どのような問題を引き起こすかというと、具体的には以下の3点です。

  • 空白行の意味が曖昧になる:「審査中」「申請前」「手動削除済み」が同じ見た目になり、残件数が正確に把握できなくなる
  • 006記事の仕分け処理に影響しない:仕分けはSTATUSが確定した行だけを動かすため直接的な影響はないが、空白行が増えるほどExcelの見通しが悪くなる
  • 手動記入コストが増大する:100件・200件と削除が続くと、1行ずつ「手動削除」と入力する作業は現実的ではなくなる

「空白」の内訳を整理する

審査中(EXAM中):Adobe Stockに申請済みで審査結果待ち。ファイルは image_stockphoto_updone_examafter に存在する。
申請前・未処理:まだ審査に出していない。ファイルはいずれかの作業フォルダに存在する。
手動削除済み:申請前に目視NGで削除済み。ファイルはどこにも存在しない。← ここを自動判定したい

①と②はいずれかのフォルダにファイルが存在しますが、③はどこにも存在しません。 つまり「ファイルが存在するかどうか」という一点だけで、③を他の2種類から分離できます。 この判定はPythonに任せられます。

手動削除の判定方法:「ファイルが存在しない=削除済み」

判定のロジックはシンプルです。 ExcelのB列に記録されているファイル名が、画像検索ルートフォルダの配下(サブフォルダ含む)に一切存在しない場合、そのファイルは削除済みとみなすという考え方です。

このプロジェクトでは、全画像ファイルが E:\temp\ai_stockphoto 配下のいずれかのフォルダに存在しています。 アップスケール前フォルダ、アップスケール後フォルダ、審査申請フォルダなど複数のフォルダに分散していますが、削除されていない限りどこかに必ずファイルがあります。 このルートを丸ごと再帰検索して見つからなければ「削除済み」と判断できます。

以下のようなフォルダ構成で画像管理やモジュール管理をしています。手動削除した場合は、ゴミ箱に移動されるので、下図のフォルダ群からは見つかりません。

Adobeストックフォト管理フォルダ群
Excelのファイル名(B列)検索ルート配下に存在するかSTATUS列の現在値処理
20260514_023.jpg✅ あり空白何もしない
20260514_047.jpg❌ なし空白「手動削除」を記入
20260514_088.jpg✅ ありOKSKIP(上書きしない)
20260514_099.jpg❌ なしNGSKIP(上書きしない)

比較は全ファイル名をPythonの set 型に格納してから行います。 set はハッシュテーブルによる実装のため、1万件のファイル名に対しても1件の検索がほぼO(1)で完了します。 ループでリストを線形検索する方式と比べて、数千〜数万件規模での処理速度が大幅に変わります。

処理が終わると下図のようにSTATUS列に「手動削除」と書き込まれます。

Adobeストックフォト管理Excel手動削除記入済画像

大文字小文字の統一

Excelのセル値とファイルシステム上のファイル名を比較するとき、どちらも .lower() で小文字に統一してから比較します。 Windowsのファイルシステムは大文字小文字を区別しないため、 Excelに IMG_0001.JPG と記録されていてもファイル名が img_0001.jpg であれば同一として扱われます。 統一しないと大文字小文字の違いだけで「存在しない」と誤判定するリスクがあります。

shudou_delete.pyの実装

shudou_delete.py の処理フローを整理します。

処理の流れ

  • コマンドライン引数で stockphoto.json のパスを受け取る(省略時は自スクリプトから上位フォルダを順に検索)
  • JSONの shudou_delete セクションから各種設定を読み込む
  • openpyxlで analysis_results.xlsx を読み込む
  • 画像検索ルート配下の全ファイル名を小文字で set として保持
  • ヘッダー行の次行から最終行まで順にチェック
  • STATUS列(T列)に値がある行 → SKIP
  • ファイル名列(B列)が空の行 → スルー
  • ファイル名が set に存在しない → STATUS列に「手動削除」を書き込む
  • 最後にExcelを保存し、サマリーを出力する

ファイル一覧の構築部分は以下のコードです。Path.rglob("*") でサブフォルダを含む全ファイルを列挙し、ファイル名のみ(パスなし)を小文字でsetに入れます。

existing_files = {
    p.name.lower()
    for p in Path(search_root).rglob("*")
    if p.is_file()
}

チェックのメインループはシンプルです。STATUS列が空白の行のみを対象に絞り、ファイル名がsetに含まれなければ書き込みます。

for row in range(header_row + 1, ws.max_row + 1):

    status_cell = ws.cell(row=row, column=status_col)

    # STATUS列に値があればスキップ
    if status_cell.value not in (None, ""):
        skipped += 1
        continue

    file_name = ws.cell(row=row, column=filename_col).value
    if not file_name:
        continue

    file_name = str(file_name).strip().lower()
    checked += 1

    if file_name not in existing_files:
        status_cell.value = marker_text   # 例:"手動削除"
        deleted += 1

処理後のコンソール出力(ログ)の形式は以下の通りです。

出力ラベル意味
検出ファイル数 : 5,291検索ルート配下で見つかったファイルの総数
チェック対象 : 996STATUS列が空白だった行数(処理対象)
手動削除 : 1ファイルが存在せず「手動削除」を記入した行数
SKIP : 1,635STATUS列にすでに値があってスキップした行数

中編(0081記事)では、この shudou_delete.py をプロジェクト全体の設定ファイル stockphoto.json に組み込み、 batファイルとStreamlit UIから呼び出せる形に整える手順を解説します。

FAQ

search_rootを正しく設定すれば誤判定はありませんか?

設定が正しければ誤判定は起きません。ただし画像ファイルを search_root の範囲外(別ドライブや別フォルダ)に移動した場合、ファイルが存在するにもかかわらず「手動削除」と記入されてしまいます。 実行前に search_root が全画像を網羅する範囲になっているかを確認することを推奨します。 誤って記入されても、該当セルを手動で削除(空白に戻す)すれば問題ありません。

処理速度はどのくらいですか?

ファイル一覧の構築(rglob)はディスクI/Oに依存しますが、5,000件程度であれば数秒以内で完了します。 Excelのチェックループはopenpyxlのメモリ内操作のため高速で、1,000〜2,000行のExcelでも合わせて10秒前後が目安です。 比較に set を使うことで、ファイル数が増えても検索コストはほぼ変わりません。

STATUS列が空白でなくても記入されることはありますか?

ありません。スクリプトはSTATUS列が None または空文字の行のみを処理対象とします。 OKNGEXAM などすでに値が入っている行はすべてSKIPカウントされ、既存の値を上書きすることはありません。