تمرین و پروژه‌های پایتون

مرتب‌سازی خودکار فایل‌ها با پایتون؛ پروژه کامل File Organizer با Pathlib

مرتب‌سازی خودکار فایل‌ها با پایتون

🗓 آخرین به‌روزرسانی:

مرتب‌سازی خودکار فایل‌ها با پایتون یکی از کاربردی‌ترین پروژه‌ها برای یادگیری اتوماسیون است. اگر پوشه Downloads یا Desktop شما بعد از مدتی پر از PDF، عکس، ویدیو، فایل ZIP، فایل‌های Word و داده‌های مختلف شود، می‌توانید با چند ده خط پایتون برنامه‌ای بسازید که فایل‌ها را بر اساس پسوند شناسایی کند، پوشه مناسب را بسازد و هر فایل را به محل درست منتقل کند.

این مقاله نوزدهمین پروژه فاز اول دسته تمرین و پروژه‌های پایتون است. برخلاف پروژه‌های داده‌محور قبلی، اینجا مستقیماً با فایل‌سیستم سیستم‌عامل کار می‌کنیم. برای درک بهتر Path و عملیات فایل، مطالعه خواندن و نوشتن فایل‌های متنی در پایتون و مدیریت خطا در پایتون مفید است.

فهرست مطالب
  1. در این پروژه چه چیزی می‌سازیم؟
  2. چرا File Organizer پروژه خوبی برای اتوماسیون است؟
  3. مرحله اول: ساخت فایل پروژه
  4. مرحله دوم: وارد کردن ماژول‌ها
  5. مرحله سوم: تعریف دسته‌بندی فایل‌ها
  6. مرحله چهارم: پیدا کردن دسته هر فایل
  7. مرحله پنجم: ساخت پوشه مقصد
  8. مرحله ششم: حل مشکل فایل هم‌نام
  9. مرحله هفتم: انتقال یک فایل
  10. مرحله هشتم: اضافه کردن Dry Run
  11. مرحله نهم: مرتب‌سازی کل پوشه
  12. چرا از list(base_path.iterdir()) استفاده کردیم؟
  13. مرحله دهم: ساخت منوی ساده اجرا
  14. کد کامل File Organizer با پایتون
  15. نمونه خروجی Dry Run
  16. چرا حالت Dry Run مهم است؟
  17. اشتباه‌های رایج در پروژه مرتب‌سازی فایل‌ها
  18. چطور پروژه را حرفه‌ای‌تر کنیم؟
  19. تمرین پیشنهادی: مرتب‌سازی بر اساس سال و ماه
  20. این پروژه چه مفاهیمی را تمرین می‌دهد؟
  21. سوالات متداول
  22. جمع‌بندی

در این پروژه چه چیزی می‌سازیم؟

  • انتخاب یک پوشه برای مرتب‌سازی
  • پیمایش فایل‌های داخل پوشه
  • تشخیص نوع فایل از روی پسوند
  • دسته‌بندی تصاویر، اسناد، ویدیوها، صداها، آرشیوها و کدها
  • ساخت خودکار پوشه مقصد
  • انتقال فایل با shutil.move()
  • جلوگیری از خراب شدن فایل هنگام تداخل نام
  • نادیده گرفتن پوشه‌ها و خود فایل اسکریپت
  • حالت Dry Run برای پیش‌نمایش عملیات
  • گزارش تعداد فایل‌های منتقل‌شده و ردشده
  • مدیریت خطاهای Permission و FileNotFound
  • پشتیبانی از پسوندهای ناشناخته با پوشه Other

چرا File Organizer پروژه خوبی برای اتوماسیون است؟

در این پروژه با مفاهیمی کار می‌کنید که در بسیاری از ابزارهای واقعی اتوماسیون استفاده می‌شوند: پیمایش دایرکتوری، تشخیص نوع فایل، ساخت مسیر، تغییر نام، انتقال، مدیریت استثنا و گزارش عملیات. همین الگو را بعداً می‌توان برای بکاپ، پردازش عکس، مرتب‌سازی فایل‌های دانلودشده، پردازش گزارش‌ها یا مدیریت فایل‌های یک سرور توسعه داد.

مرحله اول: ساخت فایل پروژه

یک فایل با نام زیر ایجاد کنید:

file_organizer.py

