0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Day10. ファイル整理ツール(拡張子ごとに仕分け) - 勝手にChatGPTチャレンジ (Python)

Last updated at Posted at 2025-12-10

前提

本日のお題


10. ファイル整理ツール(拡張子ごとに仕分け)

何を作る?
1つのフォルダに散らばるファイルを、拡張子ごとにサブフォルダへ自動仕分けするスクリプト。

学べること

  • os / pathlib でのファイル運用
  • 辞書で「拡張子 → カテゴリ」をマッピング
  • ユーザー入力で「本当に移動していい?」確認

面白いところ

  • ダウンロードフォルダが一気に片付く快感
  • 実行前に「ドライラン(dry-run)」モードを作るとツール感が増す

回答

コード

10_organize_file.py
"""
ファイル整理ツール(拡張子ごとに仕分け)

1つのフォルダに散らばるファイルを、拡張子ごとにサブフォルダへ自動仕分けする。
- デフォルトではカレントディレクトリを対象にする。
- ドライランで「どう移動されるか」だけ確認することも可能。

使い方:
    python organize_files.py               # カレントフォルダを整理(対話的に確認)
    python organize_files.py /path/to/dir  # 指定フォルダを整理
    python organize_files.py --dry-run     # 移動せず、予定だけ表示
    python organize_files.py --yes         # 確認なしで実行
"""

import argparse
import shutil
from pathlib import Path
from collections import Counter


# 拡張子 → カテゴリ名
# (必要に応じて自分の使い方に合わせて追加・変更してください)
EXT_TO_CATEGORY = {
    # 画像
    ".jpg": "images",
    ".jpeg": "images",
    ".png": "images",
    ".gif": "images",
    ".bmp": "images",
    ".tif": "images",
    ".tiff": "images",

    # ドキュメント
    ".pdf": "documents",
    ".doc": "documents",
    ".docx": "documents",
    ".xls": "documents",
    ".xlsx": "documents",
    ".ppt": "documents",
    ".pptx": "documents",
    ".txt": "documents",
    ".md": "documents",

    # アーカイブ
    ".zip": "archives",
    ".tar": "archives",
    ".gz": "archives",
    ".bz2": "archives",
    ".7z": "archives",
    ".rar": "archives",

    # 音声
    ".mp3": "audio",
    ".wav": "audio",
    ".flac": "audio",
    ".m4a": "audio",

    # 動画
    ".mp4": "videos",
    ".mov": "videos",
    ".avi": "videos",
    ".mkv": "videos",

    # コード / スクリプト
    ".py": "code",
    ".js": "code",
    ".ts": "code",
    ".html": "code",
    ".css": "code",
    ".c": "code",
    ".cpp": "code",
    ".cc": "code",
    ".h": "code",
    ".hpp": "code",
    ".java": "code",
    ".sh": "code",
    ".bat": "code",
}


def decide_category(path: Path) -> str:
    """ファイルパスからカテゴリ名を決める。該当がなければ 'others' にする。"""
    ext = path.suffix.lower()
    return EXT_TO_CATEGORY.get(ext, "others")


def collect_targets(base_dir: Path):
    """
    対象ディレクトリ直下のファイルだけを整理対象として集める。
    (サブフォルダも含めたい場合は iterdir() を walk に変える)
    """
    files = []
    for p in base_dir.iterdir():
        if p.is_file():
            files.append(p)
    return files


def build_plan(base_dir: Path):
    """
    どのファイルをどこに移動するかの「計画」を作る。
    return: list of (src_path, dst_path, category)
    """
    plan = []
    files = collect_targets(base_dir)
    for src in files:
        category = decide_category(src)
        dst_dir = base_dir / category
        dst = dst_dir / src.name
        plan.append((src, dst, category))
    return plan


