フォルダー内のファイル一覧をCSVで作成する
サブフォルダーを走査し、ファイルパス、拡張子、サイズ、更新日時を表として記録します。元のファイルと既存の結果をそのまま保持したまま、まず4つの小さなサンプルファイルで確認します。
まずファイルサイズで絞り込み、その後 SHA-256 digest を比較し、元ファイルを変更せずに確認専用の CSV を作成します。7ファイルの合成例を使い、どの path が report に含まれ、どれが含まれないかを確認します。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者削除判断と scan を分離したまま、重複の可能性があるファイルを特定したい人向けのガイドです。
この workflow では、まず files を byte size で group 化します。複数 path を含む size group だけ hash 計算が必要です。異なる sizes の files が完全に同じ bytes を持つことはありません。同じ size group 内で SHA-256 digests が一致した files を duplicate candidates とします。names の一致は必要条件でも十分条件でもありません。
candidate group は削除指示ではありません。同一 content が異なる folders で別の目的を持つ場合があり、digest も絶対的な equality proof ではありません。report は自動的に keep file を選ばず、一致したすべての paths を記録します。
以下の dataset は合成データであり、この記事のために作成したものです。コードを create_duplicate_demo.py として保存し、作業フォルダから実行してください。binary writes を使うことで contents を正確にします。newline は追加されず、empty files は 0 bytes です。
from pathlib import Path
DEMO = Path("outputs") / "duplicate_demo"
FILES = {
"archive/a_saved.txt": b"abc",
"empty_a.txt": b"",
"empty_b.txt": b"",
"notes/a.txt": b"abc",
"notes/a_copy.txt": b"abc",
"notes/other.txt": b"xyz",
"unique.txt": b"solo",
}
DEMO.parent.mkdir(parents=True, exist_ok=True)
DEMO.mkdir() # Refuse to reuse an existing destination.
for relative_path, content in FILES.items():
target = DEMO / relative_path
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as stream:
stream.write(content)
python create_duplicate_demo.py| 相対 path | 正確な text content | Bytes |
|---|---|---|
| archive/a_saved.txt | abc | 3 |
| empty_a.txt | Empty | 0 |
| empty_b.txt | Empty | 0 |
| notes/a.txt | abc | 3 |
| notes/a_copy.txt | abc | 3 |
| notes/other.txt | xyz | 3 |
| unique.txt | solo | 4 |
7つの files の合計は 16 bytes です。4つは size 3 ですが、notes/other.txt は3つの abc files とは異なる bytes を持ちます。これは equal size だけでは不十分であることを意図的に確認するための構成です。
python find_duplicate_files.pyscript は resolved source folder 内に report destination がある場合、それを拒否します。これにより scan が自身の output を含めることを防ぎます。relative paths は引き続き terminal の working directory に依存します。
scanner は traversal 中に見つかった symbolic links を skip し、non-regular file entries を無視します。traversal または read errors が発生すると実行を停止します。hashing 前後の metadata comparison により一部の concurrent changes を検出できますが、locked snapshot を作成するものではありません。
import csv
import hashlib
import os
import stat
from pathlib import Path
SOURCE = Path("outputs") / "duplicate_demo"
OUTPUT_DIR = Path("outputs") / "duplicate_review"
CHUNK_BYTES = 1024 * 1024
def signature(info: os.stat_result) -> tuple[int, ...]:
return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns)
def raise_walk_error(error: OSError) -> None:
raise error
def file_digest(path: Path, expected: os.stat_result) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
before = os.fstat(stream.fileno())
if not stat.S_ISREG(before.st_mode) or signature(before) != signature(expected):
raise RuntimeError(f"File changed before hashing: {path}")
while chunk := stream.read(CHUNK_BYTES):
digest.update(chunk)
after = os.fstat(stream.fileno())
if signature(after) != signature(expected) or signature(path.lstat()) != signature(expected):
raise RuntimeError(f"File changed during hashing: {path}")
return digest.hexdigest()
def main() -> None:
root = SOURCE.resolve(strict=True)
if not root.is_dir():
raise ValueError("SOURCE must be a directory.")
if OUTPUT_DIR.resolve().is_relative_to(root):
raise ValueError("The report folder must be outside SOURCE.")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Stop if the destination already exists.
by_size = {}
scanned = 0
for directory, directories, filenames in os.walk(
root, followlinks=False, onerror=raise_walk_error
):
base = Path(directory)
directories[:] = sorted(
name for name in directories if not (base / name).is_symlink()
)
for name in sorted(filenames):
path = base / name
info = path.lstat()
if not stat.S_ISREG(info.st_mode):
continue
by_size.setdefault(info.st_size, []).append((path, info))
scanned += 1
groups = []
hashed = 0
for size, members in sorted(by_size.items()):
if len(members) < 2:
continue
by_hash = {}
for path, initial in members:
digest = file_digest(path, initial)
by_hash.setdefault(digest, []).append(path)
hashed += 1
for digest, paths in sorted(by_hash.items()):
if len(paths) > 1:
ordered = sorted(paths, key=lambda path: path.as_posix())
groups.append((size, digest, ordered))
report = OUTPUT_DIR / "duplicates.csv"
with report.open("x", encoding="utf-8", newline="") as stream:
writer = csv.writer(stream)
writer.writerow([
"group_id", "size_bytes", "sha256", "relative_path", "review_status"
])
for number, (size, digest, paths) in enumerate(groups, start=1):
for path in paths:
writer.writerow([
f"G{number:03d}", size, digest,
path.relative_to(root).as_posix(), "UNREVIEWED"
])
matched = sum(len(paths) for _, _, paths in groups)
print(f"Scanned {scanned} regular file paths; hashed {hashed}.")
print(f"Duplicate candidates: {len(groups)} groups, {matched} paths.")
print(f"Review CSV: {report.as_posix()}")
if __name__ == "__main__":
main()
想定 report には header に加えて5つの data rows があります。groups は size、その後 digest の順に並び、各 group 内の paths は sort されます。以下の表では sha256 column を省略しています。ここでは digests を計算していないためです。script を実行するとその column が入力されます。
| group_id | size_bytes | relative_path | review_status |
|---|---|---|---|
| G001 | 0 | empty_a.txt | UNREVIEWED |
| G001 | 0 | empty_b.txt | UNREVIEWED |
| G002 | 3 | archive/a_saved.txt | UNREVIEWED |
| G002 | 3 | notes/a.txt | UNREVIEWED |
| G002 | 3 | notes/a_copy.txt | UNREVIEWED |
hashing が必要な files は6個です。2つの empty files と4つの three-byte files です。four-byte file には同じ size の peer がないため、hashing stage では skip されます。想定 console text は手作業で導出したもので、execution log ではありません。
Scanned 7 regular file paths; hashed 6.
Duplicate candidates: 2 groups, 5 paths.
Review CSV: outputs/duplicate_review/duplicates.csvempty files は意図的な placeholders の場合があります。contents が一致しても不要とは限らず、それらを削除しても file-content bytes は減りません。
これらは reader checks であり、この記事のために実行された tests ではありません。zero groups の completed report と、exception で停止した scan は別です。
| 症状 | 確認すること |
|---|---|
| FileNotFoundError | 先に setup を実行し、SOURCE と terminal の working directory を確認します。 |
| FileExistsError | 以前の destination を確認し、outputs の下に新しい folder を選択します。existing empty destinations も拒否されます。 |
| PermissionError or another read error | access problems を解決するか、source を意図的に狭めます。interrupted scan を complete として扱わないでください。 |
| File changed before or during hashing | source を変更する applications を停止し、新しい output destination で再 scan します。 |
| Unexpected byte sizes | text editor が newline を追加したか encoding を変更した可能性があります。binary-writing setup script で例を作り直します。 |
| Empty folder or partial report after failure | interrupted run は destination を rollback しません。その result を completed reports と分けて保持します。 |
hashing は content を chunks で読み取りますが、script は file metadata, paths, groups を memory に保持します。これは constant-memory inventory ではありません。比較するのは完全な bytes であり、visual similarity や document meaning ではありません。見た目が同じ documents でも digests が異なる場合があります。
report は independent physical copies ではなく paths を数えます。hard links は separate content storage を持たなくても複数の reported paths を生成する場合があるため、reported sizes がそのまま disk savings になるとは限りません。Windows junctions, mount points, hostile concurrent changes には symbolic-link checks を超える追加処理が必要です。
2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: csv, hashlib, os, pathlib, stat · 未実行
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。