برای تست بهتر، ابتدا یک پوشه آزمایشی بسازید و چند فایل با پسوندهای مختلف داخل آن قرار دهید. بهتر است در اولین اجرا سراغ پوشه مهم سیستم نروید.

مرحله دوم: وارد کردن ماژول‌ها

from pathlib import Path
import shutil

Pathlib کار با مسیرها را خواناتر می‌کند و shutil برای انتقال فایل‌ها استفاده می‌شود.

مرحله سوم: تعریف دسته‌بندی فایل‌ها

پسوندها را داخل یک Dictionary دسته‌بندی می‌کنیم:

FILE_CATEGORIES = {
    "Images": {
        ".jpg", ".jpeg", ".png",
        ".gif", ".webp", ".svg",
    },
    "Documents": {
        ".pdf", ".doc", ".docx",
        ".txt", ".rtf", ".odt",
        ".xls", ".xlsx", ".csv",
        ".ppt", ".pptx",
    },
    "Videos": {
        ".mp4", ".mkv", ".mov",
        ".avi", ".webm",
    },
    "Audio": {
        ".mp3", ".wav", ".flac",
        ".aac", ".m4a", ".ogg",
    },
    "Archives": {
        ".zip", ".rar", ".7z",
        ".tar", ".gz",
    },
    "Code": {
        ".py", ".js", ".html",
        ".css", ".json", ".xml",
        ".java", ".cpp", ".c",
    },
}

استفاده از Set باعث می‌شود بررسی عضویت پسوندها سریع و خوانا باشد.

مرحله چهارم: پیدا کردن دسته هر فایل

def get_category(file_path):
    extension = file_path.suffix.lower()

    for category, extensions in (
        FILE_CATEGORIES.items()
    ):
        if extension in extensions:
            return category

    return "Other"

اگر پسوند در هیچ گروهی نباشد، فایل به پوشه Other منتقل می‌شود. این تصمیم بهتر از حذف یا نادیده گرفتن فایل ناشناخته است.

مرحله پنجم: ساخت پوشه مقصد

def ensure_folder(base_path, category):
    destination = base_path / category
    destination.mkdir(
        parents=True,
        exist_ok=True,
    )
    return destination

exist_ok=True باعث می‌شود اگر پوشه از قبل وجود داشته باشد، برنامه خطا ندهد.

مرحله ششم: حل مشکل فایل هم‌نام

فرض کنید در پوشه Images از قبل فایل photo.jpg وجود دارد و فایل دیگری با همان نام قرار است منتقل شود. نباید فایل قبلی را ناخواسته جایگزین کنیم. برای این کار نام جدید می‌سازیم:

def unique_destination(destination):
    if not destination.exists():
        return destination

    stem = destination.stem
    suffix = destination.suffix
    parent = destination.parent
    counter = 1

    while True:
        candidate = parent / (
            f"{stem}_{counter}{suffix}"
        )

        if not candidate.exists():
            return candidate

        counter += 1

در نتیجه فایل‌های هم‌نام به شکل photo_1.jpg، photo_2.jpg و … ذخیره می‌شوند.

مرحله هفتم: انتقال یک فایل

def move_file(file_path, base_path):
    category = get_category(file_path)
    folder = ensure_folder(
        base_path,
        category,
    )

    target = unique_destination(
        folder / file_path.name
    )

    shutil.move(
        str(file_path),
        str(target),
    )

    return category, target

مرحله هشتم: اضافه کردن Dry Run

یک ابزار File Organizer بدون حالت Preview می‌تواند خطرناک باشد، چون بلافاصله فایل‌ها را جابه‌جا می‌کند. با Dry Run ابتدا فقط نشان می‌دهیم چه اتفاقی خواهد افتاد:

def move_file(
    file_path,
    base_path,
    dry_run=False,
):
    category = get_category(file_path)
    folder = base_path / category
    target = unique_destination(
        folder / file_path.name
    )

    if dry_run:
        return category, target

    folder.mkdir(
        parents=True,
        exist_ok=True,
    )

    shutil.move(
        str(file_path),
        str(target),
    )

    return category, target

این روش قبل از تغییر واقعی فایل‌ها یک لایه اطمینان ایجاد می‌کند.

مرحله نهم: مرتب‌سازی کل پوشه

