البحث في الموقع
المحتوى عن 'أتمتة المهام ببايثون'.
-
من المفيد معرفة وحدات بايثون Python المختلفة لتحرير جداول البيانات وتنزيل الملفات وتشغيل البرامج، ولكن لا توجد في بعض الأحيان أيّ وحداتٍ للتطبيقات التي تحتاج إلى العمل معها، فالأدوات الأساسية لأتمتة المهام على حاسوبك هي البرامج التي تكتبها وتتحكم في لوحة المفاتيح والفأرة مباشرةً، حيث يمكن لهذه البرامج التحكم في التطبيقات الأخرى من خلال إرسال ضغطات مفاتيح افتراضية ونقرات افتراضية بالفأرة إليها كما لو أنك جالس أمام حاسوبك وتتفاعل مع التطبيقات بنفسك. تُعرَف هذه التقنية باسم أتمتة واجهة المستخدم الرسومية Graphical User Interface Automation أو GUI automation اختصارًا، حيث يمكن لبرامجك باستخدام هذه التقنية فعل أيّ شيء يمكن أن يفعله المستخدم الجالس أمام الحاسوب باستثناء سكب القهوة على لوحة المفاتيح طبعًا. يمكن عَدّ أتمتة واجهة المستخدم الرسومية كبرمجة ذراع آلية، حيث يمكنك برمجة الذراع الآلية للكتابة باستخدام لوحة المفاتيح وتحريك الفأرة نيابةً عنك، وتُعَد هذه التقنية مفيدة خاصةً للمهام التي تتضمن الكثير من النقر أو ملء الاستمارات. تبيع بعض الشركات حلولَ الأتمتة المبتكرة والمكلفة، والتي تُسوَّق عادةً بأنها أتمتة العمليات الآلية Robotic Process Automation أو RPA اختصارًا، حيث لا تختلف هذه المنتجات فعليًا عن سكربتات بايثون التي يمكنك إنشاؤها بنفسك باستخدام الوحدة pyautogui التي تحتوي على دوال لمحاكاة حركات الفأرة ونقرات الأزرار وتمرير عجلة الفأرة. سنوضّح في هذا المقال مجموعة فرعية فقط من ميزات الوحدة PyAutoGUI، حيث يمكنك العثور على التوثيق الكامل على موقعها الرسمي. تثبيت الوحدة pyautogui يمكن لوحدة pyautogui إرسال ضغطات المفاتيح ونقرات الفأرة الافتراضية إلى أنظمة تشغيل ويندوز Windows وماك macOS ولينكس Linux، حيث يمكن لمستخدمي ويندوز وماك macOS ببساطة استخدام أداة pip لتثبيت الوحدة PyAutoGUI، ولكن يجب على مستخدمي نظام لينكس أولًا تثبيت بعض البرامج التي تعتمد عليها وحدة PyAutoGUI، لذا افتح نافذة طرفية Terminal وأدخِل الأوامر التالية: sudo apt install scrot python3-tk python3-dev يمكنك تثبيت الوحدة PyAutoGUI من خلال تشغيل الأمر pip install --user pyautogui، ولكن لا تستخدم الأمر sudo مع الأداة pip، إذ يمكن أن تثبِّتَ وحداتٍ مع تثبيت بايثون الذي يستخدمه نظام التشغيل، مما يتسبب في حدوث تعارضات مع أيّ سكربتات تعتمد على ضبطها الأصلي، ولكن يجب استخدام الأمر sudo عند تثبيت التطبيقات باستخدام apt. يمكنك اختبار صحة تثبيت الوحدة PyAutoGUI من خلال تشغيل الأمر import pyautogui في الصدفة التفاعلية Interactive Shell والتحقق من وجود رسائل خطأ. ملاحظة: لا تحفظ برنامجك بالاسم pyautogui.py، إذ ستستورد لغة بايثون برنامجك بدلًا من الوحدة PyAutoGUI وستتلقّى رسائل خطأ مثل الرسالة AttributeError: module 'pyautogui' has no attribute 'click' عند تشغيل الأمر import pyautogui. إعداد تطبيقات إمكانية الوصول Accessibility على نظام ماك macOS لا يسمح نظام ماك للبرامج بالتحكم في الفأرة أو لوحة المفاتيح كإجراءٍ أمني، لذا يجب ضبط البرنامج الذي يشغّل سكربت بايثون ليكون تطبيقًا لإمكانية الوصول لكي تعمل وحدة PyAutoGUI على نظام تشغيل ماك، إذ لن يكون لاستدعاءات دوال PyAutoGUI أيّ تأثير بدون إجراء هذه الخطوة. اجعل تطبيقك مفتوحًا سواء شغّلته من محرّر Mu أو بيئة IDLE أو الطرفية Terminal، ثم افتح "تفضيلات النظام System Preferences" وانتقل إلى التبويب "إمكانية الوصول Accessibility". ستظهر التطبيقات المفتوحة حاليًا تحت العنوان "السماح للتطبيقات التالية بالتحكم في حاسوبك Allow the apps below to control your computer". تحقّق من تطبيق Mu أو IDLE أو الطرفية Terminal أو أيّ تطبيق تستخدمه لتشغيل سكربتات بايثون الخاصة بك، وسيُطلَب منك إدخال كلمة مرورك لتأكيد هذه التغييرات. البقاء على المسار الصحيح يجب أن تعرف كيفية التهرب من المشكلات التي قد تواجهك قبل الانتقال إلى أتمتة واجهة المستخدم الرسومية، فمثلًا يمكن لسكربت بايثون تحريك الفأرة والكتابة من خلال ضغطات المفاتيح بسرعة مذهلة، وقد يكون الأمر سريعًا جدًا بحيث لا تتمكّن البرامج الأخرى من مجاراة هذه السرعة، وإذا حدث خطأٌ ما مع استمرار برنامجك في تحريك الفأرة، فسيكون من الصعب معرفة ما يفعله البرنامج بالضبط أو كيفية حل هذه المشكلة. كما يمكن أن يخرج برنامجك عن السيطرة بالرغم من أنه يتبع تعليماتك بطريقة مثالية مثل المكانس المسحورة من فيلم The Sorcerer’s Apprentice من إنتاج شركة ديزني، والتي ظلت تملأ حوض ميكي بالماء ثم تملأه أكثر من اللازم، وقد يكون إيقاف البرنامج أمرًا صعبًا إذا كانت الفأرة تتحرك من تلقاء نفسها، مما يمنعك من النقر على نافذة محرّر Mu لإغلاقه. توجد لحسن الحظ عدة طرق لمنع مشاكل أتمتة واجهة المستخدم الرسومية أو حلها، والتي سنوضّحها فيما يلي. التوقف المؤقت والفشل الآمن إذا ظهر خطأ في برنامجك ولم تتمكّن من استخدام لوحة المفاتيح والفأرة لإغلاقه، فيمكنك استخدام ميزة الفشل الآمن في وحدة PyAutoGUI. حرّك الفأرة بسرعة إلى إحدى زوايا الشاشة الأربعة مثلًا، حيث يكون لكل استدعاء للدالة الخاصة بوحدة PyAutoGUI تأخير قدره 10 جزء من الثانية بعد تنفيذ الإجراء الخاص بها ليمنحك وقتًا كافيًا لتحريك الفأرة إلى الزاوية. إذا وجدَت وحدة PyAutoGUI بعد ذلك أن مؤشر الفأرة في الزاوية، فستطلق الاستثناء pyautogui.FailSafeException. لن يكون للتعليمات التي ليست تابعة لوحدة PyAutoGUI هذا التأخير الذي مقداره 10 جزء من الثانية. إذا وجدت نفسك في موقف تحتاج فيه إلى إيقاف برنامج PyAutoGUI، فما عليك سوى تحريك الفأرة بسرعة باتجاه الزاوية لإيقافه. إغلاق كل شيء من خلال تسجيل الخروج قد تكون أبسط طريقة لإيقاف برنامج أتمتة واجهة المستخدم الرسومية الخارج عن السيطرة هي تسجيل الخروج، مما يؤدي إلى إيقاف تشغيل جميع البرامج التي تكون قيد التشغيل. مفتاح اختصار تسجيل الخروج هو CTRL-ALT-DEL في نظامي ويندوز ولينكس، وهو -SHIFT-OPTION-Q على نظام ماك. ستفقد أيّ عمل غير محفوظ عند تسجيل الخروج، ولكنك لن تضطر إلى الانتظار حتى تنتهي عملية إعادة التشغيل الكاملة للحاسوب. التحكم في حركة الفأرة ستتعلّم في هذا القسم كيفية تحريك الفأرة وتعقّب موضعها على الشاشة باستخدام الوحدة PyAutoGUI، ولكن يجب أولًا أن تفهم كيفية عمل هذه الوحدة مع الإحداثيات. تستخدم دوال الفأرة الخاصة بوحدة PyAutoGUI إحداثيات x و y، حيث يبين الشكل التالي نظام إحداثيات شاشة الحاسوب، وهو مشابه لنظام الإحداثيات المستخدَم مع الصور الذي ناقشناه في المقال السابق، إذ توجد نقطة الأصل Origin حيث تكون قيمة x و y صفر في الزاوية العلوية اليسرى من الشاشة، وتزداد إحداثيات x باتجاه اليمين، وتزداد إحداثيات y باتجاه الأسفل. تكون جميع الإحداثيات أعدادًا صحيحة موجبة، إذ لا توجد إحداثيات سالبة. إحداثيات شاشة الحاسوب بدقة مقدارها 1920×1080 تمثّل الدقة Resolution عدد البكسلات لعرض وطول الشاشة، حيث إذا كانت دقة شاشتك مضبوطة على القيمة 1920×1080، فستكون إحداثيات الزاوية العلوية اليسرى هو (0, 0)، وستكون إحداثيات الزاوية السفلية اليمنى هو (1919, 1079). تعيد الدالة pyautogui.size() مجموعةً Tuple مكوّنة من عددين صحيحين لعرض الشاشة وارتفاعها بالبكسل. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> wh = pyautogui.size() # الحصول على دقة الشاشة >>> wh Size(width=1920, height=1080) >>> wh[0] 1920 >>> wh.width 1920 تعيد الدالة pyautogui.size() المجموعة (1920, 1080) على حاسوب دقته 1920×1080، إذ قد تختلف القيمة المُعادة اعتمادًا على دقة شاشتك. يُعَد الكائن Size الذي تعيده الدالة size() مجموعة مُسمَّاة Named Tuples، حيث يكون للمجموعات المُسماة فهارس رقمية مثل المجموعات العادية وأسماء سمات Attribute مثل الكائنات، إذ يُقيَّم كل من wh[0] و wh.width بأنه عرض الشاشة. لن نشرح المجموعات المُسمَّاة في هذا المقال، ولكن تذكّر فقط أنه يمكنك استخدامها باستخدام الطريقة نفسها التي تستخدم بها المجموعات العادية. تحريك الفأرة تعرّفنا على مفهوم إحداثيات الشاشة، ويمكننا الآن تحريك الفأرة، حيث تحرّك الدالة pyautogui.moveTo() مؤشر الفأرة مباشرةً إلى موضعٍ محدّد على الشاشة. تشكّل القيم الصحيحة لإحداثيات x و y الوسيطين الأول والثاني لهذه الدالة على التوالي، ويحدّد وسيط الكلمات المفتاحية Keyword Argument الاختياري duration -الذي هو عدد صحيح أو عشري- عدد الثواني التي يجب أن يستغرقها تحريك الفأرة للوصول إلى وِجهتها، وإذا تركتَ هذا الوسيط دون تحديد، فإن القيمة الافتراضية هي 0 للحركة الفورية، وتكون جميع وسطاء الكلمات المفتاحية duration في دوال PyAutoGUI اختيارية. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> for i in range(10): # تحريك الفأرة في مربع ... pyautogui.moveTo(100, 100, duration=0.25) ... pyautogui.moveTo(200, 100, duration=0.25) ... pyautogui.moveTo(200, 200, duration=0.25) ... pyautogui.moveTo(100, 200, duration=0.25) يحرّك المثال السابق مؤشر الفأرة باتجاه عقارب الساعة وفق نمطٍ مربع بين الإحداثيات الأربعة المُعطات 10 مرات، حيث تستغرق كل حركة ربع ثانية كما يحدّده وسيط الكلمات المفتاحية duration=0.25، وإن لم تمرّر الوسيط الثالث إلى أيٍّ من استدعاءات الدالة pyautogui.moveTo()، فسينتقل مؤشر الفأرة من نقطة إلى أخرى مباشرةً. تحرّك الدالة pyautogui.move() مؤشر الفأرة نسبةً إلى موضعه الحالي، حيث يحرّك المثال التالي الفأرة وفق نمط المربع نفسه، ولكنه يبدأ المربع من أيّ مكان توجد فيه الفأرة على الشاشة عند بدء تشغيل الشيفرة البرمجية: >>> import pyautogui >>> for i in range(10): ... pyautogui.move(100, 0, duration=0.25) # إلى اليمين ... pyautogui.move(0, 100, duration=0.25) # للأسفل ... pyautogui.move(-100, 0, duration=0.25) # إلى اليسار ... pyautogui.move(0, -100, duration=0.25) # للأعلى تأخذ الدالة pyautogui.move() أيضًا ثلاثة وسطاء هي: عدد البكسلات التي يجب تحريكها أفقيًا إلى اليمين، وعدد البكسلات التي يجب تحريكها عموديًا للأسفل، والمدة التي يجب أن يستغرقها إكمال الحركة (اختياريًا). سيؤدي استخدام العدد الصحيح السالب مع الوسيط الأول أو الثاني إلى تحريك الفأرة إلى اليسار أو للأعلى على التوالي. الحصول على موضع الفأرة يمكنك تحديد موضع الفأرة الحالي من خلال استدعاء الدالة pyautogui.position() التي ستعيد المجموعة المُسمَّاة Point لموضعي x و y الخاصين بمؤشر الفأرة عند استدعاء الدالة. أدخِل مثلًا ما يلي في الصدفة التفاعلية مع تحريك الفأرة بعد كل استدعاء: >>> pyautogui.position() # الحصول على موضع الفأرة الحالي Point(x=311, y=622) >>> pyautogui.position() # الحصول على موضع الفأرة الحالي مرة أخرى Point(x=377, y=481) >>> p = pyautogui.position() # الحصول على موضع الفأرة الحالي مرة أخرى >>> p Point(x=1536, y=637) >>> p[0] # يقع الإحداثي x عند الفهرس 0 1536 >>> p.x # يوجد الإحداثي x أيضًا في السمة x 1536 ستختلف قيمك المُعادة اعتمادًا على مكان مؤشر الفأرة. التحكم في تفاعل الفأرة تعرّفتَ كيفية تحريك الفأرة ومعرفة مكانها على الشاشة، وأصبحتَ الآن جاهزًا لبدء النقر والسحب والتمرير. النقر باستخدام الفأرة يمكنك إرسال نقرة افتراضية باستخدام الفأرة إلى حاسوبك من خلال استدعاء التابع pyautogui.click()، حيث تستخدم هذه النقرة زر الفأرة الأيسر افتراضيًا وتُطبَّق في أيّ مكان يوجد فيه مؤشر الفأرة حاليًا. يمكنك تمرير إحداثيات x و y لهذه النقرة كوسيط أول وثانٍ اختياريين إلى التابع إذا أدرتَ أن تُطبَّق في مكانٍ آخر غير موضع الفأرة الحالي. إذا أدرتَ تحديد زر الفأرة الذي يجب استخدامه، فضمّن وسيط الكلمات المفتاحية button مع قيم 'left' أو 'middle' أو 'right'، فمثلًا سيؤدي الاستدعاء pyautogui.click(100, 150, button='left') إلى النقر على زر الفأرة الأيسر عند الإحداثيات (100, 150)، بينما سيؤدي الاستدعاء pyautogui.click(200, 250, button='right') إلى النقر بزر الفأرة الأيمن عند الإحداثيات (200, 250). أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.click(10, 5) # تحريك الفأرة إلى الإحداثيات (10, 5) ثم النقر يُفترَض أن ترى مؤشر الفأرة يتحرك بالقرب من الزاوية العلوية اليسرى من الشاشة ثم يحدث النقر مرة واحدة. يمكن تعريف "النقرة" الكاملة على أنها الضغط على زر الفأرة للأسفل ثم تحريره للأعلى دون تحريك المؤشر، ويمكنك أيضًا إجراء نقرة من خلال استدعاء الدالة pyautogui.mouseDown() التي تضغط زر الفأرة للأسفل فقط، ثم استدعاء الدالة pyautogui.mouseUp() التي تحرّر الزر. تمتلك هاتان الدالتان وسطاء الدالة click() نفسها، ولكن تُعَد الدالة click() مجرد دالة مغلِّفة ملائمة لهاتين الدالتين. تنقر الدالة pyautogui.doubleClick() نقرتين باستخدام زر الفأرة الأيسر، بينما تنقر الدالتان pyautogui.rightClick() و pyautogui.middleClick() نقرة واحدة باستخدام زري الفأرة الأيمن والأوسط على التوالي. سحب Dragging الفأرة يعني السحب تحريكَ الفأرة أثناء الضغط باستمرار على أحد أزرارها، فمثلًا يمكنك نقل الملفات بين المجلدات من خلال سحب أيقونات المجلدات، أو يمكنك نقل المواعيد في تطبيق التقويم من خلال سحبها باستخدام الفأرة. توفر وحدة PyAutoGUI الدالتين pyautogui.dragTo() و pyautogui.drag() لسحب مؤشر الفأرة إلى موقع جديد أو موقع متعلق بموقعه الحالي. تستخدم الدالتان dragTo() و drag() وسطاء الدالتين moveTo() و move() نفسها وهي: حركة الإحداثي x أو الحركة أفقيًا وحركة الإحداثي y أو الحركة عموديًا ومدة زمنية اختيارية. لا يطبّق نظام ماك السحب تطبيقًا صحيحًا عندما تتحرك الفأرة بسرعة كبيرة، لذا يوصَى بتمرير وسيط الكلمات المفتاحية duration. لنجرّب هذه الدوال، لذا افتح تطبيق رسمٍ مثل تطبيق الرسام MS Paint على ويندوز أو تطبيق Paintbrush على نظام ماك ، أو تطبيق GNU Paint على نظام لينكس، حيث سنستخدم وحدة PyAutoGUI للرسم في هذه التطبيقات. إن لم يكن لديك تطبيق رسم، فيمكنك استخدام تطبيق sumopaint عبر الإنترنت. أدخِل ما يلي في نافذة ملفٍ جديد في محرّرك واحفظه بالاسم spiralDraw.py مع وجود مؤشر الفأرة على لوحة الرسم الخاصة بتطبيق الرسم وتحديد أداة القلم Pencil أو الفرشاة Brush: import pyautogui, time ➊ time.sleep(5) ➋ pyautogui.click() # النقر لتنشيط النافذة distance = 300 change = 20 while distance > 0: ➌ pyautogui.drag(distance, 0, duration=0.2) # التحرك يمينًا ➍ distance = distance – change ➎ pyautogui.drag(0, distance, duration=0.2) # التحرك للأسفل ➏ pyautogui.drag(-distance, 0, duration=0.2) # التحرك يسارًا distance = distance – change pyautogui.drag(0, -distance, duration=0.2) # التحرك للأعلى سيكون هناك تأخير لمدة خمس ثوانٍ ➊ عند تشغيل هذا البرنامج لتتمكّن من تحريك مؤشر الفأرة على نافذة برنامج الرسم مع تحديد أداة القلم أو الفرشاة، ثم سيتحكّم برنامج spiralDraw.py في الفأرة وينقر لتنشيط نافذة برنامج الرسم ➋. النافذة النشطة هي النافذة التي تقبل حاليًا الإدخال من لوحة المفاتيح، وستؤثّر الإجراءات التي تتخذها مثل الكتابة أو سحب الفأرة على تلك النافذة، وتُعرَف النافذة النشطة أيضًا بالنافذة المُركَّزة أو النافذة الأمامية. يرسم برنامج spiralDraw.py نمطًا حلزونيًا مربعًا مثل النمط الموجود على يسار الشكل الآتي بعد أن يصبح برنامج الرسم نشطًا. يمكنك أيضًا إنشاء صورة حلزونية مربعة باستخدام الوحدة Pillow التي ناقشناها في المقال السابق، ولكن يتيح لك إنشاء الصورة من خلال التحكم في الفأرة لرسمها في برنامج الرسام MS Paint الاستفادةَ من أنماط الفرشاة المتنوعة لهذا البرنامج كما في الشكل الموجود على يمين الشكل التالي، بالإضافة إلى ميزات متقدمة أخرى مثل التدرجات أو أداة التعبئة، حيث يمكنك تحديد إعدادات الفرشاة مسبقًا بنفسك أو جعل شيفرة بايثون الخاصة بك تحدّد هذه الإعدادات، ثم يمكنك تشغيل برنامج الرسم الحلزوني. نتائج مثال استخدام الدالة pyautogui.drag() المرسومة باستخدام فُرش برنامج الرسام المختلفة يبدأ المتغير distance عند القيمة 200، لذلك يسحب استدعاء الدالة drag() الأول المؤشر بمقدار 200 بكسل إلى اليمين، ويستغرق ذلك 0.2 ثانية ➌ في التكرار الأول لحلقة while، ثم تُقلَّل قيمة المتغير distance إلى القيمة 195 ➍، ويسحب استدعاء الدالة drag() الثاني المؤشر بمقدار 195 بكسل للأسفل ➎. يسحب استدعاء الدالة drag() الثالث المؤشر بمقدار -195 أفقيًا (أي بمقدار 195 إلى اليسار) ➏، وتُقلَّل قيمة المتغير distance إلى 190، ويسحب استدعاء drag() الأخير المؤشر بمقدار 190 بكسل للأعلى. تُسحَب الفأرة إلى اليمين والأسفل واليسار والأعلى في كل تكرار، وتكون قيمة المتغير distance أصغر قليلًا مما كانت عليه في التكرار السابق. يمكنك تحريك مؤشر الفأرة لرسم شكل حلزوني مربع من خلال تكرار هذه الشيفرة البرمجية. يمكنك رسم هذا الحلزوني يدويًا (أو باستخدام الفأرة)، ولكن يجب أن تعمل ببطء لتكون دقيقًا جدًا، ولكن يمكن للوحدة PyAutoGUI إنجاز ذلك في بضع ثوانٍ. ملاحظة: لا تستطيع الوحدة PyAutoGUI حاليًا إرسال نقرات الفأرة أو ضغطات المفاتيح إلى برامج معينة مثل برامج مكافحة الفيروسات (لمنع الفيروسات من تعطيل البرنامج) أو ألعاب الفيديو على نظام ويندوز (التي تستخدم طريقة مختلفة لتلقي دخل الفأرة ولوحة المفاتيح). يمكنك التحقق من توثيق الوحدة PyAutoGUI على موقعها الرسمي لمعرفة ما إذا كانت هذه الميزات مدعومة في نظامك. التمرير بالفأرة دالة الوحدة PyAutoGUI الأخيرة الخاصة بالفأرة هي الدالة scroll() التي نمرّر إليها وسيطًا نوعه عدد صحيح يمثّل عدد الوحدات التي تريد تمرير الفأرة عبرها للأعلى أو للأسفل، حيث يختلف حجم هذه الوحدة باختلاف نظام التشغيل والتطبيق، لذا يجب اأن تجرّب لمعرفة مقدار التمرير في حالتك، ويُطبَّق التمرير عند الموضع الحالي لمؤشر الفأرة. يؤدي تمرير عدد صحيح موجب إلى التمرير للأعلى، بينما يؤدي تمرير عدد صحيح سالب إلى التمرير للأسفل. شغّل ما يلي في الصدفة التفاعلية للمحرّر Mu أثناء وجود مؤشر الفأرة على نافذة هذا المحرّر: >>> pyautogui.scroll(200) سترى أن برنامج Mu يُمرَّر للأعلى إذا كان مؤشر الفأرة على حقل نصي يمكن تمريره للأعلى. تخطيط حركات الفأرة إحدى صعوبات كتابة برنامج يؤتمت عملية النقر على الشاشة هي العثور على إحداثيات x و y للأشياء التي ترغب في النقر عليها، ولكن يمكن أن تساعدك الدالة pyautogui.mouseInfo() في هذا الأمر، حيث يُفترَض استدعاء هذه الدالة من الصدفة التفاعلية، وليس كجزء من برنامجك. تشغِّل هذه الدالة تطبيقًا صغيرًا اسمه MouseInfo المُضمَّن مع الوحدة PyAutoGUI، وتبدو نافذة هذا التطبيق كما يلي: نافذة التطبيق MouseInfo أدخِل الآن ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.mouseInfo() يؤدي ذلك إلى ظهور نافذة تطبيق MouseInfo، حيث توفر لك هذه النافذة معلومات حول الموضع الحالي لمؤشر الفأرة، بالإضافة إلى لون البكسل الموجود تحت مؤشر الفأرة كمجموعة RGB مكونة من ثلاثة أعداد صحيحة وكقيمة ست عشرية، حيث يظهر اللون نفسه في مربع اللون Color الموجود في النافذة. يمكنك تسجيل معلومات الإحداثيات أو البكسلات من خلال النقر على أحد أزرار النسخ Copy أو التسجيل Log الثمانية، حيث ستنسخ أزرار Copy All و Copy XY و Copy RGB و Copy RGB Hex معلوماتها الخاصة في الحافظة Clipboard، وستكتب الأزرار Log All و Log XY و Log RGB و Log RGB Hex معلوماتها الخاصة في الحقل النصي الكبير من هذه النافذة، ويمكنك حفظ النص الموجود في هذا الحقل لتسجيل النص بالنقر على زر الحفظ Save Log. لاحظ تحديد مربع الاختيار 3 Sec. Button Delay افتراضيًا، مما يتسبب في تأخير لمدة 3 ثوانٍ بين النقر على زر النسخ Copy أو التسجيل Log وحدوث النسخ أو التسجيل، ويمنحك ذلك وقتًا قصيرًا للنقر على الزر ثم تحريك الفأرة إلى الموضع المطلوب. قد يكون من الأسهل إلغاء تحديد مربع الاختيار 3 Sec. Button Delay، وتحريك الفأرة إلى موضعٍ معين، ثم الضغط على المفاتيح من F1 إلى F8 لنسخ موضع الفأرة أو تسجيله. يمكنك إلقاء نظرة على قوائم النسخ والتسجيل الموجودة أعلى نافذة تطبيق MouseInfo لمعرفة المفاتيح المرتبطة بهذه الأزرار. ألغِ مثلًا تحديد مربع الاختيار 3 Sec. Button Delay، ثم حرّك الفأرة على الشاشة أثناء الضغط على الزر F6، ولاحظ كيفية تسجيل إحداثيات x و y للفأرة في الحقل النصي الكبير في منتصف النافذة، حيث يمكنك لاحقًا استخدام هذه الإحداثيات في سكربتات PyAutoGUI الخاصة بك. اطّلع على توثيق تطبيق MouseInfo الكامل لمزيدٍ من المعلومات. العمل مع الشاشات ليس من الضروري أن تنقر أو تكتب برامجُك لأتمتة واجهة المستخدم الرسومية دون رؤية ما يحدث، إذ تحتوي الوحدة PyAutoGUI على ميزات لقطة الشاشة التي يمكنها إنشاء ملف صورة بناءً على محتويات الشاشة الحالية، ويمكن لهذه الدوال أيضًا إعادة كائن Image الخاص بالوحدة Pillow لمظهر الشاشة الحالي. اطّلع على المقال السابق وثبّت الوحدة pillow قبل الاستمرار في هذا القسم. يجب أن تثبّت برنامج scrot على الحواسيب التي تعمل على نظام لينكس لاستخدام دوال لقطة الشاشة في الوحدة PyAutoGUI، لذا شغّل الأمر sudo apt install scrot في نافذةالطرفية لتثبيت هذا البرنامج. إذا كنت تستخدم نظام تشغيل ويندوز أو ماك، فانتقل إلى الخطوة التالية من هذا القسم. الحصول على لقطة الشاشة يمكنك التقاط لقطات شاشة في لغة بايثون من خلال استدعاء الدالة pyautogui.screenshot()، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> im = pyautogui.screenshot() سيحتوي المتغير im على كائن Image للقطة الشاشة، وبالتالي يمكنك الآن استدعاء التوابع مع كائن Image في المتغير im مثل أيّ كائن Image آخر. اطّلع على المقال السابق للحصول على مزيدٍ من التفاصيل حول كائنات Image. تحليل لقطة الشاشة لنفترض أن إحدى الخطوات في برنامجك لأتمتة واجهة المستخدم الرسومية هي النقر على زر رمادي، حيث يمكنك التقاط لقطة شاشة قبل استدعاء التابع click() وإلقاء نظرة على البكسل الذي سينقر سكربتك عليه، فإن لم يكن لون هذا البكسل رماديًا مثل الزر الرمادي، فهذا يعني أن برنامجك يعلم أن هناك خطأً ما، وبالتالي قد تتحرك النافذة بطريقة غير متوقعة، أو قد يوقِف مربع حوار منبثق الزر. يمكن لبرنامجك عندها رؤية أنه لا ينقر على الشيء الصحيح ويوقِف نفسه بدلًا من الاستمرار وإحداث فوضى من خلال النقر على الشيء الخطأ. يمكنك الحصول على قيمة لون RGB لبكسل معين على الشاشة باستخدام الدالة pixel(). أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.pixel((0, 0)) (176, 176, 175) >>> pyautogui.pixel((50, 200)) (130, 135, 144) مرّر مجموعة من الإحداثيات مثل (0, 0) أو (50, 200) إلى الدالة pixel() التي ستخبرك بلون البكسل عند تلك الإحداثيات في صورتك. القيمة المُعادة من الدالة pixel() هي مجموعة RGB مكونة من ثلاثة أعداد صحيحة تمثّل مقدار اللون الأحمر والأخضر والأزرق في البكسل، ولا توجد قيمة رابعة تمثّل قيمة ألفا Alpha، لأن صور لقطة الشاشة معتمة Opaque تمامًا. تعيد الدالة pixelMatchesColor() الخاصة بوحدة PyAutoGUI القيمة True إذا تطابق البكسل الموجود عند إحداثيات x و y المُعطاة على الشاشة مع اللون المُعطَى. يُعَد الوسيطان الأول والثاني أعدادًا صحيحة تمثّل إحداثيات x و y، والوسيط الثالث هو مجموعة مكونة من ثلاثة أعداد صحيحة تمثّل لون RGB الذي يجب أن يتطابق مع البكسل الموجود على الشاشة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui ➊ >>> pyautogui.pixel((50, 200)) (130, 135, 144) ➋ >>> pyautogui.pixelMatchesColor(50, 200, (130, 135, 144)) True ➌ >>> pyautogui.pixelMatchesColor(50, 200, (255, 135, 144)) False استخدمنا الدالة pixel() للحصول على مجموعة RGB التي تمثّل لون البكسل عند إحداثيات مُحدَّدة ➊، وسنمرّر الآن الإحداثيات نفسها ومجموعة RGB إلى الدالة pixelMatchesColor() ➋، والتي يجب أن تعيد القيمة True. نغيّر بعد ذلك قيمةً من مجموعة RGB ونستدعي الدالة pixelMatchesColor() مرةً أخرى مع الإحداثيات نفسها ➌، حيث يجب أن تعيد القيمة False. يمكن أن يكون استدعاء هذه الدالة مفيدًا عندما تكون برامجك لأتمتة واجهة المستخدم الرسومية على وشك استدعاء التابع click() . لاحظ أن اللون عند الإحداثيات المحددة يجب أن يكون متطابقًا تمامًا، حيث إذا كان مختلفًا قليلًا مثل (255, 255, 254) بدلًا من (255, 255, 255)، فستعيد الدالة pixelMatchesColor() القيمة False. التعرف على الصور إن لم تكن على معرفة مسبقة بالمكان الذي يجب أن تنقر عليه الوحدة PyAutoGUI، فيمكنك استخدام ميزة التعرّف على الصور، لذا أعطِ وحدة PyAutoGUI صورةً لما تريد النقر عليه ودعه يكتشف الإحداثيات. إذا التقطتَ مسبقًا لقطة شاشة للحصول على صورة زر الإرسال Submit في الملف submit.png مثلًا، فستعيد الدالة locateOnScreen() إحداثيات مكان وجود تلك الصورة. لنتعرّف على كيفية عمل هذه الدالة، لذا حاول التقاط لقطة شاشة لمنطقة صغيرة من شاشتك، ثم احفظ الصورة وأدخِل ما يلي في الصدفة التفاعلية مع وضع اسم ملف لقطة الشاشة الخاصة بك مكان 'submit.png': >>> import pyautogui >>> b = pyautogui.locateOnScreen('submit.png') >>> b Box(left=643, top=745, width=70, height=29) >>> b[0] 643 >>> b.left 643 يُعَد كائن Box مجموعةً مسماة تعيدها الدالة locateOnScreen() وله إحداثي x للحافة اليسرى وإحداثي y للحافة العلوية والعرض والارتفاع لمكان العثور على الصورة الأول على الشاشة. إذا طبّقتَ ذلك على حاسوبك باستخدام لقطة شاشتك، فستكون القيمة المُعادة مختلفة عن القيمة الموضحة في مثالنا. إذا تعذر العثور على الصورة على الشاشة، فستعيد الدالة locateOnScreen() القيمة None. لاحظ أن الصورة الموجودة على الشاشة يجب أن تتطابق تمامًا مع الصورة المُقدَّمة للتعرّف عليها، حيث إذا كانت الصورة مختلفة ببكسل واحد، فسترفع الدالة locateOnScreen() الاستثناء ImageNotFoundException. إذا غيّرتَ دقة الشاشة، فقد لا تتطابق الصور من لقطات الشاشة السابقة مع الصور الموجودة على شاشتك الحالية، لذا يمكنك تغيير القياسات في إعدادات العرض لنظام تشغيلك كما هو موضّح في الشكل التالي: إعدادات قياسات العرض في نظام ويندوز 10 (على اليسار) ونظام ماك (على اليمين) إذا عُثِر على الصورة في عدة أماكن على الشاشة، فستعيد الدالة locateAllOnScreen() كائن Generator الذي يمكنك تمريره إلى التابع list() لإعادة قائمة من المجموعات المكونة من أربعة أعداد صحيحة، حيث ستوجد مجموعة واحدة مكونة من أربعة أعداد صحيحة لكل موقع توجد فيه الصورة على الشاشة. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي مع وضع ملف الصورة الخاص بك مكان 'submit.png': >>> list(pyautogui.locateAllOnScreen('submit.png')) [(643, 745, 70, 29), (1007, 801, 70, 29)] تمثل كل مجموعة من هذه المجموعات المكونة من أربعة أعداد صحيحة منطقةً من الشاشة، حيث تظهَر الصورة في موقعين في المثال السابق. إذا وُجِدت صورتُك في منطقةٍ واحدة فقط، فسيؤدي استخدام التابع list() والدالة locateAllOnScreen() إلى إعادة قائمة تحتوي على مجموعة واحدة فقط. نحصل على المجموعة المكونة من أربعة أعداد صحيحة التي تمثّل الصورة التي تريد تحديدها، ثم يمكننا النقر على مركز هذه المنطقة من خلال تمرير هذه المجموعة إلى التابع click(). لندخل الآن مثلًا ما يلي في الصدفة التفاعلية: >>> pyautogui.click((643, 745, 70, 29)) يمكنك أيضًا تمرير اسم ملف الصورة مباشرةً إلى التابع click() كما يلي: >>> pyautogui.click('submit.png') تقبل الدالتان moveTo() و dragTo() أيضًا وسطاء لاسم ملف الصورة. تذكّر أن الدالة locateOnScreen() ترفع استثناءً إن لم تتمكّن من العثور على الصورة على الشاشة، لذا يجب أن تستدعيها من داخل تعليمة try كما يلي: try: location = pyautogui.locateOnScreen('submit.png') except: print('Image could not be found.') سيؤدي استثناء عدم العثور على الصورة على الشاشة إلى تعطّل برنامجك إن لم تستخدم تعليمات try و except، لذا من الجيد استخدام تعليمات try و except عند استدعاء الدالة locateOnScreen() لأنك لا تستطيع التأكّد من أن برنامجك سيعثر على الصورة دائمًا. الحصول على معلومات النافذة يُعَد التعرّف على الصور طريقة ضعيفة للعثور على الأشياء التي تظهر على الشاشة، حيث إذا كان هناك بكسل واحد بلون مختلف، فلن تتمكن الدالة pyautogui.locateOnScreen() من العثور على الصورة، لذا إذا كنت بحاجة إلى العثور على مكان وجود نافذة معينة على الشاشة، فمن الأسرع والأكثر موثوقية استخدام ميزات النافذة الخاصة بوحدة PyAutoGUI. ملاحظة: تعمل ميزات النافذة الخاصة بوحدة PyAutoGUI على نظام تشغيل ويندوز فقط، ولا تعمل على نظام تشغيل ماك أو لينكس ابتداءً من الإصدار 0.9.46، وتأتي هذه الميزات من احتواء الوحدة PyAutoGUI على الوحدة PyGetWindow. الحصول على النافذة النشطة النافذة النشطة على شاشتك هي النافذة الموجودة حاليًا في المقدمة والتي تقبل الإدخال من لوحة المفاتيح. إذا كنت تكتب حاليًا شيفرة برمجية في المحرّر Mu، فإن نافذته هي النافذة النشطة، حيث ستُنشَّط نافذة واحدة فقط من بين جميع النوافذ التي تظهر على شاشتك في كل مرة. استدعِ الدالة pyautogui.getActiveWindow() في الصدفة التفاعلية للحصول على كائن Window أو كائن Win32Window عند التشغيل على نظام ويندوز. يمكنك استرداد أيٍّ من سمات الكائن Window التي تمثّل حجمه وموضعه وعنوانه بعد الحصول عليه وهذه السمات هي: left و right و top و bottom: عدد صحيح واحد يمثّل الإحداثي x أو y لطرف النافذة. topleft و topright و bottomleft و bottomright: مجموعة مسماة مكونة من عددين صحيحين يمثّلان إحداثيات (x, y) لزاوية النافذة. midleft و midright و midleft و midright: مجموعة مسماة مكونة من عددين صحيحين يمثلان إحداثيات (x, y) لمنتصف طرف النافذة. width و height: عدد صحيح واحد يمثّل أحد أبعاد النافذة بالبكسل. size: مجموعة مسماة مكونة من عددين صحيحين يمثّلان عرض وارتفاع النافذة (width, height). area: عدد صحيح واحد يمثل مساحة النافذة بالبكسل. center: مجموعة مسماة مكونة من عددين صحيحين يمثلان إحداثيات (x, y) لمركز النافذة. centerx و centery : عدد صحيح واحد يمثل إحداثي x أو y لمركز النافذة. box: مجموعة مسماة مكونة من أربعة أعداد صحيحة لقياسات يسار وأعلى وعرض وارتفاع النافذة (left, top, width, height). title: سلسلة من النص الموجود في شريط العنوان أعلى النافذة. أدخِل مثلًا ما يلي في الصدفة التفاعلية للحصول على معلومات موضع النافذة وحجمها وعنوانها من كائن Window: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() >>> fw Win32Window(hWnd=2034368) >>> str(fw) '<Win32Window left="500", top="300", width="2070", height="1208", title="Mu 1.0.1 – test1.py">' >>> fw.title 'Mu 1.0.1 – test1.py' >>> fw.size (2070, 1208) >>> fw.left, fw.top, fw.right, fw.bottom (500, 300, 2070, 1208) >>> fw.topleft (256, 144) >>> fw.area 2500560 >>> pyautogui.click(fw.left + 10, fw.top + 20) يمكنك الآن استخدام هذه السمات لحساب الإحداثيات الدقيقة في النافذة، فمثلًا إذا كنت تعلم أن الزر الذي تريد النقر عليه يقع دائمًا على بعد 10 بكسلات على اليمين و20 بكسلًا للأسفل من الزاوية العلوية اليسرى للنافذة، وأن الزاوية العلوية اليسرى للنافذة تقع عند إحداثيات الشاشة (300, 500)، فسيؤدي استدعاء التابع pyautogui.click(310, 520) (أو pyautogui.click(fw.left + 10, fw.top + 20) إذا احتوى المتغير fw على كائن Window الخاص بالنافذة) إلى النقر على هذا الزر، وبالتالي لن تضطر إلى الاعتماد على الدالة locateOnScreen() الأبطأ والأقل موثوقية للعثور على الزر. طرق أخرى للحصول على النافذة تُعَد الدالة getActiveWindow() مفيدةً للحصول على النافذة النشطة في وقت استدعاء الدالة، ولكن ستحتاج إلى استخدام بعض الدوال الأخرى للحصول على كائنات Window للنوافذ الأخرى على الشاشة، حيث تعيد الدوال الأربع التالية قائمةً بكائنات Window، وإن لم تتمكّن من العثور على أيّ نوافذ، فستعيد قائمةً فارغة: pyautogui.getAllWindows(): تعيد قائمةً بكائنات Window لكل نافذة مرئية على الشاشة. pyautogui.getWindowsAt(x, y): تعيد قائمةً بكائنات Window لكل نافذة مرئية تتضمن النقطة (x, y). pyautogui.getWindowsWithTitle(title): تعيد قائمةً بكائنات Window لكل نافذة مرئية تتضمن السلسلة النصية title في شريط العنوان الخاص بها. pyautogui.getActiveWindow(): تعيد كائن Window للنافذة التي تتلقّى تركيز لوحة المفاتيح حاليًا. تحتوي الوحدة PyAutoGUI أيضًا على الدالة pyautogui.getAllTitles() التي تعيد قائمةً بالسلاسل النصية لكل نافذة مرئية. معالجة النوافذ يمكن لسمات النوافذ أن تفعل أكثر من مجرد إخبارك بحجم النافذة وموضعها، إذ يمكنك أيضًا ضبط قيمها لتغيير حجم النافذة أو تحريكها، فمثلًا أدخِل ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() ➊ >>> fw.width # الحصول على العرض الحالي للنافذة 1669 ➋ >>> fw.topleft # الحصول على الموضع الحالي للنافذة (174, 153) ➌ >>> fw.width = 1000 # تغيير حجم العرض ➍ >>> fw.topleft = (800, 400) # تحريك النافذة نستخدم أولًا سمات كائن Window لمعرفة معلومات حول حجم النافذة ➊ وموضعها ➋. يجب أن تتحرك النافذة ➍ وتصبح أضيق ➌ كما في الشكل التالي بعد استدعاء هذه الدوال في المحرّر Mu: نافذة المحرّر Mu قبل (في الأعلى) وبعد (في الأسفل) باستخدام سمات كائن Window لتحريك النافذة وتغيير حجمها يمكنك أيضًا اكتشاف وتغيير حالات تصغير النافذة وتكبيرها وتنشيطها، لذا جرّب إدخال ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() ➊ >>> fw.isMaximized # تعيد القيمة True عند تكبير النافذة False ➋ >>> fw.isMinimized # تعيد القيمة True عند تصغير النافذة False ➌ >>> fw.isActive # تعيد القيمة True إذا كانت النافذة نشطة True ➍ >>> fw.maximize() # تكبير النافذة >>> fw.isMaximized True ➎ >>> fw.restore() # التراجع عن إجراء التصغير/التكبير ➏ >>> fw.minimize() # تصغير النافذة >>> import time >>> # الانتظار 5 ثوانٍ أثناء تنشيط نافذة مختلفة: ➐ >>> time.sleep(5); fw.activate() ➑ >>> fw.close() # سيؤدي ذلك إلى إغلاق النافذة التي تكتب فيها تحتوي السمات isMaximized ➊ و isMinimized ➋ و isActive ➌ على قيمٍ منطقية تشير إلى ما إذا كانت النافذة حاليًا في حالة التكبير أو التصغير أو التنشيط أم لا، بينما تغير التوابع maximize() ➍ و minimize() ➏ و activate() ➐ و restore() ➎ حالة النافذة، وسيعيد التابع restore() النافذة إلى حجمها وموضعها السابق بعد تكبير النافذة أو تصغيرها باستخدام التابعين maximize() و minimize(). يغلق التابع close() النافذة، ولكن كن حذرًا عند استخدام هذا التابع، لأنه قد يتجاوز أي مربعات حوار للرسائل التي تطلب منك حفظ عملك قبل الخروج من التطبيق. يمكن العثور على التوثيق الكامل لميزة التحكم في النوافذ الخاصة بوحدة PyAutoGUI على موقعها الرسمي. يمكنك أيضًا استخدام هذه الميزات بصورة منفصلة عن الوحدة PyAutoGUI مع الوحدة PyGetWindow الذي يمكنك الاطلاع على توثيقها على موقعها الرسمي. التحكم بلوحة المفاتيح تحتوي الوحدة PyAutoGUI أيضًا على دوال لإرسال ضغطات مفاتيح افتراضية إلى حاسوبك، والتي تمكّنك من ملء الاستمارات أو إدخال نصٍ في التطبيقات. إرسال سلسلة نصية من لوحة المفاتيح ترسل الدالة pyautogui.write() ضغطات مفاتيح افتراضية إلى الحاسوب، حيث يعتمد ما تفعله هذه الضغطات على النافذة النشطة والحقل النصي المُركَّز عليه، لذا قد ترغب أولًا في إرسال نقرة بالفأرة إلى الحقل النصي الذي تريده للتأكد من التركيز عليه. لنستخدم لغة بايثون لكتابة النص "Hello, world!" في نافذة محرر الملفات. افتح أولًا نافذة جديدة في محرّر ملفاتك وَضَعها في الزاوية العلوية اليسرى من شاشتك بحيث تنقر وحدة PyAutoGUI في المكان المناسب للتركيز عليها، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> pyautogui.click(100, 200); pyautogui.write('Hello, world!') لاحظ كيف أننا وضعنا أمرين على السطر نفسه، وفصلنا بينهما بفاصلة منقوطة، مما يمنع الصدفة التفاعلية من مطالبتك بالإدخال بين تشغيل هاتين التعليمتين، ويمنعك من التركيز على نافذة جديدة عن طريق الخطأ بين الاستدعائين click() و write() الذي يمكن أن يفسد مثالنا. سترسل شيفرة بايثون أولًا نقرة افتراضية بالفأرة إلى الإحداثيات (100, 200)، والتي يجب أن تنقر على نافذة محرّر الملفات لنقل التركيز إليها، وسيرسل استدعاء الدالة write() النص "Hello, world!" إلى النافذة، مما يجعلها تبدو كما في الشكل التالي، وبالتالي أصبح لديك الآن شيفرة برمجية يمكن أن تكتب نيابةً عنك. استخدام وحدة PyAutogGUI للنقر على نافذة محرّر الملفات وكتابة النص "Hello, world!" فيها ستكتب الدالة write() افتراضيًا السلسلة النصية الكاملة مباشرةً، ولكن يمكنك تمرير وسيطٍ ثانٍ اختياري لإضافة توقف قصير بين كل محرف والآخر، وهذا الوسيط هو عدد صحيح أو عشري يمثل عدد الثواني للتوقف مؤقتًا، فمثلًا سينتظر الاستدعاء pyautogui.write('Hello, world!', 0.25) ربع ثانية بعد كتابة الحرف H، وربع ثانية أخرى بعد كتابة الحرف e، وإلخ. قد يكون تأثير الآلة الكاتبة التدريجي مفيدًا للتطبيقات الأبطأ التي لا يمكنها معالجة ضغطات المفاتيح بسرعة كافية للتماشي مع الوحدة PyAutoGUI. ستحاكي أيضًا الوحدة PyAutoGUI الضغط باستمرار على مفتاح SHIFT آليًا بالنسبة للمحارف A أو ! . أسماء المفاتيح لا يُعَد تمثيل كافة المفاتيح باستخدام محارف نصية مفردة أمرًا سهلًا مثل تمثيل المفتاح SHIFT أو مفتاح السهم الأيسر بمحرف واحد، لذا تمثل وحدة PyAutoGUI مفاتيح لوحة المفاتيح هذه بقيم سلاسل نصية قصيرة، فمثلًا نمثّل مفتاح ESC باستخدام السلسلة النصية 'esc' و نمثّل مفتاح ENTER باستخدام السلسلة النصية 'enter'. يمكن تمرير قائمة بالسلاسل النصية لهذه المفاتيح إلى الدالة write() بدلًا من تمرير وسيط سلسلة نصية واحدة، فمثلًا يضغط الاستدعاء التالي على المفتاح A ثم على المفتاح B ثم على مفتاح السهم الأيسر مرتين ويضغط أخيرًا على المفتاحين X و Y: >>> pyautogui.write(['a', 'b', 'left', 'left', 'X', 'Y']) يؤدي الضغط على مفتاح السهم الأيسر إلى تحريك مؤشر لوحة المفاتيح، لذا سينتج عن ذلك الخرج XYab. يوضّح الجدول الآتي السلاسل النصية لمفاتيح لوحة المفاتيح الخاصة بوحدة PyAutoGUI، والتي يمكنك تمريرها إلى الدالة write() لمحاكاة الضغط على أيّ مجموعة من المفاتيح. يمكنك أيضًا الاطلاع على قائمة pyautogui.KEYBOARD_KEYS لرؤية جميع السلاسل النصية لمفاتيح لوحة المفاتيح المحتملة التي ستقبلها وحدة PyAutoGUI. تشير السلسلة النصية 'shift' إلى مفتاح SHIFT الأيسر وهي تكافئ السلسلة النصية 'shiftleft'، وينطبق الأمر نفسه على السلاسل النصية 'ctrl' و 'alt' و 'win' التي تشير جميعها إلى مفتاح الجهة اليسرى من لوحة المفاتيح. يوضح الجدول التالي قيم سمات PyKeyboard: السلسلة النصية لمفتاح لوحة المفاتيح معناها 'a' و 'b' و 'c' و 'A' و 'B' و 'C' و '1' و '2' و '3' و '!' و '@' و '#' وإلخ مفاتيح المحارف المفردة 'enter' (أو 'return' أو '\n') مفتاح ENTER 'esc' مفتاح ESC 'shiftleft' و 'shiftright' مفتاحا SHIFT الأيسر والأيمن 'altleft' و 'altright' مفتاحا ALT الأيسر والأيمن 'ctrlleft' و 'ctrlright' مفتاحا CTRL الأيسر والأيمن 'tab' (أو '\t') مفتاح TAB 'backspace' و 'delete' مفتاح BACKSPACE ومفتاح DELETE 'pageup' و 'pagedown' مفتاح PAGE UP ومفتاح PAGE DOWN 'home' و 'end' مفتاح HOME ومفتاح END 'up' و 'down' و 'left' و 'right' مفاتيح الأسهم للأعلى وللأسفل وإلى اليسار وإلى اليمين 'f1' و 'f2' و 'f3' وإلخ المفاتيح من F1 إلى F12 'volumemute' و 'volumedown' و 'volumeup' مفاتيح كتم الصوت وخفض مستوى الصوت ورفع مستوى الصوت. لا تحتوي بعض لوحات المفاتيح على هذه المفاتيح، ولكن سيبقى نظام تشغيل حاسوبك قادرًا على فهم محاكاة هذه الضغطات للمفاتيح 'pause' مفتاح PAUSE 'capslock' و 'numlock' و 'scrolllock' مفتاح CAPS LOCK ومفتاح NUM LOCK ومفتاح SCROLL LOCK 'insert' مفتاح INS أو INSERT 'printscreen' مفتاح PRTSC أو PRINT SCREEN 'winleft' و 'winright' مفتاح WIN الأيسر والأيمن على نظام ويندوز 'command' مفتاح Command على نظام ماك 'option' مفتاح OPTION على نظام ماك الضغط على لوحة المفاتيح وتحريرها ترسل الدالتان pyautogui.keyDown() و pyautogui.keyUp() ضغطات مفاتيح افتراضية وتحريرها إلى الحاسوب مثل الدالتين mouseDown() و mouseUp()، ونمرّر إلى هاتين الدالتين سلسلة نصية لمفاتيح لوحة المفاتيح (اطّلع على الجدول السابق) كوسيطٍ لها. توفّر وحدة PyAutoGUI الدالة pyautogui.press() التي تستدعي هاتين الدالتين لمحاكاة ضغطة كاملة على المفاتيح. شغّل الشيفرة البرمجية التالية التي ستكتب محرف إشارة الدولار $ الذي نحصل عليه من خلال الضغط على مفتاح SHIFT مع الضغط على الرقم 4: >>> pyautogui.keyDown('shift'); pyautogui.press('4'); pyautogui.keyUp('shift') تضغط التعليمة السابقة على مفتاح SHIFT، ثم تضغط على (وتحرر) الرقم 4، ثم تحرّر مفتاح SHIFT. إذا أردتَ كتابة سلسلة نصية في حقل نصي، فستكون الدالة write() أكثر ملاءمة، ولكن ستكون الدالة press() الطريقة الأبسط بالنسبة للتطبيقات التي تأخذ أوامرًا ذات مفتاح واحد. مجموعات مفاتيح التشغيل السريع Hotkey أو الاختصارات مفاتيح التشغيل السريع أو الاختصارات هي مجموعة من الضغطات على المفاتيح لاستدعاء بعض وظائف التطبيق، فمفتاح التشغيل السريع الشائع لنسخ تحديدٍ مثلًا هو CTRL-C في نظامي تشغيل ويندوز ولينكس أو -C في نظام تشغيل ماك. يضغط المستخدم مع الاستمرار على مفتاح CTRL، ثم يضغط على المفتاح C، ثم يحرّر المفتاحين C و CTRL، حيث يمكننا تطبيق ذلك باستخدام الدالتين keyDown() و keyUp() الخاصتين بالوحدة PyAutoGUI من خلال إدخال ما يلي: pyautogui.keyDown('ctrl') pyautogui.keyDown('c') pyautogui.keyUp('c') pyautogui.keyUp('ctrl') يُعَد ذلك معقدًا إلى حدٍ ما، لذا استخدم الدالة pyautogui.hotkey() بدلًا من ذلك، حيث تأخذ هذه الدالة عدة وسطاء تمثّل السلسلة النصية لمفاتيح لوحة المفاتيح، وتضغط عليها بالترتيب، ثم تحررها بالترتيب العكسي، إذ ستكون الشيفرة البرمجية الخاصية بمثال CTRL-C ببساطة كما يلي: pyautogui.hotkey('ctrl', 'c') تُعَد هذه الدالة مفيدة خاصةً لمجموعات مفاتيح التشغيل السريع الأكبر حجمًا، فمثلًا تعرض مجموعة مفاتيح التشغيل السريع Ctrl-Alt-Shift-S لوحة الأنماط Style في برنامج وورد Word، حيث يمكنك استخدام الاستدعاء hotkey('ctrl', 'alt', 'shift', 's') فقط بدلًا من إجراء ثمانية استدعاءات لدوال مختلفة (أربعة استدعاءات للدالة keyDown() وأربعة استدعاءات للدالة keyUp()). إعداد سكربتات أتمتة واجهة المستخدم الرسومية تُعَد سكربتات أتمتة واجهة المستخدم الرسومية طريقةً رائعة لأتمتة المهام المملة، ولكنها قد تكون صعبة التحقيق، حيث إذا كان هناك نافذة في مكان خاطئ على سطح المكتب أو ظهرت بعض النوافذ المنبثقة بطريقة غير متوقعة، فقد ينقر السكربت الخاص بك على الأشياء الخاطئة على الشاشة، لذا إليك بعض النصائح لإعداد سكربتات أتمتة واجهة المستخدم الرسومية: استخدم دقة الشاشة نفسها في كل مرة تشغّل فيها السكربت حتى لا يتغير موضع النوافذ. يجب تكبير نافذة التطبيق التي ينقر عليها السكربت الخاص بك بحيث تكون الأزرار والقوائم في المكان نفسه في كل مرة تشغّل فيها السكربت. أضِف فترات توقف كافية أثناء انتظار تحميل المحتوى، إذ لا تريد أن يبدأ السكربت بالنقر قبل أن يصبح التطبيق جاهزًا. استخدم الدالة locateOnScreen() للعثور على الأزرار والقوائم التي يمكنك النقر عليها بدلًا من الاعتماد على إحداثيات XY. إن لم يتمكّن السكربت الخاص بك من العثور على الشيء الذي يريد النقر عليه، فأوقِف البرنامج بدلًا من السماح له بمواصلة النقر عشوائيًا. استخدم الدالة getWindowsWithTitle() للتأكّد من وجود نافذة التطبيق التي تعتقد أن السكربت الخاص بك ينقر عليها، واستخدم التابع activate() لوضع تلك النافذة في المقدمة. استخدم الوحدة logging التي تحدثنا عنها في مقالٍ سابق للاحتفاظ بملفٍ يسجّل ما فعله السكربت الخاص بك، وبالتالي إذا أوقفتَ السكربت في منتصف العملية، فيمكنك تعديله للمتابعة من مكان توقف هذا السكربت. أضِف أكبر عدد ممكن من عمليات التحقق إلى السكربت الخاص بك، فمثلًا يمكن أن يفشل السكربت إذا ظهرت نافذة منبثقة غير متوقعة أو إذا فقدَ حاسوبك اتصاله بالإنترنت. قد ترغب في الإشراف على السكربت عندما يبدأ لأول مرة للتأكد من أنه يعمل بصورة صحيحة. قد ترغب أيضًا في التوقف مؤقتًا في بداية السكربت الخاص بك حتى يتمكّن المستخدم من إعداد النافذة التي سينقر عليها السكربت، حيث تحتوي وحدة PyAutoGUI على الدالة sleep() التي تعمل بطريقة مماثلة للدالة time.sleep()، ولكنها توفّر عليك الاضطرار إلى إضافة التعليمة import time إلى السكربتات الخاصة بك، وتوجد أيضًا الدالة countdown() التي تطبع أرقام العد التنازلي لمنح المستخدم إشارة مرئية إلى أن السكربت سيستأنف عمله قريبًا. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.sleep(3) # إيقاف البرنامج مؤقتًا لمدة 3 ثوانٍ >>> pyautogui.countdown(10) # العد التنازلي لمدة 10 ثوانٍ 10 9 8 7 6 5 4 3 2 1 >>> print('Starting in ', end=''); pyautogui.countdown(3) Starting in 3 2 1 يمكن أن تساعد هذه النصائح في جعل سكربتات أتمتة واجهة المستخدم الرسومية أسهل في الاستخدام وأكثر قدرة على الاستعادة من الظروف غير المتوقعة. مراجعة لدوال وحدة PyAutoGUI يغطي هذا المقال العديد من الدوال المختلفة، لذا سنوضّح فيما يلي مرجعًا موجزًا سريعًا لهذه الدوال: moveTo(x, y): تحرك مؤشر الفأرة إلى إحداثيات x و y المُحدَّدة. move(xOffset, yOffset): تحرك مؤشر الفأرة بالنسبة إلى موضعه الحالي. dragTo(x, y): تحرك مؤشر الفأرة أثناء الضغط المستمر على زر الفأرة الأيسر. drag(xOffset, yOffset): تحرّك مؤشر الفأرة نسبة إلى موضعه الحالي أثناء الضغط المستمر على زر الفأرة الأيسر. click(x, y, button): تحاكي النقر (بزر الفأرة الأيسر افتراضيًا). rightClick(): تحاكي النقر بالزر الأيمن. middleClick(): تحاكي النقر بالزر الأوسط. doubleClick(): تحاكي النقر المزدوج على الزر الأيسر. mouseDown(x, y, button): تحاكي الضغط على الزر المُحدَّد في الموضع x, y. mouseUp(x, y, button): تحاكي تحرير الزر المُحدَّد في الموضع x, y. scroll(units): تحاكي عجلة التمرير في الفأرة، حيث نمرّر وسيطًا موجبًا للتمرير للأعلى، ونمرّر وسيطًا سالبًا للتمرير للأسفل. write(message): تكتب المحارف الموجودة في سلسلة الرسالة النصية المُحدَّدة. write([key1, key2, key3]): تكتب سلاسل نصية لمفاتيح لوحة المفاتيح المُحدَّدة. press(key): تضغط على السلسلة النصية لمفتاحٍ محدَّد من لوحة المفاتيح. keyDown(key): تحاكي الضغط على مفتاح لوحة المفاتيح المُحدَّد. keyUp(key): تحاكي تحرير مفتاح لوحة المفاتيح المُحدَّد. hotkey([key1, key2, key3]): تحاكي الضغط على السلاسل النصية لمفاتيح لوحة المفاتيح المُحدَّدة بالترتيب ثم تحرّرها بترتيب عكسي. screenshot(): تعيد لقطة الشاشة بوصفها كائن Image. اطّلع على المقال السابق للحصول على معلومات حول كائنات Image. getActiveWindow() و getAllWindows() و getWindowsAt() و getWindowsWithTitle(): تعيد هذه الدوال كائنات Window التي يمكنها تغيير حجم نوافذ التطبيقات وإعادة تموضعها على سطح المكتب. getAllTitles(): تعيد قائمةً بالسلاسل النصية لشريط عنوان كلّ نافذةٍ على سطح المكتب. اختبارات كابتشا Captcha وأخلاقيات استخدام الحواسيب تُعَد اختبارات كابتشا Captcha اختصارًا للعبارة الإنجليزية "Completely Automated Public Turing test to tell Computers and Humans Apart" أو "اختبار تورينج العام الآلي بالكامل للتمييز بين الحواسيب والبشر"، وهي الاختبارات الصغيرة التي تطلب منك كتابة حروف موجودة في صورة غير واضحة أو النقر على صور صنابير إطفاء الحرائق مثلًا. يسهُل على البشر اجتياز هذه الاختبارات، ولكن يكاد يكون من المستحيل على البرامج حلها بالرغم من أننا نجدها مزعجة. يمكنك أن ترى بعد قراءة هذا المقال مدى سهولة كتابة سكربت يمكنه التسجيل في مليارات حسابات البريد الإلكتروني المجانية مثلًا أو إغراق المستخدمين برسائل مزعجة، لذا تعمل اختبارات كابتشا على تخفيف ذلك من خلال المطالبة بخطوة لا يمكن إلا للبشر اجتيازها. لا تطبق جميع مواقع الويب اختبارات كابتشا، وقد تكون عرضةً لإساءة الاستخدام من المبرمجين غير الأخلاقيين، إذ يُعَد تعلّم البرمجة مهارة قوية ومهمة، ولكن قد تميل إلى إساءة استخدام هذه القوة لتحقيق مكاسب شخصية أو حتى لمجرد التفاخر، فلا يُعَد الباب المفتوح مبررًا للتعدّي على ممتلكات الآخرين، لذا تقع مسؤولية برامجك على عاتقك الشخصي بوصفك مبرمجًا. لا يُعَد التحايل على الأنظمة لإحداث ضررٍ أو انتهاك الخصوصية أو الحصول على ميزة غير عادلة معيارًا للذكاء، لذا نأمل أن تركز على عملك دون الضرر بالآخرين. تطبيق عملي: ملء الاستمارات آليًا يُعَد ملء الاستمارات مهمةً روتينية مملةً جدًا، إذًا لنفترض مثلًا أن لديك كمية هائلة من البيانات في جدول بيانات، ويجب أن تعيد كتابتها في واجهة استمارة تطبيق آخر دون وجود شخص آخر لإنجاز ذلك نيابةً عنك. تحتوي بعض التطبيقات على ميزة الاستيراد التي تسمح لك برفع جدول بيانات يحتوي على المعلومات، ولكن قد لا توجد طريقة أخرى سوى النقر والكتابة دون اهتمام لساعات متواصلة في بعض الأحيان، لذا لنحاول إيجاد وسيلة لأتمتة هذه المهمة المملة. استمارة هذا المشروع هي استمارة على مستندات جوجل Google Docs، والتي يمكنك العثور عليها على autbor.com، وتبدو كما يلي: الاستمارة المستخدمَة في هذا المشروع إليك الخطوات العامة لما يجب أن يفعله برنامجك: النقر على الحقل النصي الأول من الاستمارة. التنقل عبر الاستمارة، وكتابة المعلومات في كل حقل. النقر على زر الإرسال Submit. تكرار العملية مع المجموعة التالية من البيانات. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: استدعاء الدالة pyautogui.click() للنقر على الاستمارة وزر الإرسال Submit. استدعاء الدالة pyautogui.write() لإدخال النص في الحقول. معالجة الاستثناء KeyboardInterrupt حتى يتمكّن المستخدم من الضغط على الاختصار CTRL-C للإنهاء. افتح نافذة جديدة في محرّرك لإنشاء ملف جديد واحفظه بالاسم formFiller.py. الخطوة الأولى: معرفة الخطوات ملء الاستمارة يجب أولًا معرفة ضغطات المفاتيح ونقرات الفأرة التي ستملأ الاستمارة قبل كتابة الشيفرة البرمجية. يمكن أن يساعدك التطبيق الذي يُشغّله استدعاء الدالة pyautogui.mouseInfo() في معرفة إحداثيات الفأرة المُحدَّدة، ويجب معرفة إحداثيات الحقل النصي الأول فقط، ثم يمكنك الضغط على مفتاح TAB لنقل التركيز إلى الحقل التالي بعد النقر على الحقل الأول، مما يوفّر عليك الاضطرار إلى معرفة إحداثيات x و y للنقر على كل حقل. إليك خطوات إدخال البيانات في الاستمارة: انقل تركيز لوحة المفاتيح على حقل الاسم Name بحيث يؤدي الضغط على المفاتيح إلى كتابة نص في الحقل. اكتب اسمًا، ثم اضغط على مفتاح TAB. اكتب خوفك الأكبر في الحقل Greatest Fear ثم اضغط على مفتاح TAB. اضغط على مفتاح السهم للأسفل عددًا صحيحًا من المرات لتحديد مصدر قواك السحرية Wizard Power Source، إذ ستضغط مرة واحدة لخيار العصا السحرية wand ومرتين لخيار التعويذة amulet وثلاث مرات لخيار الكرة البلورية crystal ball وأربع مرات لخيار المال money، ثم اضغط على مفتاح TAB. لاحظ أنه يجب أن تضغط على مفتاح السهم للأسفل مرة أخرى لكل خيار في نظام ماك macOS، وقد تحتاج إلى الضغط على مفتاح ENTER أيضًا بالنسبة لبعض المتصفحات. اضغط على مفتاح السهم الأيمن لتحديد إجابة سؤال RoboCop، واضغط عليه مرة واحدة للخيار 2 أو مرتين للخيار 3 أو ثلاث مرات للخيار 4 أو أربع مرات للخيار 5 أو اضغط على مفتاح المسافة لتحديد الخيار 1 المُحدَّد افتراضيًا، ثم اضغط على مفتاح TAB. اكتب تعليقًا إضافيًا في الحقل Additional Comments، ثم اضغط على مفتاح TAB. اضغط على مفتاح ENTER للنقر على زر الإرسال Submit. سينقلك المتصفح إلى صفحة أخرى بعد إرسال الاستمارة، حيث يجب اتباع رابط في هذه الصفحة للعودة إلى صفحة الاستمارة. قد تعمل المتصفحات الأخرى على أنظمة تشغيل مختلفة بطريقة مختلفة قليلًا عن الخطوات التي ذكرناها، لذا تأكد من أن هذه المجموعات من ضغطات المفاتيح تعمل على حاسوبك قبل تشغيل البرنامج. الخطوة الثانية: إعداد الإحداثيات حمّل مثال الاستمارة التي نزّلتها (الشكل السابق) في المتصفح من خلال الانتقال إلى الرابط autbor.com، واجعل شيفرتك البرمجية كما يلي: #! python3 # formFiller.py - ملء الاستمارة آليًا import pyautogui, time # منح المستخدم فرصةً لإنهاء السكربت # الانتظار حتى تحميل صفحة الاستمارة # ملء حقل الاسم Name # ملء حقل مخاوفك الكبرى Greatest Fear(s) # ملء حقل مصدر قواك السحرية Source of Wizard Powers # ملء الحقل RoboCop # ملء حقل التعليقات الإضافية Additional Comments # الضغط على زر الإرسال Submit # الانتظار حتى تحميل صفحة الاستمارة # النقر على رابط إرسال رد آخر تحتاج الآن البيانات التي تريد إدخالها فعليًا في هذه الاستمارة، حيث قد تأتي هذه البيانات في العالم الحقيقي من جدول بيانات أو ملف نص عادي أو من موقع ويب، وقد تتطلب شيفرة برمجية إضافية لتحميلها في البرنامج، ولكننا سنكتب كل هذه البيانات ضمن متغير في مثالنا، لذا أضِف ما يلي إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- formData = [{'name': 'Alice', 'fear': 'eavesdroppers', 'source': 'wand', 'robocop': 4, 'comments': 'Tell Bob I said hi.'}, {'name': 'Bob', 'fear': 'bees', 'source': 'amulet', 'robocop': 4, 'comments': 'n/a'}, {'name': 'Carol', 'fear': 'puppets', 'source': 'crystal ball', 'robocop': 1, 'comments': 'Please take the puppets out of the break room.'}, {'name': 'Alex Murphy', 'fear': 'ED-209', 'source': 'money', 'robocop': 5, 'comments': 'Protect the innocent. Serve the public trust. Uphold the law.'}, ] --snip– تحتوي القائمة formData على أربعة قواميس لأربعة أسماء مختلفة، ويحتوي كل قاموس على أسماء الحقول النصية كمفاتيحٍ له والردود كقيمٍ له. أخيرًا، نضبط المتغير PAUSE الخاص بوحدة PyAutoGUI للانتظار لمدة نصف ثانية بعد كل استدعاء دالة، ونذكّر المستخدم بالنقر على المتصفح لجعله النافذة النشطة. أضِف ما يلي إلى برنامجك بعد تعليمة إسناد قيمٍ إلى القائمة formData: pyautogui.PAUSE = 0.5 print('Ensure that the browser window is active and the form is loaded!') الخطوة الثالثة: البدء في كتابة البيانات سنكرّر حلقة for على كلٍّ من القواميس الموجودة في قائمة formData، ونمرّر القيم الموجودة في القاموس إلى دوال وحدة PyAutoGUI التي ستكتب فعليًا في الحقول النصية. أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- for person in formData: # منح المستخدم فرصةً لإنهاء السكربت print('>>> 5-SECOND PAUSE TO LET USER PRESS CTRL-C <<<') ➊ time.sleep(5) --snip– يحتوي السكربت على توقف مؤقت لمدة خمس ثوانٍ ➊ كميزة أمانٍ صغيرة، مما يمنح المستخدم فرصةً للضغط على Ctrl-C (أو تحريك مؤشر الفأرة إلى الزاوية العلوية اليسرى من الشاشة لرفع استثناء FailSafeException) لإيقاف تشغيل البرنامج في حالة أنه يعمل شيئًا غير متوقع. أضِف ما يلي بعد الشيفرة البرمجية التي تنتظر إعطاء الصفحة وقتًا للتحميل: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- ➊ print('Entering %s info...' % (person['name'])) ➋ pyautogui.write(['\t', '\t']) # ملء حقل الاسم Name ➌ pyautogui.write(person['name'] + '\t') # ملء حقل مخاوفك الكبرى Greatest Fear(s) ➍ pyautogui.write(person['fear'] + '\t') --snip– نضيف استدعاء الدالة print() من حينٍ لآخر لعرض حالة البرنامج في نافذته الطرفية لإعلام المستخدم بما يحدث ➊. حصلت الاستمارة على وقتها الكافي للتحميل، لذا نستدعي الدالة pyautogui.write(['\t', '\t']) للضغط على مفتاح TAB مرتين والتركيز على حقل الاسم Name ➋، ثم نستدعي الدالة write() مرة أخرى لإدخال السلسلة النصية في person['name'] ➌. نضيف المحرف '\t' إلى نهاية السلسلة النصية التي نمرّرها إلى الدالة write() لمحاكاة الضغط على مفتاح TAB، مما ينقل تركيز لوحة المفاتيح إلى الحقل التالي وهو Greatest Fear(s). يؤدي استدعاءٌ آخر للدالة write() إلى كتابة السلسلة النصية في person['fear'] ضمن هذا الحقل ثم ينتقل إلى الحقل التالي في الاستمارة ➍. الخطوة الرابعة: التعامل مع قوائم التحديد وأزرار الاختيار تُعَد القائمة المنسدلة لسؤال "القوى السحرية Wizard Powers" وأزرار الاختيار الخاصة بحقل RoboCop أصعب في التعامل من الحقول النصية، حيث يمكنك النقر على هذه الخيارات باستخدام الفأرة من خلال معرفة إحداثيات x و y لكل خيار ممكن، ولكن من الأسهل استخدام مفاتيح الأسهم في لوحة المفاتيح لإجراء التحديد بدلًا من ذلك. أضِف ما يلي إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- # ملء حقل مصدر قواك السحرية Source of Wizard Powers ➊ if person['source'] == 'wand': ➋ pyautogui.write(['down', '\t'] , 0.5) elif person['source'] == 'amulet': pyautogui.write(['down', 'down', '\t'] , 0.5) elif person['source'] == 'crystal ball': pyautogui.write(['down', 'down', 'down', '\t'] , 0.5) elif person['source'] == 'money': pyautogui.write(['down', 'down', 'down', 'down', '\t'] , 0.5) # ملء الحقل RoboCop ➌ if person['robocop'] == 1: ➍ pyautogui.write([' ', '\t'] , 0.5) elif person['robocop'] == 2: pyautogui.write(['right', '\t'] , 0.5) elif person['robocop'] == 3: pyautogui.write(['right', 'right', '\t'] , 0.5) elif person['robocop'] == 4: pyautogui.write(['right', 'right', 'right', '\t'] , 0.5) elif person['robocop'] == 5: pyautogui.write(['right', 'right', 'right', 'right', '\t'] , 0.5) --snip– تذكر أنك كتبتَ شيفرة برمجية لمحاكاة الضغط على مفتاح TAB بعد ملء حقل المخاوف الكبرى Greatest Fear(s)، وبالتالي سيؤدي الضغط على مفتاح السهم للأسفل إلى الانتقال إلى العنصر التالي في قائمة التحديد بعد التركيز على القائمة المنسدلة. يجب أن يرسل برنامجك عددًا من ضغطات مفاتيح السهم للأسفل قبل الانتقال إلى الحقل التالي اعتمادًا على القيمة الموجودة فيperson['source']، حيث إذا كانت قيمة مفتاح 'source' الموجودة في قاموس هذا المستخدم هي 'wand' ➊، فسنحاكي الضغط على مفتاح السهم للأسفل مرة واحدة (لتحديد القيمة Wand) والضغط على مفتاح TAB ➋. إذا كانت القيمة الموجودة في مفتاح 'source' هي 'amulet'، فسنحاكي الضغط على مفتاح السهم للأسفل مرتين والضغط على مفتاح TAB، وينطبق الشيء نفسه بالنسبة للإجابات المُحتمَلة الأخرى. يضيف الوسيط 0.5 في استدعاءات الدالة write() توقفًا مؤقتًا لمدة نصف ثانية بين المفاتيح حتى لا يتحرك البرنامج بسرعة كبيرة في الاستمارة. يمكن تحديد أزرار الاختيار الخاصة بسؤال RoboCop باستخدام مفاتيح الأسهم إلى اليمين، أو إذا أردتَ تحديد الخيار الأول ➌، فاضغط على على شريط المسافة فقط ➍. الخطوة الخامسة: إرسال الاستمارة ثم الانتظار يمكنك ملء حقل التعليقات الإضافية Additional Comments باستخدام الدالة write() من خلال تمرير person['comments'] كوسيطٍ لها. يمكنك كتابة مفتاح '\t' إضافي لنقل تركيز لوحة المفاتيح إلى الحقل التالي أو إلى زر الإرسال Submit. سيؤدي استدعاء الدالة pyautogui.press('enter') إلى محاكاة الضغط على مفتاح ENTER وإرسال الاستمارة بعد التركيز على زر الإرسال Submit، ثم سينتظر برنامجك خمس ثوانٍ حتى تحميل الصفحة التالية. ستحتوي الصفحة الجديدة بعد تحميلها على رابط إرسال ردٍ آخر الذي سيوجّه المتصفح إلى صفحة استمارة جديدة فارغة، حيث خزّنا إحداثيات هذا الرابط بوصفها مجموعةً في المتغير submitAnotherLink في الخطوة الثانية، لذا مرّر هذه الإحداثيات إلى الدالة pyautogui.click() للنقر على هذا الرابط. يمكن لحلقة for الخارجية الخاصة بالسكربت الاستمرار إلى التكرار التالي وإدخال معلومات الشخص التالي في الاستمارة عندما تكون الاستمارة الجديدة جاهزة. أكمِل برنامجك بإضافة الشيفرة البرمجية التالي: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- # ملء حقل التعليقات الإضافية Additional Comments pyautogui.write(person['comments'] + '\t') # الضغط على زر الإرسال Submit من خلال الضغط على مفتاح Enter time.sleep(0.5) # Wait for the button to activate. pyautogui.press('enter') # الانتظار حتى تحميل صفحة الاستمارة print('Submitted form.') time.sleep(5) # النقر على رابط إرسال رد آخر pyautogui.click(submitAnotherLink[0], submitAnotherLink[1]) سيُدخِل البرنامج المعلومات الخاصة بكل شخص بعد انتهاء حلقة for الرئيسية، حيث يوجد في مثالنا أربعة أشخاص للإدخال فقط، ولكن إذا كان لديك 4000 شخص، فستوفر كتابة برنامج لإنجاز ذلك عليك الكثير من الوقت والكتابة. عرض مربعات الرسائل تميل جميع البرامج التي كتبناها حتى الآن إلى استخدام خرجٍ يحتوي على نصٍ عادي باستخدام الدالة print() ودخلٍ يحتوي على نصٍ عادي باستخدام الدالة input()، ولكن ستستخدم برامج PyAutoGUI سطح المكتب بأكمله، إذ يُحتمَل فقدان النافذة النصية التي يعمل فيها برنامجك سواء كانت نافذة المحرّر Mu أو نافذة طرفية Terminal عندما ينقر برنامج PyAutoGUI الخاص بك ويتفاعل مع النوافذ الأخرى، مما يصعّب الحصول على الدخل والخرج من المستخدم عند إخفاء نوافذ المحرّر Mu أو نافذة الطرفية تحت نوافذ أخرى. يمكن حل هذه المشكلة باستخدام وحدة PyAutoGUI التي تقدّم مربعات رسائل منبثقة لتقديم إشعارات للمستخدم وتلقي الدخل منه، إذ توجد أربع دوال لمربعات الرسائل وهي: pyautogui.alert(text): تعرض النص text وتحتوي على زر موافقة OK واحد. +pyautogui.confirm(text): تعرض النص text وتحتوي على زر موافقة OK وزر إلغاء Cancel، وتعرض إما 'OK' أو 'Cancel' اعتمادًا على الزر الذي نقرنا عليه. pyautogui.prompt(text): تعرض النص text وتحتوي على حقل نصي ليكتب المستخدم فيه، والذي تعيده كسلسلة نصية. pyautogui.password(text): تماثل الدالة prompt()، ولكنها تعرض علامات نجمية على النص المُدخَل حتى يتمكّن المستخدم من إدخال معلومات حساسة مثل كلمة المرور. تحتوي هذه الدوال أيضًا على معاملٍ ثانٍ اختياري يقبل قيمة سلسلة نصية لاستخدامها بوصفها عنوانًا في شريط العنوان الخاص بمربع الرسالة. لن تعود هذه الدوال حتى ينقر المستخدم على الزر الموجود عليها، لذلك يمكن استخدامها أيضًا لإدخال فترات توقف مؤقت في برامج PyAutoGUI الخاصة بك. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.alert('This is a message.', 'Important') 'OK' >>> pyautogui.confirm('Do you want to continue?') # اضغط على زر الإلغاء 'Cancel' >>> pyautogui.prompt("What is your cat's name?") 'Zophie' >>> pyautogui.password('What is the password?') 'hunter2' تبدو مربعات الرسائل المنبثقة التي تنتجها السطور السابقة كما يلي: النوافذ من أعلى اليسار إلى أسفل اليمين هي: النوافذ التي تنشئها الدوال alert() و confirm() و prompt() وpassword() يمكن استخدام هذه الدوال لتقديم إشعارات أو طرح أسئلة على المستخدم أثناء تفاعل باقي البرنامج مع الحاسوب من خلال الفأرة ولوحة المفاتيح. اطّلع على التوثيق الكامل عبر الإنترنت للوحدة PyMsgBox على موقعها الرسمي. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لإبقاء الحالة "مشغول" على برنامج المراسلة الفورية تحدّد العديد من برامج المراسلة الفورية ما إذا كنت في وضع السكون أو كنت بعيدًا عن حاسوبك من خلال اكتشاف عدم وجود حركة للفأرة خلال فترة زمنية معينة مثل 10 دقائق. قد تكون بعيدًا عن حاسوبك ولكنك لا تريد أن يرى الآخرون حالة المراسلة الفورية الخاصة بك وهي في وضع السكون، لذا اكتب برنامجًا لتحريك مؤشر الفأرة قليلًا كل 10 ثوانٍ، حيث يجب أن تكون الحركة صغيرة وغير متكررة بدرجة كافية حتى لا تعترض طريقك إذا أردتَ استخدام حاسوبك أثناء تشغيل السكربت. استخدام الحافظة Clipboard لقراءة حقل نصي يمكنك إرسال ضغطات المفاتيح إلى الحقول النصية في التطبيق باستخدام الدالة pyautogui.write()، ولكن لا يمكنك استخدام وحدة PyAutoGUI وحدها لقراءة النص الموجود فعليًا ضمن الحقل النصي، لذا يمكن أن تساعد الوحدة Pyperclip في ذلك. استخدم الوحدة PyAutoGUI للحصول على نافذة محرّر نصوص مثل المحرّر Mu أو المفكرة Notepad، وإحضارها إلى مقدمة الشاشة من خلال النقر عليها، ثم النقر داخل الحقل النصي، وإرسال مفتاح التشغيل السريع CTRL-A أو -A لتحديد الكل وإرسال مفتاح التشغيل السريع Ctrl-C أو -C للنسخ إلى الحافظة، ويمكن لسكربت بايثون بعد ذلك قراءة نص الحافظة من خلال تشغيل التعليمتين import pyperclip و pyperclip.paste(). اكتب برنامجًا يتبع هذا الإجراء لنسخ النص من الحقول النصية في النافذة، لذا استخدم الدالة pyautogui.getWindowsWithTitle('Notepad') (أو أي محرّر نصوص تختاره) للحصول على كائن Window. يمكن لسمات top و left لكائن Window أن تخبرك بمكان هذه النافذة، وسيضمن التابع activate() وجودها في مقدمة الشاشة. يمكنك بعد ذلك النقر على الحقل النصي الرئيسي لمحرّر النصوص من خلال إضافة 100 أو 200 بكسل مثلًا إلى قيم السمات top و left باستخدام التابع pyautogui.click() لنقل تركيز لوحة المفاتيح إلى هذا الحقل، ثم استدعِ الدالتين pyautogui.hotkey('ctrl', 'a') و pyautogui.hotkey('ctrl', 'c') لتحديد النص بأكمله ونسخه إلى الحافظة. أخيرًا، استدعِ الدالة pyperclip.paste() لاسترداد النص من الحافظة ولصقه في برنامج بايثون. يمكنك بعد ذلك استخدام هذه السلسلة النصية كما تريد، ولكن مرّرها إلى الدالة print() حاليًا. لاحظ أن دوال النافذة الخاصة بوحدة PyAutoGUI تعمل فقط على نظام ويندوز بدءًا من الإصدار 1.0.0 من وحدة PyAutoGUI، ولن تعمل على نظام ماك macOS أو لينكس. بوت المراسلة الفورية تستخدم برامج المراسلة الفورية بروتوكولاتٍ خاصة تصعّب كتابة وحدات بايثون التي يمكنها التفاعل مع هذه البرامج، ولكن لا يمكن لهذه البروتوكولات الخاصة منعك من كتابة أداة أتمتة لواجهة المستخدم الرسومية. يحتوي تطبيق واتس أب Whatsapp على شريط بحث يتيح لك إدخال اسم مستخدم في قائمة أصدقائك وفتح نافذة مراسلة عند الضغط على مفتاح ENTER، حيث ينتقل تركيز لوحة المفاتيح إلى النافذة الجديدة آليًا، وتمتلك تطبيقات المراسلة الفورية الأخرى طرقًا مشابهة لفتح نوافذ الرسائل الجديدة. اكتب برنامجًا يرسل رسالة إشعار آليًا إلى مجموعة مختارة من الأشخاص في قائمة أصدقائك، وقد يضطر برنامجك إلى التعامل مع حالات استثنائية مثل ظهور نافذة الدردشة في إحداثيات مختلفة على الشاشة، أو ظهور مربعات التأكيد التي تقاطع رسائلك. يجب على برنامجك التقاط لقطات شاشة لتوجيه تفاعل واجهة المستخدم الرسومية واعتماد طرقٍ لاكتشاف متى لا تُرسَل ضغطات المفاتيح الافتراضية. ملاحظة: قد ترغب في إعداد بعض الحسابات التجريبية الوهمية حتى لا ترسل رسائل غير مرغوب فيها إلى أصدقائك الحقيقيين عن طريق الخطأ أثناء كتابة هذا البرنامج. الخلاصة تتيح لك أتمتة واجهة المستخدم الرسومية باستخدام وحدة pyautogui التفاعل مع التطبيقات الموجودة على حاسوبك من خلال التحكم في الفأرة ولوحة المفاتيح، حيث يُعَد هذا النهج مرنًا بما يكفي لفعل أيّ شيء يمكن للمستخدم تطبيقه، ولكن يتمثّل جانبه السلبي في أن هذه البرامج لا تستطيع رؤية ما تنقر عليه أو تكتبه. حاول التأكد من أن برامج أتمتة واجهة المستخدم الرسومية عند كتابتها ستتعطّل بسرعة عند إعطائها تعليمات سيئة، إذ قد يكون تعطل البرنامج أمرًا مزعجًا، ولكنه أفضل بكثير من استمرار البرنامج مع وجود الخطأ. يمكنك تحريك مؤشر الفأرة على الشاشة ومحاكاة نقرات الفأرة وضغطات المفاتيح واختصارات لوحة المفاتيح باستخدام وحدة PyAutoGUI التي يمكنها أيضًا التحقق من الألوان على الشاشة، ويمكنها أن تزوّد برنامج أتمتة واجهة المستخدم الرسومية الخاص بك بفكرة كافية عن محتويات الشاشة لمعرفة خروجه عن المسار الصحيح أم لا، ويمكنك إعطاء الوحدة PyAutoGUI لقطة شاشة والسماح لها بمعرفة إحداثيات المنطقة التي تريد النقر عليها. يمكنك الجمع بين جميع ميزات PyAutoGUI لأتمتة أيّ مهمة متكررة على حاسوبك. قد تكون مشاهدة مؤشر الفأرة يتحرك من تلقاء نفسه ورؤية النص يظهر على الشاشة آليًا أمرًا مملًا للغاية، ولكن يوجد شعور معين بالرضا يأتي من رؤية كيف أنقذك ذكاؤك من إنجاز المهام المملة. ترجمة -وبتصرُّف- للمقال Controlling the Keyboard and Mouse with GUI Automation لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: معالجة الصور باستخدام لغة بايثون Python الأدوات المستخدمة في بناء الواجهات الرسومية في بايثون جدولة المهام وتشغيل برامج أخرى باستخدام لغة بايثون واجهات المستخدم الرسومية في بايثون باستخدام TKinter
-
ستصادف ملفات الصور الرقمية طوال الوقت إذا كان لديك كاميرا رقمية أو حتى إذا رفعتَ صورًا من هاتفك على حسابك على فيسبوك أو انستغرام مثلًا، وقد تعرف كيفية استخدام برامج الرسوميات الأساسية مثل الرسام Microsoft Paint أو Paintbrush، أو حتى التطبيقات الأكثر تقدمًا مثل أدوبي فوتوشوب Adobe Photoshop، ولكن إذا كنت بحاجة إلى تعديل عددٍ كبير من الصور، فيمكن أن يكون إنجاز ذلك يدويًا مهمة طويلة ومملة. تُعَد Pillow وحدةَ بايثون خارجية للتفاعل مع ملفات الصور، وتحتوي هذه الوحدة على العديد من الدوال التي تسهّل قص محتوى الصورة وتغيير حجمه وتعديله. يمكن لبايثون Python تعديل مئات أو آلاف الصور آليًا بسهولة بفضل القدرة على معالجة الصور باستخدام الطريقة نفسها التي تستخدمها برامجٌ مثل برنامج الرسام أو الفوتوشوب. يمكنك تثبيت وحدة Pillow من خلال تشغيل الأمر pip install --user -U pillow==9.2.0. أساسيات الصور الحاسوبية يجب أن تفهم أساسيات كيفية تعامل الحواسيب مع الألوان والإحداثيات في الصور وكيفية العمل مع الألوان والإحداثيات في الوحدة Pillow من أجل معالجة الصور، لذا ثبّت هذه الوحدة قبل المتابعة. الألوان وقيم RGBA تمثّل البرامج الحاسوبية لونًا في صورة بوصفه قيمة RGBA، والتي هي مجموعة من الأعداد التي تحدّد مقدار اللون الأحمر والأخضر والأزرق وقيمة ألفا Alpha (أو الشفافية Transparency) في اللون. كل قيمة من قيم هذه المكونات هي عددٌ صحيح قيمته من 0 (عدم وجود لون على الإطلاق) إلى 255 (الحد الأقصى). تُسنَد قيم RGBA إلى البكسلات، والبكسل Pixel هو أصغر نقطة من لون واحد يمكن أن تظهرها شاشة الحاسوب، فهناك ملايين البكسلات على الشاشة، ويعبّر إعداد RGB الخاص بالبكسل عن درجة اللون التي يجب أن يعرضها بدقة. تحتوي الصور أيضًا على قيمة ألفا لإنشاء قيم RGBA، حيث إذا عُرِضت صورة على الشاشة فوق صورة خلفية أو خلفية سطح مكتب، فإن قيمة ألفا تحدِّد مقدار الخلفية التي يمكنك رؤيتها عبر بكسل الصورة. تُمثَّل قيم RGBA في الوحدة Pillow باستخدام مجموعة Tuple مكونة من أربع قيم صحيحة، فمثلًا يُمثَّل اللون الأحمر باستخدام المجموعة (255, 0, 0, 255)، حيث يحتوي هذا اللون على الحد الأقصى من اللون الأحمر، ولا يحتوي على اللون الأخضر أو الأزرق، ويحتوي على الحد الأقصى من قيمة ألفا، مما يعني أنه مُعتَم Opaque تمامًا. يُمثَّل اللون الأخضر باستخدام المجموعة (0, 255, 0, 255)، ويُمثَّل اللون الأزرق باستخدام المجموعة (0, 0, 255, 255)، ويُمثَّل اللون الأبيض الذي هو مزيج من كل الألوان باستخدام المجموعة (255, 255, 255, 255)، واللون الأسود الذي لا لون له هو (0, 0, 0, 255). إذا كانت قيمة الشفافية للون هي 0، فسيكون اللون غير مرئي، وتصبح قيم RGB غير مهمة، وبالتالي سيبدو اللون الأحمر غير المرئي مثل اللون الأسود غير المرئي. تستخدم الوحدة Pillow أسماء الألوان المعيارية التي تستخدمها لغة HTML، حيث يوضح الجدول التالي مجموعة مختارة من أسماء الألوان المعيارية وقيم RGBA الخاصة بها: اسم اللون قيمة RGBA الخاصة به White (الأبيض) (255, 255, 255, 255) Green (الأخضر) (0, 128, 0, 255) Gray (الرمادي) (128, 128, 128, 255) Black (الأسود) (0, 0, 0, 255) Red (الأحمر) (255, 0, 0, 255) Blue (الأزرق) (0, 0, 255, 255) Yellow (الأصفر) (255, 255, 0, 255) Purple (البنفسجي) (128, 0, 128, 255) توفّر الوحدة Pillow الدالة ImageColor.getcolor()، وبالتالي لن تكون مضطرًا إلى حفظ قيم RGBA للألوان التي تريد استخدامها، وتأخذ هذه الدالة سلسلة نصية تمثّل اسم اللون كوسيطٍ أول لها والسلسلة النصية 'RGBA' كوسيطٍ ثانٍ لها، وتعيد مجموعة RGBA. أدخِل ما يلي في الصدفة التفاعلية Interactive Shell لمعرفة كيفية عمل هذه الدالة: ➊ >>> from PIL import ImageColor ➋ >>> ImageColor.getcolor('red', 'RGBA') (255, 0, 0, 255) ➌ >>> ImageColor.getcolor('RED', 'RGBA') (255, 0, 0, 255) >>> ImageColor.getcolor('Black', 'RGBA') (0, 0, 0, 255) >>> ImageColor.getcolor('chocolate', 'RGBA') (210, 105, 30, 255) >>> ImageColor.getcolor('CornflowerBlue', 'RGBA') (100, 149, 237, 255) يجب أولًا استيراد الوحدة ImageColor من PIL ➊ (وليس من Pillow). السلسلة النصية لاسم اللون التي تمررها إلى الدالة ImageColor.getcolor() غير حساسة لحالة الأحرف، لذا يعطي تمرير السلسلة النصية 'red' ➋ وتمرير السلسلة النصية 'RED' ➌ مجموعة RGBA نفسها، ويمكنك أيضًا تمرير أسماء ألوان غير اعتيادية مثل 'chocolate' و 'Cornflower Blue'. تدعم الوحدة Pillow عددًا كبيرًا من أسماء الألوان من 'aliceblue' إلى 'whitesmoke'، حيث يمكنك العثور على القائمة الكاملة لأكثر من 100 اسم لون معياري في الموارد الموجودة على nostarch. الإحداثيات والمجموعات المربعة تُعنوَن بكسلات الصورة باستخدام إحداثيات x و y، والتي تُحدِّد موقع البكسل الأفقي والعمودي على التوالي في الصورة، وتكون نقطة الأصل Origin (أو مبدأ الإحداثيات) هي البكسل الموجود في الزاوية العلوية اليسرى من الصورة ونحدّدها بالصيغة (0, 0)، حيث يمثل الصفر الأول الإحداثي x الذي يبدأ من الصفر عند نقطة الأصل وتزداد قيمته من اليسار إلى اليمين، ويمثل الصفر الثاني الإحداثي y الذي يبدأ من الصفر عند نقطة الأصل وتزداد قيمته نزولًا إلى أسفل الصورة. تزداد إحداثيات y باتجاه الأسفل، إذ يُعَد ذلك عكس الطريقة التي استخدمناها سابقًا لإحداثيات y في حصص الرياضيات في المدرسة. يوضح الشكل التالي كيفية عمل هذا النظام من الإحداثيات: إحداثيات x و y لصورة أبعادها 28×27 لأحد أنواع أجهزة تخزين البيانات القديمة تأخذ العديد من دوال وتوابع الوحدة Pillow وسيطًا نوعه مجموعة مربعة Box Tuple، وهذا يعني أن الوحدة Pillow تتوقع مجموعةً مؤلفة من أربعة إحداثيات صحيحة تمثل منطقةً مستطيلة في الصورة، والأعداد الصحيحة الأربعة هي بالترتيب كما يلي: Left: الإحداثي x للحافة اليسرى من المربع. Top: الإحداثي y للحافة العلوية من المربع. Right: الإحداثي x لبكسل واحد على يمين الحافة اليمنى القصوى للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح الأيسر Left. Bottom: الإحداثي y لبكسل واحد تحت الحافة السفلية للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح العلوي Top. لاحظ أن المربع يتضمن الإحداثيات اليسرى والعليا حتى الوصول إلى الإحداثيات اليمنى والسفلى ولكنه لا يتضمنها، فمثلًا تمثل المجموعة المربعة (3, 1, 9, 6) جميع البكسلات الموجودة في المربع الأسود في الشكل التالي: المنطقة التي تمثّلها المجموعة المربعة (3, 1, 9, 6) معالجة الصور باستخدام الوحدة Pillow عرفنا كيفية عمل الألوان والإحداثيات في الوحدة Pillow، وسنستخدمها الآن لمعالجة الصور. سنستخدم الصورة التالية لجميع أمثلة الصدفة التفاعلية في هذا المقال: القطة زوفي Zophie ضع ملف الصورة zophie.png في مجلد العمل الحالي، ثم ستكون جاهزًا لتحميل صورة هذه القطة إلى شيفرة بايثون كما يلي: >>> from PIL import Image >>> catIm = Image.open('zophie.png') يمكنك تحميل الصورة من خلال استيراد الوحدة Image من الوحدة Pillow واستدعاء الدالة Image.open()، ثم تمرّر اسم ملف الصورة إلى هذه الدالة، ويمكنك بعد ذلك تخزين الصورة المُحمَّلة في المتغير CatIm. اسم الوحدة Pillow هو PIL لجعلها متوافقة مع الإصدارات السابقة من وحدةٍ أقدم اسمها Python Imaging Library، ولذلك يجب تشغيل التعليمة from PIL import Image بدلًا من التعليمة from Pillow import Image، ويجب استخدام تعليمة الاستيراد from PIL import Image بدلًا من التعليمة import PIL وفقًا للطريقة التي أعدّ بها منشئو Pillow هذه الوحدة. إن لم يكن ملف الصورة موجودًا في مجلد العمل الحالي، فغيّر مجلد العمل إلى المجلد الذي يحتوي على ملف الصورة من خلال استدعاء الدالة os.chdir(): >>> import os >>> os.chdir('C:\\folder_with_image_file') تعيد الدالة Image.open() قيمة من نوع البيانات كائن Image، وهي الطريقة التي تمثّل بها الوحدة Pillow الصورة بوصفها قيمة بايثون. يمكنك تحميل كائن Image من ملف صورة (بأيّ صيغة) من خلال تمرير السلسلة النصية التي تمثل اسم الملف إلى الدالة Image.open()، ويمكن حفظ أيّ تغييرات تجريها على كائن Image في ملف صورة (بأيّ صيغة أيضًا) باستخدام التابع save(). تُجرَى جميع عمليات التدوير وتغيير الحجم والقص والرسم وغيرها من عمليات معالجة الصور من خلال استدعاءات التوابع الموافقة لهذه العمليات مع كائن Image. سنفترض أنك استوردتَ الوحدة Image الخاصة بالوحدة Pillow وأن لديك صورة القطة Zophie مُخزَّنة في المتغير catIm لاختصار الأمثلة في هذا المقال. تأكّد من وجود الملف zophie.png في مجلد العمل الحالي حتى تتمكّن الدالة Image.open() من العثور عليه، وإلّا فيجب تحديد المسار المطلق الكامل في وسيط السلسلة النصية للدالة Image.open(). العمل مع نوع البيانات Image يحتوي الكائن Image على العديد من السمات Attributes المفيدة التي توفر لك معلومات أساسية حول ملف الصورة الذي جرى تحميله منه مثل: عرضه وارتفاعه، واسم الملف، وصيغة الرسوميات (مثل JPEG أو GIF أو PNG). أدخل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image >>> catIm = Image.open('zophie.png') >>> catIm.size ➊ (816, 1088) ➋ >>> width, height = catIm.size ➌ >>> width 816 ➍ >>> height 1088 >>> catIm.filename 'zophie.png' >>> catIm.format 'PNG' >>> catIm.format_description 'Portable network graphics' ➎ >>> catIm.save('zophie.jpg') ننشئ كائن Image من ملف الصورة zophie.png ونخزّن هذا الكائن في المتغير catIm، ثم يمكننا أن نرى أن سمة الحجم size الخاصة بهذا الكائن تحتوي على مجموعةٍ Tuple تتألف من عرض الصورة وارتفاعها بالبكسل ➊. يمكننا إسناد القيم الموجودة في هذه المجموعة إلى المتغيرين width و height ➋ للوصول إلى العرض ➌ والارتفاع ➍ لوحدهما. تمثّل السمة filename اسم الملف الأصلي، وتمثّل السمات format و format_description -التي هي سلاسل نصية- صيغة الصورة للملف الأصلي، مع كون السمة format_description أكثر تفصيلًا. أخيرًا، يؤدي استدعاء التابع save() وتمرير 'zophie.jpg' إلى هذا التابع إلى حفظ صورة جديدة بالاسم zophie.jpg على قرص حاسوبك الصلب ➎. ترى الوحدة Pillow أن امتداد الملف هو .jpg وتحفظ الصورة تلقائيًا بصيغة الصورة JPEG. يُفترَض أن يكون لديك الآن صورتان هما zophie.png و zophie.jpg على قرص حاسوبك الصلب، حيث يعتمد هذان الملفان على الصورة نفسها، ولكنهما غير متطابقين بسبب اختلاف صيغتيهما. توفر الوحدة Pillow أيضًا الدالة Image.new() التي تعيد كائن Image، حيث تشبه هذه الدالة إلى حدٍ كبيرالدالة Image.open()، باستثناء أن الصورة التي يمثلها كائن الدالة Image.new() فارغة. وسطاء الدالة Image.new() هي كما يلي: السلسلة النصية 'RGBA' التي تضبط نمط الألوان على القيمة RGBA، إذ توجد أنماط أخرى لن نوضّحها في هذا المقال. حجم الصورة الذي نمثّله بمجموعة مكونة من عددين صحيحين لعرض الصورة الجديدة وارتفاعها. لون الخلفية الذي يجب أن تبدأ به الصورة، ونمثّله بمجموعة مكونة من أربعة أعداد صحيحة لقيمة RGBA، حيث يمكنك استخدام القيمة التي تعيدها الدالة ImageColor.getcolor() لهذا الوسيط، ولكن تدعم الدالة Image.new() بدلًا من ذلك تمرير سلسلة نصية تمثّل اسم اللون المعياري فقط. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image ➊ >>> im = Image.new('RGBA', (100, 200), 'purple') >>> im.save('purpleImage.png') ➋ >>> im2 = Image.new('RGBA', (20, 20)) >>> im2.save('transparentImage.png') ننشئ كائن Image لصورة عرضها 100 بكسل وطولها 200 بكسل مع خلفية بنفسجية ➊، ثم نحفظ هذه الصورة في الملف purpleImage.png. نستدعي بعد ذلك الدالة Image.new() مرةً أخرى لإنشاء كائن Image آخر مع تمرير المجموعة (20, 20) التي تمثّل الأبعاد دون تمرير شيء للون الخلفية ➋. يُعَد اللون الأسود غير المرئي (0, 0, 0, 0) هو اللون الافتراضي المُستخدَم عند عدم تحديد وسيط اللون، وبالتالي فإن الصورة الثانية لها خلفية شفافة، ثم نحفظ هذا المربع الشفاف الذي أبعاده 20×20 في الملف transparentImage.png. قص الصور يمثّل قص الصورة تحديد منطقة مستطيلة من الصورة وإزالة كل شيء خارج هذا المستطيل. يأخذ التابع crop() مع كائنات Image مجموعة مربعة ويعيد كائن Image يمثل الصورة التي قصّها. يترك التابع crop() كائن Image الأصلي دون تغيير بعد القص، ويعيد كائن Image جديد. تذكّر أن المجموعة المربعة (أي الجزء المقصوص في هذه الحالة) يتضمّن العمود الأيسر والصف العلوي من البكسلات وحتى الوصول إلى العمود الأيمن والصف السفلي من البكسلات دون تضمينها. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image >>> catIm = Image.open('zophie.png') >>> croppedIm = catIm.crop((335, 345, 565, 560)) >>> croppedIm.save('cropped.png') ننشئ كائن Image جديد للصورة المقصوصة، ونخزّن هذا الكائن في المتغير croppedIm، ثم نستدعي التابع save() مع croppedIm لحفظ الصورة المقصوصة في الملف cropped.png. سينشَأ الملف الجديد cropped.png من الصورة الأصلية كما في الشكل التالي: تكون الصورة الجديدة هي الجزء المقصوص من الصورة الأصلية نسخ ولصق الصور في صور أخرى يعيد التابع copy() كائن Image جديد يحتوي على الصورة نفسها للكائن Image الذي استدعيناه معه، ويُعَد ذلك مفيدًا إذا كنت بحاجة إلى إجراء تغييرات على الصورة ولكنك تريد الاحتفاظ بنسخة دون تغييرات من النسخة الأصلية، فمثلًا أدخِل ما يلي في الصدفة التفاعلية: >>> from PIL import Image >>> catIm = Image.open('zophie.png') >>> catCopyIm = catIm.copy() يحتوي المتغيران catIm و catCopyIm على كائني Image منفصلين، وكلاهما لهما الصورة نفسهما. أصبح لديك الآن كائن Image مُخزَّن في المتغير catCopyIm، ويمكنك الآن تعديل المتغير catCopyIm كما تريد وحفظه باسم ملف جديد، مع ترك الملف zophie.png دون تغيير. لنحاول مثلًا تعديل المتغير catCopyIm باستخدام التابع paste()، حيث يُستدعَى هذا التابع مع كائن Image ويلصق صورة أخرى فوقه. إذًا لنتابع مثال الصدفة من خلال لصق صورة أصغر على catCopyIm كما يلي: >>> faceIm = catIm.crop((335, 345, 565, 560)) >>> faceIm.size (230, 215) >>> catCopyIm.paste(faceIm, (0, 0)) >>> catCopyIm.paste(faceIm, (400, 500)) >>> catCopyIm.save('pasted.png') نمرّر أولًا مجموعة مربعة للمنطقة المستطيلة في الصورة zophie.png التي تحتوي على وجه القطة إلى التابع crop()، مما يؤدي إلى إنشاء كائن Image يمثل جزءًا مقصوصًا أبعاده 230×215، والذي نخزّنه في المتغير faceIm. يمكننا الآن لصق faceIm فوق catCopyIm، حيث يأخذ التابع paste() وسيطين هما: كائن Image المصدَر "Source" ومجموعة من إحداثيات x و y لمكان لصق الزاوية العلوية اليسرى من كائن Image المصدر على كائن Image الرئيسي. استدعينا في مثالنا التابع paste() مرتين مع catCopyIm، ومرّرنا المجموعة (0, 0) في المرة الأولى والمجموعة (400, 500) في المرة الثانية، مما يؤدي إلى لصق faceIm على catCopyIm مرتين، حيث نلصق الزاوية العلوية اليسرى من faceIm عند الإحداثيات (0, 0) على catCopyIm في المرة الأولى، ونلصق الزاوية العلوية اليسرى من faceIm عند الإحداثيات (400, 500). أخيرًا، نحفظ المتغير catCopyIm المُعدَّل في الملف pasted.png، وستبدو الصورة كما يلي: القطة زوفي بعد لصق وجهها مرتين ملاحظة: لا يستخدم التابعان copy() و paste() في الوحدة Pillow حافظة Clipboard حاسوبك بالرغم من أن اسميهما يدلان على ذلك. لاحظ أن التابع paste() يعدّل كائن Image في المكان نفسه، أي أنه لا يعيد كائن Image جديد مع الصورة المُلصَقة، ولكن إذا أردتَ استدعاء هذا التابع مع الاحتفاظ أيضًا بالنسخة غير المُعدَّلة من الصورة الأصلية، فيجب نسخ الصورة أولًا ثم استدعاء التابع paste() مع تلك النسخة. لنفترض أنك تريد وضع رأس القطة على الصورة بأكملها كما في الشكل الآتي، حيث يمكنك تحقيق هذا التأثير باستخدام بضع حلقات for فقط، لذا تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي: >>> catImWidth, catImHeight = catIm.size >>> faceImWidth, faceImHeight = faceIm.size ➊ >>> catCopyTwo = catIm.copy() ➋ >>> for left in range(0, catImWidth, faceImWidth): ➌ for top in range(0, catImHeight, faceImHeight): print(left, top) catCopyTwo.paste(faceIm, (left, top)) 0 0 0 215 0 430 0 645 0 860 0 1075 230 0 230 215 --snip-- 690 860 690 1075 >>> catCopyTwo.save('tiled.png') حلقات for المتداخلة المستخدمة مع التابع paste() لتكرار وجه القطة نخزّن عرض وارتفاع catIm في المتغيرين catImWidth و catImHeight، ثم ننشئ نسخة من catIm ونخزّنها في المتغير catCopyTwo ➊. أصبح لدينا الآن نسخة يمكننا لصقها، وبالتالي نبدأ بتكرار لصق faceIm على catCopyTwo، حيث يبدأ المتغير left الخاص بحلقة for الخارجية من القيمة 0 ويزداد بمقدار faceImWidth(230) ➋، ويبدأ المتغير top الخاص بحلقة for الداخلية من القيمة 0 ويزداد بمقدار faceImHeight(215) ➌. تعطي حلقات for المتداخلة هذه قيمًا للمتغيرين left و top للصق شبكةٍ من صور faceIm فوق كائن Image الذي هو catCopyTwo كما هو موضّح في الشكل السابق. نطبع قيم المتغيرين left و top لرؤية كيفية عمل هذه الحلقات المتداخلة، ثم نحفظ الصورة catCopyTwo المُعدَّلة في الملف tiled.png بعد اكتمال اللصق. تغيير حجم الصورة يُستدعَى التابع resize() مع الكائن Image ويعيد كائن Image جديد مع العرض والارتفاع المُحدَّدين، حيث يقبل هذا التابع وسيطًا هو مجموعة مكونة من عددين صحيحين يمثلان العرض والارتفاع الجديدين للصورة المُعادة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image >>> catIm = Image.open('zophie.png') ➊ >>> width, height = catIm.size ➋ >>> quartersizedIm = catIm.resize((int(width / 2), int(height / 2))) >>> quartersizedIm.save('quartersized.png') ➌ >>> svelteIm = catIm.resize((width, height + 300)) >>> svelteIm.save('svelte.png') نسند القيمتين الموجودتين في المجموعة catIm.size إلى المتغيرين width و height ➊، حيث يؤدي استخدام هذين المتغيرين بدلًا من catIm.size[0] و catIm.size[1] إلى جعل بقية الشيفرة البرمجية أكثر قابلية للقراءة. يمرّر استدعاء التابع resize() الأول القيمة int(width / 2) للعرض الجديد والقيمة int(height / 2) للارتفاع الجديد ➋، لذا سيكون لكائن Image الذي يعيده التابع resize() نصف طول ونصف عرض الصورة الأصلية أو ربع حجم الصورة الأصلية. يقبل التابع resize() الأعداد الصحيحة فقط في وسيط المجموعة الخاص به، ولذلك يجب تغليف عمليتي القسمة على 2 باستدعاء الدالة int(). يحافظ تغيير الحجم على النسب نفسها للعرض والارتفاع، ولكن لا حاجة إلى أن يكون العرض والارتفاع الجديدين المُمرَّرين إلى التابع resize() متناسبين مع الصورة الأصلية. يحتوي المتغير svelteIm على كائن Image له العرض الأصلي ولكن يكون ارتفاعه أكبر من الطول الأصلي بمقدار 300 بكسل ➌، مما يمنح القطة مظهرًا أرشق. لاحظ أن التابع resize() لا يعدّل كائن Image ذاته، بل يعيد كائن Image جديد. تدوير وقلب الصور يمكن تدوير الصور باستخدام التابع rotate() الذي يعيد كائن Image جديد للصورة المُدوَّرة ويترك كائن Image الأصلي دون تغيير. وسيط التابع rotate() هو عدد صحيح أو عدد عشري يمثّل عدد الدرجات لتدوير الصورة بعكس اتجاه عقارب الساعة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image >>> catIm = Image.open('zophie.png') >>> catIm.rotate(90).save('rotated90.png') >>> catIm.rotate(180).save('rotated180.png') >>> catIm.rotate(270).save('rotated270.png') لاحظ كيفية سَلسَلة استدعاءات التوابع من خلال استدعاء التابع save() مباشرةً مع كائن Image الذي يعيده التابع rotate(). يؤدي الاستدعاء الأول للتابعين rotate() و save() إلى إنشاء كائن Image جديد يمثّل الصورة المُدوَّرة بعكس اتجاه عقارب الساعة بمقدار 90 درجة وحفظ الصورة المُدوَّرة في الملف rotated90.png، ويفعل الاستدعاءان الثاني والثالث الشيء نفسه، ولكن بمقدار 180 درجة و270 درجة. ستبدو النتائج كما يلي: الصورة الأصلية (على اليسار) والصورة المُدوَّرة بعكس اتجاه عقارب الساعة بمقدار 90 و 180 و 270 درجة لاحظ أن عرض الصورة وارتفاعها يتغيران عند تدوير الصورة بمقدار 90 أو 270 درجة، ولكن إذا دوّرتَ الصورة بمقدار آخر، فستحافظ الصورة على الأبعاد الأصلية. نستخدم في نظام ويندوز Windows خلفية سوداء لملء أي فراغات ناتجة عن التدوير، كما هو موضح في الشكل الآتي، بينما نستخدم في نظام ماك macOS بكسلات شفافة لهذه الفراغات. يحتوي التابع rotate() على وسيط كلمات مفتاحية Keyword Argument اختياري هو expand، حيث يمكن ضبط هذا الوسيط على القيمة True لتكبير أبعاد الصورة لتناسب الصورة الجديدة المُدوَّرة بالكامل. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> catIm.rotate(6).save('rotated6.png') >>> catIm.rotate(6, expand=True).save('rotated6_expanded.png') يدوّر الاستدعاء الأول الصورة بمقدار 6 درجات ويحفظها في الملف rotate6.png كما هو موضَّح على يسار الشكل التالي، ويدوّر الاستدعاء الثاني الصورة بمقدار 6 درجات مع ضبط الوسيط expand على القيمة True ويحفظها في الملف rotate6_expanded.png كما هو موضَّح على يمين الشكل التالي: تدوير الصورة بمقدار 6 درجات تدويرًا عاديًا (على اليسار) والتدوير مع الوسيط expand=True (على اليمين) يمكنك أيضًا قلب الصورة Mirror Flip باستخدام التابع transpose()، حيث يجب تمرير إما المعامل Image.FLIP_LEFT_RIGHT أو المعامل Image.FLIP_TOP_BOTTOM إلى هذا التابع. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> catIm.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png') >>> catIm.transpose(Image.FLIP_TOP_BOTTOM).save('vertical_flip.png') ينشئ التابع transpose() -مثل التابع rotate()- كائن Image جديد. مرّرنا في مثالنا المعامل Image.FLIP_LEFT_RIGHT إلى هذا التابع لقلب الصورة أفقيًا ثم حفظنا النتيجة في الملف horizontal_flip.png، ويمكننا قلب الصورة عموديًا من خلال تمرير Image.FLIP_TOP_BOTTOM ونحفظ النتيجة في الملف vertical_flip.png. ستبدو النتائج كما هو موضّح في الشكل التالي: الصورة الأصلية (على اليسار)، وقلب الصورة أفقيًا (في الوسط)، وقلب الصورة عموديًا (على اليمين) تغيير البكسلات الفردية يمكن استرداد لون البكسل الفردي أو ضبطه باستخدام التابعين getpixel() و putpixel()، حيث يأخذ هذان التابعان مجموعةً تمثل إحداثيات x و y للبكسل، ويأخذ التابع putpixel() أيضًا وسيطًا إضافيًا هو مجموعة تمثّل لون البكسل، فهذا الوسيط هو مجموعة RGBA مؤلفة من أربعة أعداد صحيحة أو مجموعة RGB مؤلفة من ثلاثة أعداد صحيحة. أدخل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image ➊ >>> im = Image.new('RGBA', (100, 100)) ➋ >>> im.getpixel((0, 0)) (0, 0, 0, 0) ➌ >>> for x in range(100): for y in range(50): ➍ im.putpixel((x, y), (210, 210, 210)) >>> from PIL import ImageColor ➎ >>> for x in range(100): for y in range(50, 100): ➏ im.putpixel((x, y), ImageColor.getcolor('darkgray', 'RGBA')) >>> im.getpixel((0, 0)) (210, 210, 210, 255) >>> im.getpixel((0, 50)) (169, 169, 169, 255) >>> im.save('putPixel.png') ننشئ أولًا صورةً جديدة، والتي هي مربع شفاف أبعاده 100×100 ➊، ثم نستدعي التابع getpixel() مع بعض الإحداثيات في هذه الصورة، مما يؤدي إلى إعادة المجموعة (0, 0, 0, 0) لأن الصورة شفافة ➋. يمكن تلوين البكسلات في هذه الصورة من خلال استخدام حلقات for متداخلة للمرور على جميع البكسلات في النصف العلوي من الصورة ➌ وتلوين كلّ بكسل باستخدام التابع putpixel() ➍، حيث مرّرنا مجموعة RGB هي (210, 210, 210) باللون الرمادي الفاتح إلى التابع putpixel(). لنفترض أننا نريد تلوين النصف السفلي من الصورة باللون الرمادي الداكن، ولكننا لا نعرف مجموعة RGB للون الرمادي الداكن، حيث لا يقبل التابع putpixel() اسم لون معياري مثل 'darkgray'، لذلك يجب استخدام الدالة ImageColor.getcolor() للحصول على مجموعة اللون المقابلة للون 'darkgray'. لنمر الآن ضمن حلقة على البكسلات الموجودة في النصف السفلي من الصورة ➎، ونمرّر القيمة المُعادة من الدالة ImageColor.getcolor() إلى التابع putpixel() ➏، ويجب أن يكون لدينا الآن صورة رمادية فاتحة في النصف العلوي ورمادية داكنة في النصف السفلي كما هو موضّح في الشكل التالي. يمكنك استدعاء التابع getpixel() مع بعض الإحداثيات للتأكد من أن لون أيّ بكسل هو ما تتوقعه. أخيرًا، احفظ الصورة في الملف putPixel.png. الصورة putPixel.png لا يُعَد رسم بكسل واحد في كل مرة على الصورة أمرًا مريحًا للغاية، فإذا كنت بحاجة إلى رسم الأشكال، فاستخدم دوال الوحدة ImageDraw التي سنوضّحها لاحقًا. تطبيق عملي: إضافة شعار إلى صورة لنفترض أن لديك مهمة مملة تتمثل في تغيير حجم آلاف الصور وإضافة شعار صغير يمثل علامة مائية في زاوية كل من هذه الصور، إذ قد يستغرق ذلك الأمر وقتًا طويلًا باستخدام برنامج رسوميات أساسي مثل برنامج الرسام Paint أو Paintbrush. يمكن لتطبيق رسوميات أكثر تقدمًا مثل برنامج الفوتوشوب Photoshop إنجاز معالجةٍ لمجموعة من الصور، ولكنه يكلف مئات الدولارات. إذًا لنكتب سكربتًا ينجز هذه المهمة نيابةً عنك. لنفترض أن الشكل التالي هو الشعار الذي تريد إضافته إلى الزاوية اليمنى السفلية من كل صورة، وهذا الشعار هو رمزٌ لقطة سوداء ذات حدود بيضاء مع جعل بقية الصورة شفافة: الشعار المراد إضافته إلى الصورة إليك الخطوات العامة التي يجب أن يطبّقها برنامجك: تحميل صورة الشعار. المرور ضمن حلقة على جميع الملفات ذات الامتداد .png و .jpg الموجودة في مجلد العمل. التحقق مما إذا كانت الصورة أعرض أو أطول من 300 بكسل. إذا كان الأمر كذلك، فيجب تقليل العرض أو الارتفاع (الأكبر) إلى 300 بكسل وتقليص البعد الآخر بمقدارٍ متناسب معه. لصق صورة الشعار في زاوية الصورة. حفظ الصور المُعدَّلة في مجلد آخر. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: فتح الملف catlogo.png بوصفه كائن Image. المرور ضمن حلقة على السلاسل النصية التي يعيدها التابع os.listdir('.'). الحصول على عرض الصورة وارتفاعها من السمة size. حساب العرض والارتفاع الجديدين للصورة التي غيّرنا حجمها. استدعاء التابع resize() لتغيير حجم الصورة. استدعاء التابع paste() للصق الشعار. استدعاء التابع save() لحفظ التغييرات باستخدام اسم الملف الأصلي. الخطوة الأولى: فتح صورة الشعار افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد للمشروع، وأدخِل الشيفرة البرمجية التالية، واحفظها بالاسم resizeAndAddLogo.py: #! python3 # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا # أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى import os from PIL import Image ➊ SQUARE_FIT_SIZE = 300 ➋ LOGO_FILENAME = 'catlogo.png' ➌ logoIm = Image.open(LOGO_FILENAME) ➍ logoWidth, logoHeight = logoIm.size # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها # حساب العرض والارتفاع الجديدين لتغيير الحجم # تغيير حجم الصورة # إضافة الشعار # حفظ التغييرات سهّلنا تغيير البرنامج لاحقًا من خلال إعداد الثاثبتين SQUARE_FIT_SIZE ➊ و LOGO_FILENAME ➋ في بداية البرنامج، فلنفترض أن الشعار الذي تضيفه ليس رمزًا لقطة، أو لنفترض أنك تقلّل البعد الأكبر للصور الناتجة إلى قيمة مغايرة عن 300 بكسل، إذ يمكنك فتح الشيفرة البرمجية وتغيير تلك القيم مرة واحدة فقط باستخدام هذه الثوابت في بداية البرنامج، أو يمكنك إجراء ذلك بحيث تأخذ قيم هذه الثوابت من وسطاء سطر الأوامر. إن لم تستخدم هذه الثوابت، فيجب عليك البحث في الشيفرة البرمجية عن جميع نسخ القيم 300 و 'catlogo.png' ووضع قيمٍ أخرى لمشروعك الجديد مكانها، وبالتالي يجعل استخدام الثوابت برنامجك أعم. تعيد الدالة Image.open() كائن Image للشعار ➌. نسند القيم الواردة من السمة logoIm.size إلى المتغيرين logoWidth و logoHeight لسهولة القراءة ➍. ملاحظة: تُعَد بقية البرنامج شيفرة هيكلية للتعليقات الموجودة في نهاية الشيفرة البرمجية السابقة حاليًا. الخطوة الثانية: المرور ضمن حلقة على جميع الملفات وفتح الصور يجب الآن العثور على جميع الملفات ذات الامتداد .png و .jpg في مجلد العمل الحالي، ولكننا لا نريد إضافة صورة الشعار إلى صورة الشعار نفسها، لذلك يجب على البرنامج تخطي أيّ صورة لاسم ملف مماثل لقيمة الثابت LOGO_FILENAME. إذًا أضِف ما يلي إلى شيفرتك البرمجية: #! python3 # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا # أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى import os from PIL import Image --snip-- os.makedirs('withLogo', exist_ok=True) # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل ➊ for filename in os.listdir('.'): ➋ if not (filename.endswith('.png') or filename.endswith('.jpg')) \ or filename == LOGO_FILENAME: ➌ continue # تخطي الملفات التي ليست صورًا وملف الشعار نفسه ➍ im = Image.open(filename) width, height = im.size --snip– أولًا، ينشئ استدعاء التابع os.makedirs() مجلدًا بالاسم withLogo لتخزين الصور النهائية مع الشعار بدلًا من الكتابة فوق ملفات الصور الأصلية، ويمنع وسيط الكلمات المفتاحية exist_ok=True التابع os.makedirs() من رفع استثناء إذا كان المجلد withLogo موجودًا مسبقًا. نمر ضمن حلقة على جميع الملفات الموجودة في مجلد العمل باستخدام التابع os.listdir('.') ➊، وتتحقق تعليمة if الطويلة ➋ عبر هذه الحلقة مما إذا كانت جميع أسماء الملفات لا تنتهي بالامتداد .png أو .jpg، فإذا كان الأمر كذلك أو كان الملف صورة الشعار نفسه، فيجب أن تتخطاه الحلقة وتستخدم التعليمة continue ➌ للانتقال إلى الملف التالي. إذا كان اسم الملف filename ينتهي بالامتداد .png أو .jpg (وليس ملف الشعار)، فيمكنك فتحه بوصفه كائن Image ➍ وضبط العرض width والارتفاع height الخاصين به. الخطوة الثالثة: تغيير حجم الصور يجب أن يغيّر البرنامج حجم الصورة فقط إذا كان العرض أو الارتفاع أكبر من قيمة الثابت SQUARE_FIT_SIZE (أي 300 بكسل في مثالنا) فقط، لذا ضع الشيفرة البرمجية الخاصة بتغيير الحجم ضمن تعليمة if التي تتحقق من متغيرات العرض width والارتفاع height. إذًا أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا # أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى import os from PIL import Image --snip-- # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE: # حساب العرض والارتفاع الجديدين لتغيير الحجم if width > height: ➊ height = int((SQUARE_FIT_SIZE / width) * height) width = SQUARE_FIT_SIZE else: ➋ width = int((SQUARE_FIT_SIZE / height) * width) height = SQUARE_FIT_SIZE # تغيير حجم الصورة print('Resizing %s...' % (filename)) ➌ im = im.resize((width, height)) --snip– إذا كانت الصورة بحاجة إلى تغيير حجمها، فيجب معرفة ما إذا كان عرض الصورة أو ارتفاعها أكبر من قيمة الثابت SQUARE_FIT_SIZE (أي 300 بكسل في مثالنا)، وإذا كان العرض width أكبر من الارتفاع height، فيجب تقليل الارتفاع بمقدار النسبة نفسها لتقليل العرض ➊، وهذه النسبة هي قيمة الثابت SQUARE_FIT_SIZE مقسومة على العرض الحالي، وتساوي قيمة الارتفاع height الجديدة النسبة مضروبة بقيمة الارتفاع height الحالية. يعيد معامل القسمة قيمة عشرية، ولكن يتطلب التابع resize() أن تكون الأبعاد أعدادًا صحيحة، لذا تذكّر تحويل النتيجة إلى عدد صحيح باستخدام الدالة int(). أخيرًا، ستُضبَط قيمة العرض width الجديدة على قيمة الثابت SQUARE_FIT_SIZE. إذا كان الارتفاع height أكبر أو يساوي العرض width (عالجنا كلتا الحالتين في تعليمة else)، فستُجرَى العملية الحسابية نفسها باستثناء التبديل بين المتغيرين height و width ➋. سيحتوي المتغيران width و height على أبعاد الصورة الجديدة، لذا مرّرهما إلى التابع resize() وخزّن كائن Image المُعاد في المتغير im ➌. الخطوة الرابعة: إضافة الشعار وحفظ التغييرات يجب لصق الشعار في الزاوية السفلية اليمنى سواء تغيّر حجم الصورة أم لا، ويعتمد المكان الذي يجب لصق الشعار فيه على حجم الصورة وحجم الشعار، حيث يوضح الشكل التالي كيفية حساب موضع اللصق. سيكون الإحداثي الأيسر لمكان لصق الشعار هو عرض الصورة مطروحًا منه عرض الشعار، وسيكون الإحداثي العلوي لمكان لصق الشعار هو ارتفاع الصورة مطروحًا منه ارتفاع الشعار. يجب أن تكون الإحداثيات اليسرى والعلوية لوضع الشعار في الزاوية السفلية اليمنى هي عرض/ارتفاع الصورة مطروحًا منه عرض/ارتفاع الشعار يجب أن تحفظ شيفرتك البرمجية كائن Image المُعدَّل بعد لصق الشعار في الصورة. إذًا أضِف ما يلي إلى برنامجك: #! python3 # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا # أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى import os from PIL import Image --snip-- # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها --snip-- # إضافة الشعار ➊ print('Adding logo to %s...' % (filename)) ➋ im.paste(logoIm, (width - logoWidth, height - logoHeight), logoIm) # حفظ التغييرات ➌ im.save(os.path.join('withLogo', filename)) تطبع الشيفرة البرمجية الجديدة رسالة تخبر المستخدم بإضافة الشعار ➊، وتلصق logoIm على im عند الإحداثيات المحسوبة ➋، وتحفظ التغييرات في اسم ملف ضمن المجلد withLogo ➌. سيبدو الخرج كما يلي عند تشغيل هذا البرنامج مع الملف zophie.png بوصفه الصورة الوحيدة في مجلد العمل: Resizing zophie.png... Adding logo to zophie.png… سنغيّر الصورة zophie.png إلى صورة بحجم 225×300 بكسل تشبه الشكل التالي، وتذكّر أن التابع paste() لن يلصق البكسلات الشفافة إن لم تمرّر logoIm إلى الوسيط الثالث للتابع paste(). يمكن لهذا البرنامج تغيير حجم مئات الصور وإضافة شعار إليها تلقائيًا في بضع دقائق فقط. غيّرنا حجم الصورة zophie.png وأضفنا الشعار (على اليسار). إذا نسيت الوسيط الثالث، فستُنسَخ البكسلات الشفافة في الشعار بوصفها بكسلات بيضاء (على اليمين) أفكار لبرامج مماثلة يمكن أن تكون القدرة على دمج الصور أو تعديل أحجامها دفعة واحدة مفيدة في العديد من التطبيقات، حيث يمكنك كتابة برامج مماثلة تنجز المهام التالية: إضافة نص أو عنوان URL لموقع ويب إلى الصور. إضافة علامات زمنية Timestamps إلى الصور. نسخ الصور أو نقلها إلى مجلدات مختلفة بناءً على أحجامها. إضافة علامة مائية شفافة تقريبًا إلى الصورة لمنع الآخرين من نسخها. الرسم على الصور إذا أردتَ رسم خطوط أو مستطيلات أو دوائر أو أشكال بسيطة أخرى على صورة ما، فاستخدم الوحدة ImageDraw الخاصة بوحدة Pillow. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image, ImageDraw >>> im = Image.new('RGBA', (200, 200), 'white') >>> draw = ImageDraw.Draw(im) نستورد أولًا الوحدتين Image و ImageDraw، ثم ننشئ صورة جديدة، والتي هي صورة بيضاء أبعادها 200×200 ، ونخزّن كائن Image في المتغير im. نمرّر كائن Image إلى الدالة ImageDraw.Draw() للحصول على كائن ImageDraw، حيث يحتوي هذا الكائن على عدة توابع لرسم الأشكال والنصوص على كائن Image. نخزّن بعد ذلك كائن ImageDraw في المتغير draw حتى نتمكّن من استخدامه بسهولة في المثال التالي. رسم الأشكال ترسم توابع الكائن ImageDraw التالية أنواعًا مختلفة من الأشكال على الصورة، وتُعَد معاملات fill و outline لهذه التوابع اختيارية وستُضبَط افتراضيًا على اللون الأبيض إذا تُرِكت دون تحديد. رسم النقاط يرسم التابع point(xy, fill) بكسلات فردية، حيث يمثل الوسيط xy قائمةً من النقاط التي تريد رسمها، إذ يمكن أن تكون هذه القائمة قائمةً بمجموعات Tuples إحداثيات x و y مثل [(x, y), (x, y), ...]، أو قائمة بإحداثيات x و y بدون مجموعات مثل [x1, y1, x2, y2, ...]. يمثّل الوسيط fill لون النقاط وهو إما مجموعة RGBA أو سلسلة نصية لاسم اللون مثل 'red'، ويُعَد هذا الوسيط اختياريًا. رسم الخطوط يرسم التابع line(xy, fill, width) خطًا أو سلسلةً من الخطوط، حيث يمثّل الوسيط xy إما قائمة من المجموعات مثل [(x, y), (x, y), ...]، أو قائمةً من الأعداد الصحيحة مثل [x1, y1, x2, y2, ...]، وتُعَد كلّ نقطة واحدةً من النقاط المتصلة على الخطوط التي ترسمها. يمثّل الوسيط fill الاختياري لون الخطوط باستخدام اسم اللون أو مجموعة RGBA، ويمثّل الوسيط width الاختياري عرض الخطوط وتكون قيمته الافتراضية 1 إذا تُرِك دون تحديد. رسم المستطيلات يرسم التابع rectangle(xy, fill, outline) مستطيلًا، حيث يمثّل الوسيط xy مجموعة مربعة وفق الصيغة (left, top, right, bottom)، إذ تحدّد القيم left و top إحداثيات x و y للزاوية العلوية اليسرى للمستطيل، بينما تحدد القيم right و bottom الزاوية السفلية اليمنى. يمثّل الوسيط الاختياري fill اللونَ الذي سيملأ الجزء الداخلي من المستطيل، ويمثّل الوسيط الاختياري outline لون المخطط المحيط بالمستطيل. رسم الأشكال البيضاوية يرسم التابع ellipse(xy, fill, outline) شكلًا بيضاويًا، وإذا كان عرض وارتفاع هذا الشكل متطابقين، فسيرسم هذا التابع دائرة. يمثّل الوسيط xy المجموعة المربعة (left, top, right, bottom) التي تمثّل مربعًا يحتوي على الشكل البيضاوي بدقة، ويمثّل الوسيط الاختياري fill لون الجزء الداخلي من الشكل البيضاوي، ويمثل الوسيط الاختياري outline لون المخطط المحيط بالشكل البيضاوي. رسم المضلعات يرسم التابع polygon(xy, fill, outline) مضلعًا عشوائيًا، حيث يمثّل الوسيط xy قائمةً من المجموعات مثل [(x, y), (x, y), ...] أو أعدادًا صحيحة مثل [x1, y1, x2, y2, ...]، والتي تمثّل النقاط التي تربط أضلاع المضلع، ويُربَط الزوج الأخير من الإحداثيات بالزوج الأول تلقائيًا. يمثل الوسيط الاختياري fill لون الجزء الداخلي من المضلع، ويمثّل الوسيط الاختياري outline لون المخطط المحيط بالمضلع. تطبيق عملي لرسم الأشكال أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> from PIL import Image, ImageDraw >>> im = Image.new('RGBA', (200, 200), 'white') >>> draw = ImageDraw.Draw(im) ➊ >>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black') ➋ >>> draw.rectangle((20, 30, 60, 60), fill='blue') ➌ >>> draw.ellipse((120, 30, 160, 60), fill='red') ➍ >>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown') ➎ >>> for i in range(100, 200, 10): draw.line([(i, 0), (200, i - 100)], fill='green') >>> im.save('drawing.png') ننشئ كائن Image لصورة بيضاء أبعادها 200×200، ونمرّره إلى الدالة ImageDraw.Draw() للحصول على كائن ImageDraw الذي نخزّنه في المتغير draw، حيث يمكنك استدعاء توابع الرسم مع هذا المتغير. رسمنا مخططًا رفيعًا باللون الأسود عند حواف الصورة ➊، ومستطيلًا أزرق تكون زاويته العلوية اليسرى عند النقطة (20, 30) وزاويته السفلية اليمنى عند النقطة (60, 60) ➋، وشكلًا بيضاويًا باللون الأحمر نحدّده باستخدام مربعٍ من النقطة (120, 30) إلى النقطة (160, 60) ➌، ومضلعًا بنيًا بخمس نقاط ➍، ونمطًا Pattern من الخطوط الخضراء مرسومة باستخدام حلقة for ➎. سيبدو الملف drawing.png الناتج كما يلي: صورة drawing.png الناتجة توجد العديد من توابع رسم الأشكال الأخرى لكائنات ImageDraw، لذا اطّلع على توثيقها الكامل على موقع وحدة Pillow الرسمي. رسم النصوص يحتوي كائن ImageDraw أيضًا على التابع text() لرسم النصوص على الصور، حيث يأخذ هذا التابع أربعة وسطاء هي: xy و text و fill و font: الوسيط xy هو مجموعة مكونة من عددين صحيحين تحدّد الزاوية العلوية اليسرى من مربع النص. الوسيط text هو سلسلة النص الذي تريد كتابته. الوسيط fill الاختياري هو لون النص. الوسيط font الاختياري هو كائن ImageFont، حيث يُستخدَم هذا الوسيط لضبط خط النص وحجمه (سنوضّح هذا الوسيط بمزيد من التفصيل في القسم التالي). من الصعب معرفة حجم كتلة النص التي لها خط معين مسبقًا، لذا توفّر الوحدة ImageDraw التابع textsize()، ووسيطه الأول هو سلسلة النص الذي تريد قياس حجمه، والوسيط الثاني هو كائن ImageFont اختياري. يعيد التابع textsize() مجموعة مكونة من عددين صحيحين تمثّل العرض والارتفاع الذي سيكون عليه النص الذي له خطٌ محدّد إذا كان مكتوبًا على الصورة، حيث يمكنك استخدام هذا العرض والارتفاع لمساعدتك في حساب المكان الذي تريد وضع النص فيه على صورتك. تُعَد الوسطاء الثلاثة الأولى للتابع text() واضحةً، ولكن لنلقِ نظرة على الوسيط الرابع الاختياري كائن ImageFont قبل أن نستخدم التابع text() لرسم نص على صورة. يأخذ كل من التابعين text() و textsize() كائن ImageFont اختياري بوصفه الوسيط الأخير لهما. لننشئ أحد هذه الكائنات من خلال تشغيل التعليمة التالية: >>> from PIL import ImageFont استوردنا الوحدة ImageFont الخاصة بوحدة Pillow، ويمكننا الآن استدعاء الدالة ImageFont.truetype() التي تأخذ وسيطين. الوسيط الأول هو سلسلة نصية لملف TrueType الخاص بالخط، وهو ملف الخط الفعلي الموجود على قرص حاسوبك الصلب، ويكون لملف TrueType الامتداد .ttf، حيث يمكن العثور عليه في المجلدات التالية: على نظام ويندوز: C:\Windows\Fonts. على نظام ماك: /Library/Fonts و /System/Library/Fonts. على نظام لينكس: /usr/share/fonts/truetype. لا تحتاج إلى إدخال هذه المسارات بوصفها جزءًا من السلسلة النصية لملف TrueType لأن لغة بايثون تعرف أنها ستبحث تلقائيًا عن الخطوط في هذه المجلدات، ولكنها ستعرض خطأً إن لم تتمكّن من العثور على الخط الذي حدّدته. الوسيط الثاني للدالة ImageFont.truetype() هو عدد صحيح يمثّل حجم الخط بالنقاط بدلًا من البكسلات. ضع في بالك أن الوحدة Pillow تنشئ صور PNG بكثافة 72 بكسلًا لكل بوصة افتراضيًا، والنقطة هي 1/72 من البوصة. أدخِل مثلًا ما يلي في الصدفة التفاعلية مع وضع اسم المجلد الفعلي الذي يستخدمه نظام تشغيلك مكان الثابت FONT_FOLDER: >>> from PIL import Image, ImageDraw, ImageFont >>> import os ➊ >>> im = Image.new('RGBA', (200, 200), 'white') ➋ >>> draw = ImageDraw.Draw(im) ➌ >>> draw.text((20, 150), 'Hello', fill='purple') >>> fontsFolder = 'FONT_FOLDER' # مثلًا ‘/Library/Fonts' ➍ >>> arialFont = ImageFont.truetype(os.path.join(fontsFolder, 'arial.ttf'), 32) ➎ >>> draw.text((100, 150), 'Howdy', fill='gray', font=arialFont) >>> im.save('text.png') نستورد الوحدات Image و ImageDraw و ImageFont و os، ثم ننشئ كائن Image لصورة بيضاء جديدة أبعادها 200×200 ➊، وننشئ كائن ImageDraw من الكائن Image ➋. نستخدم التابع text() لرسم النص "Hello" عند النقطة (20, 150) باللون البنفسجي ➌. لم نمرّر الوسيط الرابع الاختياري في استدعاء التابع text()، لذا لم نخصّص خط وحجم هذا النص. يمكن ضبط خط وحجم النص من خلال تخزين اسم المجلد (مثل /Library/Fonts) في المتغير fontsFolder، ثم نستدعي الدالة ImageFont.truetype()، ونمرر إليها ملف .ttf للخط الذي نريده متبوعًا بعدد صحيح يمثّل حجم الخط ➍. نخزّن الكائن Font الذي نحصل عليه من الدالة ImageFont.truetype() في المتغير arialFont مثلًا، ثم نمرّر هذا المتغير إلى التابع text() في وسيط الكلمات المفتاحية الأخير. يرسم استدعاء التابع text() ➎ النص "Howdy" عند النقطة (100, 150) باللون الرمادي بخط Arial وبحجم 32 نقطة. سيبدو الملف text.png الناتج كما في الشكل التالي: صورة text.png الناتجة مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. توسيع وإصلاح برامج تطبيقنا العملي يعمل برنامج resizeAndAddLogo.py الموجود في هذا المقال مع ملفات PNG و JPEG، ولكن تدعم الوحدة Pillow العديد من صيغ الصور الأخرى، إذًا لنوسّع هذا البرنامج لمعالجة صور GIF و BMP أيضًا. توجد مشكلة صغيرة هي أن البرنامج لا يعدّل ملفات PNG و JPEG إلّا إذا كانت امتدادات الملفات الخاصة بها بأحرف صغيرة، فمثلًا سيعالج هذا البرنامج الملف zophie.png ولن يعالج الملف zophie.PNG، لذا عدّل الشيفرة البرمجية بحيث يكون التحقق من امتداد الملف غير حساس لحالة الأحرف. أخيرًا، يُفترَض أن يكون الشعار المُضاف إلى الزاوية السفلية اليمنى مجرد علامة صغيرة، ولكن إذا كانت الصورة بحجم الشعار نفسه تقريبًا، فستبدو النتيجة كما في الشكل التالي، لذا عدّل البرنامج resizeAndAddLogo.py بحيث يجب أن يكون عرض وارتفاع الصورة على الأقل ضعف عرض وارتفاع صورة الشعار قبل لصقه، وإلّا فيجب تخطي إضافة الشعار. ستبدو النتائج غير جميلة عندما لا تكون الصورة أكبر بكثير من الشعار تحديد مجلدات الصور الموجودة على القرص الصلب قد تنقل الملفات من الكاميرا الرقمية إلى مجلدات مؤقتة في مكانٍ ما على القرص الصلب ثم تنسى هذه المجلدات، لذا من الجيد كتابة برنامج يمكنه فحص القرص الصلب بأكمله والعثور على مجلدات الصور التي نسيت مكانها. حاول كتابة برنامج يمر على كل مجلد موجودٍ على قرص حاسوبك الصلب ويعثر على مجلدات الصور المُحتملة، لذا يجب عليك أولًا تحديد ما يعنيه مجلد الصور، إذًا لنفترض أن مجلد الصور هو أيّ مجلد أكثر من نصف ملفاته صور، ولكن يجب أيضًا تحديد ما هي ملفات الصور، حيث يجب أن يكون لملف الصورة الامتداد .png أو .jpg. تُعَد الصور الرقمية صورًا كبيرة، إذ يجب أن يكون عرض وارتفاع ملف الصورة أكبر من 500 بكسل، فمعظم صور الكاميرا الرقمية يبلغ عرضها وارتفاعها عدة آلاف من البكسلات. إليك شيفرة هيكلية تقريبية لما قد يبدو عليه هذا البرنامج: #! python3 # استيراد الوحدات وكتابة التعليقات لوصف هذا البرنامج for foldername, subfolders, filenames in os.walk('C:\\'): numPhotoFiles = 0 numNonPhotoFiles = 0 for filename in filenames: # التحقق مما إذا كان امتداد الملف ليس .png أو .jpg if TODO: numNonPhotoFiles += 1 continue # الانتقال إلى اسم الملف التالي # فتح ملف الصورة باستخدام الوحدة Pillow # التحقق مما إذا كان العرض والارتفاع أكبر من 500 if TODO: # الصورة كبيرة بما يكفي لعدّها صورة numPhotoFiles += 1 else: # الصورة صغيرة جدًا بحيث لا يمكن عدّها صورة numNonPhotoFiles += 1 # إذا كان أكثر من نصف الملفات هي صور، فاطبع المسار المطلق للمجلد if TODO: print(TODO) يجب أن يطبع البرنامج عند تشغيله المسار المطلق لأي مجلدات صور على الشاشة. برنامج لإنشاء بطاقات جلوس مخصصة أنجزنا في مقالٍ سابق مشروعًا تدريبيًا لإنشاء دعوات مخصصة لقائمة من الضيوف موجودة في ملف نص عادي، لذا أضِف على هذا المشروع لإنشاء صور لبطاقات الجلوس المخصصة لضيوفك باستخدام الوحدة pillow. أنشئ ملف صورة باسم الضيف وبعض زخارف الزهور لكل من الضيوف المدرجين في الملف guests.txt الذي يتوفّر على الموارد الموجودة على موقع nostarch، وتتوفر أيضًا صورة زهرة ذات ملكية عامة دون حقوق نشر في هذه الموارد. أضِف مستطيلًا أسود على حواف صورة الدعوة بحيث تكون كدليل للقص عند طباعة الصورة للتأكد من أن جميع بطاقات جلوس لها الحجم نفسه. تُضبَط ملفات PNG التي تنتجها وحدة Pillow على 72 بكسلًا لكل بوصة، لذا تتطلب البطاقة التي أبعادها 4×5 بوصة صورةً بحجم 288×360 بكسل. الخلاصة تتكون الصور من مجموعة من البكسلات، وكل بكسل له قيمة RGBA للون الخاص به ويمكن تحديد مكانه باستخدام إحداثيات x و y، وهناك صيغتان شائعتان للصور هما JPEG و PNG، حيث يمكن لوحدة pillow التعامل مع هذه الصيغ للصور وغيرها من الصيغ. إذا حمّلنا صورة إلى كائن Image، فستُخزَّن أبعاد العرض والارتفاع الخاصة بها بوصفها مجموعة مكونة من عددين صحيحين في السمة size. تمتلك كائنات نوع البيانات Image أيضًا توابعًا لمعالجة الصور الشائعة وهي: crop() و copy() و paste() و resize() و rotate() و transpose(). يمكنك حفظ كائن Image في ملف صورة من خلال استدعاء التابع save(). إذا أردتَ أن يرسم برنامجك أشكالًا على الصور، فاستخدم توابع الوحدة ImageDraw لرسم النقاط والخطوط والمستطيلات والأشكال البيضاوية والمضلعات، وتوفر هذه الوحدة أيضًا توابعًا لرسم النصوص مع استخدام الخط والحجم الذي تختاره. توفّر التطبيقات المتقدمة وذات الكلفة العالية مثل برنامج الفوتوشوب ميزات معالجة آلية لحزمة من الصور، ولكن يمكنك استخدام سكربتات بايثون لإجراء العديد من التعديلات نفسها مجانًا. كتبنا في المقالات السابقة برامج بايثون للتعامل مع الملفات النصية العادية وجداول البيانات وملفات PDF وغيرها، ووسّعنا قدراتك البرمجية لمعالجة الصور أيضًا باستخدام وحدة pillow. ترجمة -وبتصرُّف- للمقال Manipulating Images لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: إرسال الرسائل النصية القصيرة باستخدام لغة بايثون معالجة النصوص باستخدام لغة بايثون Python مشاريع بايثون عملية تناسب المبتدئين أهم 10 مكتبات بايثون تستخدم في المشاريع الصغيرة
-
يمكنك كتابة برامج لإرسال رسائل البريد الإلكتروني والرسائل النصية القصيرة SMS لإعلامك بالأشياء حتى عندما تكون بعيدًا عن حاسوبك. إذا أجريتَ أتمتةً لمهمة تستغرق بضع ساعات لإنجازها، فلن ترغب في العودة إلى حاسوبك كل بضع دقائق للتحقق من حالة البرنامج، لذا يمكن لبرنامجك إرسال رسالة نصية إلى هاتفك عند الانتهاء فقط، مما يحرّرك من التركيز على أشياء أكثر أهمية عندما تكون بعيدًا عن حاسوبك. إرسال رسائل نصية باستخدام بوابات البريد الإلكتروني لخدمة الرسائل القصيرة SMS تكون الهواتف الذكية في متناول أيدينا أكثر من الحواسيب، لذلك تُعَد الرسائل النصية وسيلةً فورية وموثوقية لإرسال الإشعارات أكثر من البريد الإلكتروني، وتكون الرسائل النصية أقصر، ممّا يزيد من احتمالية أن يتمكّن الشخص من قراءتها. الطريقة الأسهل والأكثر موثوقية لإرسال رسائل نصية هي استخدام بوابة البريد الإلكتروني لخدمة الرسائل القصيرة SMS (أو Short Message Service)، وهذه البوابة هي خادم بريد إلكتروني يُعِدّه مزوّد الهاتف المحمول لتلقي الرسائل النصية عبر البريد الإلكتروني ثم يوجّهها إلى المستلم بوصفها رسالة نصية. يمكنك كتابة برنامج لإرسال رسائل البريد الإلكتروني باستخدام الوحدتين ezgmail أو smtplib، حيث يشكّل كلٌّ من رقم الهاتف وخادم البريد الإلكتروني لشركة الهاتف عنوانَ البريد الإلكتروني للمستلم، وسيكون موضوع ونص Body البريد الإلكتروني هو نص الرسالة النصية، فمثلًا يمكنك إرسال رسالة نصية إلى رقم الهاتف 415-555-1234 الذي يملكه عميل شركة Verizon من خلال إرسال بريد إلكتروني إلى العنوان 4155551234@vtext.com. يمكنك العثور على بوابة البريد الإلكتروني لخدمة الرسائل القصيرة SMS الخاصة بمزوّد الهاتف المحمول من خلال إجراء بحث على الويب عن اسم مزوّد بوابة البريد الإلكتروني لخدمة الرسائل القصيرة، ولكن يوضّح الجدول الآتي هذه البوابات للعديد من مزوّدي الخدمة المشهورين. يمتلك العديد من المزوّدين خوادم بريد إلكتروني منفصلة للرسائل النصية القصيرة، والتي تحدّد أن تحتوي الرسائل على 160 محرفًا، وخوادم أخرى منفصلة لخدمة رسائل الوسائط المتعددة MMS، والتي ليس لها حد أقصى لعدد المحارف. إذا أردتَ إرسال صورة، فيجب استخدام بوابة MMS وإرفاق الملف بالبريد الإلكتروني. إن لم تعرف مزوّد الهاتف المحمول للمستلم، فيمكنك تجربة استخدام موقعٍ للبحث عن شركة الاتصالات، والذي يجب أن يوفّر شركة الاتصالات الخاصة برقم الهاتف، حيث يمكنك العثور على هذه المواقع من خلال البحث في الويب عن مزود الهاتف المحمول لرقمٍ ما. ستتيح لك العديد من هذه المواقع البحث عن الأرقام مجانًا بالرغم من أنها ستفرض عليك رسومًا إذا كنت بحاجة إلى البحث عن مئات أو آلاف أرقام الهواتف من خلال واجهة برمجة التطبيقات الخاصة بها. يوضّح الجدول التالي بوابات البريد الإلكتروني لخدمة الرسائل القصيرة الخاصة بمزوّدي خدمات الهاتف المحمول: مزود الهاتف الخليوي بوابة SMS بوابة MMS AT&T البوابة number@txt.att.net البوابة number@mms.att.net Boost Mobile البوابة number@sms.myboostmobile.com بوابة SMS نفسها Cricket البوابة number@sms.cricketwireless.net البوابة number@mms.cricketwireless.net Google Fi البوابة number@msg.fi.google.com بوابة SMS نفسها Metro PCS البوابة number@mymetropcs.com بوابة SMS نفسها Republic Wireless البوابة number@text.republicwireless.com بوابة SMS نفسها Sprint البوابة number@messaging.sprintpcs.com البوابة number@pm.sprint.com T-Mobile البوابة number@tmomail.net بوابة SMS نفسها U.S. Cellular البوابة number@email.uscc.net البوابة number@mms.uscc.net Verizon البوابة number@vtext.com البوابة number@vzwpix.com Virgin Mobile البوابة number@vmobl.com البوابة number@vmpix.com XFinity Mobile البوابة number@vtext.com البوابة number@mypixmessages.com تُعَد بوابات البريد الإلكتروني لخدمة SMS مجانية وسهلة الاستخدام، ولكن لها بعضٌ من العيوب الرئيسية وهي: لا يوجد أيّ ضمان بأن النص سيصل مباشرةً أو قد لا يصل أبدًا. لا توجد طريقة لمعرفة فشل النص في الوصول. لا توجد طريقة للرد خاصةٌ بمستلم النص. قد تحظرك بوابات SMS إذا أرسلتَ عددًا كبيرًا جدًا من رسائل البريد الإلكتروني، ولا توجد طريقة لمعرفة عدد الرسائل التي ستكون "أكثر من الحد المسموح". لا يعني أن بوابة SMS تسلّم رسالة نصية اليوم أنها ستعمل غدًا. يُعَد إرسال النصوص عبر بوابة SMS مثاليًا عندما تحتاج إلى إرسال رسالة عابرة غير عاجلة، وإذا كنت بحاجة إلى خدمة أكثر موثوقية، فاستخدم خدمة بوابة SMS التي ليست عبر البريد الإلكتروني كما سنوضّح لاحقًا. إرسال رسائل نصية باستخدام خدمة Twilio ستتعلّم في هذا القسم كيفية التسجيل في خدمة Twilio المجانية واستخدام وحدة بايثون الخاصة بها لإرسال رسائل نصية. خدمة Twilio هي خدمة بوابة SMS، مما يعني أنها تسمح لك بإرسال رسائل نصية من برامجك عبر الإنترنت. يحتوي الحساب التجريبي المجاني لخدمة Twilio على كمية محدودة من الرصيد وستكون النصوص مسبوقة بجملة أن النص مُرسَل من حساب Twilio تجريبي "Sent from a Twilio trial account"، ولكن قد تكون هذه الخدمة التجريبية مناسبة لبرامجك الشخصية. ليست خدمة Twilio خدمة بوابة SMS الوحيدة، فإن لم تفضل استخدام Twilio، فيمكنك العثور على خدمات بديلة من خلال البحث عبر الإنترنت عن "free sms" أو "gateway" أو "python sms api" أو حتى "بدائل twilio". ثبّت الوحدة twilio باستخدام الأمر pip install --user --upgrade twilio على نظام ويندوز Windows (أو استخدم الأداة pip3 على نظامي ماك macOS ولينكس Linux) قبل التسجيل للحصول على حساب Twilio. ملاحظة: يُعَد هذا القسم خاصًا بالولايات المتحدة الأمريكية، ولكن تقدم Twilio خدمات الرسائل النصية القصيرة لدول أخرى غير الولايات المتحدة، لذا اطّلع على موقع Twilio الرسمي لمزيد من المعلومات، حيث ستعمل وحدة twilio ودوالها باستخدام الطريقة نفسها خارج الولايات المتحدة الأمريكية. التسجيل للحصول على حساب Twilio انتقل إلى موقع Twilio الرسمي واملأ استمارة التسجيل، ولكن يجب التحقق من رقم الهاتف المحمول الذي تريد إرسال الرسائل النصية إليه بعد التسجيل للحصول على حساب جديد. انتقل إلى صفحة معرّفات المتصل التي جرى التحقق منها Verified Caller IDs وأضِف رقم هاتف يمكنك الوصول إليه، ثم سترسل خدمة Twilio رمزًا إلى هذا الرقم والذي يجب أن تدخله للتحقق منه، حيث يكون هذا التحقق ضروريًا لمنع الأشخاص من استخدام الخدمة لإرسال رسائل نصية غير مرغوب فيها إلى أرقام هواتف عشوائية. ستتمكّن الآن من إرسال رسائل نصية إلى رقم الهاتف باستخدام الوحدة twilio. توفّر خدمة Twilio لحسابك التجريبي رقم هاتف لاستخدامه بوصفه مرسلًا للرسائل النصية، وستحتاج أيضًا معرّف SID ومفتاح الاستيثاق auth token الخاصين بحسابك، إذ يمكنك العثور على هذا المعرّف والمفتاح في صفحة لوحة التحكم Dashboard عندما تسجّل الدخول إلى حسابك على Twilio، حيث تعمل هذه القيم بوصفها اسم مستخدم وكلمة مرور Twilio عند تسجيل الدخول من برنامج بايثون. إرسال رسائل نصية ثبّت الوحدة twilio وسجّل على حساب Twilio، ثم تحقق من رقم هاتفك وسجّل رقم هاتف Twilio، ثم ستحصل على المعرّف SID ومفتاح الاستيثاق الخاصين بحسابك، وستكون أخيرًا جاهزًا لإرسال رسائل نصية لنفسك من سكربتات بايثون الخاصة بك . تُعَد شيفرة بايثون الفعلية بسيطةً إلى حدٍ ما بالمقارنة مع جميع خطوات التسجيل. أدخِل ما يلي في الصدفة التفاعلية أثناء اتصال حاسوبك بالإنترنت، مع استبدال قيم المتغيرات accountSID و authToken و myTwilioNumber و myCellPhone بمعلوماتك الحقيقية: ➊ >>> from twilio.rest import Client >>> accountSID = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' >>> authToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ➋ >>> twilioCli = Client(accountSID, authToken) >>> myTwilioNumber = '+14955551234' >>> myCellPhone = '+14955558888' ➌ >>> message = twilioCli.messages.create(body='Mr. Watson - Come here - I want to see you.', from_=myTwilioNumber, to=myCellPhone) يُفترَض أن تتلقى رسالة نصية بعد لحظات قليلة من كتابة السطر الأخير، وهذه الرسالة النصية هي: "Sent from your Twilio trial account - Mr. Watson - Come here - I want to see you". يجب استيراد الوحدة twilio باستخدام التعليمة from twilio.rest import Client، وليس باستخدام التعليمة import twilio فقط ➊ وفقًا للطريقة التي جرى فيها إعداد هذه الوحدة. خزّن معرّف SID الخاص بحسابك في المتغير accountSID وخزّن مفتاح الاستيثاق الخاص بك في المتغير authToken ثم استدعِ الدالة Client() ومرّر إليها accountSID و authToken. يعيد استدعاء الدالة Client() كائن Client ➋، حيث يحتوي هذا الكائن على السمة Attribute التي هي messages، والتي بدورها تحتوي على التابع create() الذي يمكنك استخدامه لإرسال رسائل نصية، وهو التابع الذي يوجّه خوادم Twilio لإرسال رسالتك النصية. خزّن رقم Twilio ورقم هاتفك المحمول في المتغيرين myTwilioNumber و myCellPhone، ثم استدعِ التابع create() ومرّر إليه وسطاء الكلمات المفتاحية Keyword Arguments التي تحدد نص الرسالة النصية ورقم المرسل (myTwilioNumber) ورقم المستلم (myCellPhone) ➌. يحتوي الكائن Message الذي يعيده التابع create() على معلومات حول الرسالة النصية المُرسَلة. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي: >>> message.to '+14955558888' >>> message.from_ '+14955551234' >>> message.body 'Mr. Watson - Come here - I want to see you.' يجب أن تحتوي السمات to و from_ و body على رقم هاتفك المحمول ورقم Twilio والرسالة على التوالي. لاحظ أن رقم الهاتف المرسِل موجود في السمة from_ مع شرطة سفلية في النهاية وليس from، لأن الكلمة from هي كلمة مفتاحية في لغة بايثون، إذ لا بد أنك رأيتها مستخدمةً في صيغة تعليمة الاستيراد from modulename import * مثلًا، لذلك لا يمكن استخدامها بوصفها اسمًا للسمة. تابع مثال الصدفة التفاعلية بما يلي: >>> message.status 'queued' >>> message.date_created datetime.datetime(2023, 7, 8, 1, 36, 18) >>> message.date_sent == None True يجب أن تعطي السمة status سلسلة نصية، ويجب أن تعطي السمات date_created و date_sent كائن datetime إذا أُنشِئت وأُرسِلت الرسالة. قد يبدو غريبًا ضبط السمة status على القيمة 'queued' وضبط السمة date_sent على القيمة None عندما تتلقى الرسالة النصية مسبقًا، والسبب في ذلك هو أنك التقطتَ الكائن Message في المتغير message قبل إرسال النص فعليًا. يجب إعادة جلب الكائن Message حتى تتمكّن من رؤية أحدث نسخة من السمتين status و date_sent. تحتوي كل رسالة من رسائل Twilio على معرّف سلسلة نصية SID فريد يمكن استخدامه لجلب آخر تحديث من الكائن Message. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي: >>> message.sid 'SM09520de7639ba3af137c6fcb7c5f4b51' ➊ >>> updatedMessage = twilioCli.messages.get(message.sid) >>> updatedMessage.status 'delivered' >>> updatedMessage.date_sent datetime.datetime(2023, 7, 8, 1, 36, 18) يعطي إدخال التعليمة message.sid معرّف SID الطويل الخاص بهذه الرسالة، ويمكنك استرداد كائن Message جديد مع أحدث المعلومات من خلال تمرير هذا المعرّف SID إلى التابع get() الخاص بعميل Twilio ➊، حيث تكون السمات status و date_sent صحيحة في كائن Message الجديد. تُضبَط السمة status على إحدى القيم التالية التي يكون نوعها سلسلة نصية: 'queued' أو 'sending' أو 'sent' أو 'delivered' أو 'undelivered' أو 'failed'. ملاحظة: يُعَد استلام الرسائل النصية باستخدام خدمة Twilio أكثر تعقيدًا بعض الشيء من إرسالها، إذ تتطلب خدمة Twilio أن يكون لديك موقع ويب يشغّل تطبيقه الويب، ويُعَد ذلك خارج نطاق هذا المقال. تطبيق عملي: وحدة لإرسال رسائل نصية يُحتمَل أن يكون الشخص الذي سترسل إليه رسائل نصية من برامجك هو أنت، إذ تُعَد الرسائل النصية طريقة رائعة لإرسال إشعارات لنفسك عندما تكون بعيدًا عن حاسوبك. إذا أردتَ أتمتة مهمة مملة باستخدام برنامج يستغرق تشغيله بضع ساعات، فيمكنك جعله يُعلِمكَ برسالةٍ نصية عند الانتهاء، أو قد يكون لديك برنامج مجدول ليعمل خلال فترات زمنية منتظمة ويحتاج إلى الاتصال بك في بعض الأحيان مثل برنامج التحقق من الطقس الذي يرسل إليك رسالة تذكيرية بأن تجلب مظلتك معك. سنوضّح فيما يلي برنامج بايثون صغير يحتوي على الدالة textmyself() التي ترسل رسالة نمرّرها إلى هذه الدالة كوسيط نوعه سلسلة نصية. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية، مع وضع معلوماتك الخاصة مكان معرّف SID ومفتاح الاستيثاق الخاصين بالحساب وأرقام الهاتف، واحفظ الملف بالاسم textMyself.py. #! python3 # textMyself.py - تعريف الدالة textmyself() التي ترسل رسالة نصية نمرّرها إليها بوصفها سلسلة نصية # القيم المُحدَّدة مسبقًا: accountSID = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' authToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' myNumber = '+15559998888' twilioNumber = '+15552225678' from twilio.rest import Client ➊ def textmyself(message): ➋ twilioCli = Client(accountSID, authToken) ➌ twilioCli.messages.create(body=message, from_=twilioNumber, to=myNumber) يخزِّن هذا البرنامج معرّف SID ومفتاح الاستيثاق الخاصين بالحساب والرقم المرسِل والرقم المستلِم، ثم يعرّف الدالة textmyself() التي تأخذ وسيطًا ➊، وينشئ كائن Client ➋، ويستدعي التابع create() مع الرسالة التي مرّرتها ➌. إذا أردتَ إتاحة الدالة textmyself() لبرامجك الأخرى، فما عليك سوى وضع الملف textMyself.py في المجلد نفسه الذي يحتوي على سكربت بايثون الخاص بك، وإذا أردتَ أن يرسل أحد برامجك رسالة نصية إليك، فأضِف إليه ما يلي: import textmyself textmyself.textmyself('The boring task is finished.') يجب التسجيل في خدمة Twilio وكتابة الشيفرة البرمجية الخاصة بإرسال الرسائل النصية مرة واحدة فقط، ثم يمكنك إرسال رسالة نصية من أيٍّ من برامجك الأخرى من خلال كتابة سطرين فقط من الشيفرة البرمجية. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر من خلال استخدام المعلومات التي حصلتَ عليها من المقال السابق وهذا المقال. برنامج لإرسال رسائل بريد إلكتروني لإنجاز مهمة روتينية عشوائية اكتب برنامجًا يأخذ قائمةً بعناوين البريد الإلكتروني لأشخاص وقائمةً بمهام روتينية يجب تنفيذها ويسند المهام الروتينية للأشخاص عشوائيًا، وأرسل بريدًا إلكترونيًا لكل شخص بالمهام الروتينية المُسنَدة إليه. احتفظ أيضًا بسجل للمهام الروتينية المُسنَدة مسبقًا لكل شخص لتتمكّن من التأكد من أن البرنامج يتجنب إعطاء أيّ شخص المهمة الروتينية نفسها التي أنجزها سابقًا، ويمكنك جدولة البرنامج لتشغيله مرة واحدة في الأسبوع تلقائيًا. إذا مرّرتَ قائمةً إلى الدالة random.choice()، فستعيد عنصرًا مُحدَّدًا عشوائيًا من القائمة. يمكن أن يبدو جزء من شيفرتك البرمجية كما يلي: chores = ['dishes', 'bathroom', 'vacuum', 'walk dog'] randomChore = random.choice(chores) chores.remove(randomChore) # أُنجِزت هذه المهمة الروتينية، لذا يجب إزالتها برنامج للتذكير بإحضار المظلة وضّحنا في مقالٍ سابق كيفية استخدام الوحدة requests لاستخراج البيانات من موقع الطقس، لذا اكتب برنامجًا يعمل قبل أن تستيقظ في الصباح مباشرةً ويتحقق مما إذا كانت السماء تمطر في ذلك اليوم. إذا كانت ستمطر، فاطلب من البرنامج أن يرسل لك رسالة تذكيرية بضرورة إحضار مظلة قبل مغادرة المنزل. برنامج لإلغاء الاشتراك التلقائي اكتب برنامجًا يبحث في حساب بريدك الإلكتروني ليجد جميع روابط إلغاء الاشتراك في جميع رسائل بريدك الإلكتروني، ويفتحها في المتصفح تلقائيًا. يجب على هذا البرنامج أن يسجّل الدخول إلى خادم IMAP الخاص بمزوّد بريدك الإلكتروني، وينزّل جميع رسائل بريدك الإلكتروني، ويمكنك استخدام المكتبة Beautiful Soup التي وضّحناها في مقالٍ سابق للتحقق من النسخة التي فيها كلمة إلغاء الاشتراك Unsubscribe ضمن الوسم link في شيفرة HTML. يمكنك استخدام الدالة webbrowser.open() لفتح جميع هذه الروابط لعناوين URL تلقائيًا في المتصفح بعد الحصول على قائمة بهذه العناوين، ثم يجب المرور على الخطوات الإضافية وإكمالها يدويًا لإلغاء الاشتراك بهذه القوائم، حيث يتضمن ذلك النقر على الرابط للتأكيد في معظم الحالات. يوفّر هذا السكربت عليك الاضطرار إلى المرور على جميع رسائل بريدك الإلكتروني بحثًا عن روابط إلغاء الاشتراك، ويمكنك بعد ذلك إعطاء هذا السكربت إلى أصدقائك حتى يتمكنوا من تشغيله على حسابات بريدهم الإلكتروني، ولكن تأكّد من أن كلمة مرور بريدك الإلكتروني غير مكتوبة في شيفرتك المصدرية. التحكم في حاسوبك من خلال البريد الإلكتروني اكتب برنامجًا يتحقق من حساب البريد الإلكتروني كل 15 دقيقة بحثًا عن أيّ تعليمات ترسلها إليه عبر البريد الإلكتروني وينفّذ تلك التعليمات تلقائيًا. يُعَد BitTorrent مثلًا نظام تنزيل يستخدم تقنية الند للند peer-to-peer، حيث يمكنك تنزيل ملفات الوسائط الكبيرة على حاسوبك المنزلي باستخدام برنامج BitTorrent مجاني مثل البرنامج qBittorrent. إذا أرسلتَ رابط BitTorrent (رابطًا قانونيًا وليس رابط قرصنة) إلى البرنامج عبر البريد الإلكتروني، فسيتحقق البرنامج من بريده الإلكتروني ويعثر على هذه الرسالة ويستخرج الرابط، ثم يشغّل برنامج qBittorrent لبدء تنزيل الملف. يمكنك بهذه الطريقة جعل حاسوبك المنزلي يبدأ التنزيلات أثناء تواجدك بعيدًا عن المنزل، ويمكن الانتهاء من التنزيل (القانوني وغير المقرصن) بحلول وقت عودتك إلى المنزل. وضّحنا في مقالٍ سابق السابق كيفية تشغيل البرامج على حاسوبك باستخدام الدالة subprocess.Popen()، فمثلًا سيؤدي الاستدعاء التالي إلى تشغيل برنامج qBittorrent مع ملف تورنت: qbProcess = subprocess.Popen(['C:\\Program Files (x86)\\qBittorrent\\ qbittorrent.exe', 'shakespeare_complete_works.torrent']) يجب أن يتأكد البرنامج من أن رسائل البريد الإلكتروني تأتي منك، إذ قد ترغب في اشتراط أن تحتوي رسائل البريد الإلكتروني على كلمة مرور، لأنه من السهل إلى حدٍ ما أن يزيّف المخترقون عنوان "من from" في رسائل البريد الإلكتروني. يجب أن يحذف البرنامج رسائل البريد الإلكتروني التي يجدها حتى لا يكرر التعليمات في كل مرة يتحقق فيها من حساب البريد الإلكتروني، واجعل البرنامج أيضًا يرسل لك بريدًا إلكترونيًا أو رسالة تأكيد في كل مرة ينفّذ فيها أمرًا. من الجيد استخدام دوال التسجيل الموضّحة في مقالٍ سابق لكتابة سجل ملف نصي يمكنك التحقق منه في حالة ظهور أخطاء، نظرًا لأنك لن تجلس أمام الحاسوب الذي يشغّل البرنامج. يتمتع برنامج qBittorrent وتطبيقات BitTorrent الأخرى بميزةٍ تمكّنه من الإغلاق تلقائيًا بعد اكتمال التنزيل، حيث وضّحنا في مقالٍ سابق كيف يمكنك تحديد موعد إنهاء التطبيق المُشغَّل باستخدام التابع wait() لكائنات Popen. سيوقِف استدعاء التابع wait() التنفيذ حتى يتوقف البرنامج qBittorrent، ثم يمكن لبرنامجك إرسال بريد إلكتروني أو رسالة نصية إليك لإعلامك باكتمال التنزيل. هناك الكثير من الميزات المحتملة التي يمكنك إضافتها إلى هذا المشروع، ولكن إذا واجهتك مشكلة، فيمكنك تنزيل مثال تطبيق هذا البرنامج من nostarch. الخلاصة تختلف الرسائل النصية عن البريد الإلكتروني بعض الشيء، لأنه هناك حاجة إلى أكثر من مجرد اتصال بالإنترنت لإرسال رسائل نصية قصيرة SMS على عكس البريد الإلكتروني، حيث توفر خدمات مثل خدمة Twilio وحداتٍ تسمح بإرسال رسائل نصية من برامجك. ستتمكّن بعد إجراء عملية الإعداد الأولية من إرسال الرسائل النصية باستخدام سطرين فقط من الشيفرة البرمجية. ستتمكّن باستخدام هذه الوحدات مع مهاراتك الأخرى من برمجة الشروط المُحدَّدة التي بموجبها يجب على برامجك إرسال الإشعارات أو التذكيرات، وبالتالي ستصل برامجك الآن إلى ما هو أبعد من حاسوبك الذي تعمل عليه. ترجمة -وبتصرُّف- للقسم Sending Text Messages من مقال Sending Email and Text Messages لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: إرسال رسائل البريد الإلكتروني باستخدام لغة بايثون جدولة المهام وتشغيل برامج أخرى باستخدام لغة بايثون قراءة مستندات جداول إكسل باستخدام لغة بايثون Python الكتابة في مستندات إكسل باستخدام لغة بايثون Python
-
يحتاج التحقق من البريد الإلكتروني والرد عليه الكثير من الوقت، ولا يمكنك كتابة برنامج للتعامل مع جميع رسائل بريدك الإلكتروني، إذ تتطلب كل رسالة ردًا خاصًا بها، ولكن يمكنك أتمتة الكثير من المهام الأخرى المتعلقة بالبريد الإلكتروني بعد أن تعرف كيفية كتابة البرامج التي يمكنها إرسال واستقبال رسائل البريد الإلكتروني. قد يكون لديك مثلًا جدول بيانات يحتوي على الكثير من سجلات العملاء وتريد إرسال رسالة تحتوي على استمارة Form خاصة بكل عميل اعتمادًا على تفاصيل عمره وموقعه، وقد لا تتمكن البرمجيات التجارية من فعل ذلك نيابةً عنك، ولكن يمكنك كتابة برنامجك الخاص لإرسال هذه الرسائل عبر البريد الإلكتروني، مما يوفّر عليك الكثير من الوقت لنسخ ولصق رسائل البريد الإلكتروني التي تحتوي على استمارات. سنوضّح في هذا المقال الوحدة EZGmail التي تُعَد طريقة بسيطة لإرسال وقراءة رسائل البريد الإلكتروني من حسابات جيميل Gmail، وهي بايثون Python لاستخدام بروتوكولات البريد الإلكتروني المعيارية SMTP و IMAP. ملاحظة: نوصي بشدة بإعداد حساب بريد إلكتروني منفصل لأيّ سكربتات ترسل أو تستقبل رسائل البريد الإلكتروني، مما يؤدي إلى منع الأخطاء الموجودة في برامجك من التأثير على حساب بريدك الإلكتروني الشخصي مثل حذف رسائل البريد الإلكتروني أو إرسال رسائل غير مرغوب بها Spam إلى جهات الاتصال الخاصة بك عن طريق الخطأ. يُفضَّل أن تجري أولًا تشغيلًا تجريبيًا من خلال تعليق الشيفرة البرمجية التي ترسل رسائل البريد الإلكتروني أو تحذفها فعليًا ووضع استدعاء مؤقت للدالة print() مكانها، وبالتالي يمكنك اختبار برنامجك قبل تشغيله تشغيلًا حقيقيًا. إرسال واستقبال رسائل البريد الإلكتروني باستخدام واجهة برمجة تطبيقات جيميل Gmail API يمتلك جيميل ما يقرب من ثلث حصة سوق عملاء البريد الإلكتروني، إذ لا بد أنّ لديك عنوان بريد إلكتروني واحد على الأقل على جيميل. يتميز جيميل بتدابير الأمان الإضافية ومكافحة البريد الالكتروني غير المرغوب به، لذا من الأسهل التحكم في حساب جيميل باستخدام الوحدة EZGmail بدلًا من التحكم به باستخدام الوحدتين smtplib و imapclient اللتين سنناقشهما لاحقًا في هذا المقال. كتب Al Sweigart وحدة EZGmail، حيث تعمل هذه الوحدة فوق واجهة برمجة تطبيقات جيميل الرسمية وتوفّر دوالًا تسهّل استخدام جيميل من شيفرة بايثون. اطّلع على تفاصيل EZGmail الكاملة على GitHub، حيث لا تنتِج جوجل هذه الوحدة وليست تابعة لها، واطّلع على التوثيق الرسمي لواجهة برمجة تطبيقات جيميل Gmail API. يمكنك تثبيت وحدة EZGmail من خلال تشغيل الأمر pip install --user --upgrade ezgmail على نظام ويندوز، أو استخدم الأداة pip3 على نظامي ماك macOS ولينكس Linux. يضمن الخيار --upgrade تثبيت أحدث إصدار من الحزمة، وهو أمر ضروري للتفاعل مع خدمة دائمة التغير عبر الإنترنت مثل واجهة برمجة تطبيقات جيميل. تفعيل واجهة برمجة تطبيقات جيميل يجب عليك أولًا التسجيل للحصول على حساب بريد إلكتروني على جيميل قبل أن تكتب شيفرتك البرمجية. انتقل بعد ذلك إلى صفحة البدء السريع لاستخدام بايثون، وانقر على زر تفعيل واجهة برمجة تطبيقات جيميل Enable the Gmail API في تلك الصفحة، واملأ الاستمارة التي ستظهر. ستقدم الصفحة رابطًا للملف credentials.json بعد ملء الاستمارة، حيث يجب أن تنزّل هذا الملف وتضعه في المجلد نفسه لملف .py الخاص بك. يحتوي الملف credentials.json على معرّف العميل Client ID ومعلومات العميل السرية Client Secret، والتي يجب عليك التعامل معها مثل كلمة مرور حسابك على جيميل وعدم مشاركتها مع أيّ شخص آخر. لندخِل الآن الشيفرة البرمجية التالية في الصدفة التفاعلية Interactive Shell: >>> import ezgmail, os >>> os.chdir(r'C:\path\to\credentials_json_file') >>> ezgmail.init() تأكّد من ضبط مجلد العمل الحالي على المجلد نفسه الذي يوجد به الملف credentials.json وأنك متصل بالإنترنت. تفتح الدالة ezgmail.init() متصفحك على صفحة تسجيل الدخول إلى جوجل، لذا أدخِل عنوان جيميل وكلمة مرورك. قد تحذّرك الصفحة بعدم التحقق من هذا التطبيق This app isn’t verified""، ولكن لا بأس بذلك. انقر بعد ذلك على "خيارات متقدمة Advanced"، وانقر على خيار الانتقال إلى صفحة البدء السريع (غير آمن) "Go to Quickstart (unsafe)". (إذا أردتَ كتابة سكربتات بايثون لأشخاص آخرين ولا تريد ظهور هذا التحذير لهم، فيجب أن تتعرّف على عملية التحقق من تطبيق جوجل، والتي لن نناقشها في هذا المقال. انقر على خيار "السماح Allow" ثم أغلق المتصفح عندما تعرض الصفحةُ التالية الرسالةَ "تريد صفحة البدء السريع الوصول إلى حسابك جوجل Quickstart wants to access your Google Account". يتولّد بعد ذلك ملف token.json لمنح سكربتات بايثون الخاصة بك إمكانية الوصول إلى حساب جيميل الذي أدخلته، ولن يفتح المتصفح إلّا على صفحة تسجيل الدخول إن لم يتمكّن من العثور على ملف token.json موجودٍ مسبقًا. يمكن لسكربتات بايثون الخاصة بك باستخدام الملفين credentials.json و token.json إرسالَ رسائل البريد الإلكتروني وقراءتها من حسابك على جيميل دون مطالبتك بتضمين كلمة مرور جيميل في شيفرتك المصدرية. إرسال رسائل البريد الإلكتروني من حساب جيميل يجب أن تكون وحدة EZGmail قادرةً على إرسال بريد إلكتروني باستخدام استدعاء دالةٍ واحد بعد حصولك على الملف token.json كما يلي: >>> import ezgmail >>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email') إذا أردتَ إرفاق ملفاتٍ ببريدك الإلكتروني، فيمكنك توفير وسيط قائمة إضافي للدالة send(): >>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email', ['attachment1.jpg', 'attachment2.mp3']) لاحظ أنه -كجزء من ميزات الأمان ومكافحة الرسائل غير المرغوب بها- قد لا يرسل جيميل رسائل بريد إلكتروني متكررة تحتوي على النص نفسه لأنها يمكن أن تكون رسائلًا غير مرغوب بها، أو رسائل بريد إلكتروني تحتوي على مرفقات ملفات لها الامتداد .exe أو .zip لأنها يمكن أن تكون فيروسات. يمكنك أيضًا توفير وسطاء الكلمات المفتاحية Keyword Arguments الاختيارية cc و bcc لإرسال نسخ مطابقة Carbon Copies ونسخ مطابقة مخفية Blind Carbon Copies: >>> import ezgmail >>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email', cc='friend@example.com', bcc='otherfriend@example.com,someoneelse@example.com') إذا أردتَ أن تتذكر عنوان جيميل الذي ضُبِط الملف token.json عليه، فيمكنك فحص المتغير ezgmail.EMAIL_ADDRESS، حيث يُملَأ هذا المتغير فقط بعد استدعاء الدالة ezgmail.init() أو أي دالة أخرى خاصة بالوحدة EZGmail. >>> import ezgmail >>> ezgmail.init() >>> ezgmail.EMAIL_ADDRESS 'example@gmail.com' تأكّد من التعامل مع الملف token.json بالطريقة نفسها للتعامل مع كلمة مرورك، حيث إذا حصل شخصٌ آخر على هذا الملف، فيمكنه الوصول إلى حسابك على جيميل بالرغم من أنه لن يتمكّن من تغيير كلمة مرور حسابك على جيميل. يمكنك إبطال ملفات token.json الصادرة مسبقًا من خلال الانتقال إلى الرابط https://security.google.com/settings/security/permissions?pli=1/، ثم أبطِل الوصول إلى تطبيق البدء السريع Quickstart، ولكن يجب تشغيل الدالة ezgmail.init() ومتابعة عملية تسجيل الدخول مرة أخرى للحصول على ملف token.json جديد. قراءة رسائل البريد الإلكتروني من حساب جيميل ينظّم جيميل رسائل البريد الإلكتروني التي تمثل ردودًا على بعضها البعض ضمن سلاسل محادثات Conversation Threads. إذا سجّلتَ الدخول إلى جيميل في متصفح الويب أو من خلال أحد التطبيقات، فسترى سلاسل رسائل البريد الإلكتروني بدلًا من رسائل البريد الإلكتروني الفردية، حتى لو احتوت إحدى تلك السلاسل على رسالة بريد إلكتروني واحدة فقط. تحتوي الوحدة EZGmail على كائنات GmailThread و GmailMessage لتمثيل سلاسل المحادثات ورسائل البريد الإلكتروني الفردية على التوالي، ويحتوي الكائن GmailThread على سمةٍ Attribute هي السمة messages التي تحتوي على قائمة بكائنات GmailMessage. تعيد الدالة unread() قائمةً بكائنات GmailThread لجميع رسائل البريد الإلكتروني غير المقروءة، والتي يمكن بعد ذلك تمريرها إلى الدالة ezgmail.summary() لطباعة ملخصٍ لسلاسل المحادثات في تلك القائمة: >>> import ezgmail >>> unreadThreads = ezgmail.unread() # قائمة بكائنات GmailThread >>> ezgmail.summary(unreadThreads) Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09 Jon - Thanks for stopping me from buying Bitcoin. - Dec 09 تُعَد الدالة summary() مفيدة لعرض ملخصٍ سريع لسلاسل رسائل البريد الإلكتروني، ولكن يمكنك الوصول إلى رسائل محددة (وأجزاء منها) من خلال فحص السمة messages الخاصة بالكائن GmailThread، حيث تحتوي هذه السمة على قائمة بكائنات GmailMessage التي تشكّل سلسلة المحادثات، وتحتوي هذه الكائنات على سمات subject و body و timestamp و sender و recipient التي توصف البريد الإلكتروني. >>> len(unreadThreads) 2 >>> str(unreadThreads[0]) "<GmailThread len=2 snippet= Do you want to watch RoboCop this weekend?'>" >>> len(unreadThreads[0].messages) 2 >>> str(unreadThreads[0].messages[0]) "<GmailMessage from='Al Sweigart <al@inventwithpython.com>' to='Jon Doe <example@gmail.com>' timestamp=datetime.datetime(2018, 12, 9, 13, 28, 48) subject='RoboCop' snippet='Do you want to watch RoboCop this weekend?'>" >>> unreadThreads[0].messages[0].subject 'RoboCop' >>> unreadThreads[0].messages[0].body 'Do you want to watch RoboCop this weekend?\r\n' >>> unreadThreads[0].messages[0].timestamp datetime.datetime(2018, 12, 9, 13, 28, 48) >>> unreadThreads[0].messages[0].sender 'Al Sweigart <al@inventwithpython.com>' >>> unreadThreads[0].messages[0].recipient 'Jon Doe <example@gmail.com>' تعيد الدالة ezgmail.recent() أحدث 25 سلسلة محادثات في حسابك على جيميل كما تفعل الدالة ezgmail.unread()، ولكن يمكنك تمرير وسيط الكلمات المفتاحية maxResults الاختياري لتغيير هذا الحد كما يلي: >>> recentThreads = ezgmail.recent() >>> len(recentThreads) 25 >>> recentThreads = ezgmail.recent(maxResults=100) >>> len(recentThreads) 46 البحث عن رسائل البريد الإلكتروني في حساب جيميل يمكنك البحث عن رسائل بريد إلكتروني محددة باستخدام الطريقة نفسها التي تستخدمها لإدخال استعلامات في مربع البحث على جيميل من خلال استدعاء الدالة ezgmail.search(): >>> resultThreads = ezgmail.search('RoboCop') >>> len(resultThreads) 1 >>> ezgmail.summary(resultThreads) Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09 يجب أن يؤدي الاستدعاء السابق للدالة search() إلى النتائج نفسها عندما تدخل الكلمة "RoboCop" في مربع البحث كما في الشكل التالي: البحث عن رسائل البريد الإلكتروني "RoboCop" في موقع جيميل الإلكتروني تعيد الدالة search() قائمةً بكائنات GmailThread كما تفعل الدالتان unread() و recent()، ويمكنك أيضًا تمرير أيٍّ من معاملات البحث الخاصة التي يمكنك إدخالها في مربع البحث إلى الدالة search() مثل المعاملات التالية: 'label:UNREAD': لرسائل البريد الإلكتروني غير المقروءة. 'from:al@inventwithpython.com': لرسائل البريد الإلكتروني الواردة من al@inventwithpython.com. 'subject:hello': لرسائل البريد الإلكتروني التي تحتوي على الكلمة "hello" في موضوعها. 'has:attachment': لرسائل البريد الإلكتروني التي تحتوي على ملفات مرفقة. ملاحظة: اطّلع على القائمة الكاملة لمعاملات البحث. تنزيل المرفقات من حساب جيميل تحتوي كائنات GmailMessage على السمة attachments، والتي هي قائمة بأسماء الملفات المُرفَقة مع الرسالة، حيث يمكنك تمرير أيٍّ من هذه الأسماء إلى التابع downloadAttachment() الخاص بكائن GmailMessage لتنزيل الملفات، ويمكنك أيضًا تنزيلها جميعًا دفعةً واحدة باستخدام التابع downloadAllAttachments(). تحفظ الوحدة EZGmail المرفقات في مجلد العمل الحالي افتراضيًا، ولكن يمكنك تمرير وسيط الكلمات المفتاحية الإضافي downloadFolder إلى التابعين downloadAttachment() و downloadAllAttachments() أيضًا لتنزيل المجلد. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import ezgmail >>> threads = ezgmail.search('vacation photos') >>> threads[0].messages[0].attachments ['tulips.jpg', 'canal.jpg', 'bicycles.jpg'] >>> threads[0].messages[0].downloadAttachment('tulips.jpg') >>> threads[0].messages[0].downloadAllAttachments(downloadFolder='vacat ion2023') ['tulips.jpg', 'canal.jpg', 'bicycles.jpg'] إذا وُجِد ملف يحمل اسم الملف المرفق نفسه، فسيحل الملف المرفق الذي نزّلناه محله تلقائيًا. تحتوي الوحدة EZGmail على ميزات إضافية، حيث يمكنك العثور عليها ضمن توثيقها الكامل على Github. بروتوكول SMTP تستخدم الحواسيب بروتوكول HTTP لإرسال صفحات الويب عبر الإنترنت، ويُستخدَم بروتوكول نقل البريد البسيط Simple Mail Transfer Protocol -أو SMTP اختصارًا- لإرسال البريد الإلكتروني، حيث يتمتع هذان البروتوكولان بالمقدار نفسه من الأهمية. يحدّد بروتوكول SMTP كيفية تنسيق رسائل البريد الإلكتروني وتشفيرها ونقلها بين خوادم البريد وجميع التفاصيل الأخرى التي يعالجها حاسوبك بعد النقر على زر الإرسال، ولكنك لست بحاجة إلى معرفة هذه التفاصيل التقنية، لأن الوحدة smtplib الخاصة بلغة بايثون تبسّطها إلى بضع دوال. يتعامل بروتوكول SMTP فقط مع إرسال رسائل البريد الإلكتروني إلى المستخدمين الآخرين، ويتعامل بروتوكول مختلف هو بروتوكول IMAP مع استرداد رسائل البريد الإلكتروني المرسَلة إليك، حيث سنوضّح هذا البروتوكول لاحقًا. يوفّر معظم مزوّدي خدمات البريد الإلكتروني المستندة إلى الويب -بالإضافة إلى بروتوكولَي SMTP و IMAP- حاليًا إجراءات أمنية أخرى للحماية من البريد غير المرغوب به والتصيد الاحتيالي Phishing واستخدامات البريد الإلكتروني الضارة الأخرى. تمنع هذه الإجراءات سكربتات بايثون من تسجيل الدخول إلى حساب بريد إلكتروني باستخدام وحدتي smtplib و imapclient، ولكن تحتوي العديد من هذه الخدمات على واجهات برمجة التطبيقات API ووحدات بايثون محددة تسمح للسكربتات بالوصول إليها. سنشرح في هذا المقال الوحدة الخاصة بخدمة جيميل، ولكنك ستحتاج إلى الرجوع إلى التوثيق الرسمي للخدمات الأخرى. إرسال البريد الإلكتروني قد تكون على دراية بإرسال رسائل البريد الإلكتروني من أوت لوك Outlook أو ثندربرد Thunderbird أو من خلال موقع ويب مثل جيميل Gmail أو بريد ياهو Yahoo Mail، ولكن لسوء الحظ لا تقدّم لغة بايثون واجهة مستخدم رسومية جميلة مثل تلك التي تقدّمها هذه الخدمات، لذا يمكنك بدلًا من ذلك استدعاء الدوال لإجراء الخطوات الرئيسية من بروتوكول SMTP كما هو موضّح في مثال الصدفة التفاعلية الآتي. ملاحظة: لا تدخِل المثال التالي في الصدفة التفاعلية، إذ لن ينجح الأمر، لأن smtp.example.com و bob@example.com و MY_SECRET_PASSWORD و alice@example.com هي عناصر بديلة، إذ تُعَد هذه الشيفرة البرمجية مجرد نظرة عامة على عملية إرسال بريد إلكتروني باستخدام بايثون. >>> import smtplib >>> smtpObj = smtplib.SMTP('smtp.example.com', 587) >>> smtpObj.ehlo() (250, b'mx.example.com at your service, [216.172.148.131]\nSIZE 35882577\ n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING') >>> smtpObj.starttls() (220, b'2.0.0 Ready to start TLS') >>> smtpObj.login('bob@example.com', 'MY_SECRET_PASSWORD') (235, b'2.7.0 Accepted') >>> smtpObj.sendmail('bob@example.com', 'alice@example.com', 'Subject: So long.\nDear Alice, so long and thanks for all the fish. Sincerely, Bob') {} >>> smtpObj.quit() (221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp') سنوضّح في الأقسام التالية كل خطوة من هذه الشيفرة البرمجية مع استبدال العناصر البديلة بمعلوماتك للاتصال بخادم SMTP وتسجيل الدخول إليه، وإرسال بريد إلكتروني، وقطع الاتصال بالخادم. الاتصال بخادم SMTP إذا أعددتَ ثندربيرد أو أوت لوك أو أي برنامج آخر للاتصال بحساب بريدك الإلكتروني، فقد تكون على دراية بضبط خادم ومنفذ SMTP، حيث ستكون هذه الإعدادات مختلفة بحسب مزوّد البريد الإلكتروني، ولكن يجب أن يمكنك البحث عبر الويب عن إعدادات مزوّدك لبروتوكول SMTP للحصول على الخادم والمنفذ لاستخدامهما. يكون عادةً اسم النطاق Domain لخادم SMTP هو اسم نطاق مزوّد بريدك الإلكتروني مع وجود البادئة smtp. قبله، فمثلًا خادم SMTP الخاص بشركة Verizon موجودٌ على النطاق smtp.verizon.net. يسرد الجدول التالي بعضًا من مزوّدي البريد الإلكتروني وخوادم SMTP الخاصة بهم، حيث يُعَد المنفذ Port قيمةً صحيحة وتكون دائمًا تقريبًا 587، ويستخدمه معيار تشفير الأوامر TLS. مزوّد البريد الإلكتروني اسم نطاق خادم SMTP Gmail* اسم النطاق smtp.gmail.com Outlook.com/Hotmail.com* اسم النطاق smtp-mail.outlook.com Yahoo Mail* اسم النطاق smtp.mail.yahoo.com AT&T اسم النطاق smpt.mail.att.net (المنفذ 465) Comcast اسم النطاق smtp.comcast.net Verizon اسم النطاق smtp.verizon.net (المنفذ 465) ملاحظة: تمنع الإجراءات الأمنية الإضافية شيفرة بايثون من تسجيل الدخول إلى هذه الخوادم التي وضعنا بجانب اسمها المحرف (*) باستخدام الوحدة smtplib، ولكن يمكن لوحدة EZGmail تجاوز هذه الصعوبة لحسابات جيميل. إذا حصلتَ على اسم النطاق ومعلومات المنفذ لمزوّد بريدك الإلكتروني، فيمكنك إنشاء كائن SMTP من خلال استدعاء الدالة smptlib.SMTP()، وتمرير اسم النطاق كوسيط من نوع السلسلة النصية والمنفذ كوسيط من نوع عدد صحيح إليها. يمثل الكائن SMTP اتصالًا بخادم بريد SMTP ويمتلك توابع لإرسال رسائل البريد الإلكتروني، فمثلًا ينشئ الاستدعاء التالي كائن SMTP للاتصال بخادم بريد إلكتروني وهمي: >>> smtpObj = smtplib.SMTP('smtp.example.com', 587) >>> type(smtpObj) <class 'smtplib.SMTP'> يُظهِر إدخال الدالة type(smtpObj) وجود كائن SMTP مخزّنٍ في المتغير smtpObj، حيث ستحتاج إلى هذا الكائن لاستدعاء التوابع التي تسجل دخولك وترسل رسائل البريد الإلكتروني. إن لم ينجح استدعاء الدالة smptlib.SMTP()، فقد لا يدعم خادم SMTP الخاص بك بروتوكول TLS على المنفذ 587، وبالتالي يجب إنشاء كائن SMTP باستخدام الدالة smtplib.SMTP_SSL() والمنفذ 465 بدلًا من ذلك. >>> smtpObj = smtplib.SMTP_SSL('smtp.example.com', 465) ملاحظة: إن لم تكن متصلًا بالإنترنت، فسترفع شيفرة بايثون استثناء socket.gaierror: [Errno 11004] getaddrinfo failed أو أيّ استثناء آخر مشابه. لا تُعَد الاختلافات بين بروتوكولَي TLS و SSL مهمة بالنسبة لبرامجك، فما عليك سوى معرفة معيار التشفير الذي يستخدمه خادم SMTP الخاص بك حتى تعرف كيفية الاتصال به. سيحتوي المتغير smtpObj في كافة أمثلة الصدفة التفاعلية التالية على كائن SMTP الذي تعيده الدالة smtplib.SMTP() أو الدالة smtplib.SMTP_SSL(). إرسال رسالة الترحيب "Hello" الخاصة ببروتوكول SMTP إذا حصلنا على كائن SMTP، فيمكننا استدعاء التابع ehlo() للترحيب بخادم البريد الإلكتروني SMTP، حيث يُعَد هذا الترحيب الخطوة الأولى في بروتوكول SMTP وهو مهم لتأسيس اتصال مع الخادم. لا حاجة لمعرفة تفاصيل هذه البروتوكولات، ولكن تأكّد من استدعاء التابع ehlo() أولًا بعد الحصول على كائن SMTP، وإلّا ستؤدي استدعاءات التوابع اللاحقة إلى حدوث أخطاء. إليك مثال على استدعاء التابع ehlo() وقيمته المُعادة: >>> smtpObj.ehlo() (250, b'mx.example.com at your service, [216.172.148.131]\nSIZE 35882577\ n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING') إذا كان العنصر الأول في المجموعة Tuple المُعادة هو العدد الصحيح 250 (رمز النجاح في بروتوكول SMTP)، فهذا يعني أن الترحيب قد نجح. بدء تشفير TLS إذا كنتَ متصلًا بالمنفذ 587 على خادم SMTP (أي أنك تستخدم تشفير TLS)، فيجب استدعاء التابع starttls() لاحقًا، حيث تؤدي هذه الخطوة المطلوبة إلى تفعيل التشفير على اتصالك. إذا كنت متصلًا بالمنفذ 465 (أي أنك تستخدم بروتوكول SSL)، فهذا يعني التشفير مُعَد مسبقًا، ويجب عليك تخطي هذه الخطوة. إليك مثال لاستدعاء التابع starttls(): >>> smtpObj.starttls() (220, b'2.0.0 Ready to start TLS') يضع التابع starttls() اتصال SMTP الخاص بك في وضع TLS، ويخبرك العدد 220 الموجود في القيمة المُعادة أن الخادم جاهز. تسجيل الدخول إلى خادم SMTP إذا أعددتَ اتصالك المشفّر بخادم SMTP، فيمكنك تسجيل الدخول باستخدام اسم المستخدم الخاص بك (وهو عنوان بريدك الإلكتروني عادةً) وكلمة مرور بريدك الإلكتروني من خلال استدعاء التابع login(). >>> smtpObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD') (235, b'2.7.0 Accepted') مرّر سلسلةً نصية تمثّل عنوان بريدك الإلكتروني كوسيطٍ أول وسلسلة نصية تمثّل كلمة مرورك كوسيطٍ ثانٍ إلى التابع login()، وتعني القيمة 235 الموجودة في القيمة المُعادة أن الاستيثاق Authentication ناجح. ترفع شيفرة بايثون الاستثناء smtplib.SMTPAuthenticationError لكلمات المرور غير الصحيحة. ملاحظة: كن حذرًا بشأن وضع كلمات المرور في شيفرتك المصدرية، حيث إذا نسخ شخصٌ ما برنامجك، فسيكون بإمكانه الوصول إلى حساب بريدك الإلكتروني، لذا يُفضَّل استدعاء الدالة input() وجعل المستخدم يكتب كلمة المرور. قد يكون اضطرارك إلى إدخال كلمة المرور في كل مرة تشغّل فيها برنامجك أمرًا غير مريح، ولكن تمنعك هذه الطريقة من ترك كلمة مرورك في ملف غير مشفّر على حاسوبك بحيث يمكن للمخترق أو للص الذي يسرق حاسوبك المحمول مثلًا الحصول عليها بسهولة. إرسال رسالة عبر البريد الإلكتروني سجّلنا الدخول إلى خادم SMTP الخاص بمزوّد بريدك الإلكتروني، وبالتالي يمكننا الآن استدعاء التابع sendmail() لإرسال البريد الإلكتروني فعليًا، حيث يبدو استدعاء هذا التابع كما يلي: >>> smtpObj.sendmail('my_email_address@example.com ', 'recipient@example.com', 'Subject: So long.\nDear Alice, so long and thanks for all the fish. Sincerely, Bob') {} يتطلب التابع sendmail() ثلاثة وسطاء هي: عنوان بريدك الإلكتروني كسلسلة نصية (للعنوان "from مِن" الخاص بالبريد الإلكتروني). عنوان البريد الإلكتروني للمستلم كسلسلة نصية أو قائمةً من السلاسل النصية لمستلمين متعددين (للعنوان "to إلى"). نص Body البريد الإلكتروني كسلسلة نصية. يجب أن تبدأ السلسلة النصية لنص البريد الإلكتروني بالعبارة 'Subject: \n' لسطر موضوع البريد الإلكتروني، حيث يفصل محرف السطر الجديد '\n' سطر الموضوع عن النص الرئيسي للبريد الإلكتروني. القيمة المُعادة من التابع sendmail() هي قاموس، إذ سيكون هناك زوج مفتاح-قيمة واحد في القاموس لكل مستلمٍ فشل تسليم البريد الإلكتروني إليه، ويعني القاموس الفارغ أن البريد الإلكتروني اُرسِل بنجاح إلى جميع المستلمين. قطع الاتصال بخادم SMTP تأكّد من استدعاء التابع quit() عند الانتهاء من إرسال رسائل البريد الإلكتروني، مما يؤدي إلى قطع اتصال برنامجك بخادم SMTP. >>> smtpObj.quit() (221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp') تعني القيمة 221 الموجودة في القيمة المُعادة انتهاءَ الجلسة. البروتوكول IMAP يُعَد البروتوكول SMTP بروتوكول إرسالٍ لرسائل البريد الإلكتروني، ولكن يحدّد بروتوكول الوصول إلى رسائل الإنترنت Internet Message Access Protocol -أو IMAP اختصارًا- كيفية الاتصال بخادم مزوّد البريد الإلكتروني لاسترداد رسائل البريد الإلكتروني المُرسَلة إلى عنوان بريدك الإلكتروني. تحتوي لغة بايثون على الوحدة imaplib، ولكن تُعَد الوحدة imapclient الخارجية أسهل في الاستخدام. يقدّم هذا المقال مقدمة لاستخدام الوحدة IMAPClient، لذا اطلّع على توثيقها الرسمي الكامل على موقعها الرسمي. تنزّل الوحدة imapclient رسائل البريد الإلكتروني من خادم IMAP بتنسيقٍ معقد إلى حد ما، لذا قد تحتاج إلى تحويلها من هذا التنسيق إلى قيم سلاسل نصية بسيطة. تنفّذ الوحدة pyzmail المهمة الصعبة المتمثلة في تحليل رسائل البريد الإلكتروني نيابةً عنك، لذا اطّلع على التوثيق الكامل لهذه الوحدة. ثبّت الوحدتين imapclient و pyzmail من النافذة الطرفية Terminal باستخدام الأمرين pip install --user -U imapclient==2.1.0 و pip install --user -U pyzmail36== 1.0.4 على نظام ويندوز Windows، أو باستخدام الأداة pip3 على نظامي ماك macOS ولينكس Linux. استرداد وحذف رسائل البريد الإلكتروني باستخدام بروتوكول IMAP يُعَد البحث عن بريد إلكتروني واسترداده في لغة بايثون عملية متعددة الخطوات وتتطلب كلًا من الوحدتين الخارجيتين imapclient و pyzmail. إليك مثال كامل لتسجيل الدخول إلى خادم IMAP والبحث عن رسائل البريد الإلكتروني وجلبها، ثم استخراج نص رسائل البريد الإلكتروني منها: >>> import imapclient >>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True) >>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD') 'my_email_address@example.com Jane Doe authenticated (Success)' >>> imapObj.select_folder('INBOX', readonly=True) >>> UIDs = imapObj.search(['SINCE 05-Jul-2023']) >>> UIDs [40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041] >>> rawMessages = imapObj.fetch([40041], ['BODY[]', 'FLAGS']) >>> import pyzmail >>> message = pyzmail.PyzMessage.factory(rawMessages[40041][b'BODY[]']) >>> message.get_subject() 'Hello!' >>> message.get_addresses('from') [('Edward Snowden', 'esnowden@nsa.gov')] >>> message.get_addresses('to') [('Jane Doe', 'jdoe@example.com')] >>> message.get_addresses('cc') [] >>> message.get_addresses('bcc') [] >>> message.text_part != None True >>> message.text_part.get_payload().decode(message.text_part.charset) 'Follow the money.\r\n\r\n-Ed\r\n' >>> message.html_part != None True >>> message.html_part.get_payload().decode(message.html_part.charset) '<div dir="ltr"><div>So long, and thanks for all the fish!<br><br></div>- Al<br></div>\r\n' >>> imapObj.logout() لا حاجة لحفظ جميع هذه الخطوات، إذ يمكنك العودة إلى هذا المثال العام لتحديث ذاكرتك بعد أن استعراض جميع الخطوات بالتفصيل. الاتصال بخادم IMAP احتجنا كائن SMTP للاتصال بخادم SMTP وإرسال البريد الإلكتروني، وبالمثل نحتاج كائن IMAPClient للاتصال بخادم IMAP وتلقي البريد الإلكتروني، ولكن يجب أولًا الحصول على اسم النطاق لخادم IMAP الخاص بمزوّد بريدك الإلكتروني، والذي سيكون مختلفًا عن اسم نطاق خادم SMTP. يوضّح الجدول التالي خوادم IMAP للعديد من مزوّدي البريد الإلكتروني: مزوّد البريد الإلكتروني اسم نطاق خادم IMAP Gmail* اسم النطاق imap.gmail.com Outlook.com/Hotmail.com* اسم النطاق imap-mail.outlook.com Yahoo Mail* اسم النطاق imap.mail.yahoo.com AT&T اسم النطاق imap.mail.att.net Comcast اسم النطاق imap.comcast.net Verizon اسم النطاق incoming.verizon.net ملاحظة: تمنع الإجراءات الأمنية الإضافية شيفرة بايثون من تسجيل الدخول إلى هذه الخوادم التي وضعنا بجانب اسمها المحرف (*) باستخدام الوحدة imapclient. نحصل على اسم النطاق لخادم IMAP، ثم يمكننا استدعاء الدالة imapclient.IMAPClient() لإنشاء كائن IMAPClient. يتطلب معظم مزوّدي البريد الإلكتروني تشفير SSL، لذا مرّر وسيط الكلمات المفتاحية ssl=True إلى هذه الدالة، ولندخل مثلًا ما يلي في الصدفة التفاعلية مع استخدام اسم النطاق الخاص بمزوّدك: >>> import imapclient >>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True) سيحتوي المتغير imapObj على كائن IMAPClient الذي تعيده الدالة imapclient.IMAPClient() في كافة أمثلة الصدفة التفاعلية الموجودة في الأقسام التالية، والعميل Client هو الكائن الذي يتصل بالخادم. تسجيل الدخول إلى خادم IMAP نحصل على كائن IMAPClient، ثم يمكننا استدعاء التابع login() الخاص بهذا الكائن، وتمرير اسم المستخدم (وهو عنوان بريدك الإلكتروني عادةً) وكلمة المرور كسلاسل نصية إلى هذا التابع. >>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD') 'my_email_address@example.com Jane Doe authenticated (Success)' ملاحظة: تذكّر ألّا تكتب كلمة المرور مباشرة في شيفرتك البرمجية، لذا صمّم برنامجك لقبول كلمة المرور التي تعيدها الدالة input(). إذا رفض خادم IMAP اسم المستخدم/كلمة المرور، فسترفع شيفرة بايثون استثناء imaplib.error. البحث عن رسالة البريد الإلكتروني تُعَد عملية استرداد البريد الإلكتروني التي تهمك عمليةً مكونة من خطوتين بعد أن تسجّل الدخول، حيث يجب أولًا تحديد المجلد الذي تريد البحث فيه، ثم يجب استدعاء التابع search() الخاص بكائن IMAPClient وتمرير السلسلة النصية التي تمثّل الكلمات المفتاحية للبحث باستخدام بروتوكول IMAP. تحديد المجلد يحتوي كل حساب تقريبًا على مجلد البريد الوارد INBOX افتراضيًا، ولكن يمكنك أيضًا الحصول على قائمة المجلدات من خلال استدعاء التابع list_folders() الخاص بالكائن IMAPClient، مما يؤدي إلى إعادة قائمة من المجموعات Tuples، حيث تحتوي كل مجموعة على معلومات حول مجلد واحد. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي: >>> import pprint >>> pprint.pprint(imapObj.list_folders()) [(('\\HasNoChildren',), '/', 'Drafts'), (('\\HasNoChildren',), '/', 'Filler'), (('\\HasNoChildren',), '/', 'INBOX'), (('\\HasNoChildren',), '/', 'Sent'), --snip-- (('\\HasNoChildren', '\\Flagged'), '/', 'Starred'), (('\\HasNoChildren', '\\Trash'), '/', 'Trash')] القيم الثلاث في كل مجموعة مثل (('\\HasNoChildren',), '/', 'INBOX') هي كما يلي: مجموعة من رايات Flags المجلد (لن نوضّح في هذا المقال ما تمثله هذه الرايات، ويمكنك تجاهل هذا الحقل). المُحدِّد Delimiter المُستخدَم في سلسلة الاسم النصية لفصل المجلدات الأب عن المجلدات الفرعية. الاسم الكامل للمجلد. يمكنك تحديد مجلدٍ للبحث فيه من خلال تمرير اسم المجلد بوصفه سلسلة نصية إلى التابع select_folder() الخاص بالكائن IMAPClientكما يلي: >>> imapObj.select_folder('INBOX', readonly=True) يمكنك تجاهل القيمة التي يعيدها التابع select_folder()، وإذا كان المجلد المحدَّد غير موجود، فسترفع شيفرة بايثون استثناء imaplib.error. يمنعك وسيط الكلمات المفتاحية readonly=True من إجراء تغييرات أو حذفٍ عن طريق الخطأ على أيٍّ من رسائل البريد الإلكتروني الموجودة في هذا المجلد أثناء استدعاءات التوابع اللاحقة. إن لم تكن ترغب في حذف رسائل البريد الإلكتروني، فيُفضَّل دائمًا ضبط الوسيط readonly على القيمة True. إجراء البحث حدّدنا المجلد، ويمكننا الآن البحث عن رسائل البريد الإلكتروني باستخدام التابع search() الخاص بالكائن IMAPClient، حيث يكون وسيط هذا التابع قائمةً من السلاسل النصية، وتكون كل سلسلة نصية بتنسيق مفاتيح بحث IMAP، حيث سنوضّح في الجدول الآتي مفاتيح البحث Search Keys. لاحظ أن بعض خوادم IMAP قد يكون لها طرق تطبيق مختلفة فيما يتعلق بكيفية التعامل مع الرايات ومفاتيح البحث الخاصة بها، لذا قد يتطلب الأمر بعض التجارب في الصدفة التفاعلية لمعرفة كيف تتصرّف بالضبط. يمكنك تمرير عدة سلاسل نصية لمفاتيح بحث IMAP في وسيط القائمة إلى التابع search()، وتكون الرسائل المُعادة هي الرسائل التي تتطابق مع جميع مفاتيح البحث. إذا أردتَ المطابقة مع أيٍّ من مفاتيح البحث، فاستخدم مفتاح البحث OR، ولاحظ أن مفتاحَ البحث NOT يتبعه مفتاح بحث كامل، وأن مفتاحَ البحث OR يتبعه مفتاحا بحث كاملان. مفتاح البحث معناه 'ALL' يعيد جميع الرسائل الموجودة في المجلد. قد تواجهك قيود حجم الوحدة imaplib إذا طلبت جميع الرسائل الموجودة في مجلد كبير، لذا اطلع على القسم "قيود الحجم" التي سنوضحها لاحقًا. 'BEFORE date'و 'ON date' و 'SINCE date' تعيد مفاتيح البحث الثلاثة هذه الرسائل التي استلمها خادم IMAP قبل التاريخ date المُحدَّد أو فيه أو بعده على التوالي، حيث يجب أن يكون التاريخ بالتنسيق 05-Jul-2023. يطابق مفتاح البحث 'SINCE 05-Jul-2023' الرسائل في تاريخ 5 من الشهر السابع وبعده، ولكن يطابق مفتاح البحث 'BEFORE 05-Jul-2023' الرسائل قبل تاريخ 5 من الشهر السابع فقط دون مطابقة رسائل هذا التاريخ. 'SUBJECT string' و 'BODY string' و 'TEXT string' تعيد الرسائل التي تكون فيها السلسلة النصية string موجودة في موضوع Subject الرسالة أو نصها Body أو أيٍّ منهما على التوالي. إذا احتوت السلسلة النصية string على مسافات، فأحِطها بعلامات اقتباس مزدوجة مثل: 'TEXT "search with spaces"'. 'FROM string' و 'TO string' و 'CC string' و 'BCC string' تعيد جميع الرسائل التي تكون فيها السلسلة النصية string موجودة في عنوان البريد الإلكتروني "من from"، أو عناوين "إلى to"، أو عناوين "cc" (نسخة مطابقة)، أو عناوين "bcc" (نسخة مطابقة مخفية) على التوالي. إذا كانت هناك عناوين بريد إلكتروني متعددة في السلسلة النصية string، فافصل بينها بمسافات وأحِط كلها بعلامات اقتباس مزدوجة مثل: 'CC "firstcc@example.com secondcc@example.com"'. 'SEEN' و 'UNSEEN' تعيد جميع الرسائل مع أو بدون الراية \Seen على التوالي، حيث تحصل رسالة البريد الإلكتروني على الراية \Seen إذا وصلنا إليها باستخدام استدعاء التابع fetch() التي سنوضّحها لاحقًا أو إذا نقرنا عليها عند التحقق من البريد الإلكتروني في برنامج بريد إلكتروني أو متصفح ويب. من الشائع أن نقول أن البريد الإلكتروني "مقروء Read" بدلًا من "مُشاهَد Seen"، لكنهما يعنيان الشيء نفسه. 'ANSWERED' و 'UNANSWERED' تعيد جميع الرسائل مع أو بدون الراية \Answered على التوالي، حيث تحصل الرسالة على الراية \Answered عند الرد عليها. 'DELETED' و 'UNDELETED' تعيد جميع الرسائل مع أو بدون الراية \Deleted على التوالي. تُعطَى رسائل البريد الإلكتروني المحذوفة باستخدام التابع delete_messages() الرايةَ \Deleted ولكنها لا تُحذَف نهائيًا حتى نستدعي التابع expunge() (اطّلع على القسم "حذف رسائل البريد الإلكتروني" التي سنوضّحها لاحقًا). لاحظ أن بعض مزوّدي خدمة البريد الإلكتروني يحذفون نهائيًا Expunge رسائل البريد الإلكتروني تلقائيًا. 'DRAFT' و 'UNDRAFT' تعيد جميع الرسائل مع أو بدون الراية \Draft على التوالي. تُحفَظ عادةً رسائل المسودات في مجلد منفصل هو مجلد المسودات Drafts بدلًا من مجلد البريد الوارد INBOX. 'FLAGGED' و 'UNFLAGGED' تعيد جميع الرسائل مع أو بدون الراية \Flagged على التوالي، حيث تُستخدَم هذه الراية عادةً لوضع علامة على رسائل البريد الإلكتروني بوصفها "مهمة Important" أو "عاجلة Urgent". 'LARGER N' و 'SMALLER N' تعيد جميع الرسائل الأكبر أو الأصغر من N بايت على التوالي. 'NOT search-key' يعيد الرسائل التي لا يعيدها مفتاح البحث search-key. 'OR search-key1 search-key2' يعيد الرسائل التي تطابق مفتاح البحث search-key الأول أو الثاني. إليك فيما يلي بعض الأمثلة على استدعاءات التابع search() مع معانيها: imapObj.search(['ALL']): يعيد جميع الرسائل الموجودة في المجلد المُحدَّد حاليًا. imapObj.search(['ON 05-Jul-2023']): يعيد جميع الرسائل المُرسَلة في 5 من الشهر السادس من عام 2023. imapObj.search(['SINCE 01-Jan-2023', 'BEFORE 01-Feb-2023', 'UNSEEN']): يعيد جميع الرسائل غير المقروءة المُرسَلة في الشهر الأول من عام 2023. لاحظ أن ذلك يعني الرسائل المُرسَلة في 1 من الشهر الأول وما بعده من الشهر الأول ولا يتضمّن 1 من الشهر الثاني. imapObj.search(['SINCE 01-Jan-2023', 'FROM alice@example.com']): يعيد جميع الرسائل المُرسَلة من العنوان alice@example.com منذ بداية عام 2023. imapObj.search(['SINCE 01-Jan-2023', 'NOT FROM alice@example.com']): يعيد جميع الرسائل المُرسَلة من الجميع باستثناء العنوان alice@example.com منذ بداية عام 2023. imapObj.search(['OR FROM alice@example.com FROM bob@example.com']): يعيد جميع الرسائل المُرسَلة من العنوان alice@example.com أو العنوان bob@example.com. imapObj.search(['FROM alice@example.com', 'FROM bob@example.com']): لا يعيد هذا البحث أيّ رسائل مطلقًا، لأن الرسائل يجب أن تتطابق مع جميع كلمات البحث المفتاحية. لا يمكن أن يكون هناك سوى عنوان "من from" واحد فقط، فمن المستحيل أن تكون الرسالة من العنوان alice@example.com والعنوان bob@example.com. لا يعيد التابع search() رسائل البريد الإلكتروني، بل يعيد المعرّفات الفريدة UID لرسائل البريد الإلكتروني بوصفها قيمًا صحيحة. يمكنك بعد ذلك تمرير هذه المعرّفات الفريدة إلى التابع fetch() للحصول على محتوى البريد الإلكتروني. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي: >>> UIDs = imapObj.search(['SINCE 05-Jul-2023']) >>> UIDs [40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041] خزّنا قائمة معرّفات الرسائل (للرسائل المُستلمة في 5 من الشهر السابع وما بعده) التي يعيدها التابع search() في المتغير UIDs. تكون قائمة المعرّفات UIDs المُعادة على حاسوبك مختلفة عن القائمة الموضحة في مثالنا، لأنها فريدة لحساب بريد إلكتروني معين. استخدم قيم المعرّف الفريد UID التي تلقيتها وليس القيم الواردة في هذا المقال عندما تمرّرها لاحقًا إلى استدعاءات دوال أخرى. قيود الحجم إذا تطابق بحثك مع عدد كبير من رسائل البريد الإلكتروني، فقد ترفع شيفرة بايثون الاستثناء imaplib.error: got more than 10000 bytes، وعندها يجب قطع الاتصال بخادم IMAP وإعادة الاتصال به والمحاولة مرة أخرى. وُضِع هذا القيد لمنع برامج بايثون الخاصة بك من استهلاك الكثير من الذاكرة، ولكن يكون الحد الأقصى للحجم الافتراضي غالبًا صغيرًا جدًا، إذ يمكنك تغييره من 10000 بايت إلى 10000000 بايت من خلال تشغيل الشيفرة البرمجية التالية: >>> import imaplib >>> imaplib._MAXLINE = 10000000 يُفترَض أن تمنع الشيفرة البرمجية السابقة ظهور رسالة الخطأ مرة أخرى، لذا قد ترغب في جعل هذين السطرين جزءًا من كل برنامج IMAP تكتبه. جلب بريد إلكتروني ووضع علامة عليه كمقروء يمكنك استدعاء التابع fetch() الخاص بكائن IMAPClient للحصول على محتوى البريد الإلكتروني الفعلي بعد حصولك على قائمة المعرّفات الفريدة UID التي ستكون الوسيط الأول لهذا التابع، والوسيط الثاني هو القائمة ['BODY[]'] التي تطلب من التابع fetch() تنزيل كل المحتوى الخاص بنص رسائل البريد الإلكتروني المُحدَّدة في قائمة المعرّفات الفريدة UID الخاصة بك. لنتابع الآن مثالنا على الصدفة التفاعلية: >>> rawMessages = imapObj.fetch(UIDs, ['BODY[]']) >>> import pprint >>> pprint.pprint(rawMessages) {40040: {'BODY[]': 'Delivered-To: my_email_address@example.com\r\n' 'Received: by 10.76.71.167 with SMTP id ' --snip-- '\r\n' '------=_Part_6000970_707736290.1404819487066--\r\n', 'SEQ': 5430}} استورد الوحدة pprint ومرّر القيمة المُعادة من التابع fetch() والمُخزَّنة في المتغير rawMessages إلى الدالة pprint.pprint() "لطباعتها بمظهر جميل Pretty Print"، وسترى أن هذه القيمة المُعادة هي قاموس متداخل للرسائل ذات المعرفات الفريدة UID بوصفها مفاتيحًا. تُخزَّن كل رسالة كقاموس له مفتاحان هما: 'BODY[]' و 'SEQ'، حيث يُربَط المفتاح 'BODY[]' مع النص الفعلي للبريد الإلكتروني. يُعَد المفتاح 'SEQ' مُخصَّصًا للرقم التسلسلي Sequence Number، والذي له دورٌ مماثل للمعرّف الفريد (UID)، ولكن يمكنك تجاهله. يُعَد محتوى الرسالة الموجود في المفتاح 'BODY[]' غير مفهوم إلى حد كبير، فهو بتنسيق اسمه RFC 822، وهو مصمم لتقرأه خوادم IMAP، ولكن لا حاجة إلى فهم هذا التنسيق، إذ سنوضّحه لاحقًا عند شرح وحدة pyzmail في هذا المقال. استدعينا الدالة select_folder() مع وسيط الكلمات المفتاحية readonly=True عند تحديد مجلد للبحث فيه، حيث يؤدي ذلك إلى منعك من حذف رسالة بريد إلكتروني عن طريق الخطأ، ولكنه يعني أيضًا عدم وضع علامة على رسائل البريد الإلكتروني بوصفها مقروءة إذا جلبتها باستخدام التابع fetch()، لذا إذا أردتَ وضع علامة على رسائل البريد الإلكتروني بوصفها مقروءة عند جلبها، فيجب تمرير الوسيط readonly=False إلى الدالة select_folder(). إذا كان المجلد المُحدَّد في وضع القراءة فقط، فيمكنك إعادة تحديد المجلد الحالي باستدعاء آخر للدالة select_folder() مع وسيط الكلمات المفتاحية readonly=False كما يلي: >>> imapObj.select_folder('INBOX', readonly=False) الحصول على عناوين البريد الإلكتروني من رسالة خام Raw Message لا تُعَد الرسائل الخام التي يعيدها التابع fetch() مفيدة جدًا للأشخاص الذين يريدون قراءة رسائل بريدهم الإلكتروني فقط، لذا تحلّل الوحدة pyzmail هذه الرسائل الخام وتعيدها بوصفها كائنات PyzMessage، مما يجعل أقسام الموضوع والنص والحقل "إلى To" والحقل "من From" والأقسام الأخرى من البريد الإلكتروني قابلة للوصول بسهولة من شيفرة بايثون الخاصة بك. تطبيق عملي: إرسال رسائل البريد الإلكتروني للتذكير الأعضاء بدفع مستحقاتهم لنفترض أنك تطوعتَ لتعقّب دفع الأعضاء لمستحقاتهم في نادي تطوع إلزامي، حيث تُعَد هذه المهمة مملةً جدًا، إذ تتضمّن استخدام جدول بيانات يحتوي جميع الأشخاص الذين دفعوا في كلّ شهر وإرسال رسائل تذكير عبر البريد الإلكتروني إلى الأشخاص الذين لم يدفعوا، وبالتالي ستمر على كامل جدول البيانات بنفسك وتنسخ وتلصق رسالة البريد الإلكتروني نفسها لكلّ مَن تأخر في سداد مستحقاته، إذًا لنكتب سكربتًا ينفّذ هذه المهمة نيابةً عنك. إليك الخطوات العامة التي سيطبّقها برنامجك: قراءة البيانات من جدول بيانات إكسل. البحث عن جميع الأعضاء الذين لم يسدّدوا مستحقاتهم للشهر الأخير. البحث عن عناوين بريدهم الإلكتروني وإرسال رسائل تذكير مُخصَّصة لهم. يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: فتح وقراءة خلايا مستند إكسل باستخدام الوحدة openpyxl كما تعلّمنا في مقالٍ سابق. إنشاء قاموس للأعضاء الذين لم يسددوا مستحقاتهم. تسجيل الدخول إلى خادم SMTP من خلال استدعاء smtplib.SMTP() و ehlo() و starttls() وlogin(). إرسال رسالة تذكير مُخصَّصة عبر البريد الإلكتروني من خلال استدعاء التابع sendmail() إلى جميع الأعضاء الذين لم يسددوا مستحقاتهم. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم sendDuesReminders.py. الخطوة الأولى: فتح ملف إكسل لنفترض أن جدول بيانات إكسل الذي تستخدمه لتعقّب دفعات مستحقات العضوية يشبه الشكل التالي، وهو موجود في ملف اسمه duesRecords.xlsx ويمكنك تنزيله من nostarch. جدول تعقّب دفعات مستحقات الأعضاء يحتوي جدول البيانات على اسم كل عضو وعنوان بريده الإلكتروني، ويكون لكل شهر عمودٌ لتعقّب حالات الدفع الخاصة بالأعضاء، حيث تُميَّز الخلية الخاصة بكل عضو بالكلمة "paid" بعد دفع المستحقات. يجب أن يفتح البرنامج الملف duesRecords.xlsx ويعرف العمود الخاص بالشهر الأخير من خلال قراءة السمة sheet.max_column. اطّلع على المقال الخاص بالتعامل مع جداول بيانات إكسل باستخدام بايثون لمزيد من المعلومات حول الوصول إلى الخلايا في ملفات جداول بيانات إكسل باستخدام الوحدة openpyxl. أدخِل الشيفرة البرمجية التالية في تبويب محرّر ملفاتك: #! python3 # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات import openpyxl, smtplib, sys # فتح جدول البيانات والحصول على حالة المستحقات الأخيرة ➊ wb = openpyxl.load_workbook('duesRecords.xlsx') ➋ sheet = wb.get_sheet_by_name('Sheet1') ➌ lastCol = sheet.max_column ➍ latestMonth = sheet.cell(row=1, column=lastCol).value # التحقق من حالة الدفع لكل عضو # تسجيل الدخول إلى حساب البريد الإلكتروني # إرسال رسائل التذكير عبر البريد الإلكتروني نستورد الوحدات openpyxl و smtplib و sys، ثم نفتح الملف duesRecords.xlsx ونخزّن كائن Workbook الناتج في المتغير wb ➊. نحصل بعد ذلك على الورقة Sheet1 ونخزّن الكائن Worksheet الناتج في المتغير sheet ➋. أصبح لدينا كائن Worksheet، وبالتالي يمكننا الآن الوصول إلى الصفوف والأعمدة والخلايا، حيث نخزّن العمود الأعلى في المتغير lastCol ➌، ثم نستخدم الصف رقم 1 والعمود lastCol للوصول إلى الخلية التي يجب أن تحتوي على الشهر الأخير، حيث نحصل على القيمة الموجودة في هذه الخلية ونخزّنها في المتغير latestMonth ➍. الخطوة الثانية: البحث عن جميع الأعضاء الذين لم يدفعوا مستحقاتهم حدّدنا رقم العمود للشهر الأخير (المُخزَّن في المتغير lastCol)، ويمكننا الآن المرور ضمن حلقة على جميع الصفوف بعد الصف الأول الذي يحتوي على ترويسات الأعمدة لمعرفة الأعضاء الذين يكون لديهم النص "paid" في الخلية الخاصة بمستحقات ذلك الشهر. إن لم يدفع العضو، فيمكنك الحصول على اسم العضو وعنوان بريده الإلكتروني من العمودين 1 و2 على التوالي، حيث ستُدخِل هذه المعلومات في القاموس unpaidMembers الذي سيتعقّب جميع الأعضاء الذين لم يدفعوا في الشهر الأخير. أدخِل الشيفرة البرمجية التالية إلى برنامج sendDuesReminder.py: #! python3 # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات --snip-- # التحقق من حالة الدفع لكل عضو unpaidMembers = {} ➊ for r in range(2, sheet.max_row + 1): ➋ payment = sheet.cell(row=r, column=lastCol).value if payment != 'paid': ➌ name = sheet.cell(row=r, column=1).value ➍ email = sheet.cell(row=r, column=2).value ➎ unpaidMembers[name] = email تُعِد الشيفرة البرمجية السابقة القاموس الفارغ unpaidMembers، ثم تمر ضمن حلقة على جميع الصفوف بعد الصف الأول ➊، ثم تُخزَّن القيمة الموجودة في العمود الأخير ضمن المتغير payment لكل صف ➋. إذا لم يساوِ المتغير payment القيمة 'paid'، فستُخزَّن قيمة العمود الأول في المتغير name ➌، وتُخزَّن قيمة العمود الثاني في المتغير email ➍، ويُضاف المتغيران name و email إلى القاموس unpaidMembers ➎. الخطوة الثالثة: إرسال رسائل تذكير مخصصة عبر البريد الإلكتروني حصلنا على قائمة بجميع الأعضاء غير الدافعين، وحان الوقت الآن لإرسال رسائل تذكير لهؤلاء الأعضاء عبر البريد الإلكتروني. أدخِل الشيفرة البرمجية التالية إلى برنامجك، ولكن مع عنوان بريدك الإلكتروني ومعلومات مزوّدك الحقيقية: #! python3 # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات --snip-- # تسجيل الدخول إلى حساب البريد الإلكتروني smtpObj = smtplib.SMTP('smtp.example.com', 587) smtpObj.ehlo() smtpObj.starttls() smtpObj.login('my_email_address@example.com', sys.argv[1]) أنشئ كائن SMTP من خلال استدعاء الدالة smtplib.SMTP() ومرّر إليها اسم النطاق والمنفذ الخاص بمزوّدك، ثم استدعِ التوابع ehlo() و starttls()، ثم استدعِ التابع login() ومرّر إليه عنوان بريدك الإلكتروني والقائمة sys.argv[1] التي ستخزّن السلسلة النصية لكلمة مرورك، حيث ستدخِل كلمة المرور بوصفها وسيط سطر أوامر في كل مرة تشغّل فيها البرنامج لتجنّب حفظ كلمة مرورك في شيفرتك المصدرية. يسجّل برنامجك الدخول إلى حساب بريدك الإلكتروني، ثم يجب أن يمر على القاموس unpaidMembers ويرسل بريدًا إلكترونيًا مُخصَّصًا إلى عنوان البريد الإلكتروني لكل عضو. أضِف ما يلي إلى برنامج sendDuesReminders.py: #! python3 # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات --snip-- # إرسال رسائل التذكير عبر البريد الإلكتروني for name, email in unpaidMembers.items(): ➊ body = "Subject: %s dues unpaid.\nDear %s,\nRecords show that you have not paid dues for %s. Please make this payment as soon as possible. Thank you!'" % (latestMonth, name, latestMonth) ➋ print('Sending email to %s...' % email) ➌ sendmailStatus = smtpObj.sendmail('my_email_address@example.com', email, body) ➍ if sendmailStatus != {}: print('There was a problem sending email to %s: %s' % (email, sendmailStatus)) smtpObj.quit() تمر الشيفرة البرمجية السابقة ضمن حلقة على الأسماء ورسائل البريد الإلكتروني الموجودة في القاموس unpaidMembers، ونخصّص رسالةً لكل عضو لم يدفع، حيث تتضمّن هذه الرسالة الشهر الأخير واسم العضو، ونخزّنها في المتغير body ➊. نطبع بعد ذلك خرجًا يفيد بأننا نرسل بريدًا إلكترونيًا إلى عنوان البريد الإلكتروني لهذا العضو ➋، ثم نستدعي التابع sendmail()، ونمرّر إليه عنوان البريد الإلكتروني والرسالة المُخصَّصة ➌، ونخزّن القيمة المُعادة في المتغير sendmailStatus. تذكّر أن التابع sendmail() يعيد قيمة قاموس غير فارغ إذا أبلغ خادم SMTP عن خطأ أثناء إرسال هذا البريد الإلكتروني، لذا يتحقق الجزء الأخير من حلقة for ➍ مما إذا كان القاموس المُعاد غير فارغ، فإذا كان كذلك، فسيطبع عنوان البريد الإلكتروني للمستلم والقاموس المُعاد. نستدعي التابع quit() لقطع الاتصال بخادم SMTP بعد أن ينتهي البرنامج من إرسال كافة رسائل البريد الإلكتروني. ستكون النتيجة كما يلي عند تشغيل البرنامج: Sending email to alice@example.com... Sending email to bob@example.com... Sending email to eve@example.com… يتلقّى المستلمون رسالة بريد إلكتروني حول دفعاتهم الفائتة والتي ستشبه البريد الإلكتروني الذي ترسله يدويًا. الخلاصة نتواصل مع بعضنا بعضًا عبر الإنترنت وعبر شبكات الهاتف المحمول باستخدام العديد من الطرق، ولكن يُعَد البريد الإلكتروني والرسائل النصية الطرق الأكثر انتشارًا، حيث يمكن لبرامجك التواصل عبر هذه القنوات، مما يمنحها ميزات إشعارات جديدة. يمكنك أيضًا كتابة برامج تعمل على حواسيب مختلفة وتتواصل مع بعضها بعضًا مباشرةً عبر البريد الإلكتروني، حيث يرسل أحد البرامج رسائل البريد الإلكتروني باستخدام بروتوكول SMTP بينما يستردها البرنامج الآخر باستخدام بروتوكول IMAP. توفّر وحدة smtplib الخاصة بلغة بايثون دوالًا لاستخدام بروتوكول SMTP لإرسال رسائل البريد الإلكتروني عبر خادم SMTP الخاص بمزوّد بريدك الإلكتروني، وتتيح الوحدتان imapclient و pyzmail الخارجيتان الوصولَ إلى خوادم IMAP واسترداد رسائل البريد الإلكتروني المرسلة إليك. يُعَد بروتوكول IMAP أكثر تعقيدًا من بروتوكول SMTP، ولكنه قوي جدًا ويسمح بالبحث عن رسائل بريد إلكتروني معينة وتنزيلها وتحليلها لاستخراج موضوع ونص البريد الإلكتروني بوصفها قيمًا نوعها سلسلة نصية. لا تسمح بعض خدمات البريد الإلكتروني الشائعة مثل جيميل بأن تستخدم بروتوكولات SMTP و IMAP المعيارية للوصول إلى خدماتها كإجراء احترازي من الرسائل غير المرغوب بها وبهدف الأمان، ولكن تعمل وحدة EZGmail بمثابة مغلِّف مناسب لواجهة برمجة تطبيقات جيميل، مما يسمح لسكربتات بايثون الخاصة بك بالوصول إلى حسابك على جيميل. نوصي بشدة بإعداد حساب جيميل منفصل لاستخدام سكربتاتك حتى لا تتسبب الأخطاء المحتملة في برنامجك في حدوث مشكلات لحسابك الشخصي على جيميل. ترجمة -وبتصرُّف- للقسم Sending Email من مقال Sending Email and Text Messages لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: جدولة المهام وتشغيل برامج أخرى باستخدام لغة بايثون قراءة مستندات جداول إكسل باستخدام لغة بايثون Python الكتابة في مستندات إكسل باستخدام لغة بايثون Python
-
تعرّفنا في المقال السابق على كيفية الحصول على الوقت باستخدام الوحدتين time و datetime في لغة بايثون، وسنوضّح في هذا المقال كيفية كتابة البرامج التي تشغّل Launch برامجًا أخرى وفقًا لجدولٍ زمني محدّد باستخدام وحدتي subprocess و threading، فأسرع طريقة لكتابة البرامج في أغلب الأحيان هي الاستفادة من التطبيقات التي كتبها أشخاص آخرون مسبقًا. تعدّد الخيوط Multithreading لنفترض أنك تريد جدولة شيفرتك البرمجية لتشغيلها بعد تأخير محدّد أو في وقت معيَّن، حيث يمكنك إضافة شيفرة برمجية كما يلي في بداية برنامجك: import time, datetime startTime = datetime.datetime(2029, 10, 31, 0, 0, 0) while datetime.datetime.now() < startTime: time.sleep(1) print('Program now starting on Halloween 2029') --snip-- تحدّد الشيفرة البرمجية السابقة وقت البدء في 31 من الشهر العاشر من عام 2029، وتستمر في استدعاء الدالة time.sleep(1) حتى الوصول إلى وقت البدء، ولا يستطيع برنامجك فعل أيّ شيء أثناء انتظار انتهاء حلقة استدعاءات time.sleep()، ويبقى متوقفًا حتى يوم الهالوين من عام 2029، لأن برامج بايثون افتراضيًا لها خيط Thread تنفيذ واحد. يمكنك فهم ما هو خيط التنفيذ من خلال تذكّر ما ناقشناه في مقالٍ سابق حول التحكم في التدفق عندما تخيّلت تنفيذ برنامجٍ ما على أنه وضع إصبعك على سطرٍ من الشيفرة البرمجية في برنامجك والانتقال إلى السطر التالي أو المكان التي ترسله تعليمة التحكم في التدفق، حيث يحتوي البرنامج ذو الخيط الواحد Single-threaded على إصبع واحد فقط، ولكن يحتوي البرنامج متعدد الخيوط Multithreaded على أصابع متعددة. يستمر كل إصبع في التحرك إلى السطر التالي من الشيفرة البرمجية كما تحدِّده تعليمات التحكم في التدفق، ولكن يمكن أن تكون الأصابع في أماكن مختلفة في البرنامج لتنفيذ أسطر مختلفة من الشيفرة البرمجية في الوقت ذاته. لاحظ أن جميع البرامج التي مرّت معنا حتى الآن ذات خيط واحد. يمكنك تنفيذ الشيفرة البرمجية المؤجَّلة أو المجدولة في خيط منفصل باستخدام وحدة بايثون threading بدلًا من أن تنتظر شيفرتك البرمجية بأكملها انتهاء الدالة time.sleep()، حيث سيتوقف الخيط المنفصل مؤقتًا عند استدعاءات time.sleep، بينما يمكن لبرنامجك تنفيذ أشياء أخرى في الخيط الأصلي. ننشئ خيطًا منفصلًا من خلال إنشاء كائن Thread أولًا باستخدام استدعاء الدالة threading.Thread(). إذًا لندخِل الشيفرة البرمجية التالية في ملفٍ جديد ونحفظه بالاسم threadDemo.py: import threading, time print('Start of program.') ➊ def takeANap(): time.sleep(5) print('Wake up!') ➋ threadObj = threading.Thread(target=takeANap) ➌ threadObj.start() print('End of program.') عرّفنا في الشيفرة البرمجية السابقة الدالة التي نريد استخدامها في خيطٍ جديد ➊، واستدعينا الدالة threading.Thread() ومرّرنا لها وسيط الكلمات المفتاحية target=takeANap ➋ لإنشاء كائن Thread، وهذا يعني أن الدالة التي نريد استدعاءها في الخيط الجديد هي takeANap(). لاحظ أن وسيط الكلمات المفتاحية Keyword Argument الذي هو target=takeANap وليس target=takeANap()، لأنك تريد تمرير الدالة takeANap() كوسيط، ولا تريد استدعاءها وتمرير قيمتها المُعادة. خزّنا الكائن Thread الذي أنشأته الدالة threading.Thread() في المتغير threadObj، ثم استدعينا الدالة threadObj.start() ➌ لإنشاء الخيط الجديد والبدء في تنفيذ الدالة المستهدفة في الخيط الجديد. سيكون الناتج كما يلي عند تشغيل هذا البرنامج: Start of program. End of program. Wake up! قد يكون الخرج السابق مربكًا بعض الشيء، حيث إذا كانت التعليمة print('End of program.') هي السطر الأخير من البرنامج، فقد تعتقد أنه آخر شيء سيُطبَع، ولكن تُشغَّل الدالة المستهدفة للمتغير threadObj في خيط تنفيذ جديد عند استدعاء الدالة threadObj.start()، لذا تظهر العبارة Wake up! في النهاية. فكّر في الأمر كإصبعٍ ثانٍ يظهر في بداية الدالة takeANap()، حيث يستمر الخيط الرئيسي في تنفيذ التعليمة print('End of program.')، بينما يتوقف الخيط الجديد الذي كان ينفّذ استدعاء time.sleep(5) مؤقتًا لمدة 5 ثوانٍ، ويطبع عبارة 'Wake up!' بعد أن يستيقظ من غفوته لمدة 5 ثوان، ثم يعود من الدالة takeANap()، وبالتالي فإن عبارة 'Wake up!' هي آخر ما يطبعه البرنامج زمنيًا. ينتهي البرنامج عادةً عند تشغيل السطر الأخير من الشيفرة البرمجية في الملف أو عند استدعاء الدالة sys.exit()، ولكن يحتوي البرنامج threadDemo.py على خيطين هما: الأول هو الخيط الأصلي الذي بدأ في بداية البرنامج وينتهي بعد التعليمة print('End of program.')، والخيط الثاني الذي ينشأ عند استدعاء الدالة threadObj.start() ويبدأ عند بداية الدالة takeANap() وينتهي بعد العودة من الدالة takeANap(). لن ينتهي برنامج بايثون حتى تنتهي جميع خيوطه. لاحظ أن الخيط الثاني لا يزال ينفّذ الاستدعاء time.sleep(5) عند تشغيل البرنامج threadDemo.py بالرغم من انتهاء الخيط الأصلي. تمرير الوسطاء إلى الدالة المستهدفة للخيط إذا أخذت الدالة المستهدفة التي تريد تشغيلها في الخيط الجديد وسطاءً، فيمكنك تمرير وسطائها إلى الدالة threading.Thread(). لنفترض مثلًا أنك تريد تشغيل استدعاء الدالة print() التالية في خيطها الخاص: >>> print('Cats', 'Dogs', 'Frogs', sep=' & ') Cats & Dogs & Frogs يحتوي استدعاء الدالة print() السابق على ثلاث وسطاء عادية هي: 'Cats' و 'Dogs' و 'Frogs' ووسيط كلمات مفتاحية واحد هو sep=' & '، حيث يمكن تمرير الوسطاء العادية كقائمة إلى وسيط الكلمات المفتاحية args في الدالة threading.Thread()، ويمكن تحديد وسيط الكلمات المفتاحية كقاموس لوسيط الكلمات المفتاحية kwargs في الدالة threading.Thread(). لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import threading >>> threadObj = threading.Thread(target=print, args=['Cats', 'Dogs', 'Frogs'], kwargs={'sep': ' & '}) >>> threadObj.start() Cats & Dogs & Frogs نتأكد من تمرير الوسطاء 'Cats' و 'Dogs' و 'Frogs' إلى الدالة print() في الخيط الجديد من خلال تمرير args=['Cats', 'Dogs', 'Frogs'] إلى الدالة threading.Thread()، ونتأكد من تمرير وسيط الكلمات المفتاحية sep=' & ' إلى الدالة print() في الخيط الجديد من خلال تمرير kwargs={'sep': '& '} إلى الدالة threading.Thread(). يؤدي استدعاء الدالة threadObj.start() إلى إنشاء خيط جديد لاستدعاء الدالة print()، وستمرّر إليها 'Cats' و 'Dogs' و 'Frogs' كوسطاء والقيمة ' & ' لوسيط الكلمات المفتاحية sep. تُعَد الطريقة التالية غير صحيحة لإنشاء الخيط الجديد الذي يستدعي الدالة print(): threadObj = threading.Thread(target=print('Cats', 'Dogs', 'Frogs', sep=' & ')) تؤدي الطريقة السابقة إلى استدعاء الدالة print() وتمرير القيمة المُعادة الخاصة بها كوسيط الكلمات المفتاحية target، حيث تكون القيمة المُعادة الخاصة بالدالة print() دائمًا None، ولا تؤدي إلى تمرير الدالة print() نفسها، لذا استخدم وسطاء الكلمات المفتاحية args و kwargs الخاصة بالدالة threading.Thread() عند تمرير الوسطاء إلى دالة في خيطٍ جديد. مشاكل التزامن Concurrency يمكنك بسهولة إنشاء عدة خيوط جديدة وتشغيلها جميعًا في الوقت نفسه، ولكن يمكن أن تسبّب الخيوط المتعددة أيضًا مشكلات في التزامن التي تحدث عندما تقرأ الخيوطُ المتغيرات وتكتبها في الوقت نفسه، مما يؤدّي إلى تصادم الخيوط مع بعضها البعض. قد يكون من الصعب إعادة إنتاج مشكلات التزامن بصورة متناسقة، مما يصعّب تنقيح أخطائها Debug. تُعَد البرمجة متعددة الخيوط موضوعًا واسعًا ولن نناقشه في هذا المقال، ولكن ما عليك أن تضعه في بالك هو أنه يجب ألّا تسمح أبدًا لخيوط متعددة بقراءة أو كتابة المتغيرات نفسها لتجنب مشكلات التزامن. تأكّد من أن الدالة المستهدفة لكائن Thread الجديد عند إنشائه تستخدم المتغيرات المحلية فقط في تلك الدالة، مما سيؤدّي إلى تجنب مشكلات التزامن التي يصعب تنقيح أخطائها في برامجك. تطبيق عملي: برنامج متعدد الخيوط لتنزيل قصص XKCD الهزلية Comics كتبتَ في مقالٍ سابق برنامجًا ينزّل جميع قصص XKCD الهزلية من موقع XKCD، حيث كان برنامجًا له خيط واحد، لأنه ينزّل قصة هزلية واحدة في كل مرة. يقضي هذا البرنامج الكثير من وقت التشغيل في إنشاء اتصال بالشبكة لبدء التنزيل وكتابة الصور المُنزَّلة على القرص الصلب، وإذا كان لديك اتصال إنترنت له حيز نطاق تراسلي عريض، فلن يستخدم برنامجك ذو الخيط الواحد هذا الحيز المتوفر بأكمله. يحتوي البرنامج متعدد الخيوط على بعض الخيوط التي تنزّل القصص الهزلية، وتنشئ بعض الخيوط الأخرى الاتصالات وتكتب ملفات الصور الهزلية على القرص الصلب، حيث يستخدم هذا البرنامج اتصال الإنترنت الخاص بك بكفاءة أكبر وينزّل مجموعة القصص الهزلية بسرعة أكبر. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم threadedDownloadXkcd.py، وستعدّل هذا البرنامج لإضافة خيوط متعددة، فالشيفرة البرمجية المُعدَّلة بالكامل متاحة للتنزيل على nostarch. الخطوة الأولى: تعديل البرنامج لاستخدام دالة سيكون هذا البرنامج في أغلبه مشابهًا لشيفرة التنزيل البرمجية التي كتبناها في مقالٍ سابق، لذا سنتخطّى شرح requests وشيفرة المكتبة Beautiful Soup. التغييرات الرئيسية التي يجب أن تجريها هي استيراد الوحدة threading وإنشاء الدالة downloadXkcd() التي تأخذ أرقام البداية والنهاية للقصة الهزلية كمعاملاتٍ لها. سيؤدّي استدعاء الدالة downloadXkcd(140, 280) مثلًا إلى تكرار شيفرة التنزيل لتنزيل القصص الهزلية 140 و 141 و 142 وحتى القصة الهزلية 279. سيستدعي كلُّ خيط تنشئه الدالةَ downloadXkcd() ويمرّر مجالًا مختلفًا من القصص الهزلية لتنزيلها. أضِف الشيفرة البرمجية التالية إلى برنامج threadedDownloadXkcd.py الخاص بك: #! python3 # threadedDownloadXkcd.py - تنزيل قصص XKCD الهزلية باستخدام خيوط متعددة import requests, os, bs4, threading ➊ os.makedirs('xkcd', exist_ok=True) # تخزين القصص الهزلية في المجلد ./xkcd ➋ def downloadXkcd(startComic, endComic): ➌ for urlNumber in range(startComic, endComic): # تنزيل الصفحة print('Downloading page https://xkcd.com/%s...' % (urlNumber)) ➍ res = requests.get('https://xkcd.com/%s' % (urlNumber)) res.raise_for_status() ➎ soup = bs4.BeautifulSoup(res.text, 'html.parser') # البحث عن عنوان URL للصورة الهزلية ➏ comicElem = soup.select('#comic img') if comicElem == []: print('Could not find comic image.') else: ➐ comicUrl = comicElem[0].get('src') # تنزيل الصورة print('Downloading image %s...' % (comicUrl)) ➑ res = requests.get('https:' + comicUrl) res.raise_for_status() # حفظ الصورة في المجلد ./xkcd imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb') for chunk in res.iter_content(100000): imageFile.write(chunk) imageFile.close() # إنشاء وبدء كائنات الخيط Thread # انتظار انتهاء جميع الخيوط نستورد الوحدات التي نحتاجها، ثم ننشئ مجلدًا لتخزين القصص الهزلية ➊، ونبدأ بتعريف الدالة downloadxkcd() ➋، ثم نمر ضمن حلقة على جميع الأرقام الموجودة في المجال المحدَّد ➌ وننزّل كل صفحة ➍. نستخدم المكتبة Beautiful Soup للبحث في شيفرة HTML لكل صفحة ➎ والعثور على الصورة الهزلية ➏، حيث إذا لم نعثر على صورة هزلية في الصفحة، فإننا نطبع رسالة، وإلّا سنحصل على عنوان URL للصورة ➐ وننزّلها ➑. أخيرًا، نحفظ الصورة في المجلد الذي أنشأناه. الخطوة الثانية: إنشاء وبدء الخيوط عرّفنا الدالة downloadxkcd() وسننشئ الآن الخيوط المتعددة التي يستدعي كل منها الدالة downloadxkcd() لتنزيل مجالات مختلفة من القصص الهزلية من موقع XKCD. أضِف الشيفرة البرمجية التالية إلى البرنامج threadedDownloadXkcd.py بعد تعريف الدالة downloadxkcd(): #! python3 # threadedDownloadXkcd.py - تنزيل قصص XKCD الهزلية باستخدام خيوط متعددة --snip-- # إنشاء وبدء كائنات الخيط Thread downloadThreads = [] # قائمة بجميع كائنات الخيط Thread for i in range(0, 140, 10): # التكرار 14 مرة وإنشاء 14 خيطًا start = i end = i + 9 if start == 0: start = 1 # لا توجد قصة هزلية رقمها 0، لذا اضبط المتغير على القيمة 1 downloadThread = threading.Thread(target=downloadXkcd, args=(start, end)) downloadThreads.append(downloadThread) downloadThread.start() ننشئ أولًا قائمة فارغة downloadThreads، والتي ستساعدنا على تعقّب العديد من كائنات Thread التي سننشئها، ثم نبدأ حلقة for، حيث ننشئ في كل تكرار من هذه الحلقة كائن Thread باستخدام الدالة threading.Thread()، ونلحِق هذا الكائن بالقائمة، ونستدعي التابع start() لبدء تشغيل الدالة downloadXkcd() في الخيط الجديد. تضبط حلقة for المتغير i على القيم من 0 إلى 140 بخطوة مقدارها 10، لذا يجب ضبط المتغير i على القيمة 0 في التكرار الأول، وعلى القيمة 10 في التكرار الثاني، وعلى القيمة 20 في التكرار الثالث، وإلخ. نمرّر الوسيط args=(start, end) إلى الدالة threading.Thread()، لذا سيكون الوسيطان المُمرَّران إلى الدالة downloadXkcd() هما 1 و9 في التكرار الأول، و10 و19 في التكرار الثاني، و20 و29 في التكرار الثالث، وإلخ. سينتقل الخيط الرئيسي إلى التكرار التالي من حلقة for وينشئ الخيط التالي عند استدعاء التابع start() الخاص بالكائن Thread ويبدأ الخيط الجديد بتشغيل الشيفرة البرمجية الموجودة ضمن الدالة downloadXkcd(). الخطوة الثالثة: انتظار انتهاء جميع الخيوط يتحرك الخيط الرئيسي كالمعتاد بينما تنزّل الخيوطُ الأخرى التي أنشأناها القصصَ الهزلية، ولكن لنفترض أن هناك بعض التعليمات البرمجية التي لا تريد تشغيلها في الخيط الرئيسي حتى يكتمل تنفيذ جميع الخيوط الأخرى، حيث سيتوقّف استدعاء التابع join() الخاص بالكائن Thread حتى انتهاء هذا الخيط. يمكن للخيط الرئيسي استدعاء التابع join() على كلٍّ من الخيوط الأخرى باستخدام حلقة for للتكرار على كافة كائنات Thread الموجودة في القائمة downloadThreads. أضِف الآن ما يلي إلى نهاية برنامجك: #! python3 # threadedDownloadXkcd.py - تنزيل قصص XKCD الهزلية باستخدام خيوط متعددة --snip-- # الانتظار حتى انتهاء جميع الخيوط for downloadThread in downloadThreads: downloadThread.join() print('Done.') لن تُطبَع السلسلة النصية 'Done.' حتى إعادة جميع استدعاءات التابع join()، حيث إذا اكتمل كائن Thread عند استدعاء التابع join() الخاص به، فسيعود التابع مباشرةً ببساطة. إذا أردتَ توسيع هذا البرنامج باستخدام شيفرة برمجية تُشغَّل فقط بعد تنزيل كافة القصص الهزلية، فيمكنك وضع شيفرتك البرمجية الجديدة مكان السطر print('Done.'). تشغيل Launching برامج أخرى من برنامج بايثون يمكن لبرنامج بايثون الخاص بك بدء برامج أخرى على حاسوبك باستخدام الدالة Popen() الموجودة في الوحدة المُدمَجة subprocess، حيث يرمز الحرف P في اسم هذه الدالة إلى العملية Process. إذا كان لديك نسخ Instances متعددة من تطبيق مفتوح، فستُعَد كلّ نسخة من هذه النسخ عمليةً منفصلة للبرنامج نفسه، فمثلًا إذا فتحتَ نوافذ متعددة لمتصفح الويب في الوقت نفسه، فإن كلّ نافذة من تلك النوافذ هي عملية مختلفة لبرنامج متصفح الويب. يوضّح الشكل التالي مثالًا لعمليات آلة حاسبة متعددة مفتوحة في وقتٍ واحد: ست عمليات مُشغَّلة لبرنامج الآلة الحاسبة نفسه يمكن أن يكون لكل عملية خيوط متعددة، ولا يمكن للعملية قراءة وكتابة متغيرات عملية أخرى مباشرةً على عكس الخيوط. إذا افترضنا أن برنامجًا متعدد الخيوط يحتوي على أصابع متعددة تتبع الشيفرة البرمجية، فإن فتح عمليات متعددة للبرنامج نفسه يشبه وجود صديق لديه نسخة منفصلة من شيفرة البرنامج البرمجية، وكلاكما تنفّذان البرنامج نفسه بصورة مستقلة. إذا أردتَ بدء برنامج خارجي من سكربت بايثون الخاص بك، فمرّر اسم ملف البرنامج إلى الدالة subprocess.Popen(). انقر بزر الفأرة الأيمن على عنصر القائمة "ابدأ Start" الخاص بالتطبيق وحدّد الخيار "خصائص Properties" لعرض اسم ملف التطبيق في نظام ويندوز، وانقر مع الضغط على مفتاح CTRL على التطبيق وحدّد الخيار "إظهار محتويات الحزمة Show Package Contents" للعثور على مسار الملف القابل للتنفيذ في نظام ماك macOS. ستعيد بعد ذلك الدالة Popen() مباشرةً، وضع في بالك أن البرنامج الذي شغّلناه لا يعمل في الخيط نفسه لبرنامج بايثون الخاص بك. أدخِل ما يلي في الصدفة التفاعلية على حاسوبٍ يعمل بنظام ويندوز: >>> import subprocess >>> subprocess.Popen('C:\\Windows\\System32\\calc.exe') <subprocess.Popen object at 0x0000000003055A58> أدخِل ما يلي على نظام يعمل بنظام أوبنتو لينكس Ubuntu Linux: >>> import subprocess >>> subprocess.Popen('/snap/bin/gnome-calculator') <subprocess.Popen object at 0x7f2bcf93b20> تختلف العملية قليلًا على نظام ماك macOS، لذا اطّلع على قسم "فتح الملفات باستخدام التطبيقات الافتراضية" من هذا المقال لمزيد من التفاصيل. تكون القيمة المُعادة هي كائن Popen الذي له تابعان مفيدان هما: poll() و wait()، حيث يمكنك التفكير في التابع poll() بأنك تسأل سائقك "هل وصلنا؟" مرارًا وتكرارًا حتى الوصول إلى وِجهتك، ويعيد هذا التابع القيمة None إذا كانت العملية لا تزال قيد التشغيل في وقت استدعاء هذا التابع. إذا أُنهي البرنامج، فسيعيد العدد الصحيح لرمز الخروج exit code الخاص بالعملية، حيث يُستخدَم رمز الخروج للإشارة إلى أن العملية انتهت بدون أخطاء (رمز الخروج 0) أو ما إذا قد تسبّب خطأٌ ما في إنهاء العملية (رمز خروج غير صفري قيمته 1 عادةً، ولكنه قد يختلف اعتمادًا على البرنامج). يشبه التابع wait() الانتظار حتى يصل السائق إلى وِجهتك، حيث يوقِف هذا التابع التنفيذ حتى تنتهي العملية التي شغّلناها، ويُعَد ذلك مفيدًا إذا أردتَ أن يتوقف برنامجك مؤقتًا حتى ينتهي المستخدم من البرنامج الآخر. القيمة المُعادة من التابع wait() هي العدد الصحيح لرمز الخروج الخاص بالعملية. أدخِل ما يلي في الصدفة التفاعلية على نظام ويندوز، ولاحظ أن استدعاء التابع wait() سيوقِف التنفيذ حتى إنهاء برنامج الرسام MS Paint الذي شغّلناه: >>> import subprocess ➊ >>> paintProc = subprocess.Popen('c:\\Windows\\System32\\mspaint.exe') ➋ >>> paintProc.poll() == None True ➌ >>> paintProc.wait() # لن يعود حتى إغلاق برنامج الرسام MS Paint 0 >>> paintProc.poll() 0 فتحنا في المثال السابق عملية برنامج الرسام MS Paint ➊، وتحقّقنا مما إذا كان التابع poll() يعيد القيمة None ➋ بينما لا تزال العملية قيد التشغيل، حيث ينبغي أن يكون ذلك صحيحًا لأنها لا تزال قيد التشغيل. أغلقنا بعد ذلك برنامج الرسام MS Paint واستدعينا التابع wait() للعملية المنتهية ➌. سيعيد الآن التابعان wait() و poll() القيمة 0، مما يشير إلى أن العملية قد انتهت بدون أخطاء. ملاحظة: إذا شغّلت برنامج الآلة الحاسبة calc.exe على نظام ويندوز 10 باستخدام الدالة subprocess.Popen()، فستلاحظ أن التابع wait() يعيد مباشرةً على عكس برنامج الرسام mspaint.exe بالرغم من أن تطبيق الآلة الحاسبة لا يزال قيد التشغيل، والسبب أن برنامج الآلة الحاسبة calc.exe يطلِق تطبيق الآلة الحاسبة ثم يغلق نفسه مباشرةً. يُعَد برنامج الآلة الحاسبة الخاص بنظام ويندوز "تطبيق متجر مايكروسوفت موثوق به"، ولن ندخل في تفاصيله في هذا المقال، ولكن يكفي أن نقول أنه يمكن تشغيل البرامج بعدة طرقٍ خاصة بالتطبيق وبنظام التشغيل. تمرير وسطاء سطر الأوامر إلى الدالة Popen() يمكنك تمرير وسطاء سطر الأوامر إلى العمليات التي تنشئها باستخدام الدالة Popen() من خلال تمرير قائمة تمثّل الوسيط الوحيد لهذه الدالة. ستكون السلسلة النصية الأولى في هذه القائمة هي اسم الملف التنفيذي للبرنامج الذي تريد تشغيله، وتكون جميع السلاسل النصية اللاحقة وسطاء سطر الأوامر التي تمرّرها إلى البرنامج عندما يبدأ. تمثّل هذه القائمة قيمة sys.argv للبرنامج الذي شغّلناه. لا تستخدم معظم التطبيقات ذات واجهة المستخدم الرسومية Graphical User Interface -أو GUI اختصارًا- وسطاء سطر الأوامر على نطاق واسع كما تفعل البرامج المستندة إلى سطر الأوامر أو البرامج المستندة إلى الطرفية Terminal، ولكن تقبل معظم تطبيقات واجهة المستخدم الرسومية وسيطًا واحدًا للملف الذي ستفتحه التطبيقات مباشرةً عندما تبدأ. إذا استخدمتَ نظام ويندوز، فأنشئ مثلًا ملفًا نصيًا بسيطًا بالاسم C:\Users\Al\hello.txt، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> subprocess.Popen(['C:\\Windows\\notepad.exe', 'C:\\Users\Al\\hello.txt']) <subprocess.Popen object at 0x00000000032DCEB8> لن يؤدي ذلك إلى تشغيل تطبيق المفكرة Notepad فحسب، بل سيؤدي أيضًا إلى فتح الملف C:\Users\Al\hello.txt مباشرةً. أدوات مجدول المهام Task Scheduler و Launchd و cron إذا كنت خبيرًا في استخدام الحاسوب، فقد تكون على دراية بأداة مجدول المهام Task Scheduler على ويندوز أو أداة launchd على نظام ماك macOS أو أداة الجدولة cron على نظام لينكس، حيث تتيح لك هذه الأدوات المُوثَّقة جيدًا والموثوقة جدولةَ تشغيل التطبيقات في أوقات محددة. يوفّر عليك استخدام المجدول المُدمَج مع نظام تشغيلك كتابةَ الشيفرة البرمجية الخاصة بالتحقق من ساعتك لجدولة برامجك، ولكن يمكنك استخدام الدالة time.sleep() إذا أردتَ فقط إيقاف البرنامج مؤقتًا لفترة وجيزة، أو يمكنك تكرار شيفرتك البرمجية حتى تاريخ ووقت محدَّدين واستدعاء time.sleep(1) في كل مرة خلال الحلقة بدلًا من استخدام المجدول الخاص بنظام التشغيل. فتح المواقع باستخدام شيفرة بايثون يمكن للدالة webbrowser.open() تشغيل متصفح ويب من برنامجك إلى موقع ويب محدّد بدلًا من فتح تطبيق المتصفح باستخدام الدالة subprocess.Popen(). تشغيل سكربتات بايثون الأخرى يمكنك تشغيل سكربت بايثون من شيفرة بايثون أخرى مثل أيّ تطبيق آخر، فما عليك فعله سوى تمرير ملف بايثون python.exe القابل للتنفيذ إلى الدالة Popen() واسم ملف سكربت .py الذي تريد تشغيله بوصفه وسيطًا لهذه الدالة، فمثلًا سيشغّل ما يلي السكربت hello.py الذي كتبناه في مقالٍ سابق: >>> subprocess.Popen(['C:\\Users\\<YOUR USERNAME>\\AppData\\Local\\Programs\\ Python\\Python38\\python.exe', 'hello.py']) <subprocess.Popen object at 0x000000000331CF28> نمرّر إلى الدالة Popen() قائمةً تحتوي على سلسلة نصية تمثل مسار ملف بايثون القابل للتنفيذ وسلسلة نصية تمثّل اسم ملف السكربت، وإذا احتاج السكربت الذي تشغّله إلى وسطاء سطر الأوامر، فيمكنك إضافتها إلى القائمة بعد اسم ملف السكربت. موقع ملف بايثون القابل للتنفيذ على نظام ويندوز هو: C:\Users\<YOUR USERNAME>\AppData\Local\Programs\Python\Python38\python.exe، وعلى نظام ماك macOS هو /Library/Frameworks/Python.framework/Versions/3.8/bin/python3، وعلى نظام لينكس هو: /usr/bin/python3.8 إذا شغّل برنامج بايثون الخاص بك برنامجَ بايثون آخر، فسيُشغَّل كلا البرنامجين ضمن عمليات منفصلة ولن يتمكن كلّ منهما من مشاركة متغيرات الآخر على عكس استيراد برنامج بايثون بوصفه وحدة Module. فتح الملفات باستخدام التطبيقات الافتراضية سيؤدّي النقر المزدوج على ملف .txt على حاسوبك إلى تشغيل التطبيق المرتبط بلاحقة الملف .txt تلقائيًا، وسيكون لحاسوبك ارتباطات متعددة بامتدادات الملفات المُعَدّة مسبقًا، ويمكن لبايثون أيضًا فتح الملفات بهذه الطريقة باستخدام الدالة Popen(). يحتوي كل نظام تشغيل على برنامج يطبّق شيئًا يعادل النقر المزدوج على ملف مستند لفتحه، وهو البرنامج start على نظام ويندوز، والبرنامج open على نظام ماك macOS، والبرنامج see على نظام أوبنتو لينكس. أدخِل مثلًا ما يلي في الصدفة التفاعلية مع تمرير 'start' أو 'open' أو 'see' إلى الدالة Popen() اعتمادًا على نظامك: >>> fileObj = open('hello.txt', 'w') >>> fileObj.write('Hello, world!') 12 >>> fileObj.close() >>> import subprocess >>> subprocess.Popen(['start', 'hello.txt'], shell=True) كتبنا في مثالنا السابق عبارة Hello, world! في ملف hello.txt جديد، ثم استدعينا الدالة Popen()، ومرّرنا إليها قائمة تحتوي على اسم البرنامج (وهو 'start' في مثالنا لنظام ويندوز) واسم الملف. مرّرنا أيضًا وسيط الكلمات المفتاحية shell=True، ويُعَد هذا الوسيط مطلوبًا فقط على نظام ويندوز. يعرِف نظام التشغيل جميع ارتباطات الملفات ويمكنه معرفة أنه يجب عليه تشغيل برنامج المفكرة Notepad.exe مثلًا للتعامل مع الملف hello.txt. يُستخدَم البرنامج open لفتح ملفات المستندات والبرامج على نظام ماك macOS. إذًا لندخِل ما يلي في الصدفة التفاعلية إذا كان حاسوبك عليه نظام ماك، ويجب أن يفتح تطبيق الآلة الحاسبة: >>> subprocess.Popen(['open', '/Applications/Calculator.app/']) <subprocess.Popen object at 0x10202ff98> تطبيق عملي: برنامج بسيط للعد التنازلي يصعب العثور على تطبيق بسيط للمؤقت الزمني، وبالمثل قد يكون من الصعب العثور على تطبيق بسيط للعد التنازلي، إذًا لنكتب برنامجًا للعد التنازلي يشغّل المنبه في نهاية العد التنازلي. إليك الخطوات العامة التي سيفعلها برنامجك: العد التنازلي من العدد 60. تشغيل ملف صوتي alarm.wav عندما يصل العد التنازلي إلى الصفر. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: التوقف مؤقتًا لمدة ثانية واحدة بين عرض كل عدد في العد التنازلي من خلال استدعاء الدالة time.sleep(). استدعاء الدالة subprocess.Popen() لفتح الملف الصوتي باستخدام التطبيق الافتراضي. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم countdown.py. الخطوة الأولى: العد التنازلي يتطلب هذا البرنامج الوحدة time لاستخدام الدالة time.sleep() ووحدة subprocess لاستخدام الدالة subprocess.Popen(). أدخِل الآن الشيفرة البرمجية التالية واحفظ الملف بالاسم countdown.py: #! python3 # countdown.py - سكربت بسيط للعد التنازلي import time, subprocess ➊ timeLeft = 60 while timeLeft > 0: ➋ print(timeLeft, end='') ➌ time.sleep(1) ➍ timeLeft = timeLeft - 1 # تشغيل الملف الصوتي في نهاية العد التنازلي استوردنا الوحدتين time و subprocess، ثم أنشأنا متغيرًا بالاسم timeLeft ليحتفظ بعدد الثواني المتبقية في العد التنازلي ➊. يمكن أن تبدأ عند القيمة 60، أو يمكنك تغيير القيمة إلى ما تريده، أو يمكنك ضبطها من وسيط سطر الأوامر. يمكنك في حلقة while عرض العدد المتبقي ➋، والتوقف مؤقتًا لمدة ثانية واحدة ➌، ثم إنقاص المتغير timeLeft بمقدار 1 ➍ قبل بدء الحلقة مرة أخرى، وستستمر الحلقة في التكرار طالما أن المتغير timeLeft أكبر من 0، ثم سينتهي العد التنازلي. الخطوة الثانية: تشغيل الملف الصوتي توجد وحدات خارجية لتشغيل الملفات الصوتية بتنسيقات مختلفة، ولكن الطريقة السريعة والسهلة لذلك هي تشغيل أيّ تطبيق يستخدمه المستخدم لتشغيل الملفات الصوتية. سيكتشف نظام التشغيل من امتداد الملف .wav التطبيقَ الذي يجب تشغيله لتشغيل الملف، ويمكن أن يكون ملف .wav له أحد تنسيقات الملفات الصوتية الأخرى مثل .mp3 أو .ogg. يمكنك استخدام أيّ ملف صوتي موجود على حاسوبك لتشغيله في نهاية العد التنازلي، أو يمكنك تنزيل alarm.wav. أضِف ما يلي إلى شيفرتك البرمجية: #! python3 # countdown.py - سكربت بسيط للعد التنازلي import time, subprocess --snip-- # تشغيل الملف الصوتي في نهاية العد التنازلي subprocess.Popen(['start', 'alarm.wav'], shell=True) سيعمل الملف alarm.wav (أو الملف الصوتي الذي تختاره) بعد انتهاء الحلقة لإعلام المستخدم بانتهاء العد التنازلي. تأكّد من تضمين 'start' في القائمة التي تمرّرها إلى الدالة Popen() وتمرير وسيط الكلمات المفتاحية shell=True على نظام ويندوز، وتأكّد من تمرير 'open' بدلًا من 'start' وإزالة shell=True على نظام ماك macOS. يمكنك حفظ ملف نصي في مكانٍ ما مع رسالة مثل الرسالة "انتهى وقت الاستراحة!" بدلًا من تشغيل ملف صوتي، حيث يمكنك استخدام الدالة Popen() لفتحه في نهاية العد التنازلي، مما يؤدي إلى إنشاء نافذة منبثقة تحتوي على رسالة بفعالية، أو يمكنك استخدام الدالة webbrowser.open() لفتح موقع ويب محدَّد في نهاية العد التنازلي. يمكن أن يكون منبه برنامج العد التنازلي الخاص بك أيّ شيء تريده على عكس بعض تطبيقات العد التنازلي المجانية التي تجدها على الإنترنت. أفكار لبرامج مماثلة يمثّل العد التنازلي تأخيرًا بسيطًا قبل مواصلة تنفيذ البرنامج، ويمكنك استخدامه أيضًا لتطبيقات وميزات أخرى كما يلي: استخدام الدالة time.sleep() لمنح المستخدم فرصة الضغط على الاختصار CTRL-C لإلغاء إجراءٍ ما مثل حذف الملفات. يمكن لبرنامجك طباعة رسالة "اضغط على CTRL-C للإلغاء Press CTRL-C to cancel"، ثم معالجة أيّ استثناءات KeyboardInterrupt باستخدام تعليمات try و except. يمكنك استخدام كائنات timedelta مع العد التنازلي طويل الأجل لقياس عدد الأيام والساعات والدقائق والثواني حتى نقطة ما (مثل عيد ميلاد أو ذكرى سنوية) في المستقبل. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر من خلال استخدام المعلومات التي حصلتَ عليها من المقال السابق وهذا المقال. برنامج المؤقت الزمني ولكن بمظهر أجمل وسّع مشروع المؤقت الزمني Stopwatch الذي ناقشناه في المقال السابق من خلال استخدام توابع السلاسل النصية rjust() و ljust() "لتجميل" الخرج، فبدلًا من الخرج التالي: Lap #1: 3.56 (3.56) Lap #2: 8.63 (5.07) Lap #3: 17.68 (9.05) Lap #4: 19.11 (1.43) سيبدو الخرج كما يلي: Lap # 1: 3.56 ( 3.56) Lap # 2: 8.63 ( 5.07) Lap # 3: 17.68 ( 9.05) Lap # 4: 19.11 ( 1.43) لاحظ أنك ستحتاج إلى نسخٍ نوعها سلاسل نصية من المتغيرات lapNum و lapTime و totalTime التي نوعها أعداد صحيحة وأعداد عشرية لاستدعاء توابع السلاسل النصية عليها. استخدم بعد ذلك وحدة pyperclip التي وضّحناها في مقالٍ سابق لنسخ الخرج النصي إلى الحافظة حتى يتمكّن المستخدم من لصق الخرج بسرعة في ملف نصي أو بريد إلكتروني. برنامج لتنزيل القصص الهزلية المجدول على الويب اكتب برنامجًا يفحص مواقع الويب الخاصة بالعديد من القصص الهزلية على الويب وينزّل الصور تلقائيًا عند تحديث القصص الهزلية عن آخر زيارة للبرنامج. يمكن لمجدول نظام تشغيلك -مثل Scheduled Tasks على ويندوز وlaunchd على نظام ماكmacOS، وcron على نظام لينكس- تشغيل برنامج بايثون الخاص بك مرة واحدة يوميًا، ويمكن لبرنامج بايثون نفسه تنزيل القصة الهزلية ثم نسخها إلى سطح المكتب بحيث يسهل العثور عليها، مما يحرّرك من الاضطرار إلى التحقق من موقع الويب بنفسك للتأكد من تحديثه. الخلاصة تُستخدَم الوحدة threading لإنشاء خيوط متعددة، والتي تُعَد مفيدةً عندما تريد تنزيل ملفات متعددة أو إنجاز مهام أخرى في وقت واحد، ولكن تأكد من أن الخيط يقرأ ويكتب المتغيرات المحلية فقط، وإلا فقد تواجه مشكلات في التزامن. يمكن لبرامج بايثون الخاصة بك تشغيل تطبيقات أخرى باستخدام الدالة subprocess.Popen()، حيث يمكن تمرير وسطاء سطر الأوامر إلى استدعاء هذه الدالة لفتح مستندات محددة باستخدام التطبيق. يمكنك أيضًا استخدام برنامج start أو open أو see مع الدالة Popen() لاستخدام ارتباطات الملفات الخاصة بحاسوبك لمعرفة التطبيق الذي سيُستخدَم لفتح مستند تلقائيًا، ويمكن لبرامج بايثون الخاصة بك الاستفادة من إمكاناتها لتلبية احتياجات الأتمتة الخاصة بك باستخدام التطبيقات الأخرى الموجودة على حاسوبك. ترجمة -وبتصرُّف- للقسم Scheduling Tasks, and Launching Programs من مقال Keeping Time, Scheduling Tasks, and Launching Programs لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: استخراج الوقت باستخدام الوحدتين time و datetime في لغة بايثون قراءة مستندات جداول إكسل باستخدام لغة بايثون Python مشاريع بايثون عملية تناسب المبتدئين
-
قد تشغّل برامجك أثناء جلوسك أمام الحاسوب، ولكنك ستفضّل تشغيلها دون إشرافك المباشر، إذ يمكن لساعة حاسوبك جدولة البرامج لتشغيل الشيفرة البرمجية في وقت وتاريخ محدَّد أو على فترات زمنية منتظمة، فمثلًا يمكن أن يستخلص Scrape برنامجك البيانات من موقع ويب كل ساعة للتحقق من التغييرات أو يجري مهمةً تستخدم وحدة المعالجة المركزية بصورة كبيرة في الساعة 4 صباحًا أثناء نومك، حيث توفر وحدتا time و datetime في لغة بايثون الدوال التي تقدّم هذه الوظائف. الوحدة time تُضبَط ساعة نظام حاسوبك على تاريخٍ ووقت ومنطقة زمنية محددة، حيث تسمح الوحدة time المُدمَجة لبرامج بايثون الخاصة بك بقراءة ساعة النظام للوقت الحالي، وتُعَد الدالتان time.time() و time.sleep() الأكثر فائدة في هذه الوحدة. الدالة time.time() توقيت يونيكس Unix Epoch هو مرجع زمني شائع الاستخدام في البرمجة، وهو الساعة 12 صباحًا في 1 من الشهر الأول من عام 1970 بالتوقيت العالمي المنسق Coordinated Universal Time -أو UTC اختصارًا، حيث تعيد الدالة time.time() عدد الثواني منذ تلك اللحظة التي تمثّل توقيت يونيكس بوصفها قيمةً عشرية Float، والتي تُعَد مجرد عددٍ مع فاصلة عشرية، ويسمى هذا العدد الذي تعيده الدالة time.time() بعلامة يونيكس الزمنية Epoch Timestamp. أدخِل ما يلي مثلًا في الصدفة التفاعلية Interactive Shell: >>> import time >>> time.time() 1543813875.3518236 استدعينا الدالة time.time() في 2 من الشهر 12 من عام 2018 في الساعة 9:11 مساءً بتوقيت المحيط الهادئ، وتكون القيمة المُعادة هي عدد الثواني التي مرّت بين توقيت يونيكس ولحظة استدعاء الدالة time.time(). يمكن استخدام علامات يونيكس الزمنية لفحص أداء Profile الشيفرة البرمجية، أي قياس المدة التي يستغرقها تشغيل جزء من هذه الشيفرة البرمجية. إذا استدعيتَ الدالة time.time() في بداية مقطع الشيفرة البرمجية الذي تريد قياس المدة التي يستغرقها تشغيله وفي نهايته مرةً أخرى، فيمكنك طرح العلامة الزمنية الأولى من الثانية لإيجاد الوقت المنقضي بين هذين الاستدعاءين. افتح تبويبًا جديدًا في محرّرك لإنشاء ملفٍ جديد وأدخِل البرنامج التالي مثلًا: import time ➊ def calcProd(): # حساب حاصل ضرب أول 100,000 عدد product = 1 for i in range(1, 100000): product = product * i return product ➋ startTime = time.time() prod = calcProd() ➌ endTime = time.time() ➍ print('The result is %s digits long.' % (len(str(prod)))) ➎ print('Took %s seconds to calculate.' % (endTime - startTime)) نعرّف الدالة calcProd() ➊ للتكرار ضمن حلقة على الأعداد الصحيحة من 1 إلى 99,999 وإعادة ناتج ضربها. نستدعي الدالة time.time() ونخزّنها في المتغير startTime ➋، ثم نستدعي الدالة time.time() مرةً أخرى بعد استدعاء الدالة calcProd() مباشرةً ونخزّنها في المتغير endTime ➌، ثم نطبع عدد أرقام حاصل الضرب الذي تعيده الدالة calcProd() ➍ والمدة التي استغرقها تشغيل هذه الدالة ➎. احفظ البرنامج السابق بالاسم calcProd.py وشغّله، حيث سيبدو خرجه كما يلي: The result is 456569 digits long. Took 2.844162940979004 seconds to calculate. ملاحظة: هناك طريقة أخرى لفحص أداء شيفرتك البرمجية باستخدام الدالة cProfile.run() التي توفّر مستوًى أعلى من التفاصيل بدلًا من استخدام تقنية الدالة time.time() البسيطة. اطلّع على مقال قياس أداء وسرعة تنفيذ شيفرة بايثون لمزيدٍ من التفاصيل عن الدالة cProfile.run(). تُعَد القيمة المُعادة من الدالة time.time() مفيدةً للحصول على عدد الثواني منذ توقيت يونيكس إلى لحظة استدعائها بوصفها قيمةً عشرية، ولكن لا يستطيع البشر قراءتها، لذا توجد دالة أخرى هي الدالة time.ctime() التي تعيد سلسلةً نصية تمثّل وصفًا للوقت الحالي. يمكنك أيضًا اختياريًا تمرير عدد الثواني منذ توقيت يونيكس الذي تعيده الدالة time.time() إلى الدالة time.ctime() للحصول على قيمة السلسلة النصية التي تمثّل ذلك الوقت. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import time >>> time.ctime() 'Mon Jun 15 14:00:38 2023' >>> thisMoment = time.time() >>> time.ctime(thisMoment) 'Mon Jun 15 14:00:45 2023' الدالة time.sleep() إذا أردتَ إيقاف برنامجك مؤقتًا لفترة من الوقت، فاستدعِ الدالة time.sleep() ومرّر إليها عدد الثواني التي تريد أن يبقى فيها برنامجك متوقفًا مؤقتًا. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import time >>> for i in range(3): ➊ print('Tick') ➋ time.sleep(1) ➌ print('Tock') ➍ time.sleep(1) Tick Tock Tick Tock Tick Tock ➎ >>> time.sleep(5) تطبع حلقة for الكلمة Tick ➊، وتتوقف مؤقتًا لمدة ثانية واحدة ➋، ثم تطبع الكلمة Tock ➌، وتتوقف مؤقتًا لمدة ثانية واحدة ➍، ثم تطبع الكلمة Tick، وتتوقف مؤقتًا، وهكذا حتى طباعة كلٍّ من الكلمتين Tick و Tock ثلاث مرات. تُعَد الدالة time.sleep() مُعطِّلة، أي أنها لن تعيد شيئًا ولن تحرّر برنامجك لتنفيذ شيفرة برمجية أخرى إلّا بعد انقضاء عدد الثواني التي مررتها إلى الدالة time.sleep()، فمثلًا إذا أدخلتَ time.sleep(5) ➎، فلن تظهر تعليمة المطالبة التالية (>>>) إلّا بعد مرور 5 ثوانٍ. تقريب الأعداد سترى في أغلب الأحيان عند التعامل مع الأوقات قيمًا عشرية تحتوي على العديد من الأعداد بعد الفاصلة العشرية، حيث يمكن تسهيل التعامل مع هذه القيم من خلال تقصيرها باستخدام الدالة round() المُدمَجة مع لغة بايثون، والتي تقرّب الأعداد العشرية إلى الدقة التي تحدّدها، حيث تدخِل العدد الذي تريد تقريبه، بالإضافة إلى وسيط ثانٍ اختياري يمثّل عدد الأعداد بعد الفاصلة العشرية التي تريد تقريبه إليها. إذا حذفتَ الوسيط الثاني، فستقرّب الدالة round() العدد إلى أقرب عدد صحيح كامل بدون فاصلة عشرية. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import time >>> now = time.time() >>> now 1543814036.6147408 >>> round(now, 2) 1543814036.61 >>> round(now, 4) 1543814036.6147 >>> round(now) 1543814037 استوردنا الوحدة time وخزّنا الدالة time.time() في المتغير now، ثم استدعينا الدالة round(now, 2) لتقريب now إلى عددين بعد الفاصلة العشرية، واستدعينا الدالة round(now, 4) للتقريب إلى أربعة أعداد بعد الفاصلة العشرية، واستدعينا الدالة round(now) للتقريب إلى أقرب عدد صحيح. تطبيق عملي: برنامج المؤقت الزمني الفائق Super Stopwatch لنفترض أنك تريد تعقّب مقدار الوقت الذي تقضيه لإنجاز المهام المملة التي لم تؤتمتها بعد، فليس لديك مؤقت زمني فعلي ومن الصعب العثور على تطبيق مؤقت زمني مجاني لحاسوبك المحمول أو هاتفك الذكي غير مملوءٍ الإعلانات ولا يرسل نسخة من سجل متصفحك إلى المسوّقين، فمذكور أن هذا التطبيق يمكنه ذلك في اتفاقية ترخيصه التي وافقت عليها ولم تقرأها على الأغلب. إذًا لنكتب برنامج مؤقت زمني بسيط باستخدام لغة بايثون. إليك الخطوات العامة التي سيطبقها برنامجك: تعقّب مقدار الوقت المنقضي بين الضغطات على مفتاح ENTER، حيث تبدأ كل ضغطة "دورةً Lap" جديدة في المؤقت. طباعة رقم الدورة والوقت الإجمالي ووقت الدورة. يجب أن تطبق شيفرتك البرمجية الخطوات التالية: إيجاد الوقت الحالي من خلال استدعاء الدالة time.time() وتخزينه بوصفه علامة زمنية في بداية البرنامج، وفي بداية كل دورة أيضًا. الاحتفاظ بعدّاد دورات وزيادته في كل مرة يضغط فيها المستخدم على مفتاح ENTER. حساب الوقت المنقضي من خلال طرح العلامات الزمنية. معالجة الاستثناء KeyboardInterrupt حتى يتمكّن المستخدم من الضغط على الاختصار CTRL-C للإنهاء. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم stopwatch.py. الخطوة الأولى: إعداد البرنامج لتعقّب الأوقات يحتاج برنامج المؤقت الزمني إلى استخدام الوقت الحالي، لذا يجب استيراد الوحدة time، ويجب أن يطبع برنامجك أيضًا بعض التعليمات المختصَرة للمستخدم قبل استدعاء الدالة input()، إذ يمكن أن يبدأ المؤقت بعد أن يضغط المستخدم على مفتاح ENTER، ثم ستبدأ الشيفرة البرمجية في تعقّب أوقات الدورات. أدخِل الآن الشيفرة البرمجية التالية في محرّر ملفاتك، واكتب تعليقاتٍ في النهاية، والتي تمثّل عناصر بديلة لما تبقى من شيفرتك البرمجية: #! python3 # stopwatch.py - برنامج مؤقت زمني بسيط import time # عرض تعليمات البرنامج print('Press ENTER to begin. Afterward, press ENTER to "click" the stopwatch. Press Ctrl-C to quit.') input() # الضغط على مفتاح Enter للبدء print('Started.') startTime = time.time() # الحصول على وقت بدء الدورة الأولى lastTime = startTime lapNum = 1 # البدء بتعقّب أوقات الدورات كتبنا الشيفرة البرمجية لعرض التعليمات للمستخدم وبدء الدورة الأولى وتسجيل الوقت وضبط عدد الدورات على القيمة 1. الخطوة الثانية: تعقّب أوقات الدورات وطباعتها لنكتب الآن الشيفرة البرمجية لبدء دورات جديدة، وحساب المدة التي استغرقتها الدورة السابقة، وحساب إجمالي الوقت المنقضي منذ بدء المؤقت الزمني، حيث سنعرض وقت الدورة والوقت الإجمالي ونزيد عدد الدورات بمقدار 1 عند كل دورة جديدة. إذًا أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # stopwatch.py - برنامج مؤقت زمني بسيط import time --snip-- # البدء بتعقّب أوقات الدورات ➊ try: ➋ while True: input() ➌ lapTime = round(time.time() - lastTime, 2) ➍ totalTime = round(time.time() - startTime, 2) ➎ print('Lap #%s: %s (%s)' % (lapNum, totalTime, lapTime), end='') lapNum += 1 lastTime = time.time() # إعادة ضبط وقت الدورة الأخيرة ➏ except KeyboardInterrupt: # معالجة الاستثناء Ctrl-C لمنع عرض رسالة الخطأ print('\nDone.') إذا ضغط المستخدم على الاختصار CTRL-C لإيقاف المؤقت الزمني، فسيظهر الاستثناء KeyboardInterrupt، وسيتعطل البرنامج إذا لم يكن تنفيذه موجودًا ضمن تعليمة try، لذا غلّفنا هذا الجزء من البرنامج ضمن تعليمة try ➊، وسنعالج الاستثناء ضمن التعليمة except ➏، لذا ينتقل تنفيذ البرنامج إلى التعليمة except لطباعة الكلمة Done بدلًا من رسالة الخطأ KeyboardInterrupt عند الضغط على الاختصار CTRL-C ورفع الاستثناء. يكون التنفيذ ضمن حلقة لا نهائية ➋ حتى حدوث الاستثناء، حيث تستدعي هذه الحلقة الدالة input() وتنتظر حتى يضغط المستخدم على مفتاح ENTER لإنهاء الدورة. إذا انتهت الدورة، فسنحسب المدة التي استغرقتها الدورة من خلال طرح وقت بدء الدورة lastTime من الوقت الحالي time.time() ➌، ويمكننا حساب إجمالي الوقت المنقضي من خلال طرح وقت البدء الإجمالي للمؤقت الزمني startTime من الوقت الحالي ➍. ستحتوي نتائج حسابات الوقت على العديد من الأعداد بعد الفاصلة العشرية مثل العد 4.766272783279419، لذا استخدمنا الدالة round() لتقريب القيمة العشرية إلى عددين في التعلمتين ➌ و➍. نطبع بعد ذلك رقم الدورة والوقت الإجمالي المنقضي ووقت الدورة ➎. يطبع المستخدم، الذي يضغط على مفتاح ENTER لاستدعاء الدالة input()، سطرًا جديدًا على الشاشة، لذا مرّر الوسيط end='' إلى الدالة print() لتجنب مضاعفة المسافة بين المخرجات. نتجهّز للدورة التالية بعد طباعة معلومات الدورة من خلال إضافة القيمة 1 إلى العدّاد lapNum ونضبط المتغير lastTime على الوقت الحالي الذي يمثّل وقت بدء الدورة التالية. أفكار لبرامج مماثلة يفتح تعقّب الوقت العديدَ من الاحتمالات أمام برامجك، حيث يمكنك كتابة هذه البرامج بنفسك بالرغم من أنه يمكنك تنزيل تطبيقاتٍ تنفّذ بعضًا منها، ولكن ستكون البرامج التي تكتبها بنفسك مجانية وغير مملوءة بالإعلانات والميزات عديمة الفائدة، لذلك جرّب كتابة برامج مماثلة تطبّق ما يلي: إنشاء تطبيق جدول حضور زمني Timesheet بسيط يسجّل متى كتبتَ اسم شخصٍ ما ويستخدم الوقت الحالي لتسجيل زمن دخوله أو خروجه. إضافة ميزة إلى برنامجك لعرض الوقت المنقضي منذ بدء عمليةٍ ما مثل عملية التنزيل التي تستخدم الوحدة requests. التحقّق خلال فترات زمنية متقطعة من مدة تشغيل البرنامج ومنح المستخدم فرصة لإلغاء المهام التي تستغرق وقتًا طويلًا. الوحدة datetime تُعَد الوحدة time مفيدةً للحصول على علامة يونيكس الزمنية للعمل معها، ولكن إذا أردتَ عرض التاريخ بتنسيق أسهل أو إجراء العمليات الحسابية باستخدام التواريخ مثل معرفة التاريخ الذي كان قبل 205 يومًا أو التاريخ الذي سيكون بعد 123 يومًا من الآن، فيجب أن تستخدم الوحدة datetime. تمتلك الوحدة datetime نوع البيانات datetime الخاص بها، حيث تمثل القيم التي نوعها datetime لحظةً محددة من الزمن. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import datetime ➊ >>> datetime.datetime.now() ➋ datetime.datetime(2024, 2, 27, 11, 10, 49, 55, 53) ➌ >>> dt = datetime.datetime(2023, 10, 21, 16, 29, 0) ➍ >>> dt.year, dt.month, dt.day (2023, 10, 21) ➎ >>> dt.hour, dt.minute, dt.second (16, 29, 0) يؤدي استدعاء الدالة datetime.datetime.now() ➊ إلى إعادة الكائن datetime ➋ للتاريخ والوقت الحاليين وفقًا لساعة حاسوبك، حيث يتضمن هذا الكائن السنة والشهر واليوم والساعة والدقيقة والثانية والميكروثانية للحظة الحالية. يمكنك أيضًا استرداد الكائن datetime للحظة معينة باستخدام الدالة datetime.datetime() ➌ وتمرير أعداد صحيحة إليها، والتي تمثّل السنة والشهر واليوم والساعة والدقيقة والثانية للحظة التي تريدها، وتُخزَّن هذه الأعداد الصحيحة في سمات Attributes خاصة بالكائن datetime، وهذه السمات هي year و month و day ➍ و hour و minute و second ➎. يمكن تحويل علامة يونيكس الزمنية إلى كائن datetime باستخدام الدالة datetime.datetime.fromtimestamp()، ويُحوَّل التاريخ والوقت الخاصين بكائن datetime إلى المنطقة الزمنية المحلية. إذًا أدخِل ما يلي في الصدفة التفاعلية: >>> import datetime, time >>> datetime.datetime.fromtimestamp(1000000) datetime.datetime(1970, 1, 12, 5, 46, 40) >>> datetime.datetime.fromtimestamp(time.time()) datetime.datetime(2023, 10, 21, 16, 30, 0, 604980) يؤدي استدعاء الدالة datetime.datetime.fromtimestamp() وتمرير القيمة 1000000 إليها إلى إعادة كائن datetime للحظة التي تكون بعد 1,000,000 ثانية من توقيت يونيكس، بينما يؤدي تمرير الدالة time.time()، التي تمثّل علامة يونيكس الزمنية للحظة الحالية، إلى الدالة datetime.datetime.fromtimestamp() إلى إعادة كائن datetime للحظة الحالية، إذ يفعل التعبيران datetime.datetime.now() و datetime.datetime.fromtimestamp(time.time()) الشيء نفسه، حيث يعطيان كائن datetime للوقت الحالي. يمكنك مقارنة كائنات datetime مع بعضها البعض باستخدام عوامل المقارنة لمعرفة أيّ منها يسبق الآخر، حيث يكون لكائن datetime الأحدث القيمة الأكبر. لندخِل الآن ما يلي في الصدفة التفاعلية: ➊ >>> halloween2023 = datetime.datetime(2023, 10, 31, 0, 0, 0) ➋ >>> newyears2024 = datetime.datetime(2024, 1, 1, 0, 0, 0) >>> oct31_2023 = datetime.datetime(2023, 10, 31, 0, 0, 0) ➌ >>> halloween2023 == oct31_2023 True ➍ >>> halloween2023 > newyears2024 False ➎ >>> newyears2024 > halloween2023 True >>> newyears2024 != oct31_2023 True أنشأنا كائن datetime للحظة الأولى (منتصف الليل) من 31 من الشهر العاشر من عام 2023، وخزّناه في المتغير halloween2023 ➊، ثم أنشأنا كائن datetime للحظة الأولى من 1 من الشهر الأول من عام 2024، وخزّناه في المتغير newyears2024 ➋، ثم أنشأنا كائنًا آخر لمنتصف ليل يوم 31 من الشهر العاشر من عام 2023، وخزّناه في المتغير oct31_2023. تُظهِر المقارنة بين المتغيرين halloween2023 و oct31_2023 أنهما متساويان ➌، وتُظهِر مقارنة المتغيرين newyears2024 و halloween2023 أن newyears2024 أكبر (أو أحدث) من halloween2023 ➍ ➎. نوع البيانات timedelta توفر الوحدة datetime أيضًا نوع البيانات timedelta الذي يمثل مدة زمنية بدلًا من توفير لحظة زمنية. لندخِل الآن ما يلي في الصدفة التفاعلية: ➊ >>> delta = datetime.timedelta(days=11, hours=10, minutes=9, seconds=8) ➋ >>> delta.days, delta.seconds, delta.microseconds (11, 36548, 0) >>> delta.total_seconds() 986948.0 >>> str(delta) '11 days, 10:09:08' نستخدم الدالة datetime.timedelta() لإنشاء كائن timedelta، حيث تأخذ هذه الدالة وسطاء الكلمات المفتاحية Keyword Arguments التي هي weeks و days و hours و minutes و seconds و milliseconds و microseconds، ولا توجد وسطاء الكلمات المفتاحية month و year، إذ يُعَد الشهر أو السنة مقدارًا متغيرًا من الزمن اعتمادًا على شهرٍ أو سنة معينة. يحتوي الكائن timedelta على المدة الإجمالية المُمثَّلة بالأيام والثواني والميكروثانية، حيث تُخزَّن هذه الأعداد في السمات days و seconds و microseconds. يعيد التابع total_seconds() المدة بعدد الثواني فقط، بينما يؤدي تمرير كائن timedelta إلى الدالة str() إلى إعادة تمثيل سلسلة نصية مُنسَّقة جيدًا وقابلة للقراءة البشرية لهذا الكائن. مرّرنا في المثال السابق وسطاء الكلمات المفتاحية إلى الدالة datetime.timedelta() لتحديد مدة 11 يومًا و10 ساعات و9 دقائق و8 ثوانٍ، وخزّنا كائن timedelta المُعاد في المتغير delta ➊. تخزِّن السمة days الخاصة بكائن timedelta القيمة 11، وتخزن السمة seconds القيمة 36548 (أي 10 ساعات و9 دقائق و8 ثوانٍ من خلال التعبير عنها بالثواني) ➋. يخبرنا استدعاء الدالة total_seconds() أن 11 يومًا و10 ساعات و9 دقائق و8 ثوانٍ هي 986,948 ثانية، ويعيد تمرير كائن timedelta إلى الدالة str() سلسلةً نصية تشرح المدة بوضوح. يمكن استخدام المعاملات الحسابية لإجراء عملية حسابية للتاريخ على قيم datetime، فمثلًا أدخِل ما يلي في الصدفة التفاعلية لحساب التاريخ بعد 1000 يوم من الآن: >>> dt = datetime.datetime.now() >>> dt datetime.datetime(2018, 12, 2, 18, 38, 50, 636181) >>> thousandDays = datetime.timedelta(days=1000) >>> dt + thousandDays datetime.datetime(2021, 8, 28, 18, 38, 50, 636181) أنشأنا أولًا كائن datetime للحظة الحالية وخزّناه في المتغير dt، ثم أنشأنا كائن timedelta لمدة 1000 يوم وخزّناه في المتغير thousandDays، ثم جمعنا dt و thousandDays للحصول على كائن datetime للتاريخ بعد 1000 يوم من الآن. تجري شيفرة بايثون عملية حسابية للتاريخ لمعرفة أن 1000 يوم بعد 2 من الشهر 12 من عام 2018 ستكون في 18 من الشهر الثامن من عام 2021. يُعَد ذلك مفيدًا لأنه يجب أن تتذكّر عدد الأيام في كل شهر والعامل المشترك للسنوات الكبيسة وغيرها من التفاصيل الصعبة عندما تحسب 1000 يوم بعد تاريخ معين، لذا تعالج الوحدة datetime جميع تلك الأمور نيابةً عنك. يمكن جمع كائنات timedelta أو طرحها من كائنات datetime أو كائنات timedelta الأخرى باستخدام المعاملَين + و -، ويمكن ضرب كائن timedelta أو قسمته على عدد صحيح أو قيم عشرية باستخدام المعاملَين * و /. لندخِل مثلًا ما يلي في الصدفة التفاعلية: ➊ >>> oct21st = datetime.datetime(2019, 10, 21, 16, 29, 0) ➋ >>> aboutThirtyYears = datetime.timedelta(days=365 * 30) >>> oct21st datetime.datetime(2019, 10, 21, 16, 29) >>> oct21st - aboutThirtyYears datetime.datetime(1989, 10, 28, 16, 29) >>> oct21st - (2 * aboutThirtyYears) datetime.datetime(1959, 11, 5, 16, 29) أنشأنا كائن datetime ليوم 21 من الشهر العاشر من عام 2019 ➊، وأنشأنا كائن timedelta لمدة 30 عامًا تقريبًا بافتراض أن السنة تتكون من 365 يومًا ➋. يؤدي طرح aboutThirtyYears من oct21st إلى الحصول على كائن datetime للتاريخ قبل 30 عامًا من تاريخ 21 من الشهر العاشر من عام 2019، ويؤدي طرح 2 * aboutThirtyYears من oct21st إلى إعادة كائن datetime للتاريخ قبل 60 عامًا من تاريخ 21 من الشهر العاشر من عام 2019. الإيقاف المؤقت حتى تاريخ محدد يتيح التابع time.sleep() إيقافَ برنامجٍ ما مؤقتًا لعددٍ محدّدٍ من الثواني، حيث يمكنك إيقاف برامجك مؤقتًا حتى تاريخ محدّد باستخدام حلقة while، فمثلًا ستستمر الشيفرة البرمجية التالية في التكرار حتى يوم الهالوين من عام 2024: import datetime import time halloween2024 = datetime.datetime(2024, 10, 31, 0, 0, 0) while datetime.datetime.now() < halloween2016: time.sleep(1) سيؤدي استدعاء time.sleep(1) إلى إيقاف برنامج بايثون الخاص بك مؤقتًا حتى لا يضيع الحاسوب دورات معالجة لوحدة المعالجة المركزية بعد التحقق من الوقت مرارًا وتكرارًا، لذا تتحقق حلقة while من الشرط مرة واحدة في الثانية وتستمر إلى باقي البرنامج بعد يوم الهالوين من عام 2024 (أو أيّ تاريخ تبرمجه للتوقف). تحويل كائنات datetime إلى سلاسل نصية لا تُعَد علامات يونيكس الزمنية وكائنات datetime سهلة القراءة، لذا نستخدم التابع strftime() لعرض كائن datetime بوصفها سلسلة نصية، حيث يشير الحرف f الموجود في اسم الدالة strftime() إلى التنسيق format. يستخدم التابع strftime() موجّهات Directives مشابهة لتنسيق سلاسل بايثون النصية، حيث يحتوي الجدول التالي على قائمة كاملة بموجّهات التابع strftime(): موجّه التابع strftime() معناه %Y العام مع القرن مثل '2024' %y العام بدون القرن من '00' إلى '99' (من عام 1970 إلى 2069 مثلًا) %m الشهر كعدد عشري من '01' إلى '12' %B اسم الشهر كاملًا مثل 'November' %b اسم الشهر المختصر مثل 'Nov' %d يوم من الشهر من '01' إلى '31' %j يوم من السنة من '001' إلى '366' %w يوم من الأسبوع من '0' (الأحد) إلى '6' (السبت) %A الاسم الكامل ليوم من الأسبوع مثل 'Monday' %a الاسم المختصر ليوم من الأسبوع مثل 'Mon' %H الساعة (نظام 24 ساعة) من '00' إلى '23' %I الساعة (نظام 12 ساعة) من '01' إلى '12' %M الدقيقة من '00' إلى '59' %S الثانية من '00' إلى '59' %p صباحًا 'AM' أو مساءً 'PM' %% المحرف '%' حرفيًا مرّر سلسلة نصية ذات تنسيقٍ مخصَّص تحتوي على موجّهات التنسيق (مع أيّ خطوط مائلة ونقطتين وغير ذلك) إلى التابع strftime()، وسيعيد هذا التابع معلومات كائن datetime بوصفها سلسلة نصية مُنسَّقة. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> oct21st = datetime.datetime(2023, 10, 21, 16, 29, 0) >>> oct21st.strftime('%Y/%m/%d %H:%M:%S') '2023/10/21 16:29:00' >>> oct21st.strftime('%I:%M %p') '04:29 PM' >>> oct21st.strftime("%B of '%y") "October of '23" خزّنا في المثال السابق كائن datetime ليوم 21 من الشهر العاشر من عام 2023 في الساعة 4:29 مساءً في المتغير oct21st. يعيد تمرير سلسلة التنسيق المخصَّصة '%Y/%m/%d %H:%M:%S' إلى التابع strftime() سلسلةً نصية تحتوي على القيم 2023 و10 و21 المفصولة بخطوط مائلة والقيم 16 و29 و00 المفصولة بنقطتين، ويؤدي تمرير السلسلة النصية '%I:%M% p' إلى إعادة '04:29 PM'، ويؤدي تمرير السلسلة النصية "%B of '%y" إلى إعادة "October of '23". لاحظ أننا لا نضع datetime.datetime قبل التابع strftime(). تحويل السلاسل النصية إلى كائنات datetime إذا كان لديك سلسلة نصية تمثّل معلومات التاريخ مثل '2023/10/21 16:29:00' أو 'October 21, 2023' وتريد تحويلها إلى كائن datetime، فاستخدم الدالة datetime.datetime.strptime()، حيث تُعَد هذه الدالة عكس التابع strftime(). يجب تمرير سلسلة تنسيق مُخصَّصة تستخدم الموجّهات نفسها التي يستخدمها التابع strftime() إلى الدالة strptime() حتى تعرف كيفية تحليل السلسلة النصية وفهمها، ويشير الحرف p الموجود في اسم الدالة strptime() إلى التحليل Parse. لندخِل الآن ما يلي في الصدفة التفاعلية: ➊ >>> datetime.datetime.strptime('October 21, 2023', '%B %d, %Y') datetime.datetime(2023, 10, 21, 0, 0) >>> datetime.datetime.strptime('2023/10/21 16:29:00', '%Y/%m/%d %H:%M:%S') datetime.datetime(2023, 10, 21, 16, 29) >>> datetime.datetime.strptime("October of '23", "%B of '%y") datetime.datetime(2023, 10, 1, 0, 0) >>> datetime.datetime.strptime("November of '63", "%B of '%y") datetime.datetime(2063, 11, 1, 0, 0) يمكن الحصول على كائن datetime من السلسلة النصية 'October 21, 2023' من خلال تمرير هذه السلسلة كوسيطٍ أول إلى الدالة strptime() وتمرير سلسلة التنسيق المُخصصة المقابلة للسلسلة النصية 'October 21, 2023' كوسيطٍ ثانٍ ➊. يجب أن تتطابق السلسلة النصية التي تحتوي على معلومات التاريخ مع سلسلة التنسيق المخصصة تمامًا، وإلّا سيرفع بايثون استثناء ValueError. مراجعة لدوال بايثون الخاصة بالوقت يمكن أن تتضمن التواريخ والأوقات في بايثون عددًا من أنواع البيانات والدوال المختلفة، لذا سنوضح فيما يلي الأنواع الثلاثة المختلفة من القيم المُستخدَمة لتمثيل الوقت: علامة يونيكس الزمنية التي تستخدمها الوحدة time، وهي قيمة عشرية أو صحيحة لعدد الثواني منذ الساعة 12 صباحًا في 1 من الشهر الأول من عام 1970 بالتوقيت العالمي المنسق. كائن datetime الخاص بالوحدة datetime، والذي يحتوي على أعداد صحيحة مخزَّنة في السمات year و month و day و hour و minute و second. كائن timedelta الخاص بالوحدة datetime، والذي يمثّل مدة زمنية وليس لحظة مُحدَّدة. إليك مراجعة لدوال الوقت ومعاملاتها والقيم التي تعيدها: time.time(): تعيد هذه الدالة القيمة العشرية لعلامة يونيكس الزمنية للحظة الحالية. time.sleep(seconds):توقِف هذه الدالة البرنامج لعددٍ من الثواني التي يحدّدها الوسيط seconds. datetime.datetime(year, month, day, hour, minute, second): تعيد هذه الدالة كائن datetime للحظة التي تحدّدها وسطاؤها. إذا لم تتوفّر قيم للوسطاء hour أو minute أو second، فستكون قيمها الافتراضية 0. datetime.datetime.now(): تعيد هذه الدالة كائن datetime للحظة الحالية. datetime.datetime.fromtimestamp(epoch): تعيد هذه الدالة كائن datetime للحظة التي يمثلها وسيط العلامة الزمنية epoch. datetime.timedelta(weeks, days, hours, minutes, seconds, milliseconds, microseconds): تعيد هذه الدالة كائن timedelta الذي يمثل مدة زمنية، وتُعَد وسطاء الكلمات المفتاحية لهذه الدالة اختيارية ولا تتضمن month أو year. total_seconds(): يعيد هذا التابع الخاص بكائنات timedelta عدد الثواني التي يمثّلها كائن timedelta. strftime(format): يعيد هذا التابع سلسلة نصية للوقت الذي يمثّله كائن datetime بتنسيقٍ مخصَّص يعتمد على سلسلة التنسيق format. اطّلع على الجدول السابق للحصول على تفاصيل التنسيق. datetime.datetime.strptime(time_string, format): تعيد هذه الدالة كائن datetime للحظة التي يحدّدها الوسيط time_string، وتُحلَّل باستخدام وسيط سلسلة التنسيق format. اطّلع على الجدول السابق للحصول على تفاصيل التنسيق. الخلاصة يُعَد توقيت يونيكس (1 من الشهر الأول من عام 1970 عند منتصف الليل بالتوقيت العالمي المنسَّق) وقتًا مرجعيًا معياريًا للعديد من لغات البرمجة بما في ذلك لغة بايثون. تعيد الوحدة الخاصة بالدالة time.time() علامة يونيكس الزمنية (أي قيمة عشرية لعدد الثواني منذ توقيت يونيكس)، ولكن تُعَد الوحدة datetime أفضل لإجراء العمليات الحسابية الرياضية على التاريخ وتنسيق أو تحليل السلاسل النصية باستخدام معلومات التاريخ. ستوقَِف الدالة time.sleep() التنفيذ (أي لن تعود) لعددٍ معين من الثواني، حيث يمكنك استخدام ذلك لإضافة فترات توقف مؤقتة إلى برنامجك. ترجمة -وبتصرُّف- للقسم Keeping Time من مقال Keeping Time, Scheduling Tasks, and Launching Programs لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: العمل مع ملفات CSV وبيانات JSON باستخدام لغة بايثون الوحدات Modules والحزم Packages في بايثون مصطلحات بايثون البرمجية
-
تعلّمنا في المقال السابق كيفية استخراج النص من مستندات PDF ووورد التي تُعَدّ ملفاتٍ بتنسيق ثنائي، إذ تتطلب هذه الملفات استخدام وحدات بايثون Python خاصة للوصول إلى بياناتها، بينما تُعَد ملفات CSV و JSON مجرد ملفاتٍ نصية عادية، حيث يمكنك عرضها في محرر نصوص مثل محرر النصوص Mu. تحتوي لغة بايثون مسبقًا على وحدتين خاصتين هما csv و json، حيث توفّر كلٌّ منهما دوالًا لمساعدتك في العمل مع تنسيقات هذه الملفات. يرمز الاختصار CSV إلى "القيم المفصولة بفواصل Comma-separated Values"، وملفات CSV هي جداول بيانات بسيطة مُخزَّنة بوصفها ملفات نصية عادية، وتسهّل وحدة csv في بايثون تحليل ملفات CSV. يُنطَق الاختصار JSON بالطريقة "JAY-sawn" أو "Jason"، ولكن لا يهم كيف تنطقها لأن الناس سيقولون أنك تنطقها بطريقة خاطئة في كلتا الحالتين، وهو تنسيق يخزن المعلومات بوصفها شيفرة جافاسكربت مصدرية في ملفات نصية عادية. JSON هو اختصار لترميز الكائنات باستعمال جافاسكربت JavaScript Object Notation، ولكن لا تحتاج إلى معرفة لغة البرمجة جافاسكربت لاستخدام ملفات JSON، ولكن من المفيد معرفة تنسيق ملفات JSON لأنه يُستخدَم في العديد من تطبيقات الويب. وحدة CSV يمثّل كل سطر في ملف CSV صفًا في جدول البيانات، حيث تفصل الفواصل بين الخلايا الموجودة في الصف، فمثلًا سيبدو جدول البيانات example.xlsx في ملف CSV كما يلي: 4/5/2015 13:34,Apples,73 4/5/2015 3:41,Cherries,85 4/6/2015 12:46,Pears,14 4/8/2015 8:59,Oranges,52 4/10/2015 2:07,Apples,152 4/10/2015 18:10,Bananas,23 4/10/2015 2:40,Strawberries,98 سنستخدم هذا الملف لأمثلة الصدفة التفاعلية Interactive Shell الموجودة في هذا المقال، حيث يمكنك تنزيله أو إدخال النص في محرّر النصوص وحفظه بالاسم example.csv. تُعَد ملفات CSV بسيطة، ولا تحتوي على العديد من ميزات جدول بيانات إكسل Excel مثل الميزات التالية: ليس لديها أنواع لقيمها، فكل شيء فيها هو سلسلة نصية String. ليس لديها إعدادات لحجم الخط أو لونه. ليس لديها أوراق عمل متعددة. لا يمكنها تحديد عرض الخلية وارتفاعها. لا يمكنها أن تحتوي على خلايا مدموجة. لا يمكنها تضمين الصور أو المخططات. ميزة ملفات CSV الأساسية هي البساطة، إذ تدعمها العديد من أنواع البرامج على نطاقٍ واسع، ويمكن عرضها في برامج تحرير النصوص بما في ذلك محرّر النصوص Mu، وتُعَد ملفات CSV طريقةً مباشرة لتمثيل بيانات جداول البيانات. تنسيق ملف CSV هو مجرد ملف نصي يحتوي على قيم يُفصَل بينها بفواصل. تُعَد ملفات CSV مجرد ملفات نصية، لذا قد تقرأها بوصفها سلسلة نصية ثم تعالج تلك السلسلة باستخدام التقنيات التي تعلمتها سابقًا في مقالٍ سابق، فمثلًا يمكنك استدعاء التابع split(',') في كل سطر من النص للحصول على القيم المفصولة بفواصل بوصفها قائمةً من السلاسل النصية، لأن كل خلية في ملف CSV مفصولة عن غيرها من الخلايا بفاصلة، ولكن لا تمثّل جميع الفواصل في ملف CSV هذه الحدود بين الخلايا، إذ تحتوي ملفات CSV أيضًا على مجموعة خاصة بها من محارف الهروب Escape Characters للسماح بتضمين الفواصل والمحارف الأخرى كجزء من القيم، حيث لا يعالج التابع split() محارف الهروب. يجب عليك دائمًا استخدام وحدة csv لقراءة ملفات CSV وكتابتها بسبب هذه المخاطر المحتملة. كائنات reader يمكنك قراءة البيانات من ملف CSV باستخدام وحدة csv من خلال إنشاء كائن reader الذي يتيح لك التكرار على الأسطر الموجودة في ملف CSV. أدخِل ما يلي في الصدفة التفاعلية مع وضع الملف example.csv في مجلد العمل الحالي: ➊ >>> import csv ➋ >>> exampleFile = open('example.csv') ➌ >>> exampleReader = csv.reader(exampleFile) ➍ >>> exampleData = list(exampleReader) ➎ >>> exampleData [['4/5/2015 13:34', 'Apples', '73'], ['4/5/2015 3:41', 'Cherries', '85'], ['4/6/2015 12:46', 'Pears', '14'], ['4/8/2015 8:59', 'Oranges', '52'], ['4/10/2015 2:07', 'Apples', '152'], ['4/10/2015 18:10', 'Bananas', '23'], ['4/10/2015 2:40', 'Strawberries', '98']] تحتوي لغة بايثون على وحدة csv، لذا يمكننا استيرادها ➊ دون الحاجة إلى تثبيتها أولًا. يمكنك قراءة ملف CSV باستخدام وحدة csv من خلال فتحه أولًا باستخدام الدالة open() ➋ كما تفعل مع أيّ ملف نصي آخر، ولكننا لا نستدعي التابع read() أو readlines() لكائن File الذي تعيده الدالة open()، بل نمرّره إلى الدالة csv.reader() ➌، مما يؤدي إلى إعادة كائن reader لتستخدمه. لاحظ أنك لا تمرّر السلسلة النصية التي تمثّل اسم الملف مباشرةً إلى الدالة csv.reader(). أكثر الطرق مباشرةً للوصول إلى القيم الموجودة في كائن reader هي تحويله إلى قائمة بايثون عادية من خلال تمريره إلى التابع list() ➍، حيث يعيد استخدام التابع list() لكائن reader قائمةً من القوائم، والتي يمكنك تخزينها في متغير مثل المتغير exampleData الذي يؤدي إدخاله في الصدفة إلى عرض قائمةٍ القوائم ➎. أصبح لديك ملف CSV بوصفه قائمةً من القوائم، ويمكنك الآن الوصول إلى القيمة الموجودة في صف وعمود محدّد باستخدام التعبير exampleData[row][col]، حيث يكون row هو فهرس إحدى القوائم الموجودة في المتغير exampleData، ويكون col هو فهرس العنصر الذي تريده من تلك القائمة. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> exampleData[0][0] '4/5/2015 13:34' >>> exampleData[0][1] 'Apples' >>> exampleData[0][2] '73' >>> exampleData[1][1] 'Cherries' >>> exampleData[6][1] 'Strawberries' لاحظ أن exampleData[0][0] ينتقل إلى القائمة الأولى ويعطي السلسلة النصية الأولى، وينتقل exampleData[0][2] إلى القائمة الأولى ويعطينا السلسلة النصية الثالثة وإلخ. قراءة البيانات من كائنات reader في حلقة for يجب استخدام كائن reader في حلقة for بالنسبة لملفات CSV الكبيرة، مما يؤدي إلى تجنّب تحميل الملف بأكمله إلى الذاكرة دفعة واحدة، إذًا لندخِل ما يلي مثلًا في الصدفة التفاعلية: >>> import csv >>> exampleFile = open('example.csv') >>> exampleReader = csv.reader(exampleFile) >>> for row in exampleReader: print('Row #' + str(exampleReader.line_num) + ' ' + str(row)) Row #1 ['4/5/2015 13:34', 'Apples', '73'] Row #2 ['4/5/2015 3:41', 'Cherries', '85'] Row #3 ['4/6/2015 12:46', 'Pears', '14'] Row #4 ['4/8/2015 8:59', 'Oranges', '52'] Row #5 ['4/10/2015 2:07', 'Apples', '152'] Row #6 ['4/10/2015 18:10', 'Bananas', '23'] Row #7 ['4/10/2015 2:40', 'Strawberries', '98'] استوردنا وحدة csv وأنشأنا كائن reader من ملف CSV، ويمكنك بعد ذلك التكرار ضمن حلقة على الصفوف الموجودة في كائن reader، حيث يمثل كلّ صف قائمةً من القيم، وتمثّل كل قيمة خلية. يطبع استدعاء الدالة print() رقم الصف الحالي ومحتوياته، ويمكنك الحصول على رقم الصف من خلال استخدام المتغير line_num الخاص بكائن reader، والذي يحتوي على رقم السطر الحالي. يمكن تكرار كائن reader مرة واحدة فقط، لذا يجب استدعاء الدالة csv.reader لإنشاء كائن reader وإعادة قراءة ملف CSV. كائنات writer يتيح كائن writer كتابة البيانات في ملف CSV، حيث يمكنك استخدام الدالة csv.writer() لإنشاء هذا الكائن. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import csv ➊ >>> outputFile = open('output.csv', 'w', newline='') ➋ >>> outputWriter = csv.writer(outputFile) >>> outputWriter.writerow(['spam', 'eggs', 'meat', 'beef']) 21 >>> outputWriter.writerow(['Hello, world!', 'eggs', 'meat', 'beef']) 32 >>> outputWriter.writerow([1, 2, 3.141592, 4]) 16 >>> outputFile.close() استدعِ أولًا الدالة open() ومرّر لها القيمة 'w' لفتح الملف في وضع الكتابة ➊، مما يؤدي إلى إنشاء الكائن الذي يمكنك بعد ذلك تمريره إلى الدالة csv.writer() ➋ لإنشاء كائن writer. يجب أيضًا في نظام ويندوز تمرير سلسلة نصية فارغة لوسيط الكلمات المفتاحية Keyword Argument الذي هو newline للدالة open()، وإذا نسيت ضبط الوسيط newline، فستكون الصفوف في الملف output.csv مزدوجة المسافات كما هو موضّح في الشكل التالي: إذا نسيت وسيط الكلمات المفتاحية newline='' في الدالة open()، فسيكون ملف CSV مزدوج المسافة يأخذ التابع writerow() الخاص بكائنات writer وسيطًا من نوع قائمة، حيث تُوضَع كل قيمة من هذه القائمة في خليتها الخاصة في ملف CSV الناتج، ويعيد هذا التابع عدد المحارف المكتوبة في الملف لهذا الصف بما في ذلك محارف السطر الجديد. ينتج عن الشيفرة البرمجية السابقة ملف output.csv الذي يبدو كما يلي: spam,eggs,meat,beef "Hello, world!",eggs,meat,beef 1,2,3.141592,4 لاحظ كيف يهرّب الكائن writer تلقائيًا الفاصلة الموجودة في القيمة 'Hello, world!' مع علامات الاقتباس المزدوجة في ملف CSV، إذ توفّر وحدة csv عليك الاضطرار إلى معالجة هذه الحالات الخاصة بنفسك. وسطاء الكلمات المفتاحية delimiter و lineterminator لنفترض أنك تريد الفصل بين الخلايا باستخدام محرف جدولة Tab بدلًا من الفاصلة وتريد أن تكون الصفوف ذات مسافات مزدوجة، لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import csv >>> csvFile = open('example.tsv', 'w', newline='') ➊ >>> csvWriter = csv.writer(csvFile, delimiter='\t', lineterminator='\n\n') >>> csvWriter.writerow(['apples', 'oranges', 'grapes']) 24 >>> csvWriter.writerow(['eggs', 'meat', 'beef']) 17 >>> csvWriter.writerow(['spam', 'spam', 'spam', 'spam', 'spam', 'spam']) 32 >>> csvFile.close() يؤدي ذلك إلى تغيير محارف المحدِّد Delimiter والفاصل بين السطور Line Terminator في ملفك، فالمحدِّد هو المحرف الذي يظهر بين الخلايا في الصف، والمحدِّد الافتراضي لملف CSV هو الفاصلة، بينما يكون الفاصل بين السطور هو المحرف الذي يأتي في نهاية الصف، فالفاصل بين السطور الافتراضي هو محرف السطر الجديد. يمكنك تغيير هذه المحارف إلى قيم مختلفة باستخدام وسطاء الكلمات المفتاحية Keyword Arguments التي هي delimiter و lineterminator باستخدام الدالة csv.writer(). يؤدي تمرير الوسطاء delimiter='\t' و lineterminator='\n\n' ➊ إلى تغيير المحرف بين الخلايا إلى محرف الجدولة والمحرف بين الصفوف إلى محرفي سطر جديد. نستدعي بعد ذلك الدالة writerow() ثلاث مرات لتعطينا ثلاثة صفوف. ينتج عن ذلك ملف بالاسم example.tsv يحتوي ما يلي: apples oranges grapes eggs meat beef spam spam spam spam spam spam فصلنا بين الخلايا بمحارف جدولة، وبالتالي سنستخدم امتداد الملف .tsv للقيم المفصول بينها بمحارف جدولة. كائنات DictReader و DictWriter الخاصة بملفات CSV من الأسهل العمل مع كائنات DictReader و DictWriter بدلًا من كائنات reader و writer بالنسبة لملفات CSV التي تحتوي على صفوف الترويسات، إذ تقرأ وتكتب كائنات reader و writer صفوف ملف CSV باستخدام القوائم، وتطبّق كائنات DictReader و DictWriter الخاصة بملفات CSV الوظائف نفسها ولكن باستخدام القواميس Dictionaries، وتستخدم الصف الأول من ملف CSV بوصفها مفاتيحًا لهذه القواميس. نزّل الملف exampleWithHeader.csv الذي هو الملف example.csv نفسه باستثناء أنه يحتوي على ترويسات الأعمدة Timestamp و Fruit و Quantity في الصف الأول. أدخِل ما يلي في الصدفة التفاعلية لقراءة هذا الملف: >>> import csv >>> exampleFile = open('exampleWithHeader.csv') >>> exampleDictReader = csv.DictReader(exampleFile) >>> for row in exampleDictReader: ... print(row['Timestamp'], row['Fruit'], row['Quantity']) ... 4/5/2015 13:34 Apples 73 4/5/2015 3:41 Cherries 85 4/6/2015 12:46 Pears 14 4/8/2015 8:59 Oranges 52 4/10/2015 2:07 Apples 152 4/10/2015 18:10 Bananas 23 4/10/2015 2:40 Strawberries 98 يضبط الكائنُ DictReader ضمن الحلقة الصفَّ row على كائن القاموس مع المفاتيح المشتقة من الترويسات الموجودة في الصف الأول، ولكنه يَضبط الصف row على الكائن OrderedDict الذي يمكنك استخدامه بالطريقة نفسها لاستخدام القاموس، ولكننا لن نشرح الفرق بين هاتين الطريقتين في هذا المقال. يعني استخدام الكائن DictReader أنك لا تحتاج إلى شيفرة برمجية إضافية لتخطي معلومات ترويسة الصف الأول، لأن الكائن DictReader يفعل ذلك نيابةً عنك. إذا حاولتَ استخدام كائنات DictReader مع الملف example.csv الذي لا يحتوي على ترويسات أعمدة في الصف الأول، فسيستخدم كائن DictReader مفاتيح القاموس '4/5/2015 13:34' و 'Apples' و '73'، حيث يمكننا تجنب ذلك من خلال تزويد الدالة DictReader() بوسيطٍ ثانٍ يحتوي على أسماء الترويسات التي تريدها: >>> import csv >>> exampleFile = open('example.csv') >>> exampleDictReader = csv.DictReader(exampleFile, ['time', 'name', 'amount']) >>> for row in exampleDictReader: ... print(row['time'], row['name'], row['amount']) ... 4/5/2015 13:34 Apples 73 4/5/2015 3:41 Cherries 85 4/6/2015 12:46 Pears 14 4/8/2015 8:59 Oranges 52 4/10/2015 2:07 Apples 152 4/10/2015 18:10 Bananas 23 4/10/2015 2:40 Strawberries 98 لا يحتوي الصف الأول من الملف example.csv على أيّ نص لعناوين الأعمدة، لذا أنشأنا عناوينا الخاصة 'time' و 'name' و 'amount'. تستخدم كائنات DictWriter أيضًا القواميس لإنشاء ملفات CSV. إذا أردتَ أن يحتوي ملفك على صف الترويسات، فاكتب هذا الصف من خلال استدعاء الدالة writeheader()، وإلّا فتخطى استدعاء هذه الدالة لحذف صف الترويسات من الملف. اكتب بعد ذلك كل صف من صفوف الملف CSV باستخدام استدعاء التابع writerow() مع تمرير قاموسٍ يستخدم الترويسات بوصفها مفاتيحًا ويحتوي على البيانات المراد كتابتها في الملف. >>> import csv >>> outputFile = open('output.csv', 'w', newline='') >>> outputDictWriter = csv.DictWriter(outputFile, ['Name', 'Pet', 'Phone']) >>> outputDictWriter.writeheader() >>> outputDictWriter.writerow({'Name': 'Alice', 'Pet': 'cat', 'Phone': '555- 1234'}) 20 >>> outputDictWriter.writerow({'Name': 'Bob', 'Phone': '555-9999'}) 15 >>> outputDictWriter.writerow({'Phone': '555-5555', 'Name': 'Carol', 'Pet': 'dog'}) 20 >>> outputFile.close() يبدو الملف output.csv الذي تنشئه الشيفرة البرمجية السابقة كما يلي: Name,Pet,Phone Alice,cat,555-1234 Bob,,555-9999 Carol,dog,555-5555 لاحظ أن ترتيب أزواج القيمة-المفتاح key-value في القواميس التي مرّرتها إلى الدالة writerow() غير مهم، فهي مكتوبة بترتيب المفاتيح المعطاة إلى الدالة DictWriter()، فمثلًا لا يزال رقم الهاتف يظهر في آخر الخرج بالرغم من أنك مرّرتَ المفتاح Phone وقيمته قبل مفاتيح وقيم Name و Pet في الصف الرابع. لاحظ أيضًا أن أيّ مفاتيح مفقودة مثل 'Pet' في {'Name': 'Bob', 'Phone': '555-9999'} ستكون ببساطة فارغة في ملف CSV. تطبيق عملي: إزالة الترويسة من ملفات CSV لنفترض أن لديك مهمة مملة تتمثل في إزالة السطر الأول من عدة مئات من ملفات CSV، إذ قد تدخل هذه الملفات في عملية آلية تتطلب البيانات فقط وليس الترويسات الموجودة في أعلى الأعمدة، حيث يمكنك فتح كل ملف في إكسل وحذف الصف الأول، ثم تعيد حفظ الملف، ولكن قد يستغرق ذلك ساعات، إذًا لنكتب برنامجًا ينفّذ هذه المهمة بدلًا من ذلك. يجب أن يفتح البرنامج كل ملفٍ امتداده .csv في مجلد العمل الحالي، ويقرأ محتويات ملف CSV، ثم يعيد كتابة المحتويات بدون الصف الأول في ملف يحمل الاسم نفسه، مما يؤدي إلى وضع المحتويات الجديدة لملف CSV بدون الترويسات مكان المحتويات القديمة. ملاحظة: تأكد من إنشاء نسخة احتياطية للملفات أولًا عندما تكتب برنامجًا يعدّل الملفات، فأنت لا تريد مسح ملفاتك الأصلية عن طريق الخطأ في حالة عدم عمل البرنامج بالطريقة التي تتوقعها. إليك الخطوات العامة التي سيطبّقها برنامجك: البحث عن جميع ملفات CSV في مجلد العمل الحالي. قراءة المحتويات الكاملة لكل ملف. كتابة المحتويات مع تخطي السطر الأول في ملف CSV جديد. ولكن سيحتاج برنامجك إلى تطبيق الخطوات التالية من ناحية الشيفرة البرمجية: المرور على قائمة الملفات باستخدام التابع os.listdir() مع تخطي الملفات التي ليست ملفات CSV. إنشاء كائن reader الخاص بوحدة CSV وقراءة محتويات الملف باستخدام السمة Attribute التي هي line_num لمعرفة السطر الذي يجب تخطيه. إنشاء كائن writer الخاص بوحدة CSV وكتابة بيانات القراءة في الملف الجديد. افتح نافذة محرّر جديدة لإنشاء ملف جديد واحفظه بالاسم removeCsvHeader.py لهذا المشروع. الخطوة الأولى: المرور على جميع ملفات CSV أول شيء يجب على برنامجك فعله هو المرور على قائمة جميع أسماء ملفات CSV الموجودة في مجلد العمل الحالي، لذا اجعل الملف removeCsvHeader.py يبدو كما يلي: #! python3 # removeCsvHeader.py - إزالة الترويسات من جميع ملفات CSV الموجودة في مجلد العمل الحالي import csv, os os.makedirs('headerRemoved', exist_ok=True) # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل الحالي for csvFilename in os.listdir('.'): if not csvFilename.endswith('.csv'): ➊ continue # تخطي الملفات التي ليست ملفات CSV print('Removing header from ' + csvFilename + '...') # قراءة ملف CSV مع تخطي الصف الأول # كتابة ملف CSV يؤدي استدعاء التابع os.makedirs() إلى إنشاء المجلد headerRemoved الذي سنكتب فيه جميع ملفات CSV التي ليس لها ترويسات. ستقودك حلقة for التي نكررها على التابع os.listdir('.') إلى هذه النتيجة جزئيًا، ولكنها ستتكرر على جميع الملفات الموجودة في مجلد العمل، لذلك يجب إضافة بعض الشيفرة البرمجية في بداية الحلقة التي تتخطى أسماء الملفات التي لا تنتهي بالامتداد .csv. تجعل التعليمة continue ➊ حلقة for تنتقل إلى اسم الملف التالي عندما تصل إلى ملف ليس ملف CSV. اطبع رسالةً توضّح ملف CSV الذي يعمل عليه البرنامج حتى يكون هناك بعض المخرجات أثناء تنفيذ البرنامج، ثم أضف بعض التعليقات لما يجب أن يفعله باقي البرنامج. الخطوة الثانية: قراءة ملف CSV لا يزيل البرنامج السطر الأول من ملف CSV، بل ينشئ نسخةً جديدة من ملف CSV بدون السطر الأول، وستحل النسخة محل الملف الأصلي لأن اسم ملف النسخة هو اسم الملف الأصلي نفسه. سيحتاج برنامجك إلى طريقة لتعقّب المرور على الصف الأول حاليًا، لذا أضِف ما يلي إلى الملف removeCsvHeader.py: #! python3 # removeCsvHeader.py - إزالة الترويسات من جميع ملفات CSV الموجودة في مجلد العمل الحالي --snip-- # قراءة ملف CSV مع تخطي الصف الأول csvRows = [] csvFileObj = open(csvFilename) readerObj = csv.reader(csvFileObj) for row in readerObj: if readerObj.line_num == 1: continue # تخطي الصف الأول csvRows.append(row) csvFileObj.close() # كتابة ملف CSV يمكن استخدام السمة line_num الخاصة بكائن reader لتحديد السطر الموجود في ملف CSV الذي يقرأه حاليًا، حيث توجد حلقة for أخرى للمرور على الصفوف التي يعيدها كائن reader الخاص بوحدة CSV، وستُلحَق جميع الصفوف باستثناء الصف الأول بالقائمة csvRows. تتحقق الشيفرة البرمجية السابقة مما إذا كان readerObj.line_num مضبوطًا على القيمة 1 أثناء تكرار حلقة for على كل صف. إذا كان ذلك صحيحًا، فستُنفَّذ تعليمة continue للانتقال إلى الصف التالي دون إلحاقه بالقائمة csvRows، وستكون قيمة الشرط False دائمًا بالنسبة لكل صفٍ لاحق، وسيُلحَق هذا الصف بالقائمة csvRows. الخطوة الثالثة: كتابة ملف CSV بدون الصف الأول أصبحت القائمة csvRows تحتوي على كافة الصفوف باستثناء الصف الأول، ويجب الآن كتابة هذه القائمة في ملف CSV الموجود في المجلد headerRemoved. أضِف ما يلي إلى الملف removeCsvHeader.py: #! python3 # removeCsvHeader.py - إزالة الترويسات من جميع ملفات CSV الموجودة في مجلد العمل الحالي --snip-- # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل الحالي ➊ for csvFilename in os.listdir('.'): if not csvFilename.endswith('.csv'): continue # تخطي الملفات التي ليست ملفات CSV --snip-- # كتابة ملف CSV csvFileObj = open(os.path.join('headerRemoved', csvFilename), 'w', newline='') csvWriter = csv.writer(csvFileObj) for row in csvRows: csvWriter.writerow(row) csvFileObj.close() يكتب الكائن writer الخاص بوحدة CSV القائمة الناتجة في ملف CSV موجود ضمن المجلد headerRemoved باستخدام المتغير csvFilename الذي استخدمناه أيضًا في كائن reader الخاص بوحدة CSV، مما يؤدي إلى الكتابة فوق الملف الأصلي. نمر بعد إنشاء الكائن writer على القوائم الفرعية المُخزَّنة في القائمة csvRows ونكتب كل قائمة فرعية في الملف. تنتقل حلقة for الخارجية ➊ إلى اسم الملف التالي من os.listdir('.') بعد تنفيذ الشيفرة البرمجية، وسيكتمل البرنامج عند الانتهاء من تلك الحلقة. اختبر برنامجك من خلال تنزيل الملف المضغوط removeCsvHeader.zip وفك ضغطه في مجلدٍ ما، ثم شغّل البرنامج removeCsvHeader.py في هذا المجلد، وسيكون الخرج كما يلي: Removing header from NAICS_data_1048.csv... Removing header from NAICS_data_1218.csv... --snip-- Removing header from NAICS_data_9834.csv... Removing header from NAICS_data_9986.csv… يجب أن يطبع هذا البرنامج اسم ملفٍ في كل مرة يزيل فيها السطر الأول من ملف CSV. أفكار لبرامج مماثلة تشبه البرامجُ التي يمكنك كتابتها لملفات CSV أنواعَ البرامج التي يمكنك كتابتها لملفات إكسل، لأنهما ملفات جداول بيانات، حيث يمكنك كتابة برامج تنفّذ ما يلي: مقارنة البيانات بين صفوف مختلفة في ملف CSV واحد أو بين ملفات CSV متعددة. نسخ بيانات محددة من ملف CSV إلى ملف إكسل أو العكس. التحقق من وجود بيانات غير صالحة أو أخطاء في تنسيق ملفات CSV وتنبيه المستخدم بهذه الأخطاء. قراءة البيانات من ملف CSV بوصفها دخلًا لبرامج بايثون الخاصة بك. JSON وواجهات برمجة التطبيقات API يُعَد ترميز الكائنات باستعمال جافاسكربت JavaScript Object Notation -أو JSON اختصارًا- طريقةً شائعة لتنسيق البيانات بوصفها سلسلة نصية واحدة يمكن أن يقرأها البشر، وهو الطريقة الأصيلة التي تكتب بها برامج جافاسكربت هياكلَ البيانات الخاصة بها وتشبه ما ستنتجه دالة pprint() في بايثون. لست بحاجةٍ لمعرفة لغة جافاسكربت لتتمكّن من التعامل مع البيانات المكتوبة بتنسيق JSON. إليك مثال للبيانات المكتوبة بتنسيق JSON: {"name": "Zophie", "isCat": true, "miceCaught": 0, "napsTaken": 37.5, "felineIQ": null} تُعَد معرفة JSON أمرًا مفيدًا، لأن العديد من مواقع الويب تقدم محتوى JSON بوصفه وسيلةً للبرامج للتفاعل مع موقع الويب، ويُعرَف ذلك بتوفير واجهة برمجة تطبيقات Application Programming Interface -أو API اختصارًا. يشبه الوصول إلى واجهة برمجة التطبيقات الوصولَ إلى أيّ صفحة ويب أخرى باستخدام عنوان URL، ولكن الفرق بينهما هو أن البيانات التي تعيدها واجهة برمجة التطبيقات تكون منسَّقة لتفهمها الأجهزة باستخدام JSON مثلًا، إذ ليس من السهل أن يقرأ البشر واجهات برمجة التطبيقات. تتيح العديد من مواقع الويب بياناتها بتنسيق JSON، حيث توفر فيسبوك Facebook وتويتر Twitter وياهو Yahoo وجوجل Google وتمبلر Tumblr وويكيبيديا Wikipedia وفليكر Flickr و Data.gov وريديت Reddit و IMDb وروتن توميتوز Rotten Tomatoes ولينكد إن LinkedIn والعديد من المواقع الشهيرة الأخرى واجهات برمجة تطبيقات لتستخدمها البرامج. تتطلب بعض هذه المواقع التسجيل الذي يكون مجانيًا دائمًا، ويجب أن تعثر على توثيق عناوين URL التي يحتاج برنامجك إلى طلبها للحصول على البيانات التي تريدها، بالإضافة إلى التنسيق العام لهياكل بيانات JSON المُعادة. يجب توفير هذا التوثيق من خلال أيّ موقع يقدم واجهة برمجة التطبيقات، حيث إذا توافرت صفحة للمطورين "Developers"، فابحث عن التوثيق هناك. يمكنك كتابة البرامج التي تنفّذ ما يلي باستخدام واجهات برمجة التطبيقات: استخلاص البيانات الخام من المواقع، حيث يكون الوصول إلى واجهات برمجة التطبيقات أسهل من تنزيل صفحات الويب وتحليل شيفرة HTML باستخدام مكتبة Beautiful Soup. تنزيل المنشورات الجديدة تلقائيًا من أحد حساباتك على شبكة التواصل الاجتماعي ونشرها على حسابٍ آخر، فمثلًا يمكنك أخذ منشوراتك على تمبلر ونشرها على فيسبوك. إنشاء "موسوعة أفلام" لمجموعة أفلامك الشخصية من خلال سحب البيانات من IMDb و Rotten Tomatoes وويكيبيديا ووضعها في ملف نصي واحد على حاسوبك. ملاحظة: يمكنك رؤية بعض الأمثلة على واجهات برمجة تطبيقات JSON في الموارد الموجودة على موقع nostarch. لا تعد JSON الطريقة الوحيدة لتنسيق البيانات في سلسلة نصية يمكن أن يقرأها البشر، إذ توجد العديد من اللغات الأخرى بما في ذلك لغات XML (لغة التوصيف الموسَّعة eXtensible Markup Language) و TOML (أو Tom’s Obvious, Minimal Language) و YML (أو Yet another Markup Language) و INI (أو Initialization) وتنسيقات ASN.1 القديمة (Abstract Syntax Notation One)، حيث توفر جميع هذه اللغات هيكلًا لتمثيل البيانات بوصفها نصًا يمكن أن يقرأه البشر. لن نغطّي هذه اللغات في هذا المقال، لأن تنسيق JSON أصبح التنسيق البديل الأكثر استخدامًا على نطاق واسع، ولكن توجد وحدات بايثون خارجية يمكنها التعامل معها بسهولة. وحدة json تتعامل وحدة json في بايثون مع جميع تفاصيل الترجمة بين سلسلة نصية تحتوي على بيانات JSON وقيم بايثون الخاصة بالدوال json.loads() و json.dumps(). لا يمكن لتنسيق JSON تخزين كل أنواع قيم بايثون، إذ يمكن أن يحتوي على قيم لأنواع بياناتٍ مُحدَّدة فقط وهي: السلاسل النصية Strings والأعداد الصحيحة Integers والأعداد العشرية Floats والقيم المنطقية Booleans والقوائم Lists والقواميس Dictionaries والنوع NoneType. كما لا يمكن لتنسيق JSON أن يمثل كائنات خاصة بلغة بايثون مثل كائنات File أو كائنات reader أو writer الخاصة بوحدة CSV أو كائنات Regex أو كائنات WebElement الخاصة بالوحدة Selenium. قراءة بيانات JSON باستخدام الدالة loads() يمكنك ترجمة سلسلة نصية تحتوي على بيانات JSON إلى قيمة في لغة بايثون من خلال تمريرها إلى الدالة json.loads()، حيث يعني اسم هذه الدالة loads تحميل سلسلة نصية "load string" ولا يعني مجموعة التحميلات "loads". لندخِل الآن ما يلي في الصدفة التفاعلية Interactive Shell: >>> stringOfJsonData = '{"name": "Zophie", "isCat": true, "miceCaught": 0, "felineIQ": null}' >>> import json >>> jsonDataAsPythonValue = json.loads(stringOfJsonData) >>> jsonDataAsPythonValue {'isCat': True, 'miceCaught': 0, 'name': 'Zophie', 'felineIQ': None} نستورد أولًا الوحدة json، ثم يمكننا استدعاء الدالة loads() وتمرير سلسلة نصية من بيانات JSON إليها، حيث تستخدم سلاسل JSON النصية دائمًا علامات اقتباس مزدوجة، وتعيد هذه الدالة البيانات بوصفها قاموس بايثون. تُعَد قواميس بايثون غير مرتبة، لذا يمكن أن تظهر أزواج مفتاح-قيمة بترتيب مختلف عند طباعة jsonDataAsPythonValue. كتابة بيانات JSON باستخدام الدالة dumps() تترجم الدالة json.dumps() قيمة بايثون إلى سلسلة نصية من البيانات بتنسيق JSON، حيث يعني اسم هذه الدالة dumps تفريغ سلسلة نصية "dump string" ولا يعني الجمع "dumps"). لندخِل الآن ما يلي في الصدفة التفاعلية: >>> pythonValue = {'isCat': True, 'miceCaught': 0, 'name': 'Zophie', 'felineIQ': None} >>> import json >>> stringOfJsonData = json.dumps(pythonValue) >>> stringOfJsonData '{"isCat": true, "felineIQ": null, "miceCaught": 0, "name": "Zophie" }' يمكن أن يكون نوع القيمة أحد أنواع بيانات بايثون الأساسية فقط وهي: قاموس أو قائمة أو عدد صحيح أو عدد عشري أو سلسلة نصية أو قيمة منطقية أو None. تطبيق عملي: جلب بيانات الطقس الحالية يبدو التحقق من الطقس أمرًا بسيطًا إلى حد ما، حيث يمكنك فتح متصفح الويب الخاص بك والنقر على شريط العناوين، ثم كتابة عنوان URL لموقع ويب خاص بالطقس أو البحث عن موقع ثم النقر على الرابط، وانتظار تحميل الصفحة، والاطلاع على جميع الإعلانات وإلخ. هناك الكثير من الخطوات المملة التي يمكنك تخطيها إذا كان لديك برنامج ينزّل توقعات الطقس للأيام القليلة القادمة ويطبعها ضمن نصٍ عادي، حيث يستخدم هذا البرنامج الوحدة requests لتنزيل البيانات من الويب، فالخطوات العامة التي يطبّقها هذا البرنامج هي ما يلي: قراءة الموقع المطلوب من سطر الأوامر. تنزيل بيانات JSON الخاصة بالطقس من الموقع OpenWeatherMap.org. تحويل سلسلة بيانات JSON إلى هيكل بيانات بايثون. طباعة حالة الطقس لهذا اليوم واليومين القادمين. لذا ستطبّق الشيفرة البرمجية الخطوات التالية: ضم السلاسل النصية إلى القائمة sys.argv للحصول على الموقع. استدعاء الدالة requests.get() لتنزيل بيانات الطقس. استدعاء الدالة json.loads() لتحويل بيانات JSON إلى هيكل بيانات بايثون. طباعة توقعات الطقس. افتح نافذة محرّر جديدة لإنشاء ملف جديد لهذا المشروع واحفظه بالاسم getOpenWeather.py، ثم انتقل إلى الموقع OpenWeatherMap في متصفحك وسجّل فيه على حساب مجاني للحصول على مفتاح API، ويُسمَّى أيضًا معرّف التطبيق app ID، والذي يمثل رمز سلسلة نصية يبدو مثل الرمز '30144aba38018987d84710d0e319281e' بالنسبة لخدمة OpenWeatherMap. لا حاجة للدفع مقابل هذه الخدمة إلّا إذا أردتَ إجراء أكثر من 60 استدعاء لواجهة برمجة التطبيقات في الدقيقة. حافظ على سرية مفتاح API، إذ يمكن لأيّ شخص يعرفه كتابة سكربتات تأخذ من حصة الاستخدام الخاصة بحسابك. الخطوة الأولى: الحصول على الموقع من وسيط سطر الأوامر يأتي دخل هذا البرنامج من سطر الأوامر، لذا اجعل برنامج getOpenWeather.py كما يلي: #! python3 # getOpenWeather.py - طباعة الطقس لموقعٍ ما من سطر الأوامر APPID = 'YOUR_APPID_HERE' import json, requests, sys # حساب الموقع من وسطاء سطر الأوامر if len(sys.argv) < 2: print('Usage: getOpenWeather.py city_name, 2-letter_country_code') sys.exit() location = ' '.join(sys.argv[1:]) # تنزيل بيانات JSON من واجهة برمجة تطبيقات OpenWeatherMap.org # تحميل بيانات JSON في متغير بايثون تُخزَّن وسطاء سطر الأوامر ضمن القائمة sys.argv في لغة بايثون، ويجب ضبط المتغير APPID على قيمة مفتاح API الخاص بحسابك، إذ ستفشل طلباتك لخدمة الطقس بدون هذا المفتاح، ثم سيتحقق البرنامج من وجود أكثر من وسيط سطر أوامر بعد سطر Shebang (الذي يبدأ بالرمز !#) والتعليمة import. تذكّر أن القائمة sys.argv تحتوي دائمًا على عنصر واحد على الأقل sys.argv[0]، والذي يحتوي على اسم ملف سكربت بايثون. إذا كان هناك عنصر واحد فقط في القائمة، فهذا يعني أن المستخدم لم يقدّم موقعًا في سطر الأوامر، وستُعرَض رسالة الاستخدام "usage" للمستخدم قبل انتهاء البرنامج. تتطلب خدمة OpenWeatherMap تنسيق الاستعلام بالشكل: اسم المدينة ثم فاصلة ثم رمز البلد المكون من حرفين مثل الرمز "US" للولايات المتحدة الأمريكية، حيث يمكنك العثور على قائمة بهذه الرموز على ويكيبيديا. يعرض هذا السكربت الطقس للمدينة الأولى المُدرَجة في نص JSON المُعاد، ولكن ستُضمَّن جميع المدن التي لها الاسم نفسه مثل مدينة بورتلاند Portland في ولاية أوريجون ومدينة بورتلاند بولاية ماين، بالرغم من أن نص JSON سيتضمّن معلومات خطوط الطول والعرض للتمييز بين هذه المدن. تُقسَم وسطاء سطر الأوامر بناءً على الفراغات، حيث سيجعل وسيط سطر الأوامر San Francisco, US القائمة sys.argv تحتوي على ['getOpenWeather.py', 'San', 'Francisco,', 'US']، لذلك استدعِ التابع join() لضم جميع السلاسل النصية باستثناء السلسلة النصية الأولى في القائمة sys.argv، وخزّن هذه السلسلة النصية الناتجة عن الضم في متغير اسمه location. الخطوة الثانية: تنزيل بيانات JSON يوفّر موقع OpenWeatherMap.org معلومات الطقس بتنسيق JSON في الزمن الحقيقي، ولكن يجب عليك أولًا التسجيل في الموقع للحصول على مفتاح API مجاني، حيث يُستخدَم هذا المفتاح لتقييد عدد مرات تقديم الطلبات على الخادم، مما يؤدي إلى إبقاء تكاليف حيز النطاق التراسلي منخفضة. يجب أن ينزّل برنامجك الصفحة الموجودة على الرابط: https://api.openweathermap.org/data/2.5/forecast/daily?q=<Location>&cnt=3&APPID=<APIkey> حيث <Location> هو اسم المدينة التي تريد الطقس فيها و <API key> هو مفتاح API الشخصي الخاص بك. أضِف ما يلي إلى برنامج getOpenWeather.py: #! python3 # getOpenWeather.py - طباعة الطقس لموقعٍ ما من سطر الأوامر --snip-- # تنزيل بيانات JSON من واجهة برمجة تطبيقات OpenWeatherMap.org url ='https://api.openweathermap.org/data/2.5/forecast/daily?q=%s&cnt=3&APPID=%s ' % (location, APPID) response = requests.get(url) response.raise_for_status() # ألغِ التعليق لرؤية نص JSON الخام: #print(response.text) # تحميل بيانات JSON في متغير بايثون يأتي الموقع location من وسطاء سطر الأوامر، ويمكننا إنشاء عنوان URL الذي نريد الوصول إليه من خلال استخدام العنصر البديل s% وإدراج أيّ سلسلة نصية مُخزَّنة في location في ذلك المكان من السلسلة النصية التي تمثّل عنوان URL. نخزّن النتيجة في المتغير url الذي نمرّره إلى الدالة requests.get()، حيث يعيد استدعاء الدالة requests.get() الكائن Response، والذي يمكنك التحقق من وجود أخطاء فيه من خلال استدعاء الدالة raise_for_status(). إن لم يظهر أيّ استثناء، فسيكون النص المُنزَّل موجودًا في response.text. الخطوة الثالثة: تحميل بيانات JSON وطباعة الطقس يحتوي المتغير العضو response.text على سلسلة نصية كبيرة من البيانات المُنسَّقة بتنسيق JSON، حيث يمكنك تحويلها إلى قيمة بايثون من خلال استدعاء الدالة json.loads()، وستبدو بيانات JSON كما يلي: {'city': {'coord': {'lat': 37.7771, 'lon': -122.42}, 'country': 'United States of America', 'id': '5391959', 'name': 'San Francisco', 'population': 0}, 'cnt': 3, 'cod': '200', 'list': [{'clouds': 0, 'deg': 233, 'dt': 1402344000, 'humidity': 58, 'pressure': 1012.23, 'speed': 1.96, 'temp': {'day': 302.29, 'eve': 296.46, 'max': 302.29, 'min': 289.77, 'morn': 294.59, 'night': 289.77}, 'weather': [{'description': 'sky is clear', 'icon': '01d', --snip– يمكنك رؤية هذه البيانات من خلال تمرير المتغير weatherData إلى الدالة pprint.pprint()، وقد ترغب في الاطلاع على موقع openweathermap.org لمزيد من التوثيق الذي يوضّح معنى هذه الحقول، فمثلًا سيخبرك التوثيق عبر الإنترنت أن القيمة 302.29 بعد 'day' هي درجة الحرارة أثناء النهار بواحدة الكلفن وليست بواحدة فهرنهايت أو الدرجة المئوية. يوجد وصف الطقس الذي تريده بعد 'main' و 'description'، لذا أضِف ما يلي إلى برنامج getOpenWeather.py لطباعته بدقة: ! python3 # getOpenWeather.py - طباعة الطقس لموقعٍ ما من سطر الأوامر --snip-- # تحميل بيانات JSON في متغير بايثون weatherData = json.loads(response.text) # طباعة وصف الطقس ➊ w = weatherData['list'] print('Current weather in %s:' % (location)) print(w[0]['weather'][0]['main'], '-', w[0]['weather'][0]['description']) print() print('Tomorrow:') print(w[1]['weather'][0]['main'], '-', w[1]['weather'][0]['description']) print() print('Day after tomorrow:') print(w[2]['weather'][0]['main'], '-', w[2]['weather'][0]['description']) لاحظ كيف تخزّن الشيفرة البرمجية السابقة المتغير weatherData['list'] في المتغير w ليوفر عليك بعض الجهد عند الكتابة ➊، حيث يمكنك استخدام w[0] و w[1] و w[2] لاسترداد القواميس الخاصة بطقس اليوم والغد وبعد الغد على التوالي، ويحتوي كلّ قاموس من هذه القواميس على مفتاح 'weather' الذي يحتوي على قيمة من النوع قائمة، ويهمنا منها عنصر القائمة الأول عند الفهرس 0، وهو قاموس متداخل يحتوي على عدة مفاتيح أخرى. نطبع القيم المخزنة في المفتاحين 'main' و 'description'، حيث نفصل بينها بشرطة واصلة. سيبدو الخرج كما يلي عند تشغيل هذا البرنامج باستخدام وسيط سطر الأوامر getOpenWeather.py San Francisco, CA: Current weather in San Francisco, CA: Clear - sky is clear Tomorrow: Clouds - few clouds Day after tomorrow: Clear - sky is clear أفكار لبرامج مماثلة يمكن أن يشكّل الوصول إلى بيانات الطقس الأساس الذي نبني عليه العديد من أنواع البرامج الأخرى، حيث يمكنك إنشاء برامج مماثلة لتنفيذ ما يلي: جمع تنبؤات الطقس للعديد من مواقع التخييم أو مسارات المشي لمسافات طويلة لمعرفة أيّ منها سيكون فيها الطقس الأفضل. جدولة برنامج للتحقق من الطقس بانتظام وإرسال تنبيه عند حدوث الصقيع إذا أردتَ نقل نباتاتك إلى الداخل، حيث سنوضّح لاحقًا الجدولة وكيفية إرسال البريد الإلكتروني. سحب بيانات الطقس من مواقع متعددة لإظهارها دفعة واحدة، أو حساب وإظهار متوسط توقعات الطقس المتعددة. مشروع للتدريب: محوّل ملف إكسل إلى ملف CSV يمكن لإكسل حفظ جدول بيانات في ملف CSV باستخدام بضع نقرات بالماوس، ولكن إذا أردتَ تحويل مئاتٍ من ملفات إكسل إلى ملفات CSV، فسيستغرق الأمر ساعات من النقر، لذا استخدم وحدة openpyxl لكتابة برنامجٍ يقرأ جميع ملفات إكسل الموجودة في مجلد العمل الحالي ويخرجها بوصفها ملفات CSV. قد يحتوي ملف إكسل واحد على أوراق متعددة، لذا يجب إنشاء ملف CSV واحد لكل ورقة، ويجب أن تكون أسماء ملفات CSV هي <excel filename>_<sheet title>.csv، حيث يكون <excel filename> هو اسم ملف إكسل بدون امتداد الملف مثل 'spam_data' وليس 'spam_data.xlsx' ويكون <sheet title> هو السلسلة النصية التي تأتي من المتغير title الخاص بكائن Worksheet. سيتضمن هذا البرنامج العديد من حلقات for المتداخلة، وسيبدو هيكل هذا البرنامج كما يلي: for excelFile in os.listdir('.'): # تخطي الملفات التي ليست ملفات xlsx، وتحميل كائن المصنف workbook for sheetName in wb.get_sheet_names(): # المرور ضمن حلقة على كل ورقة في المصنف sheet = wb.get_sheet_by_name(sheetName) # إنشاء اسم ملف CSV من اسم ملف إكسل وعنوان الورقة # إنشاء كائن csv.writer لملف CSV # المرور ضمن حلقة على كل صف في الورقة for rowNum in range(1, sheet.max_row + 1): rowData = [] # إلحاق كل خلية بهذه القائمة # المرور ضمن حلقة على كل خلية في الصف for colNum in range(1, sheet.max_column + 1): # إلحاق بيانات كل خلية بالقائمة rowData # كتابة القائمة rowData في ملف CSV csvFile.close() نزّل الملف المضغوط excelSpreadsheets.zip وفك ضغط جداول البيانات في المجلد نفسه الموجود فيه برنامجك، حيث يمكنك استخدام جداول البيانات هذه كملفات لاختبار البرنامج عليها. الخلاصة تُعد CSV و JSON من تنسيقات النصوص العادية الشائعة لتخزين البيانات، حيث يسهُل على البرامج تحليلها مع بقاء قابليتها للقراءة، لذا تُستخدَم لجداول البيانات أو بيانات تطبيقات الويب البسيطة. تبسّط وحدتا csv و json عملية القراءة والكتابة في ملفات CSV و JSON بصورة كبيرة. تعلّمنا سابقًا كيفية استخدام بايثون لتحليل معلومات مجموعة واسعة من تنسيقات الملفات، فإحدى المهام الشائعة هي أخذ البيانات من مجموعة متنوعة من التنسيقات وتحليلها للحصول على المعلومات المُحدَّدة التي تحتاجها، وتكون هذه المهام مُحدَّدة لدرجة أن البرمجيات التجارية لن تفيدك في إنجازها على النحو الأمثل، لذا يمكنك جعل حاسوبك يتعامل مع كميات كبيرة من البيانات المُقدَّمة بهذه التنسيقات من خلال كتابة السكربتات الخاصة بك. سنبتعد في المقالات اللاحقة عن تنسيقات البيانات وسنتعلّم كيفية جعل برامجك يتواصل معك من خلال إرسال رسائل البريد الإلكتروني والرسائل النصية. ترجمة -وبتصرُّف- للمقال Working with CSV files and JSON data لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: العمل مع مستندات PDF ومستندات Word باستخدام بايثون العمل مع جداول بيانات جوجل Google Sheets باستخدام لغة بايثون الكتابة في مستندات إكسل باستخدام لغة بايثون Python قراءة مستندات جداول إكسل باستخدام لغة بايثون Python مقدمة إلى تطبيق مستندات جوجل Google Docs
-
تُعَد مستندات بي دي إف PDF ومستندات وورد Word ملفات ثنائية، مما يجعلها أكثر تعقيدًا من الملفات النصية العادية، إذ تخزِّن الكثير من المعلومات المتعلقة بالخطوط والألوان وتخطيط الصفحات بالإضافة إلى النصوص. إذا أدرتَ أن تقرأ برامجك أو تكتبها في ملفات PDF أو مستندات وورد، فستحتاج إلى تطبيق أكثر من مجرد تمرير أسماء الملفات إلى الدالة open()، لذا توجد وحدات بايثون Python التي تسهّل عليك التفاعل مع ملفات PDF ومستندات وورد مثل الوحدتين PyPDF2 و Python-Docx اللتين سنوضّحهما في هذا المقال. مستندات PDF يرمز الاختصار PDF إلى صيغة المستندات المنقولة Portable Document Format التي تستخدم امتداد الملف .pdf. تدعم ملفات PDF العديد من الميزات، ولكن سنركز في هذا المقال على المهمتين اللتين ستفعلهما باستخدام هذه الملفات في أغلب الأحيان وهما: قراءة محتوى النصوص من ملفات PDF وإنشاء ملفات PDF جديدة من مستندات موجودة مسبقًا. سنستخدم الوحدة PyPDF2 ذات الإصدار 1.26.0 للعمل مع ملفات PDF، لذا من المهم أن تثبّت هذا الإصدار لأن الإصدارات اللاحقة من وحدة PyPDF2 قد تكون غير متوافقة مع شيفرتنا البرمجية، إذًا شغّل الأمر pip install --user PyPDF2==1.26.0 من سطر الأوامر لتثبيت هذه الوحدة، ولاحظ أن اسم الوحدة حساس لحالة الحروف، لذا تأكد من أن الحرف y صغير والحروف الأخرى كبيرة. اتبع الإرشادات الخاصة بتثبيت الوحدات الخارجية التي سنوضّحها في مقال لاحق من هذه السلسلة. إذا جرى تثبيت هذه الوحدة بصورة صحيحة، فيُفترض ألّا يؤدي تشغيل الأمر import PyPDF2 في الصدفة التفاعلية Interactive Shell إلى عرض أيّ أخطاء. ملاحظة: تُعَد ملفات PDF رائعة لتجهيز النصوص بحيث تسهل طباعتها وقراءتها، ولكن ليس من السهل على البرمجيات تحليلها إلى نصوص عادية، لذلك قد ترتكب الوحدة PyPDF2 أخطاءً عند استخراج النص من ملف PDF، وقد لا تتمكّن من فتح بعض ملفات PDF، ولا يوجد الكثير لفعله لحل هذه المشكلة، إذ قد تكون وحدة PyPDF2 ببساطة غير قادرة على العمل مع بعض ملفات PDF، ولكننا لم نجد بعد أيّ ملفات PDF لا يمكن فتحها باستخدام وحدة PyPDF2. استخراج النص من ملفات PDF لا تمتلك وحدة PyPDF2 طريقةً لاستخراج الصور أو المخططات أو الوسائط الأخرى من مستندات PDF، ولكن يمكنها استخراج النص وإعادته بوصفه سلسلة بايثون. سنستخدم في مثالنا مستند PDF الموضّح في الشكل التالي للبدء في تعلم كيفية عمل وحدة PyPDF2: صفحة PDF التي سنستخرج النص منها نزّل ملف PDF الموضّح في الشكل السابق وأدخِل ما يلي في الصدفة التفاعلية: >>> import PyPDF2 >>> pdfFileObj = open('meetingminutes.pdf', 'rb') >>> pdfReader = PyPDF2.PdfFileReader(pdfFileObj) ➊ >>> pdfReader.numPages 19 ➋ >>> pageObj = pdfReader.getPage(0) ➌ >>> pageObj.extractText() 'OOFFFFIICCIIAALL BBOOAARRDD MMIINNUUTTEESS Meeting of March 7, 2015 \n The Board of Elementary and Secondary Education shall provide leadership and create policies for education that expand opportunities for children, empower families and communities, and advance Louisiana in an increasingly competitive global market. BOARD of ELEMENTARY and SECONDARY EDUCATION ' >>> pdfFileObj.close() نستورد أولًا وحدة PyPDF2، ثم نفتح الملف meetingminutes.pdf للقراءة في الوضع الثنائي ونخزّنه في المتغير pdfFileObj. يمكن الحصول على كائن PdfFileReader الذي يمثل ملف PDF من خلال استدعاء الدالة PyPDF2.PdfFileReader() وتمرير المتغير pdfFileObj إليها، ثم خزّن هذا الكائن في المتغير pdfReader. يُخزَّن إجمالي عدد صفحات المستند في السمة Attribute التي هي numPages الخاصة بالكائن PdfFileReader ➊، ويحتوي ملف PDF في مثالنا على 19 صفحة، ولكن نريد استخراج النص من الصفحة الأولى فقط من خلال الحصول على كائن Page من كائن PdfFileReader، حيث يمثل كائن Page صفحة واحدة من ملف PDF. يمكنك الحصول على كائن Page من خلال استدعاء التابع getPage() ➋ الخاص بكائن PdfFileReader وتمرير رقم الصفحة التي تريدها إليه، ورقم الصفحة هو 0 في مثالنا. تستخدم الوحدة PyPDF2 فهرسًا مستنِدًا إلى القيمة 0 للحصول على الصفحات، فالصفحة الأولى هي الصفحة 0، والصفحة الثانية هي الصفحة 1 وإلخ، إذ تُستخدَم هذه الطريقة دائمًا حتى لو كانت الصفحات مرقَّمة بطريقة مختلفة في المستند. لنفترض مثلًا أن لديك ملف PDF يمثّل مقطعًا من تقرير أطول، ويتألف هذا المقطع من ثلاث صفحات، وأرقام الصفحات هي 42 و 43 و 44. يمكن الحصول على الصفحة الأولى من هذا المستند من خلال استدعاء التابع pdfReader.getPage(0) وليس من خلال استدعاء getPage(42) أو getPage(1). نحصل على كائن Page، ثم نستدعي التابع extractText() الخاص بهذا الكائن لإعادة سلسلة نصية تمثل النص الموجود في الصفحة ➌. لاحظ أن استخراج النص ليس مثاليًا، فالنص "Charles E. “Chas” Roemer, President" من ملف PDF غير موجود في السلسلة النصية التي يعيدها التابع extractText()، وتكون المسافات غير مفعّلة في بعض الأحيان، ولكن قد يكون هذا المحتوى التقريبي لنص ملف PDF كافيًا لبرنامجك. فك تشفير ملفات PDF تحتوي بعض مستندات PDF على ميزة تشفير تمنع قراءتها حتى يضع الشخص الذي يفتح المستند كلمة المرور. لندخِل ما يلي في الصدفة التفاعلية مع ملف PDF الذي نزلته، وهذا الملف مُشفَّر باستخدام كلمة المرور rosebud: >>> import PyPDF2 >>> pdfReader = PyPDF2.PdfFileReader(open('encrypted.pdf', 'rb')) ➊ >>> pdfReader.isEncrypted True >>> pdfReader.getPage(0) ➋ Traceback (most recent call last): File "<pyshell#173>", line 1, in <module> pdfReader.getPage() --snip-- File "C:\Python34\lib\site-packages\PyPDF2\pdf.py", line 1173, in getObject raise utils.PdfReadError("file has not been decrypted") PyPDF2.utils.PdfReadError: file has not been decrypted >>> pdfReader = PyPDF2.PdfFileReader(open('encrypted.pdf', 'rb')) ➌ >>> pdfReader.decrypt('rosebud') 1 >>> pageObj = pdfReader.getPage(0) تحتوي جميع كائنات PdfFileReader على السمة isEncrypted التي تكون قيمتها True إذا كان ملف PDF مُشفرًا، وتكون قيمتها False إن لم يكن ملف PDF مُشفَّرًا ➊، وستؤدي أيّ محاولة لاستدعاء دالةٍ تقرأ الملف قبل فك تشفيره باستخدام كلمة المرور الصحيحة إلى حدوث خطأ ➋. ملاحظة: يوجد خطأٌ في الإصدار 1.26.0 من وحدة PyPDF2، حيث يؤدي استدعاء التابع getPage() لملف PDF مشفَّر قبل استدعاء الدالة decrypt() لهذا الملف إلى فشل استدعاءات التابع getPage() المستقبلية مع ظهور الخطأ IndexError: list index out of range، ولذلك أعاد المثال السابق فتح الملف باستخدام كائن PdfFileReader جديد. يمكن قراءة ملف PDF مشفَّر من خلال استدعاء الدالة decrypt() وتمرير كلمة المرور بوصفها سلسلة نصية إليه ➌، وسترى أن استدعاء التابع getPage() لم يعُد يسبّب خطأً بعد استدعاء الدالة decrypt() مع كلمة المرور الصحيحة، بينما إذا أعطيتَ كلمة مرور خطأ، فستعيد الدالة decrypt() القيمة 0 وسيفشل التابع getPage(). لاحظ أن التابع decrypt() يفك تشفير الكائن PdfFileReader فقط وليس ملف PDF الفعلي، إذ يبقى الملف الموجود على قرص حاسوبك الصلب مشفَّرًا بعد انتهاء البرنامج، وبالتالي يجب أن يستدعي برنامجُك التابعَ decrypt() عند تشغيله مرة أخرى. إنشاء ملفات PDF يمكن للكائن PdfFileWriter الخاص بالوحدة PyPDF2 إنشاء ملفات PDF جديدة، ولكن لا تستطيع وحدة PyPDF2 كتابة نص عشوائي في ملف PDF كما تفعل شيفرة بايثون مع ملفات النصوص العادية، لذا تقتصر إمكانات كتابة ملفات PDF في وحدة PyPDF2 على نسخ الصفحات من ملفات PDF الأخرى وتدوير الصفحات ودمجها وتشفير الملفات. لا تسمح وحدة PyPDF2 بتعديل ملف PDF مباشرةً، لذا يجب إنشاء ملف PDF جديد ثم نسخ المحتوى من مستند موجود مسبقًا. ستتبع الأمثلة الواردة في هذا القسم النهج العام التالي: فتح ملفٍ أو أكثر من ملفات PDF الموجودة مسبقًا (ملفات PDF المصدر) في كائنات PdfFileReader. إنشاء كائن PdfFileWriter جديد. نسخ الصفحات من كائنات PdfFileReader إلى كائن PdfFileWriter. استخدام كائن PdfFileWriter لكتابة ملف PDF الناتج. يؤدي إنشاء كائن PdfFileWriter إلى إنشاء قيمةٍ تمثّل مستند PDF في شيفرة بايثون فقط دون إنشاء ملف PDF الفعلي، ولذلك يجب استدعاء التابع write() الخاص بهذا الكائن. يأخذ هذا التابع كائن File عادي مفتوح في وضع الكتابة الثنائي، حيث يمكنك الحصول على كائن File من خلال استدعاء الدالة open() الخاصة بلغة بايثون مع وسيطين هما: السلسلة النصية التي تريد أن تمثّل اسم ملف PDF والوسيط 'wb' الذي يشير إلى أنه يجب فتح الملف في وضع الكتابة الثنائي. لا تقلق إذا كان ذلك مربكًا بعض الشيء، حيث سنرى كيفية ذلك في الأمثلة البرمجية التالية. نسخ الصفحات يمكنك استخدام وحدة PyPDF2 لنسخ الصفحات من مستند PDF إلى آخر، مما يتيح لك دمج ملفات PDF متعددة أو قص الصفحات غير المرغوب فيها أو إعادة ترتيب الصفحات. نزّل الملفين meetingminutes.pdf و meetingminutes2.pdf وضعهما في مجلد العمل الحالي، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> import PyPDF2 >>> pdf1File = open('meetingminutes.pdf', 'rb') >>> pdf2File = open('meetingminutes2.pdf', 'rb') ➊ >>> pdf1Reader = PyPDF2.PdfFileReader(pdf1File) ➋ >>> pdf2Reader = PyPDF2.PdfFileReader(pdf2File) ➌ >>> pdfWriter = PyPDF2.PdfFileWriter() >>> for pageNum in range(pdf1Reader.numPages): ➍ pageObj = pdf1Reader.getPage(pageNum) ➎ pdfWriter.addPage(pageObj) >>> for pageNum in range(pdf2Reader.numPages): ➍ pageObj = pdf2Reader.getPage(pageNum) ➎ pdfWriter.addPage(pageObj) ➏ >>> pdfOutputFile = open('combinedminutes.pdf', 'wb') >>> pdfWriter.write(pdfOutputFile) >>> pdfOutputFile.close() >>> pdf1File.close() >>> pdf2File.close() افتح ملفي PDF في وضع القراءة الثنائي وخزّن كائني File الناتجين في المتغيرين pdf1File و pdf2File، ثم استدعِ الدالة PyPDF2.PdfFileReader() ومرّر المتغير pdf1File إليها للحصول على كائن PdfFileReader للملف meetingminutes.pdf ➊، ثم استدعيها مرةً أخرى ومرّر المتغير pdf2File إليها للحصول على كائن PdfFileReader للملف meetingminutes2.pdf ➋، ثم أنشئ كائن PdfFileWriter جديد، والذي يمثل مستند PDF فارغ ➌. انسخ بعد ذلك جميع الصفحات من ملفي PDF المصدر وأضِفها إلى كائن PdfFileWriter، واحصل على كائن Page من خلال استدعاء التابع getPage() لكائن PdfFileReader ➍، ثم مرّر كائن Page إلى التابع addPage() الخاص بالكائن PdfFileReader ➎. نفّذ هذه الخطوات أولًا للمتغير pdf1Reader ثم للمتغير pdf2Reader مرة أخرى، ثم اكتب ملف PDF جديد اسمه combinedminutes.pdf عند الانتهاء من نسخ الصفحات من خلال تمرير كائن File إلى التابع write() الخاص بالكائن PdfFileWriter ➏. ملاحظة: لا يمكن لوحدة PyPDF2 إدراج صفحات في منتصف كائن PdfFileWriter، إذ يضيف التابع addPage() الصفحات إلى نهاية الملف فقط. أنشأنا ملف PDF جديد يدمج صفحاتٍ من الملفين meetingminutes.pdf وmeetingminutes2.pdf في مستند واحد. تذكّر أنه يجب فتح كائن File الذي مرّرناه إلى الدالة PyPDF2.PdfFileReader() في وضع القراءة الثنائي من خلال تمرير الوسيط 'rb' بوصفه وسيطًا ثانيًا للدالة open()، ويجب فتح كائن File الذي مرّرناه إلى الدالة PyPDF2.PdfFileReader() في وضع الكتابة الثنائي باستخدام الوسيط 'wb'. تدوير الصفحات يمكن تدوير صفحات ملف PDF بمقدار مضاعفات 90 درجة باستخدام التوابع rotateClockwise() و rotateCounterClockwise(). لنمرّر أحد الأعداد الصحيحة 90 أو 180 أو 270 إلى هذه التوابع، ولندخِل ما يلي في الصدفة التفاعلية مع ملف meetingminutes.pdf الموجود في مجلد العمل الحالي: >>> import PyPDF2 >>> minutesFile = open('meetingminutes.pdf', 'rb') >>> pdfReader = PyPDF2.PdfFileReader(minutesFile) ➊ >>> page = pdfReader.getPage(0) ➋ >>> page.rotateClockwise(90) {'/Contents': [IndirectObject(961, 0), IndirectObject(962, 0), --snip-- } >>> pdfWriter = PyPDF2.PdfFileWriter() >>> pdfWriter.addPage(page) ➌ >>> resultPdfFile = open('rotatedPage.pdf', 'wb') >>> pdfWriter.write(resultPdfFile) >>> resultPdfFile.close() >>> minutesFile.close() استخدمنا التابع getPage(0) لتحديد الصفحة الأولى من ملف PDF ➊، ثم استدعينا التابع rotateClockwise(90) لتلك الصفحة ➋، ثم كتبنا ملف PDF جديد مع الصفحة التي دوّرناها وحفظناه بالاسم rotatedPage.pdf ➌. سيحتوي ملف PDF الناتج على صفحة واحدة مع تدويرها بمقدار 90 درجة باتجاه عقارب الساعة كما هو موضّح في الشكل التالي، وتحتوي القيم المُعادة من التابعين rotateClockwise() و rotateCounterClockwise() على الكثير من المعلومات التي يمكنك تجاهلها. ملف rotatedPage.pdf مع تدوير الصفحة بمقدار 90 درجة باتجاه عقارب الساعة دمج الصفحات يمكن لوحدة PyPDF2 أن تدمج محتويات صفحة مع صفحة أخرى، ويُعَد ذلك مفيدًا لإضافة شعار أو علامة زمنية أو مائية إلى الصفحة، إذ تسهّل لغة بايثون إضافة علامات مائية إلى ملفات متعددة وعلى الصفحات التي يحدّدها برنامجك فقط. نزّل الملف watermark.pdf، ثم ضعه في مجلد العمل الحالي مع الملف meetingminutes.pdf، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> import PyPDF2 >>> minutesFile = open('meetingminutes.pdf', 'rb') ➊ >>> pdfReader = PyPDF2.PdfFileReader(minutesFile) ➋ >>> minutesFirstPage = pdfReader.getPage(0) ➌ >>> pdfWatermarkReader = PyPDF2.PdfFileReader(open('watermark.pdf', 'rb')) ➍ >>> minutesFirstPage.mergePage(pdfWatermarkReader.getPage(0)) ➎ >>> pdfWriter = PyPDF2.PdfFileWriter() ➏ >>> pdfWriter.addPage(minutesFirstPage) ➐ >>> for pageNum in range(1, pdfReader.numPages): pageObj = pdfReader.getPage(pageNum) pdfWriter.addPage(pageObj) >>> resultPdfFile = open('watermarkedCover.pdf', 'wb') >>> pdfWriter.write(resultPdfFile) >>> minutesFile.close() >>> resultPdfFile.close() أنشأنا في المثال السابق كائن PdfFileReader للملف meetingminutes.pdf ➊، واستدعينا التابع getPage(0) للحصول على كائن Page للصفحة الأولى وتخزين هذا الكائن في المتغير minutesFirstPage ➋. أنشأنا بعد ذلك كائن PdfFileReader للملف watermark.pdf ➌، واستدعينا التابع mergePage() للمتغير minutesFirstPage ➍، فالوسيط الذي نمرّره إلى التابع mergePage() هو كائن Page للصفحة الأولى من الملف watermark.pdf. استدعينا التابع mergePage() للمتغير minutesFirstPage، وبالتالي أصبح هذا المتغير يمثّل الصفحة الأولى التي وضعنا عليها علامة مائية، ثم أنشأنا كائن PdfFileWriter ➎ وأضفنا الصفحة الأولى التي وضعنا عليها علامة مائية ➏، ثم مررنا بحلقة على بقية الصفحات الموجودة في الملف meetingminutes.pdf، وأضفناها إلى الكائن PdfFileWriter ➐. أخيرًا، فتحنا ملف PDF جديد اسمه watermarkedCover.pdf، وكتبنا محتويات الكائن PdfFileWriter فيه. يبين الشكل التالي النتائج، حيث يحتوي ملف PDF الجديد watermarkedCover.pdf على جميع محتويات الملف meetingminutes.pdf، وتحمل الصفحة الأولى فيه علامة مائية: ملف PDF الأصلي (على اليسار) وملف PDF للعلامة مائية (في الوسط) وملف PDF لدمج الملفين (على اليمين) تشفير ملفات PDF يمكن لكائن PdfFileWriter أيضًا إضافة تشفيرٍ إلى مستند PDF. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import PyPDF2 >>> pdfFile = open('meetingminutes.pdf', 'rb') >>> pdfReader = PyPDF2.PdfFileReader(pdfFile) >>> pdfWriter = PyPDF2.PdfFileWriter() >>> for pageNum in range(pdfReader.numPages): pdfWriter.addPage(pdfReader.getPage(pageNum)) ➊ >>> pdfWriter.encrypt('swordfish') >>> resultPdf = open('encryptedminutes.pdf', 'wb') >>> pdfWriter.write(resultPdf) >>> resultPdf.close() استدعِ التابع encrypt() ومرّر إليه سلسلة كلمة المرور ➊ قبل استدعاء التابع write() للحفظ في الملف. يمكن أن تحتوي ملفات PDF على كلمة مرور المستخدم user password التي تسمح لك بعرض ملف PDF وكلمة مرور المالك owner password التي تسمح لك بضبط أذونات للطباعة والتعليق واستخراج النص وميزات أخرى، حيث تُعَد كلمة مرور المستخدم وكلمة مرور المالك الوسيطين الأول والثاني للتابع encrypt() على التوالي. إذا مرّرنا سلسلة نصية واحدة فقط كوسيط إلى التابع encrypt()، فسنستخدمها لكلمتي المرور. نسخنا في المثال السابق صفحات الملف meetingminutes.pdf إلى كائن PdfFileWriter الذي شفّرناه بكلمة المرور swordfish، وفتحنا ملف PDF جديد بالاسم encryptedminutes.pdf، وكتبنا محتويات الكائن PdfFileWriter في ملف PDF الجديد، ويجب إدخال كلمة المرور قبل التمكّن من عرض الملف encryptedminutes.pdf. قد ترغب في حذف الملف meetingminutes.pdf الأصلي غير المُشفَّر بعد التأكد من تشفير نسخته بصورة صحيحة. تطبيق عملي: دمج صفحات مختارة من عدة ملفات PDF لنفترض أن لديك مهمة مملة تتمثل في دمج عشرات من مستندات PDF في ملف PDF واحد، ويحتوي كل مستند على ورقة غلاف في الصفحة الأولى، ولكنك لا تريد تكرار ورقة الغلاف في النتيجة النهائية، إذ توجد الكثير من البرامج المجانية لدمج ملفات PDF، ولكن تدمج الكثير منها الملفات بأكملها مع بعضها البعض ببساطة. إذًا لنكتب برنامج بايثون لتخصيص الصفحات التي تريدها في ملف PDF الناتج عن دمج عدة مستندات PDF. إليك الخطوات العامة التي سيطبّقها برنامجك: البحث عن جميع ملفات PDF الموجودة في مجلد العمل الحالي. فرز أسماء الملفات بحيث تُضاف ملفات PDF بالترتيب. كتابة جميع الصفحات باستثناء الصفحة الأولى من كل ملف PDF في الملف الناتج. ولكن ستحتاج شيفرتك البرمجية إلى تطبيق الخطوات التالية من ناحية التنفيذ: استدعاء التابع os.listdir() للعثور على كافة الملفات الموجودة في مجلد العمل وإزالة الملفات التي ليست ملفات PDF. استدعاء تابع فرز القائمة sort() الخاصة بلغة بايثون لترتيب أسماء الملفات أبجديًا. إنشاء كائن PdfFileWriter لملف PDF الناتج. التكرار ضمن حلقة على كل ملف PDF لإنشاء كائن PdfFileReader له. التكرار ضمن حلقة على كل صفحة (ما عدا الصفحة الأولى) في كل ملف PDF. إضافة الصفحات إلى ملف PDF الناتج. كتابة ملف PDF الناتج في ملفٍ اسمه allminutes.pdf. افتح تبويبًا جديدًا لإنشاء ملفٍ جديد في محرّرك واحفظه بالاسم combinePdfs.py. الخطوة الأولى: البحث عن جميع ملفات PDF أولًا، يجب أن يحصل برنامجك على قائمةٍ بجميع الملفات التي لها الامتداد .pdf الموجودة في مجلد العمل الحالي وفرزها، لذا يجب أن تكون شيفرتك البرمجية تبدو كما يلي: #! python3 # combinePdfs.py - دمج جميع ملفات PDF الموجودة في مجلد العمل الحالي في ملف PDF واحد ➊ import PyPDF2, os # الحصول على جميع أسماء ملفات PDF pdfFiles = [] for filename in os.listdir('.'): if filename.endswith('.pdf'): ➋ pdfFiles.append(filename) ➌ pdfFiles.sort(key = str.lower) ➍ pdfWriter = PyPDF2.PdfFileWriter() # التكرار ضمن حلقة على جميع ملفات PDF # التكرار ضمن حلقة على جميع الصفحات (باستثناء الصفحة الأولى) وإضافتها # حفظ ملف PDF الناتج في ملف السطر الأول في الشيفرة البرمجية السابقة هو سطر Shebang (سطر يبدأ بالسلسلة النصية "#!")، والسطر الثاني هو التعليق الوصفي لما يفعله البرنامج، ثم تستورد الشيفرة البرمجية وحدات os و PyPDF2 ➊. يعيد استدعاء التابع os.listdir('.') قائمةً بالملفات الموجودة في مجلد العمل الحالي، حيث تتكرر الشيفرة البرمجية ضمن حلقة على هذه القائمة وتضيف الملفات التي لها الامتداد .pdf فقط إلى القائمة pdfFiles ➋. تُفرَز بعد ذلك هذه القائمة وفق الترتيب الأبجدي باستخدام وسيط الكلمة المفتاحية Keyword Argument الذي هو key = str.lower الخاص بالتابع sort() ➌، ويُنشَأ كائن PdfFileWriter للاحتفاظ بصفحات PDF المدموجة ➍. أخيرًا، توجد بعض التعليقات التي توضّح ما تبقى من البرنامج. الخطوة الثانية: فتح ملفات PDF يجب الآن أن يقرأ البرنامج كلّ ملف PDF موجودٍ في القائمة pdfFiles، لذا أضِف ما يلي إلى برنامجك: #! python3 # combinePdfs.py - دمج جميع ملفات PDF الموجودة في مجلد العمل الحالي في ملف PDF واحد import PyPDF2, os # الحصول على جميع أسماء ملفات PDF pdfFiles = [] --snip-- # التكرار ضمن حلقة على جميع ملفات PDF for filename in pdfFiles: pdfFileObj = open(filename, 'rb') pdfReader = PyPDF2.PdfFileReader(pdfFileObj) # التكرار ضمن حلقة على جميع الصفحات (باستثناء الصفحة الأولى) وإضافتها # حفظ ملف PDF الناتج في ملف تفتح الحلقة اسم ملف لكل ملف PDF في وضع القراءة الثنائي من خلال استدعاء الدالة open() مع الوسيط الثاني 'rb'، حيث يعيد استدعاء الدالة open() كائن File المُمرَّر إلى الدالة PyPDF2.PdfFileReader() لإنشاء كائن PdfFileReader لملف PDF. الخطوة الثالثة: إضافة الصفحات يجب التكرار ضمن حلقة على كل صفحة من كل ملف PDF باستثناء الصفحة الأولى. إذًا أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # combinePdfs.py - دمج جميع ملفات PDF الموجودة في مجلد العمل الحالي في ملف PDF واحد import PyPDF2, os --snip-- # التكرار ضمن حلقة على جميع ملفات PDF for filename in pdfFiles: --snip-- # التكرار ضمن حلقة على جميع الصفحات (باستثناء الصفحة الأولى) وإضافتها ➊ for pageNum in range(1, pdfReader.numPages): pageObj = pdfReader.getPage(pageNum) pdfWriter.addPage(pageObj) # حفظ ملف PDF الناتج في ملف تنسخ الشيفرة البرمجية الموجودة داخل حلقة for كل كائن Page إلى كائن PdfFileWriter، ولكن تذكّر أنك تريد تخطي الصفحة الأولى. يجب أن تبدأ حلقتك من القيمة 1 ➊ لأن وحدة PyPDF2 تَعُدّ القيمة 0 هي الصفحة الأولى، ثم تصل إلى العدد الصحيح الموجود في pdfReader.numPages دون تضمينه في الحلقة. الخطوة الرابعة: حفظ النتائج سيحتوي المتغير pdfWriter على كائن PdfFileWriter مع الصفحات الخاصة بجميع ملفات PDF المدموجة بعد الانتهاء من حلقات for المتداخلة، والخطوة الأخيرة هي كتابة هذا المحتوى في ملفٍ على القرص الصلب، لذا أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # combinePdfs.py - دمج جميع ملفات PDF الموجودة في مجلد العمل الحالي في ملف PDF واحد import PyPDF2, os --snip-- # التكرار ضمن حلقة على جميع الملفات PDF for filename in pdfFiles: --snip-- # التكرار ضمن حلقة على جميع الصفحات (باستثناء الصفحة الأولى) وإضافتها for pageNum in range(1, pdfReader.numPages): --snip-- # حفظ ملف PDF الناتج في ملف pdfOutput = open('allminutes.pdf', 'wb') pdfWriter.write(pdfOutput) pdfOutput.close() يؤدي تمرير 'wb' إلى الدالة open() إلى فتح ملف PDF الناتج allminutes.pdf في وضع الكتابة الثنائي، وبالتالي يؤدي تمرير كائن File الناتج إلى التابع write() إلى إنشاء ملف PDF الفعلي، ويؤدي استدعاء التابع close() إلى إنهاء البرنامج. أفكار لبرامج مماثلة تتيح لك القدرة على إنشاء ملفات PDF من صفحات ملفات PDF الأخرى إنشاءَ برامج يمكنها تطبيق ما يلي: قص صفحات محددة من ملفات PDF. إعادة ترتيب الصفحات في ملف PDF. إنشاء ملف PDF من الصفحات التي تحتوي على بعض النصوص التي يحدّدها التابع extractText(). مستندات وورد Word يمكن للغة بايثون إنشاء وتعديل مستندات وورد التي لها امتداد الملفات .docx باستخدام الوحدة docx التي يمكنك تثبيتها من خلال تشغيل الأمر pip install --user -U python-docx==0.8.10. اتبع الإرشادات الخاصة بتثبيت الوحدات الخارجية التي سنوضّحها في [مقالٍ لاحق](رابط مقال سنترجمه لاحقًا https://automatetheboringstuff.com/2e/appendixa/). ملاحظة: إذا استخدمتَ الأداة pip لتثبيت وحدة Python-Docx لأول مرة، فتأكد من تثبيت python-docx وليس docx، فاسم الحزمة docx مُخصَّص لوحدةٍ مختلفة لن نتحدّث عنها في هذا المقال، ولكن ستحتاج إلى تشغيل الأمر import docx وليس import python-docx عندما تريد استيراد الوحدة من حزمة python-docx. إن لم يكن لديك تطبيق وورد، فيمكنك استخدام ليبر أوفيس رايتر LibreOffice Writer وأوبن أوفيس رايتر OpenOffice Writer، وهما تطبيقان بديلان مجانيان لأنظمة ويندوز Windows وماك macOS ولينكس Linux، ويٌستخدمان لفتح ملفات .docx، حيث يمكنك تنزيلهما من موقعهما الرسمي، ويوجد التوثيق الكامل لوحدة Python-Docx على موقعها الرسمي. سنركّز في هذا المقال على وورد في نظام تشغيل ويندوز بالرغم من وجود إصدار من وورد على نظام تشغيل ماك macOS. تحتوي ملفات .docx على هياكل متعددة مقارنةً بالنصوص العادية، ويُمثَّل كلّ هيكلٍ منها بثلاثة أنواع مختلفة من البيانات في الوحدة Python-Docx، حيث يمثِّل كائن Document المستند بأكمله، ويحتوي كائن Document على قائمةٍ من كائنات Paragraph للفقرات الموجودة في المستند، إذ تبدأ فقرة جديدة عندما يضغط المستخدم على زر ENTER أو RETURN أثناء الكتابة في مستند وورد، ويحتوي كل كائن من كائنات Paragraph على قائمة تضم كائن Run واحدًا أو أكثر. تحتوي الفقرة المكونة من جملة واحدة في الشكل التالي على أربعة كائنات Run: كائنات Run الموجودة ضمن كائن Paragraph يُعَد النص الموجود في مستند وورد أكثرَ من مجرد سلسلة نصية، فلهذا النص نوع خطٍ وحجم ولون ومعلومات تنسيقٍ أخرى مرتبطةٌ به، حيث يمثل النمط Style في وورد مجموعةً من هذه السمات. يمثّل الكائن Run التشغيل المتجاور للنصوص التي لها النمط نفسه، إذ يجب أن يكون هناك كائن Run جديد كلما تغيّر نمط النص. قراءة مستندات وورد لنختبر الآن وحدة docx، لذا نزّل الملف demo.docx واحفظه في مجلد العمل، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> import docx ➊ >>> doc = docx.Document('demo.docx') ➋ >>> len(doc.paragraphs) 7 ➌ >>> doc.paragraphs[0].text 'Document Title' ➍ >>> doc.paragraphs[1].text 'A plain paragraph with some bold and some italic' ➎ >>> len(doc.paragraphs[1].runs) 4 ➏ >>> doc.paragraphs[1].runs[0].text 'A plain paragraph with some ' ➐ >>> doc.paragraphs[1].runs[1].text 'bold' ➑ >>> doc.paragraphs[1].runs[2].text ' and some ' ➒ >>> doc.paragraphs[1].runs[3].text 'italic' نفتح ملف .docx في بايثون ونستدعي الدالة docx.Document() ونمرّر إليها اسم الملف demo.docx ➊، مما يؤدي إلى إعادة كائن Document الذي يحتوي على السمة paragraphs التي تمثل قائمةً من كائنات Paragraph. إذا استدعينا الدالة len() للسمة doc.paragraphs، فستعيد القيمة 7، مما يخبرنا بوجود سبعة كائنات Paragraph في هذا المستند ➋. يمتلك كل كائن من كائنات Paragraph السمةَ text التي تحتوي على سلسلة نصية من النص الموجود في تلك الفقرة (بدون معلومات النمط). تحتوي السمة text الأولى في مثالنا على النص 'DocumentTitle' ➌، وتحتوي السمة text الثانية على النص 'A plain paragraph with some bold and some italic' ➍. يمتلك كل كائن Paragraph أيضًا على السمة runs التي تمثل قائمةً من كائنات Run التي تمتلك أيضًا السمة text التي تحتوي على نص كائن Run الخاص بها. لنلقِ نظرةً على سمات text في كائن Paragraph الثاني، والتي تمثّل النص 'A plain paragraph with some bold and some italic'، حيث يعطي استدعاء الدالة len() لهذا الكائن القيمةَ 4، والتي تمثل وجود أربعة كائنات Run ➎. يحتوي كائن Run الأول على النص 'A plain paragraph with some ' ➏، ثم يتغير النص إلى نمط خط عريض، وبالتالي يبدأ النص 'bold' كائن Run جديد ➐، ثم يعود النص إلى نمط خطٍ غير عريض، مما يعطي كائن Run ثالث، وهو النص ' and some ' ➑. أخيرًا، يحتوي كائن Run الرابع والأخير على النص 'italic' بنمط خطٍ مائل ➒. ستتمكن برامج بايثون الآن باستخدام الوحدة Python-Docx من قراءة النص من ملف .docx واستخدامه مثل أيّ قيمة سلسلة نصية أخرى. الحصول على النص الكامل من ملف امتداده .docx إذا كان اهتمامك بالنص فقط دون الاهتمام بمعلومات التنسيق في مستند وورد، فيمكنك استخدام الدالة getText() التي تأخذ اسم ملف .docx وتعيد قيمة سلسلة نصية واحدة تمثّل النص الخاص بهذا الملف. افتح تبويبًا جديدًا لإنشاء ملف جديد في محرّرك، وأدخِل الشيفرة البرمجية التالية، واحفظ الملف بالاسم readDocx.py: #! python3 import docx def getText(filename): doc = docx.Document(filename) fullText = [] for para in doc.paragraphs: fullText.append(para.text) return '\n'.join(fullText) تفتح الدالة getText() مستند وورد، وتتكرر ضمن حلقة على كافة كائنات Paragraph الموجودة في القائمة paragraphs، ثم تلحِق النص الخاص بها بالقائمة الموجودة في المتغير fullText. تُضَم السلاسل النصية الموجودة في المتغير fullText بعد انتهاء الحلقة مع محارف السطر الجديد. يمكن استيراد برنامج readDocx.py مثل أي وحدة أخرى، وإذا أردتَ النص فقط من مستند وورد، فيمكنك إدخال ما يلي: >>> import readDocx >>> print(readDocx.getText('demo.docx')) Document Title A plain paragraph with some bold and some italic Heading, level 1 Intense quote first item in unordered list first item in ordered list يمكنك أيضًا ضبط الدالة getText() لتعديل السلسلة النصية قبل إعادتها، فمثلًا يمكننا وضع مسافة بادئة لكل فقرة من خلال تعديل استدعاء التابع append() في الملف readDocx.py كما يلي: fullText.append(' ' + para.text) يمكننا إضافة مسافة مزدوجة بين الفقرات من خلال تغيير شيفرة استدعاء التابع join() إلى ما يلي: return '\n\n'.join(fullText) لاحظ أنك لا تحتاج سوى بضعة أسطر من الشيفرة البرمجية لكتابة الدوال التي تقرأ ملف .docx وتعيد سلسلة نصية من محتوى هذا الملف حسب رغبتك. تنسيق كائنات Paragraph وكائنات Run يمكنك رؤية الأنماط في وورد ضمن نظام ويندوز بالضغط على المفاتيح Ctrl-Alt-Shift-S لعرض لوحة الأنماط Styles التي تشبه الشكل التالي، بينما يمكنك عرض لوحة الأنماط في نظام تشغيل ماك macOS بالنقر على عنصر قائمة العرض View ثم الأنماط Styles. عرض لوحة الأنماط بالضغط على المفاتيح CTRL-ALT-SHIFT-S في نظام ويندوز يستخدم برنامج وورد ومعالجات النصوص الأخرى أنماطًا للحفاظ على تناسق العرض المرئي لأنواع النصوص المتشابهة وسهولة تغييره، فمثلًا قد ترغب في ضبط فقرات النص لتكون بخطٍ من النوع Times New Roman وحجمه 11 نقطة ومتحاذٍ من جهة اليسار وغير مضبوط من جهة اليمين، حيث يمكنك إنشاء نمط باستخدام هذه الإعدادات وإسناده لجميع فقرات النص، وإذا أردتَ لاحقًا تغيير طريقة عرض جميع فقرات النص في المستند، فيمكنك تغيير النمط فقط، وستُحدَّث جميع تلك الفقرات تلقائيًا. هناك ثلاثة أنواع من الأنماط بالنسبة لمستندات وورد وهي: أنماط الفقرة Paragraph Styles التي يمكن تطبيقها على كائنات Paragraph، وأنماط المحارف Character Styles التي يمكن تطبيقها على كائنات Run، والأنماط المرتبطة Linked Styles التي يمكن تطبيقها على كلا النوعين من الكائنات. يمكنك إعطاء كائنات Paragraph وكائنات Run أنماطٍ من خلال ضبط السمة style الخاصة بها على سلسلةٍ نصية تمثّل اسم النمط، وإذا كانت هذه السمة مضبوطةً على القيمة None، فلن يكون هناك نمط مرتبط بكائن Paragraph أو كائن Run. إليك قيم السلاسل النصية لأنماط وورد الافتراضية: يجب إضافة ' Char' إلى نهاية اسم النمط عند استخدام نمط مرتبط بكائن Run، فمثلًا يمكنك ضبط النمط المرتبط Quote لكائن Paragraph من خلال استخدام paragraphObj.style = 'Quote'، ولكنك ستستخدم runObj.style = 'Quote Char' بالنسبة لكائن Run. الأنماط الوحيدة التي يمكن استخدامها في الإصدار 0.8.10 من وحدة Python-Docx هي أنماط وورد الافتراضية والأنماط الموجودة في ملف .docx المفتوح، ولا يمكن إنشاء أنماط جديدة، بالرغم من أن ذلك قد تغيّر في الإصدارات اللاحقة من وحدة Python-Docx. إنشاء مستندات وورد مع أنماط غير افتراضية إذا أردتَ إنشاء مستندات وورد تستخدم أنماطٍ مختلفة عن الأنماط الافتراضية، فيجب فتح وورد على مستند فارغ وإنشاء الأنماط بنفسك من خلال النقر على زر "نمط جديد New Style" الموجود أسفل لوحة الأنماط كما هو موضح في الشكل التالي على نظام ويندوز: زر نمط جديد New Style (على اليسار) ونافذة إنشاء نمط جديد من التنسيق Create New Style from Formatting (على اليمين). سيؤدي الضغط على زر نمط جديد إلى فتح نافذة "إنشاء نمط جديد من التنسيق Create New Style from Formatting" حيث يمكنك إدخال النمط الجديد. ارجع بعد ذلك إلى الصدفة التفاعلية وافتح هذا المستند الفارغ باستخدام الدالة docx.Document()، واستخدمه كأساسٍ لمستند وورد الخاص بك. سيكون الاسم الذي أعطيته لهذا النمط متاحًا الآن للاستخدام مع وحدة Python-Docx. سمات الكائن Run يمكن تنسيق كائنات Run باستخدام سمات text، حيث يمكن ضبط كل سمة على قيمة من ثلاث قيم هي: القيمة True (تكون السمة مُفعَّلةً دائمًا بغض النظر عن الأنماط الأخرى المُطبَّقة على الكائن Run)، أو القيمة False (تكون السمة مُعطَّلة دائمًا)، أو القيمة None (الإعداد الافتراضي لأيّ نمطٍ مضبوط للكائن Run). يوضّح الجدول التالي السمات text التي يمكن ضبطها لكائنات Run: السمة وصفها السمة bold يظهر النص بخط عريض السمة italic يظهر النص بخط مائل السمة underline يوضَع خط تحت النص السمة strike يظهر النص مع خطٍ في وسطه السمة double_strike يظهر النص مع خط مزدوج في وسطه السمة all_caps يظهر النص بحروف كبيرة السمة small_caps يظهر النص بحروف كبيرة، وتكون الحروف الصغيرة أصغر بنقطتين السمة shadow يظهر النص مع ظل السمة outline يظهر النص مُحدَّدًا وليس ممتلئًا السمة rtl النص مكتوب من اليمين إلى اليسار السمة imprint يظهر النص مضغوطًا إلى داخل الصفحة السمة emboss يبدو النص مرتفعًا عن الصفحة ارتفاعًا بارزًا أدخِل مثلًا ما يلي في الصدفة التفاعلية لتغيير أنماط الملف demo.docx: >>> import docx >>> doc = docx.Document('demo.docx') >>> doc.paragraphs[0].text 'Document Title' >>> doc.paragraphs[0].style # The exact id may be different: _ParagraphStyle('Title') id: 3095631007984 >>> doc.paragraphs[0].style = 'Normal' >>> doc.paragraphs[1].text 'A plain paragraph with some bold and some italic' >>> (doc.paragraphs[1].runs[0].text, doc.paragraphs[1].runs[1].text, doc. paragraphs[1].runs[2].text, doc.paragraphs[1].runs[3].text) ('A plain paragraph with some ', 'bold', ' and some ', 'italic') >>> doc.paragraphs[1].runs[0].style = 'QuoteChar' >>> doc.paragraphs[1].runs[1].underline = True >>> doc.paragraphs[1].runs[3].underline = True >>> doc.save('restyled.docx') استخدمنا في المثال السابق سمات text و style لرؤية ما هو موجود في الفقرات ضمن المستند بسهولة، إذ يمكننا أن نرى أنه من السهل تقسيم الفقرة إلى كائنات Run والوصول إلى كلٍّ منها على حدة، لذلك يمكننا الحصول على كائنات Run الأولى والثانية والرابعة في الفقرة الثانية، وتنسيق كلِّ منها، وحفظ النتائج في مستند جديد. ستكون للكلمات Document Title الموجودة في أعلى المستند restyled.docx النمط العادي Normal بدلًا من نمط العنوان Title، وسيكون لكائن Run الخاص بالنص A plain paragraph with some النمط QuoteChar، وسيكون لكائني Run الخاصين بالكلمتين bold و italic سمات underline المضبوطة على القيمة True. يوضح الشكل التالي كيف تبدو أنماط الفقرات وكائنات Run في المستند restyled.docx: ملف restyled.docx كتابة مستندات وورد لندخِل ما يلي في الصدفة التفاعلية: >>> import docx >>> doc = docx.Document() >>> doc.add_paragraph('Hello, world!') <docx.text.Paragraph object at 0x0000000003B56F60> >>> doc.save('helloworld.docx') يمكننا إنشاء ملف .docx من خلال استدعاء الدالة docx.Document() لإعادة كائن مستند Document وورد جديد وفارغ، ويضيف التابع add_paragraph() الخاص بالمستند فقرةً نصية جديدة إلى المستند ويعيد مرجعًا إلى كائن Paragraph المُضاف. نمرّر سلسلةً نصية تمثّل اسم الملف إلى التابع save() الخاص بالمستند عند الانتهاء من إضافة النص لحفظ الكائن Document في ملف. تؤدي الشيفرة البرمجية السابقة إلى إنشاء ملف بالاسم helloworld.docx في مجلد العمل الحالي والذي يبدو عند فتحه كما يلي: مستند وورد الذي أنشأناه باستخدام الاستدعاء add_paragraph('Hello, world!') يمكنك إضافة فقرات من خلال استدعاء التابع add_paragraph() مرةً أخرى مع نص الفقرة الجديدة، أو يمكنك استدعاء التابع add_run() الخاص بالفقرة وتمرير سلسلة نصية إليه لإضافة نص إلى نهاية فقرة موجودة مسبقًا. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import docx >>> doc = docx.Document() >>> doc.add_paragraph('Hello world!') <docx.text.Paragraph object at 0x000000000366AD30> >>> paraObj1 = doc.add_paragraph('This is a second paragraph.') >>> paraObj2 = doc.add_paragraph('This is a yet another paragraph.') >>> paraObj1.add_run(' This text is being added to the second paragraph.') <docx.text.Run object at 0x0000000003A2C860> >>> doc.save('multipleParagraphs.docx') لاحظ أن النص "This text is being added to the second paragraph." أضيف إلى كائن Paragraph في المتغير paraObj1، وهو الفقرة الثانية المُضافة إلى المتغير doc. تعيد الدالتان add_paragraph() و add_run() كائنات Paragraph و Run على التوالي بحيث توفّر عليك عناء استخراجها في خطوة منفصلة. ضع في بالك أنه يمكن إضافة كائنات Paragraph الجديدة إلى نهاية المستند فقط، ويمكن إضافة كائنات Run الجديدة إلى نهاية كائن Paragraph فقط، وذلك اعتبارًا من الإصدار 0.8.10 من وحدة Python-Docx. أخيرًا، يمكن استدعاء التابع save() مرة أخرى لحفظ التغييرات الإضافية التي أجريتها. سيبدو المستند الناتج مثل المستند الموضّح في الشكل التالي: المستند الذي يحتوي على كائنات Paragraph و Run المتعددة المُضافة تأخذ كلّ من الدالتين add_paragraph() و add_run() وسيطًا ثانيًا اختياريًا، وهو سلسلة نصية من نمط الكائن Paragraph أو Run كما في المثال التالي: >>> doc.add_paragraph('Hello, world!', 'Title') يضيف السطر السابق فقرةً تحتوي على النص "Hello, world!" من نمط العنوان Title Style. إضافة العناوين Headings يؤدي استدعاء الدالة add_heading() إلى إضافة فقرة تحتوي على أحد أنماط العناوين. لندخِل ما يلي في الصدفة التفاعلية: >>> doc = docx.Document() >>> doc.add_heading('Header 0', 0) <docx.text.Paragraph object at 0x00000000036CB3C8> >>> doc.add_heading('Header 1', 1) <docx.text.Paragraph object at 0x00000000036CB630> >>> doc.add_heading('Header 2', 2) <docx.text.Paragraph object at 0x00000000036CB828> >>> doc.add_heading('Header 3', 3) <docx.text.Paragraph object at 0x00000000036CB2E8> >>> doc.add_heading('Header 4', 4) <docx.text.Paragraph object at 0x00000000036CB3C8> >>> doc.save('headings.docx') وسطاء الدالة add_heading() هي سلسلة نصية تمثّل نص العنوان وعدد صحيح قيمته من 0 إلى 4، حيث يجعل العدد الصحيح 0 العنوان من النمط Title style، والذي يُستخدَم في الجزء العلوي من المستند، والأعداد الصحيحة من 1 إلى 4 مُخصَّصة لمستويات العناوين المختلفة، حيث يكون العدد 1 هو العنوان الرئيسي والعدد 4 هو العنوان الفرعي الأدنى. تعيد الدالة add_heading() كائن Paragraph لتوفّر عليك إجراء خطوة استخراجه من كائن Document في خطوة منفصلة. سيبدو ملف headings.docx الناتج مثل المستند الموضّح في الشكل التالي: مستند headings.docx الذي يحتوي على العناوين من 0 إلى 4 إضافة فواصل الأسطر والصفحات يمكن إضافة فاصل أسطر بدلًا من بدء فقرة جديدة بالكامل من خلال استدعاء التابع add_break() لكائن Run الذي تريد ظهور الفاصل بعده، وإذا أردتَ إضافة فاصل صفحات، فيجب تمرير القيمة docx.enum.text.WD_BREAK.PAGE بوصفها وسيطًا وحيدًا للتابع add_break() كما في السطر ➊ من المثال التالي: >>> doc = docx.Document() >>> doc.add_paragraph('This is on the first page!') <docx.text.Paragraph object at 0x0000000003785518> ➊ >>> doc.paragraphs[0].runs[0].add_break(docx.enum.text.WD_BREAK.PAGE) >>> doc.add_paragraph('This is on the second page!') <docx.text.Paragraph object at 0x00000000037855F8> >>> doc.save('twoPage.docx') تؤدي الشيفرة البرمجية السابقة إلى إنشاء مستند وورد مؤلف من صفحتين مع وجود النص "This is on the first page!" في الصفحة الأولى والنص "This is on the second page!" في الصفحة الثانية. لا يزال هناك مساحة كبيرة في الصفحة الأولى بعد النص "This is on the first page!"، ولكننا أجبرنا الفقرة التالية على البدء في صفحة جديدة من خلال إدراج فاصل صفحات بعد كائن Run الأول للفقرة الأولى ➊. إضافة الصور تحتوي كائنات Document على التابع add_picture() الذي يتيح إضافة صورةٍ إلى نهاية المستند. لنفترض أن لديك ملفًا اسمه zophie.png مثلًا في مجلد العمل الحالي، حيث يمكنك إضافة الصورة zophie.png إلى نهاية مستندك بعرض 1 بوصة وارتفاع 4 سنتيمترات من خلال إدخال ما يلي (يستخدم برنامج وورد الوحدات الإنجليزية والمترية): >>> doc.add_picture('zophie.png', width=docx.shared.Inches(1), height=docx.shared.Cm(4)) <docx.shape.InlineShape object at 0x00000000036C7D30> الوسيط الأول للتابع add_picture() هو سلسلة نصية تمثّل اسم ملف الصورة، وتضبط وسطاء الكلمات المفتاحية width و height الاختيارية عرض الصورة وارتفاعها في المستند، وإذا تُرِكا دون ضبط، فسيكون العرض والارتفاع الافتراضي هو الحجم الطبيعي للصورة. قد تفضّل تحديد ارتفاع الصورة وعرضها بوحدات مألوفة مثل وحدات البوصة والسنتيمتر، لذا يمكنك استخدام الدالتين docx.shared.Inches() و docx.shared.Cm() عندما تحدّد وسطاء الكلمات المفتاحية width و height. إنشاء ملفات PDF من مستندات وورد لا تسمح لك وحدة PyPDF2 بإنشاء مستندات PDF مباشرةً، ولكن توجد طريقة لإنشاء ملفات PDF باستخدام بايثون إذا كنت تستخدم نظام ويندوز مع وجود مايكروسوفت وورد مثبَّتًا عليه، لذا ستحتاج إلى تثبيت الحزمة Pywin32 من خلال تشغيل الأمر pip install --user -U pywin32==224. إذا استخدمتَ هذه الحزمة مع وحدة docx، فيمكنك إنشاء مستندات وورد ثم تحويلها إلى ملفات PDF باستخدام السكربت التالي، لذا افتح تبويبًا جديدًا لإنشاء ملف جديد في محرّرك، وأدخِل الشيفرة البرمجية التالية، واحفظها بالاسم convertWordToPDF.py: # يعمل هذا السكربت على نظام ويندوز فقط، ويجب أن يكون وورد مثبتًا عليه import win32com.client # install with "pip install pywin32==224" import docx wordFilename = 'your_word_document.docx' pdfFilename = 'your_pdf_filename.pdf' doc = docx.Document() # ضع شيفرة إنشاء مستند وورد هنا doc.save(wordFilename) wdFormatPDF = 17 # Word's numeric code for PDFs. wordObj = win32com.client.Dispatch('Word.Application') docObj = wordObj.Documents.Open(wordFilename) docObj.SaveAs(pdfFilename, FileFormat=wdFormatPDF) docObj.Close() wordObj.Quit() يمكنك كتابة برنامج ينتج عنه ملفات PDF مع المحتوى الخاص بك من خلال استخدام وحدة docx لإنشاء مستند وورد، ثم استخدام وحدة win32com.client الخاصة بحزمة Pywin32 لتحويله إلى ملف PDF. ضع استدعاءات دوال الوحدة docx مكان التعليق # ضع شيفرة إنشاء مستند وورد هنا لإنشاء المحتوى الخاص بك لملف PDF في مستند وورد. قد تبدو هذه الطريقة لإنتاج ملفات PDF معقدة، ولكن تكون الحلول البرمجية الاحترافية معقدةً في أغلب الأحيان. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج للتأكد من تشفير ملفات PDF استخدم الدالة os.walk() لكتابة سكربتٍ يمر على كل ملف PDF في المجلد ومجلداته الفرعية، وشفّر ملفات PDF باستخدام كلمة المرور المتوفرة في سطر الأوامر، واحفظ كل ملف PDF مشفّر مع إضافة اللاحقة _encrypted.pdf إلى اسم الملف الأصلي، ثم اطلب من البرنامج محاولة قراءة الملف وفك تشفيره للتأكد من تشفيره بصورة صحيحة قبل حذف الملف الأصلي. اكتب بعد ذلك برنامجًا يبحث عن جميع ملفات PDF المشفرة في المجلد ومجلداته الفرعية، وينشئ نسخة مشفَّرة من ملف PDF باستخدام كلمة المرور المتوفرة. إذا كانت كلمة المرور غير صحيحة، فيجب على البرنامج طباعة رسالة للمستخدم والانتقال إلى ملف PDF التالي. برنامج لإنشاء دعوات مخصصة في مستندات وورد لنفترض أن لديك ملفًا نصيًا بأسماء الضيوف، حيث يحتوي الملف guests.txt على اسم شخص واحد في كل سطر كما يلي: Prof. Plum Miss Scarlet Col. Mustard Al Sweigart RoboCop اكتب برنامجًا ينشئ مستند وورد يحتوي على دعوات مخصصة كما يلي: مستند وورد الذي أنشأناه باستخدام سكربت الدعوات المخصصة يمكن للوحدة Python-Docx استخدام الأنماط الموجودة مسبقًا في مستند وورد فقط، لذا يجب أولًا إضافة هذه الأنماط إلى ملف وورد فارغ ثم فتح هذا الملف باستخدام وحدة Python-Docx. يجب أن تكون هناك دعوة واحدة لكل صفحة في مستند وورد الناتج، لذا استدعِ التابع add_break() لإضافة فاصل صفحة بعد الفقرة الأخيرة من كل دعوة، وبالتالي يجب فتح مستند وورد واحد فقط لطباعة كافة الدعوات دفعةً واحدة. ملاحظة: يمكنك أيضًا تنزيل نموذج الملف guests.txt. برنامج لاستخدام هجوم القوة الغاشمة لكسر كلمة مرور ملفات PDF لنفترض أن لديك ملف PDF مشفَّرًا نسيت كلمة مروره، ولكنك تتذكر أنه كان كلمة إنجليزية واحدة، وتُعَد محاولة تخمين كلمة المرور التي نسيتها مهمةً مملة جدًا، لذا يمكنك كتابة برنامج يفك تشفير ملف PDF من خلال تجربة جميع الكلمات الإنجليزية الممكنة حتى يجد الكلمة الصحيحة، ويسمى ذلك هجوم القوة الغاشمة لإيجاد كلمة المرور. نزّل الملف النصي dictionary.txt الذي يحتوي على أكثر من 44000 كلمة إنجليزية بحيث توجد كلمة واحدة في كل سطر. استخدم مهارات قراءة الملفات التي تعلمتها سابقًا لإنشاء قائمةٍ بالسلاسل النصية التي تمثّل الكلمات من خلال قراءة الملف dictionary.txt، ثم المرور على كل كلمة في هذه القائمة، وتمريرها إلى التابع decrypt(). إذا أعاد هذا التابع العدد الصحيح 0، فستكون كلمة المرور خاطئة ويجب أن ينتقل برنامجك إلى كلمة المرور التالية، وإذا أعاد التابع decrypt() القيمة 1، فيجب أن يخرج برنامجك من الحلقة ويطبع كلمة المرور المُخترقة، ويجب عليك أيضًا تجربة كلٍّ من الحروف الكبيرة والصغيرة لكل كلمة. يستغرق استعراض جميع الكلمات الكبيرة والصغيرة البالغ عددها 88000 كلمة من ملف القاموس بضع دقائق، ولذلك يجب عدم استخدام كلمة إنجليزية بسيطة لكلمات المرور الخاصة بك. الخلاصة لا تُعد المعلومات النصية مُخصَّصة للملفات النصية العادية فقط، إذ يُحتمَل أن تتعامل مع ملفات PDF ومستندات وورد في كثير من الأحيان، حيث يمكنك استخدام وحدة PyPDF2 لقراءة وكتابة مستندات PDF، ولكن قد لا تؤدي قراءة النص من مستندات PDF دائمًا إلى ترجمة مثالية للسلاسل النصية بسبب تنسيق ملف PDF المعقد، وقد لا تكون بعض ملفات PDF قابلة للقراءة على الإطلاق، وبالتالي لن يحالفك الحظ في هذه الحالات إن لم تدعم التحديثات المستقبلية لوحدة PyPDF2 ميزات إضافية لملفات PDF. تُعَد مستندات وورد أكثر موثوقية، ويمكنك قراءتها باستخدام وحدة docx الخاصة بحزمة python-docx. يمكنك معالجة النصوص في مستندات وورد باستخدام كائنات Paragraph و Run، ويمكن أيضًا إعطاء هذه الكائنات أنماطًا، بالرغم من أن هذه الأنماط يجب أن تكون من مجموعة الأنماط الافتراضية أو الأنماط الموجودة في المستند مسبقًا. يمكنك إضافة فقرات وعناوين وفواصل وصور جديدة إلى المستند في نهايته فقط. ترجِع العديد من قيود التعامل مع ملفات PDF ومستندات وورد إلى أن هذه التنسيقات تهدف إلى عرضها بصورة جيدة للقرّاء، عوضًا عن سهولة تحليلها من طرف البرمجيات، لذا سنوضّح في المقال التالي تنسيقين شائعين آخرين لتخزين المعلومات هما: ملفات JSON و CSV المُصمَّمة لتستخدمها الحواسيب، وسترى أن لغة بايثون يمكنها العمل مع هذه التنسيقات بسهولة أكبر. ترجمة -وبتصرُّف- للمقال Working with PDF and Word documents لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: العمل مع جداول بيانات جوجل Google Sheets باستخدام لغة بايثون الكتابة في مستندات إكسل باستخدام لغة بايثون Python قراءة مستندات جداول إكسل باستخدام لغة بايثون Python مقدمة إلى تطبيق مستندات جوجل Google Docs
-
يُعَد تطبيق جداول بيانات جوجل Google Sheets تطبيقًا مجانيًا ومستندًا إلى الويب ومتاحًا لأيّ شخص لديه حساب جوجل Google أو عنوان جيميل Gmail، وأصبح منافسًا مفيدًا وغنيًا بالميزات لبرنامج إكسل Excel. تحتوي جداول بيانات جوجل على واجهة برمجة تطبيقات API خاصة بها، ولكن يمكن أن تكون هذه الواجهة مربكةً بعض الشيء في عملية التعلم والاستخدام. سنغطّي في هذا المقال وحدة EZSheets الخارجية والموثقة على موقعها الرسمي، والتي لا تُعَد كاملة الميزات مثل واجهة برمجة تطبيقات جداول البيانات الرسمية من جوجل، ولكنها تسهّل تنفيذ مهام جداول البيانات الشائعة. تثبيت وإعداد وحدة EZSheets يمكنك تثبيت وحدة EZSheets من خلال فتح نافذة طرفية جديدة وتشغيل الأمر pip install --user ezsheets، وستثبِّت وحدة EZSheets أيضًا كجزء من هذا التثبيت الوحدات google-api-python-client و google-auth-httplib2 و google-auth-oauthlib، حيث تسمح هذه الوحدات لبرنامجك بتسجيل الدخول إلى خوادم جوجل وإنشاء طلبات واجهة برمجة التطبيقات API. تعالج وحدة EZSheets عملية التفاعل مع هذه الوحدات، لذلك لا داعي للقلق بشأن كيفية عملها. الحصول على الاعتماديات Credentials والملفات المفتاحية Token Files يجب تفعيل جداول بيانات جوجل وواجهات برمجة تطبيقات جوجل درايف Google Drive على حسابك على جوجل قبل أن تتمكّن من استخدام وحدة EZSheets. انتقل إلى صفحتي الويب التاليتين وانقر على زر التفعيل Enable API الموجودة في أعلى كل منهما: sheets.googleapis.com drive.googleapis.com يجب أيضًا أن تحصل على ثلاثة ملفات، والتي يجب حفظها في المجلد نفسه لسكربت بايثون Python الذي امتداده .py ويستخدم وحدة EZSheets، وهذه الملفات هي: ملف الاعتماديات واسمه credentials-sheets.json. مفتاح Token جداول بيانات جوجل واسمه token-sheets.pickle. مفتاح Token جوجل درايف واسمه token-drive.pickle. يولّد ملف الاعتماديات ملفات المفاتيح، وأسهل طريقة للحصول على ملف الاعتماديات هي الانتقال إلى صفحة Google Sheets Python Quickstart والنقر على زر التفعيل الملون باللون الأزرق Enable the Google Sheets API كما هو موضح في الشكل التالي، ولكن يجب أن تسجّل الدخول إلى حسابك في جوجل لعرض هذه الصفحة: الحصول على ملف credentials.json سيؤدي النقر على هذا الزر إلى ظهور نافذة تحتوي على رابط تنزيل ضبط العميل Download Client Configuration الذي يتيح لك تنزيل ملف credentials.json. أعِد تسمية هذا الملف إلى الاسم credentials-sheets.json وضعه في المجلد نفسه لسكربتات بايثون الخاصة بك. شغّل الأمر import ezsheets لاستيراد وحدة EZSheets بعد الحصول على الملف credentials-sheets.json، حيث ستفتح نافذة متصفح جديدة لتتمكّن من تسجيل الدخول إلى حسابك على جوجل عند استيراد وحدة EZSheets في المرة الأولى. انقر بعد ذلك على زر السماح Allow كما هو موضح في الشكل التالي: السماح لصفحة Python Quickstart بالوصول إلى حسابك على جوجل سبب ظهور الرسالة السابقة هو أنك نزّلتَ ملف الاعتماديات من صفحة Google Sheets Python Quickstart، وستفتح هذه النافذة مرتين: الأولى للوصول إلى جداول بيانات جوجل والثانية للوصول إلى جوجل درايف، حيث تستخدم وحدةُ EZSheets الوصولَ إلى جوجل درايف لرفع جداول البيانات وتنزيلها وحذفها. ستطالبك نافذة المتصفح بإغلاقه بعد تسجيل الدخول، وسيظهر الملفان token-sheets.pickle و token-drive.pickle في المجلد نفسه الذي يوجد فيه الملف credentials-sheets.json. ستجري هذه العملية فقط في المرة الأولى التي تشغِّل فيها الأمر import ezsheets. إذا واجهتَ خطأً بعد النقر على زر السماح "Allow" وكانت الصفحة معطَّلة، فتأكّد أولًا من تفعيل جداول بيانات جوجل وواجهات برمجة تطبيقات جوجل درايف من الروابط الموجودة في بداية هذا القسم. قد يستغرق الأمر بضع دقائق حتى تتمكّن خوادم جوجل من تسجيل هذا التغيير، لذا قد تضطر إلى الانتظار قبل أن تتمكّن من استخدام وحدة EZSheets. ملاحظة: لا تشارك ملفات الاعتماديات أو المفاتيح مع أيّ شخص، وتعامل معها مثل كلمات المرور. إبطال ملف الاعتماديات إذا شاركتَ ملفات الاعتماديات أو المفاتيح مع شخصٍ ما عن طريق الخطأ، فلن يتمكّن هذا الشخص من تغيير كلمة مرور حسابك على جوجل، ولكن سيكون لديه حق الوصول إلى جداول بياناتك. يمكنك إبطال هذه الملفات بالانتقال إلى صفحة طرفية المطور على منصة سحابة جوجل Google Cloud Platform، ولكن يجب تسجيل الدخول إلى حسابك على جوجل لعرض هذه الصفحة. انقر على رابط الاعتماديات Credentials في الشريط الجانبي، ثم انقر على أيقونة سلة المهملات بجانب ملف الاعتماديات الذي شاركته عن طريق الخطأ، كما هو موضح في الشكل التالي: صفحة الاعتماديات في طرفية المطور على منصة سحابة جوجل يمكن إنشاء ملف اعتماديات جديد من هذه الصفحة من خلال النقر على زر إنشاء الاعتماديات "Create Credentials" وتحديد خيار معرّف عميل OAuth أو "OAuth Client ID" كما هو موضح في الشكل السابق، ثم حدّد الخيار "أخرى Other" بالنسبة لنوع التطبيق وسمِّ الملف بأيّ اسم تريده. سيُدرَج بعد ذلك ملف الاعتماديات الجديد في الصفحة، ويمكنك النقر على أيقونة التنزيل لتنزيله. سيكون للملف الذي ستنزِّله اسم ملف طويل ومعقد، لذا يجب إعادة تسميته إلى اسم الملف الافتراضي الذي تحاول الوحدة EZSheets تحميله وهو credentials-sheets.json. يمكنك أيضًا إنشاء ملف اعتماديات جديد من خلال النقر على زر تفعيل واجهة برمجة تطبيقات جداول بيانات جوجل "Enable the Google Sheets API" المذكور في القسم السابق. كائنات جدول البيانات Spreadsheet يمكن أن يحتوي جدول البيانات Spreadsheet في جداول بيانات جوجل على أوراق Sheets متعددة والتي تُسمَّى أيضًا أوراق عمل Worksheets، وتحتوي كل ورقة على أعمدة Columns وصفوف Rows من القيم. يوضح الشكل التالي جدول بيانات بعنوان بيانات التعليم "Education Data"، والذي يحتوي على ثلاث أوراق بعنوان الطلاب "Students" والصفوف "Classes" والموارد "Resources"، ويُسمَّى العمود الأول من كل ورقة A، ويسمى الصف الأول 1: جدول بيانات بعنوان "Education Data" مكوَّن من ثلاث أوراق سيتمثّل معظم عملك في تعديل كائنات الورقة Sheet، ولكن يمكنك أيضًا تعديل كائنات جدول البيانات Spreadsheet، كما سنوضّح في القسم التالي. إنشاء جداول البيانات وتحميلها وسردها يمكنك إنشاء كائن Spreadsheet جديد من جدول بيانات موجود مسبقًا أو جدول بيانات فارغ أو جدول بيانات مرفوع على جداول بيانات جوجل، حيث يمكن إنشاء كائن Spreadsheet من جدول بياناتٍ موجود مسبقًا على جداول بيانات جوجل، ولكن أن تعرف السلسلة النصية لمعرّف جدول البيانات. يمكن العثور على المعرّف الفريد لجداول بيانات جوجل في عنوان URL، بعد الجزء spreadsheets/d/ وقبل الجزء /edit، فمثلًا يوجد جدول البيانات الموضّح في الشكل السابق على عنوان URL الذي هو https://docs.google.com/spreadsheets/d/1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU/edit#gid=151537240/ وبالتالي يكون معرّفه 1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU. ملاحظة: معرّفات جداول البيانات المُستخدَمة في هذا المقال خاصة بجداول بيانات حساب جوجل الخاص بالكاتب، إذ لن تعمل إذا أدخلتها في صدفتك التفاعلية Interactive Shell، لذا انتقل إلى جداول بيانات جوجل لإنشاء جداول بيانات ضمن حسابك ثم احصل على المعرّفات من شريط العناوين. مرّر معرّف جدول بياناتك بوصفه سلسلةً نصية إلى الدالة ezsheets.Spreadsheet() للحصول على كائن Spreadsheet لجدول البيانات الخاص بهذا المعرّف: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss Spreadsheet(spreadsheetId='1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title 'Education Data' يمكنك أيضًا الحصول على كائن Spreadsheet لجدول بيانات موجود مسبقًا من خلال تمرير عنوان URL الكامل لجدول البيانات إلى تلك الدالة، أو إذا كان هناك جدول بيانات واحد فقط في حسابك على جوجل له العنوان نفسه، فيمكنك تمرير عنوان جدول البيانات بوصفه سلسلة نصية. يمكنك إنشاء جدول بيانات جديد وفارغ من خلال استدعاء الدالة ezsheets.createSpreadsheet() وتمرير سلسلةٍ نصية إليها، حيث تمثل هذه السلسلة النصية عنوان جدول البيانات الجديد. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Title of My New Spreadsheet') >>> ss.title 'Title of My New Spreadsheet' يمكنك رفع جدول بيانات إكسل Excel أو أوبن أوفيس OpenOffice أو CSV أو TSV موجود مسبقًا إلى جداول بيانات جوجل من خلال تمرير اسم ملف جدول البيانات إلى الدالة ezsheets.upload(). إذًا لندخِل ما يلي في الصدفة التفاعلية مع وضع اسم ملف جدول بياناتك مكان الملف my_spreadsheet.xlsx: >>> import ezsheets >>> ss = ezsheets.upload('my_spreadsheet.xlsx') >>> ss.title 'my_spreadsheet' يمكنك سرد جداول البيانات الموجودة على حسابك على جوجل من خلال استدعاء الدالة listSpreadsheets() التي تعيد قاموسًا Dictionary مفاتيحه هي معرّفات جداول البيانات وقيمه هي عناوين جداول البيانات. إذًا لندخِل ما يلي في الصدفة التفاعلية بعد رفع جدول البيانات: >>> ezsheets.listSpreadsheets() {'1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU': 'Education Data'} يمكنك بعد الحصول على كائن Spreadsheet استخدام سماته وتوابعه للتعامل مع جدول البيانات المُستضاف على جداول بيانات جوجل عبر الإنترنت. سمات Attributes كائن جدول البيانات Spreadsheet توجد البيانات الفعلية في الأوراق الخاصة بجدول البيانات، ولكن يحتوي كائن Spreadsheet على السمات title و spreadsheetId و url و sheetTitles و sheets للتعامل مع جدول البيانات. لندخِل ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title # عنوان جدول البيانات 'Education Data' >>> ss.title = 'Class Data' # تغيير العنوان >>> ss.spreadsheetId # المعرّف الفريد (وهو سمة للقراءة فقط) '1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU' >>> ss.url # عنوان URL الأصلي (وهو سمة للقراءة فقط) 'https://docs.google.com/spreadsheets/d/1J-Jx6Ne2K_vqI9J2SO- TAXOFbxx_9tUjwnkPC22LjeU/' >>> ss.sheetTitles # عناوين جميع كائنات الورقة Sheet ('Students', 'Classes', 'Resources') >>> ss.sheets # كائنات الورقة Sheet في جدول البيانات بالترتيب (<Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26>, <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26>, <Sheet sheetId=151537240, title='Resources', rowCount=1000, columnCount=26>) >>> ss[0] # كائن الورقة الأول في جدول البيانات <Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26> >>> ss['Students'] # يمكن أيضًا الوصول إلى الأوراق باستخدام العنوان <Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26> >>> del ss[0] # حذف كائن الورقة الأول في جدول البيانات >>> ss.sheetTitles # أصبح كائن الورقة "Students" محذوفًا ('Classes', 'Resources') إذا عدّل شخصٌ ما جدول البيانات من موقع جداول بيانات جوجل، فيمكن للسكربت الخاص بك تحديث كائن Spreadsheet ليطابق البيانات الموجودة على الإنترنت من خلال استدعاء التابع refresh(): >>> ss.refresh() لن يحدّث هذا التابع سمات كائن Spreadsheet فحسب، بل سيحدّث البيانات الموجودة في كائنات Sheet التي يحتوي عليها كائن Spreadsheet، وستنعكس التغييرات التي تجريها على كائن Spreadsheet في جدول البيانات الموجود على الإنترنت ضمن الزمن الحقيقي. تنزيل ورفع جداول البيانات يمكنك تنزيل جدول بيانات جوجل بعددٍ من التنسيقات مثل: إكسل وأوبن أوفيس OpenOffice و CSV و TSV و PDF، ويمكنك أيضًا تنزيله كملف مضغوط ZIP يحتوي على ملفات HTML لبيانات جدول البيانات، حيث تحتوي الوحدة EZSheets على دوالٍ لكل خيار من هذه الخيارات كما سنوضح فيما يلي: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title 'Class Data' >>> ss.downloadAsExcel() # تنزيل جدول البيانات كملف إكسل 'Class_Data.xlsx' >>> ss.downloadAsODS() # تنزيل جدول البيانات كملف أوبن أوفيس 'Class_Data.ods' >>> ss.downloadAsCSV() # تنزيل الورقة الأولى فقط كملف CSV 'Class_Data.csv' >>> ss.downloadAsTSV() # تنزيل الورقة الأولى فقط كملف TSV 'Class_Data.tsv' >>> ss.downloadAsPDF() # تنزيل جدول البيانات كملف PDF 'Class_Data.pdf' >>> ss.downloadAsHTML() # تنزيل جدول البيانات كملف مضغوط ZIP مؤلَّفٍ من ملفات HTML 'Class_Data.zip' لاحظ أن الملفات التي لها تنسيق CSV و TSV يمكن أن تحتوي على ورقة واحدة فقط، لذلك إذا نزّلت جدول بيانات من جداول بيانات جوجل بهذا التنسيق، فستحصل على الورقة الأولى فقط، ولكن يمكنك تنزيل أوراق أخرى من خلال تغيير السمة index الخاصة بكائن Sheet إلى القيمة 0. تعيد جميع دوال التنزيل سلسلة نصية لاسم الملف الذي جرى تنزيله، ويمكنك أيضًا تحديد اسم ملفك لجدول البيانات من خلال تمرير اسم الملف الجديد إلى دالة التنزيل كما يلي، ويجب أن تعيد الدالة اسم الملف المُحدَّث: >>> ss.downloadAsExcel('a_different_filename.xlsx') 'a_different_filename.xlsx' حذف جداول البيانات يمكننا حذف جدول بيانات من خلال استدعاء التابع delete(): >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Delete me') # إنشاء جدول البيانات >>> ezsheets.listSpreadsheets() # التأكد من إنشاء جدول بيانات {'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'} >>> ss.delete() # حذف جدول البيانات >>> ezsheets.listSpreadsheets() {} سينقل التابع delete() جدول بياناتك إلى مجلد سلة المهملات على جوجل درايف، حيث يمكنك عرض محتويات مجلد سلة المهملات، ولكن يمكن حذف جدول البيانات نهائيًا من خلال تمرير القيمة True لوسيط الكلمة المفتاحية Keyword Argument الذي هو permanent كما يلي: >>> ss.delete(permanent=True) لا يُعَد حذف جداول البيانات حذفًا نهائيًا فكرةً جيدة، فمن المستحيل استرداد جدول البيانات الذي أدّى خطأٌ في سكربتك إلى حذفه عن غير قصد. ليس هناك داعٍ للقلق بشأن تحرير المساحة، إذ تتوفر مساحة تخزينية بالجيجابايتات حتى في حسابات جوجل درايف المجانية. كائنات الورقة Sheet يحتوي كائن Spreadsheet على كائن Sheet واحد أو أكثر، حيث تمثّل كائنات Sheet صفوف وأعمدة البيانات الموجودة في الورقة، ويمكنك الوصول إلى هذه الأوراق باستخدام عامل الأقواس المربعة وعدد صحيح يمثل الفهرس. تحتوي السمة sheets على مجموعة Tuple من كائنات Sheet بالترتيب الذي تظهر به في جدول البيانات. يمكنك الوصول إلى كائنات Sheet في جدول البيانات من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.sheets # كائنات الورقة Sheet في جدول البيانات بالترتيب (<Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26>, <Sheet sheetId=151537240, title='Resources', rowCount=1000, columnCount=26>) >>> ss.sheets[0] # الحصول على كائن الورقة الأول في جدول البيانات <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> >>> ss[0] # الحصول أيضًا على كائن الورقة الأول في جدول البيانات <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> يمكنك أيضًا الحصول على كائن Sheet باستخدام عامل الأقواس المربعة وسلسلة نصية تمثّل اسم الورقة، وتحتوي السمة sheetTitles الخاصة بكائن Spreadsheet على مجموعةٍ تمثّل جميع عناوين الأوراق. إذًا لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> ss.sheetTitles # عناوين جميع كائنات الورقة Sheet في جدول البيانات ('Classes', 'Resources') >>> ss['Classes'] # يمكن أيضًا الوصول إلى الأوراق باستخدام العنوان <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> يمكنك بعد الحصول على كائن Sheet قراءة البيانات منه وكتابة البيانات فيه باستخدام توابع كائن Sheet كما سنوضّح في القسم التالي. قراءة وكتابة البيانات تحتوي أوراق عمل جداول بيانات جوجل على أعمدة وصفوف من الخلايا التي تحتوي على بيانات كما هو الحال في جداول بيانات إكسل، حيث يمكنك استخدام عامل الأقواس المربعة لقراءة البيانات من هذه الخلايا وكتابتها فيها. أنشئ مثلًا جدول بيانات جديد وأضِف البيانات إليه من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('My Spreadsheet') >>> sheet = ss[0] # الحصول على الورقة الأولى في جدول البيانات >>> sheet.title 'Sheet1' >>> sheet = ss[0] >>> sheet['A1'] = 'Name' # ضبط القيمة في الخلية A1 >>> sheet['B1'] = 'Age' >>> sheet['C1'] = 'Favorite Movie' >>> sheet['A1'] # قراءة القيمة في الخلية A1 'Name' >>> sheet['A2'] # تعيد الخلايا الفارغة سلسلة نصية فارغة '' >>> sheet[2, 1] # العمود 2 والصف 1 هو عنوان الخلية B1 نفسه 'Age' >>> sheet['A2'] = 'Alice' >>> sheet['B2'] = 30 >>> sheet['C2'] = 'RoboCop' يجب أن ينتج عن هذه التعليمات جدول بيانات جوجل يشبه الشكل التالي: جدول البيانات الذي أنشأناه باستخدام تعليمات المثال السابق يمكن لعدة مستخدمين تحديث الورقة في الوقت ذاته، لذا يمكنك تحديث البيانات المحلية في كائن Sheet من خلال استدعاء التابع refresh() الخاص بهذا الكائن: >>> sheet.refresh() تُحمَّل كافة البيانات الموجودة في كائن Sheet عند تحميل كائن Spreadsheet لأول مرة، وبالتالي يمكن قراءة البيانات مباشرةً، ولكن تتطلب كتابة القيم في جدول البيانات عبر الإنترنت اتصالًا بالشبكة ويمكن أن تستغرق حوالي ثانية واحدة، حيث إذا كان لديك آلاف الخلايا التي تريد تحديثها، فقد يكون تحديثها واحدةً تلو الأخرى بطيئًا جدًا. عنونة الأعمدة والصفوف تعمل عنونة الخلايا في جداول بيانات جوجل كما هو الحال في إكسل، ولكن الفرق الوحيد بينهما هو احتواء جداول بيانات جوجل على أعمدة وصفوف تستند إلى القيمة 1، أي أن العمود أو الصف الأول موجود في الفهرس 1 وليس في الفهرس 0 على عكس فهارس القائمة المستندة إلى القيمة 0 في لغة بايثون. يمكنك تحويل العنوان الذي تنسيقه سلسلة نصية 'A2' إلى عنوانٍ تنسيقه مجموعة (column, row) (والعكس صحيح) باستخدام الدالة convertAddress(). تحوّل الدالتان getColumnLetterOf() و getColumnNumberOf() أيضًا عنوان العمود من الحروف إلى الأعداد وبالعكس. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ezsheets.convertAddress('A2') # تحويل العناوين... (1, 2) >>> ezsheets.convertAddress(1, 2) # … وتحويلها بالعكس مرة أخرى 'A2' >>> ezsheets.getColumnLetterOf(2) 'B' >>> ezsheets.getColumnNumberOf('B') 2 >>> ezsheets.getColumnLetterOf(999) 'ALK' >>> ezsheets.getColumnNumberOf('ZZZ') 18278 تُعَد العناوين التي لها تنسيق السلسلة النصية 'A2' ملائمةً لكتابة العناوين في شيفرتك المصدرية، وتكون العناوين التي لها تنسيق المجموعة (column, row) ملائمةً إذا أردتَ التكرار على مجالٍ من العناوين واحتجتَ صيغةً رقمية للعمود، لذا تُعَد الدوال convertAddress() و getColumnLetterOf() و getColumnNumberOf() مفيدةً عندما تريد التحويل بين هذين التنسيقين. قراءة وكتابة الأعمدة والصفوف بأكملها قد تستغرق كتابة البيانات ضمن خلية واحدة في كل مرة وقتًا طويلًا كما ذكرنا سابقًا، ولكن تحتوي وحدة EZSheets على توابع خاصة بكائن Sheet لقراءة وكتابة الأعمدة والصفوف بأكملها في الوقت ذاته، حيث يقرأ التابعان getColumn() و getRow() من الأعمدة والصفوف ويكتب التابعان updateColumn() و updateRow() في الأعمدة والصفوف. تنشِئ هذه التوابع طلبات إلى خوادم جداول بيانات جوجل لتحديث جدول البيانات، لذا يجب أن تكون متصلًا بالإنترنت. سنرفع في مثالنا جدول بيانات أسعار المنتجات produceSales.xlsx من المقال السابق إلى جداول بيانات جوجل، حيث تبدو الصفوف الثمانية الأولى كما في الشكل التالي: يمكنك رفع جدول البيانات produceSales.xlsx من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.upload('produceSales.xlsx') >>> sheet = ss[0] >>> sheet.getRow(1) # الصف الأول هو الصف 1 وليس الصف 0 ['PRODUCE', 'COST PER KiloGram', 'KiloGrams SOLD', 'TOTAL', '', ''] >>> sheet.getRow(2) ['Potatoes', '0.86', '21.6', '18.58', '', ''] >>> columnOne = sheet.getColumn(1) >>> sheet.getColumn(1) ['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic', --snip-- >>> sheet.getColumn('A') # النتيجة نفسها للتعليمة getColumn(1) ['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic', --snip-- >>> sheet.getRow(3) ['Okra', '2.26', '38.6', '87.24', '', ''] >>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230']) >>> sheet.getRow(3) ['Pumpkin', '11.50', '20', '230', '', ''] >>> columnOne = sheet.getColumn(1) >>> for i, value in enumerate(columnOne): ... # اجعل قائمة بايثون تحتوي على سلاسل نصية بأحرف كبيرة: ... columnOne[i] = value.upper() ... >>> sheet.updateColumn(1, columnOne) # تحديث العمود بأكمله في طلب واحد تسترد الدالتان getRow() و getColumn() البيانات من جميع الخلايا الموجودة في صف أو عمود محدد بوصفها قائمةً من القيم، وتصبح الخلايا الفارغة قيمًا لسلاسل نصية فارغة في القائمة. يمكنك تمرير رقم أو حرف العمود إلى الدالة getColumn() لإخبارها باسترداد بيانات عمودٍ معين، حيث وضّحنا في المثال السابق أن التعلمتين getColumn(1) و getColumn('A') تعيدان القائمة نفسها. تكتب الدالتان updateRow() و updateColumn() فوق البيانات الموجودة في الصف أو العمود على التوالي باستخدام قائمة القيم المُمرّرة إليهما، فمثلًا احتوى الصف الثالث في المثال السابق على معلومات حول البامية Okra في البداية، لكن أدّى استدعاء الدالة updateRow() إلى وضع بيانات حول اليقطين Pumpkin مكانها، ثم استدعينا الدالة sheet.getRow(3) مرةً أخرى لعرض القيم الجديدة في الصف الثالث. لنحدّث بعد ذلك جدول بيانات "produceSales"، حيث يُعَد تحديث خلية واحدة في كل مرة أمرًا بطيئًا إذا كان لديك العديد من الخلايا التي تريد تحديثها، بينما يُعَد الحصول على عمود أو صف كقائمة وتحديث القائمة ثم تحديث العمود أو الصف بأكمله باستخدام القائمة أسرع بكثير، حيث يمكن إجراء جميع التغييرات في طلبٍ واحد. يمكن الحصول على كافة الصفوف دفعةً واحدة من خلال استدعاء التابع getRows() لإعادة قائمةٍ بجميع القوائم، حيث تمثل كل قائمة من القوائم الداخلية الموجودة ضمن القائمة الخارجية صفًا واحدًا من الورقة. يمكنك تعديل هذه القيم الموجودة في هيكل البيانات لتغيير اسم المنتج Produce Name وعدد الكيلوجرامات المباعة Kilograms Sold والتكلفة الإجمالية Total لبعض الصفوف، ثم تمرّرها إلى التابع updateRows() من خلال إدخال ما يلي في الصدفة التفاعلية: >>> rows = sheet.getRows() # الحصول على جميع الصفوف في جدول البيانات >>> rows[0] # فحص القيم الموجودة في الصف الأول ['PRODUCE', 'COST PER KiloGrams', 'KiloGrams SOLD', 'TOTAL', '', ''] >>> rows[1] ['POTATOES', '0.86', '21.6', '18.58', '', ''] >>> rows[1][0] = 'PUMPKIN' # تغيير اسم المنتج >>> rows[1] ['PUMPKIN', '0.86', '21.6', '18.58', '', ''] >>> rows[10] ['OKRA', '2.26', '40', '90.4', '', ''] >>> rows[10][2] = '400' # تغيير عدد الكيلوجرامات المباعة >>> rows[10][3] = '904' # تغيير التكلفة الإجمالية >>> rows[10] ['OKRA', '2.26', '400', '904', '', ''] >>> sheet.updateRows(rows) # تحديث جدول البيانات عبر الإنترنت بالتغييرات التي أجريناها يمكنك تحديث الورقة بأكملها في طلب واحد من خلال تمرير قائمةٍ من القوائم المُعادة من الدالة getRows() والمُعدَّلة بالتغييرات التي أجريناها على الصفين 1 و 10 إلى الدالة updateRows(). لاحظ أن الصفوف الموجودة في ورقة جداول بيانات جوجل تحتوي على سلاسل نصية فارغة في نهايتها، لأن الورقة التي رفعناها تحتوي على 6 أعمدة، ولدينا 4 أعمدة فقط من البيانات. يمكنك قراءة عدد الصفوف والأعمدة في الورقة باستخدام السمتين rowCount و columnCount، ثم يمكنك تغيير حجم الورقة من خلال ضبط هاتين القيمتين. >>> sheet.rowCount # عدد الصفوف في الورقة 23758 >>> sheet.columnCount # عدد الأعمدة في الورقة 6 >>> sheet.columnCount = 4 # تغيير عدد الأعمدة إلى 4 >>> sheet.columnCount # يصبح الآن عدد الأعمدة في الورقة 4 4 يجب أن تحذف التعليمات السابقة العمودين الخامس والسادس من جدول بيانات "produceSales" كما هو موضّح في الشكل التالي: الورقة قبل (على اليسار) وبعد (على اليمين) تغيير عدد الأعمدة إلى 4. يمكن أن تحتوي جداول بيانات جوجل على ما يصل إلى 10 ملايين خلية وفقًا لمركز المساعدة في جوجل درايف، ولكن يُفضَّل أن تجعل الأوراق بالحجم الذي تحتاجه فقط لتقليل الوقت الذي يستغرقه تعديل البيانات وتحديثها. إنشاء وحذف الأوراق تبدأ جميع جداول بيانات جوجل بورقة واحدة اسمها "Sheet1"، ولكن يمكنك إضافة أوراق إضافية إلى نهاية قائمة الأوراق باستخدام التابع createSheet() الذي تمرّر إليه سلسلة نصية لاستخدامها كعنوان للورقة الجديدة، ويمكن للوسيط الثاني الاختياري الخاص بهذا التابع تحديد فهرس العدد الصحيح للورقة الجديدة. يمكنك إنشاء جدول بيانات ثم إضافة أوراق جديدة إليه من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Multiple Sheets') >>> ss.sheetTitles ('Sheet1',) >>> ss.createSheet('Spam') # إنشاء ورقة جديدة في نهاية قائمة الأوراق <Sheet sheetId=2032744541, title='Spam', rowCount=1000, columnCount=26> >>> ss.createSheet('Eggs') # إنشاء ورقة جديدة أخرى <Sheet sheetId=417452987, title='Eggs', rowCount=1000, columnCount=26> >>> ss.sheetTitles ('Sheet1', 'Spam', 'Eggs') >>> ss.createSheet('Meat', 0) # إنشاء ورقة عند الفهرس 0 في قائمة الأوراق <Sheet sheetId=814694991, title='Meat', rowCount=1000, columnCount=26> >>> ss.sheetTitles ('Meat', 'Sheet1', 'Spam', 'Eggs') تضيف التعليمات السابقة ثلاث أوراق جديدة إلى جدول البيانات هي: "Meat" و"Spam" و"Eggs" بالإضافة إلى الورقة الافتراضية "Sheet1". تُرتَّب الأوراق الموجودة في جدول البيانات، وتضاف الأوراق الجديدة إلى نهاية القائمة إن لم تمرِّر وسيطًا ثانيًا إلى الدالة createSheet()، حيث يحدّد هذا الوسيط فهرس الورقة. أنشأنا في المثال السابق الورقة التي عنوانها "Meat" في الفهرس 0، مما يجعل الورقة "Meat" هي الورقة الأولى في جدول البيانات وإزاحة الأوراق الثلاث الأخرى بمقدار موضعٍ واحد، ويشبه ذلك سلوك تابع القائمة insert(). يمكنك رؤية الأوراق الجديدة على التبويبات الموجودة أسفل الشاشة كما هو موضَّح في الشكل التالي: جدول بيانات الأوراق المتعددة "Multiple Sheets" بعد إضافة أوراق "Spam" و"Eggs" و"Meat" يحذف التابع delete() الخاص بالكائن Sheet ورقةً من جدول البيانات، ولكن إذا أدرتَ الاحتفاظ بالورقة مع حذف البيانات الموجودة فيها، فاستدعِ التابع clear() لمسح جميع الخلايا وجعل هذه الورقة ورقةً فارغة. إذا لندخِل ما يلي في الصدفة التفاعلية: >>> ss.sheetTitles ('Meat', 'Sheet1', 'Spam', 'Eggs') >>> ss[0].delete() # حذف الورقة الموجودة في الفهرس 0 أي الورقة "Meat" >>> ss.sheetTitles ('Sheet1', 'Spam', 'Eggs') >>> ss['Spam'].delete() # حذف الورقة "Spam" >>> ss.sheetTitles ('Sheet1', 'Eggs') >>> sheet = ss['Eggs'] # إسناد الورقة "Eggs" إلى متغير >>> sheet.delete() # حذف الورقة "Eggs" >>> ss.sheetTitles ('Sheet1',) >>> ss[0].clear() # مسح جميع الخلايا الموجودة في الورقة "Sheet1" >>> ss.sheetTitles # الورقة "Sheet1" فارغة ولكنها لا تزال موجودة ('Sheet1',) يكون حذف الأوراق حذفًا نهائيًا، إذ لا توجد طريقة لاستعادة البيانات، ولكن يمكنك إنشاء نسخة احتياطية من الأوراق من خلال نسخها إلى جدول بيانات آخر باستخدام التابع copyTo() كما سنوضّح في القسم التالي. نسخ الأوراق يحتوي كل كائن Spreadsheet على قائمةٍ مرتبة من كائنات Sheet الموجودة ضمنه، حيث يمكنك استخدام هذه القائمة لإعادة ترتيب الأوراق (كما وضّحنا في القسم السابق) أو نسخها إلى جداول بيانات أخرى، إذ يمكن نسخ كائن Sheet إلى كائن Spreadsheet آخر من خلال استدعاء التابع copyTo() الذي نمرّر إليه كائن Spreadsheet الهدف كوسيط. لندخِل ما يلي في الصدفة التفاعلية لإنشاء جدولي بيانات ونسخ بيانات جدول البيانات الأول إلى الورقة الأخرى: >>> import ezsheets >>> ss1 = ezsheets.createSpreadsheet('First Spreadsheet') >>> ss2 = ezsheets.createSpreadsheet('Second Spreadsheet') >>> ss1[0] <Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26> >>> ss1[0].updateRow(1, ['Some', 'data', 'in', 'the', 'first', 'row']) >>> ss1[0].copyTo(ss2) # نسخ الورقة Sheet1 الخاصة بجدول البيانات ss1 إلى جدول البيانات ss2 >>> ss2.sheetTitles # سيحتوي جدول البيانات ss2 على نسخة من الورقة Sheet1 الخاصة بجدول البيانات ss1 Sheet1 ('Sheet1', 'Copy of Sheet1') لاحظ تسمية الورقة المنسوخة بالاسم Copy of Sheet1، لأن جدول البيانات الهدف (ss2 في المثال السابق) يحتوي مسبقًا على ورقة بالاسم Sheet1. تظهر الأوراق المنسوخة في نهاية قائمة أوراق جدول البيانات الهدف، ولكن يمكنك تغيير السمة index لإعادة ترتيبها في جدول البيانات الجديد. التعامل مع الحصص Quotas في جداول بيانات جوجل تُعَد جداول بيانات جوجل متاحةً عبر الإنترنت، لذا من السهل مشاركة الأوراق بين عدة مستخدمين يمكنهم جميعًا الوصول إلى الأوراق في وقتٍ واحد، ولكن سيؤدّي ذلك إلى أن تكون قراءة الأوراق وتحديثها أبطأ من قراءة وتحديث ملفات إكسل المخزَّنة محليًا على قرص حاسوبك الصلب. تفرض جداول بيانات جوجل أيضًا قيودًا على عدد عمليات القراءة والكتابة التي يمكنك إجراؤها. يُقيَّد مستخدمو جداول بيانات جوجل بإنشاء 250 جدول بيانات جديد يوميًا، ويمكن لحسابات جوجل المجانية إجراء 100 طلب قراءة و100 طلب كتابة في كل 100 ثانية وفقًا لإرشادات مطوري جوجل، إذ ستؤدي محاولة تجاوز هذه الحصة إلى رفع الاستثناء googleapiclient.errors.HttpError أو "Quota exceeded for quota group" الذي يمثّل تجاوز الحصة المتاحة، حيث تلتقط الوحدة EZSheets تلقائيًا هذا الاستثناء وتعيد محاولة الطلب. إذا حدث ذلك، فستستغرق استدعاءات الدوال لقراءة البيانات أو كتابتها عدة ثوانٍ أو حتى دقيقة أو دقيقتين قبل أن تعيد شيئًا ما، وإذا استمر الطلب في الفشل، وهو أمرٌ ممكن إذا أجرى سكربتٌ آخر يمتلك الاعتماديات نفسها طلباتٍ أيضًا، فستعيد الوحدة EZSheets رفعَ هذا الاستثناء. يؤدي ذلك إلى أنه قد تستغرق استدعاءات توابع الوحدة EZSheets عدة ثوانٍ قبل أن تعيد شيئًا ما. إذا أردتَ عرضَ حجم استخدامك لواجهة برمجة التطبيقات أو زيادةَ حصتك، فانتقل إلى صفحة IAM & Admin Quotas للتعرف على كيفية الدفع مقابل زيادة حجم الاستخدام. إذا أردتَ التعامل مع استثناءات HttpError بنفسك، فيمكنك ضبط ezsheets.IGNORE_QUOTA على القيمة True، وسترفع توابع الوحدة EZSheets هذه الاستثناءات عندما تواجهها. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لتنزيل بيانات نماذج جوجل Google Forms تتيح لك نماذج جوجل إنشاء نماذج بسيطة عبر الإنترنت تسهّل جمع المعلومات من الأشخاص، حيث تُخزَّن المعلومات التي يدخلها هؤلاء الأشخاص في النموذج ضمن جداول بيانات جوجل. جرّب كتابة برنامجٍ يمكنه تنزيل معلومات النموذج التي أرسلها المستخدمون تلقائيًا، لذا انتقل إلى نماذج جوجل وأنشئ نموذجًا جديدًا، حيث سيكون هذا النموذج فارغًا، ثم أضِف الحقول إلى النموذج الذي يطلب من المستخدم اسمه وعنوان بريده الإلكتروني، ثم انقر على زر "إرسال Send" في الجزء العلوي الأيمن للحصول على رابط لنموذجك الجديد مثل الرابط https://goo.gl/forms/QZsq5sC2Qe4fYO592/، وحاول إدخال بعض الأمثلة على الردود في هذا النموذج. انقر على الزر الأخضر "Create Spreadsheet" في تبويب "الردود Responses" في نموذجك لإنشاء جدول بيانات جوجل الذي سيحتوي على الردود التي يرسلها المستخدمون. يُفترَض أن تشاهد إجاباتك في الصفوف الأولى من جدول البيانات. اكتب بعد ذلك سكربت بايثون مع استخدام الوحدة EZSheets لجمع قائمة بعناوين البريد الإلكتروني في جدول البيانات. برنامج لتحويل جداول البيانات إلى تنسيقات أخرى يمكنك استخدام جداول بيانات جوجل لتحويل ملف جدول بيانات إلى تنسيقات أخرى، لذا جرّب كتابة سكربت يمرر ملفًا مُرسَلًا إلى الدالة upload().نزّل جدول البيانات بعد رفعه على جداول بيانات جوجل باستخدام الدوال downloadAsExcel() و downloadAsODS() وغيرها من الدوال المماثلة لإنشاء نسخة من جدول البيانات بتنسيقات أخرى. برنامج للعثور على الأخطاء في جدول البيانات لنفترض أن لدينا جدول بيانات يحتوي على إجمالي عدد حبات الفاصولياء مرفوع على جداول بيانات جوجل، حيث يكون جدول البيانات قابلًا للعرض، ولكنه غير قابل للتحرير، ويمكنك الاطلاع عليه في متصفحك والحصول عليه باستخدام الشيفرة التالية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg') أعمدة الورقة الأولى في جدول البيانات هي عدد حبات الفاصولياء في الجرة "Beans per Jar" وعدد الجِرار "Jars" وعدد حبات الفاصولياء الكلي "Total Beans"، حيث ينتج العمود "Total Beans" من ضرب الأعداد الموجودة في العمودين "Beans per Jar" و "Jars"، ولكن يوجد خطأ في أحد الصفوف البالغ عددها 15000 صفًا في هذه الورقة. يُعَد ذلك عددًا كبيرًا جدًا من الصفوف التي لا يمكن التحقق منها يدويًا، ولكن يمكنك كتابة سكربت يتحقق من العمود "Total Beans". يمكنك الوصول إلى الخلايا الفردية في صف باستخدام ss[0].getRow(rowNum)، حيث ss هو كائن Spreadsheet و rowNum هو رقم الصف، وتذكّر أن أرقام الصفوف في جداول بيانات جوجل تبدأ من العدد 1 وليس من 0. ستكون قيم الخلايا سلاسلًا نصية، لذا يجب تحويلها إلى أعداد صحيحة حتى يتمكّن برنامجك من العمل معها. يُقيَّم التعبير int(ss[0].getRow(2)[0]) * int(ss[0].getRow(2)[1]) == int(ss[0].getRow(2)[2]) على القيمة True إذا احتوى الصف على القيمة الإجمالية الصحيحة، لذا ضع هذا الشيفرة البرمجية في حلقة لتحديد الصف الموجود في الورقة الذي يحتوي على القيمة الإجمالية غير الصحيحة. الخلاصة يُعَد تطبيق جداول بيانات جوجل تطبيقًا شائعًا لجداول البيانات عبر الإنترنت التي تعمل في متصفحك. يمكنك تنزيل جداول البيانات وإنشاؤها وقراءتها وتعديلها باستخدام الوحدة الخارجية EZSheets التي تمثّل جداول البيانات بوصفها كائنات Spreadsheet التي تحتوي على قائمة مرتبة من كائنات Sheet، وتحتوي كل ورقة على أعمدة وصفوف من البيانات التي يمكنك قراءتها وتحديثها بطرق متعددة. تسهّل جداول بيانات جوجل مشاركة البيانات وتعديلها بصورة جماعية، ولكن عيبها الرئيسي هو السرعة، إذ يجب عليك تحديث جداول البيانات باستخدام طلبات الويب، مما يؤدي إلى أن يستغرق التنفيذ بضع ثوانٍ، ولكن لن يؤثر هذا القيد على سكربتات بايثون التي تستخدم وحدة EZSheets بالنسبة لمعظم الأغراض. تحدّ جداول بيانات جوجل أيضًا من عدد المرات التي يمكنك فيها إجراء التغييرات. ملاحظة: يمكنك الحصول على التوثيق الكامل لميزات الوحدة EZSheet من موقعها الرسمي. ترجمة -وبتصرُّف- للمقال Working with Google Sheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: الكتابة في مستندات إكسل باستخدام لغة بايثون Python قراءة مستندات جداول إكسل باستخدام لغة بايثون Python مقدمة إلى تطبيق مستندات جوجل Google Docs
-
تعرّفنا في المقال السابق على كيفية قراءة مستندات إكسل باستخدام لغة بايثون، وسنتعرّف في هذا المقال على كيفية كتابة مستندات إكسل باستخدام لغة بايثون، إذ توفّر وحدة OpenPyXL الخاصة بلغة بايثون طرقًا لكتابة البيانات، مما يعني أن برامجك يمكنها إنشاء ملفات جداول البيانات وتعديلها، ومن السهل إنشاء جداول بيانات تحتوي على آلاف الصفوف من البيانات باستخدام بايثون. إنشاء وحفظ مستندات إكسل استدعِ الدالة openpyxl.Workbook() لإنشاء كائن مصنف Workbook جديد وفارغ، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() # إنشاء مصنف فارغ >>> wb.sheetnames # يبدأ المصنف بورقة واحدة ['Sheet'] >>> sheet = wb.active >>> sheet.title 'Sheet' >>> sheet.title = 'Meat Eggs Sheet' # تغيير العنوان >>> wb.sheetnames ['Meat Eggs Sheet'] سيبدأ المصنف بورقة واحدة تُسمَّى "Sheet"، ويمكنك تغيير اسم الورقة من خلال تخزين سلسلة نصية جديدة في السمة Attribute الخاصة بها وهي title. لن يُحفظ ملف جدول البيانات عند تعديل كائن Workbook أو أوراقه وخلاياه حتى استدعاء تابع المصنف save(). أدخِل ما يلي في الصدفة التفاعلية (مع الملف example.xlsx في مجلد العمل الحالي): >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb.active >>> sheet.title = 'Spam Spam Spam' >>> wb.save('example_copy.xlsx') # حفظ المصنف غيّرنا اسم الورقة، وحفظنا التغييرات من خلال تمرير اسم الملف كسلسلة نصية إلى التابع save(). يؤدي تمرير اسم ملف مختلف عن الاسم الأصلي -مثل الاسم 'example_copy.xlsx'- إلى حفظ التغييرات في نسخة من جدول البيانات. إذا عدّلتَ جدول بيانات حمّلته من ملف، فيجب عليك دائمًا حفظ جدول البيانات الجديد المُعدَّل باسم ملفٍ مختلف عن اسم الملف الأصلي، وبذلك سيظل لديك ملف جدول البيانات الأصلي للعمل عليه في حالة وجود خطأ في شيفرتك البرمجية، مما يؤدي إلى احتواء الملف الجديد المحفوظ على بيانات غير صحيحة أو تالفة. إنشاء وحذف الأوراق يمكن إضافة الأوراق وحذفها من المصنف باستخدام التابع create_sheet() والعامل del. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> wb.sheetnames ['Sheet'] >>> wb.create_sheet() # إضافة ورقة جديدة <Worksheet "Sheet1"> >>> wb.sheetnames ['Sheet', 'Sheet1'] >>> # إنشاء ورقة جديدة في الفهرس 0 >>> wb.create_sheet(index=0, title='First Sheet') <Worksheet "First Sheet"> >>> wb.sheetnames ['First Sheet', 'Sheet', 'Sheet1'] >>> wb.create_sheet(index=2, title='Middle Sheet') <Worksheet "Middle Sheet"> >>> wb.sheetnames ['First Sheet', 'Sheet', 'Middle Sheet', 'Sheet1'] يعيد التابع create_sheet() كائن Worksheet جديد اسمه SheetX، والذي ضُبِط افتراضيًا ليكون الورقة الأخيرة في المصنف. يمكن اختياريًا تحديد فهرس واسم الورقة الجديدة باستخدام وسطاء الكلمات المفتاحية Keyword Arguments التي هي index و title. لنتابع المثال السابق من خلال كتابة ما يلي: >>> wb.sheetnames ['First Sheet', 'Sheet', 'Middle Sheet', 'Sheet1'] >>> del wb['Middle Sheet'] >>> del wb['Sheet1'] >>> wb.sheetnames ['First Sheet', 'Sheet'] يمكنك استخدام العامل del لحذف ورقة من مصنف، وهذا يماثل استخدامه لحذف زوج مفتاح-قيمة من القاموس. ملاحظة: تذكّر استدعاء التابع save() لحفظ التغييرات بعد إضافة أوراق إلى المصنف أو حذفها منه. كتابة القيم في الخلايا تشبه كتابة القيم في الخلايا إلى حدٍ كبير كتابة القيم في المفاتيح الموجودة ضمن القاموس. إذًا لندخل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] >>> sheet['A1'] = 'Hello, world!' # تعديل قيمة الخلية >>> sheet['A1'].value 'Hello, world!' إذا كان لديك إحداثيات الخلية كسلسلة نصية، فيمكنك استخدامها بالطريقة نفسها لاستخدام مفتاح القاموس في الكائن Worksheet لتحديد الخلية التي تريد الكتابة فيها. تطبيق عملي: تحديث جدول بيانات ستكتب في هذا التطبيق العملي برنامجًا لتحديث الخلايا في جدول بيانات مبيعات المنتجات، حيث سيبحث برنامجك في جدول البيانات، ويعثر على أنواع معينة من المنتجات، ويحدّث أسعارها. يمكنك تنزيل جدول البيانات الخاص بهذا التطبيق العملي، ويوضح الشكل التالي كيف يبدو جدول البيانات: جدول بيانات مبيعات المنتجات يمثّل كل صف في هذا الجدول عملية بيع واحدة، والأعمدة هي نوع المنتج المباع (A)، والتكلفة لكل كيلوجرام من هذا المنتج (B)، وعدد الكيلوجرامات المباعة (C)، وإجمالي الإيرادات من عمليات البيع (D). يُضبَط عمود "الإجمالي TOTAL" على صيغة إكسل =ROUND(B3*C3, 2) التي تضرب تكلفة كل كيلوجرام بعدد الكيلوجرامات المُباعة وتقريب النتيجة إلى أقرب سنت. ستحدّث الخلايا الموجودة في العمود TOTAL نفسها تلقائيًا باستخدام هذه الصيغة في حالة وجود تغيير في العمود B أو C. لنفترض إدخال أسعار الثوم والكرفس والليمون بصورة غير صحيحة، مما يتركك أمام مهمة مملة تتمثل في المرور على آلاف الصفوف في جدول البيانات لتحديث تكلفة الكيلوجرام الواحد لصفوف الثوم Garlic والكرفس Celery والليمون Lemon. لا يمكنك إجراء عملية بحث واستبدال "find-and-replace" بسيطة للسعر بسبب وجود عناصر أخرى لها السعر نفسه ولا نريد تغييرها بصورة خاطئة. قد يستغرق تنفيذ ذلك يدويًا ساعات بالنسبة لآلاف الصفوف، ولكن يمكنك كتابة برنامج يمكنه إنجاز ذلك في ثوانٍ، حيث يطبّق برنامجك ما يلي: يتكرر على جميع الصفوف ضمن حلقة. إذا كان الصف للثوم أو الكرفس أو الليمون، فسيغيِّر السعر. وهذا يعني أن شيفرتك البرمجية يجب أن تطبّق الخطوات التالية: فتح ملف جدول البيانات. التحقق مما إذا كانت القيمة الموجودة في العمود A هي الكرفس Celery أو الثوم Garlic أو الليمون Lemon لجميع الصفوف. إذا كان الأمر كذلك، فسيتحدّث السعر في العمود B. حفظ جدول البيانات في ملف جديد حتى لا تفقد جدول البيانات القديم. الخطوة الأولى: إعداد هيكل البيانات باستخدام معلومات التحديث إليك الأسعار التي يجب تحديثها: المنتج السعر الكرفس Celery 1.19 الثوم Garlic 3.07 الليمون Lemon 1.27 ويمكنك كتابة الشيفرة البرمجية التالية: if produceName == 'Celery': cellObj = 1.19 if produceName == 'Garlic': cellObj = 3.07 if produceName == 'Lemon': cellObj = 1.27 يُعَد الحصول على بيانات المنتج والأسعار المُحدَّثة الثابتة باستخدام الطريقة السابقة أمرًا غير مناسب إلى حدٍ ما، فإذا كنت بحاجة إلى تحديث جدول البيانات مرة أخرى بأسعار مختلفة أو بمنتجات مختلفة، فيجب عليك تغيير الكثير من الشيفرة البرمجية، وبالتالي ستخاطر بإدخال أخطاء في كل مرة تغيّر فيها شيفرتك البرمجية. يتمثّل الحل الأفضل في تخزين معلومات السعر المُصحَّحة في قاموس وكتابة شيفرتك البرمجية لاستخدام هيكل البيانات، لذا أدخِل الشيفرة التالية في تبويب جديد لإنشاء ملف جديد في محرّرك: #! python3 # updateProduce.py - تصحيح أسعار المنتجات في جدول بيانات المبيعات import openpyxl wb = openpyxl.load_workbook('produceSales.xlsx') sheet = wb['Sheet'] # أنواع المنتجات وأسعارها المُحدَّثة PRICE_UPDATES = {'Garlic': 3.07, 'Celery': 1.19, 'Lemon': 1.27} # التكرار على جميع الصفوف وتحديث الأسعار احفظ الملف بالاسم updateProduce.py. إذا أردتَ تحديث جدول البيانات مرة أخرى، فيجب تحديث القاموس PRICE_UPDATES فقط دون تغيير أي شيفرة برمجية أخرى. الخطوة الثانية: التحقق من كافة الصفوف وتحديث الأسعار غير الصحيحة سيتكرر الجزء التالي من البرنامج على كافة الصفوف الموجودة في جدول البيانات، لذا أضِف الشيفرة البرمجية التالية إلى نهاية الملف updateProduce.py: #! python3 # updateProduce.py - تصحيح أسعار المنتجات في جدول بيانات المبيعات --snip-- # التكرار على جميع الصفوف وتحديث الأسعار ➊ for rowNum in range(2, sheet.max_row): # تخطي الصف الأول ➋ produceName = sheet.cell(row=rowNum, column=1).value ➌ if produceName in PRICE_UPDATES: sheet.cell(row=rowNum, column=2).value = PRICE_UPDATES[produceName] ➍ wb.save('updatedProduceSales.xlsx') نكرّر الشيفرة البرمجية على الصفوف بدءًا من الصف 2، لأن الصف 1 هو ترويسة الجدول ➊، ونخزّن الخلية الموجودة في العمود 1 (أي العمود A) في المتغير produceName ➋، حيث إذا كان هذا المتغير موجودًا بوصفه مفتاحًا في قاموس PRICE_UPDATES ➌، فيجب أن تعلم أن هذا الصف يجب تصحيح سعره، وسيكون السعر الصحيح في PRICE_UPDATES[produceName]. لاحظ مدى نظافة شيفرتك باستخدام قاموس PRICE_UPDATES، إذ لا توجد سوى تعليمة if واحدة فقط بدلًا من استخدام شيفرة تحتوي التعليمة if produceName == 'Garlic': مثلًا التي تكون ضرورية لكل نوعٍ من المنتجات يجب تحديثه. بما أن هذه الشيفرة البرمجية تستخدم قاموس PRICE_UPDATES بدلًا من كتابة شيفرة ثابتة لأسماء المنتجات وأسعارها المُحدَّثة ضمن حلقة for، فهذا يعني أنك ستعدّل قاموس PRICE_UPDATES فقط دون تعديل الشيفرة البرمجية، إذا احتاج جدول بيانات مبيعات المنتجات تغييراتٍ إضافية. تحفظ الشيفرة البرمجية كائن Workbook في الملف updatedProduceSales.xlsx ➍ بعد المرور على جدول البيانات بأكمله وإجراء التغييرات، ولا تكتب معلوماتٍ جديدة مكان معلومات جدول البيانات القديم إذا احتوى برنامجك خطأً وكان جدول البيانات المُحدَّث خاطئًا. يمكنك حذف جدول البيانات القديم بعد التحقق من أن جدول البيانات المُحدَّث صحيحًا. ملاحظة: لا تنسَ أنه يمكنك تنزيل الشيفرة المصدرية الكاملة لهذا البرنامج. أفكار لبرامج مماثلة يستخدم العديد من العاملين في وظائف مكتبية جداولَ بيانات إكسل طوال الوقت، لذا قد يكون البرنامج الذي يمكنه تعديل ملفات إكسل وكتابتها تلقائيًا مفيدًا جدًا لهم، إذ يمكن أن يفعل مثل هذا البرنامج الأمور التالية: قراءة البيانات من جدول بيانات واحد وكتابتها في أجزاء من جداول بيانات أخرى. قراءة البيانات من مواقع الويب أو الملفات النصية أو الحافظة وكتابتها في جدول بيانات. تنظيف البيانات تلقائيًا في جداول البيانات، فمثلًا يمكنه استخدام التعابير النمطية Regular Expressions لقراءة تنسيقات متعددة لأرقام الهواتف وتعديلها إلى تنسيق معياري واحد. ضبط نمط الخط Font Style في الخلايا يمكن أن يساعدك تنسيق خلايا أو صفوف أو أعمدة معينة في إبراز المناطق المهمة في جدول البيانات، إذ يمكن لبرنامجك في جدول بيانات المنتجات مثلًا تطبيق نص عريض على صفوف البطاطا Potato والثوم Garlic والجزر الأبيض Parsnip، أو يمكنه كتابة الصفوف التي تكلفة الكيلوجرام الواحد منها أكبر من 5 دولارات بخط مائل. قد يكون تنسيق أجزاء من جدول بيانات كبير أمرًا مملًا يدويًا، ولكن يمكن لبرامجك تطبيق ذلك مباشرةً. يمكنك تخصيص أنماط الخطوط في الخلايا من خلال استيراد الدالة Font() من الوحدة openpyxl.styles، مما يتيح لك كتابة Font() بدلًا من openpyxl.styles.Font() اختصارًا: from openpyxl.styles import Font ينشئ المثال التالي مصنفًا جديدًا ويضبط الخلية A1 ليكون الخط فيها خطًا مائلًا وبحجم 24 نقطة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> from openpyxl.styles import Font >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] ➊ >>> italic24Font = Font(size=24, italic=True) # إنشاء الخط ➋ >>> sheet['A1'].font = italic24Font # تطبيق الخط في الخلية A1 >>> sheet['A1'] = 'Hello, world!' >>> wb.save('styles.xlsx') تعيد الدالة Font(size=24, italic=True) كائن Font المُخزَّن في المتغير italic24Font ➊، وتضبط وسطاء الكلمات المفتاحية الخاصة بالدالة Font() -مثل الوسيطين size و italic- معلومات تنسيق الكائن Font، وإذا أسندنا الكائن italic24Font ➋ إلى sheet['A1'].font، فستُطبَّق جميع معلومات تنسيق الخط على الخلية A1. كائنات الخط Font يمكن ضبط السمات font من خلال تمرير وسطاء الكلمات المفتاحية إلى الدالة Font()، حيث يوضّح الجدول التالي وسطاء الكلمات المفتاحية المُحتمَلة للدالة Font(): وسيط الكلمة المفتاحية نوع البيانات الوصف name سلسلة نصية اسم الخط مثل 'Calibri' أو 'Times New Roman' size عدد صحيح حجم الخط مقاسًا بالنقاط bold قيمة منطقية قيمته True إذا كان الخط عريضًا italic قيمة منطقية قيمته True إذا كان الخط مائلًا يمكنك استدعاء الدالة Font() لإنشاء كائن Font وتخزينه في متغير، ثم تسند السمة font الخاصة بكائن Cell إلى هذا المتغير، فمثلًا تنشئ الشيفرة البرمجية التالية تنسيقات خطوط مختلفة: >>> import openpyxl >>> from openpyxl.styles import Font >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] >>> fontObj1 = Font(name='Times New Roman', bold=True) >>> sheet['A1'].font = fontObj1 >>> sheet['A1'] = 'Bold Times New Roman' >>> fontObj2 = Font(size=24, italic=True) >>> sheet['B3'].font = fontObj2 >>> sheet['B3'] = '24 pt Italic' >>> wb.save('styles.xlsx') نخزّن كائن Font في المتغير fontObj1 الذي نسنده إلى السمة font الخاصة بالكائن Cell للخلية A1، ونكرر العملية نفسها مع كائن خط آخر لضبط خط الخلية الثانية. إذا نفّذنا الشيفرة البرمجية السابقة، فسيُضبَط تنسيق الخلايا A1 و B3 في جدول البيانات على تنسيقات الخطوط المُخصَّصة، وستبدو كما يلي: جدول بيانات يحتوي على أنماط خطوط مُخصَّصة ضبطنا اسم الخط في الخلية A1 على القيمة 'Times New Roman' والوسيط bold على القيمة true، حيث يظهر النص بالخط Times New Roman العريض، ولكننا لم نحدد حجم الخط، لذلك اُستخدِم الحجم الافتراضي للوحدة openpyxl، وهو الحجم 11. استخدمنا الخط المائل وبحجم 24 في الخلية B3، ولكن لم نحدد اسم الخط، لذلك اُستخدِم الخط الافتراضي للوحدة openpyxl، وهو الخط Calibri. صيغ إكسل تضبط صيغ إكسل -التي تبدأ بإشارة يساوي- الخلايا لتحتوي على قيم ناتجة عن تطبيق عمليات حسابية على خلايا أخرى، لذا سنستخدم في هذه الفقرة وحدة openpyxl لإضافة الصيغ إلى الخلايا برمجيًا مثل أيّ قيمة عادية أخرى كما يلي: >>> sheet['B9'] = '=SUM(B1:B8)' ستؤدي التعليمة السابقة إلى تخزين الصيغة =SUM(B1:B8) بوصفها قيمةً في الخلية B9، مما يؤدي إلى ضبط الخلية B9 على صيغة تحسب مجموع القيم في الخلايا من B1 إلى B8 كما في الشكل التالي: تحتوي الخلية B9 على الصيغة =SUM(B1:B8) التي تجمع القيم الموجودة في الخلايا من B1 إلى B8 تُضبَط صيغ إكسل مثل أيّ قيمة نصية أخرى في الخلية. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet['A1'] = 200 >>> sheet['A2'] = 300 >>> sheet['A3'] = '=SUM(A1:A2)' # ضبط الصيغة >>> wb.save('writeFormula.xlsx') ضبطنا الخلايا A1 و A2 على القيمتين 200 و 300 على التوالي، وضبطنا القيمة الموجودة في الخلية A3 على صيغة تجمع القيم الموجودة في الخليتين A1 و A2، وبالتالي ستظهر قيمة الخلية A3 على أنها 500 عند فتح جدول البيانات في إكسل. توفر صيغ إكسل مستوًى مقبولًا من البرمجة لجداول البيانات، ولكن تصبح هذه الصيغ غير قابلة للإدارة بسرعة بالنسبة للمهام المعقدة، فمثلًا حتى لو كنت على دراية كبيرة بصيغ إكسل، فمن الصعب محاولة تفسير ما تفعله الصيغة التالية: =IFERROR(TRIM(IF(LEN(VLOOKUP(F7, Sheet2!$A$1:$B$10000, 2, FALSE))>0,SUBSTITUTE(VLOOKUP(F7, Sheet2!$A$1:$B$10000, 2, FALSE), " ", ""),"")), "") لاحظ أن شيفرة بايثون البرمجية أكثر قابلية للقراءة من الصيغة السابقة. تعديل الصفوف والأعمدة يُعَد تعديل أحجام الصفوف والأعمدة في برنامج إكسل أمرًا سهلًا مثل النقر على حواف ترويسة الصف أو العمود وسحبها، ولكن إذا أردتَ ضبط حجم صف أو عمود بناءً على محتويات خلاياه أو إذا أردتَ ضبط الأحجام في عدد كبير من ملفات جداول البيانات، فستكون كتابة برنامج بايثون لذلك أسرع بكثير. يمكن أيضًا إخفاء الصفوف والأعمدة بصورة كاملة، أو يمكن تثبيتها بحيث تكون مرئية دائمًا على الشاشة وتظهر في جميع الصفحات عند طباعة جدول البيانات، إذ يُعَد ذلك مفيدًا للعناوين. ضبط ارتفاع الصف وعرض العمود تمتلك كائنات Worksheet سمات row_dimensions و column_dimensions التي تتحكم في ارتفاع الصفوف وعرض الأعمدة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet['A1'] = 'Tall row' >>> sheet['B2'] = 'Wide column' >>> # ضبط الارتفاع والعرض >>> sheet.row_dimensions[1].height = 70 >>> sheet.column_dimensions['B'].width = 20 >>> wb.save('dimensions.xlsx') تُعَد السمات row_dimensions و column_dimensions الخاصة بالورقة قيمًا تشبه القاموس، إذ تحتوي السمة row_dimensions على كائنات RowDimension وتحتوي السمة column_dimensions على كائنات ColumnDimension. يمكنك الوصول إلى أحد الكائنات باستخدام رقم الصف (في مثالنا 1 أو 2) في row_dimensions، ويمكنك الوصول إلى أحد الكائنات باستخدام حرف العمود (في مثالنا A أو B) في column_dimensions. يبدو جدول البيانات dimensions.xlsx كما يلي: ضبطنا الصف 1 والعمود B على ارتفاع وعرض أكبر يمكنك ضبط ارتفاع الكائن RowDimension بعد الحصول عليه، ويمكنك ضبط عرض الكائن ColumnDimension بعد الحصول عليه. يمكن ضبط ارتفاع الصف ليكون عددًا صحيحًا أو عشريًا قيمته بين 0 و 409، إذ تمثّل هذه القيمة الارتفاع المُقاس بالنقاط، حيث تساوي النقطة الواحدة 1/72 من البوصة، ويكون ارتفاع الصف الافتراضي 12.75. يمكن ضبط عرض العمود ليكون عددًا صحيحًا أو عشريًا قيمته بين 0 و255، إذ تمثّل هذه القيمة عدد المحارف التي يمكن عرضها في الخلية بحجم الخط الافتراضي (11 نقطة)، ويكون عرض العمود الافتراضي 8.43 محرفًا. تُخفَى الأعمدة التي يبلغ عرضها 0 أو الصفوف التي يبلغ ارتفاعها 0 عن المستخدم. دمج وإلغاء دمج الخلايا يمكن دمج منطقة مستطيلة من الخلايا في خلية واحدة باستخدام التابع merge_cells() الخاص بالورقة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet.merge_cells('A1:D3') # دمج جميع هذه الخلايا >>> sheet['A1'] = 'Twelve cells merged together.' >>> sheet.merge_cells('C5:D5') # دمج هاتين الخليتين >>> sheet['C5'] = 'Two merged cells.' >>> wb.save('merged.xlsx') وسيط التابع merge_cells() هو سلسلة نصية واحدة من الخلايا العلوية اليسرى والسفلية اليمنى للمنطقة المستطيلة المُراد دمجها، حيث تدمج 'A1:D3' اثنتا عشر خلية في خلية واحدة، ويمكنك ضبط قيمة هذه الخلايا المدموجة من خلال ضبط قيمة الخلية العلوية اليسرى لمجموعة الخلايا المدموجة. سيبدو الملف merged.xlsx كما يلي عند تشغيل الشيفرة البرمجية السابقة: الخلايا المدموجة في جدول البيانات يمكن إلغاء دمج الخلايا من خلال استدعاء التابع unmerge_cells() الخاص بالورقة. إذًا لندخِل ما يلي الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('merged.xlsx') >>> sheet = wb.active >>> sheet.unmerge_cells('A1:D3') # فصل هذه الخلايا عن بعضها >>> sheet.unmerge_cells('C5:D5') >>> wb.save('merged.xlsx') إذا حفظتَ تغييراتك ثم ألقيتَ نظرة على جدول البيانات، فسترى أن الخلايا المدموجة عادت إلى كونها خلايا مفردة. تثبيت الأجزاء من المفيد تثبيت عددٍ من الصفوف العلوية أو الأعمدة الموجودة في أقصى اليسار على الشاشة في جداول البيانات الكبيرة جدًا التي لا يمكن عرضها كاملة، فمثلًا تكون ترويسات الأعمدة أو الصفوف المُثبَّتة مرئيةً للمستخدم دائمًا حتى أثناء التمرير في جدول البيانات، ويُعرَف ذلك بتثبيت الأجزاء Freeze Panes. يحتوي كل كائن Worksheet في وحدة OpenPyXL على السمة freeze_panes التي يمكن ضبطها على كائن Cell أو سلسلة نصية من إحداثيات الخلية. لاحظ تثبيت كافة الصفوف الموجودة أعلى هذه الخلية وجميع الأعمدة الموجودة على يسارها، ولكن لن يُثبَّت صف وعمود الخلية نفسها. يمكن إلغاء تثبيت جميع الأجزاء من خلال ضبط السمة freeze_panes على القيمة None أو 'A1'. يوضح الجدول التالي الصفوف والأعمدة التي ستُثبَّت في بعض الأمثلة عند ضبط قيمة السمة freeze_panes: ضبط السمة freeze_panes الصفوف والأعمدة المُثبَّتة sheet.freeze_panes = 'A2' الصف 1 sheet.freeze_panes = 'B1' العمود A sheet.freeze_panes = 'C1' العمودان A و B sheet.freeze_panes = 'C2' الصف 1 والعمودان A و B sheet.freeze_panes = 'A1' أو sheet.freeze_panes = None لا توجد أجزاء مُثبَّتة تأكّد من حصولك على جدول بيانات مبيعات المنتجات، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('produceSales.xlsx') >>> sheet = wb.active >>> sheet.freeze_panes = 'A2' # تثبيت الصفوف الموجودة أعلى الصف A2 >>> wb.save('freezeExample.xlsx') إذا ضبطتَ السمة freeze_panes على القيمة 'A2'، فسيُعرَض الصف 1 دائمًا بغض النظر عن المكان الذي ينتقل إليه المستخدم عند التمرير في جدول البيانات، ويمكنك رؤية ذلك في الشكل التالي: يكون الصف 1 مرئيًا دائمًا حتى عندما يمرّر المستخدم جدول البيانات إلى الأسفل، إذا ضبطنا السمة freeze_panes على القيمة 'A2' المخططات Charts تدعم الوحدة OpenPyXL إنشاء مخططات شريطية وخطية ومبعثرة ودائرية باستخدام البيانات الموجودة في خلايا الورقة، حيث يمكنك إنشاء مخطط باتباع الخطوات التالية: إنشاء كائن Reference من خلايا المنطقة المستطيلة المُحدّدة. إنشاء كائن Series من خلال تمرير الكائن Reference. إنشاء كائن Chart. إلحاق كائن Series بكائن Chart. إضافة الكائن Chart إلى الكائن Worksheet مع تحديد الخلية التي يجب أن تكون في الزاوية العلوية اليسرى من المخطط اختياريًا. يمكنك إنشاء كائنات Reference من خلال استدعاء الدالة openpyxl.chart.Reference() وتمرير ثلاثة وسطاء هي: كائن Worksheet الذي يحتوي على بيانات مخططك. مجموعة Tuple مكونة من عددين صحيحين، حيث تمثل هذه المجموعة الخلية العلوية اليسرى من خلايا المنطقة المستطيلة المُحدَّدة التي تحتوي على بيانات مخططك، ويمثل العددُ الصحيح الأول في المجموعة الصفَّ، ويمثل العدد الصحيح الثاني العمود. لاحظ أن العدد 1 هو الصف الأول وليس العدد 0. مجموعة مكونة من عددين صحيحين، حيث تمثل هذه المجموعة الخلية السفلية اليمنى من خلايا المنطقة المستطيلة المُحدَّدة التي تحتوي على بيانات مخططك، ويمثل العدد الصحيح الأول في المجموعة الصف، ويمثل العدد الصحيح الثاني العمود. يوضح الشكل التالي بعض النماذج من وسطاء الإحداثيات: وهي من اليسار إلى اليمين: (1, 1), (10, 1); (3, 2), (6, 4); (5, 3), (5, 3) أدخِل مثال الصدفة التفاعلية التالي لإنشاء مخطط شريطي وإضافته إلى جدول البيانات: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> for i in range(1, 11): # إنشاء بعض البيانات في العمود A ... sheet['A' + str(i)] = i ... >>> refObj = openpyxl.chart.Reference(sheet, min_col=1, min_row=1, max_col=1, max_row=10) >>> seriesObj = openpyxl.chart.Series(refObj, title='First series') >>> chartObj = openpyxl.chart.BarChart() >>> chartObj.title = 'My Chart' >>> chartObj.append(seriesObj) >>> sheet.add_chart(chartObj, 'C5') >>> wb.save('sampleChart.xlsx') وينتج عن ذلك جدول بيانات يشبه ما يلي: جدول بيانات مع مخطط مضافٍ إليه أنشأنا مخططًا شريطيًا من خلال استدعاء الدالة openpyxl.chart.BarChart()، ويمكنك إنشاء مخططات خطية ومخططات مبعثرة ومخططات دائرية من خلال استدعاء الدوال openpyxl.charts.LineChart() و openpyxl.charts.LineChart() و openpyxl.charts.LineChart(). مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي، لكسب خبرة عملية أكبر. برنامج لإنشاء جدول الضرب أنشئ برنامج multiplicationTable.py الذي يأخذ العدد N من سطر الأوامر وينشئ جدول الضرب N×N في جدول بيانات إكسل، فمثلًا إذا شغّلنا البرنامج كما يلي: py multiplicationTable.py 6 يجب أن ينشئ جدول بيانات يشبه الشكل التالي: توليد جدول الضرب في جدول بيانات يجب استخدام الصف 1 والعمود A للتسميات أو عناوين الصفوف والأعمدة ويجب أن يكونا بالخط العريض. برنامج لإدراج صف فارغ أنشئ برنامج blankRowInserter.py الذي يأخذ عددين صحيحين وسلسلة نصية لاسم الملف كوسطاء لسطر الأوامر، حيث نسمّي العدد الصحيح الأول N والعدد الصحيح الثاني M. يجب على البرنامج إدراج صفوف فارغة بعدد M في جدول البيانات بدءًا من الصف N، فمثلًا إذا شغّلنا البرنامج كما يلي: python blankRowInserter.py 3 2 myProduce.xlsx يجب أن تبدو جداول البيانات "قبل" و"بعد" الإدراج كما في الشكل التالي: قبل (يسار) وبعد (يمين) إدراج الصفين الفارغين عند الصف 3 يمكنك كتابة هذا البرنامج من خلال قراءة محتويات جدول البيانات، ثم استخدام حلقة for لنسخ الأسطر N الأولى عند كتابة جدول البيانات الجديد، وجمع العدد M مع رقم الصف في جدول البيانات الناتج بالنسبة للأسطر المتبقية. برنامج لعكس خلايا جدول البيانات اكتب برنامجًا لعكس الصف والعمود الخاص بالخلايا في جدول البيانات، فمثلًا ستكون القيمة في الصف 5 والعمود 3 موجودة في الصف 3 والعمود 5 والعكس صحيح، ويجب تطبيق ذلك على جميع الخلايا في جدول البيانات، إذ ستبدو جداول البيانات "قبل" و"بعد" العكس كما في الشكل التالي: جدول البيانات قبل (العلوي) وبعد (السفلي) يمكنك كتابة هذا البرنامج باستخدام حلقات for متداخلة لقراءة بيانات جدول البيانات في قائمة من قوائم هيكل البيانات، ويمكن أن يحتوي هيكل البيانات على sheetData[x][y] للخلية الموجودة في العمود x والصف y. استخدم بعد ذلك sheetData[y][x] للخلية الموجودة في العمود x والصف y عند كتابة جدول البيانات الجديد. برنامج لتحويل الملفات النصية إلى جدول بيانات اكتب برنامجًا لقراءة محتويات العديد من الملفات النصية (يمكنك إنشاء الملفات النصية بنفسك) وإدراج هذه المحتويات في جدول بيانات، بحيث يقابل سطر واحد من الملف النصي صفًا واحدًا من جدول البيانات. ستكون أسطر الملف النصي الأول في خلايا العمود A، وستكون أسطر الملف النصي الثاني في خلايا العمود B، وهكذا. استخدم التابع readlines() الخاص بالكائن File لإعادة قائمة من السلاسل النصية، حيث تقابل كل سلسلة نصية واحدة سطرًا في الملف. يكون السطر الأول من الملف الأول مقابلًا للعمود 1 والصف 1، ويجب كتابة السطر الثاني في العمود 1 والصف 2، وما إلى ذلك. سيُكتَب الملف التالي المقروء باستخدام التابع readlines() في العمود 2، وسيُكتَب الملف الذي يليه في العمود 3، وهكذا. برنامج لتحويل جدول بيانات إلى ملفات نصية اكتب برنامجًا يؤدّي مهام البرنامج السابق بترتيب عكسي، إذ يجب أن يفتح البرنامج جدول بيانات ويكتب خلايا العمود A في ملف نصي واحد، وخلايا العمود B في ملف نصي آخر، وهكذا. الخلاصة لا يكون الجزء الصعب من معالجة المعلومات هو المعالجة بحد ذاتها في كثير من الأحيان، بل تتمثل الصعوبة ببساطة في الحصول على البيانات بالتنسيق الصحيح المناسب لبرنامجك، ولكن يمكنك استخراج بيانات جدول بياناتك ومعالجتها بصورة أسرع بكثير مما يمكنك فعله يدويًا بعد تحميله إلى ملف بايثون. يمكنك أيضًا توليد جداول بيانات بوصفها خرجًا لبرامجك، لذا إذا كان زملاؤك بحاجة إلى نقل ملفك النصي أو ملف PDF الذي يحتوي على آلاف جهات اتصال المبيعات إلى ملف جدول بيانات، فلن تضطر إلى نسخه ولصقه بالكامل في ملف إكسل. إذا كان لديك وحدة openpyxl وبعض المعرفة البرمجية، فستجد أن معالجة جداول البيانات الكبيرة تُعَد أمرًا سهلًا للغاية. سنلقي نظرة في المقال التالي على استخدام لغة بايثون للتفاعل مع برنامج آخر لجداول بيانات، وهو تطبيق جداول بيانات جوجل Google Sheets الشهير على الإنترنت. ترجمة -وبتصرُّف- للقسم Writing Excel Documents من مقال Working with Excel Spreadsheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: قراءة مستندات جداول إكسل باستخدام لغة بايثون Python استخراج البيانات من الويب عبر لغة بايثون Python قراءة وكتابة الملفات باستخدام لغة بايثون Python التعامل مع الملفات والمسارات في بايثون نظرة عامة على برنامج مايكروسوفت إكسل Microsoft Excel النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
-
قد لا نفكر في جداول البيانات بوصفها أدوات برمجية، ولكن يستخدمها الجميع تقريبًا لتنظيم المعلومات في هياكل بيانية ثنائية الأبعاد، وإجراء العمليات الحسابية باستخدام الصيغ، وعرض المخرجات على شكل مخططات، لذا سندمج لغة بايثون مع تطبيقين شائعين خاصين بجداول البيانات وهما: مايكروسوفت إكسل Microsoft Excel وجداول بيانات جوجل Google Sheets. يُعَد إكسل تطبيق جداول بيانات قوي وشائع الاستخدام لنظام تشغيل ويندوز، وتوجد وحدةٌ برمجية هي وحدة openpyxl التي تسمح لبرامجك المكتوبة بلغة بايثون بقراءة وتعديل ملفات جداول بيانات إكسل. يمكن أن تكون لديك مهمة مملة متمثلة في نسخ بيانات معينة من جدول بيانات ولصقها في جدول بيانات آخر مثلًا، أو قد يتعين عليك أن تمر على آلاف الصفوف واختيار عددٍ قليل منها لإجراء تعديلات بسيطة بناءً على بعض المعايير، أو قد يتعين عليك الاطلاع على مئات جداول البيانات الخاصة بميزانيات الأقسام باحثًا عن الخلايا الملونة باللون الأحمر، وهذه هي مهام جداول البيانات البسيطة والمملة التي يمكن لبايثون تطبيقها نيابةً عنك. يُعَد برنامج إكسل برنامجًا خاصًا بشركة مايكروسوفت، ولكن توجد بدائل مجانية تعمل على أنظمة تشغيل ويندوز وماك macOS ولينكس Linux، حيث يعمل كل من ليبرأوفيس كالك LibreOffice Calc وأوبن أوفيس كالك OpenOffice Calc بنجاح مع صيغة ملفات جداول البيانات .xlsx الخاصة بإكسل، مما يعني أن الوحدة openpyxl يمكن أن تعمل مع جداول البيانات الخاصة بهذين التطبيقين أيضًا، ويمكنك تنزيلهما من موقعهما الرسمي مباشرةً. قد تجد أن هذين البرنامجين أسهل في الاستخدام من إكسل بالرغم من أن برنامج إكسل مُثبَّتٌ مسبقًا على حاسوبك الشخصي، ولكن لقطات الشاشة الموجودة في هذا المقال جميعها مأخوذة من إكسل 2010 على نظام تشغيل ويندوز 10. مستندات إكسل لنتعرّف أولًا على بعض التعريفات الأساسية، حيث يُسمَّى مستند جدول بيانات إكسل بالمصنف Workbook، ويُحفَظ المصنف في ملف امتداده .xlsx، ويمكن أن يحتوي المصنف على أوراق Sheets متعددة (وتسمَّى أوراق عمل Worksheets أيضًا)، كما تُسمَّى الورقة التي يعرضها المستخدم حاليًا -أو المعروضة آخر مرة قبل إغلاق إكسل- بالورقة النشطة Active Sheet. تحتوي كل ورقة على أعمدة Columns تتعامل معها باستخدام حروف تبدأ بالحرف A، وصفوف Rows تتعامل معها باستخدام أعداد تبدأ بالعدد 1. يسمى المربع الموجود في عمود أو صف معين بالخلية Cell، ويمكن أن تحتوي كل خلية على قيمة عددية أو نصية، وتتشكّل الورقة من شبكةٍ من الخلايا التي تحتوي على البيانات. تثبيت وحدة openpyxl لا تحتوي لغة بايثون مسبقًا على وحدة OpenPyXL، إذ يجب عليك تثبيتها أولًا، لذا اتبع الإرشادات الخاصة بتثبيت الوحدات الخارجية التي سنوضحها في مقال لاحق. سنستخدم في هذا المقال الإصدار 2.6.2 من الوحدة OpenPyXL، لذا من المهم أن تثبّت هذا الإصدار من خلال تشغيل الأمرpip install --user -U openpyxl==2.6.2، لأن الإصدارات الأحدث منها غير متوافقة مع المعلومات الموجودة في هذا المقال. أدخِل الأمر التالي في الصدفة التفاعلية Interactive Shell لاختبار ما إذا كان كانت وحدة OpenPyXL مُثبَّتة بصورة صحيحة: >>> import openpyxl إذا جرى تثبيت الوحدة بصورة صحيحة، فلن ينتج عن الأمر السابق أيّ رسائل خطأ. تذكّر استيراد الوحدة openpyxl قبل تشغيل أمثلة أوامر الصدفة التفاعلية في هذا المقال، وإلّا فستحصل على الخطأ NameError: name 'openpyxl' is not defined. ملاحظة: يمكنك العثور على توثيق وحدة OpenPyXL الكامل على موقعها الرسمي. قراءة مستندات إكسل سنستخدم في الأمثلة الواردة في هذا المقال جدول بيانات اسمه example.xlsx مُخزَّن في المجلد الجذر، حيث يمكنك إما إنشاء جدول البيانات بنفسك أو تنزيله. يوضّح الشكل التالي تبويبات للأوراق الافتراضية الثلاثة التي اسمها Sheet1 و Sheet2 و Sheet3 التي يوفرها إكسل تلقائيًا للمصنفات الجديدة، ولكن قد يختلف عدد هذه الأوراق الافتراضية بحسب نظام التشغيل وبرنامج جداول البيانات: توجد التبويبات الخاصة بأوراق المصنف في الزاوية السفلية اليسرى من إكسل يجب أن تبدو الورقة 1 في مثالنا مثل الجدول التالي، ولكن إن لم تنزّل الملف example.xlsx من الموقع، فيجب أن تدخِل هذه البيانات في الورقة بنفسك: A B C 1 4/5/2015 1:34:02 PM Apples 73 2 4/5/2015 3:41:23 AM Cherries 85 3 4/6/2015 12:46:51 PM Pears 14 4 4/8/2015 8:59:43 AM Oranges 52 5 4/10/2015 2:07:00 AM Apples 152 6 4/10/2015 6:10:37 PM Bananas 23 7 4/10/2015 2:40:46 AM Strawberries 98 أصبح جدول البيانات جاهزًا الآن، إذًا لنتعرّف على كيفية التعامل معه باستخدام وحدة openpyxl. فتح مستندات إكسل باستخدام وحدة OpenPyXL يمكنك استخدام الدالة openpyxl.load_workbook() بعد استيراد وحدة openpyxl مباشرةً، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> type(wb) <class 'openpyxl.workbook.workbook.Workbook'> تأخذ الدالة openpyxl.load_workbook() اسم الملف وتعيد قيمة نوع بيانات المصنف workbook، حيث يمثل الكائن Workbook ملف إكسل، ويشبه ذلك كيفية تمثيل الكائن File ملفًا نصيًا مفتوحًا. تذكّر أن الملف example.xlsx يجب أن يكون موجودًا في مجلد العمل الحالي لتتمكّن من التعامل معه، إذ يمكنك معرفة مجلد العمل الحالي من خلال استيراد وحدة os واستخدام الدالة os.getcwd()، ويمكنك تغيير مجلد العمل الحالي باستخدام الدالة os.chdir(). الحصول على الأوراق من المصنف يمكنك الحصول على قائمة بجميع أسماء الأوراق في المصنف من خلال الوصول إلى السمة Attribute التي هي sheetnames، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> wb.sheetnames # أسماء الأوراق الخاصة بالمصنف ['Sheet1', 'Sheet2', 'Sheet3'] >>> sheet = wb['Sheet3'] # الحصول على ورقة من المصنف >>> sheet <Worksheet "Sheet3"> >>> type(sheet) <class 'openpyxl.worksheet.worksheet.Worksheet'> >>> sheet.title # الحصول على عنوان الورقة بوصفه سلسلة نصية 'Sheet3' >>> anotherSheet = wb.active # الحصول على الورقة النشطة >>> anotherSheet <Worksheet "Sheet1"> يمثّل الكائنُ Worksheet الورقة التي يمكنك الحصول عليها باستخدام الأقواس المربعة مع السلسلة النصية التي تمثّل اسم هذه الورقة، حيث يشبه ذلك استخدام مفتاح القاموس، ويمكنك استخدام السمة active للكائن Workbook للحصول على ورقة المصنف النشطة، فالورقة النشطة هي الورقة الموجودة في المقدمة عند فتح المصنف في إكسل. كما يمكنك الحصول على اسم الكائن Worksheet من سمة العنوان title بعد الحصول على هذا الكائن. الحصول على الخلايا من الأوراق يمكنك الوصول إلى الكائن Cell باستخدام اسمه بعد الحصول على الكائن Worksheet، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] # الحصول على ورقة من المصنف >>> sheet['A1'] # الحصول على خلية من الورقة <Cell 'Sheet1'.A1> >>> sheet['A1'].value # الحصول على القيمة من الخلية datetime.datetime(2015, 4, 5, 13, 34, 2) >>> c = sheet['B1'] # الحصول على خلية أخرى من الورقة >>> c.value 'Apples' >>> # الحصول على الصف والعمود والقيمة من الخلية >>> 'Row %s, Column %s is %s' % (c.row, c.column, c.value) 'Row 1, Column B is Apples' >>> 'Cell %s is %s' % (c.coordinate, c.value) 'Cell B1 is Apples' >>> sheet['C1'].value 73 يحتوي كائن الخلية Cell على سمة القيمة value التي تحتوي على القيمة المُخزَّنة في هذه الخلية، وتحتوي الكائنات Cell أيضًا على سمات الصف row والعمود column والإحداثيات coordinate التي توفّر معلومات موقع الخلية، فمثلًا يعطينا الوصول إلى السمة value للكائن Cell الخاص بالخلية B1 السلسلة النصية 'Apples'، وتعطينا السمة row العدد الصحيح 1، وتعطينا السمة column العمود 'B'، وتعطي السمة coordinate القيمة 'B1'. تفسّر الوحدة OpenPyXL تلقائيًا التواريخ الموجودة في العمود A وتعيدها بوصفها قيم datetime بدلًا من إعادتها كسلاسل نصية، حيث سنوضّح لاحقًا نوع البيانات datetime. يمكن أن يكون تحديد عمود باستخدام حرف أمرًا صعبًا برمجيًا، وخاصةً لأن الأعمدة تبدأ باستخدام حرفين مثل AA و AB و AC بعد العمود Z، لذا يمكنك بدلًا من ذلك الحصول على خلية باستخدام التابع cell() الخاص بالورقة وتمرير أعداد صحيحة لوسطاء الكلمات المفتاحية Keyword Arguments التي هي row و column الخاصة بهذا التابع، ويكون العدد الصحيح للصف أو العمود الأول هو 1 وليس 0. لندخِل ما يلي في الصدفة التفاعلية: sheet.cell(row=1, column=2) <Cell 'Sheet1'.B1> >>> sheet.cell(row=1, column=2).value 'Apples' >>> for i in range(1, 8, 2): # المرور على جميع الصفوف الأخرى ... print(i, sheet.cell(row=i, column=2).value) ... 1 Apples 3 Pears 5 Apples 7 Strawberries لاحظ أن استخدام التابع cell() الخاص بالورقة وتمرير row=1 و column=2 له يعطي كائن Cell للخلية B1 كما فعل استخدام sheet['B1'] تمامًا. يمكنك بعد ذلك كتابة حلقة for لطباعة قيم سلسلة من الخلايا باستخدام التابع cell() ووسطاء الكلمات المفتاحية الخاصة به. لنفترض أنك تريد الانتقال إلى العمود B وطباعة القيمة الموجودة في جميع الخلايا التي يكون رقم صفها عددًا فرديًا، حيث يمكنك الحصول على الخلايا لجميع الصفوف لها أرقام فردية من خلال تمرير القيمة 2 لمعامل "الخطوة step" الخاص بالدالة range(). يُمرَّر المتغير i الخاص بالحلقة for إلى وسيط الكلمة المفتاحية row الخاص بالتابع cell()، بينما تُمرّر القيمة 2 دائمًا إلى وسيط الكلمة المفتاحية column في هذه الحالة. لاحظ أننا مرّرنا العدد الصحيح 2 ولم نمرّر السلسلة النصية 'B'. يمكنك تحديد حجم الورقة باستخدام السمتين max_row و max_column للكائن Worksheet كما يلي: import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> sheet.max_row # الحصول على عدد الصفوف الأكبر 7 >>> sheet.max_column # الحصول على عدد الأعمدة الأكبر 3 لاحظ أن السمة max_column تمثل عددًا صحيحًا ولا تمثّل الحرف الذي يظهر في إكسل. تحويل حروف الأعمدة إلى أعداد يمكنك تحويل أسماء الأعمدة من حروف إلى أعداد من خلال استدعاء الدالة openpyxl.utils.column_index_from_string()، ويمكنك التحويل من أعداد إلى حروف من خلال استدعاء الدالة openpyxl.utils.get_column_letter(). إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> from openpyxl.utils import get_column_letter, column_index_from_string >>> get_column_letter(1) # ترجمة العمود 1 إلى حرف 'A' >>> get_column_letter(2) 'B' >>> get_column_letter(27) 'AA' >>> get_column_letter(900) 'AHP' >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> get_column_letter(sheet.max_column) 'C' >>> column_index_from_string('A') # الحصول على العدد المقابل للحرف A 1 >>> column_index_from_string('AA') 27 يمكنك استدعاء الدالة get_column_letter() وتمرير عدد صحيح لها مثل العدد 27 لمعرفة اسم الحرف في العمود السابع والعشرين بعد استيراد الدالتين السابقتين من الوحدة openpyxl.utils. بينما تطبّق الدالة column_index_string() العكس، حيث تمرّر لها الحرف الذي يمثل اسم العمود، وتعطيك رقم هذا العمود. لا تحتاج إلى تحميل مصنف لاستخدام هذه الدوال، ولكن إن أردت يمكنك تحميل مصنف، ثم الحصول على الكائن Worksheet واستخدام سمته max_column مثلًا للحصول على عدد صحيح، ثم يمكنك تمرير هذا العدد الصحيح إلى الدالة get_column_letter(). الحصول على الصفوف والأعمدة من الأوراق يمكنك تقسيم الكائنات Worksheet للحصول على كافة الكائنات Cell الموجودة في صف أو عمود أو منطقة مستطيلة من جدول البيانات، ثم يمكنك التكرار على جميع الخلايا الموجودة في كل قسم. أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> tuple(sheet['A1':'C3']) # الحصول على جميع الخلايا من A1 إلى C3 ((<Cell 'Sheet1'.A1>, <Cell 'Sheet1'.B1>, <Cell 'Sheet1'.C1>), (<Cell 'Sheet1'.A2>, <Cell 'Sheet1'.B2>, <Cell 'Sheet1'.C2>), (<Cell 'Sheet1'.A3>, <Cell 'Sheet1'.B3>, <Cell 'Sheet1'.C3>)) ➊ >>> for rowOfCellObjects in sheet['A1':'C3']: ➋ ... for cellObj in rowOfCellObjects: ... print(cellObj.coordinate, cellObj.value) ... print('--- نهاية الصف ---') A1 2015-04-05 13:34:02 B1 Apples C1 73 --- نهاية الصف --- A2 2015-04-05 03:41:23 B2 Cherries C2 85 --- نهاية الصف --- A3 2015-04-06 12:46:51 B3 Pears C3 14 --- نهاية الصف — حدّدنا في المثال السابق أننا نريد كائنات Cell في المنطقة المستطيلة من الخلية A1 إلى الخلية C3، وحصلنا على كائن Generator الذي يحتوي على كائنات Cell في تلك المنطقة، حيث يمكننا تصوّر الكائن Generator من خلال استخدام الدالة tuple() معه لعرض كائنات Cell الخاصة به ضمن مجموعة Tuple. تحتوي هذه المجموعة على ثلاث مجموعات أخرى، مجموعة لكل صف من أعلى المنطقة المطلوبة إلى أسفلها، حيث تحتوي كل مجموعة من هذه المجموعات الداخلية الثلاث على كائنات Cell في صف واحد من المنطقة المطلوبة من الخلية الموجودة في أقصى اليسار إلى اليمين. يحتوي قسم الورقة الذي حدّدناه على جميع كائنات Cell في المنطقة المؤلفة من الخلية A1 إلى الخلية C3، بدءًا من الخلية العلوية اليسرى وانتهاءً بالخلية السفلية اليمنى. طبعنا قيم كل خلية في هذه المنطقة باستخدام حلقتي for، حيث تتكرر حلقة for الخارجية على كل صف في القسم ➊، ثم تتكرر حلقة for المتداخلة لكل صف على كل خلية في هذا الصف ➋. يمكن أيضًا الوصول إلى قيم الخلايا في صف أو عمود معين من خلال استخدام سمة rows و columns الخاصة بكائن Worksheet، ولكن يجب تحويل هذه السمات إلى قوائم باستخدام الدالة list() قبل أن تتمكّن من استخدام الأقواس المربعة والفهرس معها. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb.active >>> list(sheet.columns)[1] # الحصول على خلايا العمود الثاني (<Cell 'Sheet1'.B1>, <Cell 'Sheet1'.B2>, <Cell 'Sheet1'.B3>, <Cell 'Sheet1'. B4>, <Cell 'Sheet1'.B5>, <Cell 'Sheet1'.B6>, <Cell 'Sheet1'.B7>) >>> for cellObj in list(sheet.columns)[1]: print(cellObj.value) Apples Cherries Pears Oranges Apples Bananas Strawberries سيؤدي استخدام السمة rows مع الكائن Worksheet إلى إعطاء مجموعة من عدة مجموعات، وتمثل كل مجموعة من هذه المجموعات الداخلية صفًا، وتحتوي على كائنات Cell الموجودة في هذا الصف. تعطي السمة columns أيضًا مجموعة من عدة مجموعات، حيث تحتوي كل مجموعة من المجموعات الداخلية على كائنات Cell في عمود معين. لدينا في مثالنا الملف example.xlsx الذي يحتوي على 7 صفوف و3 أعمدة، وبالتالي تعطي السمة rows مجموعة مؤلفةً من 7 مجموعات، حيث تحتوي كل منها على 3 كائنات Cell، وتعطي السمة columns مجموعة مكونة من 3 مجموعات، حيث تحتوي كل منها على 7 كائنات Cell. يمكن الوصول إلى مجموعة معينة من خلال الإشارة إليها باستخدام فهرسها في المجموعة الكبرى، فمثلًا نحصل على المجموعة التي تمثل العمود B من خلال استخدام list(sheet.columns)[1]، ونحصل على المجموعة التي تحتوي على كائنات Cell في العمود A من خلال استخدام list(sheet.columns)[0]. يمكنك بعد أن يكون لديك مجموعة تمثل صفًا أو عمودًا واحدًا التكرار على كائنات Cell الخاصة بها وطباعة قيمها. المصنفات والأوراق والخلايا إليك ملخص بجميع الدوال والتوابع وأنواع البيانات المستخدمة في قراءة خلية من ملف جدول بيانات: استيراد وحدة openpyxl. استدعاء الدالة openpyxl.load_workbook(). الحصول على كائن Workbook. استخدام السمة active أو السمة sheetnames. الحصول على كائن Worksheet. استخدم طريقة الفهرسة أو تابع cell() الخاص بالورقة مع وسطاء الكلمات المفتاحية row و column. الحصول على كائن Cell. قراءة السمة value الخاصة بالكائن Cell. تطبيق عملي: قراءة البيانات من جدول بيانات لنفترض أن لديك جدول بيانات يمثّل الإحصاء السكاني للولايات المتحدة الأمريكية لعام 2010، ولديك مهمة مملة تتمثل في استعراض آلاف الصفوف لحساب إجمالي عدد السكان وعدد المناطق الإحصائية Census Tracts لكل مقاطعة County، فالمنطقة الإحصائية هي ببساطة منطقة جغرافية محددة لأغراض الإحصاء السكاني، ويمثل كل صف في جدول البيانات منطقةً إحصائية واحدة. سنسمّي ملف جدول البيانات censuspopdata.xlsx الذي يمكنك تنزيله، وتبدو محتوياته كما يلي: جدول بيانات censuspopdata.xlsx يستطيع إكسل حساب مجموع خلايا محددة متعددة، ولكن يجب عليك أيضًا تحديد الخلايا التي تمثل المقاطعات التي يزيد عدد سكانها عن 3000 نسمة. كما قد يستغرق حساب عدد سكان المقاطعة يدويًا بضع ثوانٍ فقط، ولكنه قد يستغرق ساعات لجدول البيانات بأكمله. ستكتب في هذا التطبيق العملي سكربتًا يمكنه القراءة من ملف جدول بيانات الإحصاء السكاني وحساب إحصائيات كل مقاطعة في غضون ثوانٍ، إذ سيفعل برنامجك ما يلي: يقرأ البيانات من جدول بيانات إكسل. يحسب عدد المناطق الإحصائية في كل مقاطعة. يحسب إجمالي عدد السكان في كل مقاطعة. يطبع النتائج. وهذا يعني أن شيفرتك البرمجية ستحتاج ما يلي: فتح وقراءة خلايا مستند إكسل باستخدام وحدة openpyxl. حساب جميع بيانات المناطق الإحصائية وعدد السكان وتخزينها في هيكل بيانات. كتابة هيكل البيانات في ملف نصي له الامتداد .py باستخدام الوحدة pprint. الخطوة الأولى: قراءة بيانات جدول البيانات توجد ورقة واحدة فقط في جدول البيانات censuspopdata.xlsx، تسمى "عدد السكان حسب المنطقة الإحصائية" 'Population by Census Tract'، ويحتوي كل صف على بيانات منطقة إحصائية واحدة، والأعمدة هي رقم المنطقة (A) واختصار الولاية (B) واسم المقاطعة (C) وعدد سكان المنطقة (D). افتح تبويبًا جديدًا لإنشاء ملف جديد في محرّرك وأدخِل الشيفرة البرمجية التالية، واحفظ الملف بالاسم readCensusExcel.py: #! python3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة ➊ import openpyxl, pprint print('Opening workbook...') ➋ wb = openpyxl.load_workbook('censuspopdata.xlsx') ➌ sheet = wb['Population by Census Tract'] countyData = {} # املأ بيانات المقاطعة countyData بعدد سكان كل مقاطعة ومناطقها الإحصائية print('Reading rows...') ➍ for row in range(2, sheet.max_row + 1): # يحتوي كل صف في جدول البيانات على بياناتٍ لمنطقة إحصائية واحدة state = sheet['B' + str(row)].value county = sheet['C' + str(row)].value pop = sheet['D' + str(row)].value # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه تستورد الشيفرة البرمجية السابقة الوحدة openpyxl والوحدة pprint التي ستستخدمها لطباعة بيانات المقاطعة النهائية ➊، ثم تفتح الملف censuspopdata.xlsx ➋، وتحصل على الورقة التي تحتوي على البيانات الإحصائية ➌، وتبدأ بالتكرار على صفوف هذه الورقة ➍. لاحظ أنك أنشأتَ أيضًا متغيرًا بالاسم countyData، والذي سيحتوي على عدد السكان وعدد المناطق التي تحسبها لكل مقاطعة، ولكن يجب عليك تحديد كيفية هيكلة البيانات بداخله قبل أن تتمكن من تخزين أيّ شيء فيه. الخطوة الثانية: ملء هيكل البيانات يُعَد هيكل البيانات المُخزَّن في المتغير countyData قاموسًا تكون اختصارات أسماء الولايات مفاتيحًا له، حيث سيُربَط اختصار كل ولاية مع قاموس آخر مفاتيحه هي سلاسل نصية تمثّل أسماء المقاطعات في تلك الولاية، وسيُربَط كل اسم مقاطعة بدوره مع قاموسٍ آخر يحتوي على مفتاحين فقط هما 'tracts' و 'pop'، ويُربَط هذان المفتاحان مع عدد المناطق الإحصائية وعدد السكان في المقاطعة، فمثلًا سيبدو القاموس مشابهًا لما يلي: {'AK': {'Aleutians East': {'pop': 3141, 'tracts': 1}, 'Aleutians West': {'pop': 5561, 'tracts': 2}, 'Anchorage': {'pop': 291826, 'tracts': 55}, 'Bethel': {'pop': 17013, 'tracts': 3}, 'Bristol Bay': {'pop': 997, 'tracts': 1}, --snip– إذا خُزِّن القاموس السابق في المتغير countyData، فيمكن تقييم التعابير التالية كما يلي: >>> countyData['AK']['Anchorage']['pop'] 291826 >>> countyData['AK']['Anchorage']['tracts'] 55 وستكون مفاتيح قاموس countyData كما يلي: countyData[state abbrev][county]['tracts'] countyData[state abbrev][county]['pop'] عرفتَ كيفية تنظيم هيكل بيانات countyData، ويمكنك الآن كتابة الشيفرة البرمجية التي ستملؤه ببيانات المقاطعة، لذا أضِف الشيفرة البرمجية التالية إلى نهاية برنامجك: #! python 3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة --snip-- for row in range(2, sheet.max_row + 1): # يحتوي كل صف في جدول البيانات على بيانات لمنطقة إحصائية واحدة state = sheet['B' + str(row)].value county = sheet['C' + str(row)].value pop = sheet['D' + str(row)].value # تأكد من وجود مفتاح هذه الولاية state ➊ countyData.setdefault(state, {}) # تأكد من وجود مفتاح هذه المقاطعة county في تلك الولاية ➋ countyData[state].setdefault(county, {'tracts': 0, 'pop': 0}) # يمثل كل صف منطقة إحصائية واحدة، لذا يجب زيادة عدد المناطق بمقدار واحد ➌ countyData[state][county]['tracts'] += 1 # زيادة عدد سكان pop المقاطعة بمقدار عدد السكان في هذه المنطقة الإحصائية ➍ countyData[state][county]['pop'] += int(pop) # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه يجري السطران الأخيران من الشيفرة البرمجية السابقة العمليات الحسابية الفعلية، حيث تزيد قيمة المناطق الإحصائية tracts ➌ وقيمة عدد السكان pop ➍ للمقاطعة الحالية في كل تكرار لحلقة for. بينما سببُ وجود الشيفرة البرمجية المتبقية هو أنه لا يمكنك إضافة قاموس المقاطعة بوصفه قيمةً لمفتاح اختصار الولاية إلّا عند وجود المفتاح نفسه في countyData، إذ ستتسبّب التعليمة countyData['AK']['Anchorage']['tracts'] += 1 في حدوث خطأ إن لم يكن المفتاح 'AK' موجودًا بعد. يمكنك التأكد من وجود مفتاح اختصار الولاية في هيكل بياناتك من خلال استدعاء التابع setdefault() لضبط قيمة الولاية state ➊ إن لم تكن موجودة مسبقًا. يحتاج قاموس countyData إلى قاموس آخر بوصفه قيمةً لكل مفتاح يمثّل اختصار الولاية، وبالتالي سيحتاج كلٌّ من هذه القواميس إلى قاموس خاص به بوصفه قيمة لكل مفتاح مقاطعة ➋، وسيحتاج كل من هذه القواميس بدوره إلى مفاتيح 'tracts' و 'pop' التي تبدأ بالقيمة الصحيحة 0. إذا شعرت بالضياع عند تتبّع بنية القاموس، فارجع إلى مثال القاموس في بداية هذه الفقرة. لن يفعل التابع setdefault() شيئًا إذا كان المفتاح موجودًا مسبقًا، وبالتالي يمكنك استدعاؤه في كل تكرار للحلقة for بدون مشاكل. الخطوة الثالثة: كتابة النتائج في ملف سيحتوي قاموس countyData بعد انتهاء حلقة for على جميع معلومات عدد السكان والمناطق المرتبطة بمفتاح المقاطعة county والولاية state، ويمكنك عندها برمجة مزيدٍ من الشيفرة البرمجية لكتابة هذه المعلومات في ملف نصي أو جدول بيانات إكسل آخر. لنستخدم الآن الدالة pprint.pformat() لكتابة قيمة قاموس countyData بوصفها سلسلة نصية ضخمة في ملف اسمه census2010.py، لذا أضِف الشيفرة البرمجية التالية إلى نهاية برنامجك، وتأكد من إبقائه بدون مسافة بادئة بحيث يبقى خارج حلقة for: #! python 3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة --snip-- for row in range(2, sheet.max_row + 1): --snip-- # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه print('Writing results...') resultFile = open('census2010.py', 'w') resultFile.write('allData = ' + pprint.pformat(countyData)) resultFile.close() print('Done.') ينتج عن الدالة pprint.pformat() سلسلةٌ نصية مُنسَّقة كشيفرة بايثون صالحة، والتي يمكنك إخراجها إلى ملف نصي اسمه census2010.py، وبالتالي سيتولد برنامج بايثون من برنامج بايثون الخاص بك. قد يبدو ذلك معقدًا، ولكن تتمثّل الفائدة في أنه يمكنك استيراد الملف census2010.py مثل أي وحدة بايثون أخرى. غيّر مجلد العمل الحالي إلى المجلد الذي يحتوي على الملف census2010.py ثم استورده في الصدفة التفاعلية كما يلي: >>> import os >>> import census2010 >>> census2010.allData['AK']['Anchorage'] {'pop': 291826, 'tracts': 55} >>> anchoragePop = census2010.allData['AK']['Anchorage']['pop'] >>> print('The 2010 population of Anchorage was ' + str(anchoragePop)) The 2010 population of Anchorage was 291826 يُعَد برنامج readCensusExcel.py عديم الجدوى، فلا حاجة لتشغيله مرة أخرى بعد حفظ نتائجه في الملف census2010.py، ويمكنك تشغيل الأمر import census2010 عندما تحتاج إلى بيانات المقاطعة. سيستغرق حساب هذه البيانات يدويًا ساعات يدويًا، ولكن أنجز هذا البرنامج هذا الأمر في بضع ثوان، ولن تواجه أيّ مشكلة في استخراج المعلومات المحفوظة في جدول بيانات إكسل وإجراء العمليات الحسابية عليها باستخدام وحدة OpenPyXL. ملاحظة: لا تنسَ أنه يمكنك تنزيل البرنامج الكامل. أفكار لبرامج مماثلة تستخدم العديد من الشركات والمكاتب برنامج إكسل لتخزين أنواع مختلفة من البيانات، وتصبح جداول البيانات كبيرة الحجم وغير عملية بسهولة. تمتلك البرامج التي تحلّل جدول بيانات إكسل بنية مماثلة، فهي تحمّل ملف جدول البيانات، وتجهّز بعض المتغيرات أو هياكل البيانات، ثم تتكرر على كل صف من الصفوف في جدول البيانات، حيث يمكن أن تفعل مثل هذه البرامج ما يلي: مقارنة البيانات في صفوف متعددة في جدول بيانات. فتح ملفات إكسل متعددة ومقارنة البيانات بين جداول بيانات. التحقق من احتواء جدول البيانات على صفوف فارغة أو بيانات غير صالحة في أيّ خلايا وتنبيه المستخدم في حالة وجود ذلك. قراءة البيانات من جدول البيانات واستخدامها كدخلٍ لبرامج بايثون الخاصة بك. الخلاصة تعرّفنا في هذا المقال على كيفية قراءة مستندات إكسل باستخدام بايثون، حيث وضّحنا كيفية فتح مستندات إكسل والحصول على الأوراق من المصنف والحصول على الخلايا والصفوف والأعمدة من الأوراق باستخدام وحدة OpenPyXL الخاصة بلغة بايثون، وطبّقنا هذه المعرفة على مثال عملي لقراءة البيانات من جدول بيانات يمثل الإحصاء السكان في الولايات المتحدة الأمريكية لعام 2010، وسنتابع في المقال التالي العمل على جداول بيانات إكسل من خلال توضيح كيفية الكتابة في مستندات إكسل باستخدام بايثون. ترجمة -وبتصرُّف- للقسم Reading Excel Documents من مقال Working with Excel Spreadsheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: استخراج البيانات من الويب عبر لغة بايثون Python قراءة وكتابة الملفات باستخدام لغة بايثون Python التعامل مع الملفات والمسارات في بايثون كيفية التعامل مع الملفات النصية في بايثون 3 نظرة عامة على برنامج مايكروسوفت إكسل Microsoft Excel النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
-
كانت لحظة مرعبة حينما جلست على حاسوبي بعد أن «عضّ القرش» كبل الإنترنت وانقطع، وأدركت حينها كم أقضي وقتًا على الإنترنت حينما أستعمل حاسوبي؛ فلدي عادة أن أتحقق من بريدي يدويًا (مع أن التنبيهات تصلني أولًا بأول!) وأفتح تويتر (إكس، سمهِّ ما شئت) وأنظر ما آخر المستجدات. كثيرٌ من عملنا على الحاسوب يتطلب وصولًا إلى الانترنت، ومصطلح «استخراج البيانات من الويب» Web scraping يستعمل مع البرامج التي تنزل المحتوى من الإنترنت وتعالجه. فمثلًا لدى غوغل عدد من البوتات لتنزيل محتوى صفحات الويب وفهرستها وأرشفتها لتستعملها في محرك البحث. سنتعلم في هذا المقال عن عددٍ من الوحدات في بايثون التي تسهل علينا استخراج البيانات من الويب: webbrowser: حزمة تأتي مع بايثون وتفتح متصفحًا على صفحة ويب معينة. requests: تنزل الملفات وصفحات الويب من الإنترنت. bs4: تفسّر شيفرات HTML التي تكتب فيها صفحات الويب. selenium: تشغل وتتحمل في متصفح ويب، والوحدة selenium قادرة على ملء الاستمارات ومحاكاة ضغطات الفأرة في المتصفح. مشروع: برنامج mapIt.py مع وحدة webbrowser الدالة open() في الوحدة webbrowser تفتح صفحة ويب معينة في نافذة متصفح جديدة. جرب ما يلي في الصدفة التفاعلية: >>> import webbrowser >>> webbrowser.open('https://academy.hsoub.com/') ستجد أكاديمية حسوب مفتوحةً في لسانٍ جديد في المتصفح. هذا كل ما تستطيع الوحدة webbrowser فعله، لكن مع ذلك يمكننا أن نجري بعض الأمور اللطيفة مع الدالة open()، فمثلًا قد تكون مهمة فتح خرائط جوجل والبحث عن عنوان معين أمرًا مملًا، ونستطيع التخلص من بضع خطوات لو كتبنا سكربتًا يفتح خرائط جوجل ويوجهها إلى عنوان الشارع المنسوخ في الحافظة لديك، وبالتالي سيكون عليك أن تنسخ العنوان إلى الحافظة وتشغل السكربت، وستفتح الخريطة لديك. هذا ما يفعله البرنامج: يحصل على عنوان الشارع من وسائط سطر الأوامر أو من الحافظة يفتح نافذة متصفح ويوجهها إلى صفحة خرائط جوجل المرتبطة بعنوان الشارع. هذا يعني أن على الشيفرة البرمجية أن تفعل ما يلي: تقرأ وسائط سطر الأوامر تقرأ محتويات الحافظة تستدعي الدالة webbrowser.open() لتفتح صفحة الويب. احفظ ملفًا جديدًا باسم mapIt.py، ولنبدأ برمجته. الخطوة 1: معرفة الرابط الصحيح اعتمادًا على التعليمات الموجودة في الملحق ب، اضبط برنامج mapIt.py ليعمل من سطر الأوامر كما في المثال الآتي: C:\> mapit 870 Valencia St, San Francisco, CA 94110 سيستخدم البرنامج وسائط سطر الأوامر بدلًا من الحافظة، وإذا لم نمرر إليه أيّة وسائط فحينها سيقرأ محتويات الحافظة. علينا بدايةً أن نحصِّل ما هو عنوان URL الذي يجب فتحه للعثور على شارع معين. إذا فتحت خرائط جوجل في المتصفح وبحثت عن عنوان فسيكون الرابط في الشريط العلوي يشبه: https://www.google.com/maps/place/870+Valencia+St/@37.7590311,-122.4215096,17z/data=!3m1!4b1!4m2!3m1!1s0x808f7e3dadc07a37:0xc86b0b2bb93b73d8 نعم العنوان في الرابط، لكن هنالك نص كثير إضافي غيره. تضيف المواقع عادةً بيانات إضافية لتتبع الزوار أو تخصيص المواقع. لكن إن حاولت الذهاب إلى الرابط التالي: https://www.google.com/maps/place/870+Valencia+St+San+Francisco+CA/ فسترى العنوان مفتوحًا أمامك. وبهذا كل ما نحتاج إليه هو فتح صفحة ويب ذات العنوان التالي: https://www.google.com/maps/place/your_address_string حيث your_address_string هو العنوان الذي تريد عرضه في الخريطة. الخطوة 2: التعامل مع وسائط سطر الأوامر يجب أن تبدو الشيفرة لديك كما يلي: #! python3 # mapIt.py - تشغيل خريطة في المتصفح باستخدام عنوان # من سطر الأوامر أو الحافظة. import webbrowser, sys if len(sys.argv) > 1: # Get address from command line. address = ' '.join(sys.argv[1:]) # TODO: الحصول على العنوان من الحافظة. بعد سطر !# سنستورد الوحدة webbrowser لتشغيل المتصفح والوحدة sys لمحاولة قراءة وسائط سطر الأوامر. يخزن المتغير sys.argv اسم الملف ووسائط سطر الأوامر، وإذا احتوت هذه القائمة على أكثر من عنصر واحد (الذي هو اسم الملف) فيجب أن تكون قيمة استدعاء len(sys.argv) أكبر من 1، وهذا يعني أن هنالك وسائط في سطر الأوامر. عادةً ما يُفصَل بين وسائط سطر الأوامر بفراغات، لكن في هذه الحالة نريد أن نفسر جميع الوسائط على أنها سلسلة نصية واحدة، ولما كانت قيمة sys.argv هي قائمة تحتوي على سلاسل نصية، فيمكننا استخدام التابع join() معها، مما يعيد سلسلة نصية واحدة؛ لكن انتبه أننا لا نريد اسم الملف ضمن تلك السلسلة النصية، فعلينا استخدام sys.arv[1:] لإزالة أول عنصر في القائمة، ثم سنتخزن تلك القيمة في المتغير address. يمكنك تشغيل البرنامج بكتابة ما يلي في سطر الأوامر: mapit 870 Valencia St, San Francisco, CA 94110 وستكون قيمة المتغير sys.argv هي القائمة: ['mapIt.py', '870', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110'] وبالتالي تكون قيمة المتغير address هي السلسلة النصية '870 Valencia St, San Francisco, CA 94110'. الخطوة 3: التعامل مع محتويات الحافظة وتشغيل المتصفح تأكد أن الشيفرة الخاصة بك تشبه الشيفرة الآتية: #! python3 # mapIt.py - تشغيل خريطة في المتصفح باستخدام عنوان # من سطر الأوامر أو الحافظة. import webbrowser, sys, pyperclip if len(sys.argv) > 1: # Get address from command line. address = ' '.join(sys.argv[1:]) else: # الحصول على العنوان من الحافظة. address = pyperclip.paste() webbrowser.open('https://www.google.com/maps/place/' + address) إذا لم تكن هنالك وسائط ممررة عبر سطر الأوامر، فسيفترض البرنامج أن العنوان منسوخ إلى الحافظة، ويمكننا الحصول على محتوى الحافظة باستخدام pyperclip.paste() وتخزينها في المتغير address. آخر خطوة هي تشغيل المتصفح مع توفير رابط URL صحيح لخرائط غوغل عبر webbrowser.open(). ستوفر عليك بعض البرامج التي ستطورها ساعات من العمل، لكن بعض قد يكون بسيطًا ويوفر عليك بضع ثوانٍ في كل مرة تجري فيها مهمة تكرارية مثل فتح عنوان ما على الخريطة، الجدول التالي يقارن بين الخطوات اللازمة لعرض الخريطة: فتح الخريطة يدويًا استخدام سكربت mapIt.py تحديد العنوان نسخ العنوان فتح متصفح الويب الذهاب إلى خرائط غوغل الضغط على حقل الإدخال لصق العنوان الضغط على enter تحديد العنوان نسخ العنوان تشغيل mapIt.py مقارنة الخطوات اللازمة لعرض الخريطة أفكار لبرامج مشابهة تساعدك الوحدة webbrowser إذا كان لديك عنوان URL لصفحة معينة تريد اختصار عملية فتح المتصفح والتوجه إليها، يمكنك أن تستفيد منها لإنشاء: برنامج يفتح جميع الروابط المذكورة في مستند نصي في ألسنة جديدة. برنامج يفتح المتصفح على صفحة الطقس لمدينتك. برنامج يفتح مواقع التواصل الاجتماعي التي تزورها عادة. تنزيل الملفات من الويب باستخدام الوحدة requests تسمح لك الوحدة requests بتنزيل الملفات من الويب دون أن تفكر في مشاكل الشبكة أو الاتصال أو ضغط البيانات. لا تأتي الوحدة requests مضمنةً في بايثون، وإنما عليك تثبيتها أولًا من سطر الأوامر بتشغيل الأمر pip install --user requests. أتت الوحدة requests لتقدم حلًا بديلًا لوحدة urllib2 في بايثون لأنها معقدة زيادة عن اللزوم، وأنصحك أن تزيل الوحدة urllib2 من ذهنك تمامًا، لأنها وحدة صعبة دون داعٍ، وعليك استخدام الوحدة requests دومًا. لنجرب الآن أن الحزمة requests مثبتة صحيحًا بإدخال ما يلي في الصدفة التفاعلية: import requests>>> import requests إذا لم تظهر أي رسالة خطأ فهذا يعني أن الحزمة requests مثبتة عندك. تنزيل صفحة ويب باستخدام الدالة requests.get() الدالة requests.get() تقبل سلسلةً نصيةً فيها رابط URL لتنزيلها. إذا استعملت الدالة type() على القيمة المعادة من الدالة requests.get() فسترى أن الناتج هو كائن Response، الذي يحتوي على الرد الذي تلقاه برنامجك بعد إتمام الطلبية إلى خادم الويب. سنستكشف سويةً الكائن Response بالتفصيل لاحقًا، لكن لنكتب الآن الأسطر الآتية في الطرفية التفاعلية على حاسوب متصل بالإنترنت: >>> import requests ➊ >>> res = requests.get('https://automatetheboringstuff.com/files/rj.txt') >>> type(res) <class 'requests.models.Response'> ➋ >>> res.status_code == requests.codes.ok True >>> len(res.text) 178981 >>> print(res.text[:250]) The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare This eBook is for the use of anyone anywhere at no cost and with almost no restrictions whatsoever. You may copy it, give it away or re-use it under the terms of the Proje الرابط الذي طلبناه هو مستند نصي لمسرحية روميو وجولييت على موقع الكتاب الأصلي ➊، يمكنك أن ترى نجاح الطلبية إلى صفحة الويب بالنظر إلى السمة status_code من الكائن Response، إذا كانت القيمة الخاصة بها تساوي requests.codes.ok فهذا يعني أن كل شيء على ما يرام ➋. بالمناسبة، رمز الاستجابة الذي يدل على أن الأمور على ما يرام OK في HTTP هو 200، ومن المرجح أنك تعرف الحالة 404 التي تشير إلى رابط غير موجود. يمكنك العثور على قائمة برموز الاستجابة في HTTP ومعانيها في الصفحة قائمة رموز الاستجابة في HTTP من ويكيبيديا. إذا نجحت الطلبية، فستخزن صفحة الويب التي نزلناها كسلسلة نصية في المتغير text في كائن Response. يحتوي هذا المتغير على سلسلة نصية طويلة فيها المسرحية كاملةً. وإذا استدعيت len(res.text) فسترى أنها أطول من 178,000 محرف. استدعينا في النهاية print(res.text[:250]) لعرض أول 250 محرف. إذا فشل الطلب وظهرت رسالة خطأ مثل "Failed to establish a new connection" أو "Max retries exceeded" فتأكد من اتصالك بالإنترنت. لا نستطيع نقاش جميع أسباب عدم القدرة على الاتصال بالخوادم لتعقيد الموضوع، لكن أنصحك بالبحث في الويب عن المشكلة التي تواجهك لترى حلها. التأكد من عدم وجود مشاكل كما رأينا سويةً، يملك الكائن Response السمة status_code التي تأكدنا أنها تساوي requests.codes.ok (وهو متغير فيه القيمة الرقمية 200) للتحقق من نجاح عملية التنزيل. هنالك طريقة أخرى سهلة للتحقق من نجاح التنفيذ هو استدعاء التابع raise_for_status() على الكائن Response، التي ستؤدي إلى إطلاق استثناء إذا حدث خطأ حين تنزيل الملف، ولن تفعل شيئًا إن سارت الأمور على ما يرام. جرب ما يلي في الصدفة التفاعلية: >>> res = requests.get('https://inventwithpython.com/page_that_does_not_exist') >>> res.raise_for_status() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\site-packages\requests\models .py", line 940, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://inventwithpython .com/page_that_does_not_exist.html يضمن لنا التابع ()raise_for_status أن البرنامج سيتوقف إذا حدثت مشكلة في التنزيل، وهذا مناسب جدًا إذا أردنا إيقاف البرنامج حين حصول مشكلة في التنزيل. أما لو كنا نريد استمرار البرنامج حتى لو فشل تنزيل الملف فيمكننا حينئذٍ أن نحيط التابع raise_for_status() بالتعبيرات try و except لمعالجة هذا الاستثناء: import requests res = requests.get('https://inventwithpython.com/page_that_does_not_exist') try: res.raise_for_status() except Exception as exc: print('There was a problem: %s' % (exc)) التابع raise_for_status() في الشيفرة السابقة سيؤدي إلى طباعة ما يلي: There was a problem: 404 Client Error: Not Found for url: https:// inventwithpython.com/page_that_does_not_exist.html احرص دومًا على استدعاء التابع raise_for_status() بعد استدعاء requests.get() لتضمن أن برنامجك قد نزَّل الملف دون مشاكل قبل إكمال التنفيذ. حفظ الملفات المنزلة إلى نظام الملفات يمكنك الآن حفظ صفحة الويب إلى نظام الملفات لديك باستخدام الدالة open() والتابع write()، هنالك بعض الاختلافات البسيطة عمّا فعلناه سابقًا: علينا أن نفتح الملف في وضع الكتابة بالنظام الثنائي write binary بتمرير السلسلة النصية 'wb' كوسيط ثانٍ إلى الدالة open() حتى لو كان الملف المنزل نصيًا (مثل مسرحية روميو وجولييت في المثال أعلاه)، لأننا نريد كتابة البيانات الثنائية بدلًا من البيانات النصية للحفاظ على ترميز النص encoding. لنستعمل حلقة for مع التابع iter_content() للكائن Response لكتابة صفحة الويب إلى ملف: >>> import requests >>> res = requests.get('https://automatetheboringstuff.com/files/rj.txt') >>> res.raise_for_status() >>> playFile = open('RomeoAndJuliet.txt', 'wb') >>> for chunk in res.iter_content(100000): playFile.write(chunk) 100000 78981 >>> playFile.close() يعيد التابع iter_content() قطعًا من النص في كل تكرار لحلقة التكرار، وكل قطعة يكون لها نوع البيانات bytes وتحدد لها كم بايتًا يجب أن يكون طول كل قطعة، وأرى أن مئة ألف بايت هو حجم مناسب، لذا لنمرر 100000 إلى التابع iter_content(). أنشأنا الآن الملف RomeoAndJuliet.txt في مجلد العمل الحالي، لاحظ أن اسم الملف في موقع الويب هو rj.txt بينما اسم الملف المحفوظ لدينا مختلف. تذكر أن الوحدة requests تنزل محتويات صفحات الويب لديك، وبعد تنزيلها يكون على عاتقك التعامل معها وحفظها إن شئت أينما تشاء. للمراجعة، هذه هي الخطوات الكاملة لتنزيل وحفظ ملف: استدعاء التابع requests.get() لتنزيل ملف. استدعاء open() مع الخيار 'wb' لإنشاء ملف جديد وفتحه للكتابة في الوضع الثنائي. المرور على التابع iter_content() للكائن Response. استدعاء التابع write() في كل تكرار لكتابة المحتوى إلى الملف. إغلاق الملف close(). هذا كل ما يتعلق بالوحدة requests! قد تبدو لك حلقة for مع iter_content() معقدةً مقارنةً باستخدام open() و write() و close() التي استخدمناها لكتابة الملفات النصية؛ لكننا فعلنا ذلك لنضمن أن برنامجنا لن يستهلك ذاكرة كثيرة إذا نزلنا ملفات ضخمة. يمكنك قراءة المزيد حول ميزات الوحدة requests الأخرى من requests.readthedocs.org. لغة HTML قبل أن نستخلص المعلومات من صفحات الويب، لنتعلم بعض أساسيات HTML أولًا، ولنرى كيف نصل إلى أدوات المطور في متصفح الويب، التي ستجعل عملية استخراج البيانات من الويب أمرًا سهلًا جدًا. مصادر لتعلم HTML لغة HTML هي الصيغة التي تكتب فيها صفحات الويب، ويفترض هذا المقال أن لديك بعض الأساسيات حول HTML، لكن إن كنت تريد البدء من الصفر فأنصحك أن تراجع: توثيق HTML في موسوعة حسوب. قسم HTML في أكاديمية حسوب. تذكرة سريعة في حال لم تلمس HTML من مدة، سأخبرك بملخص بسيط عنها. ملفات HTML هي ملفات نصية لها اللاحقة html، وتكون النصوص فيها محادثة بالوسوم tags، وكل وسم يكون ضمن قوسي زاوية <>، وتخبر هذه الوسوم المتصفحات كيف يجب أن تعرض الصفحة. يمكن أن يكون هنالك نص بين وسم البداية ووسم النهاية، وهذا ما يؤلف عنصرًا element. فمثلًا، الشيفرة الآتية تعرض لك Hello, world! في المتصفح، وتكون كلمة Hello بخط عريض: <strong>Hello</strong>, world! وستبدو في المتصفح كما في الشكل الآتي: مثال Hello, world! معروضة في متصفح وسم البداية <strong> يخبر المتصفح أن النص سيكون بخط عريض، ووسم النهاية </strong> يخبر المتصفح أين هي نهاية النص العريض. هنالك وسوم متنوعة في HTML، ولبعض تلك الوسوم خاصيات تُذكَر ضمن قوسَي الزاوية <>، مثلًا الوسم <a> يعني أن النص هو رابط، وتكون قيمة هذا الرابط محددة بالخاصية href. مثال: Al's free <a href="https://inventwithpython.com">Python books</a>. ستبدو صفحة الويب كما في الشكل الآتي: رابط معروض في متصفح. تمتلك بعض العناصر الخاصية id التي تستخدم لتعريف عناصر الصفحة بشكل فريد، ويمكنك أن تطلب من برامجك البحث عن عنصر ما باستخدام معرفه id، لهذا تكون معرفة قيمة id لأحد العناصر من أهم الأمور التي سنستعمل فيها أدوات المطور أثناء كتابة برامج استخلاص البيانات من صفحات الويب. عرض مصدر صفحة HTML إذا أردت إلقاء نظرة على مصدر صفحة HTML لإحدى الصفحات التي تزورها، اضغط على الزر الأيمن للفأرة واختر View page source كما في الشكل التالي. هذا النص هو ما يحصل عليه متصفحك وهو يعرف كيف يعرض الصفحة اعتمادًا على ذاك النص. عرض مصدر صفحة ويب أنصح -وبشدة- أن تجرب عرض مصدر صفحات HTML لبعض مواقعك المفضلة، حتى لو لم تفهم تمامًا كل ما تراها أمامك، فلست بحاجة إلى احتراف HTML لتعرف كيف تكتب برامج لاستخراج البيانات؛ فليس المطلوب منك برمجة موقعك الشخصي وإنما أن يكون لديك ما يكفي لتحصل البيانات المطلوبة. فتح أدوات المطور إضافةً إلى عرض المصدر، يمكنك أن تلقي نظرة على صفحات HTML باستخدام أدوات المطور في متصفحك. يمكنك أن تضغط الزر F12 في كروم لإظهارها، أو الضغط على F12 مرة أخرى لإخفائها. يمكنك أيضًا فتحها من القائمة الجانبية ثم Developer Tools، أو الضغط على ⌘-⌥-I في ماك. أدوات المطور في متصفح كروم أما في فايرفكس، فيمكنك فتح أدوات المطور بالضغط على Ctrl+Shift+C في ويندوز ولينكس، أو ⌘-⌥-C في ماك، وأدوات المطور هنا تشبه كروم كثيرًا. بعد تفعيل أدوات المطور في متصفحك، يمكنك الضغط بالزر الأيمن للفأرة على أي عنصر واختيار Inspect Element في القائمة المنبثقة لترى شيفرة HTML المسؤولة عن ذاك الجزء من الصفحة. ستستفيد من ذلك كثيرًا عندما تبدأ تفسير صفحات HTML لاستخراج البيانات منها. استخدام أدوات المطور للعثور على عناصر HTML بعد أن نزل برنامج صفحة الويب باستخدام الوحدة requests فسيكون لدينا صفحة الويب كاملةً كسلسلة نصية واحدة، وعلينا أن نجد طريقة لمعرفة ما هي عناصر HTML التي تحتوي على المعلومات التي نريد استخراجها من الصفحة. ستساعدنا هنا أدوات المطور في ذلك. لنقل مثلًا أننا نريد كتابة برنامج لجلب بيانات الطقس من موقع weather.gov. لكن قبل أن نبدأ بكتابة الشيفرات فعلينا القيام ببعض التحريات أولًا. إذا زرنا الموقع وبحثنا عن الرمز البريدي 94105 فستحصل على صفحة تظهر لك الطقس في تلك المنطقة. ماذا إن كانت مهتمًا باستخراج بيانات الطقس لهذا العنوان البريدي؟ اضغط بالزر الأيمن على مكان معلومات الطقس في تلك الصفحة واختر Inspect Element من القائمة، وستظهر لك نافذة أدوات المطور، التي تريك شيفرات HTML المسؤولة عن ذاك الجزء من الصفحة كما يظهر في الشكل التالي. انتبه إلى أن الموقع قد يتغير دوريًا وقد تظهر لك عناصر مختلفة وعليك اتباع نفس الخطوات لتفحص العناصر الجديدة. تفحص عناصر صفحة الطقس باستخدام أدوات المطور يمكننا أن نرى ما هي شيفرة HTML المسؤولة عن عرض الطقس في الصفحة السابقة، وهي: <div class="col-sm-10 forecast-text">Sunny, with a high near 64. West wind 11 to 16 mph, with gusts as high as 21 mph.</div> هذا ما نبحث عنه تمامًا، يبدو أن معلومات الطقس موجودة داخل عنصر <div> له صنف CSS باسم forecast-text. اضغط بالزر الأيمن على العنصر في أدوات المطور واختر من القائمة Copy ▸ CSS Selector، وستُنسَخ لك سلسلة نصية مثل الآتية إلى الحافظة: 'div.row-odd:nth-child(1) > div:nth-child(2)' ويمكنك أن تستخدم تلك السلسلة النصية مع التابع ذ في BS4 أو find_element_by_css_selector() في Selenium كما هو مشروح في هذا المقال. الآن وبعد أن عرفت ما الذي تبحث عنه تحديدًا، يمكن لوحدة Beautiful Soup أن تساعدك في العثور عليه. تفسير HTML مع وحدة BS4 تستخدم الوحدة Beautiful Soup في استخراج المعلومات من صفحة HTML، واسم الوحدة في بايثون هو bs4 للإشارة إلى الإصدار الرابع منها، ويمكننا تثبيتها عبر الأمر pip install --user beautifulsoup4 (راجع الملحق أ لتعليمات حول تثبيت الوحدات). صحيح أننا استخدمنا beautifulsoup4 لتثبيت الوحدة، لكن حين استيرادها في بايثون سنستعمل import bs4. ستكون أمثلتنا عن BS4 عن تفسير ملف HTML (أي تحليلها والتعرف على أجزائها) مخزن محليًا على حاسوبنا. افتح لسانًا جديدًا في محرر Mu وأدخل ما يلي فيه واحفظه باسم example.html: <!-- This is the example.html example file. --> <html><head><title>The Website Title</title></head> <body> <p>Download my <strong>Python</strong> book from <a href="https:// inventwithpython.com">my website</a>.</p> <p class="slogan">Learn Python the easy way!</p> <p>By <span id="author">Al Sweigart</span></p> </body></html> نعم، حتى ملفات HTML البسيطة فيها مختلف العناصر والخاصيات، وستكون الأمور أعقد بكثير في المواقع الكبيرة، لكن مكتبة BS4 هنا لمساعدتنا وتسهيل الأمر علينا. إنشاء كائن BeautifulSoup من سلسلة HTML نصية يمكننا استدعاء الدالة bs4.BeautifulSoup() مع تمرير سلسلة نصية تحتوي على شيفرة HTML التي ستفسر. تعيد الدالة bs4.BeautifulSoup() كائن BeautifulSoup. جرب ما يلي في الصدفة التفاعلية على حاسوبك مع وجود إنترنت: >>> import requests, bs4 >>> res = requests.get('https://academy.hsoub.com') >>> res.raise_for_status() >>> academySoup = bs4.BeautifulSoup(res.text, 'html.parser') >>> type(academySoup) <class 'bs4.BeautifulSoup'> هذه الشيفرة تستعمل requests.get() لتنزيل صفحة الويب من موقع أكاديمية حسوب ثم تمرر قيمة السمة text للرد إلى الدالة bs4.BeautifulSoup() التي تعيد كائن BeautifulSoup نخزنه في المتغير academySoup. يمكننا أيضًا تحميل ملف HTML من القرص لدينا بتمرير كائن File إلى الدالة bs4.BeautifulSoup() مع وسيط ثانٍ يخبر وحدة BS4 ما المفسر الذي عليها استعماله لتفسير الملف. أدخل ما يلي في الصدفة التفاعلية مع التأكد أنك في نفس المجلد الذي يحتوي على ملف example.html: >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile, 'html.parser') >>> type(exampleSoup) <class 'bs4.BeautifulSoup'> المفسر 'html.parser' المستخدم هنا يأتي مع بايثون، لكن يمكنك استخدام المفسر 'lxml' الأسرع إذا ثبتت الوحدة الخارجية lxml. اتبع التعليمات الموجودة في الملحق أ لتثبيت الوحدة باستخدام الأمر pip install --user lxml. إذا لم نضمِّن الوسيط الثاني الذي يحدد المفسر فسيظهر التحذير: UserWarning: No parser was explicitly specified. بعد أن يكون لدينا كائن BeautifulSoup سنتمكن من استخدام توابعه لتحديد أجزاء معينة من مستند HTML. العثور على عنصر باستخدام التابع select() يمكنك الحصول على عنصر من عناصر صفحة الويب في الكائن BeatuifulSoup باستدعاء التابع select() وتمرير محدد CSS (أي CSS Selector) للعنصر الذي تبحث عنه. المحددات تشبه التعابير النمطية في وظيفتها: تحدد نمطًا يمكن البحث عنه في صفحات الويب. نقاش محددات CSS خارج سياق هذه السلسلة، لكن هذه مقدمة مختصرة عنها في الجدول التالي، وأنصحك بالاطلاع على توثيق المحددات في موسوعة حسوب. المحدد الذي يمرر إلى التابع ()select سيطابق soup.select('div') جميع عناصر <div> soup.select('#author') جميع العناصر التي لها الخاصية id وقيمتها author soup.select('.notice') جميع العناصر التي لها خاصية class ولها صنف CSS باسم notice soup.select('div span') جميع عناصر <span> الموجود داخل عناصر <div> soup.select('div > span') جميع العناصر <span> الموجودة مباشرةً داخل عناصر <div> بدون وجود أي عنصر بينهما soup.select('input[name]') جميع عناصر <input> التي لها الخاصية name بغض النظر عن قيمتها soup.select('input[type="button"]') جميع عناصر <input> التي لها الخاصية type وتكون مساوية إلى button أمثلة عن محددات CSS يمكن دمج مختلف أنماط المحددات مع بعضها لمطابقة أنماط أكثر تعقيدًا، فمثلًا soup.select('p #author') ستطابق أي عنصر له خاصية id قيمتها author لكن يجب أن يكون هذا العنصر موجودًا داخل عنصر <p>. بدلًا من محاولة كتابة المحددات بنفسك، يمكنك الضغط بالزر الأيمن على أي عنصر في صفحة الويب واختيار Inspect Element، وحينما تفتح أدوات المطور اضغط بالزر الأيمن على عنصر HTML واختر Copy ▸ CSS Selector لنسخ محدد CSS إلى الحافظة لتستطيع لصقه في الشيفرة لديك. التابع select() سيعيد قائمةً بعناصر Tag، وهذا ما تفعله وحدة BS4 لتمثيل عنصر HTML. هذه القائمة ستحتوي على كائن Tag لكل مطابقة للمحدد في كائن BeautifulSoup. يمكن تمرير كائن Tag إلى الدالة str() لعرض وسوم HTML التي تمثلها. تمتلك قيم Tag أيضًا السمة attrs التي تظهر جميع خاصيات HTML للعنصر ممثلةً كقاموس. لنجرب على ملف example.html بإدخال ما يلي في الصدفة التفاعلية: >>> import bs4 >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile.read(), 'html.parser') >>> elems = exampleSoup.select('#author') >>> type(elems) # elems هي قائمة من كائنات Tag <class 'list'> >>> len(elems) 1 >>> type(elems[0]) <class 'bs4.element.Tag'> >>> str(elems[0]) # تحويل الكائن إلى سلسلة نصية. '<span id="author">Al Sweigart</span>' >>> elems[0].getText() 'Al Sweigart' >>> elems[0].attrs {'id': 'author'} ستستخرج الشيفرة السابقة العنصر الذي له id="author" في مستند HTML. سنستخدم select('#author') للحصول على قائمة بجميع العناصر التي لها id="author"، وسنخزن قائمة كائنات Tag في المتغير elems، وسيخبرنا التعبير len(elems) أن لدينا كائن Tag وحيد في القائمة، أي جرت عملية المطابقة لعنصر HTML وحيد. استدعاء التابع getText() على العناصر سيعيد النص الموجود في العنصر، أي السلسلة النصية الموجودة بين وسم البدء والإغلاق. أما تمرير الكائن إلى الدالة str() سيعيد سلسلة نصيةً فيها وسمَي البداية والنهاية مع نص العنصر؛ أما السمة attrs فستعطينا قاموسًا فيه خاصيات العنصر كاملةً. يمكنك أيضًا استخراج جميع عناصر <p> من كائن BeautifulSoup كما في المثال الآتي: >>> pElems = exampleSoup.select('p') >>> str(pElems[0]) '<p>Download my <strong>Python</strong> book from <a href="https:// inventwithpython.com">my website</a>.</p>' >>> pElems[0].getText() 'Download my Python book from my website.' >>> str(pElems[1]) '<p class="slogan">Learn Python the easy way!</p>' >>> pElems[1].getText() 'Learn Python the easy way!' >>> str(pElems[2]) '<p>By <span id="author">Al Sweigart</span></p>' >>> pElems[2].getText() 'By Al Sweigart' ستعطينا select() ثلاث مطابقات، والتي ستخزن في المتغير pElems. سنجرب استخدام str() على الكائنات pElems[0] و pElems[1] و pElems[2] لعرض العناصر كسلاسل نصية، وسنجرب أيضًا التابع getText() لإظهار نص تلك العناصر فقط. الحصول على معلومات من خاصيات العنصر يسهل علينا التابع get() الخاص بكائنات Tag الوصول إلى خاصيات العناصر، ونمرر لهذا التابع سلسلةً نصيةً باسم الخاصية attribute وسيعيد لنا قيمتها. لنجرب المثال الآتي على الملف example.html: >>> import bs4 >>> soup = bs4.BeautifulSoup(open('example.html'), 'html.parser') >>> spanElem = soup.select('span')[0] >>> str(spanElem) '<span id="author">Al Sweigart</span>' >>> spanElem.get('id') 'author' >>> spanElem.get('some_nonexistent_addr') == None True >>> spanElem.attrs {'id': 'author'} يمكننا استخدام select() للعثور على أي عناصر <span> ثم تخزين أول عنصر مطابق في المتغير spanElem، الذي سنمرر للتابع get() القيمة 'id' للحصول على قيمة المعرف الخاصة به، وهي 'author' في مثالنا. مشروع: فتح جميع نتائج البحث في كل مرة أبحث فيها في جوجل، لا أريد أن أرى نتيجة بحث واحدة فقط ثم أنتقل إلى النتيجة التي بعدها، بل أضغط بسرعة على الزر الأوسط للفأرة على كل الروابط (أو الضغط عليها مع زر Ctrl) لفتحها في لسان جديد وأقرؤها معًا لاحقًا. فعلت هذا الأمر كثيرًا في جوجل لدرجة أنني مللت من البحث ثم الضغط على كل الروابط واحدًا واحدًا. ماذا لو كتبت برنامجًا أستطيع عبره كتابة عبارة البحث وسيفتح لي متصفحًا فيه كل نتائج البحث الأولى مفتوحةً كل واحد منها في لسان في المتصفح؟ لنكتب سكربتًا يفعل ذلك مع صفحة نتائج بحث فهرس حزم بايثون. يمكن تعديل هذا البرنامج ليعمل على الكثير من المواقع الأخرى، لكن انتبه إلى أن محركات البحث غوغل و DockDockGo عادةً ما تصعب عملية استخراج نتائج البحث من مواقعها. هذا ما يفعله البرنامج: الحصول على عبارة البحث من سطر الأوامر الحصول على صفحة نتائج البحث فتح لسان متصفح جديد لكل نتيجة هذا يعني أن على برنامجك أن يفعل ما يلي: قراءة وسائط سطر الأوامر من sys.argv. الحصول على صفحة نتائج البحث عبر الوحدة requests. العثور على جميع روابط نتائج البحث. استدعاء الدالة webbrowser.open() لفتحها في المتصفح. لنبدأ البرمجة بفتح ملف جديد باسم searchpypi.py في محررنا. الخطوة 1: الحصول على وسائط سطر الأوامر وطلب صفحة نتائج البحث قبل أن نبدأ بالبرمجة، علينا أن نعرف ما هو رابط URL لصفحة نتائج البحث. انظر إلى شريط العنوان في المتصفح بعد إتمامك لعملية البحث، وسترى أن الرابط يشبه https://pypi.org/search/?q=SEARCH_TERM_HERE. يمكننا استخدام الوحدة requests لتنزيل هذه الصفحة، ثم استخدام BS4 للبحث عن الروابط في مستند HTML. يمكننا في النهاية أن نستعمل وحدة webbrowser لفتح تلك الروابط في ألسنة جديدة. يحب أن تكون الشيفرة كما يلي: #! python3 # searchpypi.py - فتح عدة نتائج بحث. import requests, sys, webbrowser, bs4 print('Searching...') # عرض النص ريثما تحمل الصفحة res = requests.get('https://pypi.org/search/?q=' + ' '.join(sys.argv[1:])) res.raise_for_status() # TODO: Retrieve top search result links. # TODO: فتح لسان لكل نتيجة. سيحدد المستخدم كلمات البحث عبر تمريرها إلى سطر الأوامر أثناء تشغيل البرنامج، وستخزن في sys.argv كما رأينا في المقالات السابقة لهذه السلسلة. الخطوة 2: العثور على كل النتائج ستحتاج الآن إلى وحدة BS4 لاستخراج نتائج البحث من مستند HTML المنزل. لكن كيف يمكننا معرفة المحدد المناسب لحالتنا؟ ليس من المنطقي مثلًا البحث عن جميع عناصر <a> لوجود روابط كثيرة لا تهمنا؛ والحل هنا أن نفتح أدوات المطور في المتصفح وننظر إلى المحدد المناسب الذي سينتقي لنا الروابط التي نريدها فقط. بعد أن نبحث فسنجد بعض العناصر التي تشبه التالي: <a class="package-snippet" href="/project/pyautogui/"> ولا يهمنا إن كانت تلك العناصر معقدة، وإنما نحتاج إلى النمط الذي علينا استخدامه للبحث عن الروابط. لنعدل شيفرتنا إلى: #! python3 # searchpypi.py - Opens several google results. import requests, sys, webbrowser, bs4 --snip-- # Retrieve top search result links. soup = bs4.BeautifulSoup(res.text, 'html.parser') # فتح لسان لكل نتيجة. linkElems = soup.select('.package-snippet') إذا ألقيت نظرةً إلى عناصر <a> فستجد أن جميع نتائج البحث لها الخاصية class="package-snippet" وإذا بحثنا في كامل مصدر الصفحة فسنتأكد أن الصنف package-snippet ليس مستخدمًا إلا لروابط نتائج البحث. لا يهمنا ما هو الصنف package-snippet ولا ما يفعل، وإنما يهمنا كيف سنستفيد منه لتحديد عناصر <a> التي نبحث عنها. لننشِئ كائن BeautifulSoup من صفحة HTML التي نزلناها ونستخدم المحدد '.package-snippet' لتحديد جميع عناصر <a> التي لها صنف CSS المحدد؛ ضع في ذهنك أن موقع PyPI قد يحدث واجهته الرسومية وقد تحتاج إلى استخدام محدد CSS مختلف مستقبلًا، إن حدث ذلك فمرر المحدد الجديد إلى التابع soup.select() وسيعمل البرنامج على ما يرام. الخطوة 3: فتح نتائج البحث في المتصفح لنخبر برنامجنا الآن أن يفتح لنا النتائج الأولى في متصفح الويب. أضف ما يلي إلى برنامجك: #! python3 # searchpypi.py - فتح عدة نتائج بحث. import requests, sys, webbrowser, bs4 --snip-- # فتح لسان لكل نتيجة. linkElems = soup.select('.package-snippet') numOpen = min(5, len(linkElems)) for i in range(numOpen): urlToOpen = 'https://pypi.org' + linkElems[i].get('href') print('Opening', urlToOpen) webbrowser.open(urlToOpen) سيفتح البرنامج افتراضيًا أول خمسة روابط في المتصفح باستخدام الوحدة webbrowser، لكن قد يكون ناتج بحث المستخدم أقل من خمس نتائج، فحينها ننظر أيهما أقل، 5 أم عدد عناصر القائمة المعادة من استدعاء التابع soup.select(). الدالة المضمنة في بايثون min() تعيد العدد الأصغر من الوسائط الممررة إليها (والدالة max() تفعل العكس). يمكنك أن تستخدم الدالة min() لمعرفة إذا كان عدد الروابط أقل من 5 وتخزين الناتج في المتغير numOpen، والذي ستستفيد منه في حلقة for عبر range(numOpen). سنستخدم الدالة webbrowser.open() في كل تكرار لحلقة for لفتح الرابط في المتصفح. لاحظ أن قيمة الخاصية href في عناصر <a> لا تحتوي على https://pypi.org لذا علينا إضافتها بأنفسنا إلى قيمة الخاصية href قبل فتح الرابط. يمكنك الآن فتح أول 5 نتائج بحث عن «boring stuff» في محرك بحث PyPI بكتابة الأمر searchpypi boring stuff في سطر الأوامر. أفكار لبرامج مشابهة من الجميل أن يكون لدينا برنامج يفتح لنا عدة ألسنة في المتصفح لأي عملية روتينية مكررة، مثل: فتح جميع صفحات المنتجات في متجر أمازون بعد البحث عن منتج معين. فتح كل روابط المراجعات لمنتج ما. فتح الصور الناتجة بعد إجراء عملية بحث سريعة على أحد مواقع الصور مثل Flickr. مشروع: تنزيل كل رسمات XKCD عادةً ما تحدث المواقع والمدونات الصفحة الرئيسية لها بعرض آخر منشور فيها، مع وجود زر «السابق» الذي يأخذك إلى المنشور السابق، ثم تجد في المنشور السابق زرًا يأخذك للمنشور الذي قبله، وهلم جرًا، مما ينشِئ سلسلةً من الصفحات التي تأخذك من الصفحة الرئيسية إلى صفحة أول منشور في الموقع. إذا أردت نسخةً من محتوى موقعٍ ما لتقرأها وأنت غير متصل بالإنترنت، فيمكنك أن تفتح بنفسك كل صفحة وتحفظها، لكن هذا الأمر ممل جدًا ومن المناسب كتابة برنامج لأتمتة هذه المهمة. موقع XKCD هو موقع كوميكس له نفس البنية المذكورة. وصفحته الرئيسية فيها زر Prev للعودة إلى الكوكميس السابقة، لكن عملية تنزيل كل الكوميكس واحدةً واحدةً تأخذ وقتًا كثيرًا، لكننا نستطيع كتابة سكربت لأتمتة ذلك في ثوانٍ معدودة. هذا ما سيفعله برنامجنا: فتح صفحة XKCD الرئيسية حفظ صورة الكوميكس في تلك الصفحة الانتقال إلى صفحة الكوميكس السابق تكرار العملية حتى الوصول إلى أول صورة كوميكس. هذا يعني أن الشيفرة البرمجية ستفعل ما يلي: تنزيل الصفحات باستخدام الوحدة requests. العثور على رابط URL لصورة الكوميكس باستخدام وحدة BS4. تنزيل وحفظ صورة الكوميكس إلى نظام الملفات باستخدام iter_content(). العثور على رابط صورة الكوميكس السابقة، وتكرار العملية كلها. افتح ملفًا جديدًا في محررك وسمِّه باسم downloadXkcd.py. الخطوة 1: تصميم البرنامج إذا فتحت أدوات المطور في المتصفح ونظرت إلى عناصر الصفحة، فسترى ما يلي: رابط URL لملف صورة الكوميكس في الخاصية href في العنصر <img>. العنصر <img> موجود داخل العنصر <div id="comic">. الزر Perv له الخاصية rel وقيمتها prev. صفحة أول صورة كوميكس يشير فيها الزر Prev إلى الرابط https://xkcd.com/# مما يشير إلى عدم وجود صفحات سابقة. عدّل الشيفرة لتبدو كما يلي: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # TODO: Download the page. # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') سيكون لدينا متغير اسمه url يبدأ بالقيمة 'https://xkcd.com' وتحدث قيمته دوريًا ضمن حلقة for لتصبح الرابط الموجود في الزر Prev للصفحة الحالية. وسننزل صورة الكوميكس في كل تكرار من تكرارات حلقة for الموجودة في الرابط url، وسننتهي من حلقة التكرار حينما تنتهي قيمة url بالعلامة '#'. سننزل ملفات الصور إلى مجلد موجود في مجلد العمل الحالي باسم xkcd، وسنتأكد من أن المجلد موجود باستدعاء os.makedirs() مع تمرير وسيط الكلمات المفتاحية exist_ok=True الذي يمنع الدالة من رمي استثناء إن كان المجلد موجودًا مسبقًا. بقية الشيفرة هو تعليقات سنملؤها في الأقسام القادمة. الخطوة 2: تنزيل صفحة الويب لنكتب الشيفرة الآتية التي ستنزل الصفحة: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # Download the page. print('Downloading page %s...' % url) res = requests.get(url) res.raise_for_status() soup = bs4.BeautifulSoup(res.text, 'html.parser') # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') لنطبع بدايةً قيمة url ليعرف المستخدم ما هو رابط URL الذي يحاول البرنامج تنزيله، ثم سنستخدم الدالة requests.get() في الوحدة requests لتنزيل الصفحة. ثم سنستدعي التابع raise_for_status() كالعادة لرمي استثناء إن حدثت مشكلة ما في التنزيل؛ ثم إن سارت الأمور على ما يرام فسننشِئ كائن BeautifulSoup من نص الصفحة المنزلة. الخطوة 3: البحث عن صورة الكوميكس وتنزيلها لنعدل شيفرة برنامجنا: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 --snip-- # Find the URL of the comic image. comicElem = soup.select('#comic img') if comicElem == []: print('Could not find comic image.') else: comicUrl = 'https:' + comicElem[0].get('src') # Download the image. print('Downloading image %s...' % (comicUrl)) res = requests.get(comicUrl) res.raise_for_status() # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') بعد تفحص صفحة XKCD عبر أدوات المطور، سنعرف أن عنصر <img> لصورة الكوميكس موجود داخل عنصر <div> له الخاصية id التي قيمتها هي comic، وبالتالي سيكون المحدد '#comic img' صحيحًا لتحديد العنصر <img> عبر كائن BeautifulSoup. تمتلك بعض صفحات موقع XKCD على محتوى خاص لا يمثل صورة بسيطة، لكن لا بأس فيمكننا تخطي تلك الصفحات، فإن لم يطابِق المحدد الخاص بنا أي عنصر فسيعيد استدعاء soup.select('#comic img') قائمة فارغة، وحينها سيظهر برنامجنا رسالة خطأ ويتجاوز الصفحة دون تنزيل الصورة. عدا ذلك، فسيعيد المحدد السابق قائمةً فيها عنصر <img> وحيد، ويمكننا الحصول على قيمة الخاصية src لعنصر <img>ونمررها إلى requests.get() لتنزيل صورة الكوميكس. الخطوة 4: حفظ الصورة والعثور على الصفحة السابقة لنعدل الشيفرة لتصبح كما يلي: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 --snip-- # حفظ الصورة إلى ./xkcd. imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb') for chunk in res.iter_content(100000): imageFile.write(chunk) imageFile.close() # الحصول على رابط الصفحة السابقة. prevLink = soup.select('a[rel="prev"]')[0] url = 'https://xkcd.com' + prevLink.get('href') print('Done.') أصبح لدينا ملف صورة الكوميكس مخزنًا في المتغير res، وعلينا كتابة بيانات هذه الصورة إلى ملف في نظام الملفات المحلي لدينا. سنحدد اسم الملف المحلي للصورة ونمرره إلى الدالة open()، لاحظ أن المتغير comicUrl يحتوي على قيمة تشبه القيمة الآتية: 'https://imgs.xkcd.com/comics/heartbleed_explanation.png' ويبدو أنك لاحظت وجود مسار الملف فيها. يمكنك استخدام الدالة os.path.basename() مع comicUrl لإعادة آخر جزء من رابط URL السابق، أي 'heartbleed_explanation.png'، ويمكنك حينها استخدام هذا الاسم حين حفظ الصورة إلى نظام الملفات المحلي. يمكنك أن تضيف هذا الاسم إلى اسم مجلد xkcd باستخدام الدالة os.path.join() كما تعلمنا سابقًا لكي يستخدم برنامجك الفاصل الصحيح (الخط المائل الخلفي \ في ويندوز، والخط المائل / في لينكس وماك). أصبح لدينا الآن اسم ملف صحيح، ويمكننا استدعاء الدالة open() لفتح ملف جديد بوضع الكتابة الثنائية 'wb'. إذا كنت تذكر حينما حفظنا الملفات التي نزلناها عبر الوحدة requests في بداية المقال كنا نمر بحلقة تكرار على القيمة المعادة من التابع iter_content()، وستكتب الشيفرة الموجودة في حلقة for قطعًا من بيانات الصورة (كحد أقصى 100,000 بايت كل مرة) إلى الملف، ثم تغلق الملف. أصبحت الصورة محفوظة لديك محليًا الآن! علينا بعدئذٍ استخدام المحدد 'a[rel="prev"]' لتحديد العنصر <a> الذي له العلاقة rel مضبوطةً إلى prev. يمكنك استخدام قيمة الخاصية href لعنصر <a> المحدد للحصول على رابط صورة الكوميكس السابقة، والتي ستخزن في المتغير url، ثم ستدور حلقة while مرة أخرى وتعيد تنفيذ عملية التنزيل من جديد على الصفحة الجديدة. سيبدو ناتج تنفيذ البرنامج كما يلي: Downloading page https://xkcd.com... Downloading image https://imgs.xkcd.com/comics/phone_alarm.png... Downloading page https://xkcd.com/1358/... Downloading image https://imgs.xkcd.com/comics/nro.png... Downloading page https://xkcd.com/1357/... Downloading image https://imgs.xkcd.com/comics/free_speech.png... Downloading page https://xkcd.com/1356/... Downloading image https://imgs.xkcd.com/comics/orbital_mechanics.png... Downloading page https://xkcd.com/1355/... Downloading image https://imgs.xkcd.com/comics/airplane_message.png... Downloading page https://xkcd.com/1354/... Downloading image https://imgs.xkcd.com/comics/heartbleed_explanation.png... --snip– هذا المشروع هو مثال ممتاز لبرامج تتبع الروابط تلقائًا لاستخراج كمية معلومات كبيرة من الويب. يمكنك تعلم المزيد من المعلومات حول ميزات Beautiful Soup الأخرى من توثيقها الرسمي. أفكار لبرامج مشابهة تنزيل صفحات الويب وتتبع الروابط فيها هو أساس أي برنامج زحف crawler. يمكنك أن تبرمج برامج تفعل ما يلي: تنسخ موقعًا كاملًا احتياطيًا. تنسخ جميع التعليقات من أحد المنتديات. تعرض قائمة بجميع المنتجات التي عليها تخفيضات في أحد المتاجر. الوحدتان requests و bs4 رائعتين جدًا، لكن عليك أن تعرف ما هو رابط URL المناسب لتمريره إلى requests.get()، لكن في بعض الأحيان قد لا يكون ذلك سهلًا؛ أو ربما عليك تسجيل الدخول إلى أحد المواقع قبل بدء عملية الاستخراج، لهذا السبب سنستكشف سويةً الوحدة selenium لأداء مهام معقدة. التحكم في المتصفح عبر الوحدة selenium تسمح الوحدة selenium لبايثون بالتحكم برمجيًا بالمتصفح بضغط الروابط وتعبئة الاستمارات، مثلها كمثل أي تفاعل من مستخدم بشري. يمكننا أن نتفاعل مع صفحات الويب بطرائق أكثر تقدمًا في الوحدة selenium مقارنة مع requests و bs4، لكنها قد تكون أبطأ وأصعب بالتشغيل في الخلفية لأننا نفتح متصفح ويب كامل مقارنة بتنزيل بعض الصفحات من الويب. لكن إن كان علينا التعامل مع صفحات الويب بطريقة تعتمد على شيفرات JavaScript التي تحدث الصفحة، فعلينا استخدام selenium بدلًا من requests. أضف إلى ذلك أن المواقع الشهيرة مثل أمازون تستخدم برمجيات للتعرف على الزيارات الآتية من سكربتات التي تحاول استخراج البيانات من صفحاتها أو إنشاء عدّة حسابات مجانية، ومن المرجح أن تحجب هذه المواقع برامجك بعد فترة؛ وهنا تأتي الوحدة selenium التي تعمل مثل متصفحات الويب العادية. أحد أشهر العلامات التي تتعرف فيها المواقع أن الزيارات آتية من برنامج (أو سكربت script) هي عبارة user-agent، التي تُعرِّف متصفح الويب المستخدم وتضمَّن في كل طلبيات HTTP. فمثلًا تكون عبارة user-agent للوحدة requests شيء يشبه 'python-requests/2.21.0'. يمكنك زيارة موقع مثل whatsmyua.info لتعرف ما هي user-agent الخاصة بك. أما الوحدة selenium فمن المرجح أن تظن المواقع أنها مستخدم بشري، لأنها تستخدم user-agent شبيه بالمتصفحات العادية مثل: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 ولأن نمط التصفح فيها يشبه المتصفحات الأخرى، فستُنزَّل الصور والإعلانات وملفات تعريف الارتباط والمتتبعات كما في المتصفحات العادية. لكن يمكن للمواقع كشف selenium وتحاول شركات شراء بطاقات الدخول والمتاجر الإلكترونية حجب أي سكربتات تستعمل متصفح selenium. تشغيل متصفح تتحكم فيه selenium سنرى في الأمثلة القادمة كيفية التحكم في متصفح فيرفكس من selenium، إذا لم يكن مثبتًا لديك فيمكنك تنزيله من getfirefox.com، ويمكنك تثبيت selenium،من سطر الأوامر بتشغيل pip install --user selenium، وأذكرك بالذهاب إلى الملحق أ لمزيد من المعلومات. قد يكون استيراد مكونات selenium مختلفًا عليك، فبدلًا من import selenium سنستعمل from selenium import webdriver (سبب فعل selenium لذلك خارج نطاق هذه السلسلة لا تقلق حوله). يمكنك بعد ذلك تشغيل فايرفوكس مع selenium بكتابة ما يلي في الصدفة التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> type(browser) <class 'selenium.webdriver.firefox.webdriver.WebDriver'> >>> browser.get('https://inventwithpython.com') ستلاحظ أن متصفح فايرفوكس قد بدأ بالعمل حين استدعاء webdriver.Firefox(). استدعاء type() على webdriver.Firefox() سيظهر أنها من نوع البيانات WebDrive، واستدعاء التالي: browser.get('https://inventwithpython.com') سيوجه المتصفح إلى https://inventwithpython.com/. بعد استدعاء webdriver.Firefox() و get() ستفتح الصفحة في متصفح فايرفوكس إذا واجهت مشكلة من قبيل 'geckodriver' executable needs to be in PATH فهذا يعني أن عليك تنزيل محرك فايرفوكس يدويًا قبل استخدام selenium للتحكم به. يمكنك التحكم بمتصفحات أخرى غير فيرفكس إذا ثبتت محرك الويب لهم webdriver. لمتصفح فايرفوكس، اذهب إلى github.com وثبت محرك geckodriver لنظام تشغيلك. (كلمة «Gecko» هي اسم محرك المتصفح engine في فيرفكس). سيحتوي الملف المضغوط المنزل على geckodriver.exe في ويندوز أو geckodriver في ماك ولينكس، وعليك وضعه في مسار PATH الخاص في نظامك، راجع الإجابة الآتية stackoverflow.com. أما لمتصفح كروم فاذهب إلى chromium.org ونزل الملف المضغوط المناسب لنظامك، وضع الملف chromedriver.exe أو chromedriver في مسار PATH كما ذكرنا أعلاه. يمكن فعل نفس الأمر لبقية المتصفحات الرئيسية، ابحث سريعًا في الويب عن اسم المتصفح متبوعًا بالكلمة webdriver وستجدها. قد تواجه مشاكل في تشغيل المتصفحات الجديدة في selenium، بسبب عدم التوافقية بينهما، لكن يمكنك إجراء حل التفافي بتثبيت نسخة قديمة من المتصفح أو من وحدة selenium. يمكنك معرفة تاريخ الإصدارات من pypi.org. للأسف قد تحدث مشاكل توافقية بين selenium وبعض إصدارات المتصفحات، وأنصحك حينها بالبحث في الويب عن الحلول المقترحة، وراجع الملحق أ لمعلومات حول تثبيت إصدار محدد من selenium (مثل تثبيت pip install --user -U selenium==3.14.1). العثور على عناصر في الصفحة توفر كائنات WebDriver عددًا من التوابع للعثور على عناصر صفحة، ويمكن تقسيمها إلى قسمين من التوابع find_element_ و find_elements_*. توابع find_element_* تعيد كائن WebElement وحيد يمثل أول عنصر في الصفحة يطابق الطلبية التي حددتها، بينما تعيد توابع _find_elements قائمة من كائنات WebElement_* لكل عنصر مطابق في الصفحة. يظهر الجدول الموالي أمثلة على التوابع find_element_ و find_elements_ المستدعاة على كائن WebDriver مخزن في المتغير browser. اسم التابع كائن/قائمة كائنات WebElement browser.find_element_by_class_name(name) browser.find_elements_by_class_name(name) العناصر التي تستخدم صنف CSS باسم name browser.find_element_by_css_selector(selector) browser.find_elements_by_css_selector(selector) العناصر التي تطابق محدد CSS معين browser.find_element_by_id(id) browser.find_elements_by_id(id) العناصر التي تطابق id معين browser.find_element_by_link_text(text) browser.find_elements_by_link_text(text) عناصر <a> التي يطابق محتواها القيمة text كاملةً browser.find_element_by_partial_link_text(text) browser.find_elements_by_partial_link_text(text) عناصر <a> التي يطابق محتواها القيمة text جزئيًا browser.find_element_by_name(name) browser.find_elements_by_name(name) العناصر التي لها الخاصية name وتطابق قيمة معينة browser.find_element_by_tag_name(name) browser.find_elements_by_tag_name(name) العناصر التي تطابق وسمًا معينًا، وهي غير حساسة لحالة الأحرف (أي <a> يمكن أن يطابق عبر 'a' أو 'A') توابع WebDriver للعثور على العناصر في selenium باستثناء التوابع *_by_tag_name() تكون جميع الوسائط الممررة إلى الدوال حساسةً لحالة الأحرف، وإذا لم تكن العناصر موجودة في الصفحة فستطلق selenium الاستثناء NoSuchElement، وعليك استخدام عبارات try و except إذا لم ترغب بتوقف برنامج عن العمل عند ذلك. بعد أن تحصل على كائن WebElement فيمكنك أن تتعرف على المزيد حوله بقراءة سماته أو استدعاء توابع المذكورة في الجدول التالي: السمة أو التابع الوصف tag_name اسم الوسم، مثل 'a' لعنصر <a> get_attribute(name) قيمة خاصية name للعنصر text النص الموجود داخل العنصر، مثل 'hello' في <span>hello</span> clear() مسح النص المكتوب في الحقول النصية is_displayed() إعادة True إذا كان العنصر ظاهرًا، وإلا فيعيد False is_enabled() إعادة True إذا كان حقل الإدخال مفعلًا، وإلا فيعيد False is_selected() لأزرار الانتقاء radio ومربعات التأشير checkbox ستعيد True إذا كان العنصر مختار، وإلا فتعيد False location قاموس فيه المفاتيح 'x' و 'y' لمكان العنصر في الصفحة سمات وتوابع الكائن WebElement مثلًا، افتح ملفًا جديدًا واكتب البرنامج الآتي: from selenium import webdriver browser = webdriver.Firefox() browser.get('https://inventwithpython.com') try: elem = browser.find_element_by_class_name(' cover-thumb') print('Found <%s> element with that class name!' % (elem.tag_name)) except: print('Was not able to find an element with that name.') فتحنا هنا متصفح فيرفكس ووجهناه إلى رابط URL معين، وحاولنا العثور على العناصر التي لها الصنف bookcover، وإذا عثرنا على أحد العناصر فسنطبع اسم الوسم عبر السمة tag_name؛ أما لو لم يعثر على أي عنصر فسنطبع رسالة مختلفة. سيعيد البرنامج الناتج الآتي: Found <img> element with that class name! أي عثرنا على عنصر <img> له اسم الصنف 'bookcover'. الضغط على عناصر الصفحة تمتلك كائنات WebElement المعادة من التوابع find_element_* و find_elements_* التابع click() الذي يحاكي ضغطات الفأرة على العنصر، ويمكن استخدام هذا التابع للضغط على رابط أو تحديد زر انتقاء أو الضغط على زر الإرسال أو أي حدث آخر يُطلَق عند الضغط على أحد العناصر. جرب المثال الآتي في الطرفية التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> browser.get('https://inventwithpython.com') >>> linkElem = browser.find_element_by_link_text('Read Online for Free') >>> type(linkElem) <class 'selenium.webdriver.remote.webelement.FirefoxWebElement'> >>> linkElem.click() # follows the "Read Online for Free" link سيفتح متصفح فيرفكس الصفحة ويحصل على العنصر <a> الذي يحتوي على النص Read it Online ويحاكي الضغط على الرابط كما لو ضغطته بنفسك. تعبئة وإرسال الاستمارات يمكن تعبئة الحقول النصية مثل <input> أو <textarea> بالبحث عنها ثم استدعاء التابع send_keys(). جرب ما يلي في الطرفية التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> browser.get('https://login.metafilter.com') >>> userElem = browser.find_element_by_id('user_name) >>> userElem.send_keys('your_real_username_here') >>> passwordElem = browser.find_element_by_id('user_pass') >>> passwordElem.send_keys('your_real_password_here') >>> passwordElem.submit() لطالما لم تغيّر صفحة MetaFilter المعرف id لحقول اسم المستخدم وكلمة المرور بعد نشر هذا الكتاب، فيمكنك استخدام الشيفرة السابقة لملء تلك الحقول النصية (تذكر أنك تستطيع استخدام أدوات المطور للتأكد من المعرف id للحقول في أي وقت). استدعاء التابع submit() على أي عنصر في الاستمارة له نفس تأثير الضغط على زر الإرسال. تحذير: ابتعد عن تخزين كلمات المرور الخاصة بك في الشيفرة المصدرية لبرامجك قدر الإمكان، فمن السهل تسريب كلمات المرور إذا تركناها دون تشفير. يمكن لبرنامجك أن يطلب كلمة المرور من المستخدم أثناء التشغيل كما ناقشنا في مقال سابق. إرسال المفاتيح الخاصة تمتلك selenuim وحدةً للمفاتيح الخاصة التي لا يمكن كتابتها كسلسلة نصية، مثل زر Tab أو الأسهم. تلك القيم مخزنة كسمات في الوحدة selenium.webdriver.common.keys، ولأن اسم الوحدة طويل جدًا فمن الأسهل كتابة from selenium.webdriver.common.keys import Keys في بداية البرنامج، وحينها تستطيع استخدام Keys في أي مكان تريد أن تكتب فيه selenium.webdriver.common.keys. يعرض الجدول 12-5 أشهر القيم المستخدمة. الجدول 12-5: أشهر القيم المستخدمة في الوحدة selenium.webdriver.common.keys. السمة الشرح Keys.DOWN و Keys.UP و Keys.LEFT و Keys.RIGHT الأسهم في لوحة المفاتيح Keys.ENTER و Keys.RETURN زر Enter Keys.HOME و Keys.END و Keys.PAGE_DOWN و Keys.PAGE_UP أزرار Home و End و PageDown و PageUp على التوالي Keys.ESCAPE و Keys.BACK_SPACE و Keys.DELETE أزرار Escape و Backspace و Delete على التوالي Keys.F1 و Keys.F2 و . . . و Keys.F12 الأزرار F1 حتى F12 في الصف العلوي من لوحة المفاتيح Keys.TAB زر Tab فمثلًا لو لم يكن المؤشر موجودًا داخل حقل نصي، فالضغط على زر Home و End سيؤدي إلى التمرير إلى بداية ونهاية الصفحة على التوالي. جرب ما يلي على الصدفة التفاعلية ولاحظ كيف يؤدي استدعاء send_keys() إلى تمرير الصفحة: >>> from selenium import webdriver >>> from selenium.webdriver.common.keys import Keys >>> browser = webdriver.Firefox() >>> browser.get('https://nostarch.com') >>> htmlElem = browser.find_element_by_tag_name('html') >>> htmlElem.send_keys(Keys.END) # scrolls to bottom >>> htmlElem.send_keys(Keys.HOME) # scrolls to top العنصر <html> موجود في كل ملفات HTML، ويكون كامل محتوى مستند HTML داخله؛ وتحديد هذا العنصر مفيد لو أردنا إرسال المفاتيح إلى صفحة الويب عمومًا، وهذا بدوره مفيد إذا كانت الصفحة تحمِّل محتوى جديد عند التمرير إلى أسفل الصفحة. الضغط على أزرار المتصفح يمكن أن تحاكي الوحدة selenium الضغط على أزرار المتصفح المختلفة: التابع browser.back() يضغط على زر الرجوع التابع browser.forward() يضغط على زر إلى الأمام التابع browser.refresh() يضغط على زر التحديث التابع browser.quit() يضغط على زر إغلاق النافذة المزيد من المعلومات حول Selenium يمكن للوحدة selenium فعل الكثير مما لم نذكره هنا، فيمكنك أن تعدل محتوى ملفات تعريف الارتباط، وتأخذ لقطة شاشة، وتشغل سكربت JavaScript مخصص …إلخ. لمزيد من المعلومات حول تلك الخصائص فأنصحك أن تزور التوثيق الرسمي. الخلاصة المهام المملة ليست متعلقة بالملفات المحلية على حاسوبك. إذا كنت قادرًا على تنزيل صفحات الويب برمجيًا فستستطيع أتمتة أي مهمة تردك! تسهل الوحدة requests تنزيل صفحات الويب، وبعد تعلم أساسيات HTML فيمكنك أن تستخدم الوحدة BeautifulSoup لتفسير الصفحات التي تنزلها. لكن إن كانت تريد أتمتة أي مهمة متعلقة بالويب أيًا كانت، فيمكنك التحكم برمجيًا مباشرةً بمتصفح الويب عبر الوحدة selenium، التي تسمح لك بفتح المواقع وملء الاستمارات كما لو كان برنامجك كائنًا بشريًا يتصفح المواقع. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. إرسال البريد الإلكتروني من سطر الأوامر اكتب برنامج يقبل عنوان بريد إلكتروني وسلسلة نصية في سطر الأوامر، ثم يستخدم selenium للدخول إلى حسابك البريدي وإرسال بريد إلكتروني إلى العنوان المحدد (أنصحك بإنشاء حساب تجريبي مختلف عن حسابك الأساسي). اكتب برامج أخرى لإضافة منشورات إلى فيسبوك أو تويتر (إكس، سمه ما شئت!). منزِّل الصور اكتب برنامج يفتح أحد مواقع مشاركة الصور مثل Flickr ويبحث عن تصنيف معين من الصور وينزل جميع نتائج البحث في الصفحة الأولى. يمكنك أن تكتب برنامج يعمل على أي موقع مشاركة صور ولديه خاصية البحث. لعبة 2048 لعبة 2048 هي لعبة بسيطة تسمح لك بجمع مربعات بجعلها تنزلق إلى أحد الاتجاهات باستخدام أزرار الأسهم. يمكنك أن تحصل على نتيجة عالية إذا جعلت الانزلاق بهذا الترتيب مرارًا وتكرارًا: الأعلى ثم اليمين ثم الأسفل ثم اليسار. اكتب برنامج بسيط يفتح اللعبة gabrielecirulli.github.io وينفذ النمط السابق للعب اللعبة تلقائيًا. التحقق من الروابط اكتب برنامج يأخذ رابط URL من صفحة ويب، ويحاول أن ينزل كل صفحة مرتبطة فيها، ويُعلِّم كل الصفحات التي تعيد 404 وتؤشر عليها أنها روابط مكسورة. ترجمة -بتصرف- للمقال Web Scraping من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون برمجة عملاء ويب باستخدام بايثون أهم 10 مكتبات بايثون تستخدم في المشاريع الصغيرة مشاريع بايثون عملية تناسب المبتدئين النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
تعرّفتَ في المقالات السابقة على معلومات كافية لكتابة برامج أكثر تعقيدًا، ولكنك ستعثر على أخطاء كثيرة فيها، لذا سنوضّح في هذا المقال بعض الأدوات والتقنيات لإيجاد السبب الجذري للأخطاء في برنامجك لمساعدتك في إصلاح الأخطاء بسرعةٍ أكبر وبجهد أقل. يمكن القول أن كتابة الشيفرة البرمجية تمثّل 90% من عملية البرمجة، ولكن يمثّل تنقيح أخطاء هذه الشيفرة البرمجية نسبة 90% أخرى من العمل، حيث سينفّذ حاسوبك ما تطلب منه فقط، إذ لن يقرأ أفكارك أو يطبّق ما تنوي فعله. يتسبّب المبرمجون وحتى المحترفون منهم بالأخطاء طوال الوقت، لذلك لا تشعر بالإحباط إذا واجه برنامجك مشكلةً ما. يوجد عددٌ من الأدوات والتقنيات لتحديد ما تفعله شيفرتك البرمجية بالضبط ومكان حدوث الخطأ لحسن الحظ، حيث سنوضّح في هذا المقال أولًا التسجيل Logging والتأكيدات Assertions، وهما ميزتان مساعدتان في اكتشاف الأخطاء في وقت مبكر، إذ يصبح إصلاح الأخطاء أسهل كلما اكتشفتَها مبكرًا. سنوضّح بعد ذلك كيفية استخدام منقّح الأخطاء Debugger الذي يمثّل ميزةً من ميزات المحرّر Mu، حيث ينفّذ منقّح الأخطاء البرنامج من خلال تنفيذ تعليمةٍ واحدة في كل مرة، مما يمنحك فرصة لفحص القيم في المتغيرات أثناء تشغيل شيفرتك البرمجية وتعقّب كيفية تغير هذه القيم عبر برنامجك بأكمله. يُعَد ذلك أبطأ بكثير من تشغيل البرنامج بأقصى سرعة، ولكنه مفيد لرؤية القيم الفعلية في البرنامج أثناء تشغيله بدلًا من استنتاج ما ستكون عليه القيم من الشيفرة المصدرية. رفع الاستثناءات Raising Exceptions ترفع لغة بايثون Python استثناءً عندما تحاول تنفيذ شيفرة برمجية غير صالحة، حيث تعرّفنا في مقالٍ سابق على كيفية التعامل مع استثناءات بايثون باستخدام تعليمتي try و except حتى يتمكّن برنامجك من التعافي من الاستثناءات المُتوقَّعة. يمكنك أيضًا رفع استثناءاتك الخاصة في شيفرتك البرمجية، حيث يمثّل رفع الاستثناء طريقةً لإيقاف تشغيل الشيفرة البرمجية في هذه الدالة ونقل تنفيذ البرنامج إلى التعليمة except. تُرفَع الاستثناءات باستخدام التعليمة raise التي تتكون ممّا يلي: الكلمة المفتاحية raise. استدعاء الدالة Exception(). سلسلة نصية تحتوي على رسالة خطأ مفيدة نمرَّرها إلى الدالة Exception(). لندخِل مثلًا ما يلي في الصدفة التفاعلية Interactive Shell: >>> raise Exception('This is the error message.') Traceback (most recent call last): File "<pyshell#191>", line 1, in <module> raise Exception('This is the error message.') Exception: This is the error message. إن لم تُوجَد تعليمات try و except المغلِّفة للتعليمة except التي ترفع الاستثناء، فسيتعطل البرنامج ويعرض رسالة خطأ الاستثناء ببساطة. تعرِف الشيفرة البرمجية التي تستدعي الدالة -وليس الدالة نفسها- كيفية التعامل مع الاستثناء، وهذا يعني أنك سترى التعليمة raise ضمن الدالة وتعليمتي try و except في الشيفرة البرمجية التي تستدعي هذه الدالة. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد، وأدخِل مثلًا الشيفرة البرمجية التالية واحفظ البرنامج بالاسم boxPrint.py: def boxPrint(symbol, width, height): if len(symbol) != 1: ➊ raise Exception('Symbol must be a single character string.') if width <= 2: ➋ raise Exception('Width must be greater than 2.') if height <= 2: ➌ raise Exception('Height must be greater than 2.') print(symbol * width) for i in range(height - 2): print(symbol + (' ' * (width - 2)) + symbol) print(symbol * width) for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)): try: boxPrint(sym, w, h) ➍ except Exception as err: ➎ print('An exception happened: ' + str(err)) اطّلع على تنفيذ هذا البرنامج، حيث عرّفنا الدالة boxPrint() التي تأخذ محرفًا وعرضًا وارتفاعًا، وتستخدم هذا المحرف لإنشاء صورة صغيرة لمربع له هذا العرض والارتفاع، وتطبع شكل هذا المربع على الشاشة. لنفترض أننا نريد أن يكون هذا المحرف مفردًا، وأن يكون العرض والارتفاع أكبر من 2، فسنضيف تعليمات if لرفع الاستثناءات عند عدم استيفاء هذه المتطلبات. ستتعامل لاحقًا تعليمات try/except مع الوسطاء غير الصالحة عندما نستدعي الدالة boxPrint() مع وسطاء مختلفة. يستخدم هذا البرنامج الصيغة except Exception as err للتعليمة except ➍. إذا أُعيد الكائن Exception من الدالة boxPrint() ➊ ➋ ➌، فستخزِّنه التعليمة except في متغير بالاسم err، ثم يمكننا تحويل الكائن Exception إلى سلسلة نصية من خلال تمريره إلى الدالة str() لإعطاء رسالة خطأ مألوفة للمستخدم ➎، وسيبدو الخرج كما يلي عند تشغيل البرنامج boxPrint.py: **** * * * * **** OOOOOOOOOOOOOOOOOOOO O O O O O O OOOOOOOOOOOOOOOOOOOO An exception happened: Width must be greater than 2. An exception happened: Symbol must be a single character string. يمكنك التعامل مع الأخطاء بأمان أكبر باستخدام تعليمات try و except بدلًا من ترك البرنامج يتعطل بأكمله. الحصول على التعقب العكسي Traceback كسلسلة نصية إذا واجهت شيفرة بايثون خطأً ما، فستعطي كنزًا من المعلومات عن الخطأ، حيث يُسمّى هذه المعلومات بالتعقّب العكسي Traceback الذي يتضمّن رسالة الخطأ ورقم السطر الذي تسبّب في الخطأ وسلسلة استدعاءات الدوال التي أدّت إلى الخطأ، وتسمّى هذه السلسلة من الاستدعاءات بمكدس الاستدعاءات Call Stack. افتح تبويبًا جديدًا في محرّر Mu لإنشاء ملف جديد، وأدخِل البرنامج التالي واحفظه بالاسم errorExample.py: def spam(): beef() def beef(): raise Exception('This is the error message.') spam() سيبدو الخرج كما يلي عند تشغيل البرنامج errorExample.py: Traceback (most recent call last): File "errorExample.py", line 7, in <module> spam() File "errorExample.py", line 2, in spam beef() File "errorExample.py", line 5, in beef raise Exception('This is the error message.') Exception: This is the error message. يمكنك أن ترى من التعقّب العكسي السابق أن الخطأ حدث في السطر رقم 5 في الدالة beef()، وأتى هذا الاستدعاء للدالة beef() من السطر رقم 2 في الدالة spam() التي اُستدعيت بدورها في السطر رقم 7. يمكن أن يساعدك مكدس الاستدعاءات في تحديد الاستدعاء الذي أدى إلى الخطأ في البرامج التي يمكن فيها استدعاء الدوال من أماكن متعددة. تعرض لغة بايثون التعقّب العكسي عند عدم التعامل مع الاستثناء المرفوع، ولكن يمكنك أيضًا الحصول على التعقب العكسي بوصفه سلسلة نصية من خلال استدعاء الدالة traceback.format_exc()، حيث تكون هذه الدالة مفيدة إذا أردتَ الحصول على المعلومات من التعقب العكسي الخاص بالاستثناء ولكنك تريد أيضًا التعليمة except للتعامل مع الاستثناء بأمان. يجب استيراد الوحدة traceback الخاصة بلغة بايثون قبل استدعاء هذه الدالة. يمكنك مثلًا كتابة معلومات التعقّب العكسي في ملف نصي والحفاظ على تشغيل البرنامج بدلًا من تعطّل برنامجك عند حدوث الاستثناء، حيث يمكنك إلقاء نظرة على الملف النصي لاحقًا عندما تكون مستعدًا لتنقيح أخطاء برنامجك. أدخِل الآن ما يلي في الصدفة التفاعلية: >>> import traceback >>> try: ... raise Exception('This is the error message.') except: ... errorFile = open('errorInfo.txt', 'w') ... errorFile.write(traceback.format_exc()) ... errorFile.close() ... print('The traceback info was written to errorInfo.txt.') 111 The traceback info was written to errorInfo.txt. تُعَد القيمة 111 هي القيمة التي يعيدها التابع write()، حيث كُتِبت المحارف 111 في الملف، وكُتِب نص التعقّب العكسي في الملف errorInfo.txt: Traceback (most recent call last): File "<pyshell#28>", line 2, in <module> Exception: This is the error message. سنتعلّم لاحقًا كيفية استخدام الوحدة logging التي تُعَد أكثر فعالية من مجرد كتابة معلومات الخطأ في ملفات نصية. التأكيدات Assertions يُعَد التأكيد Assertion فحص سلامةٍ للتأكد من أن شيفرتك البرمجية لا تفعل شيئًا خاطئًا، حيث يمكن إجراء عمليات التحقق من السلامة من خلال استخدام التعليمة assert، وإذا فشل التحقق من السلامة، فسيُرفَع الاستثناء AssertionError. تتكون التعليمة assert مما يلي في الشيفرة البرمجية: الكلمة المفتاحية assert. شرط (أيّ تعبير يمكن تقييمه بالقيمة True أو False). فاصلة. سلسلة نصية تُعرَض عندما تكون قيمة الشرط False. تمثّل التعليمة assert التأكيد على أن الشرط صحيح، وإذا لم يكن الأمر كذلك، فلا بد من وجود خطأٍ في مكانٍ ما، لذا يجب إيقاف البرنامج مباشرةً. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73] >>> ages.sort() >>> ages [15, 17, 22, 26, 47, 54, 57, 73, 80, 92] >>> assert ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير تؤكّد التعليمة assert في المثال السابق على أن العنصر الأول في القائمة ages يجب أن يكون أصغر من العنصر الأخير أو يساويه، ويمثّل ذلك التحقق من السلامة، حيث إذا كانت الشيفرة البرمجية الموجودة في التابع sort() خالية من الأخطاء وأدت عملها، فسيكون التأكيد صحيحًا. يُقيَّم التعبير ages[0] <= ages[-1] على أنه True، وبالتالي لن تفعل التعليمة assert شيئًا، ولكن لنتظاهر بوجود خطأ في شيفرتنا البرمجية، ولنفترض أننا استدعينا عن طريق الخطأ تابعَ القائمة reverse() بدلًا من تابع القائمة sort()، حيث سترفع التعليمة assert خطأ AssertionError مثلًا عندما ندخِل ما يلي في الصدفة التفاعلية: >>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73] >>> ages.reverse() >>> ages [73, 47, 80, 17, 15, 22, 54, 92, 57, 26] >>> assert ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError يجب ألّا تتعامل شيفرتك البرمجية مع التعليمة assert باستخدام تعليمات try و except على عكس الاستثناءات، حيث إذا فشلت التعليمة assert، فيُفترَض أن يتعطل برنامجك، وبالتالي ستختصر الوقت المنقضي من حدوث السبب الأصلي للخطأ إلى وقت ملاحظة الخطأ لأول مرة من خلال هذا الفشل السريع، مما سيؤدي إلى تقليل الشيفرة البرمجية التي يجب أن تتحقق منها قبل العثور على سبب الخطأ. تُعَد التأكيدات مُخصَّصة لأخطاء المبرمج وليست خاصة بأخطاء المستخدم، إذ يجب أن تفشل التأكيدات فقط عندما يكون البرنامج قيد التطوير، ويجب ألّا يرى المستخدم أبدًا خطأ تأكيد في البرنامج النهائي، لذا يجب أن ترفع استثناءً بدلًا من اكتشافه باستخدام التعليمة assert بالنسبة للأخطاء التي يمكن أن يتعرض لها برنامجك كجزءٍ عادي من عمله مثل عدم العثور على ملف أو إدخال المستخدم بيانات غير صالحة، ويجب ألّا تستخدم تعليمات assert بدلًا من رفع الاستثناءات، لأنه يمكن للمستخدمين اختيار إيقاف التأكيدات. إذا شغّلتَ سكربت بايثون باستخدام الأمر python -O myscript.py بدلًا من الأمر python myscript.py، فستتخطّى شيفرة بايثون تعليمات assert، وقد يعطّل المستخدمون التأكيدات عندما يطورون برنامجًا ويحتاجون إلى تشغيله في بيئة الإنتاج التي تتطلب أداءً أعلى، بالرغم من أنهم في كثير من الحالات سيتركون التأكيدات مفعّلة حتى في ذلك الوقت. لا تُعَد التأكيدات بديلًا عن الاختبار الشامل، فمثلًا إذا ضبطنا القائمة ages في المثال السابق على القيمة [10, 3, 2, 1, 20]، فلن تلاحظ تعليمة التأكيد assert ages[0] <= ages[-1] أن القائمة غير مرتبة، لأنها ترى أن العمر الأول أصغر من أو يساوي العمر الأخير، وهو الشيء الوحيد الذي تتحقق منه تعليمة التأكيد. استخدام التأكيد في برنامج لمحاكاة إشارات المرور لنفترض أنك تنشئ برنامجًا لمحاكاة إشارات المرور، حيث يكون هيكل البيانات الذي يمثّل إشارات التوقف عند التقاطع هو قاموس له المفاتيح 'ns' و 'ew' لإشارات التوقف التي تمثّل جهة الشمال-الجنوب وجهة الشرق-الغرب على التوالي. ستكون القيم الموجودة في هذه المفاتيح إحدى السلاسل النصية 'green' أو 'yellow' أو 'red'، حيث ستبدو الشيفرة البرمجية كما يلي: market_2nd = {'ns': 'green', 'ew': 'red'} mission_16th = {'ns': 'red', 'ew': 'green'} يمثّل المتغيران السابقان تقاطعات شارع السوق Market Street والشارع الثاني 2nd Street، وشارع ميشن Mission Street والشارع السادس عشر 16th Street. نبدأ المشروع من خلال كتابة الدالة switchLights() التي تأخذ قاموسًا يمثّل التقاطع بوصفه وسيطًا وتبّدل بين الأضواء. نعتقد في البداية أن الدالة switchLights() يجب أن تحوّل ببساطة كل ضوء إلى اللون التالي في السلسلة، حيث يجب أن تتغيّر جميع القيم 'green' إلى القيمة 'yellow'، ويجب أن تتغير قيم 'yellow' إلى القيم 'red'، ويجب أن تتغير القيم 'red' إلى القيم 'green'، إذ قد تبدو الشيفرة البرمجية لتطبيق هذه الفكرة كما يلي: def switchLights(stoplight): for key in stoplight.keys(): if stoplight[key] == 'green': stoplight[key] = 'yellow' elif stoplight[key] == 'yellow': stoplight[key] = 'red' elif stoplight[key] == 'red': stoplight[key] = 'green' switchLights(market_2nd) لا بد أنك رأيتَ مشكلة هذه الشيفرة البرمجية، ولكن لنفترض أنك كتبتَ بقية شيفرة المحاكاة التي يبلغ طولها آلاف الأسطر دون أن تلاحظ ذلك، حيث لن يتعطل البرنامج عندما تشغّل المحاكاة في النهاية، ولكن ستتعطّل سياراتك الافتراضية في البرنامج. لن يكون لديك أيّ فكرة عن مكان وجود الخطأ بما أنك كتبتَ بقية البرنامج فعليًا، إذ قد يكون الخطأ في الشيفرة البرمجية التي تحاكي السيارات أو في الشيفرة البرمجية التي تحاكي السائقين الافتراضيين، وبالتالي قد يستغرق الأمر ساعات لتعقّب الخطأ العكسي إلى الدالة switchLights(). إذا أضفتَ تأكيدًا أثناء كتابة الدالة switchLights() للتحقّق من أن أحد الأضواء يكون دائمًا باللون الأحمر على الأقل، فيمكن تضمين ما يلي في نهاية الدالة: assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) سيتعطّل برنامجك مع ظهور رسالة الخطأ التالية باستخدام التأكيد السابق: Traceback (most recent call last): File "carSim.py", line 14, in <module> switchLights(market_2nd) File "carSim.py", line 13, in switchLights assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) ➊ AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'} السطر المهم في المثال السابق هو AssertionError ➊، حيث لا يُعَد تعطّل برنامجك أمرًا مثاليًا، ولكنه يشير مباشرةً إلى فشل التحقق من السلامة، إذ لا يحتوي أي من اتجاهي حركة المرور على ضوء أحمر، مما يعني أن حركة المرور يمكن أن تسير في كلا الاتجاهين من التقاطع. يمكنك توفير الكثير من جهد تنقيح الأخطاء مستقبلًا من خلال الفشل السريع في وقتٍ مبكر من تنفيذ البرنامج. التسجيل Logging إذا سبق لك أن وضعتَ التعليمة print() في شيفرتك البرمجية لإنتاج قيمة بعض المتغيرات أثناء تشغيل البرنامج، فلا بد أنك استخدمتَ صيغة تسجيلٍ لتنقيح أخطاء شيفرتك البرمجية، حيث يُعَد التسجيل طريقةً رائعة لفهم ما يحدث في برنامجك وترتيب حدوثه. تسهّل الوحدة logging في بايثون إنشاء سجلٍ للرسائل المُخصَّصة التي تكتبها، إذ توضّح هذه الرسائل وقت وصول تنفيذ البرنامج إلى استدعاء دالة التسجيل وتسرد المتغيرات التي حدّدتها في ذلك الوقت. بينما تشير رسالة السجل الناقصة إلى تخطي جزء من الشيفرة البرمجية وعدم تنفيذه مطلقًا. استخدام الوحدة logging يمكن تفعيل الوحدة logging لعرض رسائل السجل على شاشتك أثناء تشغيل البرنامج من خلال نسخ ما يلي إلى بداية برنامجك، ولكن ضمّن السطر Shebang الذي هو #! python: import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname) s - %(message)s') لا داعي للقلق كثيرًا بشأن كيفية عمل السطر السابق، ولكنه ينشئ الكائن LogRecord الذي يحتوي على معلومات حول حدثٍ ما عندما تسجّل بايثون هذا الحدث. تتيح لك الدالة basicConfig() الخاصة بالوحدة logging تحديدَ التفاصيل المتعلقة بكائن LogRecord الذي تريد رؤيته وكيفية عرض هذه التفاصيل. لنفترض أنك كتبتَ دالةً لحساب عاملي Factorial عددٍ ما، حيث يكون عاملي العدد 4 في الرياضيات هو 1 × 2 × 3 × 4 أو القيمة 24 وعاملي العدد 7 هو 1 × 2 × 3 × 4 × 5 × 6 × 7 أو القيمة 5040. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية التي تحتوي على خطأ، ولكنك ستدخِل أيضًا عددًا من رسائل السجل لمساعدة نفسك في اكتشاف الخطأ الذي يحدث، واحفظ البرنامج بالاسم factorialLog.py: import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.debug('Start of program') def factorial(n): logging.debug('Start of factorial(%s%%)' % (n)) total = 1 for i in range(n + 1): total *= i logging.debug('i is ' + str(i) + ', total is ' + str(total)) logging.debug('End of factorial(%s%%)' % (n)) return total print(factorial(5)) logging.debug('End of program') نستخدم الدالة logging.debug() عندما نريد طباعة معلومات السجل، وتستدعي الدالةُ debug() الدالةَ basicConfig() وستطبع سطرًا من المعلومات، حيث ستكون هذه المعلومات بالتنسيق الذي حددناه في الدالة basicConfig() وستتضمّن الرسائل التي مرّرناها إلى الدالة debug(). يُعَد استدعاء الدالة print(factorial(5)) جزءًا من البرنامج الأصلي، لذا ستُعرَض النتيجة حتى لو تعطّلت رسائل التسجيل. يبدو خرج هذا البرنامج كما يلي: 2024-05-23 16:20:12,664 - DEBUG - Start of program 2024-05-23 16:20:12,664 - DEBUG - Start of factorial(5) 2024-05-23 16:20:12,665 - DEBUG - i is 0, total is 0 2024-05-23 16:20:12,668 - DEBUG - i is 1, total is 0 2024-05-23 16:20:12,670 - DEBUG - i is 2, total is 0 2024-05-23 16:20:12,673 - DEBUG - i is 3, total is 0 2024-05-23 16:20:12,675 - DEBUG - i is 4, total is 0 2024-05-23 16:20:12,678 - DEBUG - i is 5, total is 0 2024-05-23 16:20:12,680 - DEBUG - End of factorial(5) 0 2024-05-23 16:20:12,684 - DEBUG - End of program تعيد الدالة factorial() القيمة 0 بوصفها ناتج عاملي العدد 5، وهذا ليس صحيحًا، حيث يجب أن تضرب حلقة for القيمة الموجودة في المتغير total بالأعداد من 1 إلى 5، ولكن توضّح رسائل السجل التي تعرضها الدالة logging.debug() أن المتغير i يبدأ من القيمة 0 بدلًا من القيمة 1، وبالتالي تكون قيمة بقية التكرارات خاطئة للمتغير total أيضًا، لأن ضرب الصفر بأيّ شيء يساوي صفرًا. توفّر رسائل التسجيل سلسلةً من مسارات التنقل التي يمكن أن تساعدك في معرفة متى بدأت الأمور تسوء. غيّر السطر for i in range(n + 1): إلى for i in range(1, n + 1):، وشغّل البرنامج مرة أخرى، وسيبدو الخرج كما يلي: 2024-05-23 17:13:40,650 - DEBUG - Start of program 2024-05-23 17:13:40,651 - DEBUG - Start of factorial(5) 2024-05-23 17:13:40,651 - DEBUG - i is 1, total is 1 2024-05-23 17:13:40,654 - DEBUG - i is 2, total is 2 2024-05-23 17:13:40,656 - DEBUG - i is 3, total is 6 2024-05-23 17:13:40,659 - DEBUG - i is 4, total is 24 2024-05-23 17:13:40,661 - DEBUG - i is 5, total is 120 2024-05-23 17:13:40,661 - DEBUG - End of factorial(5) 120 2024-05-23 17:13:40,666 - DEBUG - End of program يؤدي استدعاء الدالة factorial(5) إلى إعادة القيمة 120 الصحيحة، وتظهِر رسائل السجل ما يحدث ضمن الحلقة، مما يقودك إلى الخطأ مباشرةً. يمكنك أن ترى أن استدعاءات الدالة logging.debug() لا تطبع السلاسل النصية المُمرَّرة إليها فقط، بل تطبع أيضًا العلامة الزمنية Timestamp والكلمة DEBUG. لا تنقح الأخطاء باستخدام الدالة print() تُعَد كتابة التعليمتين import logging و logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') أمرًا صعبًا بعض الشيء، لذا سترغب في استخدام استدعاءات الدالة print() بدلًا من ذلك، ولكن لا تنخدع بسهولة استخدام هذه الدالة، لأنك ستقضي كثيرًا من الوقت في إزالة استدعاءات الدالة print() من شيفرتك البرمجية لجميع رسائل السجل بعد الانتهاء من تنقيح الأخطاء، وقد تزيل أيضًا عن طريق الخطأ بعض استدعاءات الدالة print() المُستخدَمة للرسائل التي ليس لها علاقة بالسجل. يمكنك ملء برنامجك بالعدد الذي تريده من رسائل السجل، ويمكنك دائمًا تعطيلها لاحقًا من خلال إضافة استدعاء واحد للدالة logging.disable(logging.CRITICAL)، حيث تسهّل الوحدة logging التبديل بين إظهار وإخفاء رسائل السجل على عكس الدالة print(). تُعَد رسائل السجل خاصةً بالمبرمج وليست خاصة بالمستخدم، إذ لن يهتم المستخدم بمحتويات بعض قيم القاموس التي تحتاج إلى رؤيتها للمساعدة في تنقيح الأخطاء، لذا استخدم رسالة سجل لذلك، ولكن يجب أن تستخدم استدعاء الدالة print() بالنسبة للرسائل التي يرغب المستخدم في رؤيتها مثل "عدم العثور على ملف File not found" أو "قيمة إدخال غير صالحة لذا أدخِل قيمة عددية من فضلك Invalid input, please enter a number"، فلن ترغب في حرمان المستخدم من المعلومات المفيدة له بعد تعطيل رسائل السجل. مستويات التسجيل توفر مستويات التسجيل طريقة لتصنيف رسائل السجل حسب الأهمية، إذ توجد خمسة مستويات للتسجيل في لغة بايثون التي سنوضحها في الجدول التالي من الأقل إلى الأكثر أهمية، حيث يمكن تسجيل الرسائل على كل مستوى باستخدام دالة تسجيل مختلفة: مستوى التسجيل دالة التسجيل وصفها المستوى DEBUG الدالة logging.debug() المستوى الأدنى، ويُستخدَم للتفاصيل الصغيرة، حيث تهتم بهذه الرسائل عند تشخيص المشاكل فقط. المستوى INFO الدالة logging.info() يُستخدَم لتسجيل معلومات عن الأحداث العامة في برنامجك أو للتأكد من أن الأمور تسير جيدًا في البرنامج. المستوى WARNING الدالة logging.warning() يُستخدم للإشارة إلى مشكلة محتملة لا تمنع البرنامج من العمل ولكنها قد تفعل ذلك مستقبلًا. المستوى ERROR الدالة logging.error() يُستخدم لتسجيل خطأٍ تسبّب في فشل البرنامج بعمل شيءٍ ما. المستوى CRITICAL الدالة logging.critical() المستوى الأعلى، ويُستخدَم للإشارة إلى خطأ كبير تسبّب أو أنه على وشك التسبّب في توقف البرنامج عن العمل بالكامل. تُمرَّر رسالة التسجيل بوصفها سلسلة نصية إلى هذه الدوال. تُعَد مستويات التسجيل مجرد اقتراحات، فالأمر متروك لك لتحديد الفئة التي تندرج ضمنها رسالة السجل الخاصة بك. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import logging >>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') >>> logging.debug('Some debugging details.') 2024-05-18 19:04:26,901 - DEBUG - Some debugging details. >>> logging.info('The logging module is working.') 2024-05-18 19:04:35,569 - INFO - The logging module is working. >>> logging.warning('An error message is about to be logged.') 2024-05-18 19:04:56,843 - WARNING - An error message is about to be logged. >>> logging.error('An error has occurred.') 2024-05-18 19:05:07,737 - ERROR - An error has occurred. >>> logging.critical('The program is unable to recover!') 2024-05-18 19:05:45,794 - CRITICAL - The program is unable to recover! تتمثّل فائدة مستويات التسجيل في أنه يمكنك تغيير أولوية رسالة التسجيل التي تريد رؤيتها، حيث سيؤدي تمرير المستوى logging.DEBUG إلى وسيط الكلمة المفتاحية Keyword Argument الذي هو level الخاص بالدالة basicConfig() إلى إظهار الرسائل من جميع مستويات التسجيل، حيث يُعَد المستوى DEBUG هو المستوى الأدنى. قد تكون مهتمًا بالأخطاء فقط بعد تطوير برنامجك، وبالتالي يمكنك ضبط الوسيط level الخاص بالدالة basicConfig() على المستوى logging.ERROR في هذه الحالة، إذ سيعرض هذا المستوى رسائل المستوى ERROR ورسائل المستوى CRITICAL فقط ويتخطى رسائل المستويات DEBUG و INFO و WARNING. تعطيل التسجيل لا بد أنك تفضّل عدم إظهار جميع رسائل السجل التي تؤدي إلى جعل الشاشة مزدحمة بعد تنقيح أخطاء برنامجك، لذا توجد الدالة logging.disable() التي تعمل على تعطيل رسائل السجل حتى لا تضطر إلى الدخول إلى برنامجك وإزالة جميع استدعاءات التسجيل يدويًا. يمكنك تمرير هذه الدالة إلى مستوى التسجيل فقط، وستمنع هذه الدالة جميع رسائل السجل عند هذا المستوى أو المستويات الأقل، لذا إذا أردتَ تعطيل التسجيل بالكامل، فما عليك سوى إضافة الاستدعاء logging.disable(logging.CRITICAL) إلى برنامجك. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import logging >>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s - %(levelname)s - %(message)s') >>> logging.critical('Critical error! Critical error!') 2024-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error! >>> logging.disable(logging.CRITICAL) >>> logging.critical('Critical error! Critical error!') >>> logging.error('Error! Error!') ستعطّل الدالة logging.disable() جميع الرسائل بعدها، لذا يُحتمَل أنك تريد إضافتها بالقرب من سطر الاستيراد import logging في برنامجك، وبالتالي يمكنك بسهولة العثور عليه لتعليق هذا الاستدعاء أو إلغاء تعليقه لتفعيل رسائل التسجيل أو تعطيلها حسب الحاجة. التسجيل في ملف يمكنك كتابة رسائل السجل في ملف نصي بدلًا من عرضها على الشاشة، حيث تأخذ الدالة logging.basicConfig() وسيط الكلمة المفتاحية filename كما يلي: import logging logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') ستُحفَظ رسائل السجل في الملف myProgramLog.txt.يمكن أن تؤدي رسائل التسجيل إلى جعل شاشتك مزدحمة بالعناصر وتجعل من الصعب قراءة خرج البرنامج بالرغم من فوائدها التي تحدّثنا عنها سابقًا، لذا كتبنا رسائل التسجيل في ملف لإبقاء شاشتك واضحة ولتخزين الرسائل حتى تتمكّن من قراءتها بعد تشغيل البرنامج، حيث يمكنك فتح هذا الملف النصي في أي محرّر نصوص مثل المفكرة Notepad أو TextEdit. منقح أخطاء المحرر Mu يُعَد منقّح الأخطاء من ميزات المحرّر Mu و IDLE وبرامج تحرير النصوص الأخرى التي تسمح لك بتنفيذ برنامجك من خلال تنفيذ سطر واحد في كل مرة، حيث يشغّل منقّح الأخطاء سطرًا واحد من الشيفرة البرمجية ثم ينتظر حتى تخبره بالمتابعة. يمكنك أن تأخذ الوقت الذي تريده لفحص القيم الموجودة في المتغيرات في أي مرحلة معينة من عمر البرنامج من خلال تشغيل برنامجك مع منقّح الأخطاء باستخدام هذه الطريقة، إذ يُعَد منقّح الأخطاء أداةً مفيدة لتعقّب الأخطاء. يمكنك تشغيل برنامج مع منقّح أخطاء المحرّر Mu من خلال النقر على زر تنقيح الأخطاء "Debug" في الصف العلوي من الأزرار بجانب زر التشغيل "Run". ستفتح نافذة فحص تنقيح الأخطاء "Debug Inspector" على طول الجانب الأيمن من النافذة بالإضافة إلى نافذة الخرج المعتاد في الأسفل، حيث تسرد نافذة فحص تنقيح الأخطاء قيم المتغيرات الحالية في برنامجك. يوقِف منقّح الأخطاء في الشكل التالي تنفيذ البرنامج قبل تشغيل السطر الأول من الشيفرة البرمجية، حيث يمكنك رؤية هذا السطر مميزًا في محرّر الملفات: تشغيل المحرّر Mu لبرنامجٍ ما مع منقّح الأخطاء يضيف وضع تنقيح الأخطاء أيضًا أزرارًا جديدة إلى أعلى المحرّر وهي: زر المتابعة "Continue" وزر "Step Over" وزر "Step In" وزر "Step Out"، ويوجد زر التوقف "Stop" المعتاد أيضًا. زر المتابعة Continue يؤدي النقر على زر المتابعة "Continue" إلى تنفيذ البرنامج بطريقة طبيعية حتى ينتهي البرنامج أو يصل إلى نقطة توقف Breakpoint (سنوضح نقاط التوقف لاحقًا في هذا المقال). إذا انتهيتَ من تنقيح الأخطاء وأردتَ أن يتابع البرنامج عمله بطريقة طبيعية، فانقر على زر المتابعة "Continue". زر Step In يؤدي النقر على زر "Step In" إلى أن ينفّذ منقّح الأخطاء السطر التالي من الشيفرة البرمجية ثم يوقفه مرةً أخرى. إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيدخل منقّح الأخطاء إلى تلك الدالة وينتقل إلى السطر الأول من الشيفرة البرمجية فيها. زر Step Over يؤدي النقر على زر "Step Over" إلى تنفيذ السطر التالي من الشيفرة البرمجية مثل زر "Step In"، ولكن إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيتجاوز زر "Step Over" الشيفرة البرمجية الموجودة في هذه الدالة، حيث ستُنفَّذ الشيفرة البرمجية الخاصة بالدالة بالسرعة القصوى، وسيتوقّف منقّح الأخطاء بعد العودة من استدعاء هذه الدالة. إذا استدعى السطر التالي من الشيفرة البرمجية الدالة spam() مثلًا، ولكنك لا تهتم بالشيفرة البرمجية الموجودة ضمن هذه الدالة، فيمكنك النقر على زر "Step Over" لتنفيذ الشيفرة البرمجية الموجودة في الدالة بالسرعة العادية ثم التوقف عندما تعود الدالة، لذلك يُعَد استخدام زر "Step Over" أكثر شيوعًا من استخدام زر "Step In". زر Step Out يؤدي النقر على زر "Step Out" إلى أن ينفّذ منقّح الأخطاء سطورًا من الشيفرة البرمجية بالسرعة القصوى حتى العودة من الدالة الحالية. إذا دخلتَ في استدعاء دالة باستخدام زر "Step In" وتريد الاستمرار في تنفيذ التعليمات حتى الخروج منها، فانقر على زر "Step Out" للخروج من استدعاء الدالة الحالي. زر التوقف Stop إذا أردتَ إيقاف تنقيح الأخطاء وعدم إزعاجك بمتابعة تنفيذ بقية البرنامج، فانقر فوق زر التوقّف "Stop"، حيث سيؤدي الزر "Stop" إلى إنهاء البرنامج مباشرةً. تنقيح أخطاء برنامج لجمع الأعداد افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية: print('Enter the first number to add:') first = input() print('Enter the second number to add:') second = input() print('Enter the third number to add:') third = input() print('The sum is ' + first + second + third) احفظ البرنامج بالاسم buggyAddingProgram.py وشغّله أولًا دون تفعيل منقّح الأخطاء، وسيكون خرج البرنامج كما يلي: Enter the first number to add: 5 Enter the second number to add: 3 Enter the third number to add: 42 The sum is 5342 لم يتعطل البرنامج، ولكن من الواضح أن ناتج الجمع خاطئ. شغّل البرنامج مرة أخرى ولكن مع منقّح الأخطاء هذه المرة، حيث إذا نقرتَ على زر تنقيح الأخطاء "Debug"، فسيتوقف البرنامج مؤقتًا عند السطر 1، وهو سطر الشيفرة البرمجية الذي نوشك على تنفيذه، إذ يجب أن يبدو المحرّر Mu كما في الشكل السابق. انقر على زر "Step Over" مرة واحدة لتنفيذ الاستدعاء الأول print()، إذ يجب أن تستخدم زر "Step Over" بدلًا من زر "Step In" هنا، لأنك لا تريد الدخول إلى الشيفرة البرمجية الخاصة بالدالة print()، بالرغم من أن المحرّر Mu يجب أن يمنع منقّح الأخطاء من الدخول إلى دوال بايثون المُدمَجة. ينتقل منقّح الأخطاء إلى السطر 2، ويميّز السطر 2 في محرر الملفات كما هو موضح في الشكل التالي، مما يوضّح لك مكان تنفيذ البرنامج حاليًا: نافذة المحرّر Mu بعد النقر على زر "Step Over" انقر على زر "Step Over" مرة أخرى لتنفيذ استدعاء الدالة input()، حيث سيختفي التمييز عن الشيفرة البرمجية أثناء انتظار المحرّر Mu أن تكتب شيئًا ما لاستدعاء الدالة input() في نافذة الخرج. أدخِل القيمة 5 واضغط على مفتاح ENTER، ثم سيعود التمييز إلى الشيفرة البرمجية. استمر في النقر على زر "Step Over"، وأدخِل القيمتين 3 و 42 بوصفهما العددين التاليين. يجب أن تبدو نافذة المحرّر Mu كما في الشكل التالي عندما يصل منقّح الأخطاء إلى السطر 7 الذي يمثّل استدعاء الدالة print() النهائي في البرنامج: توضّح نافذة فحص تنقيح الأخطاء Debug Inspector الموجودة على الجانب الأيمن أن المتغيرات مضبوطة بوصفها سلاسلًا نصية وليست أعدادًا صحيحة، مما تسبّب في حدوث الخطأ يجب أن ترى في نافذة فحص تنقيح الأخطاء Debug Inspector أن المتغيرات first و second و third مضبوطة بوصفها سلاسلًا نصية '5' و '3' و '42' بدلًا من الأعداد الصحيحة 5 و 3 و 42. تضم لغة بايثون هذه السلاسل النصية مع بعضها بعضًا عند تنفيذ السطر الأخير بدلًا من جمع هذه الأعداد، مما يتسبّب في حدوث الخطأ. يُعَد التنقل عبر البرنامج باستخدام منقّح الأخطاء بطيئًا، لذا نريد في أغلب الأحيان أن يعمل البرنامج بصورة طبيعية حتى يصل إلى سطر معين من الشيفرة البرمجية، حيث يمكنك ضبط منقّح الأخطاء لتطبيق ذلك باستخدام نقاط التوقف. نقاط التوقف Breakpoints يمكن ضبط نقطة توقف على سطر معين من الشيفرة البرمجية وإجبار منقح الأخطاء على التوقف مؤقتًا عندما يصل تنفيذ البرنامج إلى هذا السطر. افتح تبويبًا جديدًا في محرّر الملفات وأدخِل البرنامج التالي الذي يحاكي رمي عملة معدنية 1000 مرة، واحفظ البرنامج بالاسم coinFlip.py: import random heads = 0 for i in range(1, 1001): ➊ if random.randint(0, 1) == 1: heads = heads + 1 if i == 500: ➋ print('Halfway done!') print('Heads came up ' + str(heads) + ' times.') سيعيد الاستدعاء random.randint(0, 1) ➊ القيمة 0 في نصف الوقت والقيمة 1 في النصف الآخر من الوقت، حيث يمكن استخدام هذا الاستدعاء لمحاكاة رمي قطعة نقود وفق الاحتمال 50/50 وتمثل القيمة 1 الصورة من العملة المعدنية. يكون خرج هذا البرنامج كما يلي عند تشغيله بدون منقّح الأخطاء: Halfway done! Heads came up 490 times. إذا شغّلتَ هذا البرنامج مع منقّح الأخطاء، فيجب أن تنقر على زر "Step Over" آلاف المرات قبل إنهاء البرنامج. إذا كنت مهتمًا بالقيمة heads التي تمثّل صورة العملة المعدنية عند منتصف تنفيذ البرنامج أو عند اكتمال 500 مرة من 1000 مرة لرمي قطعة نقود، فيمكنك ضبط نقطة توقف على السطر print('Halfway done!') ➋، حيث يمكنك ضبط نقطة توقف من خلال النقر على رقم السطر في محرّر الملفات بحيث تظهر نقطة حمراء كما في الشكل التالي: يؤدي ضبط نقطة التوقف إلى ظهور نقطة حمراء (محاطة بدائرة) بجانب رقم السطر لا نريد ضبط نقطة توقف عند سطر التعليمة if لأنها تُنفَّذ في كل تكرار للحلقة، حيث إذا ضبطنا نقطة التوقف على الشيفرة البرمجية الموجودة ضمن التعليمة if، فسيتوقّف منقّح الأخطاء فقط عندما يدخل التنفيذ إلى هذه التعليمة. سيكون للسطر الذي يحتوي على نقطة التوقف نقطة حمراء بجانبه. إذا شغّلنا البرنامج مع منقّح الأخطاء، فسيبدأ في حالة التوقّف المؤقت عند السطر الأول كالمعتاد، ولكن إذا نقرت على زر المتابعة "Continue"، فسيُشغَّل البرنامج بالسرعة القصوى حتى يصل إلى السطر الذي ضبطنا نقطة التوقف عنده. يمكنك بعد ذلك النقر على أزرار "Continue" أو "Step Over" أو "Step In" أو "Step Out" للمتابعة كالمعتاد. إذا أردتَ إزالة نقطة توقف، فانقر على رقم السطر مرة أخرى، وستختفي النقطة الحمراء، ولن يتوقف منقّح الأخطاء عند هذا السطر لاحقًا. مشروع للتدريب حاول كتابة برنامج يطبّق ما يلي لكسب خبرة عملية أكبر. تنقيح أخطاء برنامج لرمي عملة معدنية يهدف هذا البرنامج إلى إنشاء لعبة تخمين بسيطة لرمي عملة معدنية مع حصول اللاعب على تخمينين، ولكنه يحتوي على العديد من الأخطاء بالرغم من سهولة هذه اللعبة. شغّل البرنامج عدة مرات للعثور على الأخطاء التي تمنع البرنامج من العمل بصورة صحيحة. import random guess = '' while guess not in ('heads', 'tails'): print('Guess the coin toss! Enter heads or tails:') guess = input() toss = random.randint(0, 1) # تمثّل القيمة 0 الكتابة، وتمثل القيمة 1 الصورة في العملة المعدنية if toss == guess: print('You got it!') else: print('Nope! Guess again!') guesss = input() if toss == guess: print('You got it!') else: print('Nope. You are really bad at this game.') الخلاصة تُعَد التأكيدات والاستثناءات والتسجيل ومنقح الأخطاء أدوات قيّمة للعثور على الأخطاء ومنعها في برنامجك، حيث تُعَد التأكيدات باستخدام التعليمة assert في لغة بايثون طريقة جيدة لتنفيذ فحص السلامة الذي يمنحك تحذيرًا مبكرًا عندما لا يكون الشرط الضروري صحيحًا، وتكون التأكيدات مخصَّصة فقط للأخطاء التي يجب ألّا يحاول البرنامج التعافي منها ويجب أن يفشل بسرعة، وإلّا فيجب أن ترفع استثناء. يمكن اكتشاف الاستثناء ومعالجته باستخدام تعليمات try و except. تُعَد الوحدة logging طريقة جيدة لتفحّص شيفرتك البرمجية أثناء تشغيلها، وهي أكثر ملاءمة للاستخدام من الدالة print() لاحتوائها على مستويات تسجيل متعددة ولقدرتها على التسجيل في ملف نصي. يتيح لك منقّح الأخطاء التنقل عبر برنامجك سطرًا تلو الآخر، ويمكنك تشغيل برنامجك بالسرعة العادية وجعل منقّح الأخطاء يوقف التنفيذ مؤقتًا عندما يصل إلى سطرٍ ضبطنا عنده نقطة توقف، ويمكنك رؤية حالة قيمة أيّ متغير في أي وقت من عمر البرنامج باستخدام منقّح الأخطاء. تساعدك هذه الأدوات والتقنيات لتنقيح الأخطاء على كتابة البرامج الناجحة، حيث يُعَد إدخال أخطاء في شيفرتك البرمجية عن طريق الخطأ حقيقة يجب التسليم بها بغض النظر عن عدد سنوات خبرتك في البرمجة. ترجمة -وبتصرُّف- للمقال Debugging لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: تنظيم الملفات باستخدام بايثون كتابة شيفرات بايثون: صيغ شائعة الاستخدام على نحو خاطئ أساسيات البرمجة بلغة بايثون النسخة الكاملة لكتاب البرمجة بلغة بايثون
-
تعلّمنا في مقالٍ سابق كيفية إنشاء ملفات جديدة والكتابة فيها باستخدام لغة بايثون Python، ويمكن لبرامجك أيضًا تنظيم الملفات الموجودة مسبقًا على القرص الصلب. لا بد أنك جرّبتَ تصفح مجلدٍ مليء بالعشرات أو المئات أو حتى الآلاف من الملفات ونسخها أو إعادة تسميتها أو نقلها أو ضغطها جميعًا يدويًا، أو جرّبتَ مهامًا أخرى مثل المهام التالية: إنشاء نُسخ من جميع ملفات PDF الموجودة في كل مجلدٍ فرعي من مجلدٍ ما. إزالة الأصفار البادئة في أسماء الملفات لكل ملفٍ في مجلد يضم مئات الملفات المسمّاة مثلًا spam001.txt و spam002.txt وإلخ. ضغط محتويات عدة مجلدات في ملف مضغوط ZIP واحد، والذي يمكن أن يكون نظام نسخ احتياطي بسيط. يمكنك تنفيذ كافة هذه المهام المملة آليًا في شيفرة بايثون البرمجية، فإذا برمجتَ حاسوبك لإجراء هذه المهام، فيمكنك تحويله إلى محرر ملفات سريع في العمل ولا يرتكب أخطاءً أبدًا. من المفيد رؤية امتداد الملف (مثل .txt و .pdf و .jpg وإلخ) بسرعة عند بدء العمل مع الملفات، إذ يُرجَّح أن يعرض متصفح ملفاتك الامتدادات تلقائيًا في نظامي ماك macOS ولينكس Linux، ولكن قد تكون امتدادات الملفات مخفية افتراضيًا في نظام ويندوز Windows، لذا يمكنك إظهار الامتدادات من خلال الانتقال إلى قائمة ابدأ Start، ثم لوحة التحكم Control Panel، ثم المظهر وإضفاء طابع شخصي Appearance and Personalization، ثم خيارات مستكشف الملفات Folder Options. ألغِ تحديد خانة الاختيار إخفاء ملحقات الملفات لأنواع الملفات المعروفة Hide extensions for known file types في تبويب عرض View ضمن الإعدادات المتقدمة Advanced Settings. وحدة shutil تحتوي وحدة shutil (التي هي اختصار لأدوات الصدفة المساعدة Shell Utilities) على دوالٍ تتيح لك نسخ الملفات ونقلها وإعادة تسميتها وحذفها في برامج بايثون الخاصة بك، ولكن يجب أولًا أن تستخدم التعليمة import shutil لاستخدام هذه الدوال. نسخ الملفات والمجلدات توفّر وحدة shutil دوالًا لنسخ الملفات والمجلدات الكاملة، حيث سيؤدي استدعاء الدالة shutil.copy(source, destination) إلى نسخ الملف من مسار المصدر source إلى المجلد الموجود في مسار الوِجهة destination، إذ يمكن أن يكون كلٌّ من source و destination سلاسلًا نصية أو كائنات Path. إذا كان destination اسم ملف، فسنستخدمه بوصفه اسمًا جديدًا للملف المنسوخ. تعيد هذه الدالة سلسلة نصية أو كائن Path للملف المنسوخ. أدخِل مثلًا ما يلي في الصدفة التفاعلية Interactive Shell لترى كيفية عمل الدالة shutil.copy(): >>> import shutil, os >>> from pathlib import Path >>> p = Path.home() ➊ >>> shutil.copy(p / 'spam.txt', p / 'some_folder') 'C:\\Users\\Al\\some_folder\\spam.txt' ➋ >>> shutil.copy(p / 'eggs.txt', p / 'some_folder/eggs2.txt') WindowsPath('C:/Users/Al/some_folder/eggs2.txt') ينسخ استدعاء الدالة shutil.copy() الأول الملف الموجود في C:\Users\Al\spam.txt إلى المجلد C:\Users\Al\some_folder، وتكون القيمة المُعادة هي مسار الملف المنسوخ، ولاحظ استخدام اسم الملف spam.txt الأصلي لاسم الملف المنسوخ الجديد عند تحديد مجلدٍ بوصفه الوِجهة ➊. ينسخ استدعاء الدالة shutil.copy() الثاني ➋ الملف الموجود في C:\Users\Al\eggs.txt إلى المجلد C:\Users\Al\some_folder، ولكنه يعطي الملف المنسوخ الاسم eggs2.txt. تنسخ الدالة shutil.copy() ملفًا واحدًا، وتنسخ الدالة shutil.copytree() مجلدًا كاملًا مع جميع المجلدات والملفات الموجودة فيه، حيث يؤدي استدعاء الدالة shutil.copytree(source, destination) إلى نسخ المجلد الموجود في مسار المصدر source مع جميع ملفاته ومجلداته الفرعية إلى المجلد الموجود في مسار الوِجهة destination، إذ تكون المعاملات source و destination سلاسلًا نصية. تعيد هذه الدالة سلسلة نصية تمثّل مسار المجلد المنسوخ. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import shutil, os >>> from pathlib import Path >>> p = Path.home() >>> shutil.copytree(p / 'spam', p / 'spam_backup') WindowsPath('C:/Users/Al/spam_backup') ينشئ استدعاء الدالة shutil.copytree() السابق مجلدًا جديدًا بالاسم spam_backup الذي يحتوي على محتوى المجلد spam الأصلي نفسه، وبالتالي سنحصل بأمان على نسخة احتياطية من المجلد spam. نقل وإعادة تسمية الملفات والمجلدات سيؤدي استدعاء الدالة shutil.move(source, destination) إلى نقل الملف أو المجلد الموجود في مسار المصدر source إلى مسار الوجهة destination، وسيعيد سلسلة نصية للمسار المطلق الخاص بالموقع الجديد. إذا أشار مسار الوجهة destination إلى مجلد، فسيُنقَل ملف المصدر source إلى الوجهة destination ويحتفظ باسم الملف الحالي. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import shutil >>> shutil.move('C:\\beef.txt', 'C:\\eggs') 'C:\\eggs\\beef.txt' لنفترض أن المجلد الذي اسمه eggs موجودٌ مسبقًا في المجلد C:\، فسيمثّل استدعاء الدالة shutil.move() نقلَ الملف C:\beef.txt إلى المجلد C:\eggs. إذا كان الملف beef.txt موجودًا مسبقًا في المجلد C:\eggs، فسيُكتَب فوقه، لذا يجب عليك توخي الحذر عند استخدام الدالة move() لأنه من السهل الكتابة فوق الملفات عن طريق الخطأ باستخدام هذه الطريقة. يمكن لمسار الوِجهة destination أيضًا تحديد اسم الملف، حيث سننقل ملف المصدر source ونعيد تسميته في المثال التالي: >>> shutil.move('C:\\beef.txt', 'C:\\eggs\\new_beef.txt') 'C:\\eggs\\new_beef.txt' يمثّل السطر السابق نقل الملف C:\beef.txt إلى المجلد C:\eggs وإعادة تسميته بالاسم new_beef.txt. نفترض في المثالين السابقين وجود المجلد eggs في المجلد C:\، ولكن إن لم يوجَد المجلد eggs، فستعيد الدالة move() تسمية الملف beef.txt إلى ملفٍ اسمه eggs. >>> shutil.move('C:\\beef.txt', 'C:\\eggs') 'C:\\eggs' لم تتمكّن الدالة move() من العثور على مجلدٍ بالاسم eggs في المجلد C:\، وبالتالي تفترض أن الوجهة destination يجب أن تحدد اسم ملفٍ وليس اسم مجلد، لذلك أُعيدت تسمية الملف النصي beef.txt إلى egg (ملف نصي بدون امتداد الملف .txt)، وربما ليس هذا ما أردته. يمكن أن يكون ذلك خطأً يصعب اكتشافه في برامجك، لأن استدعاء الدالة move() يمكن أن يفعل شيئًا قد يكون مختلفًا تمامًا عمّا تتوقعه، وهذا سبب آخر لتوخي الحذر عند استخدام الدالة move(). أخيرًا، يجب أن تكون المجلدات التي تشكّل الوِجهة موجودة فعليًا، وإلّا فسترمي شيفرة بايثون استثناءً. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> shutil.move('spam.txt', 'c:\\does_not_exist\\eggs\\meat') Traceback (most recent call last): --snip-- FileNotFoundError: [Errno 2] No such file or directory: 'c:\\does_not_exist\\ eggs\\meat' تبحث شيفرة بايثون عن المجلدين eggs و meat ضمن المجلد does_not_exist، ولكنها لا تعثر على هذا المجلد، لذا لا يمكنها نقل الملف spam.txt إلى المسار الذي حدّدته. حذف الملفات والمجلدات نهائيًا يمكنك حذف ملف واحد أو مجلد واحد فارغ باستخدام دوال تابعة للوحدة os، ولكن يمكنك حذف مجلدٍ وجميع محتوياته باستخدام الوحدة shutil، وهذه الدوال هي: سيؤدي استدعاء الدالة os.unlink(path) إلى حذف الملف الموجود في المسار path. سيؤدي استدعاء الدالة os.rmdir(path) إلى حذف المجلد الموجود في المسار path، ولكن يجب أن يكون هذا المجلد خاليًا من أي ملفات أو مجلدات. سيؤدي استدعاء الدالة shutil.rmtree(path) إلى إزالة المجلد الموجود في المسار path، وستُحذَف جميع الملفات والمجلدات الموجودة ضمنه. كن حذرًا عند استخدام هذه الدوال في برامجك، إذ من الجيد أن تشغّل برنامجك أولًا مع تعليق هذه الاستدعاءات وإضافة استدعاءات الدالة print() لإظهار الملفات التي ستُحذَف. إليك فيما يلي برنامج بايثون الذي يهدف إلى حذف الملفات التي لها امتداد الملف .txt، ولكن يوجد به خطأ مطبعي يؤدي إلى حذف ملفات .rxt بدلًا من ذلك: import os from pathlib import Path for filename in Path.home().glob('*.rxt'): os.unlink(filename) إذا كان لديك أيّ ملفات مهمة تنتهي بالامتداد .rxt فستُحذَف نهائيًا عن طريق الخطأ، لذا يجب أولًا أن تشغّل البرنامج كما يلي: import os from pathlib import Path for filename in Path.home().glob('*.rxt'): #os.unlink(filename) print(filename) لاحظ أننا علّقنا استدعاء الدالة os.unlink()، لذا تجاهلته شيفرة بايثون، وستطبع اسم الملف المحذوف فقط، حيث سيؤدي تشغيل هذه النسخة من البرنامج أولًا إلى إظهار أنك طلبت من البرنامج عن طريق الخطأ حذف ملفات .rxt بدلًا من ملفات .txt. تأكّد من عمل البرنامج بالطريقة الصحيحة، ثم احذف سطر التعليمة print(filename) وألغِ التعليق عند سطر التعليمة os.unlink(filename)، ثم شغّل البرنامج مرة أخرى لحذف الملفات فعليًا. الحذف الآمن باستخدام وحدة send2trash تحذف الدالة shutil.rmtree() المُدمَجة مع لغة بايثون الملفات والمجلدات نهائيًا، ولكن قد يكون استخدامها خطيرًا، لذا توجد طريقة أفضل بكثير لحذف الملفات والمجلدات، وهي استخدام الوحدة send2trash الخارجية. يمكنك تثبيت هذه الوحدة من خلال تشغيل الأمر pip install --user send2trash من نافذة الطرفية Terminal. يُعَد استخدام الوحدة send2trash أكثر أمانًا من دوال الحذف العادية الخاصة بلغة بايثون، لأنها سترسل المجلدات والملفات إلى سلة المهملات أو سلة المحذوفات الخاصة بحاسوبك بدلًا من حذفها نهائيًا. إذا أدّى خطأٌ ما في برنامجك إلى حذفٍ شيءٍ باستخدام الوحدة send2trash ولا تريد حذفه، فيمكنك استعادته من سلة المحذوفات لاحقًا. أدخِل مثلًا ما يلي في الصدفة التفاعلية بعد تثبيت الوحدة send2trash: >>> import send2trash >>> beefFile = open('beef.txt', 'a') # إنشاء الملف >>> beefFile.write('Beef is not a vegetable.') 25 >>> beefFile.close() >>> send2trash.send2trash('beef.txt') يجب دائمًا أن تستخدم الدالة send2trash.send2trash() لحذف الملفات والمجلدات، ولكن بالرغم من أن إرسال الملفات إلى سلة المحذوفات يتيح لك استعادتها لاحقًا، إلّا أنه لن يؤدي إلى تحرير مساحةٍ من القرص الصلب كما يفعل الحذف النهائي، لذا إذا أردتَ أن يحرّر برنامجك مساحةً من القرص الصلب، فاستخدم دوال الوحدتين os و shutil لحذف الملفات والمجلدات. لاحظ أن الدالة send2trash() يمكنها إرسال الملفات إلى سلة المحذوفات فقط، ولا يمكنها سحب الملفات منها. المرور على شجرة مجلدات لنفترض أنك تريد إعادة تسمية كل ملف في مجلدٍ ما وكل ملفٍ في كل مجلدٍ فرعي من هذا المجلد، وهذا يعني أنك تريد المرور على شجرة المجلدات، والتفاعل مع جميع الملفات أثناء المرور عليها. قد تكون كتابة برنامجٍ لذلك أمرًا صعبًا، ولكن توفّر بايثون الدالة os.walk() للتعامل مع هذه العملية نيابةً عنك. أولًا، لنلقِ نظرة على المجلد C:\delicious ومحتوياته كما هو موضح في الشكل التالي: مثال لمجلد يحتوي على ثلاثة مجلدات وأربعة ملفات إليك فيما يلي مثال لبرنامج يستخدم الدالة os.walk() مع شجرة المجلدات من الشكل السابق: import os for folderName, subfolders, filenames in os.walk('C:\\delicious'): print('The current folder is ' + folderName) for subfolder in subfolders: print('SUBFOLDER OF ' + folderName + ': ' + subfolder) for filename in filenames: print('FILE INSIDE ' + folderName + ': '+ filename) print('') نمرّر قيمة سلسلة نصية واحدة تمثّل مسار المجلد إلى الدالة os.walk() التي يمكنك استخدامها في تعليمة حلقة for للمرور على شجرة المجلدات، حيث يشبه ذلك استخدام الدالة range() للمرور على مجالٍ من الأعداد، ولكن ستعيد الدالة os.walk() ثلاث قيم في كل تكرار من هذه الحلقة، وهذه القيم هي: سلسلة نصية تمثّل اسم المجلد الحالي. قائمة من السلاسل النصية التي تمثّل المجلدات الموجودة في المجلد الحالي. قائمة من السلاسل النصية التي تمثّل الملفات الموجودة في المجلد الحالي. ملاحظة: المجلد الحالي هو المجلد الخاص بالتكرار الحالي لحلقة for، ولم تغيّر الدالة os.walk() مجلد العمل الحالي للبرنامج. يمكنك اختيار اسم المتغير i في شيفرة for i in range(10):، ويمكنك أيضًا اختيار أسماء المتغيرات للقيم الثلاث المذكورة سابقًا، ولكننا سنستخدم أسماء المتغيرات foldername و subfolders و filenames في أغلب الأحيان. إذا شغّلنا البرنامج، فسينتج ما يلي: The current folder is C:\delicious SUBFOLDER OF C:\delicious: cats SUBFOLDER OF C:\delicious: walnut FILE INSIDE C:\delicious: spam.txt The current folder is C:\delicious\cats FILE INSIDE C:\delicious\cats: catnames.txt FILE INSIDE C:\delicious\cats: zophie.jpg The current folder is C:\delicious\walnut SUBFOLDER OF C:\delicious\walnut: waffles The current folder is C:\delicious\walnut\waffles FILE INSIDE C:\delicious\walnut\waffles: butter.txt. تعيد الدالة os.walk() قوائمًا من السلاسل النصية التي تمثّل المتغيرات subfolder و filename، لذا يمكنك استخدام هذه القوائم في حلقات for الخاصة بها. ضع شيفرتك البرمجية مكان استدعاءات الدالة print()، أو احذف حلقتي for إن لم تكن بحاجة إليهما. ضغط الملفات باستخدام الوحدة zipfile قد تكون على دراية بالملفات المضغوطة ZIP ذات امتداد الملف .zip، والتي يمكنها الاحتفاظ بالمحتويات المضغوطة للعديد من الملفات الأخرى، حيث يؤدي ضغط الملف إلى تقليل حجمه، ويُعَد ذلك أمرًا مفيدًا عند نقله عبر الإنترنت. يمكن أن يحتوي ملف ZIP أيضًا على ملفات ومجلدات فرعية متعددة، لذا تُعَد طريقة سهلة لحَزم ملفات متعددة في ملف واحد، ويمكن بعد ذلك مثلًا إرفاق هذا الملف الذي يسمّى ملف الأرشفة Archive File مع رسالة بريد إلكتروني. يمكن لبرامج بايثون الخاص بك إنشاء ملفات ZIP وفتحها (أو فك ضغطها Extract) باستخدام الدوال الموجودة في الوحدة zipfile. لنفترض أن لديك ملف ZIP بالاسم example.zip ويحتوي على المحتويات الموضّحة في الشكل التالي: محتويات الملف example.zip يمكنك تنزيل هذا الملف من موقع nostarch أو المتابعة باستخدام ملف ZIP موجود مسبقًا على حاسوبك. قراءة الملفات المضغوطة ZIP يمكنك قراءة محتويات ملف مضغوط ZIP من خلال إنشاء كائن ZipFile أولًا (لاحظ الأحرف الكبيرة Z و F)، حيث تتشابه كائنات ZipFile مع كائنات File التي تعيدها الدالة open()، فهي قيم يتفاعل البرنامج من خلالها مع الملف. ننشئ كائن ZipFile من خلال استدعاء الدالة zipfile.ZipFile() وتمرير سلسلة نصية تمثّل اسم ملف .ZIP إليها. لاحظ أن zipfile هو اسم وحدة بايثون، وأن ZipFile() هو اسم الدالة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import zipfile, os >>> from pathlib import Path >>> p = Path.home() >>> exampleZip = zipfile.ZipFile(p / 'example.zip') >>> exampleZip.namelist() ['spam.txt', 'cats/', 'cats/catnames.txt', 'cats/zophie.jpg'] >>> spamInfo = exampleZip.getinfo('spam.txt') >>> spamInfo.file_size 13908 >>> spamInfo.compress_size 3828 ➊ >>> f'Compressed file is {round(spamInfo.file_size / spamInfo .compress_size, 2)}x smaller!' ) 'Compressed file is 3.63x smaller!' >>> exampleZip.close() يحتوي كائن ZipFile على التابع namelist() الذي يعيد قائمةً من السلاسل النصية التي تمثّل جميع الملفات والمجلدات الموجودة في ملف ZIP، حيث يمكن تمرير هذه السلاسل النصية إلى التابع getinfo() لإعادة كائن ZipInfo لهذا الملف المحدّد. تمتلك كائنات ZipInfo سماتها Attributes الخاصة مثل السمات file_size و compress_size بالبايتات، والتي تحتوي على أعداد صحيحة لحجم الملف الأصلي وحجم الملف المضغوط على التوالي. يمثل كائن ZipInfo ملف أرشفة كامل، ولكن يحمل كائن ZipInfo أيضًا معلومات مفيدة حول ملف واحد في ملف الأرشفة. يحسب الأمر الموجود في التعليمة ➊ مدى كفاءة ضغط الملف example.zip من خلال قسمة حجم الملف الأصلي على حجم الملف المضغوط ويطبع هذه المعلومات. فك ضغط ملفات ZIP يفك التابع extractall() الخاص بكائنات ZipFile ضغط جميع الملفات والمجلدات من ملف مضغوط ZIP إلى مجلد العمل الحالي. >>> import zipfile, os >>> from pathlib import Path >>> p = Path.home() >>> exampleZip = zipfile.ZipFile(p / 'example.zip') ➊ >>> exampleZip.extractall() >>> exampleZip.close() يؤدي تشغيل الشيفرة البرمجية السابقة إلى فَك ضغط محتويات الملف example.zip في المجلد C:\. يمكنك اختياريًا تمرير اسم مجلد إلى التابع extractall() لفك ضغط الملفات في مجلد آخر مختلفٍ عن مجلد العمل الحالي، وإذا كان المجلد الذي مرّرناه إلى التابع extractall() غير موجود، فسيُشَأ هذا المجلد، فمثلًا إذا وضعتَ الاستدعاء exampleZip.extractall('C:\\delicious') مكان الاستدعاء ➊، فستفك الشيفرة البرمجية ضغط الملفات من الملف example.zip إلى المجلد C:\delicious الذي أنشأناه. يفك التابع extract() الخاص بكائنات ZipFile ضغط ملفٍ واحد من الملف المضغوط ZIP. تابع مثال الصدفة التفاعلية بما يلي: >>> exampleZip.extract('spam.txt') 'C:\\spam.txt' >>> exampleZip.extract('spam.txt', 'C:\\some\\new\\folders') 'C:\\some\\new\\folders\\spam.txt' >>> exampleZip.close() يجب أن تتطابق السلسلة النصية التي تمررها إلى التابع extract() مع إحدى السلاسل النصية الموجودة في القائمة التي يعيدها التابع namelist()، ويمكنك اختياريًا تمرير وسيطٍ ثانٍ إلى التابع extract() لفك ضغط الملف في مجلد آخر مختلف عن مجلد العمل الحالي، حيث إذا كان هذا الوسيط الثاني مجلدًا غير موجودٍ بعد، فستنشِئ شيفرة بايثون هذا المجلد. القيمة التي يعيدها التابع extract() هي المسار المطلق الذي فكينا ضغط الملف فيه. إنشاء ملفات ZIP والإضافة إليها يمكنك إنشاء ملفات ZIP المضغوطة من خلال فتح كائن ZipFile في وضع الكتابة مع تمرير 'w' كوسيط ثانٍ، حيث يشبه ذلك فتح ملفٍ نصي في وضع الكتابة من خلال تمرير 'w' إلى الدالة open(). إذا مرّرتَ مسارًا إلى التابع write() مع كائن ZipFile، ستضغط شيفرة بايثون الملف الموجود في هذا المسار وتضيفه إلى ملف ZIP. الوسيط الأول للتابع write() هو سلسلة نصية تمثّل اسم الملف المراد إضافته، والوسيط الثاني هو معامل يمثّل نوع عملية الضغط، إذ يخبر هذا النوع الحاسوبَ بالخوارزمية التي يجب أن يستخدمها لضغط الملفات، حيث يمكنك دائمًا ضبط هذه القيمة على zipfile.ZIP_DEFLATED التي تحدّد خوارزمية الضغط Deflate التي تعمل على جميع أنواع البيانات. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import zipfile >>> newZip = zipfile.ZipFile('new.zip', 'w') >>> newZip.write('spam.txt', compress_type=zipfile.ZIP_DEFLATED) >>> newZip.close() ستؤدي الشيفرة البرمجية السابقة إلى إنشاء ملف ZIP جديد بالاسم new.zip، حيث يحتوي هذا الملف على محتويات مضغوطة للملف spam.txt. ضع في بالك أن وضع الكتابة سيؤدي إلى مسح جميع المحتويات الموجودة مسبقًا في ملف ZIP كما هو الحال مع الكتابة في الملفات. إذا أردتَ ببساطة إضافة ملفات إلى ملف ZIP موجود مسبقًا، فمرّر 'a' كوسيطٍ ثانٍ إلى الدالة zipfile.ZipFile() لفتح ملف ZIP في وضع الإلحاق Append Mode. تطبيق عملي: إعادة تسمية الملفات ذات تواريخ النمط الأمريكي إلى تواريخ النمط الأوروبي لنفترض أن مديرك في العمل يرسل إليك عبر البريد الإلكتروني آلاف الملفات ذات تواريخ النمط الأمريكي (MM-DD-YYYY) الموجودة في أسماء هذه الملفات ويريد إعادة تسميتها إلى تواريخ النمط الأوروبي (DD-MM-YYYY)، إذ قد يستغرق إنجاز هذه المهمة المملة يدويًا وقتًا طويلًا، إذًا لنكتب برنامجًا ينّفذ هذه المهمة نيابةً عنك. إليك الخطوات التي يفعلها هذا البرنامج: البحث في جميع أسماء الملفات الموجودة في مجلد العمل الحالي عن التواريخ ذات النمط الأمريكي. إعادة تسمية الملف مع التبديل بين الشهر واليوم لجعله على النمط الأوروبي عند العثور على أحد هذه الملفات. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: إنشاء تعبير نمطي Regex يمكنه تحديد نمط النص للتواريخ ذات النمط الأمريكي. استدعاء التابع os.listdir() للعثور على جميع الملفات الموجودة في مجلد العمل. المرور ضمن حلقة على جميع أسماء الملفات باستخدام التعبير النمطي للتحقق من احتوائه على تاريخ. إذا احتوى اسم الملف على تاريخ، فيجب إعادة تسمية الملف باستخدام الدالة shutil.move(). افتح نافذةً جديدة في محرّرك لإنشاء ملف جديد للمشروع واحفظ شيفرتك البرمجية بالاسم renameDates.py. الخطوة الأولى: إنشاء تعبير نمطي للتواريخ ذات النمط الأمريكي سيحتاج الجزء الأول من البرنامج إلى استيراد الوحدات الضرورية وإنشاء تعبير نمطي يمكنه تحديد تواريخ النمط الأمريكي MM-DD-YYYY. ستذكرك تعليقات TODO في النهاية بما تبقى لتكتبه في هذا البرنامج، حيث كتبناها ليسهل عليك العثور عليها باستخدام ميزة البحث Ctrl-F في محرّر Mu. اجعل شيفرتك البرمجية تبدو كما يلي: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY ➊ import shutil, os, re # إنشاء تعبير نمطي يطابق الملفات ذات تنسيق التاريخ الأمريكي ➋ datePattern = re.compile(r"""^(.*?) # كل النص قبل التاريخ ((0|1)?\d)- # رقم أو رقمين للشهر ((0|1|2|3)?\d)- # رقم أو رقمين لليوم ((19|20)\d\d) # أربعة أرقام للسنة (.*?)$ # كل النص بعد التاريخ """, re.VERBOSE➌) # TODO: المرور ضمن حلقة على الملفات الموجودة في مجلد العمل # TODO: تخطي الملفات التي تكون بدون تاريخ # TODO: الحصول على الأجزاء المختلفة من اسم الملف # TODO: تشكيل اسم الملف على النمط الأوروبي # TODO: الحصول على مسارات الملفات الكاملة والمطلقة # TODO: إعادة تسمية الملفات تعلّمنا في هذا المقال أنه يمكن استخدام الدالة shutil.move() لإعادة تسمية الملفات، ووسطاؤها هي اسم الملف المراد إعادة تسميته واسم الملف الجديد، ويجب استيراد الوحدة shutil ➊ بسبب وجود هذه الدالة فيها. يجب تحديد الملفات التي تريد إعادة تسميتها قبل إعادة تسميتها، إذ يجب إعادة تسمية أسماء الملفات التي لها تواريخ مثل spam4-4-1984.txt و 01-03-2014eggs.zip، بينما يمكن تجاهل أسماء الملفات التي لا تحتوي على تواريخ مثل littlebrother.epub. يمكنك استخدام تعبير نمطي لتحديد هذا النمط، لذا استدعِ التابع re.compile() لإنشاء كائن Regex ➋ بعد استيراد الوحدة re في البداية. سيسمح تمرير القيمة re.VERBOSE للوسيط الثاني ➌ بوجود المسافات البيضاء والتعليقات في السلسلة النصية للتعبير النمطي لجعلها أكثر قابلية للقراءة. تبدأ السلسلة النصية للتعبير النمطي بالمحارف ^(.*?) لمطابقة أيّ نصٍ موجود في بداية اسم الملف الذي قد يأتي قبل التاريخ. تطابق المجموعة ((0|1)?\d) الشهر، حيث يمكن أن يكون الرقم الأول إما 0 أو 1، وبالتالي يطابق التعبير النمطي القيمةَ 12 للشهر 12 ويطابق القيمة 02 للشهر الثاني، حيث يكون هذا الرقم اختياريًا أيضًا بحيث يمكن أن يكون الشهر 04 أو 4 للشهر الرابع. مجموعة اليوم هي ((0|1|2|3)?\d) وتتبع منطقًا مشابهًا لمجموعة الشهر، حيث تُعَد القيم 3 و 03 و 31 أرقامًا صالحة للأيام. لاحظ أن هذا التعبير النمطي سيقبل بعض التواريخ غير الصالحة مثل 4-31-2022 و 2-29-2023 و 0-15-2024، إذ تحتوي التواريخ على الكثير من الحالات الخاصة التي يمكن أن نخطئ بها بسهولة، ولكن يعمل التعبير النمطي في هذا البرنامج جيدًا بما فيه الكفاية للتبسيط. تُعَد السنة 1885 سنةً صالحة، ولكن يمكنك فقط البحث عن السنوات في القرن العشرين أو الحادي والعشرين، مما سيؤدي إلى منع برنامجك من مطابقة أسماء الملفات التي لها تنسيق مشابه للتاريخ ولكنها لا تمثّل تواريخًا مثل 10-10-1000.txt عن طريق الخطأ. أخيرًا، يتطابق الجزء $(?*.) من التعبير النمطي مع أيّ نص يأتي بعد التاريخ. الخطوة الثانية: تحديد أجزاء التاريخ من أسماء الملفات يجب بعد ذلك أن يمر البرنامج ضمن حلقة على قائمة السلاسل النصية لأسماء الملفات التي يعيدها التابع os.listdir()، وأن يطابقها مع التعبير النمطي. يجب تخطي أيّ ملفات لا تتضمن تاريخًا في اسمها، وسيُخزَّن النص المطابق في عدة متغيرات بالنسبة لأسماء الملفات التي تحتوي على تاريخ فيها. املأ المهام TODO الثلاثة الأولى في برنامجك بالشيفرة البرمجية التالية: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY --snip-- # المرور ضمن حلقة على الملفات الموجودة في مجلد العمل for amerFilename in os.listdir('.'): mo = datePattern.search(amerFilename) # تخطي الملفات التي تكون بدون تاريخ ➊ if mo == None: ➋ continue ➌ # الحصول على الأجزاء المختلفة من اسم الملف beforePart = mo.group(1) monthPart = mo.group(2) dayPart = mo.group(4) yearPart = mo.group(6) afterPart = mo.group(8) --snip– إذا كانت قيمة كائن Match الذي يعيده التابع search() هي None ➊، فلن يتطابق اسم الملف الموجود في المتغير amerFilename مع التعبير النمطي، وستتخطى تعليمة continue ➋ بقية الحلقة وتنتقل إلى اسم الملف التالي، وإلّا فستُخزَّن السلاسل النصية المختلفة المطابِقة مع مجموعات التعبير النمطي في متغيرات بالاسم beforePart و monthPart و dayPart و yearPart و afterPart ➌. ستُستخدَم السلاسل النصية الموجودة في هذه المتغيرات لتشكيل اسم الملف على النمط الأوروبي في الخطوة التالية. أبقِ أرقام المجموعة سهلة الاستخدام من خلال محاولة قراءة التعبير النمطي من البداية وحساب كل مرة يظهر فيها قوس مفتوح. اكتب مخططًا تفصيليًا للتعبير النمطي دون التفكير في الشيفرة البرمجية، حيث يمكن أن يساعدك المثال التالي في تصوّر هذه المجموعات: datePattern = re.compile(r"""^(1) # كل النص قبل التاريخ (2 (3) )- # رقم أو رقمين للشهر (4 (5) )- # رقم أو رقمين لليوم (6 (7) ) # أربعة أرقام للسنة (8)$ # كل النص بعد التاريخ """, re.VERBOSE) تمثل الأرقام من 1 إلى 8 في المثال السابق المجموعات في التعبير النمطي الذي كتبته. يمكن أن يمنحكَ إنشاء مخطط تفصيلي للتعبير النمطي باستخدام الأقواس وأرقام المجموعات فقط فهمًا أوضح لتعبيرك النمطي قبل الانتقال إلى بقية البرنامج. الخطوة الثالثة: تشكيل اسم الملف الجديد وإعادة تسمية الملفات جرّب سَلسَلة السلاسل النصية الموجودة في المتغيرات من الخطوة السابقة مع التاريخ ذي النمط الأوروبي، حيث يأتي اليوم قبل الشهر. املأ المهام TODO الثلاثة المتبقية في برنامجك بالشيفرة البرمجية التالية: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY --snip-- # تشكيل اسم الملف على النمط الأوروبي ➊ euroFilename = beforePart + dayPart + '-' + monthPart + '-' + yearPart + afterPart # الحصول على مسارات الملفات الكاملة والمطلقة absWorkingDir = os.path.abspath('.') amerFilename = os.path.join(absWorkingDir, amerFilename) euroFilename = os.path.join(absWorkingDir, euroFilename) # إعادة تسمية الملفات ➋ print(f'Renaming "{amerFilename}" to "{euroFilename}"...') ➌ #shutil.move(amerFilename, euroFilename) # ألغِ التعليق بعد الاختبار خزّن السلسلة النصية المتسلسلة في متغير بالاسم euroFilename ➊، ثم مرّر اسم الملف الأصلي الموجود في المتغير amerFilename والمتغير euroFilename الجديد إلى الدالة shutil.move() لإعادة تسمية الملف ➌. يتضمن هذا البرنامج تعليقًا على استدعاء الدالة shutil.move()، حيث يطبع أسماء الملفات التي ستُعاد تسميتها ➋. يمكن أن يتيح لك تشغيل البرنامج بهذه الطريقة أولًا التحققَ من إعادة تسمية الملفات بطريقة صحيحة، ثم يمكنك إلغاء تعليق استدعاء الدالة shutil.move() وتشغيل البرنامج مرة أخرى لإعادة تسمية الملفات فعليًا. أفكار لبرامج مماثلة هناك العديد من الأسباب الأخرى التي قد تجعلك ترغب في إعادة تسمية عدد كبير من الملفات مثل: إضافة بادئة إلى بداية اسم الملف مثل إضافة spam_ لإعادة تسمية الملف eggs.txt إلى الاسم spam_eggs.txt. تغيير أسماء الملفات التي تحتوي على تواريخ ذات نمط أوروبي إلى تواريخ ذات نمط الأمريكي. حذف الأصفار من أسماء الملفات مثل spam0042.txt. تطبيق عملي: إنشاء نسخة احتياطية لمجلد في ملف مضغوط ZIP لنفترض أنك تعمل على مشروع تحتفظ بملفاته في مجلد بالاسم C:\AlsPythonBook، ولا بد أنك قلق بشأن فقدان عملك، لذا سترغب في إنشاء "لقطات" من ملفات ZIP للمجلد بأكمله، إذ قد ترغب في الاحتفاظ بنسخ مختلفة، لذلك يجب أن يزيد اسم ملف ZIP في كل مرة تنشئ فيها نسخة مثل AlsPythonBook_1.zip و AlsPythonBook_2.zip و AlsPythonBook_3.zip وإلخ. يمكنك إنجاز ذلك يدويًا، ولكنه أمر مزعج إلى حدٍ ما، وقد تخطئ في ترقيم أسماء ملفات ZIP، فمن الأسهل تشغيل برنامج ينجز هذه المهمة المملة نيابةً عنك. افتح نافذة جديدة في محرّرك لإنشاء ملف جديد لهذا المشروع واحفظه بالاسم backupToZip.py. الخطوة الأولى: اكتشاف اسم الملف المضغوط ZIP سنضع الشيفرة البرمجية الخاصة بهذا البرنامج في دالة اسمها backupToZip()، حيث سيؤدي ذلك إلى تسهيل نسخ الدالة ولصقها في برامج بايثون الأخرى التي تحتاج إليها. ستُستدعَى هذه الدالة لإجراء النسخ الاحتياطي في نهاية البرنامج، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها ➊ import zipfile, os def backupToZip(folder): # إنشاء نسخة احتياطية من محتويات المجلد بالكامل في ملف ZIP folder = os.path.abspath(folder) # التأكد من أن المجلد مسار مطلق # اكتشاف اسم الملف الذي يجب أن تستخدمه هذه الشيفرة البرمجية بناءً على الملفات الموجودة مسبقًا ➋ number = 1 ➌ while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): break number = number + 1 ➍ # TODO: إنشاء ملف مضغوط ZIP # TODO: المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد print('Done.') backupToZip('C:\\delicious') أضِف أولًا سطر Shebang (الذي يبدأ بالسلسلة النصية !#) مع وصف ما يفعله البرنامج، ثم استورد وحدات zipfile و os ➊. عرّف بعد ذلك دالة بالاسم backupToZip()، حيث تأخذ هذه الدالة معاملًا واحدًا فقط هو folder، والذي هو سلسلة نصية تمثّل مسارًا إلى المجلد الذي يجب نسخ محتوياته احتياطيًا. ستحدّد هذه الدالة اسم الملف المُستخدَم لملف ZIP الذي ستنشئه، ثم تنشئ هذه الدالة الملف، وتمر على المجلد folder، وتضيف كلًا من المجلدات الفرعية والملفات إلى ملف ZIP. اكتب تعليقات TODO لهذه الخطوات في الشيفرة البرمجية لتذكير نفسك بإنجازها لاحقًا ➍. يستخدم الجزء الأول -الذي يمثّل تسمية الملف ZIP- الاسم الأساسي للمسار المطلق للمجلد folder. إذا كان المجلد الذي ننسخه احتياطيًا هو C:\delicious، فيجب أن يكون اسم الملف ZIP هو delicious_N.zip، حيث N = 1 هي المرة الأولى التي نشغّل فيها البرنامج و N = 2 هي المرة الثانية وإلخ. يمكنك تحديد ما يجب أن تكون عليه قيمة N من خلال التحقق مما إذا كان الملف delicious1.zip موجودًا مسبقًا، ثم التحقق مما إذا كان الملف delicious2.zip موجودًا مسبقًا وإلخ. استخدم متغيرًا اسمه number لتمثيل N ➋، واستمر في زيادته ضمن الحلقة التي تستدعي التابع os.path.exists() للتحقق من وجود الملف ➌. سيؤدي العثور على أول اسم ملف غير موجود إلى كسر الحلقة باستخدام التعليمة break، لأنها عثرت على اسم الملف المضغوط الجديد. الخطوة الثانية: إنشاء ملف مضغوط ZIP جديد لننشئ الآن ملف ZIP، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها --snip-- while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): break number = number + 1 # إنشاء ملف مضغوط ZIP print(f'Creating {zipFilename}...') ➊ backupZip = zipfile.ZipFile(zipFilename, 'w') # TODO: المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد print('Done.') backupToZip('C:\\delicious') خزّنا اسم ملف ZIP الجديد في المتغير zipFilename، ويمكننا الآن استدعاء الدالة zipfile.ZipFile() لإنشاء ملف ZIP فعليًا ➊. تأكد من تمرير 'w' كوسيطٍ ثانٍ لهذه الدالة لفتح الملف ZIP في وضع الكتابة. الخطوة الثالثة: المرور على شجرة المجلدات والإضافة إلى الملف المضغوط ZIP يجب الآن أن تستخدم الدالة os.walk() لسرد كل ملف موجود في المجلد ومجلداته الفرعية، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها --snip-- # المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد ➊ for foldername, subfolders, filenames in os.walk(folder): print(f'Adding files in {foldername}...') # إضافة المجلد الحالي إلى ملف ZIP ➋ backupZip.write(foldername) # إضافة كافة الملفات الموجودة في هذا المجلد إلى ملف ZIP ➌ for filename in filenames: newBase = os.path.basename(folder) + '_' if filename.startswith(newBase) and filename.endswith('.zip'): continue # لا تنشئ نسخة احتياطية من ملفات ZIP الاحتياطية backupZip.write(os.path.join(foldername, filename)) backupZip.close() print('Done.') backupToZip('C:\\delicious') يمكنك استخدام الدالة os.walk() في حلقة for ➊، حيث ستعيد في كل تكرار اسم المجلد الحالي لهذا التكرار والمجلدات الفرعية الموجودة في هذا المجلد وأسماء الملفات الموجودة في هذا المجلد. يُضاف المجلد إلى ملف ZIP ➋ في حلقة for، ويمكن لحلقة for المتداخلة المرور على كل اسم ملف في القائمة filenames ➌، ويُضاف كل منها إلى ملف ZIP باستثناء ملفات ZIP الاحتياطية التي أنشأناها مسبقًا. سينتج ما يلي عند تشغيل هذا البرنامج: Creating delicious_1.zip... Adding files in C:\delicious... Adding files in C:\delicious\cats... Adding files in C:\delicious\waffles... Adding files in C:\delicious\walnut... Adding files in C:\delicious\walnut\waffles... Done. سيضع هذا البرنامج جميع الملفات الموجودة في المجلد C:\delicious في ملف ZIP بالاسم delicious_2.zip في المرة الثانية لتشغيله وهكذا. أفكار لبرامج مماثلة يمكنك الاستفادة من فكرة المرور على شجرة المجلدات وإضافة الملفات إلى ملفات الأرشفة ZIP المضغوطة في العديد من البرامج الأخرى، فمثلًا يمكنك كتابة البرامج التي تنجز المهام التالية: المرور على شجرة المجلدات وأرشفة الملفات التي لها امتدادات محددة فقط مثل .txt أو .py. المرور على شجرة المجلدات وأرشفة جميع الملفات باستثناء ملفات .txt و .py. البحث عن المجلد في شجرة المجلدات الذي يحتوي على أكبر عدد من الملفات أو المجلد الذي يستخدم أكبر مساحة على القرص الصلب. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لإنشاء نسخة انتقائية للملفات من شجرة المجلدات اكتب برنامجًا يمر على شجرة المجلدات ويبحث عن الملفات التي لها امتداد ملف محدّد (مثل .pdf أو .jpg)، وانسخ هذه الملفات من أيّ موقع توجد فيه في مجلد جديد. برنامج لحذف الملفات غير الضرورية يمكن أن تَشغَل بعض الملفات أو المجلدات غير الضرورية والضخمة الجزء الأكبر من المساحة على قرص حاسوبك الصلب، فإذا أدرتَ تحرير مساحةٍ على حاسوبك، فستحصل على أقصى استفادة من خلال حذف أكبر عدد ممكن من الملفات غير المرغوب فيها، ولكن يجب أولًا العثور عليها. اكتب برنامجًا يمر على شجرة المجلدات ويبحث عن الملفات أو المجلدات الكبيرة استثنائيًا مثل الملفات أو المجلدات التي يزيد حجم ملفها عن 100 ميجابايت، حيث يمكنك استخدام الدالة os.path.getsize() من وحدة os للحصول على حجم الملف. اطبع هذه الملفات مع مسارها المطلق على الشاشة. ملء الفجوات في ترقيم أسماء الملفات اكتب برنامجًا يبحث عن جميع الملفات ذات البادئة المُحدَّدة -مثل spam001.txt و spam002.txt وإلخ- في مجلدٍ واحد، ويحدّد هذا البرنامج موقع أيّ فجوات في الترقيم مثل وجود الملفين spam001.txt و spam003.txt دون وجود الملف spam002.txt، لذا اجعل البرنامج يعيد تسمية جميع الملفات اللاحقة لسد هذه الفجوة. اكتب أيضًا برنامجًا آخر يمكنه إدراج فجوات في الملفات المرقّمة بحيث يمكن إضافة ملف جديد. الخلاصة يُحتمَل أنك تتعامل مع الملفات يدويًا باستخدام الفأرة ولوحة المفاتيح حتى لو كنت من مستخدمي الحاسوب ذوي الخبرة. تسهّل مستكشفات الملفات الحديثة العمل مع عددٍ من الملفات، ولكن ستحتاج في بعض الأحيان إلى تنفيذ مهمة قد تستغرق ساعات باستخدام مستكشف الملفات الخاص بحاسوبك. توفّر وحدتا os و shutil دوالًا لنسخ الملفات ونقلها وإعادة تسميتها وحذفها، ولكن قد ترغب في استخدام وحدة send2trash عند حذف الملفات لنقلها إلى سلة المحذوفات أو سلة المهملات بدلًا من حذفها نهائيًا. يُفضَّل تعليق الشيفرة البرمجية التي تنسخ أو تنقل أو تعيد التسمية أو تحذف الملفات فعليًا عند كتابة البرامج التي تتعامل مع الملفات، ويجب إضافة استدعاء الدالة print() حتى تتمكّن من تشغيل البرنامج والتحقق مما سيفعله بالضبط. يجب في أغلب الأحيان تنفيذ هذه العمليات على الملفات الموجودة في أحد المجلدات وعلى كل مجلد موجود في هذا المجلد وعلى كل مجلد موجود في تلك المجلدات وإلخ. تتولى الدالة os.walk() هذه الرحلة عبر المجلدات نيابةً عنك حتى تتمكّن من التركيز على ما يحتاج برنامجك إلى فعله مع الملفات الموجودة في هذه المجلدات. تمنحك وحدة zipfile طريقةً لضغط وفك ضغط الملفات في أرشيفات .ZIP باستخدام لغة بايثون. تسهّل الوحدة zipfile -مع دوال معالجة الملفات الخاصة بوحدتي os و shutil- تجميعَ العديد من الملفات من أيّ مكان على قرص حاسوبك الصلب. يُعَد رفع هذه الملفات المضغوطة ZIP على مواقع الويب أو إرسالها بوصفها مرفقات في البريد الإلكتروني أسهلَ بكثير من العديد من الملفات المنفصلة. وفرنا في هذه السلسلة من المقالات شيفرة برمجية يمكنك نسخها ولصقها في برنامجك، ولكن يُحتمَل ألّا تظهر بمظهرٍ مثالي في البداية. يركز المقال التالي على بعض وحدات بايثون التي ستساعدك على تحليل برامجك وتنقيح أخطائها لتتمكّن من تشغيلها بصورة صحيحة بسرعة. ترجمة -وبتصرُّف- للمقال Organizing Files لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: قراءة وكتابة الملفات باستخدام لغة بايثون python التعامل مع الملفات والمسارات في بايثون كيفية التعامل مع الملفات النصية في بايثون 3 مشاريع بايثون عملية تناسب المبتدئين
-
يمكننا تخزين المعلومات في المتغيرات في برنامجنا وستبقى موجودة طالما استمر تشغيل البرنامج، لكنا ماذا لو أردنا الحفاظ على البيانات بعد انتهاء تنفيذ البرنامج؟ سنحتاج إلى حفظها إلى ملف؛ وسنتعلم في هذا المقال كيفية استخدام بايثون لإنشاء وقراءة وكتابة الملفات في حاسوبنا. الملفات ومساراتها يملك كل ملف خاصيتان أساسيتان: اسم الملف ومساره. يحدد مسار الملف أين سيظهر في حاسوبك، فمثلًا هنالك ملف في حاسوبي الذي يعمل بنظام ويندوز موجود اسمه project.docx في المسار C:\Users\Al\Documents. الجزء الذي يلي اسم الملف ويأتي بعد النقطة يسمى بامتداد الملف file extension ويخبرنا ما هو نوع الملف، فمثلًا الملف project.docx هو مستند وورد؛ بينما تشير Users و Al و Documents إلى مجلدات. يمكن أن تحتوي المجلدات على ملفات ومجلدات أخرى، فمثلًا الملف project.docx موجود في المجلد Documents الذي بدوره موجود في المجلد Al الموجود في المجلد Users. يوضح الشكل الآتي هذه البنية. الشكل 1: ملف موجود في مجلد الجزءC:\ من المسار يسمى بالمجلد الجذر root، أي يحتوي على جميع المجلدات الأخرى. وهو المجلد C:\ في نظام ويندوز أو يسمى القرص C:؛ أما في ماك أو لينكس فيكون المجلد الجذر هو /. سنستعمل في هذه السلسلة نمط مسارات ويندوز لأنها أكثر شيوعًا، لكن إن كنت تستعمل ماك أو لينكس فاستعمل بنية المسارات المناسبة لنظامك. تظهر أجهزة التخزين الأخرى مثل أقراص DVD أو وسائط تخزين USB بطرائق مختلف حسب نظام التشغيل، ففي ويندوز ستظهر على شكل قرص جديد له حرف مختلف مثل D:/ أو E:/. بينما في ماك فستظهر كمجلد جديد في المجلد /Volumes، وفي لينكس ستظهر كمجلدات جديدة في المجلد /mnt (أو /media حسب التوزيعة عندك). من المهم أن تلاحظ أن أسماء الملفات والمجلدات غير حساسة لحالة الأحرف في ويندوز وماك، لكنها حساسة لحالة الأحرف في لينكس. ملاحظة: من المؤكد أن بنية المجلدات وأسماء الملفات في حاسوبك ونظام تشغيلك تختلف عمّا هو عندي، لذا لن تستطيع اتباع أمثلة هذه السلسلة حرفيًا. لكن جرب المتابعة على الملفات والمجلدات الموجودة عندك. الخط المائل الخلفي \ في ويندوز والخط المائل الأمامي / في ماك ولينكس تستعمل مسارات الملفات في أنظمة ويندوز الخط المائل الخلفي \ الذي يفصل بين أسماء المجلدات؛ أما في ماك ولينكس فيستعمل الخط المائل الأمامي / فاصلًا بين المجلدات؛ ولو أردت أن تعمل برامجك التي تكتبها على جميع الأنظمة (وهذا أمر مهم أنصحك به) فعليك أن تتعامل مع كلا الحالتين. لحسن الحظ هنالك دالة في الوحدة pathlib باسم Path()، التي نمرر إليها سلاسل نصية بأسماء المجلدات والملف المطلوب، وستعيد الدالة Path() سلسلةً نصيةً لمسار الملف يستعمل الفاصل الصحيح بين المجلدات وفقًا لنظام التشغيل: >>> from pathlib import Path >>> Path('spam', 'olive', 'eggs') WindowsPath('spam/olive/eggs') >>> str(Path('spam', 'olive', 'eggs')) 'spam\\olive\\eggs' لاحظ أن من الشائع حين استيراد pathlib أن نكتب from pathlib import Path وإلا فسنحتاج إلى كتابة pathlib.Path في كل مرة نريد استعمال Path فيها. سأشغل أمثلة هذا المقال على نظام ويندوز، لذا ستعيد الدالة Path('spam', 'olive', 'eggs') الكائن WindowsPath للمسار النهائي WindowsPath('spam/olive/eggs')؛ وصحيحٌ أن ويندوز يستعمل الخط المائل الخلفي في المسارات، لكن تمثيل الكائن WindowsPath في الطرفية التفاعلية يستعمل الخط المائل الأمامي، هذا لأن مطوري البرمجيات مفتوحة المصدر يفضلون نظام لينكس ويستعملون العادات الخاصة به أثناء التطوير. إذا أردنا الحصول على سلسلة نصية من المسار، فيمكننا تمرير الكائن WindowsPath إلى الدالة str() التي ستعيد في مثالنا السلسلة النصية 'spam\\olive\\eggs'، لاحظ أن الخطوط المائلة الخلفية مضاعفة لأن كل خط مائل خلفي يحتاج إلى خطٍ مائل خلفي آخر لتهريبه. إذا استخدمنا الدالة السابقة في نظام لينكس فستعيد كائن PosixPath، الذي حين تمريره إلى الدالة str() فسيعيد السلسلة النصية 'spam/olive/eggs' (كلمة POSIX تشير إلى مجموعة من المعايير الحاكمة للأنظمة الشبيهة بيونكس Unix-like مثل لينكس، إذا لم تجرب لينكس من قبل فأنصحك وبشدة أن تجربه). يمكن تمرير كائنات Path (سواءً كانت WindowsPath أو PosixPath اعتمادًا على نظام تشغيلك) إلى دوال أخرى متعلقة بالتعامل مع الملفات والتي سنشرحها خلال هذا المقال. المثال الآتي يولد مجموعة من مسارات الملفات في أحد المجلدات: >>> from pathlib import Path >>> myFiles = ['accounts.txt', 'details.csv', 'invite.docx'] >>> for filename in myFiles: print(Path(r'C:\Users\Al', filename)) C:\Users\Al\accounts.txt C:\Users\Al\details.csv C:\Users\Al\invite.docx يفصل الخط المائل الخلفي بين المجلدات في ويندوز، لذا لا يمكنك استخدامه في أسماء الملفات، لكنك تستطيع استخدام الخطوط المائلة الخلفية \ في ماك ولينكس، فبينما يشير المسار Path(r'spam\eggs') إلى مجلدين مختلفين (أو الملف eggs في المجلد spam) في ويندوز، لكنه يشير إلى مجلد أو ملف باسم spam\eggs في ماك ولينكس. لهذا السبب من المستحسن استخدام الخطوط المائلة الأمامية / في شيفرات بايثون دومًا، وسنفعل المثل في أمثلة المقال، وستضمن لنا الوحدة pathlib أن المسارات التي نستخدمها تعمل على جميع أنظمة التشغيل. لاحظ أن الوحدة pathlib جديدة في بايثون 3.4 وأتت لتستبدل دوال os.path القديمة؛ وتدعمها دوال المكتبة القياسية في بايثون بدءًا من الإصدار 3.6. إذا كنت تعمل مع سكربتات مكتوبة بإصدار بايثون 2 فأنصحك أن تستعمل الوحدة pathlib2 التي توفر إمكانية pathlib في بايثون 2.7. يشرح المقال 1 خطوات تثبيت pathlib2 باستخدام pip. سأوضح أي اختلافات واستخدامات للوحدة os حين الحاجة، فقد تستفيد منها حين قراءة السكربتات القديمة أو التي كتبها غيرك. استخدام العامل / لجمع المسارات نستخدم العامل + عادةً لجمع عددين كما في التعبير 2 + 2، الذي ينتج القيمة العددية 4، لكن يمكننا أيضًا استخدام العامل + لجمع سلسلتين نصيتين كما في التعبير 'Hello' + 'World' الذي ينتج السلسلة النصية 'HelloWorld'. وبشكل مشابه يستعمل العامل / للقسمة لكن يمكنه أيضًا أن يجمع بين كائنات Path والسلاسل النصية، وهو يفيد في التعامل مع كائنات Path التي أنشأناها سابقًا عبر الدالة Path(): >>> from pathlib import Path >>> Path('spam') / 'olive' / 'eggs' WindowsPath('spam/olive/eggs') >>> Path('spam') / Path('olive/eggs') WindowsPath('spam/olive/eggs') >>> Path('spam') / Path('olive', 'eggs') WindowsPath('spam/olive/eggs') يسهل استخدام العامل / مع كائنات Path عملية جمع المسارات مع بعضها كما لو كانت سلاسل نصية بسيطة، واستخدامه أكثر أمانًا من إجراء عملية جمع للسلاسل النصية يدويًا أو عبر التابع join() كما في المثال الآتي: >>> homeFolder = r'C:\Users\Al' >>> subFolder = 'spam' >>> homeFolder + '\\' + subFolder 'C:\\Users\\Al\\spam' >>> '\\'.join([homeFolder, subFolder]) 'C:\\Users\\Al\\spam' الشيفرة السابقة ليست آمنة لأن الخطوط المائلة الخلفية لا تعمل إلا على ويندوز كما ناقشنا في الأقسام السابقة. يمكنك أن تضيف عبارةً شرطيةً if للتحقق من sys.platform (الذي يحتوي على سلسلة نصية تصف نظام التشغيل المستعمل) لتحديد ما هو نوع الخط المائل الذي نريد استخدامه. لكن استخدام هذه الشيفرة في كل مكان تريد التعامل مع مسارات الملفات فيه هو أمر متعب وغير متناسق ومن المرجح أن يسبب علل برمجية. تحل الوحدة pathlib هذه المشاكل بإعادة استخدام معامل القسمة / ليجمع بين المسارات دون مشاكل بغض النظر عن نظام التشغيل المستعمل. يوضح المثال الآتي آلية استخدامه: >>> homeFolder = Path('C:/Users/Al') >>> subFolder = Path('spam') >>> homeFolder / subFolder WindowsPath('C:/Users/Al/spam') >>> str(homeFolder / subFolder) 'C:\\Users\\Al\\spam' أمر واحد مهم يجب أن نبقيه في ذهننا أثناء استخدام العامل / لجمع المسارات هو أن إحدى أول قيمتين من المسارات التي يجب جمعها يجب أن تكون كائن Path، وإلا فستظهر لك بايثون رسالة خطأ: >>> 'spam' / 'olive' / 'eggs' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for /: 'str' and 'str' تقيّم بايثون التعبير البرمجي الذي يستعمل العامل / من اليسار إلى اليمين إلى كائن Path، لهذا يجب أن يكون أول أو ثاني قيمة من اليسار من النوع Path لكي تستطيع إنتاج كائن Path من التعبير البرمجي. هذه هي آلية العمل التي تتبعها بايثون للحصول على كائن Path النهائي: الشكل 2: آلية تفسير المسارات مع العامل / إذا ظهرت رسالة الخطأ TypeError: unsupported operand type(s) for /: 'str' and 'str' فهذا يعني أن علينا وضع الكائن Path في الطرف الأيسر من التعبير البرمجي. يستبدل العامل / الدالةَ os.path.join() التي يمكنك معرفة المزيد عنها من التوثيق الرسمي https://docs.python.org/3/library/os.path.html#os.path.join. مجلد العمل الحالي يملك كل برنامج تشغله على حاسوب ما يسمى «مجلد العمل الحالي» current working directory أو اختصارًا cwd؛ تفترض بايثون أن أي ملفات أو مسارات لا تبدأ بالمجلد الجذر هي موجودة في مجلد العمل الحالي. ملاحظة: صحيح أننا نقول «مجلد» ترجمةً لكلمة directory التي تعني «موجِّه»، لكنها شائعة بين المستخدمين العرب أكثر؛ ولا أحد يقول current working folder. أغلبية الاصطلاحات البرمجية تستعمل directory بدلًا من folder، لذا ستجد هذه الكلمة مستعملةً في أسماء الدوال المشروحة تاليًا. يمكننا الحصول على سلسلة نصية تمثل مجلد العمل الحالي باستخدام Path.cwd()، ويمكن تغييرها باستخدام os.chdir(): >>> from pathlib import Path >>> import os >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')' >>> os.chdir('C:\\Windows\\System32') >>> Path.cwd() WindowsPath('C:/Windows/System32') لاحظ أن مجلد العمل الحالي هو C:\Users\Al\AppData\Local\Programs\Python\Python37 لذا إذا استعملنا اسم الملف project.docx فإنه سيشير إلى المسار C:\Users\Al\AppData\Local\Programs\Python\Python37\project.docx. حينما بدلنا مجلد العمل الحالي إلى C:\Windows\System32 فسيفسر project.docx إلى المسار C:\Windows\System32\project.docx. ستظهر بايثون رسالة خطأ حين محاولة تغيير مجلد العمل الحالي إلى مجلد غير موجود: >>> os.chdir('C:/ThisFolderDoesNotExist') Traceback (most recent call last): File "<stdin>", line 1, in <module> FileNotFoundError: [WinError 2] The system cannot find the file specified: 'C:/ThisFolderDoesNotExist' لا توجد دالة في pathlib لتغيير مجلد العمل الحالي، لأن تغييره أثناء تشغيل البرنامج قد يسبب علل برمجية نحن في غنى عنها. الدالة os.getcwd() هي الطريقة القديمة للحصول على سلسلة نصية تمثل مجلد العمل الحالي. مجلد المنزل يمتلك جميع المستخدمون مجلدًا خاصًا بهم يسمى مجلد المنزل home directory، ويمكننا الحصول على كائن Path لمجلد المنزل باستدعاء Path.home(): >>> Path.home() WindowsPath('C:/Users/Al') توجد مجلدات المنزل عادةً في مكان محدد يختلف حسب نظام تشغيلك: في ويندوز تكون في C:\Users. في ماك تكون في /Users. في لينكس تكون في /home. من شبه المؤكد أن سكربتات بايثون التي تكتبها ستمتلك أذونات القراءة والكتابة في مجلد المنزل، لذا من المستحسن أن تضع الملفات التي ستعالجها عبر بايثون فيه. المسارات النسبية والمسارات المطلقة هنالك طريقتان لتحديد مسار ملف: مسار مطلق absolute path، الذي يبدأ من المجلد الجذر. مسار نسبي relative path، الذي يبدأ من مجلد العمل الحالي للبرنامج. هنالك النقطة . والنقطتان .. حين التعامل مع المسارات، وهي ليست مجلدات حقيقة ولكنها أسماء خاصة، إذا تمثل النقطة . المجلد الحالي، بينما النقطتان .. تمثل المجلد الأب. يوضح الشكل 3 بعض الملفات والمجلدات ويكون مجلد العمل الحالي فيه هو C:\olive، وفيه مسارات الملفات والمجلدات كلها المطلقة والنسبية. الشكل 3: المسارات المطلقة والنسبية لاحظ أن .\ في بداية المسارات النسبية اختيارية، إذ يشير .\spam.txt و spam.txt إلى نفس الملف. إنشاء مجلدات جديدة باستخدام الدالة os.makedires() يمكن لبرامجك إنشاء مجلدات جديدة باستخدام الدالة os.makedires(): >>> import os >>> os.makedirs('C:\\delicious\\walnut\\waffles') سينتشئ المثال السابق المجلد C:\delicious وبداخله المجلد walnut وبداخله المجلد waffles. فالدالة os.makedires() ستنشِئ أي مجلدات لازمة غير موجودة مسبقًا. الشكل 4: ناتج تنفيذ os.makedirs('C:\delicious\walnut\waffles') لإنشاء مجلد من كائن Path فيمكننا استدعاء التابع mkdir()، فمثلًا سأنشِئ المجلد spam في مجلد المنزل في حاسوبي: >>> from pathlib import Path >>> Path(r'C:\Users\Al\spam').mkdir() لاحظ أن التابع mkdir() يستطيع إنشاء مجلد واحد فقط، ولن ينشِئ مجلدات فرعية كما في الدالة os.makedirs(). التعامل مع المسارات النسبية والمطلقة توفر الوحدة pathlib توابع للتحقق إن كان أحد المسارات نسبيًا أو مطلقًا، وتستطيع إعادة المسار المطلق من مسارٍ نسبي. سيعيد استدعاء التابع is_absolute() على كائن Path القيمة True إذا كان يمثل مسارًا مطلقًا أو False إذا كان يمثل مسارًا نسبيًا. تذكر أن تستعمل مسارات موجودة في حاسوبك في الأمثلة القادمة: >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37') >>> Path.cwd().is_absolute() True >>> Path('spam/olive/eggs').is_absolute() False للحصول على مسار مطلق من مسار نسبي يمكننا وضع Path.cwd() / قبل كائن Path، فحينما نقول «مسار نسبي» فهذا يعني أن المسار منسوب إلى مجلد العمل الحالي: >>> Path('my/relative/path') WindowsPath('my/relative/path') >>> Path.cwd() / Path('my/relative/path') WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37/my/relative/ path') أما إذا كان المسار النسبي منسوب إلى مسار مختلف عن مجلد العمل الحالي، فعلينا تبديل Path.cwd() إلى ذاك المسار، ففي المثال الآتي سنحصل على المسار النسبي نسبةً إلى مجلد المنزل الخاص بنا بدلًا من مجلد العمل الحالي: >>> Path('my/relative/path') WindowsPath('my/relative/path') >>> Path.home() / Path('my/relative/path') WindowsPath('C:/Users/Al/my/relative/path') تمتلك الوحدة os.path عددًا من الدوال المفيدة المتعلقة بالمسارات النسبية والمطلقة: ستعيد os.path.abspath(path) سلسلةً نصية فيها المسار المطلق للوسيط الممرر إليها، وهذه أسهل طريقة لتحويل مسار نسبي إلى مسار مطلق. ستعيد os.path.isabs(path) القيمة True إن كان الوسيط الممرر مسارًا مطلقًا و False إذا كان مسارًا نسبيًا. ستعيد os.path.relpath(path, start) سلسلةً نصيةً للمسار النسبي للمسار path بدءًا من المسار start. إذا لم نوفر المعامل start فسيستعمل مجلد العمل الحالي بدلًا منه. لنجرب الدوال السابقة: >>> os.path.abspath('.') 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37' >>> os.path.abspath('.\\Scripts') 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37\\Scripts' >>> os.path.isabs('.') False >>> os.path.isabs(os.path.abspath('.')) True ولمّا كان المسار C:\Users\Al\AppData\Local\Programs\Python\Python37 هو مجلد العمل الحالي حين استدعاء الدالة os.path.abspath()، فسيكون المجلد . (نقطة واحدة) هو المسار المطلق لمجلد العمل الحالي 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37'. لنجرب الدالة os.path.relpath() في الصدفة التفاعلية: >>> os.path.relpath('C:\\Windows', 'C:\\') 'Windows' >>> os.path.relpath('C:\\Windows', 'C:\\spam\\eggs') '..\\..\\Windows' إذا امتلك المسار النسبي نفس الأب لمجلد العمل الحالي كما في 'C:\\Windows' و 'C:\\spam\\eggs' فيمكن استخدام النقطتين .. للوصول إلى المجلد الأب. الحصول على أقسام المسار يمكننا استخلاص مختلف أقسام المسار عبر خاصيات الكائن Path، وهذا يفيدنا في حال أردنا إنشاء مسار جديد اعتمادًا على مسار ملف موجود مسبقًا. الشكل التالي يوضح هذه الخاصيات: الشكل 5: أجزاء المسار (ويندوز في الأعلى، ولينكس أو ماك في الأسفل) تتألف مسارات الملفات من الأقسام الآتية: المرساة anchor وهي المجلد الجذر root directory في نظام الملفات. المحرك drive في ويندوز وهو حرف واحد يشير إلى القرص المستخدم. الأب parent وهو مسار المجلد الذي يحتوي على الملف. اسم الملف name وهو يتألف من الاسم الأساسي stem والامتداد أو اللاحقة suffix. لاحظ أن كائنات Path تمتلك الخاصية drive في ويندوز، لكنها غير موجودة في ماك أو لينكس. لاحظ أيضًا أن الخاصية drive لا تتضمن أول خط مائل خلفي. لنجرب هذه الخاصيات على أحد المسارات: >>> p = Path('C:/Users/Al/spam.txt') >>> p.anchor 'C:\\' >>> p.parent # لن يعيد سلسلة نصية بل كائن Path WindowsPath('C:/Users/Al') >>> p.name 'spam.txt' >>> p.stem 'spam' >>> p.suffix '.txt' >>> p.drive 'C:' ستعيد هذه الخاصيات سلاسل نصية باستثناء الخاصية parent التي ستعيد كائن Path آخر. تمنحنا الخاصية parents (التي تختلف عن الخاصية parent السابقة) وصولًا إلى كائنات Path للمجلدات الأب مع فهرس رقمي: >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37') >>> Path.cwd().parents[0] WindowsPath('C:/Users/Al/AppData/Local/Programs/Python') >>> Path.cwd().parents[1] WindowsPath('C:/Users/Al/AppData/Local/Programs') >>> Path.cwd().parents[2] WindowsPath('C:/Users/Al/AppData/Local') >>> Path.cwd().parents[3] WindowsPath('C:/Users/Al/AppData') >>> Path.cwd().parents[4] WindowsPath('C:/Users/Al') >>> Path.cwd().parents[5] WindowsPath('C:/Users') >>> Path.cwd().parents[6] WindowsPath('C:/') تمتلك الوحدة os.path القديمة دوال مشابهة لما سبق للحصول على مختلف أقسام المسارات كسلسلة نصية. ستعيد الدالة os.path.dirname(path) سلسلةً نصيةً فيها كل ما يسبق آخر خط مائل في المعامل path، بينما ستعيد os.path.basename(path) سلسلةً نصيةً فيها كل ما يلي آخر خط مائل في المعامل path. الشكل 6 يوضح مخرجات الدالتين السابقتين: الشكل 6: اسم المجلد dirname واسم الملف basename في الوحدة os.path لنجربها عمليًا في الطرفية التفاعلية: >>> calcFilePath = 'C:\\Windows\\System32\\calc.exe' >>> os.path.basename(calcFilePath) 'calc.exe' >>> os.path.dirname(calcFilePath) 'C:\\Windows\\System32' إذا احتجت إلى اسم المجلد واسم الملف معًا، فيمكنك استدعاء الدالة os.path.split() للحصول على صف فيه سلسلتين نصيتين كما يلي: >>> calcFilePath = 'C:\\Windows\\System32\\calc.exe' >>> os.path.split(calcFilePath) ('C:\\Windows\\System32', 'calc.exe') لاحظ أنك تستطيع إنشاء نفس الصف السابق باستدعاء الدالتين os.path.dirname() و os.path.basename() بنفسك: >>> (os.path.dirname(calcFilePath), os.path.basename(calcFilePath)) ('C:\\Windows\\System32', 'calc.exe') لكن استخدام os.path.split() سيوفر عليك بعض الوقت أحيانًا. لاحظ أن الدالة os.path.split() لا تأخذ مسار أحد الملفات وتعيد قائمةً من السلاسل النصية تمثل كل مجلد، وإنما علينا استخدام الدالة split() الخاصة بالسلاسل النصية وتقسيم المسار عند كل فاصل os.sep (لاحظ أن الفاصل sep موجود في os وليس os.path). يتمثل المتغير os.sep الفاصل بين المجلدات في نظام التشغيل المستخدم، وهو '\\' في ويندوز و '/' في ماك ولينكس: >>> calcFilePath.split(os.sep) ['C:', 'Windows', 'System32', 'calc.exe'] سعيد المثال السابق جميع أقسام المسار كقائمة من السلاسل النصية. سيكون أول عنصر في القائمة المعادة في أنظمة ماك ولينكس هو سلسلة نصية فارغة: >>> '/usr/bin'.split(os. sep) ['', 'usr', 'bin'] الحصول على حجم ملف ومحتويات مجلد بعد أن تعلمنا كيف نتعامل مع مسارات الملفات والمجلدات، يمكننا أن نبدأ بجمع المعلومات حولها. توفر الوحدة os.path ما يلزم لمعرفة الحجم التخزيني لملفٍ ما بالبايت، أو للملفات والمجلدات الموجودة في مجلد معين. استدعاء os.path.getsize(path) سيعيد الحجم التخزيني للملف الموجود في المسار path بالبايت. استدعاء os.listdir(path) سيعيد قائمة list فيها أسماء كل الملفات الموجودة في المسار path، لاحظ أننا استعملنا هنا الوحدة os وليس os.path. لنجرب هذه الدوال في الطرفية التفاعلية: >>> os.path.getsize('C:\\Windows\\System32\\calc.exe') 27648 >>> os.listdir('C:\\Windows\\System32') ['0409', '12520437.cpx', '12520850.cpx', '5U877.ax', 'aaclient.dll', --snip-- 'xwtpdui.dll', 'xwtpw32.dll', 'zh-CN', 'zh-HK', 'zh-TW', 'zipfldr.dll'] كما هو واضح، حجم الملف calc.exe في حاسوبي هو 27,648 بايت، ولدي الكثير من الملفات في المسار C:\Windows\system32، وإذا أردت الحصول على الحجم التخزيني لكل الملفات في هذا المجلد فسأستخدم os.path.getsize() و os.listdir() معًا. >>> totalSize = 0 >>> for filename in os.listdir('C:\\Windows\\System32'): totalSize = totalSize + os.path.getsize(os.path.join('C:\\Windows\\System32', filename)) >>> print(totalSize) 2559970473 سأمر بحلقة التكرار على كل ملف موجود في المجلد C:\Windows\System32 وسأزيد قيمة المتغير totalSize بمقدار الحجم التخزيني لكل ملف، لاحظ أنه حين استدعاء os.path.getsize() فنستخدم os.path.join() لإضافة اسم المجلد إلى اسم الملف الحالي. ستضاف القيمة العددية المعادة من os.path.getsize() إلى قيمة totalSize، وبعد إنهاء المرور على جميع الملفات فسنطبع قيمة totalSize لمعرفة حجم المجلد C:\Windows\System32 التخزيني. الحصول على قائمة من الملفات التي تطابق نمطًا معينًا باستخدام glob() إذا أردت العمل على ملفات محددة فمن الأسهل استخدام التابع glob() بدلًا من listdir()، إذ تملك كائنات Path التابع glob() للحصول على قائمة الملفات الموجودة داخل مجلد وفقًا لنمط يسمى Glob pattern، والتي تعرف أيضًا بالمحارف البديلة wildcard characters وهي نسخة مبسطة من التعابير النمطية التي يشيع استخدامها في سطر الأوامر (وتسمى هناك بالتوسعات expansion). يعيد التابع glob() كائنًا مولدًا generator object (وهو خارج عن سياق هذا الكتاب)، الذي يمكننا تمريره إلى الدالة list() لتسهيل التعامل معه: >>> p = Path('C:/Users/Al/Desktop') >>> p.glob('*') <generator object Path.glob at 0x000002A6E389DED0> >>> list(p.glob('*')) [WindowsPath('C:/Users/Al/Desktop/1.png'), WindowsPath('C:/Users/Al/ Desktop/22-ap.pdf'), WindowsPath('C:/Users/Al/Desktop/cat.jpg'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] رمز النجمة * يعني "مجموعة من أي نوع من المحارف"، وبالتالي سيعيد p.glob('*') مولدًا فيه جميع الملفات الموجودة في المسار المخزن في p. وكما في التعابير النمطية، يمكننا كتابة أنماط معقدة بعض الشيء: >>> list(p.glob('*.txt') # قائمة بكل الملفات النصية [WindowsPath('C:/Users/Al/Desktop/foo.txt'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] سيعيد النمط '*.txt' كل الملفات التي تبدأ بأي مجموعة من المحارف طالما أنها تنتهي بالسلسلة النصية '.txt'، وهو عادةً امتداد الملفات النصية. أما رمز إشارة الاستفهام ? فيعني أي محرف واحد: >>> list(p.glob('project?.docx') [WindowsPath('C:/Users/Al/Desktop/project1.docx'), WindowsPath('C:/Users/Al/ Desktop/project2.docx'), --snip-- WindowsPath('C:/Users/Al/Desktop/project9.docx')] التعبير 'project?.docx' سيعيد 'project1.docx' أو 'project5.docx'، لكنه لن يطابق 'project10.docx' لأن رمز علامة الاستفهام ? سيطابق محرفًا واحدًا فقط، لذا لن يستطيع مطابقة محرفين '10'. يمكنك أن تستعمل رمز النجمة وعلامة الاستفهام معًا لإنشاء تعابير مخصصة مثل: >>> list(p.glob('*.?x?') [WindowsPath('C:/Users/Al/Desktop/calc.exe'), WindowsPath('C:/Users/Al/ Desktop/foo.txt'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] التعبير '*.?x?' سيعيد جميع الملفات التي لها أي اسم لكنها تنتهي بلاحقة تتألف من 3 محارف، والمحرف الوسط بينها هو 'x'. يسهل علينا التابع glob() تحديد الملفات التي نريدها باختيار الملفات التي يطابق اسمها نمطًا معينًا. سنستخدم هنا الحلقة for للمرور على المولد generator المولد من التابع glob(): >>> p = Path('C:/Users/Al/Desktop') >>> for textFilePathObj in p.glob('*.txt'): ... print(textFilePathObj) # طباعة كائن Path كسلسلة نصية ... # معالجة الملف النصي ... C:\Users\Al\Desktop\foo.txt C:\Users\Al\Desktop\spam.txt C:\Users\Al\Desktop\zzz.txt إذا أردت إجراء نفس العملية على جميع الملفات الموجودة في المجلد، فيمكنك أن تستعمل حينها os.listdir(p) أو p.glob('*'). التأكد من المسارات ستفشل أغلبية دوال بايثون التي تتعامل مع الملفات إذا أعطيناها مسارًا غير موجود، لكن لحسن الحظ هنالك توابع لكائنات Path للتحقق أن المسار المعطى موجود فعلًا، وهل هو ملف أم مجلد. فعلى فرض أن المتغير p يشير إلى كائن Path، فبالتالي سيعيد استدعاء: p.exists() القيمة True إذا كان المسار موجودًا، أو False إن لم يكن موجودًا. p.is_file() القيمة True إن كان المسار موجودًا ويشير إلى ملف، أو False خلاف ذلك. p.is_dir() القيمة True إذا كان المسار موجودًا ويشير إلى مجلد، أو False خلاف ذلك. دعني أجرب هذه التوابع على حاسوبي الشخصي: >>> winDir = Path('C:/Windows') >>> notExistsDir = Path('C:/This/Folder/Does/Not/Exist') >>> calcFile = Path('C:/Windows /System32/calc.exe') >>> winDir.exists() True >>> winDir.is_dir() True >>> notExistsDir.exists() False >>> calcFile.is_file() True >>> calcFile.is_dir() False إذا كنت تستعمل ويندوز فيمكنك أن تتحقق إن كان قرص التخزين المؤقت («الفلاشة») موصولًا إلى الحاسوب عبر التابع exists()، فمثلًا لو أردت التحقق أن القرص المسمى D:`` موجود على حاسوبي: >>> dDrive = Path('D:/') >>> dDrive.exists() False يبدو أنني نسيت وصل القرص إلى الحاسوب. الوحدة القديمة os.path تستطيع إنجاز نفس المهمة باستخدام os.path.exists(path) و os.path.isfile(path) و os.path.isdir(path)، التي تعمل ملف مكافأتها في كائنات Path. وبدءًا من الإصدار بايثون 3.6 أصبحت تقبل هذه التوابع كائنات Path إضافةً إلى سلاسل نصية تحتوي على مسارات الملفات. عملية قراءة الملفات والكتابة إليها بعد أن تصبح مرتاحًا بالتعامل مع المجلدات والمسارات النسبية، فستتمكن من تحديد موقع الملفات التي تريد قراءتها أو الكتابة إليها. الدوال التي سنشرحها في الأقسام الآتية تعمل على الملفات النصية البسيطة، التي هي ملفات تحتوي على محارف نصية دون أن تحتوي على معلومات التنسيقات مثل الخطوط أو الألوان أو خلاف ذلك، ومن الأمثلة على الملفات النصية البسيطة هي ملفات txt أو py التي تحتوي على شيفرات بايثون. يمكن فتح هذه الملفات باستخدام المفكرة Notepad في ويندوز، أو TextEdit في ماك، أو Kate أو Gedit في لينكس. وتستطيع أن تفتح هذه الملفات في برامجك وتعاملها كسلاسل نصية عادية. الملفات الثنائية هي نوع آخر من الملفات، مثل الملفات التي تنتجها برامج إنشاء العروض التقديمية أو ملفات PDF أو الصور أو الملفات التنفيدية …إلخ. وإذا فتحتها بالمفكرة مثلًا فستجد أنها مجموعة من الرموز غير المفهومة: الشكل 7: برنامج calc.exe مفتوح في المفكرة ولأن كل نوع من الملفات الثنائية يجري التعامل معه بطريقة مختلفة، فلن ندخل بتفاصيل تعديل الملفات الثانية مباشرةً في هذا الكتاب؛ وهنالك وحدات تسهل التعامل معها مثل الوحدة shelve التي ستتعامل معها لاحقًا في هذا المقال. التابع read_text() في الوحدة pathlib تعيد سلسلةً نصية فيها كل محتويات الملف النصي، بينما يكتب التابع write_text() ما يمرر إليه إلى ملف نصي جديد (أو يعيد الكتابة فوق ملف موجود مسبقًا): >>> from pathlib import Path >>> p = Path('spam.txt') >>> p.write_text('Hello, world!') 13 >>> p.read_text() 'Hello, world!' سننشئ ملفًا باسم spam.txt فيه المحتويات 'Hello, world!'، لاحظ أن التابع write_text() قد أعاد الرقم 13 الذي يشير إلى عدد المحارف التي كتبت إلى الملف (ونتجاهل تخزين هذا الرقم عادةً)، ويقرأ التابع read_text() محتويات الملف الجديد ويعيدها على شكل سلسلة نصية. تذكر أن توابع الكائن Path توفر الأمور الأساسية في التعامل مع الملفات؛ والطريقة الأشيع لقراءة الملفات تكون عبر الدالة open() والكائن File. هنالك خطوات ثلاث لقراءة أو كتابة الملفات في بايثون: استدعاء الدالة open() لإعادة الكائن File. استدعاء التابع read() أو write() على الكائن File. إغلاق الملف باستدعاء التابع close() على الكائن File. سنشرح هذه الخطوات في الأقسام الآتية. فتح الملفات عبر الدالة open() لفتح ملف باستخدام الدالة open() فنمرر سلسلة نصية تحتوي على مسار الملف الذي نريد فتحه؛ والذي يكون إما مسارًا مطلقًا absolute أو نسبيًا relative. ستعيد الدالة open() كائنًا من النوع File. لنجربها بإنشاء ملف نصي بسيط اسمه hello.txt باستخدام المفكرة أو أي محرر نصوص، وكتابة Hello, World! داخلها وحفظه في مجلد المنزل، ثم كتابة ما يلي في الطرفية التفاعلية: >>> helloFile = open(Path.home() / 'hello.txt') تقبل الدالة open() السلاسل النصية أيضًا، فإذا كنت تستخدم ويندوز فاكتب: >>> helloFile = open('C:\\Users\\your_home_folder\\hello.txt') أما إذا كان نظامك ماك: >>> helloFile = open('/Users/your_home_folder/hello.txt') تذكر أن تبدل الكلمة yourhomefolder باسم المستخدم في حاسوبك، فلو كان hsoub مثلًا فستدخل 'C:\\Users\\hsoub\\hello.txt' في ويندوز؛ لاحظ أن الدالة open() أصبحت تقبل كائنات Path بدءًا من إصدار بايثون 3.6، وكان عليك استخدام السلاسل النصية فقط فيما سبق. ستفتح هذه الأوامر الملف في وضع "قراءة الملفات النصية" أو اختصارًا "وضع القراءة"، وحينما يفتح الملف في وضع القراءة فتسمح لنا بايثون بقراءة الملفات من الملف فقط، ولا يمكنك أن تكتب عليه أو تعدله بأي شكل. لكن إذا أردت أن تحدد أنك تريد فتح الملف بوضع القراءة بوضوح فمرر القيمة 'r' كثاني وسيط إلى الدالة open()، أي أن open('/Users/Al/hello.txt', 'r') و open('/Users/Al/hello.txt') متكافئتان تمامًا. سيعيد استدعاء الدالة open() كائن File، ويمثل كائن File ملفًا على حاسوبك، وهو نوع مختلف من القيم في بايثون مثله كمثل القوائم أو القواميس التي تعرفت عليها مسبقًا. خزنّا في المثال السابق كائن File في المتغير helloFile، ويمكنك أن تستسخدمه لأي عمليات قراءة أو كتابة مستقبلًا باستدعاء التوابع المناسبة على الكائن File المخزن في المتغير helloFile. قراءة محتويات الملفات أصبح لدينا الآن كائن File، ويمكننا أن نبدأ بقراءة كامل محتويات الملف كسلسلة نصية، وذلك عبر التابع read()، لنكمل مثالنا السابق الذي فيه المتغير helloFile بكتابة ما يلي: >>> helloContent = helloFile.read() >>> helloContent 'Hello, world!' لو تخيلت أن جميع محتويات الملف هي سلسلة نصية كبيرة، فإن التابع read() يعيد تلك السلسلة النصية. بدلًا من ذلك، يمكنك استخدام التابع readlines() للحصول على قائمة list فيها سلاسل نصية من الملف، وكل سلسلة نصية تمثل سطرًا فيه، فمثلًا لو كان لدينا ملف اسمه sonnet29.txt في نفس المجلد الذي فيه الملف hello.txt وكتبنا فيه النص الآتي: When, in disgrace with fortune and men's eyes, I all alone beweep my outcast state, And trouble deaf heaven with my bootless cries, And look upon myself and curse my fate, تأكد أنك قد فصلت بين الأسطر الأربعة السابقة كلٌ في سطر مختلف، ثم أدخل ما يلي في الطرفية التفاعلية: >>> sonnetFile = open(Path.home() / 'sonnet29.txt') >>> sonnetFile.readlines() [When, in disgrace with fortune and men's eyes,\n', ' I all alone beweep my outcast state,\n', And trouble deaf heaven with my bootless cries,\n', And look upon myself and curse my fate,'] لاحظ أن كل عنصر من عناصر القائمة (باستثناء آخر واحد) هو سلسلة نصية تنتهي بمحرف السطر الجديد \n. يكون في العادة من الأسهل التعامل مع قائمة من السلاسل النصية بدل سلسلة نصية واحدة كبيرة. الكتابة إلى الملفات تسمح لنا بايثون بكتابة محتوى إلى الملفات بشكل يشبه "كتابة" الدالة print() للسلاسل النصية إلى الشاشة. لا يمكنك أن تكتب إلى ملف قد فتحته بوضع القراءة، لذا عليك أن تفتحه بوضع "الكتابة على الملفات النصية" أو "الإضافة إلى الملفات النصية" واختصارًا وضع الكتابة أو وضع الإضافة. وضع الكتابة سيعيد الكتابة فوق ملف موجود ويبدأ من الصفر، كما لو أعدنا إسناد قيمة جديدة إلى متغير. يمكنك فتحت الملف بوضع الكتابة بتمرير 'w' كثاني وسيط إلى الدالة open(). أما وضع الإضافة فسيضيف النص إلى نهاية ملف موجود مسبقًا، كما لو أضفنا عنصرًا إلى قائمة موجودة في متغير بدلًا من إعادة الكتابة. مرر 'a' كثاني وسيط إلى الدالة open() لفتح الملف في وضع الإضافة. إذا لم يكن الملف الممرر إلى الدالة open() موجودًا فسينشأ ملف جديد في وضع الكتابة والإسناد. لا تنسَ أن تستدعي الدالة close() قبل إعادة فتح الملف مجددًا. لنجرب هذه المفاهيم معًا بكتابة: >>> oliveFile = open('olive.txt', 'w') >>> oliveFile.write('Hello, world!\n') 13 >>> oliveFile.close() >>> oliveFile = open('olive.txt', 'a') >>> oliveFile.write('Olive is not a vegetable.') 25 >>> oliveFile.close() >>> oliveFile = open('olive.txt') >>> content = oliveFile.read() >>> oliveFile.close() >>> print(content) Hello, world! Olive is not a vegetable. في البداية فتحنا الملف becon.txt بوضع الكتابة، ولعدم وجود الملف becon.txt بعد فإن بايثون تنشئه لنا، واستدعاء التابع write() وتمرير السلسلة النصية 'Hello, world! /n' سيؤدي إلى كتابتها إلى الملف وإعادة عدد المحارف المكتوبة بما فيها محرف السطر الجديد. ثم أغلقنا في النهاية الملف. لإضافة نص إلى المحتويات الموجودة لملف بدلًا من استبداله، فسنفتح الملف في وضع الإضافة، وأضفنا السلسلة النصية 'Olive is not a vegetable.' إلى الملف وأغلقناه. في النهاية نريد أن نطبع محتويات الملف فاستخدمنا الدالة open() لفتح الملف في الوضع الافتراضي وهو وضع القراءة، وخزنّا محتويات الملف في المتغير content ثم أغلقنا الملف وطبعنا محتوياته. لاحظ أن التابع write() لا يضيف محرف السطر الجديد إلى نهاية السلسلة النصية مثلما تفعل الدالة print() لذا عليك أن تضيفه بنفسك. تذكر أنك تستطيع تمرير كائن Path إلى الدالة open() بدلًا من سلسلة نصية بسيطة بدءًا من إصدار بايثون 3.6. حفظ المتغيرات باستخدام الوحدة shelve يمكنك أن تحفظ المتغيرات الموجودة في برامجك إلى ملفات ثنائية باستخدام الوحدة shelve، وبالتالي يمكنك أن تستعيد البيانات إلى المتغيرات من ملف مخزن على حاسوبك. تسمح لك الوحدة shelve بحفظ واستعادة البيانات إلى برنامجك، فلو ضبطتَ مثلًا بعض المتغيرات في برنامجك، يمكنك أن تحفظ تلك البيانات على الرف (shelf، ومن هنا أتى اسم الوحدة) ثم تستعيد تلك القيم حينما تشغل برنامجك مجددًا. >>> import shelve >>> shelfFile = shelve.open('mydata') >>> cats = ['Zophie', 'Pooka', 'Simon'] >>> shelfFile['cats'] = cats >>> shelfFile.close() لقراءة وكتابة البيانات باستخدام الوحدة shelve عليك أن تستوردها أولًا، ثم تستدعي shelve.open() وتمرر إليها اسم الملف ثم تخزن القيم. لاحظ أنك تستطيع التعامل مع القيم كما لو أنها قاموس. بعد أن تنتهي لا تنسَ استدعاء close(). أنشأنا في المثال السابق القائمة cats وكتبنا shelfFile['cats'] = cats لتخزين القائمة في shelfFile كقيمة مرتبطة مع المفتاح 'cat' (كم