def resolve_conflict(dst: Path) -> Path:
    """
    移動先に同名ファイルがある場合、ファイル名に _1, _2, ... をつけて衝突を回避する。
    """
    if not dst.exists():
        return dst

    stem = dst.stem
    suffix = dst.suffix
    parent = dst.parent

    i = 1
    while True:
        cand = parent / f"{stem}_{i}{suffix}"
        if not cand.exists():
            return cand
        i += 1


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="拡張子ごとにファイルをサブフォルダへ仕分けするツール"
    )
    parser.add_argument(
        "dir",
        nargs="?",
        type=Path,
        default=Path("."),
        help="整理対象ディレクトリ(デフォルト: カレントディレクトリ)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="実際には移動せず、どう整理されるかだけ表示する",
    )
    parser.add_argument(
        "--yes",
        "-y",
        action="store_true",
        help="確認なしで実行する(注意して使う)",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    base_dir: Path = args.dir.resolve()

    if not base_dir.exists() or not base_dir.is_dir():
        print(f"[ERROR] ディレクトリが存在しません: {base_dir}")
        return

    print(f"=== ファイル整理ツール ===")
    print(f"対象ディレクトリ: {base_dir}")
    print(f"ドライラン: {'ON' if args.dry_run else 'OFF'}")
    print()

    plan = build_plan(base_dir)

    if not plan:
        print("[INFO] 対象となるファイルがありません。")
        return

    # 自分自身のスクリプトは除外しておくと安全(任意)
    script_path = Path(__file__).resolve()
    filtered_plan = []
    for src, dst, category in plan:
        if src.resolve() == script_path:
            continue
        filtered_plan.append((src, dst, category))

    if not filtered_plan:
        print("[INFO] 整理対象のファイルはありません(自分自身のみなど)。")
        return

    # カテゴリごとの件数をざっくり出しておく
    counter = Counter()
    for src, dst, category in filtered_plan:
        counter[category] += 1

    print("=== 整理プラン(カテゴリ別件数) ===")
    for category, cnt in sorted(counter.items(), key=lambda x: x[0]):
        print(f"{category:10s}: {cnt:3d}")
    print()

    print("=== 移動プレビュー ===")
    for src, dst, category in filtered_plan:
        # 名前衝突を考慮した実際の移動先(dry-run でも計算だけはする)
        real_dst = resolve_conflict(dst)
        if real_dst != dst:
            print(f"[{category:10s}] {src.name} -> {real_dst.relative_to(base_dir)} (※名前衝突のためリネーム)")
        else:
            print(f"[{category:10s}] {src.name} -> {dst.relative_to(base_dir)}")

    print()

    if args.dry_run:
        print("[INFO] dry-run モードのため、実際の移動は行いません。")
        return

    if not args.yes:
        ans = input("本当にこれらのファイルを移動してよろしいですか? [y/N]: ").strip().lower()
        if ans not in ("y", "yes"):
            print("[INFO] キャンセルしました。ファイルは移動されません。")
            return

    # 実際に移動する
    moved = 0
    for src, dst, category in filtered_plan:
        dst_dir = dst.parent
        dst_dir.mkdir(parents=True, exist_ok=True)

        real_dst = resolve_conflict(dst)
        try:
            shutil.move(str(src), str(real_dst))
            moved += 1
        except Exception as e:
            print(f"[ERROR] 移動に失敗しました: {src} -> {real_dst} ({e})")

    print()
    print("=== 完了 ===")
    print(f"移動したファイル数: {moved}")


if __name__ == "__main__":
    main()

実行例

※ファイルは適当に準備しています。特に意味はないです。

# 実行前
tree /f 10_in
10_IN
    00_kadai.md
    01_number_hit.py
    02_db.json
    20251106195906.zip
    balenaEtcher-2.1.4.Setup.exe
    deeplabv3_test.png
    image01.jpg
    image03.gif

サブフォルダーは存在しません

# 実行
$python 10_organize_file.py 10_in          
=== ファイル整理ツール ===
対象ディレクトリ: C:\Users\kokekokko\Documents\Src\chatgpt-challenge-python\10_in
ドライラン: OFF

=== 整理プラン(カテゴリ別件数) ===
archives  :   1 件
code      :   1 件
documents :   1 件
images    :   3 件
others    :   2 件

=== 移動プレビュー ===
[documents ] 00_kadai.md -> documents\00_kadai.md
[code      ] 01_number_hit.py -> code\01_number_hit.py
[others    ] 02_db.json -> others\02_db.json
[archives  ] 20251106195906.zip -> archives\20251106195906.zip
[others    ] balenaEtcher-2.1.4.Setup.exe -> others\balenaEtcher-2.1.4.Setup.exe
[images    ] deeplabv3_test.png -> images\deeplabv3_test.png
[images    ] image01.jpg -> images\image01.jpg
[images    ] image03.gif -> images\image03.gif

本当にこれらのファイルを移動してよろしいですか? [y/N]: y

=== 完了 ===
移動したファイル数: 8

# 実行後
tree /f 10_in
10_IN
├─archives
│      20251106195906.zip
│
├─code
│      01_number_hit.py
│
├─documents
│      00_kadai.md
│
├─images
│      deeplabv3_test.png
│      image01.jpg
│      image03.gif
│
└─others
        02_db.json
        balenaEtcher-2.1.4.Setup.exe

感想

  • 特にコメントもなくやるだけ!みたいな課題でしたが、dry-runモードとか確認なし実行とか設計のお勉強になりました。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?