def organize_folder(
    folder_path,
    dry_run=False,
):
    base_path = Path(folder_path).expanduser()

    if not base_path.exists():
        print("Folder does not exist.")
        return

    if not base_path.is_dir():
        print("The path must be a folder.")
        return

    moved = 0
    skipped = 0

    script_path = Path(__file__).resolve()

    for item in list(base_path.iterdir()):
        if not item.is_file():
            continue

        try:
            if item.resolve() == script_path:
                skipped += 1
                continue
        except OSError:
            skipped += 1
            continue

        try:
            category, target = move_file(
                item,
                base_path,
                dry_run=dry_run,
            )
        except (
            PermissionError,
            OSError,
        ) as error:
            print(
                f"Error for {item.name}: {error}"
            )
            skipped += 1
            continue

        action = (
            "would move"
            if dry_run
            else "moved"
        )

        print(
            f"{item.name} -> "
            f"{category} | {action}"
        )
        moved += 1

    print("\n=== Report ===")
    print(f"Processed: {moved}")
    print(f"Skipped/failed: {skipped}")

چرا از list(base_path.iterdir()) استفاده کردیم؟

هنگام پیمایش، برنامه هم‌زمان داخل همان پوشه زیرپوشه می‌سازد و فایل‌ها را جابه‌جا می‌کند. تبدیل نتیجه iterdir() به یک List باعث می‌شود ابتدا Snapshot اولیه از ورودی‌ها ساخته شود و سپس عملیات انتقال انجام شود.

مرحله دهم: ساخت منوی ساده اجرا

def main():
    folder = input(
        "Folder to organize: "
    ).strip()

    preview = input(
        "Preview only first? (y/n): "
    ).strip().lower()

    dry_run = preview == "y"

    organize_folder(
        folder,
        dry_run=dry_run,
    )

کد کامل File Organizer با پایتون

from pathlib import Path
import shutil

FILE_CATEGORIES = {
    "Images": {
        ".jpg", ".jpeg", ".png",
        ".gif", ".webp", ".svg",
    },
    "Documents": {
        ".pdf", ".doc", ".docx",
        ".txt", ".rtf", ".odt",
        ".xls", ".xlsx", ".csv",
        ".ppt", ".pptx",
    },
    "Videos": {
        ".mp4", ".mkv", ".mov",
        ".avi", ".webm",
    },
    "Audio": {
        ".mp3", ".wav", ".flac",
        ".aac", ".m4a", ".ogg",
    },
    "Archives": {
        ".zip", ".rar", ".7z",
        ".tar", ".gz",
    },
    "Code": {
        ".py", ".js", ".html",
        ".css", ".json", ".xml",
        ".java", ".cpp", ".c",
    },
}


def get_category(file_path):
    extension = file_path.suffix.lower()

    for category, extensions in (
        FILE_CATEGORIES.items()
    ):
        if extension in extensions:
            return category

    return "Other"


def unique_destination(destination):
    if not destination.exists():
        return destination

    stem = destination.stem
    suffix = destination.suffix
    parent = destination.parent
    counter = 1

    while True:
        candidate = parent / (
            f"{stem}_{counter}{suffix}"
        )

        if not candidate.exists():
            return candidate

        counter += 1


def move_file(
    file_path,
    base_path,
    dry_run=False,
):
    category = get_category(file_path)
    folder = base_path / category
    target = unique_destination(
        folder / file_path.name
    )

    if dry_run:
        return category, target

    folder.mkdir(
        parents=True,
        exist_ok=True,
    )

    shutil.move(
        str(file_path),
        str(target),
    )

    return category, target


def organize_folder(
    folder_path,
    dry_run=False,
):
    base_path = Path(
        folder_path
    ).expanduser()

    if not base_path.exists():
        print("Folder does not exist.")
        return

    if not base_path.is_dir():
        print("The path must be a folder.")
        return

    moved = 0
    skipped = 0
    script_path = Path(__file__).resolve()

    for item in list(base_path.iterdir()):
        if not item.is_file():
            continue

        try:
            if item.resolve() == script_path:
                skipped += 1
                continue
        except OSError:
            skipped += 1
            continue

        try:
            category, target = move_file(
                item,
                base_path,
                dry_run=dry_run,
            )
        except (
            PermissionError,
            OSError,
        ) as error:
            print(
                f"Error for {item.name}: {error}"
            )
            skipped += 1
            continue

        action = (
            "would move"
            if dry_run
            else "moved"
        )

        print(
            f"{item.name} -> "
            f"{category} | {action}"
        )
        moved += 1

    print("\n=== Report ===")
    print(f"Processed: {moved}")
    print(f"Skipped/failed: {skipped}")


