التنقل في شجرة الملفات
من المعتاد كتابة سكربتات تحتوي على بعض التفاعل مع نظام التشغيل، من ابسطها مثل قراءة ملف بيانات إلى أكثرها تعقيدا مثل تحليل آلاف الملفات في بنية مجلدات متداخلة. تحتوي مكتبة بيثون على العديد من الوظائف المفيدة لهذه المهام.
رأينا أن الدالة open تتيح لنا انشاء وفتح ملفات في شجرة ملفات النظام، وقد اخترنا سابقا لغاية التبسيط أن نقوم بانشاء وفتح ملفات في مجلد العمل نفسه، اي في نفس المجلد حيث نحفظ سكربتاتنا. لكن الدالة open تتيح لنا عبر وسيطها الاول أن نختار أي مجلد نريد. في هذا الدليل سنتحدث قليلا عن كيفية التنقل في شجرة ملفات النظام وسنرى وظيفة تقوم مقام open وتحمل نفس الاسم.
# الوحدة النمطية os.path (مهملة)
المشكلة الاولى التي كانت تعترض المبرمجين، هي كيفية التنقل في شجرة الملفات، فإذا أضفنا لها مشكلة تنوع طرق التنقل بين أنظمة التشغيل المختلفة (الرمز \ في وندوز والرمز / في يونكس ...) تزداد صعوبة تطوير برنامج يعمل على منصات مختلفة.
قبل الاصدار 3.4 من بيثون، كانت المكتبة القياسية لبيثون توفر للمطورين مجموعة من الأدوات تيسر التنقل في ملفات الانظمة على اختلاف أنواعها:
- المكتبة os التي توفر وحدات ودوال متنوعة للتعامل مع نظام التشغيل: https://docs.python.org/3/library/os.html
- الوحدة os.path لمعالجة مسار الملفات وتسمياتها، الخ... راجع: https://docs.python.org/3/library/os.path.html
- الوحدة glob للبحث في ملفات النظام : https://docs.python.org/3/library/glob.html
كل هذه المجموعة قليلة التجانس تم استبدالها بالمكتبة `pathlib` التي توفر جميع الخدمات السابقة ضمن بيئة موحدة وعصرية، وبالتالي يُنصح باستخدامها في كتابة الاكواد الجديدة.
قبل الحديث عن `pathlib` سنستعرض باختصار شديد الوحدات القديمة، حيث من الممكن أن تعترضك في سكربتات منتشرة؛ الاسماء التالية كلها تمثل دوال، على خلاف `pathlib` التي سنرى أنها توفر لنا بيئة كائنية:
- الدالة os.path.join تقوم باضافة الرمز / او الرمز \ في مسار الملفات حسب نوع نظام التشغيل
- الدالة os.path.basename تستخرج اسم ملف من مساره في شجرة الملفات
- الدالة os.path.dirname تستخرج اسم مجلد من مساره في شجرة الملفات
- الدالة os.path.getsize تستخرج حجم ملف
- الدالة os.path.getatime (وكذلك getmtime و getctime) للحصول على وقت انشاء/تحرير ملف
- الدالة os.makedirs لانشاء مسار من المجلدات recursively
- الدالة os.getcwd تستخرج مجلد العمل الحالي
- الدالة os.remove (وكذلك os.unlink) لحذف ملف
- الدالة os.rmdir لحذف مجلد على أن يكون فارغا
- الدالة os.removedirs لحذف مجلد بما فيه
- الدالة os.rename لاعادة تسمية ملف
- الدالة glob.glob للبحث عن ملف او اكثر، مثلا *.txt
## مثال سريع: نريد سكربت يعمل على وندوز ولينكس وهو يحتاج أن يكتب في ملف موجود في المسار التالي على لينكس أو وندوز:
كود :
# /home/$USER/sub_folder/sub_sub_folder/results.log # المسار على لينكس
# C:\%USERPROFILE%\sub_folder\sub_sub_folder\results.log # المسار على وندوز
import os
import glob
file_extension = ['*.txt','*.py', '*.log'] # سنبحث عن أنواع الملفات المذكورة
home = os.path.expanduser("~") # الطريقة القديمة لتحديد مسار مجلد المنزل
path = os.path.join(home,"sub_folder","sub_sub_folder") # عدل هذا بحسب مسار البحث
d = os.path.join(path, "results.log") # results.log سنكتب نتائج البحث في ملف يحمل اسم
with open(d, "w", encoding='utf-8') as log_file:
for s in file_extension:
ffound = glob.glob(os.path.join(path,s))
log_file.write("Found {1:>3} files ({0:^5}) in {2}:\n".format(s, len(ffound), path))
for f in ffound:
f = os.path.basename(f)
log_file.write(" {}\n".format(f))
with open(d, "r", encoding='utf-8') as log_file:
print(log_file.read())
## مثال أخر يبرز بعض تعقيد الطريقة القديمة: انشاء مجلدات متداخلة في مجلد العمل الحالي
كود :
# Simple: How to create recursively subdirectories in current working directory:
import os, errno
def create_sub_dirs(sub_folder, *sub_folders):
path = os.path.join(os.getcwd(), sub_folder)
if len(sub_folders) > 0:
for f in sub_folders:
path = os.path.join(path, f)
try:
os.makedirs(path)
return (True, path)
except OSError as e:
if e.errno == errno.EEXIST:
return(False, path)
else:
return (False, e.args)
sub_dirs = create_sub_dirs("f1","f2", "f3","f4")
print(sub_dirs)
try:
d = os.path.join(sub_dirs[1], "results.log")
with open(d, "w", encoding='utf-8') as log_file:
log_file.write("some text\n")
print("File created in {}".format(sub_dirs[1]))
except OSError as e:
print(e.args)
# الوحدة pathlib
تم دمج هذه المكتبة في الاصدار 3.4 من بيثون والاصدارات اللاحقة، لذا فهي مكتبة حديثة نوعا ما. واحدة من بين الميزات المفيدة للوحدة `pathlib` هي أنها أكثر سهولة لمعالجة مسارات ملفات النظام بدون الحاجة الى os.path.join الطويلة والمتكررة. هذا نفس السكربت الاول أعلاه لكن باستخدام `pathlib` مع الملاحظة وأنه يمكن اختزاله أكثر لكننا كتبناه هكذا لتبسيط المقارنة
كود :
# /home/$USER/sub_folder/sub_sub_folder/results.log # المسار على لينكس
# C:\%USERPROFILE%\sub_folder\sub_sub_folder\results.log # المسار على وندوز
from pathlib import Path
file_extension = ['*.txt','*.py', '*.log'] # سنبحث عن أنواع الملفات المذكورة
sub_dirs = ["sub_folder","sub_sub_folder"] # عدل هذا بحسب مسار البحث
path = Path.home().joinpath(*sub_dirs)
d = path.joinpath('results.log') # results.log سنكتب نتائج البحث في ملف يحمل اسم
with d.open('w') as log_file: # لاحظ الطريقة الجديدة لفتح ملف
for s in file_extension:
ffound = sorted(path.glob(s)) # طريقة البحث الجديدة: هنا حولنا النتيجة الى قائمة مرتبة
log_file.write("Found {1:>3} files ({0:^5}) in {2}:\n".format(s, len(ffound), path))
for f in ffound:
f = f.name # compare with : os.path.basename(f)
log_file.write(" {}\n".format(f))
print(d.read_text()) # طريقة أخرى لفتح ملف بداية من بيثون 3.5
كما ذكرنا سابقا، توفر الوحدة pathlib بيئة كائنية، لكن ماذا يعني ذلك بالضبط؟
مثلما يوفر لنا بيثون انواعا قياسية مثل int و str، توفر لنا الوحدة نوعا آخر (كلاس آخر) يسمى Path نقوم باستيراده واستخدامه كما يلي:
كود :
from pathlib import Path
home = Path.home() # هكذا نحدد مجلد المنزل سواء في وندوز او في لينكس
print(home)
myFile = 'testfile.txt' # سلسلة نصية سنحولها الى اسم ملف
type(myFile)
في البداية، سنحاول التأكد من وجود الملف. لهذا الغرض سنقوم بانشاء كائن جديد مثيل من الكلاس Path كما يلي (ولاحظ البساطة والتناغم مع عادات بيثون في التحويل من نوع الى آخر):
كود :
myFile = Path(myFile)
type(myFile) # pathlib.PosixPath لأنني أعمل على لينكس
والآن، هذا الكائن myFile أصبح لديه عددا من الوظائف، لتراها إن كنت تعمل على محرر يدعم التكميل التلقائي (IDLE مثلا) اكتب myFile متبوعا بنقطة ثم انقر مفتاح tab :
كود :
myFile = Path.home().joinpath(myFile) # نريد انشاء ملف داخل مجلد المنزل مباشرة
print(myFile)
myFile.exists() # False لأننا لم نكتبه على القرص الى حد الآن
print(myFile.name) # attribut NOT a method
سنكتب فيه لانشائه:
كود :
myFile.open('w').write('0123456789\n')
والآن نحاول التاكد من وجوده مجددا:
كود :
myFile.exists() # True
يمكنك التأكد من وجود ملف باسم testfile.txt داخل مجلد المنزل وفتحه بمحرر النصوص اذا اردت، أو طباعة محتواه على الطرفية:
كود :
print(myFile.open().read())
print(myFile.read_text()) # بيثون 3.5
لنستخرج الميتا داتا الخاصة بهذا الملف (حجمه، تواريخ انشائه وتحريره...):
كود :
myFile.stat()
myFile.stat().st_size # حجم الملف بالبايت
mtime = myFile.stat().st_mtime # وقت آخر تحرير بالثواني منذ 1 جانفي 1970
print(mtime)
الوقت يمكن جعله أكثر وضوحا عبر:
كود :
from datetime import datetime
dt = datetime.fromtimestamp(mtime)
'{:%d-%m-%Y %H:%M:%S}'.format(dt)
لحذفه:
كود :
try:
myFile.unlink()
except FileNotFoundError:
print("no need to remove")
myFile.exists() # للتأكد من الحذف
البحث عن الملفات:
سنبحث عن ملفات .py في مجلد العمل الحالي
كود :
for pyFile in Path.cwd().glob("*.py"):
print(pyFile)
البحث في مجلد العمل الحالي ومجلداته الفرعية ان وجدت (انتبه لطول الوقت اذا كان عدد المجلدات والملفات كبير جدا)
كود :
for pyFile in Path.cwd().glob("**/*.py"):
print(pyFile)
أو:
كود :
for pyFile in Path.cwd().rglob("*.py"):
print(pyFile)
نوع الكائن (مجلد أم ملف):
كود :
myFile.is_dir() # False
myFile.is_file() # True
## للمزيد من المعلومات عن الوحدة `pathlib` ، يمكنك أن تراجع دليل المساعدة من الرابط التالي:
https://docs.python.org/3/library/pathlib.html
سترى أن أسماء الوظائف معبرة عن نفسها بدرجة كافية من الوضوح.
## وهذا ملخص مفيد عن `pathlib` صالح للطباعة في صفحة واحدة
https://github.com/chris1610/pbpython/bl...tsheet.pdf