إذا كنت تستخدم مجلد Downloads يوميًا، فغالبًا ستجد بعد فترة صورًا وملفات PDF وجداول Excel وفيديوهات وملفات ZIP كلها في مكان واحد. بدل ترتيبها يدويًا كل مرة، يمكن تحويل هذه المهمة إلى مشروع عملي باستخدام بايثون.
في هذا الدرس من سلسلة بايثون بالمثال سنبني برنامجًا ينظم الملفات تلقائيًا حسب النوع والامتداد. سيقرأ الملفات الموجودة داخل مجلد تختاره، ثم يحدد امتداد كل ملف باستخدام pathlib، وينشئ مجلدات مثل Images وPDF وExcel وVideos وArchives، ثم ينقل الملفات إلى أماكنها باستخدام shutil.move().
لن نكتفي بالنسخة البسيطة. سنضيف وضع Preview حتى ترى ما الذي سيفعله البرنامج قبل تحريك أي ملف، وسنتعامل مع مشكلة الأسماء المتكررة، والملفات المفتوحة، والامتدادات غير المعروفة، والمجلدات الموجودة داخل مجلد المصدر.
{alertInfo} فكرة المشروع: اقرأ كل ملف داخل المجلد، استخرج امتداده مثل.jpgأو
{getToc} $title={محتوى المقال}
ماذا سنبني في هذا المشروع؟
لنفترض أن مجلدًا اسمه Downloads يحتوي:
Downloads/
├── photo.jpg
├── logo.png
├── report.pdf
├── invoice.xlsx
├── notes.txt
├── video.mp4
├── backup.zip
└── file_without_extension
بعد تشغيل البرنامج نريد أن يصبح التنظيم قريبًا من:
Downloads/
├── Images/
│ ├── photo.jpg
│ └── logo.png
├── PDF/
│ └── report.pdf
├── Excel/
│ └── invoice.xlsx
├── Text/
│ └── notes.txt
├── Videos/
│ └── video.mp4
├── Archives/
│ └── backup.zip
└── Other/
└── file_without_extension
المكتبات التي سنستخدمها
المشروع لا يحتاج إلى تثبيت مكتبات خارجية. سنستخدم pathlib للتعامل مع المسارات والملفات، وshutil لنقل الملفات.
from pathlib import Path
import shutil
اختيار مجلد المصدر
في البداية لا تجرب البرنامج مباشرة على Downloads الحقيقي. أنشئ مجلد اختبار يحتوي عدة ملفات:
source_folder = Path("test_downloads")
وعلى Windows تستطيع استخدام مسار كامل:
source_folder = Path(
r"C:\Users\User\Downloads"
)
{alertWarning} مهم: ابدأ بمجلد تجريبي، ثم استخدم وضع Preview قبل النقل الحقيقي. البرنامج سيغير أماكن الملفات، لذلك يجب التأكد من القواعد أولًا.
قراءة محتويات المجلد باستخدام pathlib
for item in source_folder.iterdir():
print(item)
iterdir() تمر على العناصر الموجودة مباشرة داخل المجلد. قد يكون العنصر ملفًا أو مجلدًا، لذلك نفلتر الملفات:
for item in source_folder.iterdir():
if item.is_file():
print(item.name)
استخراج امتداد الملف باستخدام suffix
file_path = Path("photo.jpg")
print(file_path.suffix)
الناتج:
.jpg
ومن الأفضل استخدام lower() حتى نتعامل مع .JPG و.jpg بالطريقة نفسها:
extension = item.suffix.lower()
إنشاء خريطة امتدادات الملفات
CATEGORIES = {
"Images": {
".jpg", ".jpeg", ".png", ".webp", ".gif"
},
"PDF": {
".pdf"
},
"Excel": {
".xlsx", ".xls", ".csv"
},
"Documents": {
".doc", ".docx", ".odt"
},
"Text": {
".txt", ".md"
},
"Videos": {
".mp4", ".mkv", ".avi", ".mov"
},
"Audio": {
".mp3", ".wav", ".m4a", ".flac"
},
"Archives": {
".zip", ".rar", ".7z", ".tar", ".gz"
}
}
دالة تحدد فئة الملف
def get_category(extension):
for category, extensions in CATEGORIES.items():
if extension in extensions:
return category
return "Other"
إذا كان الامتداد معروفًا، ترجع الدالة اسم الفئة. وإذا لم تجد تطابقًا، ترجع Other.
اختبار التصنيف
print(get_category(".jpg"))
print(get_category(".pdf"))
print(get_category(".xlsx"))
print(get_category(".unknown"))
الناتج:
Images
PDF
Excel
Other
الملفات التي لا تملك امتدادًا
إذا كان الملف اسمه LICENSE، فغالبًا ستكون suffix فارغة. في برنامجنا سيذهب هذا الملف إلى Other ما لم تضف قاعدة خاصة له.
إنشاء مجلد الفئة تلقائيًا
destination_folder = (
source_folder / "Images"
)
destination_folder.mkdir(
parents=True,
exist_ok=True
)
exist_ok=True تمنع ظهور خطأ لمجرد أن المجلد موجود أصلًا.
نقل ملف باستخدام shutil.move
source_file = source_folder / "photo.jpg"
destination_file = (
source_folder / "Images" / "photo.jpg"
)
shutil.move(
str(source_file),
str(destination_file)
)
النسخة البسيطة من منظم الملفات
from pathlib import Path
import shutil
source_folder = Path("test_downloads")
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".webp"},
"PDF": {".pdf"},
"Excel": {".xlsx", ".xls", ".csv"},
"Videos": {".mp4", ".mkv", ".avi"},
"Archives": {".zip", ".rar", ".7z"}
}
def get_category(extension):
for category, extensions in CATEGORIES.items():
if extension in extensions:
return category
return "Other"
for item in source_folder.iterdir():
if not item.is_file():
continue
extension = item.suffix.lower()
category = get_category(extension)
destination_folder = source_folder / category
destination_folder.mkdir(exist_ok=True)
destination_file = destination_folder / item.name
shutil.move(
str(item),
str(destination_file)
)
هذا الكود يشرح الفكرة الأساسية، لكنه يحتاج تحسينًا مهمًا قبل استخدامه على مجلد حقيقي: ماذا يحدث إذا كان اسم الملف موجودًا أصلًا في الوجهة؟
منع تعارض أسماء الملفات
نريد بدل المخاطرة بملف يحمل الاسم نفسه أن تصبح الأسماء مثل:
photo.jpg
photo_1.jpg
photo_2.jpg
نستخدم دالة تبحث عن أول اسم غير مستخدم:
def get_unique_destination(
destination_folder,
file_name
):
destination = destination_folder / file_name
if not destination.exists():
return destination
file_path = Path(file_name)
counter = 1
while True:
new_name = (
f"{file_path.stem}_{counter}"
f"{file_path.suffix}"
)
destination = destination_folder / new_name
if not destination.exists():
return destination
counter += 1
ما الفرق بين stem وsuffix؟
file_path = Path("report.pdf")
print(file_path.stem)
print(file_path.suffix)
الناتج:
report
.pdf
وهذا يسمح لنا بإنشاء report_1.pdf بدون كسر الامتداد.
إضافة وضع Preview قبل تحريك الملفات
هذه من أهم إضافات المشروع. نبدأ بـ:
PREVIEW = True
ثم بدل النقل مباشرة:
if PREVIEW:
print(
f"[PREVIEW] {item.name}"
f" -> {destination_file}"
)
else:
shutil.move(
str(item),
str(destination_file)
)
لماذا Preview مهمة؟
لأن الخطأ في قاعدة تصنيف واحدة قد يرسل مجموعة ملفات إلى المكان غير المناسب. المعاينة تسمح لك برؤية المصدر والوجهة قبل أن يتغير أي ملف فعليًا.
تشغيل النقل الحقيقي
بعد مراجعة مخرجات Preview والتأكد أن المسار والتصنيفات صحيحة:
PREVIEW = False
إضافة إحصاءات حسب الفئة
stats = {}
stats[category] = (
stats.get(category, 0) + 1
)
وفي النهاية:
for category, count in stats.items():
print(category, count)
اختيار Downloads تلقائيًا
في كثير من الأجهزة يمكن بناء المسار هكذا:
SOURCE_FOLDER = (
Path.home() / "Downloads"
)
print(SOURCE_FOLDER)
لكن تحقق من المسار قبل التنفيذ، لأن مكان مجلد التنزيلات قد يختلف حسب إعداد النظام.
السؤال عن المسار بدل تثبيته داخل الكود
folder_text = input(
"أدخل مسار المجلد: "
).strip()
SOURCE_FOLDER = Path(folder_text)
if not SOURCE_FOLDER.is_dir():
print("المسار غير موجود أو ليس مجلدًا.")
raise SystemExit
التعامل مع PermissionError
قد يفشل النقل إذا لم تكن لديك صلاحية كافية أو كان الملف مستخدمًا من برنامج آخر في بعض الأنظمة.
try:
shutil.move(
str(item),
str(destination_file)
)
except PermissionError:
print(
"تعذر نقل الملف بسبب الصلاحيات "
"أو لأنه مستخدم:",
item.name
)
التعامل مع FileNotFoundError
قد يختفي ملف أو يتغير بين لحظة قراءته ولحظة نقله:
try:
shutil.move(
str(item),
str(destination_file)
)
except FileNotFoundError:
print("لم يعد الملف موجودًا:", item.name)
except PermissionError:
print("تعذر الوصول إلى الملف:", item.name)
لماذا لا نستخدم except فارغًا؟
الكود التالي يخفي سبب المشكلة:
try:
...
except:
pass
في برامج إدارة الملفات، إخفاء الخطأ خطر لأنه قد يجعلك تظن أن كل الملفات انتقلت بينما بعضها فشل. التقط الأخطاء المتوقعة واطبع اسم الملف الذي فشل.
تجاهل المجلدات الموجودة داخل المصدر
برنامجنا يستخدم:
if not item.is_file():
continue
وبذلك يتعامل فقط مع الملفات الموجودة في المستوى الحالي ولا يدخل إلى المجلدات الفرعية.
لماذا لا نستخدم rglob هنا؟
يمكن استخدام rglob() للبحث داخل المجلدات الفرعية، لكن ذلك قد يعيد تنظيم مجلدات مرتبة أصلًا. لذلك هدف هذا المشروع محدد: تنظيم الملفات الموجودة مباشرة داخل المجلد الذي تختاره.
تجاهل الملفات المخفية اختياريًا
if item.name.startswith("."):
continue
اجعل هذا الخيار مناسبًا لاحتياجك؛ لا يوجد سبب لفرضه في كل مشروع.
تجاهل ملف البرنامج نفسه
إذا وضعت organizer.py داخل مجلد المصدر، احمه من النقل أو الأفضل شغّل البرنامج من مجلد منفصل.
SKIP_FILES = {
"organizer.py"
}
if item.name in SKIP_FILES:
continue
تنظيم ملفات Excel فقط
إذا كان هدفك محددًا، لا تحتاج إلى كل الفئات:
EXCEL_EXTENSIONS = {
".xlsx",
".xls",
".csv"
}
for item in SOURCE_FOLDER.iterdir():
if (
item.is_file()
and item.suffix.lower()
in EXCEL_EXTENSIONS
):
print(item.name)
نقل الصور فقط
IMAGE_EXTENSIONS = {
".jpg",
".jpeg",
".png",
".webp"
}
وبنفس المنطق تستطيع نقل ملفات PDF أو الفيديو فقط. هذه الأمثلة تساعدك على تعديل المشروع بدل نسخه كما هو.
ماذا عن الامتداد .tar.gz؟
suffix تعيد آخر امتداد، بينما suffixes تعرض كل الأجزاء:
path = Path("backup.tar.gz")
print(path.suffix)
print(path.suffixes)
وبما أن .gz موجودة في Archives في قواعدنا، سيظل التصنيف مناسبًا للمثال.
التنظيم حسب الامتداد ليس فحصًا لمحتوى الملف
برنامجنا يعتمد على اسم الامتداد. إذا أعيدت تسمية ملف بامتداد لا يمثل محتواه الحقيقي، فقد يصنف ضمن الفئة الخطأ. هذه الطريقة مناسبة لتنظيم الملفات اليومية، وليست أداة للتحقق الأمني من نوع الملف.
النقل أم النسخ؟
shutil.move() تنقل الملف من موقعه الأصلي. إذا كنت تريد تجربة أكثر تحفظًا، يمكنك نسخ الملفات بدل نقلها:
shutil.copy2(
str(item),
str(destination_file)
)
بعد التأكد من النتيجة يمكنك استخدام النقل في النسخة النهائية.
نسخة كاملة أكثر أمانًا
from pathlib import Path
import shutil
SOURCE_FOLDER = Path("test_downloads")
PREVIEW = True
SKIP_FILES = {
"organizer.py"
}
CATEGORIES = {
"Images": {
".jpg", ".jpeg", ".png", ".webp", ".gif"
},
"PDF": {
".pdf"
},
"Excel": {
".xlsx", ".xls", ".csv"
},
"Documents": {
".doc", ".docx", ".odt"
},
"Text": {
".txt", ".md"
},
"Videos": {
".mp4", ".mkv", ".avi", ".mov"
},
"Audio": {
".mp3", ".wav", ".m4a", ".flac"
},
"Archives": {
".zip", ".rar", ".7z", ".tar", ".gz"
},
"Python": {
".py", ".pyw"
}
}
def get_category(extension):
for category, extensions in CATEGORIES.items():
if extension in extensions:
return category
return "Other"
def get_unique_destination(
destination_folder,
file_name
):
destination = destination_folder / file_name
if not destination.exists():
return destination
file_path = Path(file_name)
counter = 1
while True:
new_name = (
f"{file_path.stem}_{counter}"
f"{file_path.suffix}"
)
destination = destination_folder / new_name
if not destination.exists():
return destination
counter += 1
def organize_files(folder, preview=True):
if not folder.is_dir():
print("المسار غير موجود أو ليس مجلدًا:")
print(folder)
return
stats = {}
processed = 0
items = list(folder.iterdir())
for item in items:
if not item.is_file():
continue
if item.name in SKIP_FILES:
continue
if item.name.startswith("."):
continue
extension = item.suffix.lower()
category = get_category(extension)
destination_folder = folder / category
destination_file = get_unique_destination(
destination_folder,
item.name
)
if preview:
print(
f"[PREVIEW] {item.name}"
f" -> {destination_file}"
)
else:
destination_folder.mkdir(
parents=True,
exist_ok=True
)
try:
shutil.move(
str(item),
str(destination_file)
)
print(
f"[MOVED] {item.name}"
f" -> {category}/"
)
except PermissionError:
print(
"[ERROR] لا يمكن نقل:",
item.name
)
continue
except FileNotFoundError:
print(
"[ERROR] الملف اختفى:",
item.name
)
continue
processed += 1
stats[category] = stats.get(category, 0) + 1
print()
print("الملخص:")
for category, count in sorted(stats.items()):
print(f"- {category}: {count}")
print("إجمالي الملفات:", processed)
organize_files(
SOURCE_FOLDER,
preview=PREVIEW
)
لماذا نأخذ list من iterdir قبل النقل؟
استخدمنا:
items = list(folder.iterdir())
لأن محتويات المجلد ستتغير أثناء النقل. أخذ قائمة بالعناصر الموجودة في البداية يجعل التنفيذ أسهل في الفهم والمراجعة.
طريقة تشغيل النسخة الكاملة
ابدأ بـ:
PREVIEW = True
وسيظهر شيء مثل:
[PREVIEW] photo.jpg -> test_downloads/Images/photo.jpg
[PREVIEW] report.pdf -> test_downloads/PDF/report.pdf
[PREVIEW] video.mp4 -> test_downloads/Videos/video.mp4
بعد التأكد من كل المسارات، استخدم:
PREVIEW = False
ماذا نفعل مع الملفات غير المعروفة؟
في النسخة الحالية تذهب إلى Other. إذا كنت تفضل عدم تحريكها، اجعل الدالة تعيد None:
def get_category(extension):
for category, extensions in CATEGORIES.items():
if extension in extensions:
return category
return None
ثم:
category = get_category(extension)
if category is None:
print("تم تجاهل:", item.name)
continue
أخطاء شائعة يجب تجنبها
1. اختيار مجلد المصدر الخطأ
اطبع SOURCE_FOLDER.resolve() وراجعه قبل النقل.
2. عدم استخدام Preview
المعاينة يجب أن تكون الخطوة الأولى خصوصًا بعد تعديل قواعد الامتدادات.
3. عدم معالجة الأسماء المتكررة
لا تفترض أن مجلد الوجهة فارغ. أنشئ اسمًا بديلًا عند وجود الملف.
4. نسيان lower()
قد تفوت ملفات مثل PHOTO.JPG.
5. محاولة نقل Folder كأنه File
استخدم is_file() قبل التصنيف.
6. التقاط كل الأخطاء وإخفاؤها
اعرض الأخطاء المتوقعة حتى تعرف الملفات التي لم تنتقل.
تمرين: أضف فئة Fonts
أضف الامتدادات:
"Fonts": {
".ttf",
".otf",
".woff",
".woff2"
}
تمرين: تجاهل الملفات التي تبدأ بـtemp_
if item.name.startswith("temp_"):
continue
تمرين: عرض حجم الملف قبل نقله
size_bytes = item.stat().st_size
print(item.name, size_bytes)
أفكار لتطوير المشروع
- واجهة رسومية لاختيار المجلد.
- زر Preview وزر Organize.
- حفظ قواعد التصنيف في ملف JSON.
- إنشاء تقرير CSV بكل ملف ومساره القديم والجديد.
- ميزة Undo تعتمد على سجل النقل.
- تنظيم اختياري حسب التاريخ بدل الامتداد.
- تشغيل البرنامج دوريًا بعد اختبار القواعد جيدًا.
روابط داخلية مفيدة من بايثون العرب
- سلسلة بايثون بالمثال
- بايثون بعد الأساسيات 3: قراءة وكتابة ملفات CSV داخل مشروع بايثون
- أساسيات بايثون 25: شرح مكتبة os للتعامل مع الملفات والمجلدات
- مشكلة وحل 13: حل خطأ FileNotFoundError عند فتح الملفات
- مشكلة وحل 22: حل خطأ IsADirectoryError عند محاولة فتح مجلد كملف
مصادر رسمية للتوسع
الخلاصة
بناء برنامج لتنظيم الملفات حسب الامتداد مشروع عملي لتطبيق مهارات بايثون على مشكلة يومية. استخدمنا pathlib لقراءة الملفات ومعرفة الامتدادات وبناء المسارات، واستخدمنا shutil.move() لنقل الملفات إلى الفئات المناسبة.
النسخة الجيدة من البرنامج لا تنقل الملفات بشكل أعمى. استخدم Preview، وتحقق أن المصدر مجلد صحيح، وتجاهل المجلدات، ووحّد الامتداد باستخدام lower()، وتعامل مع الأسماء المكررة بدل المخاطرة باستبدال الملفات.
بعد اختبار المشروع على مجلد تجريبي، تستطيع تخصيص الفئات حسب احتياجاتك واستخدامه لتنظيم Downloads أو أي مجلد شخصي آخر، ثم تطويره لاحقًا إلى تطبيق بواجهة رسومية أو إضافة سجل وميزة Undo.
{alertSuccess} القاعدة العملية: في برامج إدارة الملفات، اجعل المعاينة هي الخطوة الأولى. اعرض خطة النقل، راجعها، ثم فعّل التنفيذ الحقيقي بعد التأكد أن القواعد والمسار صحيحان.
أسئلة شائعة
كيف أنظم الملفات تلقائيًا باستخدام بايثون؟
استخدم Path.iterdir() لقراءة الملفات، وPath.suffix لمعرفة الامتداد، ثم اربط كل امتداد بمجلد مناسب وانقل الملف باستخدام shutil.move().
كيف أنظم الملفات حسب الامتداد في بايثون؟
استخرج امتداد كل ملف باستخدام item.suffix.lower()، ثم قارنه بمجموعة امتدادات مثل الصور أو PDF أو Excel وحدد مجلد الوجهة.
كيف أنظم مجلد Downloads باستخدام بايثون؟
يمكن استخدام Path.home() / "Downloads" كنقطة بداية إذا كان هذا هو المسار الصحيح على جهازك، ثم تشغيل Preview قبل النقل الحقيقي.
كيف أنقل ملفًا من مجلد إلى مجلد؟
استخدم shutil.move(source, destination)، ويفضل بناء المصدر والوجهة باستخدام pathlib.Path.
كيف أعرف امتداد الملف باستخدام pathlib؟
استخدم Path("photo.jpg").suffix، وستحصل على .jpg.
كيف أنشئ مجلدًا إذا لم يكن موجودًا؟
استخدم folder.mkdir(parents=True, exist_ok=True).
كيف أمنع استبدال ملف بالاسم نفسه؟
تحقق من وجود مسار الوجهة، وإذا كان الاسم مستخدمًا أنشئ اسمًا مثل photo_1.jpg ثم photo_2.jpg.
كيف أعاين التنظيم قبل نقل الملفات؟
استخدم متغيرًا مثل PREVIEW = True واطبع المصدر والوجهة بدل تنفيذ shutil.move(). بعد المراجعة غيّره إلى False.
لماذا يظهر PermissionError أثناء النقل؟
قد تكون الصلاحيات غير كافية أو يكون الملف مستخدمًا من برنامج آخر حسب النظام. أغلق البرنامج الذي يستخدم الملف وتحقق من المسار والصلاحيات.
ماذا أفعل بالملفات التي لا أعرف امتدادها؟
يمكن نقلها إلى Other أو تجاهلها. اختر قاعدة واضحة تناسب استخدامك.
هل يمكن تنظيم الصور فقط؟
نعم. اجعل مجموعة الامتدادات تحتوي فقط على .jpg و.png و.webp وغيرها من امتدادات الصور.
ما الفرق بين shutil.move وshutil.copy2؟
move() تنقل الملف من مكانه، بينما copy2() تنشئ نسخة وتحاول الحفاظ على metadata التي يدعمها النظام.
هل pathlib أفضل من os لهذا المشروع؟
يمكن تنفيذ المشروع بكلتا الطريقتين. استخدمنا pathlib لأنها تجمع بناء المسارات وفحص الملفات والامتدادات بصياغة واضحة.
هل يمكن تشغيل منظم الملفات تلقائيًا كل يوم؟
نعم بعد اختبار السكربت وقواعده جيدًا. يمكن لاحقًا جدولته بأدوات نظام التشغيل، لكن الأفضل إبقاء Preview وإجراءات منع تعارض الملفات في النسخة النهائية.