def main():
    folder = input(
        "Folder to organize: "
    ).strip()

    preview = input(
        "Preview only first? (y/n): "
    ).strip().lower()

    organize_folder(
        folder,
        dry_run=(preview == "y"),
    )


if __name__ == "__main__":
    main()

نمونه خروجی Dry Run

Folder to organize: ~/Downloads
Preview only first? (y/n): y
photo.webp -> Images | would move
report.pdf -> Documents | would move
video.mp4 -> Videos | would move
project.zip -> Archives | would move
script.py -> Code | would move

=== Report ===
Processed: 5
Skipped/failed: 0

چرا حالت Dry Run مهم است؟

اسکریپت‌های اتوماسیون فایل می‌توانند در چند ثانیه صدها فایل را جابه‌جا کنند. یک اشتباه کوچک در مسیر یا منطق دسته‌بندی ممکن است باعث شود ساختار پوشه‌ها به‌هم بریزد. Dry Run امکان بررسی تصمیم‌های برنامه قبل از اعمال واقعی را فراهم می‌کند.

اشتباه‌های رایج در پروژه مرتب‌سازی فایل‌ها

مرتب‌سازی کل هارد بدون تست

برای شروع فقط یک پوشه آزمایشی انتخاب کنید. اجرای مستقیم روی Documents یا Desktop اصلی بدون Preview ایده خوبی نیست.

نادیده گرفتن فایل‌های هم‌نام

اگر مقصد از قبل همان نام را داشته باشد، رفتار انتقال ممکن است مشکل‌ساز شود. تابع unique_destination() این ریسک را کاهش می‌دهد.

پردازش پوشه‌ها مثل فایل

در این پروژه فقط item.is_file() پردازش می‌شود. اگر بخواهید زیرپوشه‌ها هم بررسی شوند باید منطق Recursive جداگانه اضافه کنید.

عدم مدیریت PermissionError

بعضی فایل‌ها ممکن است توسط برنامه دیگری باز باشند یا دسترسی لازم وجود نداشته باشد. مدیریت Exception باعث می‌شود کل پردازش به‌خاطر یک فایل متوقف نشود.

استفاده مستقیم از پسوند بدون lower()

.JPG و .jpg باید یکسان در نظر گرفته شوند؛ به همین دلیل پسوند را به حروف کوچک تبدیل می‌کنیم.

چطور پروژه را حرفه‌ای‌تر کنیم؟

  1. مرتب‌سازی Recursive: زیرپوشه‌ها را هم با rglob() بررسی کنید.
  2. مرتب‌سازی بر اساس تاریخ: داخل هر دسته پوشه سال و ماه بسازید.
  3. Undo: قبل از انتقال، Log ذخیره کنید تا عملیات قابل بازگشت باشد.
  4. قوانین سفارشی: تنظیمات دسته‌بندی را از JSON بخوانید.
  5. فیلتر حجم: فایل‌های بزرگ‌تر از یک مقدار مشخص را جدا کنید.
  6. حذف پوشه خالی: پس از پردازش، پوشه‌های خالی را شناسایی کنید.
  7. Watch Folder: با کتابخانه watchdog تغییرات Downloads را مانیتور کنید.
  8. GUI: با Tkinter انتخاب پوشه و دکمه Preview/Organize بسازید.
  9. Log File: تمام انتقال‌ها و خطاها را در فایل متنی ثبت کنید.
  10. قانون نام فایل: بر اساس Prefix یا Keyword نیز دسته‌بندی کنید.

تمرین پیشنهادی: مرتب‌سازی بر اساس سال و ماه

برای تصاویر، تاریخ آخرین تغییر فایل را دریافت کنید و مسیرهایی مانند زیر بسازید:

Images/
    2026/
        08/
            photo.webp

این تمرین ترکیب خوبی از File System، Timestamp و ساخت مسیر پویا است.

