【Python】フォルダーのファイル台帳を作る:パス・サイズ・更新日時の収集

PythonのTopに戻る

ファイルの内容を読まずに台帳を作る

フォルダーのファイル名、サイズ、更新日時を一覧にしたいなら、os.scandir()で項目を走査し、stat情報を集める。ファイル内容を全部開く必要はない。処理中に読めない項目があっても全体を停止するのか、失敗を記録して続けるのかを先に決めると、部分的な台帳を完全なものと取り違えずに済む。

ここではシンボリックリンクをたどらず、通常ファイルだけを台帳へ入れる。フォルダーの走査失敗と個別項目の情報取得失敗は、相対パスと例外の種類で記録する。更新日時はタイムゾーン付きのUTC表記とする。作成日時や最後に読んだ日時とは別の情報である。

走査と個別項目の失敗を分けて記録する

example.py

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
import os
import stat


def inventory(root):
    root = Path(root)
    pending, rows, errors = [root], [], []
    while pending:
        directory = pending.pop()
        try:
            with os.scandir(directory) as entries:
                items = sorted(entries, key=lambda entry: entry.name)
        except OSError as error:
            errors.append((directory.relative_to(root).as_posix(), type(error).__name__))
            continue
        for entry in items:
            path = Path(entry.path)
            relative = path.relative_to(root).as_posix()
            try:
                info = entry.stat(follow_symlinks=False)
                if stat.S_ISDIR(info.st_mode):
                    pending.append(path)
                elif stat.S_ISREG(info.st_mode):
                    modified = datetime.fromtimestamp(info.st_mtime, timezone.utc).isoformat()
                    rows.append((relative, info.st_size, modified))
            except OSError as error:
                errors.append((relative, type(error).__name__))
    return sorted(rows), sorted(errors)


with TemporaryDirectory() as temporary:
    root = Path(temporary)
    (root / "blocked").mkdir()
    (root / "a.txt").write_bytes(b"abc")
    os.utime(root / "a.txt", (0, 0))
    (root / "link.txt").symlink_to(root / "a.txt")
    real_scandir = os.scandir

    def controlled_scandir(path):
        if Path(path).name == "blocked":
            raise PermissionError("simulated for this example")
        return real_scandir(path)

    with patch("os.scandir", side_effect=controlled_scandir):
        rows, errors = inventory(root)
    assert rows == [("a.txt", 3, "1970-01-01T00:00:00+00:00")]
    assert errors == [("blocked", "PermissionError")]
    print("files:", rows)
    print("errors:", errors)

実行結果

files: [('a.txt', 3, '1970-01-01T00:00:00+00:00')]
errors: [('blocked', 'PermissionError')]

サイズと日時が意味するもの

st_sizeは通常ファイルの論理的なバイト数であり、文字数ではない。UTF-8の日本語なら一文字が複数バイトになる。また、圧縮やスパースファイルなどではディスク上の使用量と一致しないことがある。容量調査が目的なら、何のサイズを比べたいのかを明確にする。

st_mtimeは内容の最終変更時刻である。例の1970年は実行時に現在時刻を偽って観測した値ではなく、結果を再現可能にするためos.utime()で一時ファイルの時刻を0へ設定したものだ。UTCへ変換して+00:00を付けると、閲覧するPCのローカル時刻に依存しない表記になる。

st_ctimeを作成日時だと思って使うと、OSごとの差で誤解が起きる。特にUnix系ではメタデータ変更時刻であり、ファイルの誕生日とは異なる。日時の精度もファイルシステムによって違うため、表示された細かい桁まで同じ精度で測れているとは限らない。精密な比較が必要ならst_mtime_nsと保存側の精度を確認する。

エラーを再現して継続を確かめる

この例のPermissionErrorは、実機のアクセス権を変更して発生させたものではない。blockedという一時フォルダーへのscandir呼び出しだけをmockで差し替え、失敗を制御している。実行ユーザーの権限によってテスト結果が変わらず、既存フォルダーの権限にも触れない。エラーが記録され、正常なa.txtは残ることをassertで確かめた。

with os.scandir(…)により列挙用の資源を閉じる。follow_symlinks=Falseのstatではリンク自身の情報を取得し、通常ファイルでもディレクトリでもないリンクをここでは台帳から除外する。リンク先を含めたい場合は、循環や同じ実体の重複、基準フォルダーの外へ出ることを別途考える必要がある。

一覧作成中にもファイルは変わりうる。列挙できた直後に削除されればstatが失敗し、サイズ取得後に更新されれば台帳はすぐ古くなる。この台帳は走査中に観測した情報の集まりであり、ある一瞬の完全なスナップショットではない。開始・終了時刻とエラー件数を添えると、後から条件を理解しやすい。対象の保存先を他者が同時に変更できる環境では、リンクをたどらない指定だけを完全な境界保護とみなさない。

台帳として保存するとき

rowsをCSVなどへ保存する場合は、列名をpath、size_bytes、modified_utcのように単位と基準が分かる名前にする。errorsも別に保存し、0件でないなら不完全な可能性を表示する。ファイル名やパスだけでも個人名・プロジェクト名が含まれることがあるため、台帳を共有するときは内容と公開範囲を確認する。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:フォルダー内の対象ファイルだけ集める:globと除外条件 / 解析をやり直せる実行記録を残す:条件・入力ハッシュ・環境情報

PythonのTopに戻る