11  الدليل

أحيانًا نريد التعامل مع مجلَّد يحوي مجموعة ملفات، بنقلها أو نسخها أو حذفها ونحو ذلك.

وجب علينا أولاً أن نُدرك أن ثمة نوعان من الملفات في النظام:

  1. دليل: وهو قائمة من الملفات والأدلة
    • وهو الذي تراه في الواجهة الرسومية على شكل مجلَّد
    • ولا يحتاج لتحديد صيغة
  2. ملف: وهو الذي يحوي بيانات نصية أو ثنائية
    • وهي الملفات التي تراها داخل المجلد في الواجهة الرسومية
    • صيغتها هي الحروف بعد آخر نقطة؛ مثل: my_data.csv (الصيغة: csv)

والملف المبتدأُ بالنقطة مثل: .git يكون مخفيًّا.

والطريق هو الموصل لملفٍّ ما أو دليلٍ ما نحو: datasets/example_root/a/a.txt. وسبق أن بينا أنه على نوعين: مُطلَق ونسبي. وسنعتمد على وحدة pathlib لنرى عمليات تركيب المسار والبحث فيه والاستعلام عن مدلوله ونحو ذلك. وسنبدأ بها.

ملاحظة

يجدر بالذكر أن مكتبة pathlib جاءت متأخرة في إصدار Python 3.4 لمعالجة الملفات بأسلوب البرمجة الشيئية (OOP)، بينما تستعمل مكتبة os التي سبقتها لأغراض متعددة فيما يخص نظام التشغيل (os = Operating System) من ضمنها خُصِّصَت os.path للتتعامل مع نظام الملفات إلا أنها كُتِبَت بأسلوب إجرائي تأسيًّا بلغة سي (C)، فهي منخفضة المستوى (تتعامل مباشرة مع bytes و str) بالمقارنة بالبرمجة الشيئية الأعلى في التجريد؛ وهو ما نفضله. انظر مقارنة pathlib بوحدات os و os.path.

لاستعراض عمليات المكتبة، أنشأنا مُسبقًا شجرة تبدأ من دليل example_root تجده داخل datasets في مستودع مشروع الكتاب على النحو التالي:

graph LR
    A[example_root]
    A --> B[something.txt]
    A --> C[a]
    A --> D[b]
    A --> E[c]
    C --> F["a.txt"]
    C --> G["A_domestic_cat.jpg"]
    C --> I["zzz.txt"]
    D --> J["A_yellow_and_white_cat.jpg"]
    D --> L[b.txt]
    D --> M["DSC0532_(9120523417).jpg"]
    E --> O["c.txt"]
    E --> P[c_inner]
    P --> Q["inner.txt"]

الشيء الأساسي الذي نتعامل معه هو المسار (Path):

from pathlib import Path
from pathlib import PureWindowsPath

يُنشأ المسار من نص أو مجموعة نصوص بينها علامة / (وهي في الأصل علامة قسمة إذا وُضعت بين الأرقام) التي تعني ضم النص إلى المسار.

لاحظ استعمال النص الخام r'' لكون الحرف \ له معنىً خاص في نصوص بايثون، فهو يستعمل مثلا \n للانتقال لسطر جديد. لكن حين نستعمل r'' فإن الحرف \ لا يعني شيئًا خاصًا.

assert (
    Path('../../datasets/example_root/a/a.txt').as_posix() ==
    PureWindowsPath(r'..\..\datasets\example_root\a\a.txt').as_posix() ==
    (Path('../../datasets') / 'example_root' / 'a' / 'a.txt').as_posix()
)

ملاحظة، عند نسخ المسارات في نظام ويندوز (Windows)، يجب عليك إما استبدال الشرطات المائلة العكسية \ بالشرطات المائلة الأمامية / أو استخدام السلاسل النصية الخام r'' لتجنب المشاكل (كما فعلنا أعلاه).

وهكذا نستطيع استعمال الكائن Path في فعل open(file=path) للتعامل مع الملفات:

p = Path('../../datasets/example_root/a/a.txt')

f = open(file=p, mode='r')
content = f.read()
f.close()

print(content)
lorem ipsum dolfet
weilfur badem zelfur

ولأن الملف يعتبر مدير سياق (Context Manager) فيستحسن الإتيان بجملة السياق with إذ تتكفل بإغلاق الملف تلقائيّا عند نهايتها. وذلك على النحو التالي:

p = Path('../../datasets/example_root/a/a.txt')

contents = ''
with open(file=p, mode='r') as f:
    contents = f.read()
print(contents)
lorem ipsum dolfet
weilfur badem zelfur

بدلاً من قراءة الملف جملة واحدة .read() نستطيع القراءة سطرًا بسطر عن طريق الفعل .readline() على النحو التالي:

p = Path('../../datasets/example_root/a/a.txt')

with open(file=p, mode='r') as f:
    for i, line in enumerate(f, start=1):
        line = line.rstrip()
        print(f'Line {i}: {line}')
Line 1: lorem ipsum dolfet
Line 2: weilfur badem zelfur

تستعمل .rstrip() لإزالة حرف انتقال السطر \n .

الدليل

انظر قراءة الأدلة.

لعرض قائمة الدليل، نستعمل المكرر الناتج من فعل .iterdir() ونكرر عليه، وهو بدورِه يُنتج في كل كرةٍ مساراً (x). وهذا المسار يُمكن التحقق من أنه يشير إلى دليل أو لا (x.is_dir()) على النحو التالي:

p = Path('../../datasets/example_root/')
dirs = [x for x in p.iterdir() if x.is_dir()]
dirs
[PosixPath('../../datasets/example_root/a'),
 PosixPath('../../datasets/example_root/b'),
 PosixPath('../../datasets/example_root/c')]

جرب

هل تريد أن تعرف حجم دليل التنزيلات (Downloads) في جهازك؟. لديك الفعل stat() للحصول على بيانات عن الدليل، والتي من ضمنها الحجم (st_size) هكذا:

p = Path.home() / 'Downloads'
size = p.stat().st_size
print(size, 'bytes')
4096 bytes

ثم هذا الفعل لتحويل الوِحدة من البايت إلى الكيلو والميجا والقيقا:

def format_size(size):
    size_kb = size / 1024
    size_mb = size_kb / 1024
    size_gb = size_mb / 1024
    if size_gb > 0.1:
        return f'{size_gb:.2f} GB'
    elif size_mb > 0.1:
        return f'{size_mb:.2f} MB'
    return f'{size_kb:.2f} KB'

print(format_size(size))
4.00 KB

البحث في شجرة الأدلة

يستعمل الفعل glob للبحث عن نمط معيَّن بدءًا من المسار، وهذا النمط قد يبحث في مستوىً واحد أو يتخلل الشجرة بأي طريقة تريد؛ ليطابق أسماء الملفات والأدلة بنمط معيَّن فيستخرجها لك.

انظر لغة الأنماط لتتعلم هذه اللغة الصغيرة.

في هذا المثال نبحث عن ملفات الصُّوَر التي بصيغة .jpg لنحسب حجم مجموع ملفات الصور لدينا داخل المسار datasets/pathlib وما يتفرع عن هذه الشجرة نزولاً إلى الأبد (وذلك بنمط: **/*.jpg):

total_size = 0
for p in Path('../../datasets/example_root/').glob('**/*.jpg'):
    total_size += p.stat().st_size

print('Total size:', total_size, 'bytes')
print('Total size:', format_size(total_size))
Total size: 999702 bytes
Total size: 0.95 MB

المشي على جميع ملفات الشجرة

نستعمل الفعل walk للسير على جميع ملفات الشجرة نزولاً أو صعودًا؛ على هذا النحو:

p = Path('../../datasets/example_root/')
for dirpath, dirnames, filenames in p.walk(top_down=True):
    print(dirpath)
    for file in filenames:
        print(f'\t{file}')
        # print('\tFULL PATH:', Path(dirpath) / file)
../../datasets/example_root
    something.txt
../../datasets/example_root/a
    A_domestic_cat.jpg
    zzz.txt
    A_domestic_cat.jpgZone.Identifier
    a.txt
../../datasets/example_root/b
    DSC0532_(9120523417).jpg
    A_yellow_and_white_cat.jpg
    DSC0532_(9120523417).jpgZone.Identifier
    b.txt
    A_yellow_and_white_cat.jpgZone.Identifier
../../datasets/example_root/c
    c.txt
../../datasets/example_root/c/c_inner
    inner.txt

لاحظ أن .walk() تعطينا ثلاثة قيَم في كل كرة:

  • dirpath: المسار الحالي للدليل.
  • dirnames: قائمة بأسماء الأدلة التي يدل عليها.
  • filenames: قائمة بأسماء الملفات التي يدل عليها.

لإظهار كامل المسار؛ أزل علامة التعليق # من السطر الأخير لتنفيذه.

11.1 تصنيف عمليات pathlib في بايثون

وإليك تصنيف لعمليات مكتبة pathlib:

عمليات المسار

  • إنشاء وتعديل المسارات:
    • .joinpath: دمج مكونات مسار.
    • .parent: استخراج الدليل الأب.
    • .name: استخراج الاسم الأساسي للملف.
    • .stem: استخراج اسم الملف بدون الامتداد.
    • .suffix: استخراج امتداد الملف.
    • .with_name: إنشاء مسار جديد باسم مختلف.
    • .with_suffix: إنشاء مسار جديد بامتداد مختلف.
    • relative_to: إنشاء مسار نسبي.

عمليات على نظام الملفات

  • استعلام:
    • is_absolute: هل هو مطلق؟.
    • samefile: هل مساران يشيران إلى نفس الملف؟.
    • exists: هل هو مسار موجود؟.
    • is_file: هل هو مسار ملف؟.
    • is_dir: هل هو مسار دليل؟.
    • is_symlink: هل هو ارتباط رمزي؟.
    • stat: سرد إحصائيات نظام الملفات.
    • lstat: سرد إحصائيات نظام الملفات دون متابعة الارتباطات الرمزية.
  • عمليات تغيير:
    • open: فتح ملف للقراءة أو الكتابة أو الإضافة.
    • mkdir: إنشاء دليل.
    • rmdir: حذف دليل فارغ.
    • unlink: حذف ملف.
    • rename: تعديل اسم ملف.
    • replace: تعديل اسم ملف مع الكتابة فوقه إذا لزم الفعل.
    • chmod: تغيير أذونات الملف.
    • lchmod: تغيير أذونات الملف دون متابعة الارتباطات الرمزية.
    • touch: تحديث الطابع الزمني للملف.

الارتباطات الرمزية والصلبة

  • symlink_to: إنشاء ارتباط رمزي.
  • hardlink_to: إنشاء ارتباط صلب.
  • readlink: قراءة الهدف من ارتباط رمزي.

التكرار والبحث

  • iterdir: تكرار على قائمة الدليل.
  • glob: البحث عن الملفات المطابقة لنمط.
  • rglob: البحث بشكل متكرر عن الملفات المطابقة لنمط.
  • walk: التنقل بشكل متكرر في شجرة الأدلة.

11.2 النسخ والحذف والنقل (shutil)