اگر از اتوماسیون فایل‌ها لذت بردید، در آموزش ساخت QR کد با پایتون از روی یک فایل CSV به‌صورت گروهی ده‌ها QR کد می‌سازیم و در پوشه خروجی ذخیره می‌کنیم.

این پروژه چه مفاهیمی را تمرین می‌دهد؟

  • Pathlib و Path
  • پسوند فایل با suffix
  • پیمایش دایرکتوری با iterdir
  • Set و Dictionary
  • shutil.move
  • ساخت پوشه با mkdir
  • تغییر نام برای جلوگیری از Conflict
  • Exception Handling
  • Dry Run
  • اتوماسیون فایل
  • گزارش‌گیری عملیات
  • تفکیک منطق برنامه به توابع

سوالات متداول

آیا این برنامه ممکن است فایل‌های من را حذف یا خراب کند؟

نه. برنامه فایل‌ها را فقط با shutil.move() جابه‌جا می‌کند و چیزی را حذف نمی‌کند. اگر در پوشه مقصد فایلی با همان نام باشد، تابع unique_destination() یک شماره به نام اضافه می‌کند تا فایل قبلی بازنویسی نشود. با این حال، بار اول حتماً با حالت Dry Run نتیجه را ببینید.

چطور یک پسوند یا دسته جدید اضافه کنم؟

کافی است دیکشنری FILE_CATEGORIES را ویرایش کنید؛ مثلاً پسوند .epub را به مجموعه Documents اضافه کنید یا یک کلید جدید مثل "Ebooks" با مجموعه پسوندهای خودش بسازید. پسوندهایی که در هیچ دسته‌ای نباشند به پوشه Other می‌روند.

این برنامه روی مک و لینوکس هم کار می‌کند؟

بله. ماژول‌های pathlib و shutil بخشی از کتابخانه استاندارد پایتون و چندسکویی هستند. فقط مسیر پوشه را به شکل همان سیستم‌عامل وارد کنید، مثلاً ~/Downloads؛ برنامه با expanduser() آن را به مسیر کامل تبدیل می‌کند.

فایل‌های داخل زیرپوشه‌ها هم مرتب می‌شوند؟

خیر. iterdir() فقط محتوای سطح اول پوشه را برمی‌گرداند و پوشه‌ها را رد می‌کند. اگر می‌خواهید زیرپوشه‌ها هم پیمایش شوند، می‌توانید از rglob("*") استفاده کنید، اما باید پوشه‌های مقصد مثل Images و Documents را از پیمایش کنار بگذارید.

چطور مرتب‌سازی را خودکار و زمان‌بندی‌شده اجرا کنم؟

ابتدا ورودی‌های input() را با مسیر ثابت جایگزین کنید، سپس اسکریپت را در ویندوز با Task Scheduler و در لینوکس یا مک با cron زمان‌بندی کنید. نمونه‌های بیشتر اتوماسیون را در ساخت ربات و اتوماسیون با پایتون ببینید.

جمع‌بندی

در این آموزش یک برنامه کامل برای مرتب‌سازی خودکار فایل‌ها با پایتون ساختیم. File Organizer ما می‌تواند فایل‌ها را بر اساس پسوند شناسایی کند، پوشه مناسب را ایجاد کند، فایل‌های هم‌نام را بدون overwrite مدیریت کند، خطاها را رد کند و قبل از اجرای واقعی با Dry Run نتیجه را پیش‌نمایش دهد.

این پروژه نقطه ورود بسیار خوبی به اتوماسیون واقعی با پایتون است. مرحله بعدی طبیعی آن، تغییر نام گروهی فایل‌هاست؛ چون بعد از مرتب‌سازی، معمولاً نیاز داریم نام فایل‌ها را نیز با یک الگوی منظم استاندارد کنیم.

📌 همچنین بخوانید: تمرین پایتون با جواب، ساخت بازی با پایتون و Pygame و ساخت ربات با پایتون

می‌خواهی پیشرفتت ذخیره شود؟با ورود یا ثبت‌نام رایگان، درس‌های تمام‌شده را علامت بزن و درصد پیشرفتت را در حساب کاربری ببین.ورود / ثبت‌نام برای ذخیره پیشرفت
درس بعدی · پروژه‌های پایتون (19 از 19)ساخت ربات تلگرام با پایتون؛ آموزش قدم‌به‌قدم از BotFather تا اولین پاسخ ←
آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *