اذهب إلى المحتوى

Ola Abbas

الأعضاء
  • المساهمات

    189
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو Ola Abbas

  1. نشرح في مقال اليوم بنية نموذج-قالب-عرض Model-Template-View أو MTV اختصارًا التي يعتمد عليها إطار عمل جانغو Django لتطوير الويب حيث يكون النموذج Model مسؤولًا عن التفاعل مع قاعدة البيانات، ويجب أن يقابل كل نموذج جدولًا منها، ويعبر القالب Template عن جزء الواجهة الأمامية من التطبيق، وهو الشيء الذي سيراه المستخدم، أما العرض View فهو يتضمن المنطق البرمجي لواجهة التطبيق الخلفية، ومسؤولً عن استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة مفهوم النماذج Models النموذج من أفضل ميزات جانغو Django، ففي أطر عمل الويب الأخرى ستحتاج إلى إنشاء نموذج وملف تهجير Migration، وملف التهجير هو مخطط Schema لقاعدة البيانات، يصف بنية قاعدة البيانات كأسماء الأعمدة وأنواعها، ويوفر النموذج واجهةً تتعامل مع معالجة البيانات بناءً على هذا المخطط، ولكنك ستحتاج إلى نموذج فقط في جانغو، ويمكن توليد ملفات التهجير المقابلة باستخدام أمر بسيط هو python manage.py makemigrations، مما يوفر عليك كثيرًا من الوقت. يحتوي كل تطبيق جانغو على ملف models.py واحد، ويجب تعريف جميع النماذج المرتبطة بالتطبيق بداخله، ويقابل كل نموذج ملف تهجير، والذي يقابل بدوره جدولًا في قاعدة البيانات. يمكنك التمييز بين الجداول الخاصة بالتطبيقات المختلفة من خلال إسناد بادئة لكل جدول تلقائيًا، حيث سيكون لجدول قاعدة البيانات المقابل لتطبيق blog الخاص بنا البادئة blog_‎. يوضح المثال التالي نموذجًا في الملف blog/models.py: from django.db import models class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) يمكنك بعد ذلك توليد ملف التهجير باستخدام الأمر التالي: python manage.py makemigrations ويجب أن يبدو ملف التهجير الناتج blog/migrations/0001_initial.py كما يلي: # Generated by Django 4.1.2 on 2022-10-19 23:13 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Person", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("first_name", models.CharField(max_length=30)), ("last_name", models.CharField(max_length=30)), ], ), ] يمكنك اعتبار ملف التهجير بمثابة مخطط يوضح كيف يجب أن يبدو جدول قاعدة البيانات، ويمكنك استخدام الأمر التالي لتطبيق هذا المخطط: python manage.py migrate يجب أن تبدو قاعدة بياناتك db.sqlite3 كما يلي: لا تحاول تعديل أو حذف ملفات التهجير إذا كنت مبتدئًا، ودع جانغو يطبّق كل شيء نيابةً عنك إلّا إن كنت تعرف ما تفعله تمامًا. يمكن لجانغو في معظم الحالات اكتشاف التغييرات التي أجريتها في النماذج حتى إذا حذفتَ شيئًا وتوليد ملفات الترحيل وفقًا لذلك. سينشئ النموذج في مثالنا جدول قاعدة بيانات person، وسيحتوي هذا الجدول على ثلاثة أعمدة اسمها id و first_name و last_name، حيث يُنشأ العمود id تلقائيًا كما هو موضح في ملف التهجير، ويُستخدم هذا العمود كمفتاح رئيسي Primary Key للفهرسة Indexing افتراضيًا. يسمَّىCharField()‎ بنوع الحقل ويعرِّف نوع العمود، ويسمَّى max_length بخيار الحقل ويحدّد معلومات إضافية حول هذا العمود. أهم نواع حقول النموذج وخياراتها يوضّح الجدول التالي بعض أنواع الحقول الأكثر استخدامًا وننصحك بالاطلاع على جميع أنواع الحقول وخياراتها من توثيق جانغو الرسمي. نوع الحقل وصف عنه BigAutoField ينشئ عمودًا نوعه عدد صحيح يتزايد تلقائيًا، يُستخدم عادةً مع العمود id. BooleanField ينشئ عمودًا قيمه منطقية True أو False. DateField و DateTimeField يُستخدم لإضافة تواريخ وأوقات كما يوحي اسمه. FileField و ImageField ينشئ عمودًا لتخزين المسار الذي يؤشّر إلى الملف أو الصورة المرفوعة. IntegerField و BigIntegerField تتراوح قيم الأعداد الصحيحة Integer من ‎-2147483648 إلى 2147483647، وتتراوح قيم الأعداد الصحيحة الكبيرة Big Integer من ‎-9223372036854775808 إلى 9223372036854775807 SlugField الاسم المختصر Slug هو نسخة بسيطة من عنوان URL للاسم أو العنوان. CharField و TextField ينشئ كل من CharField و TextField عمودًا لتخزين السلاسل النصية Strings، ولكن يقابل TextField مربع نص أكبر في صفحة مدير جانغو Django Admin التي سنتحدث عنها لاحقًا في هذه السلسلة من المقالات. يوضح الجدول التالي بعض خيارات الحقول الأكثر استخدامًا: خيار الحقل وصف عنه blank يسمح للحقل بأن يحتوي على إدخال فارغ. choices يمنح الحقل خيارات متعددة، حيث سنوضّح ذلك لاحقًا عندما نصل إلى مدير جانغو. default يعطي الحقل قيمةً افتراضية. unique يتأكد من أن كلّ عنصر في العمود فريد، ويُستخدَم عادةً لتحديد الاسم المختصر Slug والحقول الأخرى التي يُفترَض أن تحتوي على قيم فريدة. خيارات الصنف Meta يمكنك أيضًا إضافة الصنف Class الذي هو Meta في صنف النموذج، والذي يحتوي على معلومات إضافية حول هذا النموذج مثل اسم جدول قاعدة البيانات وخيارات الترتيب والأسماء المفردة وجمعها التي يمكن أن يقرأها الإنسان كما يلي: class Category(models.Model): priority = models.IntegerField() class Meta: ordering = ["priority"] verbose_name_plural = "categories" توابع النموذج توابع النموذج هي دوال معرَّفة في صنف النموذج، تسمح بتطبيق إجراءات مخصصة على النسخة الحالية من كائن النموذج كما في المثال التالي: class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) birth_date = models.DateField() def baby_boomer_status(self): "Returns the person's baby-boomer status." import datetime if self.birth_date < datetime.date(1945, 8, 1): return "Pre-boomer" elif self.birth_date < datetime.date(1965, 1, 1): return "Baby boomer" else: return "Post-boomer" في الكود السابق، يفحص جانغو تاريخ ميلاد الشخص ويعيد حالة تمثل إن كانت فترة ولادته قبل زيادة المواليد التي حدثت بعد الحرب العالمية الثانية Pre-boomer، أم خلالها Baby-Boomer أم بعدها Post-boomer للشخص عند استدعاء التابع baby_boomer_status()‎. ملاحظة: الكائنات والتوابع والخاصيات مفاهيم مهمة جدًا في لغات البرمجة، لذا ننصح بمطالعة مقال كائنات وأصناف بايثون لمزيد من المعلومات. وراثة النماذج ستحتاج إلى أكثر من نموذج واحد في معظم تطبيقات الويب، وسيكون لبعضها حقول مشتركة، لذا يمكنك إنشاء نموذج أب Parent Model يحتوي على الحقول المشتركة، ثم تجعل النماذج الأخرى ترث هذا النموذج الأب كما يلي: class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = True class Student(CommonInfo): home_group = models.CharField(max_length=5) لاحظ أن النموذج CommonInfo نموذج مجرد Abstract، مما يعني أنه لا يقابل نموذجًا فرديًا فعليًا، بل يستخدم كأب لنماذج أخرى. لنولّد الآن ملف تهجير جديد blog/migrations/0002_student.py باستخدام الأمر التالي للتحقق من ذلك: python manage.py makemigrations سيتولّد ملف التهجير التالي: # Generated by Django 4.1.2 on 2022-10-19 23:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.CreateModel( name="Student", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("age", models.PositiveIntegerField()), ("home_group", models.CharField(max_length=5)), ], options={ "abstract": False, }, ), ] لاحظ إنشاء الجدول Student فقط. علاقات قاعدة البيانات تحدثنا عن كيفية إنشاء جداول فردية، ولكن لا تكون هذه الجداول مستقلة تمامًا في معظم التطبيقات، بل توجد علاقات بينها، فمثلًا قد يكون لديك فئة Category تنتمي إليها منشورات متعددة، ويكون كل منشور في المدونة تابع لمستخدم معين وما إلى ذلك، لذا توجد ثلاثة أنواع أساسية للتعبير عن العلاقات بين جداول قاعدة البيانات وهي: علاقة واحد إلى واحد One-to-one وعلاقة متعدد إلى واحد Many-to-one وعلاقة متعدد إلى متعدد Many-to-many وسنشرح تاليًا المزيد حول هذه العلاقات وطريقة وصفها ضمن نماذج جانغو. علاقة واحد إلى واحد علاقة واحد إلى واحد هي العلاقة الأسهل، فمثلًا يمكن أن يكون لكل شخص هاتف واحد، ويمكن أن يعود كل هاتف إلى شخص واحد، حيث يمكننا وصف هذه العلاقة في النماذج كما يلي: class Person(models.Model): name = models.CharField(max_length=100) class Phone(models.Model): person = models.OneToOneField('Person', on_delete=models.CASCADE) يعمل نوع الحقل OneToOneField مثل أي نوع حقل آخر، ولكنه يتطلب وسيطين على الأقل هما: الوسيط الأول هو اسم النموذج الآخر الذي يرتبط به هذا النموذج بعلاقة، والوسيط الثاني هو on_delete الذي يعرّف الإجراء الذي سيتخذه جانغو عند حذف البيانات، ويتعلق هذا المعامل بلغة SQL أكثر من جانغو، لذا لن نتحدث عنه بالتفصيل، ولكن إن كنت مهتمًا، فاطلع على بعض القيم المتاحة للمعامل on_delete. يمكنك الآن إنشاء عمليات تهجير وتطبيقها لهذه النماذج ومعرفة ما يحدث، وإذا واجهتك مشاكل أثناء تشغيل الأوامر التالية، فاحذف الملف db.sqlite3 وملفات التهجير للبدء من جديد: python manage.py makemigrations python manage.py migrate لاحظ أن نوع الحقل OneToOneField أنشأ العمود person_id في الجدول blog_phone، وسيخزّن هذا العمود معرّف id الشخص الذي يمتلك هذا الهاتف. علاقة متعدد إلى واحد يمكن أن تحتوي كل فئة على منشورات متعددة وينتمي كل منشور إلى فئة واحدة مثلًا، حيث يشار إلى هذه العلاقة باسم علاقة متعدد إلى واحد التي نعرّفها كما يلي: class Category(models.Model): name = models.CharField(max_length=100) class Post(models.Model): category = models.ForeignKey('Category', on_delete=models.CASCADE) ينشئ نوع الحقل ForeignKey العمود category_id في الجدول blog_post، والذي يخزن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تكون علاقة متعدد إلى متعدد أكثر تعقيدًا بعض الشيء، فمثلًا يمكن أن يكون لكل مقال وسوم Tags متعددة، ويمكن أن يكون لكل وسم مقالات متعددة: class Tag(models.Model): name = models.CharField(max_length=100) class Post(models.Model): tags = models.ManyToManyField('Tag') ستنشئ الشيفرة البرمجية السابقة جدولًا جديدًا بالاسم post_tags بدلًا من إنشاء عمود جديد، وسيحتوي هذا الجدول الجديد على عمودين هما post_id و tag_id، مما يتيح لك تحديد جميع الوسوم المرتبطة بمنشور معين والعكس صحيح. لنفترض أن لدينا الجدول التالي مثلًا لتوضيح الأمور: post_id tag_id 1 1 2 1 3 2 1 2 2 3 يمتلك المنشور الذي له المعرّف id=1 وسمين tags لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمور للعثور على المنشورات باستخدام الوسم، فيمكننا أن نرى منشورين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للوسم ذي المعرّف id=2. طبقة العرض View طبقة العرض مكون مهم في تطبيق جانغو ، فهي المكان الذي نكتب فيه المنطق البرمجي للواجهة الخلفية. يسترد العرض البيانات من قاعدة البيانات من خلال النموذج المقابل ويعالج هذه البيانات المستردة ويضعها في الموقع المقابل في القالب ويعرض هذا القالب ويعيده إلى المستخدم. لا تعمل دالة العرض View Function على استرداد البيانات فقط، إذ توجد أربع عمليات أساسية يمكن تطبيقها على البيانات في معظم تطبيقات الويب، وهي الإنشاء Create والقراءة Read والتحديث Update والحذف Delete، ويشار إلى هذه العمليات مجتمعة بالاسم CRUD، والتي سنوضّحها في مقال لاحق. تحدّثنا عن النماذج في القسم السابق، ولكننا ما زلنا لا نعرف كيفية استرداد البيانات أو تخزينها باستخدام النموذج، حيث يقدم جانغو واجهة برمجة تطبيقات API بسيطة لمساعدتنا في ذلك، تسمى QuerySet. لنفترض أن لدينا النموذج blog/models.py التالي: class Category(models.Model): name = models.CharField(max_length=100) class Tag(models.Model): name = models.CharField(max_length=200) class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() pub_date = models.DateField() category = models.ForeignKey(Category, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag) يمكنك معالجة البيانات بمساعدة QuerySet من خلال هذا النموذج ضمن دوال العرض التي توجد في الملف blog/views.py. إنشاء البيانات وحفظها لنفترض أنك تريد إنشاء فئة جديدة كما يلي: # استيراد نموذج الفئة‫ Category from blog.models import Category # إنشاء نسخة جديدة من النموذج‫ Category category = Category(name="New Category") # حفظ الفئة التي أنشأناها في قاعدة البيانات category.save() يجب أن يكون ما سبق سهلًا إذا كنت على دراية بمفهوم البرمجة كائنية التوجه، حيث أنشأنا في المثال السابق نسخة جديدة من الكائن Category واستخدمنا التابع save()‎ الذي ينتمي إلى هذا الكائن لحفظ المعلومات في قاعدة البيانات. توجد علاقة متعدد إلى واحد بين الفئة والمنشور، وقد عرفناها باستخدام الحقل ForeignKey()‎، مع افتراض أن لدينا عدد كافي من السجلات في قاعدة البيانات. from blog.models import Category, Post # ‫Post.objects.get(pk=1)‎ هي الطريقة التي نسترجع بها المنشور الذي له المفتاح الرئيسي pk=1، # حيث يرمز‫ pk إلى المفتاح الرئيسي Primary Key الذي يمثّل المعرّف id عادةً إن لم نحدّد خلاف ذلك. post = Post.objects.get(pk=1) # استرداد الفئة التي اسمها‫ "New Category" new_category = Category.objects.get(name="New Category") # إسناد المتغير‫ new_category إلى حقل فئة المنشور وحفظه post.category = new_category post.save() توجد أيضًا علاقة متعدد إلى متعدد بين المنشورات والوسوم كما يلي: from blog.models import Tag, Post post1 = Post.objects.get(pk=1) # استرداد المنشور 1 tag1 = Tag.objects.get(pk=1) # استرداد الوسم 1 tag2 = Tag.objects.get(pk=2) # استرداد الوسم 2 tag3 = Tag.objects.get(pk=3) # استرداد الوسم 3 tag4 = Tag.objects.get(pk=4) # استرداد الوسم 4 tag5 = Tag.objects.get(pk=5) # استرداد الوسم 5 post.tags.add(tag1, tag2, tag3, tag4, tag5) # إضافة الوسوم من 1 إلى 5 إلى المنشور 1 استرداد البيانات استرداد الكائنات أكثر تعقيدًا قليلًا مما رأيناه سابقًا، لذا سنوضّح فيما يلي كيف سنجد سجلًا معينًا في قاعدة بياناتنا التي تحتوي على آلاف السجلات إن لم نكن نعرف المعرّف id، وسنوضّح كيفية الحصول على مجموعة من السجلات التي تناسب معايير معينة بدلًا من الحصول على سجل واحد مثلًا. توابع QuerySet تتيح توابع QuerySet استرداد البيانات بناءً على معايير معينة، ويمكن الوصول إليها باستخدام السمة Attribute التي هي objects، حيث يُستخدَم التابع get()‎ الذي رأيناه سابقًا لاسترداد سجل معين كما يلي: first_tag = Tag.objects.get(pk=1) new_category = Category.objects.get(name="New Category") ويمكن أيضًا استرداد جميع السجلات باستخدام التابع all()‎: Post.objects.all() يعيد التابع all()‎ مجموعة من السجلات والتي نسميها مجموعة الاستعلام QuerySet، ويمكنك تحسين هذه المجموعة من خلال سَلسَلة التابع filter()‎ أو التابع exclude()‎ مع التابع all()‎ كما يلي: Post.objects.all().filter(pub_date__year=2024) سيؤدي ذلك إلى إعادة جميع المنشورات المنشورة في عام 2024، ويُسمَّى pub_date__year بوسيط البحث في الحقول Field Lookup، وسنناقش هذا الموضوع بالتفصيل لاحقًا. يمكننا أيضًا استبعاد المنشورات المنشورة عام 2024 كما يلي: Post.objects.all().exclude(pub_date__year=2024) هناك أيضًا العديد من توابع QuerySet الأخرى بالإضافة إلى get()‎ و all()‎ و filter()‎ و exclude()‎، ولكن لن نتحدث عنها جميعًا، لذا اطّلع على القائمة الكاملة لجميع توابع QuerySet من توثيق جانغو الرسمي. وسطاء عمليات البحث في الحقول Field Lookups وسطاء عمليات البحث في الحقول هي وسطاء الكلمات المفتاحية للتوابع get()‎ و filter()‎ و exclude()‎، والتي تعمل بطريقة مشابهة لتعليمة WHERE في لغة SQL، وتأخذ الصيغة fieldname__lookuptype=value مع وجود شرطة سفلية مزدوجة. Post.objects.all().filter(pub_date__lte='2024-01-01') pub_date هو اسم الحقل و lte هو نوع البحث الذي يرمز إلى أقل من أو يساوي، وستُعاد جميع المنشورات التي يكون فيها تاريخ النشر pub_date أقل من أو يساوي ‎2024-01-01‎. يمكن أيضًا استخدام وسطاء عمليات البحث في الحقول للعثور على السجلات التي لها علاقة بالسجل الحالي كما في المثال التالي، وستُعاد جميع المنشورات التي تنتمي إلى الفئة التي اسمها "Django": Post.objects.filter(category__name='Django') يمكننا تطبيق ذلك عكسيًا مثل إعادة جميع الفئات التي تحتوي على منشورٍ واحد على الأقل الذي يحتوي عنوانه على الكلمة "Django" كما يلي: Category.objects.filter(post__title__contains='Django') يمكننا أيضًا المرور عبر علاقات متعددة كما يلي: Category.objects.filter(post__author__name='Admin') يعيد الاستعلام هنا جميع الفئات التي تحتوي على منشورات ينشرها المستخدم Admin، إذ يمكن وضع سلسلة من العلاقات بالعدد الذي نريده. ويمكنك مطالعة جميع وسطاء عمليات البحث في الحقول التي يمكنك استخدامها من توثيق جانغو الرسمي. حذف الكائنات نستخدم التابع delete()‎ لحذف سجلٍ ما، حيث ستحذف الشيفرة البرمجية التالية المنشور الذي له المفتاح الرئيسي pk=1: post = Post.objects.get(pk=1) post.delete() ويمكننا أيضًا استخدامه لحذف سجلات متعددة معًا كما يلي: Post.objects.filter(pub_date__year=2022).delete() سيؤدي ذلك إلى حذف جميع المنشورات في عام 2022، ولكن قد يتعلّق السجل الذي نحذفه بسجل آخر مثل محاولة حذف فئة تحتوي على منشورات متعددة كما يلي: category = Category.objects.get(pk=1) category.delete() يحاكي جانغو سلوك قيد SQL التالي ON DELETE CASCADE، مما يعني حذف جميع المنشورات التي تنتمي إلى هذه الفئة أيضًا، ولكن إذا أردتَ تغيير ذلك، فيمكنك تغيير خيار on_delete إلى شيء آخر، لذا اطّلع على جميع خيارات on_delete المتاحة في توثيق جانغو الرسمي. دالة العرض View Function وضّحنا ما يمكن فعله داخل دالة العرض، وسنوضّح كيف تبدو دالة العرض الكاملة كما في المثال التالي، حيث تُعرَّف جميع العروض ضمن الملف views.py: from django.shortcuts import render from blog.models import Post # أنشئ عروضك الخاصة هنا def my_view(request): posts = Post.objects.all() return render(request, 'blog/index.html', { 'posts': posts, }) هناك شيئان يجب الانتباه إليهما في هذا المثال، أولهما أن دالة العرض تأخذ المتغير request كدخل، وهذا المتغير هو كائن HttpRequest يُمرَّر تلقائيًا إلى العرض من موجّه إرسال Dispatcher عناوين URL. اطّلع على مقال مدخل إلى HTTP لمزيد من المعلومات حول التواصل بين عميل وخادم التطبيق. يحتوي request على الكثير من المعلومات حول طلب HTTP الحالي، فمثلًا يمكننا الوصول إلى تابع طلب HTTP وكتابة شيفرات برمجية مختلفة لتوابع مختلفة كما يلي: if request.method == 'GET': do_something() elif request.method == 'POST': do_something_else() ملاحظة: اطّلع على جميع المعلومات التي يمكنك الوصول إليها من كائن request في توثيق جانغو الرسمي. والشيء الآخر الذي يجب الانتباه إليه هو استيراد اختصار Shortcut اسمه render()‎، ثم استخدامه لتمرير المتغير posts إلى القالب blog/index.html. يُطلق عليه اختصار لأنه من المفترض تحميل القالب افتراضيًا باستخدام التابع loader()‎ وعرض هذا القالب مع البيانات المُسترَدة وإعادة كائن HttpResponse، ولكن بسّط جانغو هذه العملية باستخدام الاختصار render()‎. لن نتحدث عن الطريقة المعقدة لذلك، لأننا لن نستخدمها في هذا المقال ويمكنك مطالعة جميع دوال الاختصار Shortcut Functions في توثيق جانغو الرسمي. نظام قوالب جانغو طبقة القوالب Template هي جزء الواجهة الأمامية لتطبيق جانغو، لذا تكون ملفات القالب هي شيفرات مكتوبة بلغة HTML لأنها تمثّل ما تراه في المتصفح، ولكن قد تكون الأمور أكثر تعقيدًا قليلًا، فإن احتوى القالب على شيفرات HTML فقط، فسيكون موقع الويب ساكنًا بالكامل، ولا نريد ذلك، لذا يجب أن يخبر القالبُ دالةَ العرض بمكان وضع البيانات المسترَدة. عمليات الضبط Configurations يجب أولًا تغيير شيء ما في الملف settings.py، إذ يجب أن تخبر جانغو بمكان وضع ملفات القوالب، لذا لننشئ المجلد templates، حيث اخترنا وضعه ضمن المجلد الجذر للمشروع، ولكن يمكنك نقله لمكان آخر. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles ├── staticfiles └── templates انتقل إلى الملف settings.py وابحث عن TEMPLATES. TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ 'templates', ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] لنعدّل الخيار DIRS الذي يؤشر إلى المجلد templates، ولنتحقق الآن من عمل هذا الإعداد، لذا ننشئ نمط عنوان URL جديد يؤشّر إلى العرض test()‎ في الملف djangoBlog/urls.py كما يلي: from django.urls import path from blog import views urlpatterns = [ path('test/', views.test), ] ولننشئ الآن العرض test()‎ في الملف blog/views.py كما يلي: def test(request): return render(request, 'test.html') انتقل إلى المجلد templates وأنشئ القالب test.html كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test Page</title> </head> <body> <p>This is a test page.</p> </body> </html> شغّل خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/‎، وستظهر الصفحة التالية: لغة قوالب جانغو لنناقش الآن محرّك قوالب جانغو بالتفصيل، وتذكّر أنه يمكننا إرسال البيانات من العرض إلى القالب كما يلي: def test(request): return render(request, 'test.html', { 'name': 'Jack' }) تُسنَد السلسلة النصية 'Jack' إلى المتغير name وتمرَّر إلى القالب، ويمكننا عرض المتغير name ضمن القالب باستخدام أقواس معقوصة مزدوجة {{ }} كما يلي: <p>Hello, {{ name }}</p> حدّث المتصفح، وسترى النتيجة التالية: ولكن لا تكون البيانات المُمرَّرة إلى القالب سلسلة نصية بسيطة في أغلب الحالات كما في المثال التالي: def test(request): post = Post.objects.get(pk=1) return render(request, 'test.html', { 'post': post }) المتغير post في المثال السابق هو قاموس Dictionary، حيث يمكنك الوصول إلى العناصر الموجودة في هذا القاموس في القالب كما يلي: {{ post.title }} {{ post.content }} {{ post.pub_date }} المرشحات Filters تحوّل المرشحات قيم المتغيرات، فمثلًا إذا كان لدينا المتغير django الذي قيمته 'the web framework for perfectionists with deadlines' ووضعنا المرشح title مع هذا المتغير كما يلي: {{ django|title }} فسيُحوَّل القالب إلى ما يلي: The Web Framework For Perfectionists With Deadlines اطّلع على جميع المرشحات المُضمَّنة في جانغو من توثيق جانغو الرسمي. الوسوم Tags تضيف الوسوم ميزات لغات البرمجة مثل التحكم في التدفق والحلقات إلى شيفرة HTML، مما يوفر الكثير من الوقت والموارد، إذ لن نضطر إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا. تُعرَّف جميع الوسوم باستخدام {% %} مثل حلقة for التالية: <ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul> وتكون تعليمة if كما يلي: {% if somevar == "x" %} This appears if variable somevar equals the string "x" {% endif %} وتكون تعليمة if-else كما في المثال التالي: {% if athlete_list %} Number of athletes: {{ athlete_list|length }} {% elif athlete_in_locker_room_list %} Athletes should be out of the locker room soon! {% else %} No athletes. {% endif %} اطّلع على جميع الوسوم المُضمَّنة في جانغو من توثيق جانغو الرسمي، حيث توجد الكثير من المرشحات والوسوم المفيدة الأخرى في نظام قوالب جانغو، والتي سنتحدث عنها لاحقًا. نظام الوراثة Inheritance الفائدة الأساسية لاستخدام قوالب جانغو هي أنك لست بحاجة إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا، فمثلًا يوجد شريط تنقل Navigation Bar وتذييل Footer في جميع صفحات تطبيق الويب النموذجي، ومن الصعب تكرار هذه الشيفرة البرمجية في كل صفحة من صيانة هذا التطبيق، لذا يقدم جانغو طريقة سهلة للغاية لحل هذه المشكلة كما سنوضح فيما يلي. لننشئ الملف layout.html في المجلد templates، ويمثّل هذا الملف المكان الذي نعرّف فيه تخطيط القالب الخاص بنا كما يلي، ولكننا لم نضع الشيفرة البرمجية الخاصة بالتذييل وشريط التنقل لتسهيل القراءة: <!DOCTYPE html> <html> <head> {% block meta %} {% endblock %} ‏<-- ‫استورد شيفرة CSS هنا –!> </head> <body> <div class="container"> <!-- ضع شيفرة شريط التنقل هنا --> {% block content %} {% endblock %} <!-- ضع شيفرة تذييل الصفحة هنا --> </div> </body> </html> لاحظ أننا عرّفنا كتلتين في هذا الملف هما meta و content باستخدام الوسم ‎{% block ... %}‎، وعرّفنا القالب home.html التالي لاستخدام هذا التخطيط: {% extends 'layout.html' %} {% block meta %} <title>Page Title</title> <meta charset="UTF-8"> <meta name="description" content="Free Web tutorials"> <meta name="keywords" content="HTML, CSS, JavaScript"> <meta name="author" content="John Doe"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %} {% block content %} <p>This is the content section.</p> {% include 'vendor/sidebar.html' %} {% endblock %} سيجد جانغو أولًا الملف layout.html عند استدعاء القالب السابق، ويملأ الكتلتين meta و content بالمعلومات الموجودة في صفحة home.html. لاحظ وجود شيء آخر في هذا القالب، حيث تخبر التعليمة ‎{% include 'vendor/sidebar.html' %}‎ جانغو بالبحث عن القالب templates/vendor/sidebar.html ووضعه في الصفحة، حيث يتضمن القالب sidebar.html المحتويات التالية مثلًا: <p>This is the sidebar.</p> لا يُعَد ذلك شريطًا جانبيًا، ولكن يمكننا استخدامه لتوضيح عمل نظام الوراثة، وتأكد أيضًا من صحة العرض كما يلي: from django.shortcuts import render def home(request): return render(request, 'home.html') وتأكّد من أن موجّه إرسال عنوان URL الخاص بك يؤشّر إلى هذا العرض كما يلي: path('home/', views.home), افتح متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/home، ويجب أن ترى الصفحة التالية: ترجمة -وبتصرّف- للمقال Django for Beginners #2 - The MTV Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: جانغو للمبتدئين الجزء الأول: البدء في إنشاء مدونة بسيطة إنشاء تطبيق حديث باستخدام Django و Vue | الجزء الأول: الأساسيات العروض والقوالب في Django - الجزء الأول العروض والقوالب في Django - الجزء الثاني مدخل إلى إطار عمل الويب جانغو Django
  2. يمكن ضرب المصفوفة بمصفوفة عمودية إذا كانت أبعادهما تسمح بذلك، حيث يُعَد ذلك عمليةً أساسيةً في الرسوميات الحاسوبية وفي العديد من المجالات الأخرى؛ إذ تُجرَى هذه العملية ملايين المرات في الثانية عند تشغيل برنامج الرسوميات ثلاثية الأبعاد. سنوضح في هذا المقال المواضيع التالية: أبعاد معامَلات ونتيجة ضرب المصفوفات بمصفوفة عمودية. أبعاد معامَلات ونتيجة ضرب مصفوفة سطرية بالمصفوفات. ضرب المصفوفات بمصفوفة عمودية. ضرب مصفوفة سطرية بالمصفوفات. استخدام الجداء النقطي لضرب المصفوفات. الوصول إلى نتيجة ضرب المصفوفات من خلال تطبيق عمليات متعددة من الجداء النقطي. قلب Flipping المصفوفة العمودية عند ضرب المصفوفات بمصفوفة عمودية. قلب أعمدة المصفوفة عند ضرب مصفوفة سطرية بها. التعريفات الرياضية لعمليات ضرب المصفوفات وخطوات تنفيذها. الخاصية غير التبديلية لضرب المصفوفات. رأينا مسبقًا عمليات شبيهة بالضرب، والتي هي: ضرب عدد حقيقي (مقدار سلمي) Scalar بعدد حقيقي، والذي يعطي عددًا حقيقيًا. ضرب مصفوفة عمودية بعدد حقيقي، والذي يعطي مصفوفة عمودية. الجداء النقطي Dot Product للمصفوفات العمودية، والذي يعطي عددًا حقيقيًا. ضرب المصفوفات بالمصفوفة العمودية ناتج ضرب مصفوفةٍ ما بمصفوفة عمودية هو مصفوفة عمودية كما في المثال التالي: سنشرح لاحقًا تفاصيل كيفية تطبيق هذه العملية، ولكن لاحظ الآن أبعاد معامَلات العملية ونتيجتها؛ إذ يمكن تشكيل عملية ضرب مصفوفة بمصفوفة عمودية إذا كان عدد أعمدة المصفوفة مساويًا لعدد أسطر المصفوفة العمودية كما يلي: Matrix(R x N) x Col. Matrix(N x 1) = Resulting Col. Matrix(R x 1)‎‎ يمكن توضيح ذلك في المثال التالي: Matrix(2‎ x 2) x Col. Matrix(2‎ x 1) = Resulting Col. Matrix(2‎ x 1)‎ نعرض الأبعاد بالشكل: "Row x Column" (بحيث يكون Row هو عدد الصفوف وColumn هو عدد الأعمدة)، إذ تكون المصفوفة التي أبعادها R x 1 مصفوفةً عمودية، لأنها تحتوي على عمود واحد. لاحظ أيضًا كيف تكون أبعاد نتيجة ضرب مصفوفة بمصفوفة عمودية في المثال التالي: Matrix(5 x 5) x Col. Matrix(5 x 1) = Resulting Col. Matrix(5 x 1)‎ لنحدّد الآن ما إذا كان من الممكن ضرب مصفوفةٍ بمصفوفة عمودية وما هي أبعاد نتيجة عملية الضرب في الأمثلة الموجودة في الجدول التالي: أبعاد المصفوفة أبعاد المصفوفة العمودية أبعاد النتيجة الأبعاد 3‎ x 3 الأبعاد 3‎ x 1 الأبعاد 3‎ x 1 الأبعاد 3‎ x 2 الأبعاد 2‎ x 1 الأبعاد 3‎ x 1 الأبعاد 2‎ x 3 الأبعاد 2‎ x 1 غير ممكنة الأبعاد 4‎ x 3 الأبعاد 3‎ x 1 الأبعاد 4‎ x 1 الأبعاد 3‎ x 5 الأبعاد 5‎ x 1 الأبعاد 3‎ x 1 لاحظ أنه من غير الممكن تشكيل عملية الضرب التي تبدو كما يلي: ‎Matrix(2 x 1) x Col. Matrix(2 x 2) = Resulting Col. Matrix‎‎(غير ممكنة) وضع المصفوفة العمودية على الجهة اليمنى فقط من عملية الضرب إذا ضربنا مصفوفةً مستطيلةً بمصفوفة عمودية، فستكون المصفوفة العمودية دائمًا على يمين المصفوفة المستطيلة؛ إذ لا يمكن ضربهما إّلا إذا كان "البعد الداخلي Inner Dimension" (البعد M مثلًا) لكل مصفوفة متساويًا: Matrix(N x M) x Col. Matrix(M x 1) = Resulting Col. Matrix(N x 1)‎ حيث يمكن تشكيل عملية الضرب التي تبدو كما يلي: ‎Matrix(1 x 2) x Col. Matrix(2 x 1) = عدد حقيقي‎‎(1 x 1) استخدام الجداء النقطي لضرب المصفوفات يبدو استخدام الجداء النقطي لضرب المصفوفات ممكنًا من خلال النظر إلى الأبعاد المصفوفات، ولكنه ليس دليلًا على إمكانية ذلك. إليك المثال التالي: قد يبدو غريبًا بعض الشيء عَدُّ العدد الحقيقي (المقدار السلمي Scalar) كائنًا أبعاده "1 × 1"، ولكن تبقي هذه الطريقة الأمور متناسقةً فيما بينها. لاحظ أن هذا المثال يشبه الجداء النقطي (وهو كذلك فعلًا)، وبالتالي ستتشكّل عملية الضرب كما يلي: إذًا لنوجد الآن ناتج عملية الضرب التالية كما يلي: قلب المصفوفة العمودية من المفيد التفكير في "قلب" المصفوفة العمودية عند تشكيل الجداء النقطي ليحاذي كل صف من المصفوفة، ثم ضرب وجمع العناصر المتقابلة كما في المثال التالي: وفي المثال التالي أيضًا: لنشكّل الآن عملية الضرب التالية: تدريب عملي اُحسب الجداء النقطي في الجدول التالي. ستجد ناتج الجداء في عمود النتيجة: المعامل الأول المعامل الثاني النتيجة المصفوفة المصفوفة العمودية ‎‎ الناتج ‎-2 المصفوفة المصفوفة العمودية ‎‎ الناتج ‎-2 المصفوفة المصفوفة العمودية ‎‎ الناتج ‎2 سنستخدم عمليات الجداء النقطي السابقة لإيجاد نتيجة ضرب المصفوفة بالمصفوفة العمودية كما يلي: إذًا يمكننا الحصول على ناتج عملية ضرب مصفوفةٍ بمصفوفة عمودية من خلال تطبيق الجداء النقطي للمصفوفة العمودية مع كل صف من المصفوفة الأخرى، ولكن يجب أن تسمح أبعاد كل معامَل بذلك؛ إذ يساعد ذلك على قلب المصفوفة العمودية، بحيث يمكن محاذاة عناصرها مع عناصر المصفوفة كما يلي: اقلب المصفوفة العمودية واحسب النتيجة ما يلي: إليك أيضًا بعض عمليات الضرب الأخرى التي قد ترغب في التدرب عليها، ولكن تذكّر أن تقلب المصفوفة العمودية وتضعها فوق المصفوفة، ثم طبّق الجداء النقطي للمصفوفة العمودية التي قلبناها مع صف واحد من المصفوفة في كل مرة: العملية النتيجة الناتج ‎‎ الناتج الناتج ‎ استخدام عمليات متعددة من الجداء نقطي لإجراء عملية ضرب المصفوفة السطرية بالمصفوفات جرّب الآن إجراء العملية التالية: لاحظ أن أبعاد المصفوفات متوافقة، لذا يمكن إجراء العملية كما يلي: يمكنك أن ترى من المثال السابقة أنه يمكن إجراء عملية ضرب مصفوفةٍ سطرية بمصفوفةٍ ما من خلال تطبيق عدة عمليات جداء نقطية، ولكن يجب قلب أعمدة المصفوفة في هذه الحالة كما يلي: وضع المصفوفة السطرية على الجهة اليسرى من عملية الضرب لا يمكن إجراء النوع التالي من عمليات الضرب، لأن الأبعاد غير صحيحة: Matrix (3 x 4) x Row Matrix (1 x 3)‎ إذًا يجب أن تكون المصفوفة السطرية على الجهة اليسرى عند ضرب مصفوفة سطرية بمصفوفة مستطيلة، حيث تكون أبعاد المعامَلات والنتيجة كما يلي: Row Matrix (1 x M) x Matrix (M x C) = Result Row Matrix (M x C)‎ ملاحظة: المصفوفة التي أبعادها 1xC هي مصفوفة سطرية مكوَّنة من عددٍ "C" من الأعمدة. نوضّح في الأمثلة الموجودة في الجدول التالي ما إذا كانت عملية الضرب ممكنة وما هي أبعاد نتيجة العملية: المعامل الأول المعامل الثاني النتيجة 3‎ x 3 ‫ 3‎ x 1 ‫ 3‎ x 1 1‎ x 2 ‫ 2‎ x 2 ‫ 1‎ x 2 1‎ x 2 ‫ 2‎ x 3 ‫ 1‎ x 3 4‎ x 4 ‫ 3‎ x 1 ‫ غير ممكنة 1‎ x 5 ‫ 5‎ x 3 ‫ 1‎ x 3 لنحاول الآن إجراء عملية الضرب التالية: وينتج ما يلي: التعريفات الدقيقة لعمليات الضرب قد تشعر بعدم الارتياح عند تعريف العمليات الرياضية بهذه الطريقة غير الرسمية، لذا إليك بعض التعريفات الأدق: ضرب مصفوفة بمصفوفة عمودية Matrix times Column Matrix: ليكن لدينا المصفوفة A التي أبعادها M x C، والمصفوفة العمودية x التي أبعادها C x 1، ويكون ناتج الجداء Ax مصفوفة عمودية أبعادها M x 1؛ إذ يتشكّل عنصرها i من ناتج الجداء النقطي للصف i من المصفوفة A مع المصفوفة العمودية x. ضرب مصفوفة سطرية بمصفوفة: ليكن لدينا المصفوفة السطرية x التي أبعادها 1‎ x R والمصفوفة **A التي أبعادها R x M، ويكون ناتج الجداء *xA* مصفوفة سطرية أبعادها 1‎ x M، حيث يتشكّل عنصرها الأول من ناتج الجداء النقطي للمصفوفة السطرية x مع العمود i من المصفوفة A. لنحاول الآن إجراء عملية الضرب التالية: وسينتج لدينا ما يلي: خطوات إجراء عملية ضرب المصفوفات يتضمن الضرب دائمًا عمليات جداء نقطية للصفوف الموجودة على اليسار والأعمدة الموجودة على اليمين وفقًا للتعريفات السابقة، لذا يجب اتباع الخطوات التالية لإجراء أيٍّ من هذه الحسابات: دراسة أبعاد معاملات عملية الضرب حيث: يجب أن تكون الأبعاد الداخلية متساوية: 1xN و NxC. نتجاهل الأبعاد الداخلية للعثور على أبعاد النتيجة: 1xC. نكتب مصفوفة فارغة بالأبعاد الصحيحة لتمثّل المصفوفة الناتجة. ملء جميع عناصر المصفوفة الناتجة كما يلي: نقلب كل عمود موجود على اليمين إلى صف على اليسار. ينتج كل عنصر من عناصر المصفوفة الناتجة عن عملية جداء نقطي. تدريب عملي لا تُعَد Ax و xA متساويتين؛ فإذا كان تطبيق عملية الضرب Ax ممكنة، فستكون أبعاد xA غير متوافقة عند ذلك، ويمكن في بعض الأحيان تشكيل كلٍّ من هاتين العمليتين، ولكن النتائج ستكون مختلفة، وبالتالي يمكن القول أن ضرب المصفوفات ليست عمليةً تبديلية. إذًا لنختبر مهاراتك في الأمثلة التالية: العملية النتيجة الناتج الناتج الناتج وصلنا إلى نهاية هذا المقال الذي تعرّفنا من خلاله على ضرب مصفوفة بمصفوفة عمودية وضرب مصفوفة سطرية بمصفوفةٍ ما، وسنتعرّف في المقال التالي على كيفية ضرب مصفوفة مستطيلة بمصفوفة أخرى. ترجمة -وبتصرُّف- للفصل Matrix-Column Matrix Multiplicaton من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد
  3. جانغو Django هو إطار عمل ويب عالي المستوى مجاني ومفتوح المصدر ومكتوب بلغة البرمجة بايثون Python، ويُستخدَم على نطاق واسع لبناء تطبيقات ويب معقدة، وهو معروف بقدرته على التعامل مع حركة المرور العالية وميزات الأمان الخاصة به ومكوناته القابلة لإعادة الاستخدام. يتبع جانغو نمط معمارية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا) ويأتي مع واجهة مُدمَجة لإدارة الموقع مع دعم الاستيثاق Authentication وربط الكائنات بالعلاقات Object-Relational Mapping (أو ORM اختصارًا) الذي يسمح بتعريف مخطط قاعدة بياناتك باستخدام أصناف Classes بايثون. يتمتع جانغو بمجتمع كبير ونشط يوفر مجموعة كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ تُبنَى العديد من المواقع الإلكترونية المعروفة وذات حركة المرور العالية -مثل إنستغرام Instagram وبنترست Pinterest وموقع واشنطن تايمز The Washington Times- باستخدام إطار عمل جانغو. يُعَد جانغو أحد أكثر أطر عمل الويب شعبيةً وقوة للغة بايثون، وتستخدمه كلٌّ من الشركات الناشئة والكبيرة على نطاق واسع، مما يجعله خيارًا رائعًا لبناء تطبيقات ويب قوية وقابلة للتوسّع، وتؤدي مرونته وقابليته للتوسع إلى جعله الخيار الأفضل للمطورين والمؤسسات التي تريد إنشاء تطبيقات ويب معقدة وصغيرة الحجم. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة تثبيت الأدوات اللازمة توجد بعض الأدوات التي يجب تثبيتها على جهازك قبل البدء، حيث تحتاج مبدئيًا لغة برمجة (بايثون)، وقاعدة بيانات (SQLite)، وخادم (سنستخدم خادم التطوير المُدمَج مع جانغو) لتشغيل التطبيق بنجاح. تذكّر أن هذه الأدوات مُخصَّصة لبيئة التطوير Dev Environment المخصصة لإنشاء واختبار التطبيق فقط، لذا لا يجب استخدام هذه الأدوات في بيئة الإنتاج Production Environment. تحتاج أيضًا إلى بيئة تطوير متكاملة IDE مثل بيئة باي تشارم PyCharm أو محرّر شيفرات برمجية على الأقل، حيث سنستخدم في هذا المقال المحرّر VS Code لأنه برنامج مجاني. اتبّع الآن الخطوات التالية للحصول على هذه الأدوات: تنزيل بايثون. تنزيل SQLite. تنزيل VS Code. تنزيل PyCharm. إنشاء مشروع جانغو جديد حان الوقت لبدء كتابة الشيفرة البرمجية بعد تثبيت الأدوات السابقة، لذا أنشئ أولًا مجلد عمل جديد باستخدام الأمر التالي لتنظيم كل شيء: mkdir django-demo ثم انتقل إلى هذا المجلد باستخدام الأمر التالي: cd django-demo أنشئ بعد ذلك بيئة افتراضية جديدة للغة بايثون ضمن مجلد العمل باستخدام الأمر التالي، حيث ستكون هذه البيئة الافتراضية معزولة عن بيئة بايثون في نظامك ومشاريع بايثون الأخرى التي قد تكون على جهازك. python -m venv env إذا استخدمتَ نظام لينكس Linux أو ماك macOS، فقد تضطر إلى تشغيل أمر python3 بدلًا من python، ولكننا سنستخدم python للتبسيط. سيعمل الأمر السابق على إنشاء المجلد env الذي يحتوي على البيئة الافتراضية التي أنشأتها، ويمكنك تنشيط هذه البيئة الافتراضية باستخدام الأمر التالي: source env/bin/activate وإذا استخدمتَ نظام ويندوز Windows، فاستخدم الأمر التالي بدلًا من ذلك: env/Scripts/activate إذا نشطت البيئة الافتراضية بنجاح، فسيكون موجّه سطر الأوامر الخاص بك كما يلي: (env) eric@djangoDemo:~/django-demo$ ملاحظة: يعني (env) هنا أنك تعمل حاليًا في بيئة افتراضية اسمها env. حان الوقت الآن لتهيئة مشروع جانغو جديد، حيث تحتاج إلى تثبيت حزمة Django من خلال تشغيل الأمر التالي: python -m pip install Django ثم يمكنك استخدام أمر django-admin لإنشاء مشروع جانغو جديد كما يلي: django-admin startproject djangoBlog وسينشَأ المجلد الجديد djangoBlog التالي: يُفضَّل إعادة هيكلة المشروع بعض الشيء بحيث نبدأ من المجلد الجذر للمشروع كما يلي، ولكن لا حاجة لذلك إن لم ترغب في ذلك. إنشاء تطبيق المدونة لا يزال المشروع فارغًا حاليًا، ولاحظ أن بنية هذا المشروع بسيطة مقارنةً ببنية مشروع لارافيل Laravel، وسنناقش كل ملف في مجلد المشروع بالتفصيل لاحقًا. يتيح جانغو إنشاء تطبيقات متعددة في مشروع واحد، فمثلًا يمكن أن يكون هناك تطبيق blog وتطبيق gallery وتطبيق forum ضمن مشروع واحد. يمكن أن تتشارك هذه التطبيقات في نفس الملفات الثابتة (ملفات CSS وجافاسكربت JavaScript) والصور ومقاطع الفيديو أو يمكن أن تكون مستقلة تمامًا عن بعضها البعض، إذ يعتمد ذلك على احتياجاتك الخاصة. سننشئ في هذا المقال تطبيق مدونة blog فقط، لذا نفّذ الأمر التالي في الطرفية: python manage.py startapp blog يجب أن ترى مجلد blog جديد ضمن مجلد جذر المشروع، ويمكنك سرد محتوى المجلد باستخدام الأمر التالي: ls وإذا أردتَ تضمين الملفات المخفية أيضًا في النتيجة، فاستخدم الأمر التالي: ls -a وإذا أردتَ رؤية بنية الملفات فاستخدم الأمر التالي: tree ولكن قد تحتاج إلى تثبيت برنامج tree ليعمل الأمر السابق بنجاح اعتمادًا على نظام تشغيلك. سنستخدم الأمر السابق لعرض بنية الملفات، حيث يجب أن يكون لمشروعك البنية التالية حاليًا: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py يجب بعد ذلك تسجيل تطبيق blog الجديد في جانغو، لذا انتقل إلى الملف settings.py وابحث عن القائمة INSTALLED_APPS: INSTALLED_APPS = [ 'blog', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] بدء تشغيل الخادم يمكنك الآن بدء تشغيل خادم التطوير لاختبار نجاح عمل كل شيء، لذا افتح الطرفية وشغّل الأمر التالي: python manage.py runserver وسيظهر الخرج التالي في واجهة سطر الأوامر: Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. October 19, 2022 - 01:39:33 Django version 4.1.2, using settings 'djangoBlog.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. افتح المتصفح وانتقل إلى العنوان http://127.0.0.1:8000/‎، وستظهر الصفحة التالية: بنية التطبيق لنتحدث عن البنية التالية لتطبيق جانغو الجديد وما يفعله كل ملف قبل أن نبدأ في كتابة الشيفرة البرمجية: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py المجلد الجذر يحتوي المجلد الجذر على الملفات والمجلدات التالية: manage.py: يمثل أداة سطر الأوامر التي تتيح لك التفاعل مع مشروع جانغو بطرق مختلفة. djangoBlog: مجلد المشروع الرئيسي الذي يحتوي على ملف الإعدادات ونقطة الدخول إلى المشروع. blog: يمثل تطبيق المدونة blog. مجلد المشروع يحتوي هذا المجلد على الملفات التالية: djangoBlog/__init__.py: ملف فارغ يخبر بايثون بأن هذا المجلد يجب عَدّه حزمة بايثون. djangoBlog/settings.py: ملف الإعدادات أو الضبط Configuration لمشروع جانغو. djangoBlog/urls.py: يحتوي على التصريحات عن عناوين URL لمشروع جانغو، ويمثل جدول محتويات تطبيق جانغو الخاص بك، حيث سنتحدث عنه أكثر لاحقًا. djangoBlog/asgi.py: يُعَد نقطة الدخول لخوادم الويب المتوافقة مع واجهة ASGI لتخديم مشروعك. djangoBlog/wsgi.py: نقطة الدخول لخوادم الويب المتوافقة مع واجهة WSGI لتخديم مشروعك. مجلد التطبيق يحتوي هذا المجلد على المجلدات والملفات التالية: blog/migrations: يحتوي هذا المجلد على جميع ملفات التهجير Migration لتطبيق المدونة، حيث ينشئ جانغو هذه الملفات تلقائيًا بناءً على نماذجك Models على عكس إطار عمل لارافيل. blog/admin.py: يأتي جانغو أيضًا مع لوحة مُدمَجة لإدارة الموقع، حيث يحتوي هذا الملف على جميع عمليات الضبط الخاصة بهذه اللوحة. blog/models.py: تصف النماذج Models بنية وعلاقة قاعدة البيانات، وتُنشَأ ملفات التهجير بناءً على هذا الملف. blog/views.py: يكافئ المتحكمات Controllers في لارافيل، ويحتوي على المنطق البرمجي الأساسي لهذا التطبيق. ضبط Configuring مشروع جانغو توجد بعض التغييرات التي يجب إجراؤها على الملف settings.py قبل البدء بالشروع كما سنوضح فيما يلي. المضيفون المسموح بهم تُعد القائمة ALLOWED_HOSTS قائمة بالنطاقات Domains التي يُسمَح لموقع جانغو بتخديمها، وهي إجراء أمني لمنع هجمات ترويسات مضيف HTTP أو HTTP Host Header Attacks، والتي يمكن حدوثها حتى عند إجراء العديد من عمليات ضبط خادم الويب الآمنة ظاهريًا. لاحظ أنه ما زال بإمكاننا الوصول إلى موقعنا باستخدام المضيف 127.0.0.1 حتى وإن كانت القائمة ALLOWED_HOSTS فارغة حاليًا، والسبب هو التحقق من صحة المضيف مقابل ‎['.localhost', '127.0.0.1', '[::1]']‎ عندما تكون قيمة DEBUG هي True وتكون القائمة ALLOWED_HOSTS فارغة. قاعدة البيانات DATABASES هو قاموس يحتوي على إعدادات قاعدة البيانات التي يحتاج موقعنا إلى استخدامها، حيث يستخدم جانغو قاعدة بيانات SQLite افتراضيًا، وهي قاعدة بيانات خفيفة الوزن وتتكون من ملف واحد فقط. يجب أن تكون قاعدة بيانات SQLite كافيةً لمشروعنا التجريبي الصغير، لكنها لن تناسب المواقع الكبيرة، لذا إذا أردتَ استخدام قواعد بيانات أخرى، فإليك المثال التالي: DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "<database_name>", "USER": "<database_user_name>", "PASSWORD": "<database_user_password>", "HOST": "127.0.0.1", "PORT": "5432", } } ملاحظة: يوصي جانغو باستخدام قاعدة بيانات PostgreSQL في بيئة الإنتاج. توجد العديد من البرامج التعليمية التي تعلمك كيفية استخدام جانغو مع قاعدة بيانات MongoDB، ولكنها تُعَد فكرة سيئة، إذ قد تكون قاعدة بيانات MongoDB حلًا رائعًا، ولكنها لا تعمل بطريقة جيدة مع إطار عمل جانغو. الملفات الساكنة Static Files وملفات الوسائط Media Files يجب أن نهتم أيضًا بالملفات الساكنة وملفات الوسائط، فالملفات الساكنة هي ملفات CSS وملفات جافاسكربت، وملفات الوسائط هي الصور ومقاطع الفيديو والأمور الأخرى التي قد يرفعها المستخدم. الملفات الساكنة Static Files يجب أولًا تحديد مكان تخزين هذه الملفات، حيث سننشئ المجلد static ضمن تطبيق المدونة لموقع جانغو الخاص بنا، ويُعَد هذا المجلد هو المكان الذي نخزّن فيه الملفات الساكنة لتطبيق blog أثناء التطوير. blog ├── admin.py ├── apps.py ├── __init__.py ├── migrations ├── models.py ├── static ├── tests.py └── views.py تختلف الأمور بعض الشيء في بيئة الإنتاج، إذ يجب أن ننشئ مجلدًا مختلفًا ضمن المجلد الجذر للمشروع، ولنسمّيه بالاسم staticfiles. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py └── staticfiles ثم يجب تحديد هذا المجلد في الملف settings.py كما يلي: STATIC_ROOT = "staticfiles/" نخبر بعد ذلك جانغو بعنوان URL الذي يجب استخدامه عند الوصول إلى هذه الملفات في المتصفح. ليس من الضروري أن يكون هذا العنوان هو ‎/static، ولكن تأكد من أنه لا يتداخل مع عمليات ضبط عناوين URL الخاصة بنا والتي سنتحدث عنها لاحقًا. STATIC_URL = "static/" ملفات الوسائط Media Files نضبط ملفات الوسائط باستخدام الطريقة نفسها، حيث يمكنك إنشاء المجلد mediafiles في المجلد الجذر للمشروع كما يلي: . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles └── staticfiles ثم نحدّد الموقع وعنوان URL في الملف settings.py كما يلي: # ملفات الوسائط MEDIA_ROOT = "mediafiles/" MEDIA_URL = "media/" موجه إرسال Dispatcher عناوين URL في جانغو يحتوي مجال تطوير الويب على بنية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، حيث يكون النموذج Model في هذه البنية مسؤولًا عن التفاعل مع قاعدة بياناتنا، ويجب أن يقابل كلُّ نموذج جدولَ قاعدة بيانات واحد. العرض View هو جزء الواجهة الأمامية Frontend من التطبيق، وهو ما يمكن للمستخدمين رؤيته، والمتحكم Controller هو المنطق البرمجي للواجهة الخلفية للتطبيق مثل استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. جانغو هو إطار عمل ويب مُصمَّم بناءً على بنية MVC مع مصطلحات مختلفة، فبنية جانغو هي بنية نموذج-قالب-عرض Model-Template-View (أو MTV اختصارًا)، فالقالب Template هو الواجهة الأمامية، والعرض هو المنطق البرمجي للواجهة الخلفية. سنركّز في هذا المقال على فهم هذه الطبقات، ولكن يجب أولًا البدء بنقطة الدخول لكل تطبيق ويب، والتي هي موجّه إرسال Dispatcher عناوين URL، حيث يقرأ هذا الموجّه عنوان URL ويوجّه المستخدم إلى الصفحة الصحيحة عندما يكتب المستخدم عنوان URL ويضغط على مفتاح Enter. عمليات ضبط عناوين URL الأساسية يُخزَّن ضبط عناوين URL في الملف example/urls.py كما يلي: djangoBlog ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py يوضّح المثال التالي ما يجب أن يبدو عليه موجّه إرسال عناوين URL: from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ] يقرأ موجّه الإرسال المعلومات من عنوان URL ويعيد عرضًا، ولكن لا تخلط بين هذا العرض وعرض إطار عمل لارافيل، فالعرض في جانغو يمثّل متحكّمًا في لارافيل. إذا تبع النطاقُ admin/‎ في هذا المثال، فسيوجّه موجّه الإرسال المستخدم إلى صفحة المدير Admin. تمرير المعاملات باستخدام موجه إرسال عناوين URL في جانغو يكون من الضروري في بعض الأحيان أخذ مقاطع من عنوان URL وتمريرها إلى العرض بوصفها معاملات إضافية، فمثلًا إذا أردتَ عرض صفحة التدوينة، فستحتاج إلى تمرير المعاملَين id أو slug الخاصين بالتدوينة إلى العرض حتى تتمكّن من استخدام هذه المعلومات الإضافية للعثور على التدوينة التي تبحث عنها. إليك فيما يلي الطريقة التي يمكننا تمرير المعاملات باستخدامها: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] ستلتقط الأقواس الزاويّة جزءًا من عنوان URL كمعامل، حيث يُسمَّى int على الجانب الأيسر بمحوِّل المسار Path Converter الذي يلتقط معاملًا من النوع الصحيح، ويمكن مطابقة أيّ سلسلة نصية باستثناء المحرف / عند عدم تضمين المحوّل. لاحظ وجود اسم المعامل على الجانب الأيمن، وهو ما سنحتاج إلى استخدامه في العرض. تتوفّر محوّلات المسارات التالية افتراضيًا: str: يطابق أيّ سلسلة نصية غير فارغة باستثناء فاصل المسار '/' افتراضيًا عند عدم تضمين محوّلٍ في التعبير. int: يطابق الصفر أو أيّ عدد صحيح موجب، ويعيد قيمة من النوع int. slug: يطابق أيّ سلسلة نصية للاسم المختصر Slug، والذي يتكون من حروف أو أرقام ASCII، بالإضافة إلى محارف الشرطة والشرطة السفلية مثل building-your-1st-django-site. uuid: يطابق معرّف UUID المنسَّق، حيث يجب تضمين شرطات، ويجب أن تكون الحروف صغيرة لمنع ربط عناوين URL متعددة مع الصفحة نفسها مثل المعرّف 075194d3-6885-417e-a8a8-6c931e272f00، ويعيد نسخةً من UUID. path: يطابق أي سلسلة نصية غير فارغة مع فاصل المسار '/'، مما يتيح لك المطابقة مع مسار URL كامل بدلًا من المطابقة مع مقطعٍ من مسار URL كما هو الحال مع المحوِّل str. استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL قد يكون النمط Pattern الذي تريد مطابقته أكثر تعقيدًا في بعض الحالات، وبالتالي يمكنك استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL. يجب استخدام التابع re_path()‎ بدلًا من التابع path()‎ لاستخدام التعابير النمطية: from django.urls import path, re_path from . import views urlpatterns = [ path('articles/2003/', views.special_case_2003), re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail), ] استيراد عمليات ضبط عناوين URL الأخرى لنفترض أن لديك مشروع جانغو يحتوي على تطبيقات متعددة مختلفة، حيث إذا وضعت جميع عمليات ضبط عناوين URL في ملف واحد، فسيكون الأمر فوضويًا جدًا، ولكن يمكنك فصل عمليات ضبط عناوين URL ونقلها إلى تطبيقات مختلفة، فمثلًا يمكننا في مشروعنا إنشاء ملف urls.py جديد ضمن تطبيق blog كما يلي: from django.urls import path, include urlpatterns = [ path('blog/', include('blog.urls')) ] وهذا يعني أنه إذا احتوى عنوان URL على النمط http://www.mydomain.com/blog/xxxx، فسينتقل جانغو إلى الملف blog/urls.py ويطابق باقي عنوان URL. لنعرّف الآن أنماط عناوين URL لتطبيق blog باستخدام الطريقة نفسها كما يلي: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] سيتطابق هذا النمط مع نمط عنوان URL الذي هو http://www.mydomain.com/blog/post/123. تسمية أنماط عناوين URL يمكنك تسمية نمط عنوان URL من خلال إعطائه معاملًا ثالثًا كما يلي: urlpatterns = [ path('articles/<int:year>/', views.year_archive, name='news-year-archive'), ] يؤدي ذلك إلى القدرة على عكس عناوين URL المطلقة من القالب، حيث سنتحدث عن ذلك لاحقًا عندما نصل إلى طبقة القوالب. تعرّفنا على أساسيات موجّه إرسال عناوين URL، ولكن لا زال هناك شيء لم نتطرق لشرحه وهو كيفية التمييز بين التوابع المختلفة لطلبات HTTP، حيث لا يمكنك في جانغو مطابقة توابع HTTP المختلفة ضمن موجه إرسال عناوين URL، إذ تتطابق جميع التوابع مع نمط عنوان URL نفسه، وبالتالي يجب استخدام التحكم في التدفق لكتابة شيفرات برمجية مختلفة لهذه التوابع في دوال العرض، حيث سنناقش ذلك بالتفصيل في المقالات اللاحقة من هذه السلسلة. ترجمة -وبتصرّف- للمقال Django for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا حزم بايثون الثمانية التي تسهل تعاملك مع Django مدخل إلى إطار عمل الويب جانغو Django تعرف على أمان تطبيقات جانغو بناء تطبيق مهام باستخدام جانغو Django وريآكت React
  4. سننشئ في هذا المقال الأخير من هذه السلسلة جزء الواجهة الأمامية من التطبيق، ولكن لنضع خطة أولًا، حيث سيكون لدينا الصفحة الرئيسية لتطبيق المدونة التي تعرض قائمة بجميع المنشورات الحديثة، وصفحة فئة Category المنشور التي تعرض قائمة المنشورات ضمن فئة معينة، وصفحة الوسم Tag التي تعرض قائمة المنشورات التي لها وسم مُحدَّد، وصفحة المنشور التي تعرض محتوى منشور معين، وصفحة بحث تعرض قائمة المنشورات بناءً على استعلام بحث معين. كما ستحتوي جميع هذه الصفحات على شريط جانبي مع مربع بحث وقائمة الفئات والوسوم، وستحتوي صفحة المنشور أيضًا على قسم المنشورات ذات الصلة في نهايتها. تحدّثنا عن قاعدة البيانات والنماذج Models في المقال السابق، وسنبدأ الآن بالوِجهات Routes. إنشاء الوِجهات ننشئ الوِجهات في لارافيل ضمن الملف routes/web.php كما يلي: . . . // الصفحة الرئيسية Route::get('/', [PostController::class, 'home'])->name('home'); // قائمة المنشورات ضمن هذه الفئة Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category'); // قائمة المنشورات التي لها هذا الوسم Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag'); // عرض منشور واحد Route::get('/post/{post}', [PostController::class, 'post'])->name('post'); // قائمة المنشورات بناء على استعلام البحث Route::post('/search', [PostController::class, 'search'])->name('search'); . . . الصفحة الرئيسية يكون لكل من هذه الوِجهات تابع تحكم مقابل، حيث سنبدأ أولًا بالمتحكم home()‎ إذًا لنضع ما يلي في الملف app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { /** * عرض الصفحة الرئيسية */ public function home(): View { $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('home', [ 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } هناك شيئان يجب عليك ملاحظتهما في السطر 20 من الشيفرة البرمجية السابقة وهما: أولًا، تتأكد التعليمة where('is_published', true)‎ من استرداد المقالات المنشورة فقط. ثانيًا، يُعَد التابع paginate()‎ أحد توابع لارافيل Laravel المُضمَّنة، مما يسمح لك بإنشاء ترقيم للصفحات Pagination في تطبيقك بسهولة. يأخذ التابع paginate()‎ عددًا صحيحًا كدخل، فمثلًا يمثّل paginate(10)‎ عرض 10 عناصر في كل صفحة، وسيُستخدَم متغير الدخل في العديد من الصفحات، لذا يمكنك إنشاء متغير بيئة في ملف ‎.env كما يلي، ثم يمكنك استرداده في أيّ مكان باستخدام التابع env()‎: . . . PAGINATE_NUM=12 لننشئ الآن عرض View الصفحة الرئيسية المقابل. اطّلع على بنية القوالب التي أنشأئاها، وإليك بنية العروض التالية: resources/views ├── category.blade.php ├── home.blade.php ├── layout.blade.php ├── post.blade.php ├── search.blade.php ├── tag.blade.php ├── vendor │ ├── list.blade.php │ └── sidebar.blade.php └── welcome.blade.php سنبدأ أولًا بالعرض layout.blade.php الذي يمثل تخطيط الصفحة كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('title') </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="{{ route('dashboard') }}" class="hover:underline hover:underline-offset-1" >Dashboard</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </body> </html> يستخدم لارافيل الأداة Vite افتراضيًا لتجميع أصول المشروع كما في السطر 8، وثبّتنا الحزمة Breeze من لارافيل في المقال السابق، وتستخدم هذه الحزمة إطار عمل Tailwind CSS، حيث سيستورد السطر 8 من الشيفرة البرمجية السابقة تلقائيًا الملفين app.css و app.js المقابلَين. يمكنك استخدام إطار عمل مختلف أيضًا، ولكن يجب أن تعود إلى توثيقه للحصول على تفاصيل حول كيفية استخدامه مع لارافيل أو Vite. لننشئ الآن عرض الصفحة الرئيسية resources/views/home.blade.php كما يلي: @extends('layout') @section('title') <title>Home</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ويكون عرض قائمة المنشورات resources/views/vendor/list.blade.php كما يلي: <!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> @foreach ($posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}" >Read more →</a > </div> </div> @endforeach </div> {{ $posts->links() }} يولّد السطر 9 Storage::url($post->cover)‎ عنوان URL الذي يؤشّر إلى صورة الغلاف. لا تُعَد الطريقة التي يخزن بها لارافيل العلامات الزمنية Timestamps سهلة الاستخدام، لذلك استخدمنا حزمة Carbon في السطر 14 لإعادة تنسيق العلامات الزمنية. استخدمنا أيضًا الدالة strip_tags()‎ في السطر 18 لإزالة وسوم HTML، ثم تحدِّد الدالة limit()‎ الحد الأقصى لطول السلسلة، وستوضَع نقاط ... مكان الجزء الزائد منها. استخدمنا التابع paginate()‎ لإنشاء مرقّم الصفحات في المتحكم، ولكن يمكننا عرض مرقّم الصفحات في العرض باستخدام التعليمة ‎{{ $posts->links() }}‎ الموجودة في السطر 30. لننشئ الآن عرض الشريط الجانبي resources/views/vendor/sidebar.blade.php كما يلي: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{{ route('search') }}" method="POST" class="grid grid-cols-4 gap-2" > {{ csrf_field() }} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> @foreach ($categories as $category) <li> <a href="{{ route('category', ['category' => $category->id]) }}" class="text-blue-500 hover:underline" >{{ $category->name }}</a > </li> @endforeach </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> @foreach ($tags as $tag) <span class="mr-2" ><a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" >{{ $tag->name }}</a ></span > @endforeach </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">...</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> </div> صفحة الفئة Category ضع ما يلي في ملف المتحكم app/Http/Controllers/CategoryController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الفئة */ public function category(string $id): View { $category = Category::find($id); $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('category', [ 'category' => $category, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الفئة المقابل resources/views/category.blade.php كما يلي: @extends('layout') @section('title') <title>Category - {{ $category->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة الوسم Tag ضع ما يلي في ملف المتحكم app/Http/Controllers/TagController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الوسم */ public function tag(string $id): View { $tag = Tag::find($id); $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('tag', [ 'tag' => $tag, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الوسم المقابل resources/views/tag.blade.php كما يلي: @extends('layout') @section('title') <title>Tag - {{ $tag->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة المنشور ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض المنشور المطلوب */ public function post(string $id): View { $post = Post::find($id); $categories = Category::all(); $tags = Tag::all(); $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) { return $query->whereIn('name', $post->tags->pluck('name')); })->where('id', '!=', $post->id)->take(3)->get(); return view('post', [ 'post' => $post, 'categories' => $categories, 'tags' => $tags, 'related_posts' => $related_posts ]); } . . . } لاحظ في السطور من 27 إلى 29 أن هذه هي الطريقة التي يمكننا من خلالها استرداد المنشورات ذات الصلة، حيث نريد الحصول على المنشورات التي يكون لها الوسوم نفسها. قد تبدو سلسلة هذه التوابع مخيفة نوعًا ما، لكن لا تقلق بشأن ذلك، حيث سنوضّحها واحدًا تلو الآخر كما يلي: يعيد التابع الأول where('is_published', true)‎ جميع المنشورات التي نشرناها. تتعقد الأمور بعض الشيء عند التابع whereHas()‎، حيث إذا أردنا فهم هذا التابع، فيجب أن نتحدث أولًا عن التابع has()‎، وهو تابع Laravel Eloquent الذي يسمح لنا بالتحقق من وجود علاقة كما في المثال التالي: $posts = Post::has('comments', '>', 3)->get(); ستسترد الشيفرة البرمجية السابقة جميع المنشورات التي تحتوي على أكثر من 3 تعليقات، ولاحظ أنه لا يمكنك استخدام where()‎ لإنجاز ذلك، لأن comments ليس عمودًا في الجدول posts، بل هو جدول آخر له علاقة بالجدول posts. يعمل التابع whereHas()‎ مثل التابع has()‎ تمامًا، ولكنه يوفر مزيدًا من القوة، فالمعامل الثاني هو دالة تسمح بفحص محتوى جدول آخر، وهو الجدول tags في حالتنا، ويمكننا الوصول إلى الجدول tags من خلال المتغير ‎$q. يأخذ التابع whereIn()‎ في السطر 28 معاملَين هما: الأول هو العمود المحدَّد، والثاني هو مصفوفة من القيم المقبولة. يعيد هذا التابع السجلات ذات القيم المقبولة فقط ويستبعد الباقي. يجب أن تكون بقية التوابع سهلة الفهم، حيث يستبعد التابع where('id', '!=', $post->id)‎ المنشور الحالي، ويأخذ التابع take(3)‎ السجلات الثلاثة الأولى. يكون عرض المنشور المقابل resources/views/post.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /> <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p> <div>{!! $post->content !!}</div> <div class="my-5"> @foreach ($post->tags as $tag) <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a> @endforeach </div> <hr> <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> @foreach ($related_posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a> </div> </div> @endforeach </div> </div> @include('vendor.sidebar') </div> @endsection يُعَد ‎{!! $post->content !!}‎ في السطر 15 هو الوضع الآمن في لارافيل، حيث يخبر لارافيل بعرض وسوم HTML بدلًا من عرضها كنص عادي. صفحة البحث ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض نتيجة البحث */ public function search(Request $request): View { $key = $request->input('q'); $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('search', [ 'key' => $key, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags, ]); } . . . } يكون عرض البحث المقابل resources/views/search.blade.php كما يلي: @extends('layout') @section('title') <title>Search - {{ $key }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ترجمة -وبتصرُّف- للمقال Laravel for Beginners #5 - Create the Frontend لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الرابع: إنشاء لوحة تحكم لموقع مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder
  5. تُستخدَم المصفوفات في العديد من المجالات لتمثيل كلٍّ من الكائنات والعمليات التي تُطبَّق عليها؛ إذ تُمثَّل النقاط والأشعة بالمصفوفات العمودية في الرسوميات الحاسوبية، وتُمثَّل العمليات عليها بمصفوفات مربّعة. سنناقش في هذا المقال المواضيع التالية: تعريف المصفوفة. عناصر المصفوفة. أبعاد المصفوفة. أسماء المصفوفات. تَساوي مصفوفتين. المصفوفة المربعة. عملية جمع المصفوفات وقواعدها. المصفوفة الصفرية. معاكس مصفوفة. طرح المصفوفات. ضرب المصفوفات بعدد حقيقي Scalar. منقول Transpose المصفوفة. تعريف المصفوفة تتكوّن المصفوفة العمودية من عمود واحد من الأعداد، ولكن يمكن ترتيب هذه الأعداد في عدة صفوف وعدة أعمدة، وبالتالي يمكن وضع الأعداد في مصفوفة ثنائية الأبعاد. إذًا المصفوفة هي مجموعة من الأعداد المرتَّبة ضمن عددٍ ثابت من الصفوف والأعمدة، وتكون هذه الأعداد أعدادًا حقيقية. يمكن أن تحتوي المصفوفات على أعداد معقدة أيضًا، ولكننا لن نراها في هذا المقال. إليك مثالًا لمصفوفة مكونة من ثلاثة صفوف وثلاثة أعمدة: الصف العلوي هو الصف 1، والعمود الموجود في أقصى اليسار هو العمود 1، والمصفوفة السابقة هي مصفوفة بحجم 3x3، لأنها تحتوي على ثلاثة صفوف وثلاثة أعمدة، ويكون التنسيق لوصف المصفوفات هو: rows X columns. يسمّى كل عدد يشكّل المصفوفة عنصرًا Element من عناصرها، ويكون للعناصر مواقع مُحدَّدة في المصفوفة؛ إذ تمثّل الزاويةُ العلوية اليسرى من المصفوفة الصفَّ 1 والعمود 1، وقيمة العنصر الموجود في الصف 1 والعمود 1 في المصفوفة السابقة هي 1، وقيمة العنصر الموجود في الصف 2 والعمود 3 هي 4.6، وقيمة العنصر في الصف 3 والعمود 1 هي 4. أبعاد المصفوفة يُطلَق مصطلح أبعاد المصفوفة على عدد صفوفها وأعمدتها. إليك فيما يلي مصفوفةً مكونةً من ثلاثة صفوف وعمودين: تُكتَب أحيانًا الأبعاد إلى جانب المصفوفة كما هو الحال في المصفوفة السابقة، ولكنه مجرد تذكير صغير وليس جزءًا من المصفوفة. إليك مثالًا آخر لمصفوفة ذات أبعاد مختلفة تتكون من صفين وثلاثة أعمدة، والتي تُعَد "نوع بيانات" مختلف عن المصفوفة السابقة: المصفوفة المربعة Square Matrix تحتوي المصفوفة المربعة Square Matrix على العدد نفسه من الصفوف والأعمدة، وتُستخدَم المصفوفات المربعة لتمثيل التحويلات الهندسية Transformations في الرسوميات الحاسوبية. المصفوفة المستطيلة Rectangular Matrix هي المصفوفة التي يمكن ألّا يكون لها العدد نفسه من الصفوف والأعمدة. مع ذلك، تطالب بعض المراجع بوجوب أن يكون عدد الصفوف مختلفًا عن عدد الأعمدة. أما المصفوفة العمودية Column Matrix فتتكون من عمود واحد، وهي مصفوفة بحجم N x 1، وقد استخدمنا المصفوفة العمودية في هذه السلسلة من المقالات لتمثيل الأشعة الهندسية مثل معظم كتب الرسوميات الحاسوبية. إليك مثالًا عن مصفوفة عمودية بحجم ‎:4 x 1 تتكون المصفوفة السطرية Row Matrix من صفٍ واحد، حيث تستخدم بعض الكتب المصفوفات السطرية لتمثيل الأشعة الهندسية. تسمّي بعض الكتب المصفوفة العمودية بالشعاع العمودي وتسمّي المصفوفة السطرية بالشعاع السطري، ويُعَد ذلك أمرًا جيدًا، ولكنه غامضٌ بعض الشيء، لذا سنستخدم في هذا المقال مصطلح شعاع Vector للتعبير عن كائن هندسي، وسنستخدم المصفوفات العمودية لتمثيل الأشعة، كما سنستخدمها أيضًا لتمثيل النقاط الهندسية. أسماء المصفوفات إذا نسيت ما إذا كانت الصفوف أو الأعمدة تأتي أولًا، فتذكر فقط مصطلح الأعمدة الرومانية ROWman COLumns. يمكن إعطاء اسمٍ للمصفوفة، ويكون هذا الاسم عادةً حرفًا إنجليزيًا كبيرًا بخط عريض في النصوص المطبوعة مثل A أو M، وتُكتَب في بعض الأحيان الأبعاد على يمين الحرف كما في المصفوفة ‎B3x3‎. لعناصر المصفوفة أسماءٌ أيضًا؛ إذ يكون اسم العنصر عادةً بحرفٍ مماثل لاسم المصفوفة، ولكنه حرفٌ صغير مع كتابة موضع العنصر بصورة منخفضة، لذلك يمكن مثلًا كتابة المصفوفة A التي أبعادها 3x3 على النحو التالي: يمكن أيضًا كتابة المصفوفة ‎A = [aij]‎ للقول بأن عناصر المصفوفة A تسمَّى aij‎. تساوي مصفوفتين إذا احتوت مصفوفتان على القيم نفسها، ولكن في مواقع مختلفة، فلا يمكن عَدّ هاتين المصفوفتين متساويتين، فلكي تكون المصفوفتان متساويتان، يجب أن يكون: لهما الأبعاد نفسها. يجب أن تكون العناصر المتقابلة متساوية. لنفترض أن لدينا المصفوفتين ‎A n x m = [aij]‎ و ‎B p x q = [bij]‎، وعندها يكون ‎A = B‎ إذا وفقط إذا كان n=p و m=q وaij=bij ‎ لجميع قيم i و j ضمن المجال المحدَّد. إليك مصفوفتين غير متساويتين بالرغم من أنهما تحتويان على العناصر نفسها: كما أن المصفوفتين التاليتين غير متساويتين، لأن عناصرهما المتقابلة غير متساوية بالرغم من لهما الأبعاد نفسها: جمع المصفوفات إذا كانت لدينا مصفوفتان لهما العدد نفسه من الصفوف والأعمدة، فيمكن حساب مجموعهما؛ حيث إذا كانت A مصفوفةً أبعادها MxN، وكانت B مصفوفةً أبعادها MxN، فإن مجموعهما هو مصفوفة أبعادها MxN مكوّنة من جمع العناصر المتقابلة للمصفوفتين A و B. إليك مثال عن جمع مصفوفتين: تكون عناصر المصفوفات أعدادًا حقيقية ذات كسور عشرية في معظم الحالات العملية، وليست أعدادًا صحيحة صغيرة كتلك المستخدمة غالبًا في أمثلتنا. المصفوفة الصفرية Zero Matrix يمكن جمع مصفوفة أبعادها 3x2 مع مصفوفة أخرى أبعادها 3x2 دون تغيير المصفوفة الثانية إذا كانت جميع عناصر المصفوفة الأولى أصفارًا، والتي تُسمَّى بالمصفوفة الصفرية. إليك مثال عن مصفوفة صفرية أبعادها 3x3: اسم المصفوفة الصفرية هو صفر مكتوب بخط عريض 0، بالرغم من أننا قد ننسى في بعض الأحيان كتابته بخط عريض. أوجد ناتج عملية الجمع التالية: المجموع بالطبع هو المصفوفة غير الصفرية نفسها. قواعد جمع المصفوفات سنوضّح فيما يلي قواعد جمع المصفوفات، إذ يفترض أن تكون للمصفوفات الأبعاد نفسها: A + B = B + A A + 0 = 0 + A = A 0 + 0 = 0 تشبه هذه القواعد بعض القواعد الخاصة بجمع الأعداد الحقيقية، ولكن انتبه إلى أنه ليست جميع قواعد رياضيات المصفوفات مماثلة لتلك الخاصة برياضيات الأعداد الحقيقية. تنص القاعدة الأولى على أن جمع المصفوفات هو عملية تبديلية Commutative، بسبب إجراء عملية الجمع العادي على العناصر المتقابلة في المصفوفتين، ويُعَد الجمع العادي (الحقيقي) تبديليًا. كما يُعَد جمع المصفوفات عملية تجميعية Associative، وهي قاعدة أخرى تماثل قواعد رياضيات الأعداد الحقيقية، بسبب إجراء عملية الجمع العادية على العناصر المتقابلة من المصفوفتين: ‎(A + B) + C = A + (B + C) لنوجِد ناتج جمع المصفوفتين التاليتين: لاحظ أن كل عنصر من عناصر المصفوفة الناتجة التي أبعادها 3x3 هو 10. ضرب المصفوفة بعدد حقيقي Scalar يمكن ضرب المصفوفة بعدد حقيقي (مقدار سلمي) من خلال ضرب كل عنصر من عناصر المصفوفة بهذا العدد. إليك مثال على ذلك، بحيث يكون المتغير a عددًا حقيقيًا: يكون كل عنصر في النتيجة معاكسًا للعنصر الأصل إذا كانت قيمة العدد الحقيقي a في المثال السابق تساوي ‎-1، كما سنرى في الفقرة التالية. معاكس المصفوفة يمكن تشكيل معاكس للمصفوفة من خلال تغيير إشارة كل عنصر من عناصر هذه المصفوفة: ‎-A = -1 A كما في المثال التالي: تنتج المصفوفة الصفرية عن جمع المصفوفة مع معاكسها. لاحظ أن الصفر الأخير هو صفر بخط عريض، والذي يعبّر عن المصفوفة الصفرية: A + (-A) = 0‎ ملاحظة: يمكن تعريف عملية الطرح مصفوفتين على أنها عملية جمع المصفوفة الأولى مع معاكس المصفوفة الثانية. طرح المصفوفات إذا كانت المصفوفتان A و B لهما العدد نفسه من الصفوف والأعمدة، فيمكن تعريف عملية الطرح ‎A - B‎ بأنها ‎A + (-B)‎؛ إذ يمكن حساب ‎A - B‎ من خلال طرح العنصر المقابل من المصفوفة B من كل عنصر من عناصر المصفوفة A. إليك مثال غير مكتمل عن طرح المصفوفات: لاحظ العناصر الموجودة في الصف الأول من الإجابة السابقة؛ إذ تُعَد طريقة حساب النتيجة للعناصر الموجودة في الصف 1 والعمود 2 مربكةً بعض الشيء. لنحاول الآن وضع الإجابة مكان علامتي الاستفهام في المثال السابق كما يلي: منقول مصفوفة Transpose منقول المصفوفة هو مصفوفة جديدة تكون صفوفها هي أعمدة المصفوفة الأصلية، مما يؤدي أيضًا إلى جعل أعمدة المصفوفة الجديدة هي صفوف المصفوفة الأصلية. إليك مثال عن مصفوفة ومنقولها: الحرف المرتفع "T" يعني المنقول "Transpose". هناك طريقة أخرى للنظر إلى المنقول، وهي أن العنصر الموجود في الصف r والعمود c في المصفوفة الأصلية يُوضَع في الصف c والعمود r من المنقول؛ إذ يصبح العنصرُ ‎arc‎ في المصفوفة الأصلية هو العنصر ‎acr‎ في المصفوفة المنقولة. نتعامل عادةً مع المصفوفات المربعة لإيجاد منقولها، ولكن يمكن نقل المصفوفات غير المربعة أيضًا مثل المصفوفة التالية: إليك مثال آخر عن كيفية إيجاد منقول مصفوفة غير مربعة: قاعدة خاصة بمنقول المصفوفة إذا أوجدنا المنقول لمنقول المصفوفة نفسها، فستحصل على المصفوفة الأصلية مرةً أخرى كما في المثال التالي: ويوضح المثال السابق القاعدة: ‎(AT)T = A‎‎. لاحظ أن منقول مصفوفة سطرية هو مصفوفة عمودية، وبالتالي فإن منقول مصفوفة عمودية هو مصفوفة سطرية كما في المثال التالي: المصفوفة المتناظرة Symmetric Matrix المصفوفة المتناظرة هي المصفوفة التي تتساوى مع منقولها، أي: ‎AT = A‎، ولكن يجب أن تكون مصفوفةً مربعة. إليك فيما يلي مثال عن بعض المصفوفات المتناظرة: إذا كانت المصفوفة متناظرة، فستكون عناصرها ‎aij = aji‎. ملاحظة: تُعَد المصفوفة الصفرية مصفوفةً متناظرةً أيضًا. قطر المصفوفة الرئيسي Main Diagonal يتكوّن القطر الرئيسي للمصفوفة من العناصر التي تقع على القطر الذي يمتد من أعلى يسار المصفوفة إلى أسفل يمينها، فإذا كانت لدينا المصفوفة A مثلًا، فإن عناصر قطرها الرئيسي هي العناصر التي يتساوى فيها رقم الصف مع رقم العمود ‎ajj‎. لا يُعَد القطر الآخر للمصفوفة مهمًا وليس له اسم. ملاحظة: عناصر المصفوفة A التي لا تتغير عند تشكيل المنقول ‎AT‎، هي العناصر الموجودة على طول القطر الرئيسي، لأن العناصر ‎ajj‎ في المصفوفة A هي العناصر نفسها في المنقول ‎AT‎. الخلاصة إليك بعض القواعد التي ناقشناها في هذا المقال؛ إذ يجب عليك فهمها بدلًا من حفظها، حيث تحتوي المصفوفات في كل قاعدة من القواعد التالية على العدد نفسه من الصفوف والأعمدة: ‎A + 0 = A‎ ‎A + B = B + A‎ 0 + 0 = ‎0 ‎A + (B + C) = (A + B) + C‎‎ ‎(ab) ‎A = a(b A) ‎a(A + B) = a B + a A‎‎ ‎a 0 = 0‎ ‎(-1) ‎A = -A A - A = 0‎ ‎(AT)T = A 0T = 0‎ حيث أن a و b هي مقادير سلمية (أعداد حقيقية)، و A و B مصفوفتان، و 0 هي المصفوفة الصفرية ذات البعد المناسب. ملاحظة: إذا كان ‎A = B‎ و ‎B = C‎، فإن ‎A = C‎. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على المصفوفات وعملياتها البسيطة، وسنناقش في المقال التالي مزيدًا من الأمور التي يمكن تطبيقها باستخدام المصفوفات. ترجمة -وبتصرُّف- للفصل Matrices and Simple Matrix Operations من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد.
  6. سننشئ في هذا المقال تطبيق مدونة باستخدام لارافيل ونجعل تطبيقنا كامل الميزات ويحتوي على منشورات وفئات Categories ووسوم Tags، حيث ناقشنا في المقال السابق عمليات CRUD للمنشورات، وسنكرر اليوم هذه العمليات نفسها على كل من الفئات والوسوم، كما سنناقش كيفية التعامل مع العلاقات فيما بينها. تهيئة مشروع لارافيل جديد سنبدأ بمشروع لارافيل جديد، حيث سننشئ مجلد العمل وننتقل إليه، ولكن تأكّد من تشغيل دوكر Docker، ثم نفّذ الأمر التالي: curl -s https://laravel.build/<app_name> | bash انتقل إلى مجلد التطبيق وابدأ تشغيل الخادم كما يلي: cd <app_name> ./vendor/bin/sail up لننشئ اسمًا بديلًا للأداة sail لتسهيل الأمور، لذا شغّل الأمر التالي: alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail' يمكنك من الآن فصاعدًا تشغيل الأداة Sail مباشرةً دون تحديد المسار بأكمله كما يلي: sail up استيثاق المستخدم User authentication يأتي إطار عمل لارافيل مع نظام بيئي كبير فهو يحتوي على العديد من الأدوات والموارد الإضافية التي تسهل على المطورين برمجة التطبيقات، وتُعَد حزمة Breeze من لارافيل جزءًا من هذا النظام، فهي توفر طريقة سريعة لإعداد استيثاق المستخدم وتسجيله في تطبيق لارافيل. تتضمّن حزمة Breeze عروضًا Views ومتحكّمات Controllers مبنية مسبقًا خاصة بالاستيثاق، بالإضافة إلى مجموعة من واجهات برمجة التطبيقات الخلفية للتعامل مع استيثاق المستخدم وتسجيله، وصُمِّمت هذه الحزمة لتكون سهلة التثبيت والضبط مع وجود الحد الأدنى من الإعداد المطلوب. استخدم الأوامر التالية لتثبيت حزمة Breeze من لارافيل: sail composer require laravel/breeze --dev sail artisan breeze:install sail artisan migrate sail npm install sail npm run dev ستولّد هذه العملية تلقائيًا كل من المتحكمات والبرمجيات الوسيطة والعروض المطلوبة اللازمة لإنشاء نظام استيثاق مستخدم أساسي في تطبيقك. يمكنك الوصول إلى صفحة التسجيل من العنوان http://127.0.0.1/register، ثم تسجّل حسابًا جديدًا وسيُعاد توجيهك إلى لوحة التحكم. لن نناقش في هذا المقال كيفية عمل نظام استيثاق المستخدم، لأنه يرتبط ببعض المفاهيم المتقدمة إلى حدٍ ما، ولكن يوصَى بشدة بإلقاء نظرة على الملفات التي تولّدت هنا، فهي توفر نظرة أعمق حول كيفية العمل في لارافيل. إعداد قاعدة البيانات يجب بعد ذلك أن يكون لدينا تصور حول كيفية ظهور تطبيق المدونة، حيث نحتاج أولًا إلى قاعدة بيانات يمكنها تخزين المنشورات والفئات والوسوم، وسيكون لكل جدول قاعدة بيانات البنيةُ التالية: جدول المنشورات Posts: المفتاح نوعه id عدد صحيح كبير BigInteger created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية Boolean جدول الفئات Categories: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد name سلسلة نصية String جدول الوسوم Tags: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد name سلسلة نصية يجب أن يكون هناك أيضًا جدول للمستخدمين users، ولكن لاحاجة لأن ننشئه فقد ولّدته حزمة Breeze من لارافيل مسبقًا، لذا سنتخطى هذه الخطوة حاليًا. يكون لهذه الجداول علاقات مع بعضها البعض كما هو موضح فيما يلي: كل مستخدم لديه منشورات متعددة. كل فئة لديها العديد من المنشورات. كل وسم لديه العديد من المنشورات. يعودة كل منشور إلى مستخدم واحد. يعود كل منشور إلى فئة واحدة. كل منشور له العديد من الوسوم. يمكننا إنشاء هذه العلاقات، ولكن يجب تعديل جدول المنشورات كما يلي: جدول المنشورات مع العلاقات: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية user_id عدد صحيح كبير category_id عدد صحيح كبير ونحتاج أيضًا إلى جدول منفصل لعلاقة المنشور مع الوسم كما يلي: جدول يمثل علاقة المنشور مع الوسم Post/Tag: المفتاح نوعه post_id عدد صحيح كبير tag_id عدد صحيح كبير تطبيق بنية قاعدة البيانات يمكن تطبيق التصميم السابق من خلال توليد النماذج Models وملفات التهجير Migration باستخدام الأوامر التالية: sail artisan make:model Post --migration sail artisan make:model Category --migration sail artisan make:model Tag --migration بالإضافة إلى توليد ملف تهجير منفصل للجدول post_tag باستخدام الأمر التالي: sail artisan make:migration create_post_tag_table وسينشأ ملف التهجير database/migrations/create_posts_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->string('cover'); $table->text('content'); $table->boolean('is_published'); $table->bigInteger('user_id'); $table->bigInteger('category_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; وسينشأ ملف التهجير database/migrations/create_categories_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('categories'); } }; وسينشأ ملف التهجير database/migrations/create_tags_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('tags', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('tags'); } }; وسينشأ أيضُا ملف التهجير database/migrations/create_post_tag_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('post_tag', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->bigInteger('post_id'); $table->bigInteger('tag_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('post_tag'); } }; طبّق هذه التغييرات باستخدام الأمر التالي: sail artisan migrate يجب بعد ذلك تفعيل الإسناد الجماعي Mass Assignment لحقول محدَّدة بالنسبة للنماذج المقابلة حتى نتمكّن من استخدام تابعَي create أو update معها كما ناقشنا في المقال السابق، ويجب أيضًا تعريف العلاقات بين جداول قاعدة البيانات. سيكون ملف النموذج app/Models/Post.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Post extends Model { use HasFactory; protected $fillable = [ "title", 'content', 'cover', 'is_published' ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } } وسيكون ملف النموذج app/Models/Category.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Category extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } ويكون ملف النموذج app/Models/Tag.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Tag extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): BelongsToMany { return $this->belongsToMany(Post::class); } } ويكون ملف النموذج app/Models/User.php كما يلي: <?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * السمات‫ Attributes التي نسندها إسنادًا جماعيًا * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * ‫السمات التي يجب أن تكون مخفية لعملية السَلسلة Serialization * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * السمات التي يجب تغيير نوعها‫ Cast * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } المتحكمات Controllers والوِجهات Routes نحتاج إلى إنشاء متحكم موارد واحد لكل مورد (منشور وفئة ووسم) باستخدام الأوامر التالية: php artisan make:controller PostController --resource php artisan make:controller CategoryController --resource php artisan make:controller TagController --resource ننشئ بعد ذلك وِجهات لكل من هذه المتحكمات في الملف routes/web.php كما يلي: <?php use App\Http\Controllers\CategoryController; use App\Http\Controllers\PostController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\TagController; use Illuminate\Support\Facades\Route; // وِجهات لوحة التحكم Route::prefix('dashboard')->group(function () { // الصفحة الرئيسية للوحة التحكم Route::get('/', function () { return view('dashboard'); })->name('dashboard'); // مورد الفئة للوحة التحكم Route::resource('categories', CategoryController::class); // مورد الوسم للوحة التحكم Route::resource('tags', TagController::class); // مورد المنشور للوحة التحكم Route::resource('posts', PostController::class); })->middleware(['auth', 'verified']); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); require __DIR__ . '/auth.php'; لاحظ تجميع جميع الوِجهات باستخدام البادئة ‎/dashboard، ويكون للمجموعة البرمجيةُ الوسيطة auth، مما يعني أنه يجب على المستخدم تسجيل الدخول للوصول إلى لوحة التحكم. متحكمات الفئة والوسم Category/Tag يُعََد المتحكمان CategoryController و TagController واضحَين إلى حدٍ ما، ويمكنك إعدادهما بالطريقة نفسها التي أنشأنا بها المتحكم PostController في المقال السابق. إذًا لننشئ أولًا متحكم الفئة app/Http/Controllers/CategoryController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $categories = Category::all(); return view('categories.index', [ 'categories' => $categories ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('categories.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل $category = new Category(); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $category = Category::all()->find($id); $posts = $category->posts(); return view('categories.show', [ 'category' => $category, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $category = Category::all()->find($id); return view('categories.edit', [ 'category' => $category ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الفئة المطلوبة ووضع البيانات المطلوبة في العمود المقابل $category = Category::all()->find($id); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * إزالة المورد المٌحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $category = Category::all()->find($id); $category->delete(); return redirect()->route('categories.index'); } } ولننشئ الآن متحكم الوسم app/Http/Controllers/TagController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $tags = Tag::all(); return view('tags.index', [ 'tags' => $tags ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('tags.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل $tag = new Tag(); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $tag = Tag::all()->find($id); $posts = $tag->posts(); return view('tags.show', [ 'tag' => $tag, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $tag = Tag::all()->find($id); return view('tags.edit', [ 'tag' => $tag ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الوسم المطلوب ووضع البيانات المطلوبة في العمود المقابل $tag = Tag::all()->find($id); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $tag = Tag::all()->find($id); $tag->delete(); return redirect()->route('tags.index'); } } تذكّر أنه يمكنك التحقق من اسم الوِجهات باستخدام الأمر التالي: sail artisan route:list متحكم المنشور Post يُعَد متحكم المنشور PostController أكثر تعقيدًا بعض الشيء، إذ يجب عليك التعامل مع عمليات رفع الصور والعلاقات بين الجداول في قاعدة البيانات في التابع store()‎. إذًا لننشئ هذا المتحكم app/Http/Controllers/PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); if ($request->input('is_published') == 'on') { $is_published = true; } else { $is_published = false; } // ‫إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post(); $post->title = $title; $post->content = $content; $post->is_published = $is_published; // حفظ صورة الغلاف $path = $request->file('cover')->store('cover', 'public'); $post->cover = $path; // ضبط المستخدم $user = Auth::user(); $post->user()->associate($user); // ضبط الفئة $category = Category::find($request->input('category')); $post->category()->associate($category); // حفظ المنشور $post->save(); // ضبط الوسوم $tags = $request->input('tags'); foreach ($tags as $tag) { $post->tags()->attach($tag); } return redirect()->route('posts.index'); } . . . } هناك بعض الأشياء التي يجب ملاحظتها في التابع store()‎، حيث سنستخدم في الأسطر من 28 إلى 32 مربع اختيار HTML لتمثيل الحقل is_published، وتكون قيمته إما 'on' أو null، ولكن تُحفَظ قيمته في قاعدة البيانات بوصفها true أو false، لذلك يجب استخدام التعليمة if لحل هذه المشكلة. يمكننا استرداد الملفات في السطور من 41 إلى 42 من خلال استخدام التابع file()‎ بدلًا من التابع input()‎، ويُحفظ الملف في القرص public ضمن المجلد cover. نحصل في السطور من 45 إلى 46 على المستخدم الحالي باستخدام التابع Auth::user()‎، ونربط المنشور بالمستخدم باستخدام التابع associate()‎، وتفعَل السطور من 49 إلى 50 الشيء نفسه بالنسبة للفئة، وتذكّر أنه يمكنك تطبيق ذلك فقط مع المتغير ‎$post وليس ‎$user أو ‎$category، لأن العمودان user_id و category_id موجودان في الجدول posts. وأخيرًا، يجب حفظ المنشور الحالي في قاعدة البيانات، ثم استرداد قائمة الوسوم وإرفاق كل منها بالمنشور واحدًا تلوَ الآخر باستخدام التابع attach()‎ بالنسبة للوسوم كما هو موضح في السطور من 56 إلى 60. تجري الأمور بطريقة مشابهة بالنسبة إلى التابع update()‎، باستثناء أنه يجب إزالة جميع الوسوم الموجودة قبل أن تتمكّن من إرفاق الوسوم الجديدة كما يلي: $post->tags()->detach(); العروض Views تذكر دائمًا أن تكون منظّمًا عند إنشاء نظام عرض، حيث سنتّبع البنية التالية في مثالنا: resources/views ├── auth ├── categories │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── components ├── layouts ├── posts │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── profile ├── tags │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── dashboard.blade.php └── welcome.blade.php أنشأنا ثلاثة مجلدات هي: posts و categories و tags، ولكل منها أربعة قوالب هي: create و edit و index و show باستثناء المجلد posts، لأن وجود صفحة show للمنشورات في لوحة التحكم أمر غير ضروري. يؤدي تضمين جميع هذه العروض في مقال واحد إلى جعل هذا المقال طويلًا بلا داعٍ، لذا سنوضّح فقط صفحات إنشاء وتعديل وفهرس المنشورات، ولكن يمكنك الاطلاع على الشيفرة المصدرية الكاملة على Github. عرض إنشاء منشور لننشئ أولًا عرض إنشاء المنشور resources/views/posts/create.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.store') }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} <input type="checkbox" name="is_published" id="is_published" /> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30" ></textarea> <br /> <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" required autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}">{{ $category->name }}</option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}">{{ $tag->name }}</option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> استخدمنا في مثالنا محرّر النصوص TinyMCE، ولكن يمكنك استخدام محرّر نصوص آخر عوضًا عنه، أو استخدم العنصر <textarea></textarea> إذا أدرتَ ذلك. يجب أن تحتوي الاستمارة الموجودة في السطر 24 على السمة التي هي enctype="multipart/form-data"‎ لأننا لا ننقل النصوص فحسب، بل توجد ملفات أيضًا. تذكر في السطر 59 استخدام السمة type="file"‎ لأننا نرفع صورة، وستُنقَل قيمة الخيار في السطور من 67 إلى 71 إلى الواجهة الخلفية. هناك شيئان يجب الانتباه إليهما في الأسطر من 74 إلى 78، فلاحظ أولًا السمة name="tags[]"‎، حيث تخبر هذه الأقواس [] لارافيل بنقل مصفوفة قابلة للتكرار بدلًا من النصوص. ثانيًا، تنشئ السمة multiple استمارة متعددة التحديد بدلًا من تحديد فردي مثل استمارة الفئات. عرض تعديل المنشور لننشئ الآن العرض resources/views/posts/edit.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} {{ method_field('PUT') }} <input type="checkbox" name="is_published" id="is_published" @checked($post- />is_published)/> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" value="{{ $post->title }}" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30"> {{ $post->content }}</textarea > <br /> <x-input-label for="cover" >{{ __('Update Cover Image') }}</x-input-label > <img src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}" alt="cover image" width="200" /> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}" @selected($post-> category->id == $category->id)>{{ $category->name }} </option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}" @selected($post-> tags->contains($tag))>{{ $tag->name }} </option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> لاحظ في السطور من 24 إلى 26 أن لغة HTML لا تدعم التابع PUT افتراضيًا، لذا نستخدم السمة method="POST"‎ ثم نخبر لارافيل باستخدام التابع PUT من خلال التعليمة ‎{{ method_field('PUT') }}‎. عرض فهرس المنشورات لننشئ الآن العرض resources/views/posts/index.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> @foreach($posts as $post) <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center" > <div class="text-gray-900 dark:text-gray-100"> <p>{{ $post->title }}</p> </div> <div class="space-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}"> <x-primary-button>{{ __('Edit') }}</x-primary-button></a > <form method="post" action="{{ route('posts.destroy', ['post' => $post->id]) }}" class="inline" > {{ csrf_field() }} {{ method_field('DELETE') }} <x-danger-button> {{ __('Delete') }} </x-danger-button> </form> </div> </div> @endforeach </div> </div> </x-app-layout> لاحظ أن زر الحذف ليس زرًا عاديًا، إذ يجب أن يكون استمارة مع تابع DELETE، لأن الرابط العادي يمتلك التابع GET فقط. يُفترَض الآن أن تكون قادرًا على إنشاء بقية نظام العرض بسهولة. لقطات الشاشة وأخيرًا، إليك بعض لقطات الشاشة للوحة التحكم التي أنشأناها: صفحة إدارة التصنيفات: ننتقل لصفحة الإدارة كما يلي: ثم نختار التصنيفات Categories من الصورة أعلاه لننتقل للصفحة التالية لإدارة التصنيفات: صفحة إنشاء تصنيف جديد: صفحة تحديث المنشور: تابع معنا المقال التالي والأخير من هذه السلسلة حيث سننشئ فيه كل ما يخص الواجهة الأمامية من التطبيق كي يتمكن المستخدمون من رؤية منشورات المدونة والتفاعل معها. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #4 - Create a Dashboard لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثالث: استخدام عمليات CRUD لإنشاء مدونة بسيطة تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel إنشاء استمارة اتصال في Laravel باستخدام ميزة Form Request
  7. سنستخدم في هذا المقال كل ما تعلّمناه في المقالين السابقين من سلسلة مقالات لارافيل للمبتدئين لإنشاء مشروع حقيقي، حيث سننشئ مدونة صغيرة تحتوي على منشورات فقط بدون فئات Categories أو وسوم Tags، وكل منشور له عنوان ومحتوى، إذ لن ننشئ تطبيق مدونة كامل المواصفات، وسنوضّح كيفية استرداد البيانات من قاعدة البيانات، وكيفية إنشاء أو تحديث معلومات جديدة، وكيفية حفظها في قاعدة البيانات، وكيفية حذفها، حيث يمثّل الاختصار CRUD عمليات الإنشاء ‎Create والقراءة ‎Read والتحديث ‎Update والحذف ‎Delete. تصميم بنية قاعدة البيانات سننشئ أولًا ملف تهجير Migration للجدول post، ويجب أن يحتوي هذا الجدول على العنوان والمحتوى، ويمكنك توليد ملف تهجير باستخدام الأمر التالي: php artisan make:migration create_posts_table وسيتضمن الملف ما يلي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->text('content'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; أنشأنا في هذا المثال خمسة أعمدة في الجدول post باستدعاء 5 توابع وهي: ينشئ التابع id()‎ عمود المعرّف id الذي يُستخدَم عادة للفهرسة. ينشئ التابع timestamps()‎ عمودين هما: created_at و uptated_at، وسيُحدَّث هذان العمودان تلقائيًا عند إنشاء السجل وتحديثه. ينشئ التابع string('title')‎ العمود title الذي نوعه VARCHAR ويبلغ طوله الافتراضي 255 بايتًا. ينشئ التابع string('content')‎ عمود المحتوى content. يمكنك تطبيق التغييرات من خلال تشغيل الأمر التالي: php artisan migrate ويجب أن ينشأ جدول posts جديد كما يلي: يمكننا الآن إنشاء النموذج Model المقابل لهذا الجدول باستخدام الأمر التالي: php artisan make:model Post وسيتضمّن هذا النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; } لننشئ الآن متحكم الموارد Resource Controller المقابل باستخدام الأمر التالي: php artisan make:controller PostController --resource وأخيرًا، سجّل هذا المتحكم في الموجِّه Router كما يلي: use App\Http\Controllers\PostController; Route::resource('posts', PostController::class); يمكنك التحقق من الوِجهات Routes المُسجَّلة من خلال تشغيل الأمر التالي: php artisan route:list وسيظهر الخرج التالي: GET|HEAD posts ................................................................. posts.index › PostController@index POST posts ................................................................. posts.store › PostController@store GET|HEAD posts/create ........................................................ posts.create › PostController@create GET|HEAD posts/{post} ............................................................ posts.show › PostController@show PUT|PATCH posts/{post} ........................................................ posts.update › PostController@update DELETE posts/{post} ...................................................... posts.destroy › PostController@destroy GET|HEAD posts/{post}/edit ....................................................... posts.edit › PostController@edit يحتوي الخرج السابق على معلومات مثل توابع الطلبات Request Methods وتوابع المتحكّمات Controller Methods بالإضافة إلى أسماء الوِجهات، وتُعَد هذه المعلومات مهمة جدًا وسنحتاج إليها لاحقًا في هذا المقال. عمليات CRUD حان الوقت الآن لنتعمّق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات أولًا ثم نصمّم القوالب ثم ننتقل إلى الموجّهات عند إنشاء تطبيقات واقعية، إذ يجب أن نفكّر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب المستخدم في اتخاذها. يجب أن يكون لدى المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post في حالتنا، وهذه العمليات هي: الإنشاء Create: يجب أن يكون المستخدم قادرًا على إنشاء موارد جديدة وحفظها في قاعدة البيانات. القراءة Read: يجب أن يكون المستخدم قادرًا على قراءة الموارد سواءً من خلال استرداد قائمة الموارد أو التحقق من تفاصيل مورد محدّد. التحديث Update: يجب أن يكون المستخدم قادرًا على تحديث الموارد الموجودة وتحديث سجل قاعدة البيانات المقابلة لها. الحذف Delete: يجب أن يكون المستخدم قادرًا على إزالة مورد من قاعدة البيانات. ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD. إجراء الإنشاء Create لا تزال قاعدة بياناتنا فارغة حاليًا، لذا قد يرغب المستخدم في إنشاء منشورات جديدة، فلنبدأ بإجراء الإنشاء (C)، حيث يمكن إكمال هذا الإجراء من خلال استخدام التابعين التاليين: تابع المتحكم create()‎ الذي يعرض استمارة، مما يسمح للمستخدم بملء العنوان والمحتوى. تابع المتحكم store()‎ الذي يحفظ المنشور الذي أنشأناه حديثًا في قاعدة البيانات، ويعيد توجيه المستخدم إلى صفحة القائمة. يطابق التابع create()‎ نمط URL الذي هو ‎/posts/create (التابع GET)، ويطابق التابع store()‎ نمط URL الذي هو ‎/post (التابع POST). إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات: تابع GET هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدَم لطلب البيانات والموارد من الخادم. يُستخدَم تابع POST لإرسال البيانات إلى الخادم، ويُستخدَم لإنشاء أو تحديث المورد. يعمل تابع HEAD مثل تابع GET تمامًا، باستثناء أن استجابة HTTP ستحتوي على الترويسة فقط بدون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging. يُعَد تابع PUT مشابهًا لتابع POST، مع اختلاف واحد بسيط، حيث إذا أرسلتَ باستخدام التابع POST موردًا موجودًا مسبقًا على الخادم، فلن يسبّب هذا الإجراء أيّ فرق، ولكن سيكرّر تابع PUT هذا المورد في كل مرة نقدّم فيها الطلب. يزيل تابع DELETE موردًا من الخادم. لنبدأ بالتابع create()‎، حيث سيتضمّن ملف المتحكم app/Http/Controllers/PostController.php ما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; . . . class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('posts.create'); } . . . } سيُنفّذ هذا التابع عندما ترسل طلب GET إلى ‎/posts/create، وسيؤشّر إلى العرض views/posts/create.blade.php. لاحظ تغيير الاستجابة Response إلى عرض View في السطر 16، لأن هذه التوابع يجب أن تعيد عرضًا. يجب بعد ذلك أن ننشئ العرض المقابل، حيث سنبدأ بعرض تخطيط الصفحة layout، إذ سيتضمّن الملف views/layout.blade.php ما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> @yield('title') </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="{{ route('posts.index') }}"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{{ route('posts.create') }}" class="hover:underline hover:underline-offset-1" >New Post</a > <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > </div> </nav> </div> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </div> </body> </html> لاحظ في السطرين 16 و 20 وجود الأقواس المزدوجة المتعرجة {{ }} التي تسمح بتنفيذ شيفرة PHP البرمجية ضمن القالب، وبالتالي سيعيد التابع route('posts.index')‎ الوِجهة التي اسمها posts.index، حيث يمكنك التحقق من أسماء الوِجهات من خلال الرجوع إلى خرج الأمر php artisan route:list. لننتقل بعد ذلك إلى العرض create، حيث يجب أن تكون منظمًا في هذا العرض. يُعَد العرض create مُخصّصًا للإجراءات المتعلقة بالمنشور، لذا أنشأنا المجلد post لتخزين هذا العرض. سيتضمن العرض views/posts/create.blade.php ما يلي: @extends('layout') @section('title') <title>Create</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{{ route('posts.store') }}" method="POST"> {{ csrf_field() }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" ></textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection توجد بعض الأشياء التي يجب ملاحظتها عند استخدام الاستمارات لنقل البيانات، وهي: السطر 6: تحدِّد سمة Attribute الإجراء action ما يحدث عند إرسال هذه الاستمارة، وتوجّه المتصفح في هذه الحالة لزيارة الوِجهة posts.store باستخدام تابع HTTP الذي هو POST. السطر 7 ‎{{ csrf_field() }}‎: يُعَد CSRF هجومًا ضارًا يستهدف تطبيقات الويب، وتوفر الدالة csrf_field()‎ الحماية من هذا النوع من الهجمات. اطلع على مقال تعرف على أمان مواقع الويب لمعرفة المزيد عن هجمات تزوير الطلبات عبر المواقع Cross-Site Request Forgery -أو CSRF اختصارًا. السمة name في السطر 12 و 20: يُربَط ما يدخله المستخدم بمتغير عند إرسال الاستمارة، وتحدّد السمة name اسم هذا المتغير، فمثلًا إذا كان name="title"‎، فسيُربَط دخل المستخدم بالمتغير title، ويمكن الوصول إلى قيمته باستخدام التعليمة ‎$request->input('title')‎، حيث سنرى كيفية عملها لاحقًا. السطر 27: يجب ضبط السمة type على القيمة submit حتى تعمل هذه الاستمارة. شغّل الآن خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/posts/create حيث ستظهر الصفحة التالية: إذا نقرتَ على زر الإرسال "Submit"، فسيرسل المتصفح طلب POST إلى الخادم، وسيُنفَّذ التابع store()‎ هذه المرة، وسيحتوي طلب POST على دخل المستخدم، ويمكن الوصول إليه كما يلي في الملف PostController.php: <?php namespace App\Http\Controllers; . . . class PostController extends Controller { . . . /** * تخزين مورد أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // إنشاء نسخة جديدة من‫ المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post; $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.index'); } . . . } سيُعاد توجيهك إلى الوِجهة posts.index بعد تخزين البيانات. عُد إلى متصفحك واكتب عنوانًا ومحتوًى جديدًا، ثم انقر على زر الإرسال. لم ننشئ العرض index بعد، لذا ستُعاد رسالة خطأ، ولكن إذا تحققتَ من قاعدة البيانات، فيجب وجود سجل جديد مضاف كما يلي: إجراء القائمة List لننتقل الآن إلى عملية القراءة Read، حيث يوجد نوعان مختلفان من عمليات القراءة هما: الأول هو إجراء القائمة الذي يعيد قائمةً بجميع المنشورات إلى المستخدم، والإجراء الثاني سنوضّحه في الفقرة التالية. يقابل إجراء القائمة التابعَ index()‎ في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $posts = Post::all(); return view('posts.index', [ 'posts' => $posts, ]); } . . . } ويكون عرض index المقابل هو views/post/index.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> @foreach($posts as $post) <h2 class="text-2xl font-semibold underline mb-2"> <a href="{{ route('posts.show', ['post' => $post->id]) }}" >{{ $post->title }}</a > </h2> <p class="mb-4">{{ Illuminate\Support\Str::words($post->content, 100) }}</p> @endforeach </div> @endsection تتكرر حلقة foreach في السطر 5 على جميع المنشورات ‎$posts المُسترَدة، ثم تسنِد كل قيمة إلى المتغير ‎$post. لاحظ في السطر7 كيفية تمرير معرّف id المنشور إلى الوِجهة posts.show، ثم ستمرِّر هذه الوِجهة هذا المتغير إلى تابع المتحكم show()‎ الذي سنراه لاحقًا. يُعَد التابع Str::words()‎ في السطر 11 دالةً مساعدة في لغة PHP، وسيأخذ أول 100 كلمة من المحتوى content فقط. إجراء العرض Show عملية القراءة الثانية هي إجراء العرض الذي يعرض تفاصيل مورد معين، ويتحقّق هذا الإجراء باستخدام التابع show()‎ في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض المورد المُحدَّد */ public function show(string $id): View { $post = Post::all()->find($id); return view('posts.show', [ 'post' => $post, ]); } . . . } ويكون عرض show المقابل هو views/post/show.blade.php كما يلي: @extends('layout') @section('title') <title>{{ $post->title }}</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ $post->title }}</h2> <p class="mb-4">{{ $post->content }}</p> <div class="grid grid-cols-2 gap-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" >Update</a > <form action="{{ route('posts.destroy', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Delete </button> </form> </div> </div> @endsection هناك بعض الأشياء التي يجب أن نلاحظها في المثال السابق. أولًا، لاحظ كيف أن زر التحديث "Update" هو رابط بسيط، ولكن زر الحذف "Delete" هو استمارة، لأن الرابط سيخبر المتصفح بإرسال طلب GET إلى الخادم، بينما نحتاج شيئًا آخر لإجراء الحذف، حيث إذا نظرتَ داخل الاستمارة، فستجد أنها تحتوي على السمة method="POST"‎، ولكننا نحتاج التابع DELETE لإجراء الحذف. تدعم لغة HTML التابعين GET و POST افتراضيًا، وإذا كنت بحاجة شيء آخر، فيجب أن تضبط السمة method="POST"‎، وأن تستخدم التابع method_field()‎ بدلًا من ذلك. إجراء التحديث Update تستخدم عملية التحديث التابع edit()‎ الذي يعرض استمارة HTML، والتابع update()‎ الذي يجري تغييرات على قاعدة البيانات، لذا ضع هذين التابعين في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $post = Post::all()->find($id); return view('posts.edit', [ 'post' => $post, ]); } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // البحث عن المنشور المطلوب ووضع البيانات المطلوبة في العمود المقابل $post = Post::all()->find($id); $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.show', ['post' => $id]); } . . . } ويكون عرض edit المقابل هو views/post/edit.blade.php كما يلي: @extends('layout') @section('title') <title>Edit</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Edit post</h2> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('PUT') }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" value="{{ $post->title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" > {{ $post->content }}</textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection إجراء الحذف Delete وأخيرًا، يوجد التابع destroy()‎ للحذف، إذًا لنضعه في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $post = Post::all()->find($id); $post->delete(); return redirect()->route('posts.index'); } } لا يتطلب هذا الإجراء عرضًا، بما أنه يعيد توجيهك إلى posts.index بعد اكتمال الإجراء. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #3 - The CRUD Operations لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثاني: استخدام بنية MVC لإنشاء مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel المبادئ الأساسيّة لإطار العمل Eloquent ORM أساسيات التحقق من المدخلات في Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel
  8. تُعدّ لغة PHP من أكثر اللغات استخدامًا في تطوير مواقع الويب لأنها سهلة وتتيح بناء تطبيقات الويب بسرعة. لكن رغم سهولتها إلا أنها تطورت وأصبحت تضم العديد من المميزات والفروق الرئيسية والدقيقة التي يمكن أن يخطئ بها المطورون بالرغم من سهولة استخدامها، فقد يقع المطورون في أخطاء بسبب هذه التفاصيل الدقيقة في اللغة، مما يستغرق منهم وقتًا طويلًا لإصلاحها. في هذا المقال. سنسلط الضوء في هذا المقال على 10 من الأخطاء الأكثر شيوعًا التي يجب على مطوري PHP توخّي الحذر منها. الخطأ 1: ترك مراجع References المصفوفة معلّقة بعد انتهاء حلقات foreach يمكن أن يكون استخدام المراجع في حلقات foreach مفيدًا إذا أردتَ العمل على كل عنصر من المصفوفة التي تمر عليها، فليكن لدينا مثلًا ما يلي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8) ولكن إن لم تكن حذرًا، فقد يكون لهذه المراجع بعض الآثار الجانبية والعواقب غير المرغوب فيها، فمثلًا سيبقى المتغير ‎$value ضمن النطاق Scope وسيحتفظ بمرجع إلى العنصر الأخير من المصفوفة بعد تنفيذ الشيفرة البرمجية في المثال السابق، وبالتالي قد تؤدي العمليات اللاحقة المُطبَّقة على المتغير ‎$value إلى تعديل العنصر الأخير في المصفوفة عن غير قصد. تذكّر أن حلقة foreach لا تنشئ نطاقًا، وبالتالي يُعَد المتغير ‎$value في المثال السابق مرجعًا ضمن النطاق العلوي للسكريبت، حيث تضبط حلقة foreach المرجع للإشارة إلى العنصر التالي من المصفوفة ‎$arr في كل تكرار، ويبقى المتغير ‎$value يؤشّر إلى العنصر الأخير من المصفوفة ‎$arr، ويبقى ضمن النطاق بعد اكتمال الحلقة. إليك فيما يلي مثال عن نوع الأخطاء المربكة التي يمكن أن تؤدي إلى هذه المشكلة: $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // من خلال المرجع echo implode(',', $array), "\n"; foreach ($array as $value) {} // من خلال القيمة (أي النسخة‫) echo implode(',', $array), "\n"; وسيكون خرج الشيفرة البرمجية السابقة ما يلي: 1,2,3 1,2,3 1,2,2 هذا ليس خطأ مطبعيًا، فالقيمة الأخيرة في السطر الأخير هي 2 وليست 3، والسبب هو بقاء المصفوفة ‎$array دون تغيير بعد المرور على حلقة foreach الأولى، ولكن يُترَك المتغير ‎$value بوصفه مرجعًا مُعلَّقًا للعنصر الأخير في المصفوفة ‎$array لأن حلقة foreach وصلت إلى المتغير ‎$value من خلال المرجع، لذا ستحدث أشياء غريبة عندما نمر على حلقة foreach الثانية، حيث تنسخ حلقة foreach كل عنصر تسلسلي من عناصر المصفوفة ‎$array إلى المتغير ‎$value في كل خطوة من الحلقة بسبب الوصول إلى هذا المتغير من خلال القيمة (أي من خلال النسخة). إليك ما يحدث في كل خطوة من حلقة foreach الثانية: تمرير القيمة 1: يؤدي إلى نسخ العنصر الأول من المصفوفة ‎$array[0]‎ (أي القيمة "1") إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذا سيساوي ‎$array[2]‎ الآن القيمة 1، وستحتوي المصفوفة ‎$array على العناصر ‎[1, 2, 1]‎. تمرير القيمة 2: يؤدي إلى نسخ العنصر الثاني من المصفوفة ‎$array[1]‎ (أي القيمة "2") إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذا سيساوي ‎$array[2]‎ الآن القيمة 2، وستحتوي المصفوفة ‎$array على العناصر ‎[1, 2, 2]‎. تمرير القيمة 3: يؤدي إلى نسخ العنصر الثالث من المصفوفة ‎$array[2]‎ (الذي يساوي القيمة "2" الآن) إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذلك لا يزال العنصر ‎$array[2]‎ يساوي القيمة 2، وستحتوي المصفوفة ‎$array الآن على العناصر ‎[1, 2, 2]‎. يمكن الاستفادة من استخدام المراجع في حلقات foreach دون التعرض لخطر هذه الأنواع من المشاكل من خلال استدعاء الدالة unset()‎ مع المتغير بعد حلقة foreach مباشرةً لإزالة المرجع كما في المثال التالي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // لم يَعُد المتغير‫ ‎$value مرجعًا إلى العنصر ‎$arr[3]‎ الخطأ 2: سوء فهم سلوك الدالة isset()‎ تعيد الدالة isset()‎ القيمة false في حالة عدم وجود عنصر، وتعيد أيضًا false للقيم null، حيث يُسبّب هذا السلوك مشكلات، فليكن لدينا المثال التالي: $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // افعل شيئًا هنا عند عدم ضبط‫ 'keyShouldBeSet' } يُفترَض أننا نريد التحقق ممّا إذا كان keyShouldBeSet مضبوطًا في ‎$data، ولكن ستعيد الدالة isset($data['keyShouldBeSet'])‎ أيضًا القيمة false حتى عند ضبط ‎$data['keyShouldBeSet']‎ على القيمة null، وبالتالي يوجد تناقض مع ما ذكرناه سابقًا. إليك أيضًا المثال التالي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; } تفترض الشيفرة البرمجية السابقة أنه إذا أعادت ‎$_POST['active']‎ القيمة true، فسيكون postData مضبوطًا، وبالتالي ستعيد الدالة isset($postData)‎ القيمة true، وتفترض أيضًا أن الطريقة الوحيدة التي ستعيد بها الدالة isset($postData)‎ القيمة false هي إذا أعادت ‎$_POST['active']‎ القيمة false، ولكن ستعيد الدالة isset($postData)‎ القيمة false عند ضبط ‎$postData على القيمة null، وبالتالي يمكن أن تعيد الدالة isset($postData)‎ القيمة false حتى إذا أعادت ‎$_POST['active']‎ القيمة true، لذا يوجد تناقض مع ما ذكرناه سابقًا. إذا كان الهدف من الشيفرة البرمجية السابقة هو التحقق مما إذا كانت ‎$_POST['active']‎ تعيد القيمة true، فسيكون الاعتماد على الدالة isset()‎ لذلك قرارًا برمجيًا سيئًا على أيّة حال، لذا يُفضَّل إعادة التحقق من ‎$_POST['active']‎ بدلًا من ذلك كما يلي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; } يُعَد استخدام التابع ‎array_key_exists()‎ حلًا أكثر قوة بالنسبة للحالات التي يكون فيها من المهم التحقق مما إذا كان المتغير مضبوطًا فعليًا، أو للتمييز بين متغير غير مضبوط ومتغير مضبوط على القيمة null، فمثلًا يمكننا إعادة كتابة المثال الأول من المثالين السابقين كما يلي: $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // افعل ذلك عند عدم ضبط‫ 'keyShouldBeSet' } إذا جمعنا بين التابعين array_key_exists()‎ و get_defined_vars()‎، فيمكننا التحقق بطريقة موثوقة من ضبط متغير ضمن النطاق الحالي كما يلي: if (array_key_exists('varShouldBeSet', get_defined_vars())) { // ‫المتغير ‎$varShouldBeSet موجود في النطاق الحالي } الخطأ 3: الخلط بين الإعادة باستخدام المرجع وإعادتها باستخدام القيمة ليكن لدينا مقطع الشيفرة البرمجية التالي: class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا شغّلتَ الشيفرة البرمجية السابقة، فستحصل على ما يلي: PHP Notice: Undefined index: test in /path/to/my/script.php on line 21 تكمن المشكلة في أن هذه الشيفرة البرمجية تخلط بين إعادة المصفوفات باستخدام المرجع وإعادتها باستخدام القيمة، حيث إن لم تخبر لغةَ PHP صراحةً بإعادة المصفوفة باستخدام المرجع (أي باستخدام &)، فستعيد لغة PHP المصفوفة باستخدام القيمة افتراضيًا، وهذا يمثّل إعادة نسخة من المصفوفة، وبالتالي لن تتمكّن الدالة المُستدعاة والمستدعِي من الوصول إلى النسخة نفسها من المصفوفة. يعيد استدعاء الدالة getValues()‎ السابق نسخة من المصفوفة ‎$values بدلًا من إعادة مرجع إليها. إذًا لنعيد النظر إلى السطرين الرئيسيين من المثال السابق: // تعيد الدالة‫ getValues()‎ نسخة من المصفوفة ‎$values، مما يؤدي إلى إضافة عنصر 'test' // إلى نسخة من المصفوفة‫ ‎$values، وليس إلى المصفوفة ‎$values نفسها $config->getValues()['test'] = 'test'; // تعيد الدالة‫ getValues()‎ مرة أخرى نسخة أخرى من المصفوفة ‎$values، ولا تحتوي هذه النسخة // ‫على عنصر 'test'، ولذلك نحصل على رسالة فهرس غير مُعرَّف "undefined index" echo $config->getValues()['test']; أحد الحلول الممكنة لهذه المشكلة هو حفظ النسخة الأولى من المصفوفة ‎$values التي تعيدها الدالة getValues()‎، ثم العمل على تلك النسخة لاحقًا كما في المثال التالي: $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test']; ستعمل هذه الشيفرة البرمجية بنجاح، أي أنها ستعطي test دون إنشاء أيّ رسالة فهرس غير مُعرَّف "undefined index"، ولكن قد يكون هذا الأسلوب مناسبًا أو غير مناسب اعتمادًا على ما تحاول تحقيقه، إذ لن تعدّل هذه الشيفرة البرمجية المصفوفة ‎$values الأصلية، لذا إذا أردتَ أن تؤثر تعديلاتك (مثل إضافة عنصر test) على المصفوفة الأصلية، فيجب أن تعدّل الدالة getValues()‎ لإعادة مرجع إلى المصفوفة ‎$values نفسها. يمكن تحقيق ذلك من خلال إضافة & قبل اسم الدالة، مما يشير إلى أنها يجب أن تعيد مرجعًا كما يلي: class Config { private $values = []; // إعادة مرجع إلى المصفوفة‫ ‎$values الفعلية public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; وسيكون الخرج test كما هو متوقع. لنجعل الآن الأمور أكثر إرباكًا كما في مقتطف الشيفرة البرمجية التالي: class Config { private $values; // استخدام كائن مصفوفة‫ ArrayObject بدلًا من المصفوفة public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا اعتقدتَ أن هذه الشيفرة البرمجية ستؤدي إلى خطأ "undefined index" نفسه الموجود في مثال المصفوفة array السابق، فأنت مخطئ، إذ ستعمل هذه الشيفرة البرمجية بنجاح، والسبب هو أن لغة PHP تمرّر الكائنات من خلال المرجع دائمًا على عكس المصفوفات، فالكائن ArrayObject هو كائن من مكتبة PHP المعيارية -أو كائن SPL- يحاكي استخدام المصفوفات، ولكنه يعمل بوصفه كائنًا. ليس من الواضح دائمًا في لغة PHP ما إذا كنت تتعامل مع نسخة أو مرجع كما توضح هذه الأمثلة، لذلك يجب أن تفهم هذه السلوكيات الافتراضية مثل تمرير المتغيرات والمصفوفات من خلال القيمة وتمرير الكائنات من خلال المرجع، ويجب التحقق بعناية من توثيق واجهة برمجة التطبيقات API الخاصة بالدالة التي تستدعيها لمعرفة ما إذا كانت تعيد قيمة، أو نسخة من مصفوفة، أو مرجعًا إلى مصفوفة، أو مرجعًا إلى كائن. يجب أيضًا تجنب ممارسة إعادة مرجع إلى مصفوفة أو كائن ArrayObject، لأنه يوفر للمستدعِي القدرة على تعديل البيانات الخاصة للنسخة، وهذا يناقض مفهوم التغليف Encapsulation، لذا يُفضَّل استخدام النمط القديم للجوالب Getters والضوابط Setters كما في المثال التالي: class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue' تمنح هذه الطريقة المستدعِي القدرةَ على ضبط أو جلب أيّ قيمة من المصفوفة دون توفير وصول عام إلى المصفوفة ‎$values الخاصة نفسها. الخطأ 4: إجراء الاستعلامات ضمن حلقة يمكن أن تصادف شيئًا كما يلي إذا كانت شيفرة PHP لا تعمل لديك: $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); } قد لا يكون هناك أيّ شيء خاطئ، ولكن إذا اتبعت المنطق الموجود في هذه الشيفرة البرمجية، فقد تجد أن الاستدعاء البسيط السابق ‎$valueRepository->findByValue()‎ يؤدي في النهاية إلى استعلامٍ من نوعٍ ما كما يلي: $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue); لذا سيؤدي كل تكرار للحلقة السابقة إلى استعلام منفصل لقاعدة البيانات، فمثلًا إذا زوّدتَ الحلقة بمصفوفةٍ مكونة من 1000 قيمة، فستنشئ 1000 استعلام منفصل للمورد، وإذا اُستدعِي مثل هذا السكربت ضمن خيوط Threads متعددة، فيُحتمَل أن يؤدي ذلك إلى توقف النظام بأكمله، لذا يجب معرفة متى تُجري شيفرتك البرمجية الاستعلامات وجمع القيم ثم تشغيل استعلام واحد لجلب جميع النتائج كلما أمكن ذلك. أحد الأمثلة على الأماكن الشائعة إلى حدٍ ما التي ترى فيها إجراء الاستعلام بطريقة غير فعالة (أي ضمن حلقة) هو عند نشر استمارة مع قائمة من القيم (المعرّفات مثلًا)، ثم ستمر الشيفرة البرمجية ضمن حلقة على المصفوفة وتجري استعلام SQL منفصل لكل معرّف لاسترداد بيانات السجل الكاملة لكل معرّف من هذه المعرّفات كما يلي: $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); } ولكن يمكن تحقيق الشيء نفسه بفعالية أكبر في استعلام SQL واحد كما يلي: $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } } لذلك يجب أن تعرف متى تجري شيفرتك البرمجية الاستعلامات سواءً بطريقة مباشرة أو غير مباشرة، ويجب أن تجمع القيم ثم تشغّل استعلامًا واحدًا لجلب جميع النتائج كلما أمكنك ذلك، ولكن يجب توخي الحذر لكي لا نحصل على خطأ PHP الشائع التالي. الخطأ 5: تزييف استخدام الذاكرة وعدم كفاءتها يكون جلب العديد من السجلات في وقت واحد أكثر كفاءة من تشغيل استعلام واحد لجلب كل صف، ولكن يمكن أن يؤدي هذا النهج إلى حالة "نفاد الذاكرة Out of Memory" في مكتبة libmysqlclient عند استخدام إضافة PHP التي هي mysql. ليكن لدينا مثلًا مربع اختبار مع موارد محدودة (512 ميجابايت من ذاكرة RAM) وقاعدة بيانات MySQL وواجهة سطر الأوامر php-cli، حيث سنهيّئ جدول قاعدة بيانات كما يلي: // الاتصال بقاعدة بيانات‫ mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // إنشاء جدول مؤلّف من 400 عمود $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // كتابة 2 مليون صف for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); } لنتحقّق الآن من استخدام الموارد كما يلي: // الاتصال بقاعدة بيانات‫ mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n"; ويكون الخرج كما يلي: Before: 224704 Limit 1: 224704 Limit 10000: 224704 لاحظ أن الاستعلامات مُدارة داخليًا بأمان فيما يتعلق بالموارد، ولكن يمكن التأكد من ذلك من خلال زيادة الحد الأقصى مرة أخرى، حيث سنضبطه على القيمة 100000، وسينتج ما يلي: PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11 المشكلة هنا هي الطريقة التي تعمل بها وحدة mysql الخاصة بلغة PHP، فهي مجرد وكيل لمكتبة libmysqlclient، والتي تذهب إلى الذاكرة مباشرةً عند تحديد جزء من البيانات. لا يدير مدير PHP الذاكرة، وبالتالي لن تعرض الدالة memory_get_peak_usage()‎ أيّ زيادة في استخدام الموارد عندما نزيد الحد الأقصى في استعلامنا. يؤدي ذلك إلى ظهور مشاكل مثل المشكلة السابقة، إذ سنُخدَع من خلال الاعتقاد بأن إدارة ذاكرتنا جيدة، ولكنها سيئة فعليًا وسنواجه مثل هذه المشكلة. يمكنك تجنب هذا التزييف من خلال استخدام الوحدة mysqlnd بدلًا من ذلك، بالرغم من أنها لن تؤدي إلى تحسين استخدام الذاكرة، حيث تُصرَّف Compiled هذه الوحدة بوصفها إضافةً أصيلة Native للغة PHP وتستخدم مدير ذاكرة PHP، لذلك إذا أجرينا الاختبار السابق باستخدام الوحدة mysqlnd بدلًا من الوحدة mysql، فسنحصل على صورة أكثر واقعية لاستخدام ذاكرتنا. Before: 232048 Limit 1: 324952 Limit 10000: 32572912 تستخدم الوحدة mysql ضعف عدد الموارد التي تستخدمها الوحدة mysqlnd لتخزين البيانات وفقًا لتوثيق PHP، لذا يستخدم السكربت الأصلي الذي يستخدم وحدة mysql ذاكرةً أكبر مما هو موضّح هنا فعليًا (حوالي ضعف ذلك). يمكن تجنب مثل هذه المشاكل من خلال تحديد حجم استعلاماتك واستخدام حلقة ذات عدد صغير من التكرارات كما يلي: $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); } إذا أمعنا النظر في هذا الخطأ والخطأ رقم 4، فإننا سندرك أن هناك توازنًا صحيًا تحتاج شيفرتك البرمجية إلى تحقيقه بطريقة مثالية، حيث يجب إما أن تكون استعلاماتك صغيرة ومتكررة جدًا من ناحية، أو أن يكون كل استعلام فردي كبيرًا جدًا من ناحية أخرى، لذا يجب إيجاد توازن بينهما، إذ يمكن أن يسبّب استخدام أيٍّ منهما إلى الحد الأقصى مشكلات مع شيفرة PHP البرمجية التي لن تعمل بطريقة صحيحة. الخطأ 6: تجاهل مشكلات الترميز Unicode/UTF-8 تُعَد هذه المشكلة خاصة بلغة PHP بحد ذاتها أكثر من كونها مشكلة قد تظهر أثناء تنقيح أخطاء شيفرة PHP، ولكن هذه المشكلة لم تُعالَج بطريقة كافية، إذ كان من المقرر جعل نواة PHP 6 متوافقةً مع الترميز الموحّد Unicode، وجرى تعليق ذلك عند تعليق تطوير PHP 6 في عام 2010، ولكن ذلك لا يعفي المطور بأيّ حال من الأحوال من تقديم ترميز UTF-8 بطريقة صحيحة وتجنب الافتراض الخاطئ بأن جميع السلاسل النصية ستكون "نصًا عاديًا قديمًا يستخدم معيار ASCII". تشتهر الشيفرة البرمجية التي تفشل في معالجة السلاسل النصية التي ليست ASCII بإدخال أخطاء هايزنبغ Heisenbug المعقدة في شيفرتك البرمجية، إذ قد تسبّب حتى استدعاءات strlen($_POST['name'])‎ البسيطة مشكلات إذا حاول شخص يحمل اسم عائلة مثل "Schrödinger" التسجيل في نظامك. إليك فيما يلي قائمة صغيرة لتجنب مثل هذه المشكلات في شيفرتك البرمجية: إذا كنت لا تعرف الكثير عن ترميز Unicode و UTF-8، فيجب أن تتعلّم الأساسيات على الأقل. تأكد من استخدام دوال mb_*‎ دائمًا بدلًا من دوال السلاسل النصية القديمة، وتأكد من تضمين إضافة "تعدد البايتات multibyte" في إصدار PHP الخاص بك. تأكد من ضبط قاعدة البيانات والجداول الخاصة بك لاستخدام ترميز Unicode، إذ لا تزال العديد من إصدارات MySQL تستخدم معيار latin1 افتراضيًا. تذكّر أن الدالة json_encode()‎ تحوّل الرموز التي ليست رموز ASCII، حيث تحوّل السلسلة النصية "Schrödinger" إلى "Schr\u00f6dinger" مثلًا، ولكن التابع serialize()‎ لا يفعل ذلك. تأكّد من أن ملفات شيفرة PHP البرمجية الخاصة بك مشفَّرة أيضًا باستخدام ترميز UTF-8 لتجنب التعارض عند ضم السلاسل النصية مع ثوابت السلاسل النصية المكتوبة أو المضبوطة. اطّلع على مقال معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP لمزيدٍ من المعلومات. الخطأ 7: افتراض أن المصفوفة ‎$_POST ستحتوي على بيانات POST الخاصة بك دائمًا لن تحتوي المصفوفة ‎$_POST على بيانات POST الخاصة بك دائمًا على عكس ما يدل عليه اسمها، إذ يمكن أن تكون فارغة. لنفترض مثلًا أننا أنشأنا طلب خادم باستخدام استدعاء التابع jQuery.ajax()‎ كما يلي: // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' }); لاحظ نوع المحتوى contentType: 'application/json'‎ هنا، حيث نرسل البيانات بتنسيق JSON، وهو أمر شائع جدًا لواجهات برمجة التطبيقات، وهو الإعداد الافتراضي للنشر في خدمة AngularJS التي هي ‎$http مثلًا. نفرّغ معلومات محتوى المصفوفة ‎$_POST من طرف الخادم في مثالنا كما يلي: // php var_dump($_POST); والمفاجأة أن النتيجة ستكون كما يلي: array(0) { } لم تظهر سلسلة JSON النصية الخاصة بنا ‎{a: 'a', b: 'b'}‎، حيث تحلّل لغة PHP حِمل طلب POST تلقائيًا عندما يكون نوع المحتوى application/x-www-form-urlencoded أو multipart/form-data فقط، إذ كان هذان النوعان من المحتوى هما النوعان الوحيدان المُستخدَمان منذ سنوات عند تنفيذ المصفوفة ‎$_POST الخاصة بلغة PHP، لذا لا تحمّل لغة PHP حِمل طلب POST تلقائيًا مع أيّ نوع محتوى آخر حتى الأنواع التي تحظى بشعبية كبيرة اليوم مثل application/json. تُعَد المصفوفة ‎$_POST ذات نطاق عام عالي Superglobal، لذا إذا عدّلناها مرة واحدة (ويُفضَّل أن يكون ذلك في وقت مبكر من السكربت)، فستكون القيمة المُعدَّلة (بما في ذلك حِمل طلب POST) قابلةً للإشارة إليها من شيفرتنا البرمجية. يُعَد ذلك أمرًا مهمًا، لأن أطر عمل PHP وجميع السكربتات المخصَّصة تقريبًا تستخدم المصفوفة ‎$_POST استخدامًا شائعًا لاستخراج بيانات الطلب وتحويلها. نحتاج إلى تحليل محتويات الطلب يدويًا (أي فك تشفير بيانات JSON) وتعديل المتغير ‎$_POST عند معالجة حِمل طلب POST مع نوع المحتوى application/json مثلًا كما يلي: // php $_POST = json_decode(file_get_contents('php://input'), true); إذا فرّغنا معلومات محتوى المصفوفة ‎$_POST بعد ذلك، فسنرى أنها تتضمّن حِمل طلب POST الصحيح كما في المثال التالي: array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" } الخطأ الشائع 8: الاعتقاد بأن لغة PHP تدعم نوع بيانات المحارف Character اطّلع على الشيفرة البرمجية التالية وحاول تخمين ما سينتج: for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; } إذا أجبت أنها ستطبع المحارف من 'a' إلى 'z'، فقد تتفاجأ عندما تعرف أنك مخطئ، حيث أنها ستطبع المحارف من 'a' إلى 'z'، ولكنها ستطبع أيضًا 'aa' إلى 'yz'، إذ لا يوجد نوع البيانات char في لغة PHP، بينما يوجد نوع بيانات السلاسل النصية string فقط، وبالتالي سينتج عن زيادة السلسلة النصية z بمقدار واحد في لغة PHP السلسلة النصية aa: php> $c = 'z'; echo ++$c . "\n"; aa ولكن تُعَد السلسلة النصية aa أصغر من z من معجميًا: php> var_export((boolean)('aa' < 'z')) . "\n"; true لذلك تطبع الشيفرة البرمجية الحروف من a إلى z، ولكنها تطبع أيضًا بعد ذلك من aa إلى yz، وتتوقف عندما تصل إلى za، وهي القيمة الأولى الأكبر من z التي تصادفها: php> var_export((boolean)('za' < 'z')) . "\n"; false لذا استخدم الطريقة التالية للمرور ضمن حلقة على القيم من 'a' إلى 'z' في لغة PHP بصورة صحيحة: for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; } أو استخدم الطريقة التالية: $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; } الخطأ 9: تجاهل قواعد ومعايير كتابة الشيفرة البرمجية لا يؤدي تجاهل قواعد كتابة الشيفرة البرمجية مباشرةً إلى الحاجة إلى تنقيح أخطاء شيفرة PHP، ولكنه لا يزال أحد أهم الأشياء التي يجب مناقشتها، إذ قد يؤدي تجاهل هذه المعايير إلى حدوث عدد كبير من المشكلات في المشروع مثل الحصول على شيفرة برمجية غير متناسقة في أحسن الأحوال لأن كل مطور ينجز عمله بطريقته الخاصة، أو الحصول على شيفرة PHP لا تعمل أو من الصعب -أو من المستحيل في بعض الأحيان- التنقل ضمنها، مما يصعّب تنقيح أخطائها وتحسينها وصيانتها في أسوأ الأحوال. يؤدي ذلك إلى تخفيض إنتاجية فريقك بما في ذلك الكثير من الجهد الضائع أو غير الضروري. توجد توصية لمعايير PHP أو PHP Standards Recommendation -أو PSR اختصارًا، والتي تتألف من المعايير الخمسة التالية: PSR-0: معيار التحميل التلقائي. PSR-1: معيار كتابة الشيفرة البرمجية الأساسي. PSR-2: دليل تنسيق كتابة الشيفرة البرمجية. PSR-3: واجهة المسجِّل. PSR-4: المحمِّل التلقائي. لقد أُنشِئت معايير PSR بناءً على مدخلات من المشرفين على المنصات الأشهر في السوق لتوفر إرشادات وممارسات موحدة لتنظيم كتابة الأكواد في PHP. وقد وُضعت هذه المعايير من قبل مجموعة من المطورين والمشرفين على أبرز المنصات وأطر العمل الشهيرة مثل Zend و Drupal و Symfony و Joomla وغيرها في هذه المعايير، وتتبع هذه المنصات تلك المعايير، ويشارك الآن إطار عمل PEAR الذي حاول أن يكون معيارًا لسنوات قبل ذلك في وضع معايير PSR. لا يهم تقريبًا ما هو معيار كتابة الشيفرة البرمجية الخاص بك طالما أنك توافق على المعيار وتلتزم به، ولكن يُعَد اتباع معايير PSR فكرة جيدة إن لم يكن لديك سبب مقنع في مشروعك يخالفها، إذ أصبحت هناك العديد من الفرق والمشاريع التي تتوافق مع معايير PSR التي يُعِدّها أغلب مطوري PHP بوصفها المعيار الحالي، لذا سيساعد استخدامها في ضمان معرفة المطورين الجدد بمعيار كتابة الشيفرة البرمجية الخاص بك وسيشعرون بالارتياح عند انضمامهم إلى فريقك. الخطأ 10: استخدام الدالة empty()‎ استخدامًا خاطئًا يفضِّل بعض مطوري PHP استخدام الدالة empty()‎ لإجراء عمليات فحص منطقية لكل شيء تقريبًا، ولكن توجد حالات يمكن أن يؤدي فيها ذلك إلى الارتباك. أولًا، إذا عُدنا إلى المصفوفات ونُسخ كائن المصفوفة ArrayObject (الذي يحاكي المصفوفات)، فمن السهل افتراض أن المصفوفات ونسخ الكائن ArrayObject ستتصرف بطريقة مماثلة بسبب تشابههما، ولكنه افتراض خطير كما هو موضّح فيما يلي في الإصدار PHP 5.0: // ‫PHP 5.0 أو بعد‫: $array = []; var_dump(empty($array)); // ‫خرجها bool(true)‎ $array = new ArrayObject(); var_dump(empty($array)); // ‫خرجها bool(false)‎ // لماذا لا ينتج كلاهما الخرج نفسه؟ وستكون النتائج مختلفة في الإصدارات التي قبل الإصدار PHP 5.0: // الإصدارات التي قبل الإصدار‫ PHP 5.0: $array = []; var_dump(empty($array)); // ‫خرجها bool(false)‎ $array = new ArrayObject(); var_dump(empty($array)); // ‫خرجها bool(false)‎ تحظى هذه الطريقة بشعبية كبيرة لسوء الحظ، فمثلًا هذه هي الطريقة التي يعيد بها كائن Zend\Db\TableGateway في إطار عمل Zend Framework 2 البيانات عند استدعاء الدالة current()‎ مع نتيجة الدالة TableGateway::select()‎، ويمكن للمطور أن يصبح بسهولة ضحيةً لهذا الخطأ مع مثل هذه البيانات. يمكن تجنب هذه المشكلات من خلال التحقق من وجود بنى مصفوفات فارغة باستخدام الدالة count()‎ كما يلي: // ‫لاحظ أن ما يلي يعمل في جميع إصدارات PHP (سواء قبل أو بعد الإصدار 5.0): $array = []; var_dump(count($array)); // ‫خرجها int(0)‎ $array = new ArrayObject(); var_dump(count($array)); // ‫خرجها int(0)‎ تحوّل لغة PHP القيمة 0 إلى false، فيمكن أيضًا استخدام الدالة count()‎ ضمن شروط تعليمة if للتحقق من المصفوفات الفارغة. تجدر الإشارة أيضًا إلى أن الدالة ‎count()‎‎ في PHP لها تعقيد ثابت Constant Complexity (أو O(1)‎) على المصفوفات، مما يوضّح أنه الاختيار الصحيح. يوجد مثال آخر لذلك عندما تمثّل الدالة empty()‎ خطرًا من خلال دمجها مع دالة الأصناف السحرية ‎__get()‎. لنعرّف مثلًا صنفين Class مع وجود الخاصية test في كليهما، حيث نعرّف أولًا الصنف Regular الذي يتضمن الخاصية العادية test كما يلي: class Regular { public $test = 'value'; } نعرّف بعد ذلك الصنف Magic الذي يستخدم التابع السحري ‎__get()‎ للوصول إلى الخاصية test الخاصة به كما يلي: class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } } لنرى الآن ما سيحدث عندما نحاول الوصول إلى الخاصية test لكل صنف كما يلي: $regular = new Regular(); var_dump($regular->test); // ‫خرجها string(4) "value"‎ $magic = new Magic(); var_dump($magic->test); // ‫خرجها string(4) "value"‎ يبدو كل شيء على ما يرام حتى الآن، ولكن لنرى الآن ما يحدث عندما نستدعي الدالة empty()‎ مع كل منهما كما يلي: var_dump(empty($regular->test)); // ‫خرجها bool(false)‎ var_dump(empty($magic->test)); // خرجها‫ bool(true)‎ إذا اعتمدنا على الدالة empty()‎، فيمكن تضليلنا للاعتقاد بأن الخاصية test الخاصة بالصنف ‎$magic فارغة، بينما هي مضبوطة على القيمة 'value'. لسوء الحظ، إذا استخدم الصنفُ التابع السحري ‎__get()‎ لاسترداد قيمة خاصيةٍ ما، فلا توجد طريقة مضمونة للتحقق مما إذا كانت قيمة الخاصية فارغة أم لا. يمكنك خارج نطاق الصنف التحقق فقط من إعادة القيمة null، وهذا لا يعني بالضرورة عدم ضبط المفتاح المقابل، لأنه كان من الممكن ضبطه على القيمة null. إذا حاولنا الإشارة إلى خاصية غير موجودة لنسخة من الصنف Regular، فستظهر ملاحظة مشابهة لما يلي: Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0 لاحظ أنه يجب استخدام التابع empty()‎ بحذر لأنه قد يؤدي إلى نتائج مربكة ومضللة. الخلاصة يمكن أن تؤدي سهولة استخدام لغة PHP إلى جعل المطورين يشعرون بالراحة الزائدة في التعامل معها، مما قد يؤدي إلى تجاهل بعض الأخطاء الدقيقة التي تحتاج إلى تنقيح ومعالجة وعدم مراعاة بعض الفروق الدقيقة والخصوصيات في اللغة، ويمكن أن يؤدي ذلك إلى عدم عمل شيفرة PHP بنجاح وظهور إحدى المشكلات التي تعرضنا لها في هذا المقال. لقد تطورت لغة PHP تطورًا ملحوظًا على مدار تاريخها الطويل وشهدت تحسينات ومميزات عديدة، لذا يُعَد التعرف على تفاصيلها الدقيقة أمرًا جديرًا بالاهتمام، لأنها ستساعد على ضمان أن البرنامج الذي تنتجه أكثر قابلية للتوسع والقوة والصيانة. ترجمة -وبتصرُّف- للمقال Buggy PHP Code: The 10 Most Common Mistakes PHP Developers Make لصاحبه Ilya Sanosian. اقرأ أيضًا تعرف على لغة PHP تعلم لغة PHP اصطلاحات ومواضيع متفرقة مهمة لكل مبرمج PHP التعامل مع الأخطاء في PHP الأخطاء العشرة الأكثر شيوعًا في شيفرة بايثون Python البرمجية
  9. سنتحدث في هذا المقال عن معمارية MVC الخاصة بإطار عمل لارافيل Laravel، فبنية MVC هي مبدأ تصميم الويب الذي يتكون من النموذج ‎Model المسؤول عن التواصل مع قاعدة البيانات، والمتحكِّم ‎Controller وهو المكان الذي نخزّن فيه شيفرة تطبيقنا البرمجية، والعرض ‎View وهو الواجهة الأمامية للتطبيق. يوجد مُوجّه Router في تطبيق الويب عادةً، وهذا ما ناقشناه في المقال السابق، حيث يؤشّر الموجّه إلى المتحكم، ويبحث المتحكم عن البيانات المطلوبة في قاعدة البيانات عبر النموذج، ثم يضع البيانات المُسترَّدة إلى الموقع المقابل في العرض، وأخيرًا يعيد هذا العرض إلى المستخدم. طبقة العرض لنبدأ أولًا بطبقة بالعرض، حيث عرّفنا في المقال السابق الوِجهة Route التالية: Route::get('/', function () { return view('welcome'); }); تؤشّر هذه الوِجهة إلى العرض welcome المُخزَّن في المجلد resources/views وله الامتداد ‎.blade.php، حيث يُعلِم هذا الامتداد لارافيل أننا نستخدم نظام قوالب Blade الذي سنتحدث عنه لاحقًا. يمكننا إنشاء عرض آخر من خلال إنشاء ملف آخر له الامتداد نفسه في المجلد views، إذًا لننشئ العرض greetings.blade.php التالي: <html> <body> <h1>Hello!</h1> </body> </html> تحدثنا أيضًا في المقال السابق عن إمكانية تمرير البيانات من الوِجهة إلى العرض كما يلي: Route::get('/', function () { return view('greeting', ['name' => 'James']); }); أسندنا في المثال السابق القيمة 'James' إلى المتغير name، ومرّرنا هذا المتغير إلى العرض greeting.blade.php الذي أنشأناه مسبقًا، ويمكننا استخدام الأقواس المزدوجة المتعرجة {{ }} لعرض هذه البيانات في العرض greetings.blade.php كما يلي: <html> <body> <h1>Hello, {{ $name }}</h1> </body> </html> من الشائع تنظيم ملفات العرض في مجلدات مختلفة بالنسبة لتطبيقات الويب الكبيرة، فمثلًا يمكن أن يحتوي تطبيقك على لوحة إدارة، وبالتالي ستخزّن العروض Views المقابلة لها في مجلد فرعي اسمه admin مثلًا، وتذكّر استخدام . بدلًا من / عندما تؤشّر إلى ملفات العرض في بنية متداخلة من الموجّه، حيث يؤشّر المثال التالي إلى الملف home.blade.php الموجود في المجلد views/admin/‎: Route::get('/admin', function () { return view('admin.home'); }); صيغة قوالب Blade تستخدم عروض لارافيل قالب Blade افتراضيًا، وهو مشابه لنظام المكونات في إطار عمل Vue.js. تُعَد العروض مستندات HTML، ويضيف قالب Blade ميزات برمجية إليها مثل نظام الوراثة والتحكم في التدفق والحلقات والمفاهيم البرمجية الأخرى، مما يؤدي إلى إنشاء صفحة ويب أكثر ديناميكية مع كتابة تعليمات برمجية أقل. التعليمة الشرطية if يمكننا إنشاء تعليمات if باستخدام التوجيهات Directives التي هي ‎@if و ‎@elseif و ‎@else و ‎@endif، حيث تعمل هذه التعليمات مثل تعليمات if التي نراها في لغات البرمجة الأخرى. @if ($num == 1) <p>The number is one.</p> @elseif ($num == 2) <p>The number is two.</p> @else <p>The number is three.</p> @endif يمكننا اختبار هذه الشيفرة البرمجية باستخدام الوِجهة كما يلي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); نفترض هنا أن المتغير num يمكن أن يكون قيمته 1 أو 2 أو 3 فقط. التعليمة switch تكون بنية هذه التعليمة كما يلي: @switch($i) @case(1) First case. . . @break @case(2) Second case. . . @break @default Default case. . . @endswitch الحلقات يمكن أن نمثل الحلقات بحلقة for بسيطة: @for ($i = 0; $i < 10; $i++) The current value is {{ $i }} @endfor أو بحلقة foreach، حيث يساوي المتغير ‎$user العنصر التالي من ‎$users لكل تكرار في المثال التالي: @foreach ($users as $user) <p>This is user {{ $user->id }}</p> @endforeach يمكن أن نمثّل الحلقات بحلقة while أيضًا: @while (true) <p>I'm looping forever.</p> @endwhile تُعَد هذه التعليمات بسيطة جدًا، وتعمل تمامًا مثل نظيراتها في لغة PHP، ولكن يوجد شيء خاص بهذه الحلقات هو المتغير ‎$loop، فمثلًا ليكن لدينا شيء نريد عرضه مرة واحدة فقط، حيث يمكنك تطبيق ما يلي في التكرار الأول: @foreach ($users as $user) @if ($loop->first) <p>This is the first iteration.</p> @endif <p>This is user {{ $user->id }}</p> @endforeach سيُعرَض عنصر الفقرة <p>This is the first iteration.</p> مرة واحدة فقط في التكرار الأول، وتوجد العديد من الخاصيات الأخرى التي يمكنك الوصول إليها، لذا اطلع عليها في توثيق لارافيل الرسمي. لاحظ أنه لا يمكن الوصول إلى المتغير ‎$loop إلا ضمن الحلقة. الصنف الشرطي Conditional Class يُعَد الصنف Class الطريقة الأكثر استخدامًا لإسناد التنسيقات Styles إلى عناصر HTML المختلفة، ويمكننا بسهولة تغيير مظهر هذا العنصر من خلال تغيير صنفه، حيث يقدّم لارافيل طريقة لإسناد الأصناف ديناميكيًا بالاعتماد على المتغيرات كما يلي: <span @class([ 'p-4', 'font-bold' => $isActive, 'bg-red' => $hasError, ])></span> يعتمد الصنفان font-bold و bg-red في المثال السابق على قيمة المتغيرين ‎$isActive و ‎$hasError‎. بناء تخطيط الصفحة Layout توجد دائمًا بعض أجزاء الصفحة التي ستظهر في صفحات متعددة عندما نبني تطبيق ويب، حيث تُعَد كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا مضيعة للوقت والموارد، ولا يُعَد ذلك جيدًا للصيانة، لذا يجب فصل الجزء الذي سيظهر عدة مرات ووضعه في ملف آخر، وسنستورده ببساطة عندما نحتاج إليه. يقدم إطار عمل لارافيل طريقتين مختلفتين لذلك هما: وراثة القوالب ووراثة المكونات، حيث يُعَد فهم وراثة القوالب أسهل بكثير للمبتدئين، ولكن توفّر المكونات مزيدًا من الميزات، إذًا لنبدأ أولًا بوراثة القوالب. وراثة القوالب نحاول في المثال التالي تعريف صفحة رئيسية، وتكون الوِجهة كما يلي: Route::get('/', function () { return view('home'); }); يستورد العرض layout.blade.php ملفات CSS وجافاسكريبت JavaScript الضرورية، بالإضافة إلى شريط التنقل وتذييل الصفحة التي ستظهر في جميع الصفحات، وسنرث layout.blade.php في الملف home.blade.php. لنضع أولًا ما يلي في الملف layout.blade.php: <html> <head> @yield('title') </head> <body> <div class="container">@yield('content')</div> </body> </html> ولنضع الآن ما يلي في الملف home.blade.php: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. . .</p> @endsection لاحظ أن ‎@section('title')‎ يقابل ‎@yield('title')‎، وأن ‎@section('content')‎ يقابل ‎@yield('content')‎. سيرى لارافيل أولًا التوجيه ‎@extends('layout')‎ عند عرض الصفحة الرئيسية في مثالنا، وسيعرف أنه يجب عليه الانتقال إلى الملف layout.blade.php. سيحدد لارافيل بعد ذلك موقع ‎@yield('title')‎ ويضع القسم title مكانه، ثم يبحث عن التوجيه ‎@yield('content')‎ ويضع القسم content مكانه. سنحتاج في بعض الأحيان إلى تحميل عرض آخر من العرض الحالي، فمثلًا إذا أردنا إضافة شريط جانبي إلى صفحتنا الرئيسية، وهو شيء سنضمّنه في بعض الصفحات دون صفحات أخرى، فلن نضعه في الملف layout.blade.php، ولكن سيكون من الصعب جدًا صيانته إذا أنشأتَ شريطًا جانبيًا لكل صفحة تتطلب ذلك. يمكننا في مثالنا إنشاء الملف sidebar.blade.php، ثم استخدام التوجيه ‎@include لاستيراد هذا الملف في الصفحة الرئيسية كما يلي: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>. . .</p> @include('sidebar') @endsection المكونات Components يشبه نظام المكونات في لارافيل إلى حد كبير النظام الموجود في إطار عمل Vue.js. سنبني صفحة رئيسية مع تخطيطها، ولكننا سنعرّف التخطيط بوصفه مكونًا، لذا سننشئ المجلد components ضمن المجلد resources/views/‎، ثم ننشئ الملف layout.blade.php بداخله. لنضع أولًا ما يلي في الملف views/components/layout.blade.php: <html> <head> . . . </head> <body> <div class="container">{{ $slot }}</div> </body> </html> يمكن استخدام هذا المكون من خلال استخدام الوسم <x-layout>، وتذكّر دائمًا البادئة x-‎. <x-layout> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد المحتوى الموجود ضمن العنصر <x-layout> إلى المتغير ‎$slot تلقائيًا، ولكن إذا كان لدينا فتحات Slots متعددة كما في مثالنا الذي يحتوي على قسم title وقسم content، فيمكن حل هذه المشكلة من خلال تعريف عنصر <x-slot> كما يلي: <x-layout> <x-slot name="title"> <title>Home Page</title> </x-slot> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد محتوى <x-slot name="title"‎> إلى المتغير ‎$title وسيُسنَد المحتوى المتبقي إلى المتغير ‎$slot كما يلي: <html> <head> {{ $title }} </head> <body> <div class="container">{{ $slot }}</div> </body> </html> التعامل مع قواعد البيانات في لارافيل سنناقش أولًا كيفية التعامل مع قواعد البيانات في لارافيل قبل أن نتحدث بمزيد من التفصيل عن طبقة النموذج والمتحكم في بنية MVC. يمكن أن نستخدم ملف ‎.txt بسيط لتخزين المعلومات عند استخدام جافاسكريبت JavaScript و Node.js، ولكن لن ينجح ذلك في تطبيق الويب الواقعي، لذا سنحتاج لاستخدام قاعدة بيانات لتخزين ومعالجتها المعلومات بكفاءة. استخدمنا في مقالنا الحالي أداة Sail الخاصة بلارافيل لإنشاء هذا المشروع، لذا أعدت هذه الأداة قاعدة بيانات MySQL، إذ يجب أن توجد متغيرات البيئة التالية في ملف ‎.env: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password عمليات تهجير Migrations قاعدة البيانات تكون قاعدة البيانات فارغة حاليًا، لذا سنضيف بعض جداول قاعدة البيانات لاستخدامها، حيث ستحتوي هذه الجداول على أعمدة، وستكون للأعمدة أسماء وبعض المتطلبات الخاصة، وتسمى عملية الإعداد هذه بتشغيل عمليات التهجير. تُخزَّن جميع ملفات التهجير في لارافيل ضمن المجلد database/migrations، ويمكننا إنشاء ملف تهجير جديد من خلال استخدام الأمر البسيط التالي، ولكن إذا استخدمتَ أداة Sail لإنشاء مشروع لارافيل، فضع ‎./vendor/bin/sail بدلًا من php لتشغيل PHP داخل حاوية دوكر Docker، وإن لم تستخدم أداة Sail، فتابع كالمعتاد: php artisan make:migration create_flights_table يمكن تطبيق ملف التهجير كما يلي: php artisan migrate وإذا أردتَ التراجع عن عملية التهجير السابقة، فاستخدم الأمر التالي: php artisan migrate:rollback أو إذا أردتَ إعادة ضبط عمليات التهجير بالكامل والتراجع عن جميع التغييرات المطبقة على قاعدة البيانات، فاستخدم الأمر التالي: php artisan migrate:reset إنشاء جدول لنلقِ نظرة على المثال التالي قبل إنشاء ملف التهجير. افتح ملف التهجير database/migrations/2014_10_12_000000_create_users_table.php التالي الذي يأتي مع لارافيل: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('users'); } }; يحتوي هذا الملف على صنف class له تابعان مختلفان، حيث يُستخدَم التابع ‎up()‎ لإنشاء جداول وأعمدة جديدة، بينما يمكن استخدام التابع ‎down()‎ لعكس العمليات التي يطبّقها التابع ‎up()‎. يستخدم لارافيل داخل التابع ‎up()‎ التابع create()‎ ضمن الصنف Schema لإنشاء جدول جديد بالاسم 'users'. وينشئ كل سطر من السطور 15 إلى 21 في الشيفرة البرمجية السابقة عمودًا مختلفًا مع اسم ونوع مختلفين، فمثلًا ينشئ السطر ‎$table->string('name');‎ عمودًا بالاسم name ضمن الجدول users، ويجب أن يخزّن هذا العمود سلاسلًا نصية. اطّلع على القائمة الكاملة لأنواع الأعمدة المتوفرة في لارافيل. لن نستعرض جميع هذه الأنواع، بل سنناقش أهمها وسبب اختيارنا لنوع العمود عندما نواجه مشكلات محددة مستقبلًا. لاحظ السطر 17 في الشيفرة البرمجية السابقة ‎$table->string('email')->unique();‎، حيث يوجد التابع unique()‎ بعد التصريح عن اسم العمود ونوعه، حيث يُسمَّى هذا التابع بمُعدِّل العمود Column Modifier، لأنه يضيف قيودًا إضافية إلى هذا العمود، ويتأكد في هذه الحالة من أن العمود لا يمكن أن يحتوي على قيم متكررة، ويجب أن يكون كل بريد إلكتروني يقدّمه المستخدم فريدًا. اطّلع على القائمة الكاملة لمُعدِّلي الأعمدة المتوفرة في لارافيل. إجراء تغييرات على الجدول يمكن أيضًا تحديث الجداول باستخدام التابع table()‎ كما يلي، حيث تضيف الشيفرة البرمجية التالية عمودًا جديدًا هو العمود age إلى الجدول users الموجود مسبقًا: Schema::table('users', function (Blueprint $table) { $table->integer('age'); }); كما يمكننا إعادة تسمية الجدول باستخدام التابع rename()‎: Schema::rename($from, $to); ويمكننا حذف جدول نهائيًا كما يلي: Schema::drop('users'); Schema::dropIfExists('users'); البذر أو توليد البيانات Seeding يُستخدَم مولّد البيانات Seeder لإنشاء بيانات وهمية لقاعدة بياناتك، وبالتالي يسهّل عليك إجراء الاختبارات، حيث يمكنك إنشاء مولّد بيانات من خلال تشغيل الأمر التالي: php artisan make:seeder UserSeeder يولّد الأمر السابق مولّد بيانات للجدول users، وسيُوضَع في المجلد database/seeders، حيث وضع ما يلي في الملف UserSeeder.php: <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; class UserSeeder extends Seeder { /** * تشغيل مولّدات بيانات قاعدة البيانات */ public function run(): void { DB::table('users')->insert([ 'name' => Str::random(10), 'email' => Str::random(10).'@gmail.com', 'password' => Hash::make('password'), ]); } } شغّل الأمر التالي لتنفيذ مولّد البيانات السابق وملء جداول قاعدة البيانات بالبيانات الافتراضية: php artisan db:seed UserSeeder باني الاستعلامات Query Builder باني الاستعلامات هو واجهة تسمح لنا بالتفاعل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع table()‎ الذي يوفره الصنف DB لاسترداد جميع الصفوف الموجودة في الجدول كما يلي: use Illuminate\Support\Facades\DB; $users = DB::table('users')->get(); يمكننا إضافة مزيدٍ من القيود من خلال إضافة التابع where()‎ إلى سلسلة التوابع كما يلي، حيث ستحصل الشيفرة البرمجية التالية على جميع الصفوف التي تكون فيها القيمة المخزنة في العمود age أكبر من أو تساوي 21: $users = DB::table('users')->where('age', '>=', 21)->get(); توجد الكثير من التوابع الأخرى إلى جانب التابعين where()‎ و get()‎، والتي سنتحدث عنها لاحقًا. طبقة النموذج Model يُعَد النموذج مفهومًا مهمًا جدًا في تصميم الويب الحديث، فهو مسؤول عن التفاعل مع قاعدة بيانات تطبيق الويب، وهو جزء من نظام رابط الكائنات بالعلاقات Object-Relational Mapper -أو ORM اختصارًا- في لارافيل الذي هو نظام Eloquent، حيث يمكن عَدّه باني استعلام مع بعض الميزات الإضافية. يمكننا استخدام الأمر make:model لإنشاء نموذج جديد كما يلي: php artisan make:model Post إذا أردتَ توليد ملف تهجير مقابل للنموذج، فاستخدم الخيار ‎--migration: php artisan make:model Post --migration وسيتضمّن ملف النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; . . . } يفترِض النموذج السابق وجود الجدول posts في قاعدة البيانات افتراضيًا، وإذا أردتَ تغيير هذا الإعداد، فاضبط الخاصية ‎$table كما يلي: class Post extends Model { /** * الجدول المرتبط بالنموذج * * @var string */ protected $table = 'my_posts'; } استرداد النموذج يُعَد نموذج Eloquent بانيَ استعلامات قويًا ويسمح بالتواصل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع all()‎ لاسترداد كافة السجلات في الجدول كما يلي، ولاحظ أننا استوردنا النموذج Post بدلًا من الصنف DB، حيث سيتصل النموذج Post تلقائيًا بالجدول posts: use App\Models\Post; $posts = Post::all(); بما أن النموذج هو باني استعلامات، لذا يمكن أن تصل النماذج إلى جميع توابع باني الاستعلامات كما يلي: $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); إذا استخدمتَ التابعين all()‎ أو get()‎ لاسترداد البيانات، فلن تكون القيمة المُعادة مصفوفةً أو كائنًا بسيطًا، ولكنها ستكون نسخة من الصنف Illuminate\Database\Eloquent\Collection، حيث يوفّر الصنف Collection عدة توابع أقوى من الكائنات البسيطة. يمكن استخدام التابع find()‎ مثلًا لتحديد موقع سجل باستخدام مفتاح رئيسي هو id عادة. $posts = Post::all(); $post = $posts->find(1); سيكون ‎$post في هذا المثال هو المنشور ذو المعرّف id==1. إدراج وتحديث النموذج يمكننا أيضًا إدراج السجلات أو تحديثها باستخدام النموذج، ولكن تأكد أولًا من أن النموذج المقابل له الخاصية ‎$fillable وتأكد من سرد جميع الأعمدة التي يجب أن تكون قابلة للملء. class Post extends Model { /** * السمات‫ Attributes التي يمكن إسنادها بطريقة جماعية * * @var array */ protected $fillable = ['title', 'content']; } يمكننا بعد ذلك استخدام التابع create()‎ لإنشاء سجل جديد كما يلي: $flight = Post::create([ 'title' => '. . .', 'content' => '. . .', ]); أو يمكننا تحديث السجلات الموجودة مسبقًا باستخدام التابع update()‎: Flight::where('published', true)->update(['published' => false]); حذف النموذج يوجد تابعان يسمحان بحذف السجلات، حيث إذا أردتَ حذف سجل واحد، فاستخدم التابع delete()‎: $posts = Post::all(); $post = $posts->find(1); $post->delete(); وإذا أردتَ حذف السجلات بطريقة جماعية، فاستخدم التابع truncate()‎: $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); $posts->truncate(); علاقات قاعدة البيانات لا تكون جداول قاعدة البيانات في تطبيقات الويب مستقلة بل تكون مترابطة بعلاقات فيما بينها، فمثلًا يمكن أن يكون لدينا مستخدم لديه منشورات ويمكن أن يكون لدينا منشوراتٌ تخص مستخدمًا ما، لذا يقدّم لارافيل طريقة لتعريف هذه العلاقات باستخدام نماذج Eloquent. يُحتمَل أن يكون القسم التالي صعبًا بعض الشيء بالنسبة للمبتدئين، ولكن لا تقلق بهذا الشأن، إذ سنعود إلى هذا الموضوع لاحقًا عندما نبدأ في إنشاء تطبيق المدونة، حيث سنوضّح فقط ما نحتاج إلى استخدامه لبناء نظام تدوين بسيط. علاقة واحد إلى واحد تُعَد علاقة واحد إلى واحد One to One العلاقة الأساسية، فمثلًا يرتبط كل مستخدم User بحيوان أليف Pet واحد. يمكن تعريف هذه العلاقة من خلال وضع التابع pet في النموذج User كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * الحصول على الحيوان الأليف المرتبط بالمستخدم */ public function pet() { return $this->hasOne(Pet::class); } } إذا أردنا أن نطبّق العكس، حيث يكون معكوس العلاقة "يملك" has one هو "يعود إلى" belongs to one، مما يعني أن كل حيوان أليف Pet يعود إلى مستخدم User واحد، ويمكن تعريف معكوس العلاقة واحد إلى واحد من خلال وضع التابع user في النموذج Pet كما يلي: class Pet extends Model { /** * الحصول على المستخدم الذي يملك الحيوان الأليف */ public function user() { return $this->belongsTo(User::class); } } لكننا لم ننتهِ بعد، إذ يجب أيضًا إجراء بعض التغييرات على جداول قاعدة البيانات المتقابلة، إذ يفترض النموذج User وجود العمود pet_id ضمن الجدول users، ويخزّن معرّف id الحيوان الأليف الذي يملكه المستخدم. يجب أيضًا إجراء تعديلات على الجدول pets المقابل، إذ يجب وجود العمود user_id الذي يخزّن معرّف id المستخدم الذي يعود إليه هذا الحيوان الأليف. علاقة واحد إلى متعدد تُستخدم علاقة واحد إلى متعدد One-to-Many لتعريف العلاقات التي يمتلك فيها نموذج واحد نسخًا متعددة من نموذج آخر، فمثلًا يمكن أن تحتوي فئة Category واحدة على العديد من المنشورات posts، ويمكن تعريف هذه العلاقة من خلال وضع التابع posts في النموذج Category كما يلي: class Category extends Model { public function posts() { return $this->hasMany(Post::class); } } ولكن يجب في بعض الأحيان العثور على الفئة عبر المنشور، فمعكوس العلاقة "لديه العديد" has many هو "ينتمي إلى" belongs to، ويمكن تعريف معكوس العلاقة واحد إلى متعدد من خلال وضع التابع category في النموذج Post كما يلي: class Post extends Model { public function category() { return $this->belongsTo(Category::class); } } تفترض هذه العلاقة أن الجدول posts يحتوي على العمود category_id الذي يخزّن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تُعَد العلاقة متعدد إلى متعدد Many-to-Many أصعب بعض الشيء، فمثلًا يمكن أن يكون لدينا مستخدم User لديه العديد من الأدوار، ويمكن أن يكون لدينا دور Role له العديد من المستخدمين كما يلي: class User extends Model { /** * الأدوار التي تخص المستخدم */ public function roles() { return $this->belongsToMany(Role::class); } } class Role extends Model { /** * المستخدمون الذين ينتمون إلى الدور */ public function users() { return $this->belongsToMany(User::class); } } تفترض هذه العلاقة وجود الجدول role_user في قاعدة البيانات واحتواء الجدول role_user على العمودين user_id و role_id، وبالتالي يمكننا مطابقة المستخدم مع دوره والعكس صحيح. لنفترض أن لدينا الجدول role_user كما يلي: user_id role_id 1 1 2 1 3 2 1 2 2 3 يمتلك المستخدم الذي له المعرّف id=1 دورَين لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمر للعثور على المستخدمين عبر الدور، فيمكننا أن نرى مستخدمَين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للدور ذي المعرّف id=2. طبقة المتحكم Controller وضّحنا الوِجهات والعروض والنماذج وعلاقات قاعدة البيانات، وحان الوقت الآن لنتعرّف على الشيء الذي يربطها جميعًا مع بعضها البعض. تحدّثنا في المقال السابق عن الوِجهات، واستخدمنا المثال التالي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); يبدو الأمر جيدًا في الأمثلة البسيطة، ولكن إذا كان لديك مشروع ضخم، فسيؤدي وضع كل الشيفرة البرمجية في ملف الوِجهة Route File إلى جعل الأمر فوضويًا جدًا، والحل الأفضل هو التأكّد من أن الوِجهة تؤشّر دائمًا إلى تابع في المتحكم مع وضع الشيفرة البرمجية ضمن هذا التابع. use App\Http\Controllers\UserController; Route::get('/users/{name}', [UserController::class, 'show']); يمكن إنشاء متحكم جديد من خلال استخدام الأمر التالي: php artisan make:controller UserController تُخزَّن ملفات متحكم لارافيل ضمن المجلد app/Http/Controllers/‎. المتحكم الأساسي لنلقِ نظرة على المثال التالي، حيث يتوقع التابع show()‎ المتغير ‎$id، ويستخدِم هذا المعرّف id لتحديد موقع المستخدم في قاعدة البيانات، ويعيد هذا المستخدم في العرض. <?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * عرض الملف الشخصي لمستخدمٍ معين */ public function show(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } تحتاج في بعض الأحيان إلى تعريف متحكم له تابع واحد فقط، ويمكننا في هذه الحالة تبسيط الشيفرة البرمجية من خلال تغيير اسم التابع إلى ‎_invoke. class UserController extends Controller { public function __invoke(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } لا حاجة الآن لتحديد اسم التابع في الوِجهة كما يلي: Route::get('/users/{id}', UserController::class); متحكم الموارد المورد Resource هو مفهوم حديث آخر لتصميم الويب، حيث نرى جميع نماذج Eloquent بوصفها مواردًا، مما يعني أننا سنجري مجموعة الإجراءات نفسها عليها جميعًا، وهذه الإجراءات هي الإنشاء ‎Create والقراءة ‎Read والتحديث ‎Update والحذف ‎Delete -أو CRUD اختصارًا. يمكن إنشاء متحكم موارد من خلال استخدام الخيار ‎--resource كما يلي: php artisan make:controller PostController --resource سيحتوي المتحكم الذي أنشأناه على التوابع التالية تلقائيًا: <?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): Response { // } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): Response { // } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // } /** * عرض المورد المُحدَّد */ public function show(string $id): Response { // } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): Response { // } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { // } } تشرح التعليقات في الشيفرة البرمجية السابقة وظيفة التوابع والغرض منها. يقدّم لارافيل أيضًا طريقة سهلة جدًا لتسجيل الوِجهات لكل من التوابع السابقة كما يلي: Route::resource('posts', PostController::class); ستَنشَأ الوِجهات التالية باستخدام توابع HTTP المختلفة تلقائيًا: تابع HTTP مسار URI الإجراء اسم الوِجهة GET ‏ ‎/posts ‏ index ‏ posts.index GET ‏ ‎/posts/create ‏ create ‏ posts.create POST ‏ ‎/posts ‏ store ‏ posts.store GET ‏ ‎/posts/{post}‎ ‏ show ‏ posts.show GET ‏ ‎/posts/{post}/edit ‏ edit ‏ posts.edit PUT/PATCH ‏ ‎/posts/{post}‎ ‏ update ‏ posts.update DELETE ‏ ‎/posts/{post}‎ ‏ destroy ‏ posts.destroy ترجمة -وبتصرُّف- للمقال Laravel for Beginners #2 - The MVC Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الأول: البدء في إنشاء مدونة بسيطة كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تعرف على إطار عمل تطوير الويب لارافيل Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder علاقات Eloquent والتحميل الحثيث في Laravel 5
  10. يُعَد الجداء الشعاعي عمليةً تستخدم شعاعين على أساس معامَلين. وتكمن أهمية هذا في الرسوميات الحاسوبية عند محاولة العثور على شعاع عمودي على مستوٍ؛ ويُعَد ذلك ضروريًا عند حساب كيفية انعكاس الضوء على السطوح. سنوضّح في هذا المقال المواضيع التالية: تعريف الجداء الشعاعي لشعاعين. قاعدة اليد اليمنى: يكون منحى Orientation الجداء الشعاعي ‎u × v‎ باتجاه "الإبهام" إذا طويت أصابعك من الشعاع u إلى الشعاع v. خاصيات الجداء الشعاعي والتي هي: حجم Magnitude الجداء الشعاعي: حجم الجداء الشعاعي u × v هو مساحة متوازي الأضلاع التي يحدّدها هذان الشعاعان. لا يُعَد الجداء الشعاعي عملية تبديلية Anti-commutative: u × v = -(v × u)‎‎ u × u = 0‎ و (ku) × u = 0 نتيجة الجداء الشعاعي لشعاع مع شعاع صفري هي شعاع صفري: u × 0 = 0 × u = 0‎ لا يُعَد الجداء الشعاعي عملية تجميعية: ‎(u × v) × w ≠ u × (v × w) إمكانية توزيع الجداء الشعاعي على جمع الأشعة: u × (v + w) = u × v + u × w‎ الجداء الشعاعي بين محاور الإحداثيات: i × j = k و j × k = i و k × i = j حساب الجداء الشعاعي باستخدام المصفوفات العمودية. استخدام أداة مساعدة شبيهة بمحدّدات المصفوفات Determinant لتذكّر صيغة حساب الجداء الشعاعي للمصفوفات العمودية. تذكير: يكون نوع الكائنات التي تمثّلها معامَلات الجداء النقطي أشعةً، وتكون نتيجة الجداء النقطي عددًا حقيقيًا (مقدارًا سلميًا) Scalar. لقد شاهدت حتى الآن العمليات التالية: ولكن لا توجد عملية تشبه الضرب وتأخذ شعاعين كمعامَلين وينتج عنها شعاع، وهذا ما يمثله جداء الأشعة الشعاعي الذي سنشرحه في هذا المقال. تعريف الجداء الشعاعي إذا كان u و v شعاعين في فضاء ثلاثي الأبعاد، فإن الجداء الشعاعي لهما ‎u × v‎ هو شعاع ثلاثي الأبعاد، ويجب أن تكون معاملات هذه العملية ونتيجتها أشعةً ثلاثية الأبعاد على عكس العمليات الأخرى؛ إذ أن عملية الجداء الشعاعي ليست مُعرَّفةً في الفضاء ثنائي الأبعاد 2D. يمكن تعريف الجداء الشعاعي لشعاعين u × v كما يلي: يجب أن يكون u و v شعاعين ثلاثي الأبعاد. والنتيجة هي شعاع ثلاثي الأبعاد مع الطول والمنحى التاليين: الطول: ‎|‎ u × v | = | u | | v | sin θ حيث θ هي الزاوية بين الشعاعين u و v. المنحى: يكون ناتج الجداء الشعاعي u × v عموديًا على كلٍّ من الشعاعي u و v. يجري الاختيار من بين منحيين متعامدين على الشعاعين u و v باستخدام قاعدة اليد اليمنى. ناتج الجداء الشعاعي لشعاعين هو شعاع عمودي على هذه الشعاعين، ولكن هناك اتجاهين متعامدين على كلٍّ من هذين المعامَلين، حيث يمكن الاختيار من بينهما باستخدام قاعدة اليد اليمنى، والتي سنشرحها في الفقرة التالية. قاعدة اليد اليمنى Right-hand Rule نضع البرغي الخشبي عموديًا على لوحٍ من الخشب، ونلفّه باتجاه اللوح الخشبي باستخدام مفك براغي؛ إذ يجب أن ندير البرغي اليميني (النوع العادي) باتجاه عقارب الساعة من رأسه إلى اللوح الخشب. يكون الجداء الشعاعي لشعاعين متعامدًا عليهما، وتختار قاعدة اليد اليمنى أحد الاتجاهين المتعامدين المُحتمَلين. ويوضح الرسمان البيانيان السابقان هذه القاعدة. لا تنسَ أن ترتيب المعاملات مهم، إذ تؤشّر نتيجة الجداء الشعاعي ‎u × v‎ إلى الاتجاه نفسه الذي يجب أن يؤشّر إليه البرغي، إذا دفع الالتفافُ من الشعاع u إلى الشعاع v البرغي إلى داخل قطعة الخشب، أو فكّر في الاتجاه الذي يؤشّر إليه إبهامك عندما تطوي أصابعك من الشعاع u إلى الشعاع v. ملاحظة: يؤشّر الجداء الشعاعي ‎u × v‎ و ‎v × u‎ إلى اتجاهين متعاكسين. تصور الجداء الشعاعي يُظهر الشكل التالي الشعاعين u و v ومتوازي الأضلاع المُحدَّد بينهما. مساحة متوازي الأضلاع هي: ‎| ‎u | | v | sin θ، حيث θ هي الزاوية بينهما. تمثّل هذه المساحة حجم الجداء الشعاعي، وتكون هذه الحقيقة مفيدةً أحيانًا لتصور الجداء الشعاعي، فإذا دوّرنا الشعاع u مثلًا، بحيث يقترب منحاه من منحى الشعاع v (دون تغيير طوله)، فستقترب مساحة متوازي الأضلاع وحجم الجداء الشعاعي من الصفر، بينما تعطي الزاوية 90 درجة بين الشعاعين u و v القيمة العظمى للجداء الشعاعي. عكس ترتيب معاملات الجداء الشعاعي تذكير: يمكن عكس اتجاه الشعاع w من خلال ضربه بالعدد ‎-1؛ إذ يؤشّر الشعاعان w و ‎-w إلى اتجاهين متعاكسين. يوضح الشكل السابق حاصل عمليتي جداء شعاعين حدّدنا منحاهما باستخدام قاعدة اليد اليمنى، ويكون حجم الجداء الشعاعي: ‎‎u × v‎ = ‎| u | | v | sin θ = | v | | u | sin θ‎‎ وبالتالي: ‎u × v‎ = -(v × u)‎‎ ويمكن القول أن: ‎-(v × u) = -v × u‎‎‎ المعاملات المتوازية للجداء الشعاعي طول ‎u × v‎ هو ‎| u | | v | sin θ، وإذا كان u و v شعاعين على استقامة واحدة Collinear (متوازيين)، فإن الزاوية بينهما θ = 0، وبالتالي سيكون sin θ = 0، وحجم ناتج الجداء الشعاعي يساوي الصفر، ولكن النتيجة هي شعاع؛ وبالتالي تكون نتيجة الجداء الشعاعي هي الشعاع الصفري 0. u × u = 0 بما أن k u‎ على استقامة واحدة مع الشعاع u (حيث k عدد حقيقي)، فسيكون: ‎(k u) × u = 0 نتيجة الجداء الشعاعي u × u = 0 عمودية على كلا المعامَلين، لأن ‎0‎ · u = 0، مما يعني أنهما متعامدان؛ فالشعاع الصفري 0 عمودي على جميع الأشعة كما مر معنا سابقًا. حجم الجداء الشعاعي ‎u × 0‎ وهو ‎| u | | 0 | sin θ يساوي الصفر، والنتيجة هي شعاع، وبالتالي: u × 0 = 0 × u = 0‎ لنوجد الآن الجداء الشعاعي ‎(k u) × v‎ من خلال الاطلاع على الرسم البياني السابق، أو من خلال العمل بالصيغة التالية: ‎(k u) × v = k( u × v‎ ) وسينتج ما يلي: | (k u) × v | = | k u | | v | sin θ = | k | | u | | v | sin θ إذًا يكون حجم ‎(k u) × v أكبر بمقدار | k | عن حجم u × v مع إبقاء الاتجاه نفسه. ليس الجداء الشعاعي للأشعة عملية تجميعية لا يُعَد الجداء الشعاعي للأشعة عمليةً تجميعية، ويمكن رؤية ذلك من خلال النظر إلى الرسم البياني السابق، ثم تشكيل الجداء الشعاعي (‎u × v‎)، ثم إجراء الجداء الشعاعي للناتج مع الشعاع w. نشكّل بعد ذلك حاصل الجداء الشعاعي (‎v × w‎)، ثم نجري الجداء الشعاعي للناتج مع الشعاع u؛ وينتج لدينا ما يلي: ‎(u × v) × w ≠ u × (v × w) لاحظ أن ناتج الجداء الشعاعي للشعاعين u × v من الرسم البياني السابق يساوي الشعاع الصفري 0، لأنهما على استقامة واحدة. إمكانية توزيع الجداء الشعاعي على جمع الأشعة توجد خاصية أخرى للجداء الشعاعي، وهي إمكانية توزيع الجداء الشعاعي على عملية جمع الأشعة كما يلي: u × (v + w) = u × v + u × w‎ ويمكن الوصول إلى هذه الخاصية من خلال التعويض في الصيغة التي تعرّف الجداء الشعاعي. الجداء الشعاعي بين محاور الإحداثيات تسمَّى أشعة الوحدة Unit Vectors التي تكون باتجاه المحاور x و y و z (في أيّ إطار إحداثي مُستخدَم)، i و j و k في أغلب الأحيان، وتوضَع أحيانًا قبعات صغيرة على كل حرف كما في الشكل التالي: بما أن طول كل شعاع من هذه الأشعة يساوي 1، وهي أشعة متعامدة فيما بينها، فإن جيب Sine الزاوية بين أيّ شعاعين منها يساوي 1.0، وستكون الصيغ التالية صحيحة: i × j = k‎ j × k = i‎ k × i = j‎ i × k = - ‎j k × j = - ‎i j × i = - k‎‎ i × i = 0‎ j × j = 0‎ k × k = 0‎ ولكن يُفضَّل رسم الرسم البياني ثم استخدام قاعدة اليد اليمنى بدلًا من حفظها. الجداء الشعاعي للمصفوفات العمودية نعرّف الجداء الشعاعي لمصفوفتين عموديتين ‎u × v‎ كما يلي: يجب أن تمثّل المصفوفتان العموديتان u و v أشعة ثلاثية الأبعاد. تمثّل النتيجة شعاعًا ثلاثي الأبعاد. إذا مثّلنا الشعاع u بالمصفوفة العمودية: ‎u = (ui, uj, uk)T‎ ومثّلنا الشعاع v بالمصفوفة العمودية: ‎v = (vi, vj, vk)T‎ فإن: ‎‎u × v = ( uj vk - uk vj, uk vi - ui vk , ui vj - uj vi )T‎‎ ملاحظة: لا تفكر في حفظ النتيجة السابقة، بل حاول إيجاد نمط محدّد لتشكيلها. لنوجد الآن الجداء الشعاعي ‎(1, 2, 3)T × (0, 0, 0)T‎: لاحظ أن: ‎(1, 2, 3)‎T × 0 = 0 أو يمكنك التعويض في صيغة الجداء الشعاعي للمصفوفات العمودية: ‎(1, 2, 3)T × (0, 0, 0)T = ( 2×0 - 3×0, 3×0 - 1×0, 1×0 - 2×0)T = 0 طريقة مساعدة لتذكر صيغة الجداء الشعاعي للمصفوفات العمودية سنوضّح الآن طريقة لحساب الجداء الشعاعي من خلال ترتيب عناصر كل شعاع في محدّد مصفوفة Determinant، والذي يساعدك لتذكر صيغة الجداء الشعاعي السابقة الصعبة جدًا، ولكن إذا كنت تعرف مسبقًا كيفية تقييم المحدّدات ذات الحجم 3x3، فيمكنك تخطي هذه الطريقة، واستخدام الصيغة الموجودة في التعريف. يحتوي الصف العلوي من المحدّد على الرموز التي تمثل كل محور، ولكن تذكّر أن هذه الطريقة لا تمثّل تعريف الجداء الشعاعي، ولا تمثّل محدد مصفوفة، ولكنها مجرد أداة مساعدة للتذكر. تعمل هذه الطريقة فقط مع الأشعة ثلاثية الأبعاد، إذ لم يُعرَّف الجداء الشعاعي إلّا للأشعة في الفضاء ثلاثي الأبعاد، لذا ضع مكونات الشعاع الأول في الصف الثاني من المحدّد، ومكونات الشعاع الثاني في الصف الثالث. تدريب عملي تدريب1: لنوجِد الجداء الشعاعي للمصفوفتين العموديتين ‎(1, 2, 1)T‎ و ‎(0, -1, 2)T‎ كما يلي: ولنقيّم الآن النتيجة من خلال البحث عن العوامل المساعدة للمكوّن i و j و k التي أحطناها بدائرة حمراء في الصف العلوي؛ فالعوامل المساعدة للمكوّن i مثلًا هي عناصر الصفين 2 و 3 التي ليست موجودة في العمود i. يمكن إيجاد العامل المساعد من خلال ضرب العنصرين الموجودين على القطر الرئيسي، ثم طرح حاصل ضرب العنصرين الموجودين على القطر الآخر من نتيجة ضرب العنصرين الموجودين على القطر الرئيسي، فعلى سبيل المثال، يكون العامل المساعد للمكوّن i هو 2×2 - 1×(-1) = 2 + 1 = 5. تدريب2: يكون حاصل الجداء الشعاعي للمصفوفتين العموديتين ‎(0, -1, 2)T‎ و ‎(1, 2, 1)T‎ كما يلي (لاحظ أننا عكسنا ترتيب الشعاعين السابقين): يتبع تقييم هذا المثال نمط المثال السابق نفسه، ولكن مع تبديل الصفين الثاني والثالث كما يلي: لاحظ أن النتيجة تمثّل معاكس النتيجة السابقة، مما يدل على أن: u × v = -(v × u)‎ ولقد رأينا ذلك سابقًا عندما استخدمنا الأشعة الهندسية. الجداء الشعاعي لمصفوفة عمودية مع نفسها يبقى الصفان الثاني والثالث كما هما عندما نأخذ الجداء الشعاع لشعاعٍ مع نفسه، وتذكّر من دروس الرياضيات في المرحلة الثانوية أنه إذا كان صفان من المحدّد متماثلين، فستكون قيمته صفرًا؛ حيث يكون كل عامل مساعد صفرًا، مما يؤدي إلى أن: 0‎ i + 0 j + 0 k = 0‎‎ يحدث الشيء نفسه إذا كان أحد الصفوف مضاعفًا للآخر؛ إذ تعكس هذه النتائج ما رأيناه مسبقًا مع الأشعة الهندسية: k u × u = 0‎ تنطبق أيضًا الخاصيات الأخرى لحاصل الجداء الشعاعي للأشعة الهندسية على حاصل الجداء الشعاعي لتمثيلاتها باستخدام المصفوفات العمودية. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الجداء الشعاعي للأشعة، وسنتعرّف في المقال التالي على المصفوفات المستطيلة وعملياتها. ترجمة -وبتصرُّف- للفصل Vector Cross Product من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: إسقاط شعاع على شعاع آخر واستخداماته في الرسوميات الحاسوبية 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D
  11. يمكن أن تخدع صيغة بايثون البسيطة وسهلة التعلم مطوري لغة بايثون Python وخاصة الجدد منهم، مما يؤدي إلى تفويت بعض التفاصيل الدقيقة والتقليل من قوة اللغة، لذا سنقدّم في هذا المقال قائمة بأكثر 10 أخطاء شيوعًا، والتي تكون دقيقة ويصعب اكتشافها ويمكن أن تخدع حتى مطور بايثون الأكثر تقدمًا. مقدمة إلى بايثون تُعَد بايثون لغة برمجة مُفسَّرة Interpreted وكائنية التوجّه Object-oriented وعالية المستوى ولها دلالات Semantics ديناميكية، وتجعل هياكلُ البيانات المُضمَّنة عالية المستوى، والتحقق الديناميكي من الأنواع، والربط الديناميكي من لغة بايثون جذابة للغاية لتطوير التطبيقات بسرعة، بالإضافة إلى استخدامها بوصفها لغة برمجة لكتابة السكربتات أو لغة لاصقة Glue Language لوصل المكونات أو الخدمات الموجودة مسبقًا مع بعضها البعض. كما تدعم لغة بايثون الوحدات والحزم، وبالتالي تشجع التقسيم إلى وحدات Modularity وإعادة استخدام الشيفرة البرمجية. ملاحظة: هذا المقال مخصَّص للمبرمجين المحترفين في بايثون، وليس موجَّهًا للمطورين الجدد الذين قد يكونون أقل دراية بأخطاء بايثون الشائعة. الخطأ 1: استخدام التعابير بوصفها قيمًا افتراضية لوسطاء الدوال بطريقة خاطئة تسمح لغة بايثون بتحديد وسيط الدالة بأنه اختياري من خلال توفير قيمة افتراضية له، ولكن قد تؤدي هذه الميزة إلى بعض الارتباك عندما تكون القيمة الافتراضية متغيرة بالرغم من أن هذه ميزة رائعة لهذه اللغة. إليك تعريف دالة بايثون التالي مثلًا: >>> def foo(bar=[]): # يُعد الوسيط‫ bar اختياريًا وقيمته الافتراضية هي [] عند عدم تحديدها ... bar.append("baz") # ولكن يمكن أن يسبّب هذا السطر مشكلة كما سنرى لاحقًا‫... ... return bar من الأخطاء الشائعة أن نعتقد أن الوسيط الاختياري مضبوط على التعبير الافتراضي المحدَّد في كل مرة تُستدعَى فيها الدالة دون توفير قيمة لهذا الوسيط الاختياري، فمثلًا قد نتوقع في الشيفرة البرمجية السابقة أن استدعاء الدالة foo()‎ بصورة متكررة (أي بدون تحديد الوسيط bar) سيؤدي دائمًا إلى إعادة القيمة 'baz'، بما أننا اعتقدنا أن الوسيط bar مضبوط على القيمة [] (أي قائمة فارغة جديدة) في كل مرة نستدعي فيها الدالة foo()‎ (بدون تحديد الوسيط bar)، ولكن لنلقِ نظرة على ما يحدث فعليًا: >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] لاحظ استمرار إلحاق القيمة الافتراضية "baz" إلى القائمة الموجودة مسبقًا في كل مرة نستدعي فيها الدالة foo()‎ بدلًا من إنشاء قائمة جديدة في كل مرة، إذ تُقيَّم القيمة الافتراضية لوسيط الدالة مرة واحدة فقط في وقت تعريف الدالة، وبالتالي يُهيَّأ الوسيط bar على قيمته الافتراضية (أي قائمة فارغة) عند تعريف الدالة foo()‎ لأول مرة فقط، ولكن ستستمر بعد ذلك استدعاءات الدالة foo()‎ (بدون تحديد الوسيط bar) في استخدام القائمة نفسها التي هيّأنا بها الوسيط bar في الأصل. الحل الشائع لهذه المشكلة هو ما يلي: >>> def foo(bar=None): ... if bar is None: # ‫أو if not bar:‎‫ ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"] الخطأ 2: استخدام متغيرات الصنف Class استخدامًا خاطئًا ليكن لدينا المثال التالي: >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1 وبالتالي سيكون لدينا أيضًا ما يلي كما هو متوقع: >>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1 ولكن سيكون لدينا ما يلي: >>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3 لاحظ تغيير قيمة C.x بالرغم من أننا غيرنا قيمة A.x فقط، حيث تُعامَل متغيرات الصنف داخليًا على أنها قواميس في لغة بايثون وتتبع ما يشار إليه غالبًا باسم ترتيب تحليل التوابع أو ترتيب استبيان التوابع Method Resolution Order -أو MRO اختصارًا وهو الآلية التي تستخدمها لغات البرمجة ومن ضمنها بايثون لتحديد ترتيب البحث عن التوابع في التسلسل الهرمي hierarchy الخاص بالكائنات في حالة استخدام الوراثة المتعددة، أي أنه يحدد المسار الذي سيتبعه البرنامج عند محاولة استدعاء دالة معينة موجودة في أكثر من صنف أو الوراثة من عدة أصناف. لذلك سنبحث عن السمة Attribute التي هي x في أصنافها الأساسية (أي الصنف A فقط في المثال السابق بالرغم من أن لغة بايثون تدعم الوراثة المتعددة) بما أننا لم نعثر على هذه السمة في الصنف C. يمكن القول أيضًا أن الصنف C ليس لديه الخاصية x الخاصة به والمستقلة عن الصنف A، وبالتالي لا يُعَد المرجع إلى C.x هو المرجع نفسه إلى A.x، ويؤدي ذلك إلى حدوث مشكلة في بايثون إن لم نتعامل معها بطريقة صحيحة. الخطأ 3: تحديد المعاملات لكتلة الاستثناء Exception بطريقة خاطئة لنفترض أن لدينا الشيفرة البرمجية التالية: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # لالتقاط الاستثناءَين ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range المشكلة في المثال السابق هي أن تعليمة except لا تأخذ قائمة الاستثناءات المُحدَّدة بهذه الطريقة، حيث تستخدم بايثون الصيغة except Exception, e لربط الاستثناء بالمعامل الثاني الاختياري المُحدَّد (هو e في هذه الحالة)، وبالتالي يمكن إتاحته لمزيد من الفحص. لم تلتقط التعليمة except الاستثناء IndexError، بل يُربَط الاستثناء بمعاملٍ اسمه IndexError، وتُعَد مثل هذه الأخطاء شائعة في شيفرة بايثون البرمجية. الطريقة الصحيحة لالتقاط الاستثناءات المتعددة في التعليمة except هي تحديد المعامل الأول بوصفه مجموعة Tuple تحتوي على جميع الاستثناءات المُلتقَطة. يمكن تحقيق أقصى قدر من قابلية النقل من خلال استخدام الكلمة المفتاحية as لأن هذه الصيغة تدعمها Python 2 و Python 3: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> الخطأ 4: سوء فهم قواعد نطاق Scope بايثون يعتمد تحليل Resolution نطاق بايثون على قاعدة LEGB، وهي اختصار للكلمات محلي ‎Local وشامل ‎Enclosing وعام ‎Global ومُضمَّن ‎Built-in. توجد بعض التفاصيل الدقيقة للطريقة التي تعمل بها هذه القاعدة في بايثون، مما يقودنا إلى مشكلة برمجة بايثون الشائعة الأكثر تقدمًا التالية، فليكن لدينا ما يلي مثلًا: >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment يحدث الخطأ السابق لأنه إذا أسندتَ قيمة إلى متغير في نطاقٍ ما، فستَعُد لغة بايثون هذا المتغير متغيرًا محليًا لذلك النطاق تلقائيًا وتظلّل أيّ متغير يحمل الاسم نفسه في أيّ نطاق خارجي. يتفاجأ الكثير من المبرمجين بالحصول على الخطأ UnboundLocalError في الشيفرة البرمجية التي عملت بنجاح سابقًا عند تعديلها من خلال إضافة تعليمة إسناد في مكانٍ ما من جسم الدالة، فمن الشائع أن يؤدي ذلك إلى أن يخطئ المطورون عند استخدام القوائم خاصةً كما في المثال التالي: >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # تعمل هذه التعليمة بنجاح ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ولكن تعطي هذه التعليمة خطأً ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment لاحظ أن الدالة foo1 تعمل بنجاح بينما تعطي الدالة foo2 خطأ، والسبب في ذلك هو مماثل لمشكلة المثال السابق ولكنه أكثر دقة، حيث لا تسند الدالة foo1 قيمة إلى المتغير lst على عكس الدالة foo2، فالتعليمة lst += [5]‎ هي مجرد اختصار للتعليمة lst = lst + [5]‎ التي تمثّل محاولة إسناد قيمة إلى المتغير lst، وبالتالي تفترض لغة بايثون أن هذا المتغير موجود في النطاق المحلي، ولكن تعتمد القيمة التي نريد إسنادها إلى المتغير lst على المتغير lst نفسه الذي يُفترَض وجوده في النطاق المحلي ولم نعرّفه بعد. الخطأ 5: تعديل القائمة أثناء المرور عليها يجب أن تكون مشكلة الشيفرة البرمجية التالية واضحة إلى حدٍ ما: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # تصرف سيء: حذف عنصر من القائمة أثناء المرور عليها ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range يُعَد حذف عنصر من قائمة أو مصفوفة أثناء المرور عليها مشكلةً معروفةً جيدًا في بايثون لمطوري البرمجيات أصحاب الخبرة، ولكن بالرغم من أن المثال السابق قد يكون واضحًا إلى حدٍ ما، إلّا أنه يمكن أن يرتكب حتى المطورون المتقدمون عن غير قصد هذا الخطأ في الشيفرة البرمجية الأكثر تعقيدًا. لحسن الحظ، تتضمن لغة بايثون عددًا من نماذج البرمجة الأنيقة التي يمكن أن تؤدي إلى شيفرة برمجية مبسطة ومنظَّمة بصورة كبيرة عند استخدامها استخدامًا صحيحًا، ممّا يقلل من احتمالية وجود خطأ الحذف غير المقصود لعنصر القائمة أثناء المرور عليها في هذه الشيفرة البرمجية الأبسط. أحد هذه النماذج هو نموذج استيعاب القوائم List Comprehensions الذي يُعَد مفيدًا خاصةً لتجنب هذه المشكلة كما هو موضّح في التطبيق البديل التالي للشيفرة البرمجية السابقة والذي يعمل بطريقة مثالية: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # الحل هنا >>> numbers [0, 2, 4, 6, 8] الخطأ 6: عدم وضوح كيفية ربط Bind بايثون للمتغيرات في المنغلقات Closures ليكن لدينا المثال التالي: >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ... قد نتوقع الخرج التالي للشيفرة البرمجية السابقة: 0 2 4 6 8 لكننا سنحصل على ما يلي: 8 8 8 8 8 يحدث ذلك بسبب سلوك الربط المتأخر Late Binding لبايثون الذي يبحث عن قيم المتغيرات المُستخدَمة في المنغلقات Closures في وقت استدعاء الدالة الداخلية، لذلك إذا استدعينا أيًا من الدوال المُعادة في المثال السابق، فسيُجرَى البحث عن قيمة المتغير i في النطاق المحيط في وقت استدعائها، حيث ستكون الحلقة قد اكتملت عندها، لذلك أُسنِدت القيمة 4 إلى المتغير i فعليًا. ويكون حل هذه المشكلة الشائعة في بايثون كما يلي: >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8 استفدنا من الوسطاء الافتراضية لإنشاء دوال مجهولة لتحقيق السلوك المطلوب. قد يَعُد البعض هذه الطريقة مناسبة، وقد يَعدها البعض رائعة، وقد يكرهها البعض الآخر، ولكن من المهم أن تفهمها إذا كنت مطور بايثون. الخطأ 7: إنشاء اعتماديات Dependencies الوحدات الدائرية لنفترض أن لدينا الملفان a.py و b.py، حيث يستورد كلّ منهما الآخر كما يلي: في الملف a.py: import b def f(): return b.x print f() وفي الملف b.py: import a x = 1 def g(): print a.f() أولًا، لنحاول استيراد الوحدة a.py كما يلي: >>> import a 1 لاحظ أن عملية الاستيراد نجحت، وقد يكون ذلك مفاجأة لك، فلدينا استيراد دائري هنا والذي يُفترَض أن يمثل مشكلة، أليس كذلك؟ ولكن لا يمثّل مجرد وجود استيراد دائري في حد ذاته مشكلة في بايثون، فلغة بايثون ذكية بما يكفي لعدم محاولة إعادة استيراد وحدة إذا كانت مستوردةً فعليًا، ولكنك قد تواجه مشكلات اعتمادًا على النقطة التي تحاول فيها كل وحدة الوصول إلى الدوال أو المتغيرات المُعرَّفة في الوحدة الأخرى. لم يكن هناك مشكلة في استيراد الوحدة b.py لأنها لا تتطلب تعريف أيّ شيء من الوحدة a.py في وقت استيرادها عندما استوردنا الوحدة a.py في المثال السابق، فالإشارة الوحيدة إلى الوحدة a في الملف b.py هو استدعاء الدالة a.f()‎، ولكن هذا الاستدعاء موجود في الدالة g()‎ ولا يوجد شيء في الملفين a.py أو b.py يستدعي الدالة g()‎، لذا لا يوجد شيء يدعو للقلق. ولكن إذا حاولنا استيراد الوحدة b.py دون استيراد الوحدة a.py مسبقًا كما يلي: >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x' فستظهر مشكلة تتمثّل في أن الوحدة b.py تحاول استيراد الوحدة a.py عند عملية استيراد الوحدة b.py، وتستدعي الوحدة a.py بدورها الدالة f()‎ التي تحاول الوصول إلى المتغير b.x الذي لم نعرّفه بعد، وبالتالي سيظهر الاستثناء AttributeError. توجد حلول مختلفة لهذا الخطأ، وسيكون أحد هذه الحلول على الأقل بسيطًا، فمثلًا عدّل الوحدة b.py لتستورد الوحدة a.py ضمن الدالة g()‎: x = 1 def g(): import a # ستُقيَّم هذه التعليمة عند استدعاء الدالة‫ g()‎ فقط print a.f() وإذا استوردناه هذه الوحدة، فسيكون كل شيء على ما يرام كما يلي: >>> import b >>> b.g() 1 # يُطبَع لأول مرة بسبب استدعاء الوحدة‫ 'a' للتعليمة 'print f()‎' في النهاية 1 # يُطبَع مرة ثانية، حيث يمثّل استدعاء الدالة‫ 'g' الخطأ 8: تعارض الأسماء مع وحدات مكتبة بايثون المعيارية تتميز لغة بايثون بوفرة وحدات المكتبات التي تأتي معها، ولكن قد يؤدي ذلك إلى الوقوع في تعارض في الأسماء بين اسم إحدى الوحدات الخاصة بك ووحدة أخرى تحمل الاسم نفسه في المكتبة المعيارية التي تأتي مع لغة بايثون إن لم تكن حذرًا، فمثلًا قد يكون لديك وحدة بالاسم email.py في شيفرتك البرمجية، والتي قد تتعارض مع وحدة المكتبة المعيارية التي تحمل الاسم نفسه. يمكن أن يؤدي ذلك إلى مشكلات خطيرة مثل استيراد مكتبة أخرى، والتي تحاول بدورها استيراد إصدارٍ من وحدة خاصة بمكتبة بايثون المعيارية، ولكن إذا كان لديك وحدة تحمل الاسم نفسه، فستستورد الحزمة الأخرى الإصدار الخاص بك عن طريق الخطأ بدلًا من الإصدار الموجود في مكتبة بايثون المعيارية، مما يؤدي إلى حدوث أخطاء، لذا يجب توخي الحذر لتجنب استخدام الأسماء نفسها الخاصة بوحدات مكتبة بايثون المعيارية. من الأسهل بالنسبة لك تغيير اسم الوحدة ضمن الحزمة الخاصة بك بدلًا من تقديم اقتراح تحسين بايثون Python Enhancement Proposal -أو PEP اختصارًا- لطلب تغيير الاسم ومحاولة الحصول على الموافقة على ذلك. الخطأ 9: الفشل في معالجة الاختلافات بين الإصدارين Python 2 و Python 3 ليكن لدينا الملف foo.py التالي مثلًا: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad() يعمل ما يلي بنجاح في Python 2: $ python foo.py 1 key error 1 $ python foo.py 2 value error 2 ولكنه يعطي خطأً في Python 3 كما يلي: $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment المشكلة هي أنه لا يمكن الوصول إلى كائن الاستثناء خارج نطاق كتلة التعليمة except في Python 3، وإلّا فيجب الاحتفاظ بدورة مرجعية مع إطار المكدس في الذاكرة حتى تشغيل كانس المهملات Garbage Collector وإزالة المراجع من الذاكرة. إحدى الطرق لتجنب هذه المشكلة هي الاحتفاظ بمرجع إلى كائن الاستثناء خارج نطاق كتلة التعليمة except بحيث يبقى قابلًا للوصول. إليك فيما يلي نسخة من المثال السابق الذي يستخدم هذه التقنية، وبالتالي ستنتج شيفرة برمجية متوافقة مع Python 2 و Python 3: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good() لنشغّل هذه الشيفرة البرمجية على الإصدار Py3k: $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2 اطّلع على مقال كيفية ترحيل شيفرة بايثون 2 إلى بايثون 3 لمزيد من المعلومات. الخطأ 10: استخدام التابع del بطريقة خاطئة لنفترض أن لدينا ما يلي في ملفٍ اسمه mod.py: import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) ثم حاولنا استيراده من الملف another_mod.py كما يلي: import mod mybar = mod.Bar() فسنحصل على الاستثناء AttributeError، والسبب هو ضبط جميع متغيرات الوحدة العامة على القيمة None عند إيقاف تشغيل المفسّر Interpreter، لذلك ضُبِط الاسم foo على القيمة None عند استدعاء التابع __del__ في المثال السابق. الحل لهذه المشكلة هو استخدام الدالة atexit.register()‎ بدلًا من ذلك، وبالتالي ستُشغَّل معالجاتك المسجَّلة قبل إيقاف تشغيل المفسِّر عندما ينتهي برنامجك من التنفيذ (أي عند الخروج منه بطريقة طبيعية). إذًا لنصلِح شيفرة mod.py البرمجية السابقة كما يلي: import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle) يوفّر هذا المثال طريقة نظيفة وموثوقة لاستدعاء أيّ دالة تنظيف مطلوبة عند إنهاء البرنامج العادي، ومن الواضح أن الأمر متروك للدالة foo.cleanup لتحديد ما يجب فعله بالكائن المرتبط بالاسم self.myhandle. مخاطر بايثون يمكن تجنبها من خلال معرفة الفروق الأساسية تُعَد بايثون لغة قوية ومرنة وتحتوي على العديد من الآليات والنماذج التي يمكن أن تحسّن الإنتاجية بصورة كبيرة، ولكن يمكن أن يكون الفهم أو التقدير المحدود لقدراتها في بعض الأحيان عائقًا أكثر من كونه فائدة كما هو الحال مع أيّ أداة أو لغة برمجية، حيث يعتقد الشخص في أن يعلم ما يكفي، ولكنه يشكّل خطرًا. سيساعد التعرف على الفروق الأساسية في لغة بايثون -مثل مشاكل البرمجة المتقدمة التي ذكرناها في هذا المقال- على تحسين استخدام اللغة مع تجنب بعض الأخطاء في بايثون. ترجمة -وبتصرُّف- للمقال The 10 Most Common Python Code Mistakes لصاحبه Martin Chikilian. اقرأ أيضًا مصطلحات شائعة مثيرة للالتباس في بايثون تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون كيف تستخدم منقح بايثون فهم العمليات المنطقية في بايثون 3 أشهر 10 مشكلات تواجه مبرمجي لغة جافا سكريبت JavaScript
  12. يُعَد لارافيل Laravel إطار ويب يستخدم لغة PHP، وهو مجاني ومفتوح المصدر ويُستخدَم على نطاق واسع لتطوير تطبيقات الويب، كما أنه معروف بصيغته الأنيقة وأدواته المُستخدَمة للمهام الشائعة مثل التوجيه Routing والاستيثاق Authentication والتخزين المؤقت أو التخبئة Caching وقدرته على التعامل مع حركة الزوار العالية. يتبع لارافيل معمارية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، ويتضمن دعمًا مُدمجًا لربط الكائنات بالعلاقات Object-Relational Mapping (أو ORM اختصارًا) وباني الاستعلامات، مما يسهّل التفاعل مع قواعد البيانات. يضم لارافيل مجتمعًا كبيرًا ونشطًا يوفّر مجموعةً كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ يُعَد لارافيل أحد أطر عمل الويب الأكثر شعبية وقوة للغة PHP، وتثق به العديد من الشركات والمؤسسات لبناء تطبيقات ويب قوية وقابلة للتوسّع. سنشرح في هذا المقال الاستخدام الأساسي لإطار عمل لارافيل من خلال بناء نظام تدوين بسيط. ملاحظة: تعتمد هذه السلسلة من المقالات على الإصدار 11 من لارافيل. إنشاء مشروع لارافيل جديد لا يُعَد إعداد بيئة تطوير PHP مهمة سهلة، وخاصة إذا استخدمت أنظمة لينكس Linux، ولكن قدّم لارافيل منذ الإصدار 8.0 أداة جديدة اسمها Sail، والتي توفر حلًا سهلًا لتشغيل تطبيقات لارافيل من خلال استخدام أداة دوكر Docker بغض النظر عن نظام التشغيل الذي تستخدمه، طالما أن هذه الأداة مُثبَّتة لديك. أنشئ أولًا مجلد عمل جديد بالاسم laravel-tutorial مثلًا، ثم افتحه في محرّر الأكواد الذي تفضله وليكن VS Code، ونفّذ الأمر الآتي لإنشاء مشروع لارافيل جديد، وتأكّد من تشغيل دوكر قبل تنفيذ هذا الأمر، إذ قد تستغرق هذه العملية من 5 إلى 10 دقائق حتى الاكتمال: curl -s https://laravel.build/<app_name> | bash ملاحظة: إذا كنت تعمل على نظام ويندوز Windows، فتأكّد من تشغيل هذا الأمر ضمن نظام ويندوز الفرعي لنظام التشغيل لينكس WSL 2 الذي يسمح لك بتشغيل نواة Linux كاملة داخل ويندوز دون الحاجة إلى تثبيت نظام لينكس. ثانيًا، استخدم الأمر التالي للانتقال إلى مجلد التطبيق وشغّل الخادم: cd <app_name> سيؤدي الأمر التالي إلى بدء حاوية دوكر بالإضافة إلى خادم تطوير لارافيل: ./vendor/bin/sail up يمكنك الوصول إلى تطبيق لارافيل على العنوان http://localhost/‎، وإن لم يعمل هذا العنوان، فحاول الانتقال إلى العنوان http://127.0.0.1/‎ بدلًا من ذلك، ثم يجب أن تشاهد صفحة ترحيب لارافيل التالية: استكشاف بنية تطبيق لارافيل لنلقِ نظرة على المجلدات والملفات التي تنشأ ضمن مشروعنا قبل أن نبدأ ببرمجته، لذا إليك نظرة عامة على المجلد الجذر للمشروع: . ├── app │ ├── Console │ ├── Exceptions │ ├── Http │ │ ├── Controllers │ │ └── Middleware │ ├── Models │ └── Providers ├── bootstrap ├── config ├── database │ ├── factories │ ├── migrations │ └── seeders ├── public ├── resources │ ├── css │ ├── js │ └── views ├── routes ├── storage │ ├── app │ ├── framework │ └── logs ├── tests │ ├── Feature │ └── Unit └── vendor المجلد app: يُعَد هذا المجلد المكون الأساسي لمشروعنا، ويحتوي على مجلدات متعددة من أهمها مجلد المتحكمات controllers والبرمجيات الوسيطة middleware والنماذج models، حيث يعرّف المتحكم الشيفرة البرمجية الأساسية للتطبيق، وتعرّف البرمجيات الوسيطة الإجراءات التي يجب اتخاذها قبل استدعاء المتحكم، ويوفّر النموذج واجهة تسمح لنا بالتعامل مع قواعد البيانات، إذ سنتحدث عن كل منها بالتفصيل لاحقًا. المجلد bootstrap: يحتوي هذا المجلد على الملف app.php الذي يشغّل المشروع بأكمله، ولا حاجة لتعديل أيّ شيء في هذا المجلد. المجلد config: يحتوي على ملفات الضبط Configuration كما يدل اسمه، ولا حاجة للاهتمام بهذا الضبط في هذه السلسلة من المقالات. المجلد database: يحتوي على ملفات التهجير migrations والمصانع factories والبذور seeds، حيث تصف ملفات التهجير بنية قاعدة البيانات، وتُعَد ملفات المصانع والبذور طريقتين مختلفتين يمكننا من خلالهما ملء قاعدة البيانات ببيانات وهمية في لارافيل. المجلد public: يحتوي على الملف index.php الذي يُعَد نقطة الدخول إلى لتطبيق. المجلد resources: يحتوي على ملفات العرض views التي تمثّل جزء الواجهة الأمامية من تطبيق لارافيل. المجلد routes: يحتوي على جميع الموجّهات Routers لعناوين URL للمشروع فعندما ترسل طلب إلى عنوان URL محدد، سيتوجه الطلب إلى المُوجِّه المناسب بناءً على الوجهات التي تحددها الملفات ضمن هذا المجلد. المجلد storage: هو مساحة تخزين المشروع بأكمله، ويحتوي على السجلات logs والعروض المُصرَّفة Compiled Views بالإضافة إلى الملفات التي رفعها المستخدم. المجلد tests: يحتوي على ملفات الاختبار، حيث يُعَد الاختبار مفهومًا أكثر تقدمًا نسبيًا في لارافيل، لذلك لن نوضّحه في هذه السلسلة من المقالات، ولكن يمكنك الاطلاع على مقال كيفية استخدام PHPUnit لاختبار تطبيقات لارافيل لمزيد من التفاصيل حول الاختبارات. المجلد vendor: يتضمن جميع الاعتماديات Dependencies. متغيرات البيئة سنلقي نظرة أيضًا على متغيرات البيئة التي تُخزَّن في ملف ‎.env، حيث أُعِدت الكثير من عمليات الضبط افتراضيًا عندما أنشأنا مشروعنا باستخدام أداة Sail في لارافيل، ولكننا سنتحدث عن عملها فيما يلي. المتغير APP_URL يحدّد المتغير APP_URL عنوان URL للتطبيق، ويكون هذا العنوان هو http://localhost افتراضيًا، ولكن قد تحتاج إلى تغييره إلى العنوان http://127.0.0.1 عندما يعطي العنوانُ http://127.0.0.1 صفحةَ خادم Apache2 الافتراضية. APP_URL=http://127.0.0.1 أو: APP_URL=http://localhost قاعدة البيانات ستثبّت أداة Sail من لارافيل نظامَ إدارة قواعد البيانات MySQL بوصفه التطبيق الخاص بقاعدة البيانات، وتُعرَّف اتصالات قاعدة البيانات كما يلي: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password يمكنك أيضًا تعريف متغيرات بيئة خاصة بك إن احتجت إليها كما يلي، ثم يمكنك الوصول إليها في أي مكان من المشروع: CUSTOM_VARIABLE=true ويمكن الوصول إلى هذا المتغير كما يلي: env('CUSTOM_VARIABLE', true) يمثّل المعامل الثاني القيمة الافتراضية لهذا المتغير، حيث إن لم يكن المتغير CUSTOM_VARIABLE موجودًا، فسيُعاد المعامل الثاني. أساسيات التوجيه Routing في لارافيل سنلقي نظرة في هذا القسم على وِجهات Route لارافيل وبرمجياتها الوسيطة، حيث تُعرَّف الوِجهات في المجلد routes. لاحظ وجود أربعة ملفات مختلفة في المجلد routes افتراضيًا، ولكن ما يهمنا الملفان api.php و web.php بالنسبة لمعظم المشاريع. إذا أردت استخدام لارافيل بطريقة صارمة مع الواجهة الخلفية (بدون العرض)، فيجب أن تحدّد الوِجهات في الملف api.php، ولكننا سنستخدم في هذا المقال لارافيل بوصفه إطار عمل متكامل full-stack، لذلك سنستخدم الملف web.php بدلًا من ذلك. الفرق بين هذين الملفين هو أن الملف api.php تغلِّفه مجموعة برمجيات api الوسيطة وأن الملف web.php موجود ضمن مجموعة برمجيات web الوسيطة، ويوفر هذان الملفان دوالًا مختلفة، وسيكون للوِجهات المحددة في الملف api.php بادئة عنوان URL التي هي /api/، وبالتالي يجب أن يكون عنوان URL مثل العنوان: http://example.com/api/somthing-else للوصول إلى وِجهة api. تقبل الوِجهة الأساسية في لارافيل عنوان URL ثم تعيد قيمة، حيث يمكن أن تكون هذه القيمة سلسلة نصية أو عرضًا أو متحكمًا. انتقل إلى الملف routes/web.php، وسترى أن هناك وِجهة مُعرَّفة مسبقًا هي: routes/web.php. يخبرنا الجزء التالي من الشيفرة البرمجية أنه إذا استقبل لارافيل الوِجهة "/"، فسيعيد عرضًا بالاسم "welcome"، والذي يقع في resources/views/welcome.blade.php: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); افتح المتصفح، وانتقل إلى العنوان http://127.0.0.1:8000، وستحصل على الصفحة التالية: يمكن التحقق من أن welcome.blade.php هو العرض الظاهر من خلال إجراء بعض التغييرات على الملف، ثم تحديث متصفحك للتأكد من تغيير الصفحة. توابع الموجهات لنلقِ الآن نظرة على الموجِّه التالي في routes/web.php ونفهم كيفية عمله: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); نستورد أولًا الصنف Route ونستدعي التابع get()‎، حيث يتطابق هذا التابع مع تابع HTTP الذي هو GET، وهناك توابع أخرى مُضمَّنة في الصنف Route، حيث تتطابق هذه التوابع مع توابع طلبات HTTP الأخرى كما يلي: Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); إذا أردتَ أن تطابق الوِجهة بين توابع HTTP متعددة، فيمكنك استخدام تابع match أو any بدلًا من ذلك، حيث يتطلب التابع match()‎ تحديد مصفوفة توابع HTTP التي ترغب في مطابقتها، ويطابق التابع any()‎ ببساطة بين جميع طلبات HTTP. Route::match(['get', 'post'], '/', function () { . . . }); Route::any('/', function () { . . . }); تمرير البيانات إلى العرض يحتوي التابع get()‎ على معاملَين هما: المعامل الأول هو عنوان URL الذي يُفترَض أن يطابقه الموجِّه، والمعامل الثاني هو دالة رد النداء Callback التي تُنفَّذ عند نجاح المطابقة. تُعاد الدالة view()‎ المُضمَّنة ضمن دالة رد النداء، حيث ستبحث هذه الدالة عن ملف العرض المقابل بناءً على المعامل المُمرَّر إليها. يقدم لارافيل اختصارًا بسيطًا هو Route::view()‎ إذا أردتَ إعادة عرض فقط، حيث يسمح لك هذا التابع بتجنب كتابة وِجهة كاملة كما يلي: Route::view('/welcome', 'welcome'); الوسيط الأول لهذا التابع هو عنوان URL، والمعامل الثاني هو العرض المقابل، ويوجد أيضًا وسيط ثالث يسمح بتمرير بعض البيانات إلى هذا العرض كما يلي: Route::view('/welcome', 'welcome', ['name' => 'Taylor']); سنتحدث عن كيفية الوصول إلى البيانات عندما نصل إلى قوالب Blade. من الموجه إلى المتحكم يمكن أن نجعل الموجّه يؤشّر إلى المتحكم الذي يؤشّر بدوره إلى العرض، فالمتحكم هو أساسًا نسخة موسعة من دالة رد النداء، حيث سنتحدث عن المتحكمات بالتفصيل في المقال التالي. يخبرنا سطر الشيفرة البرمجية التالي أنه إذا استقبل الموجّه الوِجهة "‎/user"، فسيذهب لارافيل إلى الصنف UserController، ويستدعي التابع index: Route::get('/user', [UserController::class, 'index']); معاملات الوجهة تحتاج في بعض الأحيان إلى استخدام أجزاء من عنوان URL بوصفها معاملات، فمثلًا لنفترض أن لدينا مدونة مطورة بالكامل، ويوجد مستخدم يبحث عن منشور في المدونة له الاسم Slug الموجود في الرابط this-is-a-post، ويحاول العثور على هذا المنشور من خلال كتابة http://www.example.com/posts/this-is-a-post في متصفحه. يمكن التأكد من أن المستخدم قد عثر على المنشور الصحيح من خلال أخذ الجزء الموجود بعد posts/‎ كمعامل وإرساله إلى الواجهة الخلفية، ثم يمكن للمتحكم استخدام هذا المعامل للعثور على المنشور الصحيح وإعادته إلى المستخدم، حيث يمكنك تطبيق ذلك من خلال كتابة الشيفرة البرمجية التالية: Route::get('post/{slug}', [PostController::class, 'show']); سيتأكد السطر السابق من أن موجّه لارافيل يطابق الكلمة الموجودة بعد post/‎ كمعامل، ويسندها إلى المتغير slug عندما يرسلها لارافيل إلى الواجهة الخلفية. تُضمَّن معاملات الصنف Route دائمًا بين قوسين {} ويجب أن تتكون من أحرف أبجدية، ولا يجوز أن تحتوي على المحرف -. بما أننا لم نذكر المتحكمات في مثالنا، فيمكننا وضع دالة رد نداء بسيطة مكان المعامل الثاني واختبار الشيفرة البرمجية التالية: Route::get('/post/{slug}', function ($slug) { return $slug; }); افتح الآن متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/posts/this-is-a-slug، وستظهر الصفحة التالية: يمكن أيضًا مطابقة معاملات متعددة في عنوان URL كما يلي: Route::get('category/{category}/post/{slug}', [PostController::class, 'show']); سيُسنَد الجزء الموجود بعد category/‎ إلى المتغير category في هذه الحالة، وسيُسنَد الجزء الموجود بعد post/‎ إلى المتغير slug. لا تعرف في بعض الأحيان ما إذا كان المعامل موجودًا في عنوان URL، وبالتالي يمكنك جعل هذا المعامل اختياريًا من خلال إلحاقه بإشارة استفهام (?) كما يلي: Route::get('post/{slug?}', [PostController::class, 'show']); وأخيرًا، يمكنك التحقق من صحة المعاملات باستخدام التعابير النمطية Regular Expressions، فمثلًا يمكنك التأكد من أن معرّف المستخدم هو عدد دائمًا كما يلي: Route::get('user/{id}', [UserController::class, 'show'])->where('id', '[0-9]+'); الموجهات المسماة تسمح الوِجهات المُسماة بإنشاء عناوين URL أو إعادة التوجيه بصورة مناسبة إلى وِجهات محددة، حيث يمكنك تحديد اسمٍ لوِجهة من خلال إضافة التابع name إلى سلسلة تعريف الوِجهة كما يلي: Route::get('user/profile', [UserController::class, 'show'])->name('profile'); وبالتالي إذا احتجت إلى الوصول إلى عنوان URL هذا، فيجب أن تستدعي الدالة route('profile')‎ فقط. تجميع الموجهات إذا أردت إنشاء موقع ويب كبير، فيجب أن يكون لديك عشرات أو حتى مئات من الموجّهات، وبالتالي من المنطقي أن تجمّعها مع بعضها بعضًا، فمثلًا يمكنك تجميعها بناءً على البرمجيات الوسيطة middleware كما يلي: Route::middleware(['auth'])->group(function () { Route::get('/user/profile', [UserController::class, 'show']); Route::get('/user/setting', [UserController::class, 'setting']); }); وستُسنَد الآن البرمجية الوسيطة auth إلى الموجّهَين. يمكنك أيضًا إسناد بادئات Prefixes إلى مجموعة من الموجّهات كما يلي: Route::prefix('admin')->group(function () { Route::get('/users', [UserController::class, 'show']); . . . }); سيكون لجميع الموجّهات المحدّدة في هذه المجموعة البادئة /admin/. البرمجيات الوسيطة Middleware البرمجية الوسيطة هي شيء يمكنه فحص وترشيح طلبات HTTP الواردة قبل أن تصل إلى تطبيقك، ويحدث بعد أن تطابق الوِجهة عنوان URL دون تنفيذ دالة رد النداء، وبالتالي هي شيء موجود في منطقة وسطى، ومن هنا جاءت تسميتها بالبرمجيات الوسيطة. يُعَد استيثاق المستخدم User Authentication من أمثلة البرمجيات الوسيطة، حيث إذا كان المستخدم مُستوثَقًا، فستأخذ الوِجهة هذا المستخدم إلى الهدف المفترض، وإن لم يكن الأمر كذلك، فستأخذ المستخدم إلى صفحة تسجيل الدخول أولًا. لن نكتب أيّ برمجيات وسيطة في هذه السلسلة من المقالات، ولكن البرمجية الوسيطة الوحيدة التي نحتاج إلى استخدامها هي البرمجية الوسيطة auth المُضمَّنة لأغراض الاستيثاق، والتي سنشرح أساسياتها. ننشئ برمجية وسيطة من خلال تشغيل الأمر التالي: php artisan make:middleware EnsureTokenIsValid مما سيؤدي إلى إنشاء صنف EnsureTokenIsValid جديد ضمن المجلد app/Http/Middleware. <?php namespace App\Http\Middleware; use Closure; class EnsureTokenIsValid { /** * معالجة الطلب الوارد * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($request->input('token') !== 'my-secret-token') { return redirect('home'); } return $next($request); } } ستحصل هذه البرمجيات الوسيطة على قيمة المفتاح Token من الطلب، وتقارنها مع المفتاح السري المُخزَّن على موقعنا، حيث إذا كان مطابقًا، فانتقل إلى الخطوة التالية، وإذا لم يكن كذلك، فأعِد التوجيه إلى الصفحة الرئيسية. يمكنك استخدام هذه البرمجية الوسيطة من خلال تسجيلها في لارافيل، لذا انتقل إلى الملف app/Http/Kernel.php، وابحث عن الخاصية ‎$routeMiddleware، وأدرِج البرمجية الوسيطة التي أنشأناها. /** * البرمجيات الوسيطة لوِجهات التطبيق * * يمكن إسناد هذه البرمجيات الوسيطة إلى مجموعات أو استخدامها بصورة فردية * * @var array<string, class-string|string> **/ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; وأخيرًا، تعرّفنا سابقًا على كيفية استخدام البرمجيات الوسيطة مع الموجّه كما يلي: Route::get('/profile', function () {. . .})->middleware('auth'); ترجمة -وبتصرُّف- للمقال Laravel for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا تعرف على إطار عمل تطوير الويب لارافيل Laravel مقارنة بين Django و Laravel و Rails تثبيت لارافيل وإعداده على Windows وUbuntu إنشاء مدونة باستخدام Laravel 5
  13. ناقشنا في مقالٍ سابق أن الرسوميات الحاسوبية ثلاثية الأبعاد تتكون من عمليتين: أولهما إنشاء عالم خيالي داخل الحاسوب، وثانيهما إنتاج صور ثنائية الأبعاد لذلك العالم من مجالات رؤية مختلفة؛ إذ يُعَد إنتاج صورة ثنائية الأبعاد من صورة ثلاثية الأبعاد مثالًا عن عملية الإسقاط Projection، وسنناقش في هذا المقال المفاهيم الأساسية فقط للإسقاط. سنوضّح في هذا المقال المواضيع التالية: تشكيل مستوٍ Plane واحد من شعاعين. مفهوم الإسقاط. إسقاط شعاعٍ على شعاع آخر. صيغة لإسقاط الأشعة. طريقة إسقاط الأشعة خطوةً بخطوة. المكون العمودي لإسقاط الأشعة. مثال لإسقاط الأشعة ثنائية الأبعاد. مثال لإسقاط الأشعة ثلاثية الأبعاد. المساقط ثنائية الأبعاد المتعددة يمكن إنتاج عدد لا نهائي من الصور ثنائية الأبعاد من نموذج واحد ثلاثي الأبعاد، فمثلًا، يعرض الشكل التالي إسقاط نموذج ثلاثي الأبعاد لصندوق على عرضٍ ثنائي الأبعاد؛ إذ تنتِج المواقع المختلفة لمجال الرؤية صورًا مختلفة ثنائية الأبعاد، وتُسقَط الأشعة ثلاثية الأبعاد للنموذج على سطح العرض لتكوين أشعة الصورة ثنائية الأبعاد. تشكيل مستوٍ واحد من شعاعين يمكن تشكيل مستوٍ واحد باستخدام شعاعين ونقطة دائمًا، إلا في حال كان هذان الشعاعان متوازيان. لا بد أنك تتذكر من مادة الهندسة في المرحلة الثانوية أن الخطين غير المتوازيين (ليسا على استقامة واحدة) يحددان مستوٍ واحد، وبالتالي ينطبق الشيء نفسه على الأشعة. تبدأ معظم الأمثلة في هذا المقال بشعاعين ثنائي الأبعاد، ولكن ستكون النتائج صالحة لشعاعين ثلاثي الأبعاد؛ إذ يحدّد شعاعان ثلاثي الأبعاد مستوٍ واحد أيضًا. يوضّح الشكل السابق الشعاع w والشعاع v، وجعلنا ذيل هذين الشعاعين يبدأ من النقطة نفسها لتسهيل التصور، ولكن لا تنسَ أن الأشعة ليس لها موضع محدّد، وجعلنا الشعاع v أفقيًا للسهولة أيضًا، ولكن يمكن أن يكون له أيّ منحى Orientation. إسقاط شعاع على شعاع آخر إذا ضربنا الشعاع v بعدد حقيقي Scalar (أو مقدار سلمي) k، فسيؤدي ذلك إلى تغيير طول هذا الشعاع، ولكنه لن يؤثر على منحاه، وبالتالي ليكن لدينا الشعاعان w و v كما في الشكل التالي: يمكن أن يكون الشعاعان w و v أيّ شعاعين، ونريد شعاعًا u عموديًا على الشعاع v وعددًا حقيقيًا k مضروبًا به، بحيث: w =‎ k v + u‎ ويسمى عندها k v‎ مسقط الشعاع w على الشعاع v. صيغة لكيفية إسقاط الأشعة ملاحظة: إذا كان الشعاعان u و v متعامدان، فإن الجداء النقطي لهما هو: ‎.u · v = 0 يجب أن تتحقق العلاقة: k v + u = w‎ بشرط أن يكون u عموديًا مع v، حتى يكون k v‎ مسقط الشعاع w على الشعاع v؛ إذ نعلم من علم المثلثات أن: طول المسقط k v‎ =‫ ‎| w | cos θ. منحى k v‎ هو شعاع الوحدة ‎vu‎. تذكر أن أشعة الوحدة Unit Vectors تُستخدَم لتمثيل المنحى في الفضاء ثلاثي الأبعاد، وبالتالي يكون منحى الشعاع v هو شعاع الوحدة ‎vu‎؛ إذ يبدو المنحى أفقيًا في الشكل السابق، ولكننا استخدمنا ذلك للسهولة فقط. وبما أن cos θ يساوي ‎wu · vu‎، فسينتج ما يلي: ‎k v = | w | (wu · vu) vu يمثل الجزء ‎| w | (wu · vu)‎ عددًا حقيقيًا بحيث يُضبَط الشعاع v على الطول المطلوب، وهو طول الخط الأفقي باللون الأزرق الفاتح في الشكل السابق؛ بينما يعبّر الجزء المتبقي من الصيغة: ‎vu‎ عن منحى الشعاع v نفسه. طريقة إسقاط الأشعة خطوة بخطوة تذكير: يمكنك حساب شعاع الوحدة من خلال تقسيم الشعاع على طوله. لنتّبع طريقة خطوة بخطوة لإسقاط الشعاع w على v، بدلًا من حفظ الصيغة السابقة لتطبيق عملية الإسقاط. أولًا، نحسب أطوال الأشعة كما يلي: ‎| w | = √(w · w) ‎| v | = √(v · v) ثانيًا، نحسب أشعة الوحدة: wu = w / | w‎ |‎ vu = | v | / | v |‎ ثالثًا، نحسب جيب تمام Cosine الزاوية بين الشعاعين: wu · vu رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu المكون العمودي لإسقاط الأشعة ملاحظة: لا يُعَد مسقط الشعاع w على الشعاع v مماثلًا لمسقط الشعاع v على الشعاع w، إذ يكون منحى الشعاع الناتج عن عملية الإسقاط بالاتجاه نفسه للشعاع المُسقَط عليه. أوجدنا في الفقرة السابقة k v‎، لذا من السهل العثور على الشعاع المتعامد معه u: ‎w = k v + u‎، إذًا ‎u‎ = w - k v‎ تعبّر إشارة "+" في الصيغة السابقة عن جمع الأشعة، وتعبّر إشارة "-" عن طرح الأشعة. تدريبات عملية كانت الأشعة في هذا المقال جميعها أشعةً هندسيةً حتى الآن، إذ لم يكن هناك أيّ ذكرٍ للإطارات الإحداثية أو المصفوفات العمودية، ولكن ستنجح هذه النتائج عند تمثيل الأشعة بالمصفوفات العمودية كما سنرى في الأمثلة التالية. تدريب 1: نمثّل الشعاع w بالمصفوفة العمودية ‎(6, 5)T‎، ونمثّل الشعاع v بالمصفوفة العمودية ‎(9,0)T‎، ولنوجد مسقط الشعاع w على الشعاع v، أي لنبحث عن الشعاعين k v‎ و u. يمكنك ببساطة قراءة الإجابة من الرسم البياني الورقي التالي، ولكن تظاهر أنك لم تلاحظ ذلك. أولًا، نحسب الأطوال كما يلي: ‎| w | = √ ((6, 5)T‎·(6, 5)T‎) = 7.81‎ ‎| ‎v | = √ ((9, 0)T‎·(9, 0)T‎) = 9 ثانيًا، نحسب أشعة الوحدة كما يلي: ‎wu = (6, 5)T / 7.81 ‎vu = (9, 0)T / 9 = (1, 0)T ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين وهو: ‎wu · vu = (1/7.81) (6, 5)(1, 0)T · (1, 0)(1, 0)T = 6/7.81 رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu ‎‎k v = 7.81 (6/7.81) (1, 0)T = 6(1, 0)T = (6, 0)T خامسًا، نحسب الشعاع العمودي: u = w - k v ‎u = (6, 5)T - (6, 0)T = (0, 5)T والنتيجة هي أن ‎u = (6,0)T + (0,5)T‎ كما هو متوقع. وكان هذا المثال سهلًا طبعًا. ملاحظة: ليس عليك أن تحسب طول w، لأنه يُلغَى قبل الإجابة النهائية، لذا فكل ما تحتاج إليه هو مربع طول v، أي ‎|v|2‎؛ إذ تعرض بعض الكتب صيغًا مختلفة للإسقاط تستفيد من هذه الحقائق. تدريب 2 إليك مثال آخر، ولكنه ليس بهذه السهولة، إذ نمثّل الشعاع w هنا بالمصفوفة العمودية ‎(3.2, 7)T‎، ونمثّل الشعاع v بالمصفوفة العمودية ‎(8, 4)T‎؛ ولنوجِد الآن الشعاعين k v‎ و u. أولًا، نحسب الأطوال كما يلي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = (8, 4)T ·(8, 4)T = 80 ثانيًا، نحسب أشعة الوحدة وهي: wu = (3.2, 7)T / | w |‎ vu = (8, 4)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: ‎wu · vu = (3.2, 7)T / | w | · (8, 4)T / | v | = 53.6 /( | w | | v |)‎ رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu k v = | w | [53.6 / (| w | | v |)] (8, 4)T / | v |‎ k v = 53.6 / (| v |) (8, 4)T / | v |‎ ‎k v = 53.6 / (| v |2) (8, 4)T ‎k v = 53.6 / 80 (8, 4)T k v = ((53.6*8)/80, (53.6*4)/80)T = ( 5.3, 2.68)T خامسًا، نحسب الشعاع العمودي: u = w - k v u = (3.2, 7)T - (5.3, 2.68)T = (-2.1, 4.32)T لنتحقّق الآن من أن: ‎w = k v + u‎ من خلال تعويض النتائج في العلاقة k v + u‎ كما يلي: ‎( 5.3, 2.68)‎T + (-2.1, 4.32)T = (3.2, 7.0)T = w فوائد عملية إسقاط الأشعة يجب أن تفهم جيدًا ما يحدث عند إسقاط شعاع على شعاع آخر، إذ تحدث عملية الإسقاط -كما ذكرنا سابقًا- باستمرار عندما يعرض الحاسوب الرسوميات. تُنفَّذ معظم العمليات الحسابية في عتاد الرسوميات الخاصة بالحاسوب، ويجري إسقاط ملايين الأشعة في الثانية الواحدة؛ فإذا أردتَ أن تعرف ما يحدث في الرسوميات الحاسوبية، فيجب أن تعرف ما هي عملية الإسقاط. لنوجِد الآن مسقط الشعاع w على الشعاع v في الرسم البياني التالي: حاول إجراء العمليات الحسابية المطلوبة كما فعلنا في الأمثلة السابقة، ولكن يمكن أن تحصل على الإجابات التالية مباشرةً من قراءة الرسم البياني السابق: w = (4, 8.2)T v = (2.8, 1.75)T k v = (5.8, 1.6)T u = (-1.8, 6.4)T تبدو الإجابات السابقة جيدة، ولكن لنتحقّق منها كما يلي: أولًا، هل ‎w = k v + u‎؟ ‎(5.8, 1.6)T + (-1.8, 6.4)T = (4, 8.0)T = w (حسنًا، تقريبًا) ثانيًا، هل ‎v · u = 0.0؟ ‎(5.8, 1.6)T · (-1.8, 6.4)T = -10.44 + 10.24 = -0.2 = ‎0.0‎ (تقريبًا)‎ ملاحظة: لنفكر في جميع الأشعة المحتملة w و v ومسقط الشعاع w على الشعاع v، إذ يمكن أن يكون الشعاع k v‎ أطول من الشعاع v كما في المثال السابق، ولكن لا يمكن أن يكون الشعاع k v‎ أطول من الشعاع w، فأكبر طول ممكن للشعاع k v‎ هو | w | عندما يكون w و v على استقامة واحدة. مثال آخر لعملية الإسقاط لاحظ أن عملية إسقاط الشعاع w على v لا تعتمد على طول v، بل يؤثر اتجاه الشعاع v فقط على النتائج. يكون طول المسقط هو ‎| w | cos θ، حيث θ هي الزاوية بين الشعاعين، وبما أن دالة جيب التمام لا تزيد عن 1 أبدًا، فلن يكون طول المسقط أبدًا أطول من الشعاع المُسقَط. أسقِط الشعاع w على الشعاع v من خلال الاطلاع على الرسم البياني التالي: وستحصل على الإجابات التالية: w = (-5, 5)T v = (5, 1)T k v = (-3.8, -.8)T u = (-1.3, 5.6)T حصلنا على الإجابات السابقة من خلال قراءة الرسم البياني السابق، ولكنها ليست دقيقة بالضرورة، إذًا لنتحقّق منها. أولًا، هل ‎w = k v + u‎؟ ‎(-3.8, -0.8)T + (-1.3, 5.6)T = (-5.1, 4.8)T = w (تقريبًا) ثانيًا، هل ‎v · u = 0.0؟ ‎‎(5, 1)T · (-1.3, 5.6)T = -6.5 + 5.6 = -0.9 = 0.0 (ليس تمامًا) ملاحظة: يمكن لمسقط الشعاع w على الشعاع v أن يؤشّر إلى الاتجاه المعاكس للشعاع v كما حدث في المثال السابق. أمثلة لإسقاط الأشعة ثلاثية الأبعاد سنستعرض هنا بعض الأمثلة حول كيفية إسقاط الأشعة ثلاثية الأبعاد المثال الأول يظهر الرسم البياني السابق الشعاعين التاليين: ‎w = (4, 2.5, 2.5)T‎ (الشعاع الأحمر) ‎v = (5, 0, 3.1)T (الشعاع الأخضر) أسقِط الشعاع w على الشعاع v، وتذكّر أن طول الشعاع v لا يؤثر على الإسقاط، بل يؤثر منحاه عليه فقط، لذا أنزِل رأس سهم الشعاع w على الخط المنقط. يمكن بالنظر تقدير مسقط الشعاع ‎w = (4, 2.5, 2.5)T‎ على الشعاع ‎v = (5, 0, 3.1)T‎ بأنه k v = (4, 0, 2.5)T‎. يتضمن إسقاط الشعاع w على الشعاع v في هذه الحالة "انهيار Collapsing" البعد y للشعاع w. فلنتحقّق الآن مما إذا كانت النتيجة نفسها رياضيًا، إذ يمكن تطبيق الخطوات نفسها في الفضاء ثلاثي الأبعاد 3D. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = ((5, 0, 3.1)T · (5, 0, 3.1)T) = 34.61 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2.5, 2.5)T / | w |‎ vu = (5, 0, 3.1)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: ‎wu · vu = (4, 2.5, 2.5)T / | w | · (5, 0, 3.1)T / | v | = 27.75 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu‎ k v = | w | (27.75 / (| w | | v |)) (5, 0, 3.1)T / | v |‎ k v = 27.75 / (| v |) (5, 0, 3.1)T / | v |‎ ‎k v = 27.75 / (| v |2) (5, 0, 3.1)T k v = (27.75 / 34.61) (5, 0, 3.1)T‎ ‎k v = ((27.75*5)/34.61, 0, (27.75*3.1)/34.61)T ‎k v = ( 4.00, 0, 2.49)T وبالتالي فإن التقدير بالنظر k v = (4, 0, 2.5)T‎ يطابق النتيجة الرياضية ‎(4, 0, 2.5)T‎، ويمكن الآن بسهولة حساب الشعاع العمودي: u = w - k v u = (4, 2.5, 2.5)T - (4, 0, 2.5)T = (0, 2.5, 0)T أبقِ الشعاع w كما هو في الرسم البياني السابق، وحاول تصوّر إسقاط w على شعاع مختلف v'‎ ينتج عنه المسقط k ،v'‎ الذي يؤدي إلى انهيار البعدين y و z للشعاع w؛ فمثلًا، إذا أسقطنا الشعاع ‎w = (4, 2.5, 2.5)T‎ على الشعاع ‎v' = (1, 0, 0)T‎، فستكون النتيجة k v = (4, 0, 0)T‎. المثال الثاني إذا أسقطنا شعاعًا في فضاء ثلاثي الأبعاد على محاور إحداثية، فستحصل على أحد الأبعاد الثلاثة فقط لذلك الشعاع، لذا لنلقِ نظرة على مثال آخر أصعب. لدينا من الشكل السابق الشعاعان ‎w = (4, 2, 3)T‎ و ‎v = (6, 4, 2)T‎، ونريد إسقاط الشعاع w على الشعاع v. يمكن أن نقدّر بالنظر أنه إذا "أنزلنا" رأس سهم الشعاع w عموديًا على الشعاع v، فستصل إلى الشعاع v عند حوالي 4/10 من طول v، لذا فإن k تساوي 0.4 تقريبًا و k v‎ يساوي: k v = 0.4 (6, 4, 2)T = (2.4, 1.6, 0.8)T ولكن سيتضح معنا أن هذه النتيجة التقديرية خاطئة بصورة فادحة، إذ يُعَد إيجاد الحل بالنظر في المسائل ثلاثية الأبعاد أمرًا صعبًا، إلّا إذا كانت لديك صورة ثلاثية الأبعاد فعلية أمامك. على هذا الأساس، دعنا نتحقق من الإجابة رياضيًا. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = ((6, 4, 2)T · (6, 4, 2)T) = 56 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2, 3)T / | w |‎ vu = (6, 4, 2)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: wu · vu = (4, 2, 3)T · (6, 4, 2)T / | w | | v | = 38 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = | w | {38 / (| w | | v |)} {(6, 4, 2)T / | v |} k v = {38 / | v |} {(6, 4, 2)T / | v |} k v = {38 / | v |2} (6, 4, 2)T k v = {38/56} (6, 4, 2)T k v = 0.679 (6, 4, 2)T k v = ( 4.07, 2.7, 1.36)T لنحسب الآن الشعاع u كما يلي، حيث ‎u + k v = w‎ (الشعاع u هو الشعاع الأسود المنقط في الرسم البياني السابق): u + k v = w u = w - k v = (4, 2, 3)T - ( 4.07, 2.7, 1.36)T = (-0.07, -0.7, 1.64)T بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية إسقاط شعاع على شعاع آخر في فضاء ثنائي وثلاثي الأبعاد، وسنتعرّف في المقال التالي على عملية الجداء الشعاعي للأشعة. ترجمة -وبتصرُّف- للفصل Projecting one Vector onto Another من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثلاثي الأبعاد 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D
  14. تستخدم لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- مجموعةً متنوعة من هياكل البيانات، وتُعدّ الجداول من أكثرها استخدامًا، ولكن يكون لهذه الجداول قيود أو محدوديات معينة، فمثلًا لا يمكنك تقييد المستخدمين للوصول إلى جزء من الجدول فقط، إذ يجب أن يتمكّن المستخدم من الوصول إلى الجدول بأكمله، وليس إلى بعض الأعمدة ضمنه فقط. لنفترض مثلًا أنك تريد دمج البيانات من عدة جداول في هيكل بيانات جديد، ولكنك لا تريد حذف الجداول الأصلية، حيث يمكنك إنشاء جدول آخر فقط، ولكن سيكون لديك بيانات زائدة مخزَّنة في أماكن متعددة لاحقًا. قد يسبّب ذلك الكثير من الإزعاج، حيث إذا تغيرت بعض بياناتك، فيجب عليك أن تحدّثها في أماكن متعددة، لذا يمكن أن تكون العروض Views مفيدة في مثل هذه الحالات. يُعَد العرض view في لغة SQL جدولًا افتراضيًا، وتكون محتوياته نتيجة لاستعلام محدّد لجدول واحد أو أكثر، حيث تُعرَف هذه الجداول باسم الجداول الأساسية Base Tables. يقدّم هذا المقال نظرة عامة حول عروض SQL وفوائدها، ويوضّح كيفية إنشاء العروض والاستعلام عنها وتعديلها وتدميرها باستخدام صيغة SQL المعيارية. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- التي تستخدم لغة SQL. اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، كما يمكنك الاطلاع على مقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة. نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا الخطوات باستخدام مستخدم مُنشَأ وفق الطريقة الموضحة في الخطوة 3 من المقال. ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. ستحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لتتمكن من التدرب على إنشاء العروض والتعامل معها، لذا اطّلع على القسم التالي للحصول على تفاصيل حول كيفية الاتصال بخادم MySQL وإنشاء قاعدة بيانات الاختبار المُستخدَمة في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كان نظام قاعدة بيانات SQL الخاص بك يعمل على خادم بعيد، فاتصل بالخادم مُستخدمًا بروتوكول SSH من جهازك المحلي كما يلي: $ ssh user@your_server_ip ثم افتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بك مكان user: $ mysql -u user -p أنشِئ قاعدة بيانات باسم views_db في موجّه الأوامر: mysql> CREATE DATABASE views_db; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر خرج كما يلي: الخرج Query OK, 1 row affected (0.01 sec) يمكنك اختيار قاعدة البيانات views_db من خلال تنفيذ تعليمة USE التالية: mysql> USE views_db; ويكون الخرج كما يلي: الخرج Database changed اخترنا قاعدة بيانات views_db، وسننشئ جدولين ضمنها. لنفترض مثلًا أنك تدير خدمة رعاية للكلاب، فقررت استخدام قاعدة بيانات SQL لتخزين معلومات حول كل كلب مُسجَّل في الخدمة، بالإضافة إلى معلومات كل متخصص في رعاية الكلاب توظفه خدمتك، حيث نظّمت هذه المعلومات من خلال إنشاء جدولين: أحدهما يمثل الموظفين والآخر يمثل الكلاب التي تعتني بها خدمتك. سيحتوي الجدول الذي يمثل موظفيك على الأعمدة التالية: emp_id: رقم تعريف لكل موظف يقدّم رعايةً للكلاب، ونعبّر عنه باستخدام نوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي Primary Key للجدول، أي أن كل قيمة ستمثّل معرّفًا فريدًا للصف الخاص بها، وسيكون لهذا العمود أيضًا قيد UNIQUE مطبَّق عليه، إذ يجب أن تكون كل قيمة في المفتاح الرئيسي فريدة. emp_name: اسم الموظف، ونعبّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. نفّذ تعليمة CREATE TABLE التالية لإنشاء جدول بالاسم employees ويحتوي على العمودين التاليين: mysql> CREATE TABLE employees ( mysql> emp_id int UNIQUE, mysql> emp_name varchar(20), mysql> PRIMARY KEY (emp_id) mysql> ); سيحتوي الجدول الآخر الذي يمثل الكلاب على الأعمدة الستة التالية: dog_id: رقم تعريف لكل كلب ويُعبَّر عنه بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي للجدول dogs مثل العمود emp_id في الجدول employees. dog_name: اسم الكلب ويُعبَّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. walker: يخزّن هذا العمود رقم معرّف الموظف الذي يرعى كل كلب. walk_distance: المسافة التي يجب أن يمشيها كل كلب عند اصطحابه للتمرين، ويُعبَّر عنه باستخدام نوع البيانات decimal، ويمكن أن تحتوي القيم في هذا العمود على ثلاثة أرقام على الأكثر مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. meals_perday: توفر هذه الخدمة لكل كلب عددًا معينًا من الوجبات كل يوم، حيث يحتوي هذا العمود على عدد الوجبات التي يجب أن يحصل عليها كل كلب يوميًا حسب طلب مالكه، ويستخدم نوع البيانات int فهو عدد صحيح. cups_permeal: يمثل هذا العمود عدد أكواب الطعام التي يجب أن يحصل عليها كل كلب في كل وجبة، ويُعبَّر عنه بنوع البيانات decimal مثل العمود walk_distance، ويمكن أن تحتوي القيم في هذا العمود على ما يصل إلى ثلاثة أرقام مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. نتأكد من أن العمود walker يحتوي على القيم التي تمثل أرقام معرّف الموظف الصالحة فقط من خلال تطبيق قيد مفتاح خارجي Foreign Key على العمود walker الذي يشير إلى العمود emp_ID الخاص بالجدول employees. يُعَد قيد المفتاح الخارجي طريقة للتعبير عن العلاقة بين جدولين من خلال اشتراط أن تكون القيم الموجودة في العمود الذي طُبِّق المفتاح الخارجي عليه موجودة في العمود الذي يشير إليه، حيث يشترط قيد FOREIGN KEY في المثال التالي أن تكون أيّ قيمة مضافة إلى العمود walker في الجدول dogs موجودةً في العمود emp_ID الخاص بالجدول employees. لننشئ جدولًا بالاسم dogs يحتوي على هذه الأعمدة باستخدام الأمر التالي: mysql> CREATE TABLE dogs ( mysql> dog_id int UNIQUE, mysql> dog_name varchar(20), mysql> walker int, mysql> walk_distance decimal(3,2), mysql> meals_perday int, mysql> cups_permeal decimal(3,2), mysql> PRIMARY KEY (dog_id), mysql> FOREIGN KEY (walker) mysql> REFERENCES employees(emp_ID) ); يمكنك الآن تحميل الجدولين ببعض البيانات التجريبية النموذجية. نفّذ عملية الإدخال INSERT INTO التالية لإضافة ثلاثة صفوف من البيانات تمثّل ثلاثة من موظفي الخدمة إلى الجدول employees: mysql> INSERT INTO employees mysql> VALUES mysql> (1, 'Peter'), mysql> (2, 'Paul'), mysql> (3, 'Mary'); ثم نفّذ العملية التالية لإدخال سبعة صفوف من البيانات في الجدول dogs: mysql> INSERT INTO dogs mysql> VALUES mysql> (1, 'Dottie', 1, 5, 3, 1), mysql> (2, 'Bronx', 3, 6.5, 3, 1.25), mysql> (3, 'Harlem', 3, 1.25, 2, 0.25), mysql> (4, 'Link', 2, 2.75, 2, 0.75), mysql> (5, 'Otto', 1, 4.5, 3, 2), mysql> (6, 'Juno', 1, 4.5, 3, 2), mysql> (7, 'Zephyr', 3, 3, 2, 1.5); وأصبحتَ الآن جاهزًا لمتابعة بقية هذا المقال والبدء في تعلّم كيفية استخدام العروض في لغة SQL. فهم وإنشاء العروض Views يمكن أن تصبح استعلامات SQL معقدة، ولكن إحدى الفوائد الرئيسية للغة SQL هي أنها تتضمن العديد من الخيارات والتعليمات التي تسمح لك بترشيح بياناتك بمستوى عالٍ من الدقة والتحديد. إذا كانت لديك استعلامات معقدة تريد تشغيلها بصورة متكررة، فقد يصبح الاضطرار إلى كتابتها باستمرار أمرًا مملًا، وإحدى الطرق لحل هذه المشكلات هي استخدام العروض. تُعَد العروض views جداولًا افتراضية كما ذكرنا سابقًا، وهذا يعني أنها تشبه الجداول وظيفيًا، ولكنها تمثّل نوعًا مختلفًا من هياكل البيانات لأن العرض لا يحتوي على أيّ بيانات خاصة به، فهو يسحب البيانات من جدول أساسي واحد أو أكثر، وتكون المعلومات الوحيدة حول العرض التي سيخزنها نظام إدارة قواعد البيانات DBMS هي هيكل العرض. تُسمَّى العروض أحيانًا بالاستعلامات المحفوظة Saved Queries، لأنها تمثّل الاستعلامات المحفوظة باسم مُحدَّد لتسهيل الوصول إليها. ليكن لدينا المثال التالي، تخيل أن أعمال رعاية الكلاب الخاصة بك ناجحة وتريد طباعة جدول يومي لجميع موظفيك، إذ يجب أن يحتوي هذا الجدول على كل كلب ترعاه الخدمة، والموظف المكلف برعايته، والمسافة التي يجب أن يمشيها كل كلب يوميًا، وعدد الوجبات التي يجب إطعامها لكل كلب يوميًا، وكمية الطعام التي ينبغي أن يحصل كل كلب في كل وجبة. استخدم مهاراتك في لغة SQL لإنشاء استعلام مع بياناتٍ تجريبية نموذجية من الخطوة السابقة لاسترجاع جميع هذه المعلومات للجدول، ولاحظ أن هذا الاستعلام يتضمن صيغة JOIN لسحب البيانات من الجدولين employees و dogs: mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs ON emp_ID = walker; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) لنفترض أنه يجب تنفيذ هذا الاستعلام بشكل متكرر، إذ قد يصبح أمرًا مملًا إذا اضطررت إلى كتابة الاستعلام مرارًا وتكرارًا، وخاصةً عند إجراء تعليمات استعلام أطول وأكثر تعقيدًا، وإذا أردتَ إجراء تعديلات طفيفة على الاستعلام أو التوسّع فيه، فقد يكون الأمر مملًا عند استكشاف الأخطاء وإصلاحها مع وجود العديد من الاحتمالات للأخطاء الصياغية. يمكن أن يكون العرض مفيدًا في مثل هذه الحالات، لأنه يُعَد جدولًا مشتقًا من نتائج الاستعلام. تستخدم معظم أنظمة RDBMS الصيغة التالية لإنشاء العروض: CREATE VIEW view_name AS SELECT statement; يمكنك بعد تعليمة CREATE VIEW اختيار اسمٍ للعرض الذي ستستخدمه للإشارة إليه لاحقًا، ثم تدخِل الكلمة المفتاحية AS، ثم تضع استعلام SELECT الذي تريد حفظ خرجه. يمكن أن يكون الاستعلام الذي تستخدمه لإنشاء عرضك أيّ تعليمة SELECT صالحة، ويمكن أن تستعلم التعليمة التي تضمّنها عن جدول أساسي واحد أو أكثر طالما أنك تستخدم الصيغة الصحيحة. جرّب إنشاء عرض باستخدام استعلام المثال السابق، حيث تسمّي عملية CREATE VIEW العرض بالاسم walking_schedule: mysql> CREATE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs mysql> ON emp_ID = walker; ستتمكّن بعد ذلك من استخدام هذا العرض والتفاعل معه كما تفعل مع أيّ جدول آخر، فمثلًا يمكنك تنفيذ الاستعلام التالي لإعادة جميع البيانات الموجودة في العرض: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) يُعَد هذا العرض مشتقًا من جدولين آخرين، ولكنك لن تتمكّن من الاستعلام عن العرض لأيّ بيانات من هذين الجدولين إن لم تكن موجودة مسبقًا في هذا العرض. يحاول الاستعلام التالي استرجاع العمود walker من العرض walking_schedule، ولكن سيفشل هذا الاستعلام لأن العرض لا يحتوي على أيّ أعمدة بهذا الاسم: mysql> SELECT walker FROM walking_schedule; الخرج ERROR 1054 (42S22): Unknown column 'walker' in 'field list' يعيد هذا الخرج رسالة خطأ لأن العمود walker هو جزء من الجدول dogs، ولكنه غير مُضمَّنٍ في العرض الذي أنشأناه. يمكنك أيضًا تنفيذ الاستعلامات التي تتضمّن دوالًا تجميعية Aggregate Functions تعالج البيانات ضمن العرض، يستخدم المثال التالي الدالة التجميعية MAX مع عبارة GROUP BY للعثور على أطول مسافة يجب على الموظف أن يمشيها في يوم محدّد: mysql> SELECT emp_name, MAX(walk_distance) AS longest_walks mysql> FROM walking_schedule GROUP BY emp_name; الخرج +----------+---------------+ | emp_name | longest_walks | +----------+---------------+ | Peter | 5.00 | | Paul | 2.75 | | Mary | 6.50 | +----------+---------------+ 3 rows in set (0.00 sec) توجد فائدة أخرى للعروض كما ذكرنا سابقًا، وهي أنه يمكنك استخدامها لتقييد وصول مستخدم قاعدة البيانات إلى العرض فقط بدلًا من الوصول إلى الجدول أو قاعدة البيانات بأكملها. لنفترض مثلًا أنك وظّفتَ مدير مكتب لمساعدتك في إدارة الجدول الزمني، وتريد أن يصل إلى معلومات الجدول دون الوصول لأي بيانات أخرى في قاعدة البيانات، حيث يمكنك إنشاء حساب مستخدم جديد له في قاعدة بياناتك كما يلي: mysql> CREATE USER 'office_mgr'@'localhost' IDENTIFIED BY 'password'; يمكنك بعد ذلك منح هذا المستخدم الجديد صلاحية وصول للقراءة إلى العرض walking_schedule فقط باستخدام التعليمة GRANT كما يلي: mysql> GRANT SELECT ON views_db.walking_schedule to 'office_mgr'@'localhost'; وبالتالي سيتمكّن الشخص الذي لديه صلاحية الوصول إلى حساب مستخدم MySQL الذي هو office_mgr من تنفيذ استعلامات SELECT في العرض walking_schedule فقط. تغيير وحذف العروض Views إذا أضفتَ أو غيّرتَ بياناتٍ في أحد الجداول التي نشتق العرض منها، فستُضاف أو تُحدَّث البيانات ذات الصلة في العرض تلقائيًا. نفّذ الأمر INSERT INTO التالي لإضافة صف آخر إلى الجدول dogs: mysql> INSERT INTO dogs VALUES (8, 'Charlie', 2, 3.5, 3, 1); يمكنك بعد ذلك استرجاع جميع البيانات من العرض walking_schedule مرة أخرى كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Paul | Charlie | 3.50 | 3 | 1.00 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) لاحظ وجود صف آخر في مجموعة نتائج الاستعلام، والذي يمثّل البيانات التي أضفتها إلى الجدول dogs، ولكن لا يزال العرض يسحب البيانات ذاتها من الجداول الأساسية نفسها، لذلك لم تغيّر هذه العملية العرض. تسمح لك العديد من أنظمة RDBMS بتحديث هيكل العرض بعد إنشائه باستخدام صيغة CREATE OR REPLACE VIEW: mysql> CREATE OR REPLACE VIEW view_name mysql> AS mysql> new SELECT statement إذا كان العرض الذي اسمه view_name موجودًا مسبقًا في هذه الصيغة، فسيحدّث نظام قاعدة البيانات هذا العرض بحيث يمثّل البيانات التي تعيدها التعليمة new SELECT statement. إذا لم يكن العرض بهذا الاسم موجودًا، فسينشئ نظام إدارة قواعد البيانات DBMS عرضًا جديدًا. لنفترض أنك تريد تغيير العرض walking_schedule ليسرد إجمالي كمية الطعام التي تناولها كل كلب على مدار اليوم بدلًا من سرد عدد أكواب الطعام التي يتناولها كل كلب في كل وجبة، ويمكنك تغيير العرض باستخدام الأمر التالي: mysql> CREATE OR REPLACE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, (cups_permeal * mysql> meals_perday) AS total_kibble mysql> FROM employees JOIN dogs ON emp_ID = walker; إذا أجربتَ الآن استعلامًا على هذا العرض، فستمثّل مجموعة النتائج بيانات العرض الجديدة كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | total_kibble | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 3.00 | | Peter | Otto | 4.50 | 3 | 6.00 | | Peter | Juno | 4.50 | 3 | 6.00 | | Paul | Link | 2.75 | 2 | 1.50 | | Paul | Charlie | 3.50 | 3 | 3.00 | | Mary | Bronx | 6.50 | 3 | 3.75 | | Mary | Harlem | 1.25 | 2 | 0.50 | | Mary | Zephyr | 3.00 | 2 | 3.00 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) يمكنك حذف العروض باستخدام صيغة DROP مثل معظم هياكل البيانات الأخرى التي يمكنك إنشاؤها في لغة SQL، وإليك مثالًا: DROP VIEW view_name; يمكنك مثلًا حذف العرض walking_schedule باستخدام الأمر التالي: mysql> DROP VIEW walking_schedule; يؤدي الأمر السابق إلى إزالة العرض walking_schedule من قاعدة بياناتك، ولكنه لن يحذف أيًّا من بيانات قاعدة بياناتك المتعلقة بالعرض إلّا إذا أزلتها من الجداول الأساسية. الخلاصة تعلّمنا في هذا المقال ما هي عروض SQL وكيفية إنشائها والاستعلام عنها وتغييرها وحذفها من قاعدة البيانات، وتعرّفنا على فوائد العروض، وأنشأنا مستخدم MySQL الذي يمكنه فقط قراءة البيانات من العرض التجريبي الذي أنشأناه. يجب أن تعمل الأوامر الواردة في أمثلة هذا المقال على معظم قواعد البيانات العلاقية، ولكن يجب أن تدرك أن كل قاعدة بيانات SQL لها تقديمها الفريد من هذه اللغة، لذا يجب عليك الرجوع إلى التوثيق الرسمي لنظام إدارة قواعد البيانات DBMS الخاص بك للحصول على وصف أشمل لكل أمر ومجموعته الكاملة من الخيارات. ننصحك بالاطلاع على سلسلة تعلم SQL في أكاديمية حسوب للمزيد حول كيفية التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use Views in SQL لصاحبه Mark Drake. اقرأ أيضًا الاستعلام عن البيانات في SQL جلب الاستعلامات عبر SELECT في SQL المرجع المتقدم إلى لغة SQL لغة معالجة البيانات DML الخاصة بلغة SQL
  15. رأينا في المقالات السابقة أن نستطيع استخدام الجداء النقطي Dot Product من أجل الآتي: حساب طول الشعاع: حيث يكون الجداء النقطي للشعاع مع نفسه = الطول2. اكتشاف التعامد بين شعاعين: حيث يكون الجداء النقطي لشعاعين متعامدين = 0. حساب الزاوية بين شعاعين: حيث يكون cosθ = حاصل الجداء النقطي لشعاعي وحدة Unit Vectors. لن نضيف في هذا المقال شيئًا جديدًا عمّا تعلمناه في المقال السابق، ولكننا سنوسّع مفهوم حساب الزاوية بين شعاعين ليشمل حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد. سنتعرّف في هذا المقال على المواضيع الآتية: صيغة حساب جيب تمام Cosine الزاوية بين أشعة الوحدة. قائمة بالخطوات التي يجب اتخاذها لتطبيق هذه الصيغة على الأشعة التي ليست أشعة وحدة. استخدام الأشعة في مسائل الهندسة الفراغية. الزوايا في الفضاء ثلاثي الأبعاد تقع أي حافتين من الحواف الثلاثة المشكّلة لزاوية في صندوق من الورق المقوى في مستوٍ واحد، وتساوي الزاوية بينهما 90 درجة، إذ تتقاطع جميع حواف الصندوق بزوايا قائمة، ولكن لن تساوي الزوايا بين الحواف 90 درجة عادةً عند إسقاط الحواف لتكوين صورة ثنائية الأبعاد. الزوايا بين محاور الإحداثيات لا يؤدي تغيير مجال رؤيتك إلى تغيير الكائنات ثلاثية الأبعاد، ولكن ستتغيّر الصورة ثنائية الأبعاد. تعتمد الزاوية بين شعاعين في الفضاء ثلاثي الأبعاد على علاقتهما ببعضهما البعض، ولا تعتمد على مجال رؤيتك أو إطار الإحداثيات الذي تستخدمه؛ ولكن زوايا الصورة تعتمد كثيرًا على مجال الرؤية عندما تعرض صورة ثنائية الأبعاد مشهدًا ثلاثي الأبعاد. رأينا في المقال السابق الصيغة التالية لحساب الزاوية θ بين شعاعي الوحدة: au · bu = cosθ‎ إذًا لنطبّق هذه الصيغة على شعاعي الوحدة الموازيين للمحور x والمحور y وهما: ‎(1,0,0)T‎ و ‎(0,1,0)T‎ لحساب الزاوية المحصورة بين المحورين x و y كما يلي: ‏‏‎(1,0,0)T ‎· (0,1,0)T =‎ 0 وبالتالي فإن المحورين x و y متعامدان كما توقعنا. الزاوية بين شعاعين كان المثال السابق سهل التصور والحساب، إذًا لنجرب شيئًا أصعب، حيث يظهر الشكل الآتي شعاعين: الشعاع a الذي تمثله المصفوفة العمودية ‎(4,4,4)T‎. الشعاع b الذي تمثله المصفوفة العمودية ‎(4,0,4)T‎. لاحظ أن الزاوية بين الشعاعين لا تساوي 90 درجة، وبالتالي يصعب رؤيتها مقارنةً بالمثال السابق. يمكن أن نخمّن بصريًا إلى حدٍ ما أن الزاوية بين الشعاعين a و b أقل من منتصف المسافة بين المستوي العمودي والأفقي، إذ تكون الزاوية التي تساوي 35 درجة تخمينًا جيدًا، ولكن التخمين غير كافٍ ونحتاج صيغةً لحساب الزاوية، وهذا ما سنوضّحه في الفقرة التالية. صيغة حساب الزاوية بين شعاعين لاحظ أنه عند الانتقال من ذيل الشعاع a إلى رأسه تزيد المسافة العمودية بمقدار 4، بينما تزيد المسافة الأفقية بمقدار 4‎ √2، وظل الزاوية Tangent هو: 4‎ / (4 √2) = 1.0/ √2 = 0.7071 وبالتالي فإن الزاوية مع المستوي الأفقي هي: arctan( 0.7071 )‎ =‫ 35.26 درجة. وبما أن الشعاع b يقع في المستوي الأفقي، فإن الزاوية بين الشعاعين يجب أن تساوي هذه القيمة. وبالتالي فإن صيغة حساب الزاوية θ بين شعاعي وحدة هي: au · bu = cosθ ويمكننا استخدام هذه الصيغة مع الأشعة التي ليست أشعة وحدة باتباع الخطوات التالية: توحيد Normalize كل شعاع. حساب الجداء النقطي لشعاعي الوحدة الناتجين. استخدم arc cos للحصول على الزاوية. لنطبّق هذه الصيغة على الشعاع a الذي تمثله ‎(4,4,4)T‎ والشعاع b الذي تمثله ‎(4,0,4)T‎ كما يلي: ‎| a | = √(16 + 16 + 16) = 4√3 و | b | = √(16 + 16) = 4√2 au = (4, 4, 4)T / (4√3)‎ و au = (4, 0, 4)T / (4 √2 ) au · bu = (16 + 16)/( (4√3)(4 √2) ) = 2/(√3√2 ) = √2 /√3 = cosθ‎ cosθ = 0.81649 وبالتالي فإن الزاوية بين الشعاعين هي: θ =‫ 35.26 درجة. لا تُعَد الحسابات الكثيرة في التمرين السابق هي الغرض الحقيقي، ولكن الهدف هو توضيح الصيغة ‎au · bu = cosθ، والتي تُعَد مهمةً في كل جزء من أجزاء الرسوميات ثلاثية الأبعاد. تدريب عملي يوضح الشكل الآتي شعاعين تمثّلهما المصفوفتان العموديتان التاليتان: f = (2, 4, 6)T‎ ‎g = (6, 4, 3)T يمكن أن ترغب في قياس الزاوية بين الشعاعين من خلال وضع مِنقلة مسطحة بين الشعاعين، ولكن لا يمكنك ذلك لأن كل ما تراه هو إسقاطٌ للشعاعين على الشاشة. يمكننا تخمين الزاوية التي تفصل بين الشعاعين بمقدار 30 درجة، ولكنه مجرد تخمين، إذًا لنطبّق الصيغة لمعرفة مدى صحة هذا التخمين. أولًا، نحسب الأطوال وهي: ‎| f |2 = (2, 4, 6)T · (2, 4, 6)T = 4 + 16 + 36 = 56 ‎| g |2 = (6, 4, 3)T · (6, 4, 3)T = 36 + 16 + 9 = 61 ثانيًا، نوحّد الشعاعين كما يلي: fu = (2, 4, 6)T / √56‎ gu = (6, 4, 3)T / √61‎ ثالثًا، نحسب الجداء النقطي لهما وهو: fu · gu = (2, 4, 6)T · (6, 4, 3)T / ( √56 √61) = (12+16+18)/( √56 √61) = 46 / ( √56 √61) = 0.78704‎ رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = 0.78704‎ θ = arc cos 0.78704 = 38.1‎ درجة لاحظ أن الجواب ليس بعيدًا جدًا عن التخمين. الزاوية بين اتجاهي شعاعين ليس من الضروري أن يتلامس الشعاعان عند ذيليهما حتى تكون هناك زاوية بينهما، فليس للأشعة موضع محدد، إذ يمكنك أخذ الجداء النقطي لأي شعاعين ثلاثي الأبعاد أو المصفوفات العمودية التي تمثلها. قد يبدو من الغريب قياس الزاوية بين الأشياء التي ليس لها موضع محدد، ولكن العديد من الظواهر لها منحًى Orientation، دون أن يكون لها موضع محدد؛ إذ قد تأتي الرياح من الغرب في أحد الأيام، ومن الشمال في اليوم التالي (بفارق 90 درجة)؛ وقد يصل ضوء الشمس عند الظهيرة في الصيف بزاوية مختلفة عن زاوية وصوله عند الظهيرة في الشتاء. يستخدم التظليل في الرسوميات ثلاثية الأبعاد أشعةً لمنحى السطح، وأشعة أخرى لاتجاه كل مصدر للضوء. ليكن باسم وبلال جالسين على الشاطئ، ويريدان تسمير بشرتهما كما في الشكل التالي: يؤشر إطار الإحداثيات المناسب إلى المحور x شمالًا، والمحور y للأعلى بصورة مستقيمة والمحور z شرقًا، حيث يكون الاتجاه نحو الشمس في هذا الإطار هو ‎(-3, 4, 0)T / 5، وشعاع الوحدة العمودي على ظهر باسم هو ‎( -1, 2, 2)T / 3، وشعاع الوحدة العمودي على ظهر بلال هو ‎( -2, 1, 2)T / 3. إذًا سيكون أحدهما متمتعًا بأفضل موضع للتسمير السريع، حيث سنوضّح ذلك كما يلي: الاتجاه نحو الشمس = ‎( -3, 4, 0)/5. الاتجاه العمودي على ظهر باسم = ‎( -1, 2, 2)/3. الاتجاه العمودي على ظهر بلال = ‎( -2, 1, 2)/3. يجب أن يكون ظهرك مُوجَّهًا مباشرةً نحو الشمس ليصل إليك أكبر قدرٍ من أشعتها، كما يجب أن تكون الزاوية بين الاتجاه العمودي على ظهرك واتجاه الشمس صغرى. بالنسبة لباسم: ‎( -1, 2, 2)/3 · ( -3, 4, 0)/5 = (3+8+0)/15 = 11/15 arc cos( 11/15 ) = 42.8‎ درجة وبالنسبة لبلال: ‎( -2, 1, 2)/3 · ( -3, 4, 0)/5 = (6+4+0)/15 = 10/15 arc cos( 14/15 ) = 48.2 درجة وبالتالي سيحصل باسم على تسمير لبشرته بصورة أسرع. تدريبات عملية تدريب 1: يظهر الرسم البياني الآتي الشعاعين التاليين: q = (-2, 4, 3)T p = (3, 1, -4)T وضعنا ذيل الأشعة عند نقطة الأصل لسهولة التصور، إذ تؤشّر الأشعة إلى ثُمنَين Octants مختلفتين من الفضاء ثلاثي الأبعاد. لنحسب الآن الزاوية بين هذين الشعاعين كما يلي: أولًا، نحسب الأطوال وهي: ‎| p |2 = ( -2, 4, 3)T · ( -2, 4, 3)T = 4 + 16 + 9 = 29 ‎| q |2 = ( 3, 1, -4)T · ( 3, 1, -4)T = 9 + 1 + 16 = 26 ثانيًا، نوحّد الشعاعين كما يلي: pu = (-2, 4, 3)T / √29‎ qu = (3, 1, -4)T / √26‎ ثالثًا، نحسب الجداء النقطي لهما وهو: pu · qu = (-2, 4, 3)·(3, 1, -4)T / ( √29 √26) = (-6 + 4 - 12)/( √29 √26) = -14/( √29 √26) = -0.50985‎ رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = -0.50985 θ = arc cos( -0.50985) = 120.654‎ درجة تدريب2: أوجد جيب تمام الزاوية بين شعاعي الوحدة التاليين: s = (1, 0, 1)T /√2‎ t = (1, 1, 1)T /√3‎ cosθ = (1, 0, 1)T · (1, 1, 1)T / ( √2 √3) = 2 / ( √2 √3) = √2 / √3 = 0.8164‎ استخدام صيغة الجداء النقطي لحل المسائل الهندسية يمكن استخدام صيغة الجداء النقطي لحل بعض المسائل الهندسية التي قد تكون صعبة الحل، فمثلًا، تتطابق الحواف الحمراء في الشكل التالي على محاور الإحداثيات، وتشترك في نقطة النهاية (0, 0, 0)، وتنتهي الحافة على طول المحور x عند x=2، وتنتهي الحافة على طول المحور y عند y=3، وتنتهي الحافة على طول المحور z عند z=4، وتربط الحواف المتبقية نقاط النهاية هذه. لنفترض الآن أنك تريد حساب الزاوية بين الحافتين الخضراويتين، وقد يكون ذلك مملًا إلى حدٍ ما باستخدام علم المثلثات، لذا يمكنك: تشكيل شعاع لكل حافة خضراء (من خلال طرح نقاط النهاية). توحيد كل شعاع منهما. استخدام قاعدة الجداء النقطي لحساب الزاوية بينهما. إذًا لنطبّق هذه الخطوات لحساب الزاوية بين الحافتين الخضراويتين كما يلي: أولًا، نقاط النهاية هي: (0‎, 3, 0) إلى (2‎, 0, 0) (0‎, 3, 0) إلى (0‎, 0, 4) ثانيًا، نحسب أشعة الإزاحة وهي: a = (2, 0, 0) - (0, 3, 0) = (2, -3, 0)T‎ b = (0, 0, 4) - (0, 3, 0) = (0, -3, 4)T‎ ثالثًا، نحسب الأطوال كما يلي: ‎| a |2 = (2, -3, 0)T · (2, -3, 0)T = 13 ‎| b |2 = (0, -3, 4)T · (0, -3, 4)T = 25 رابعًا، نوحّد الأشعة كما يلي: au = (2, -3, 0)T / √13‎ bu = (0, -3, 4)T / 5‎ خامسًا، نحسب الجداء النقطي لهما: au · bu = (2, -3, 0)T · (0, -3, 4)T / (5 √13) = 9 / (5 √13) = 0.49923‎ سادسًا، نحسب الزاوية بينهما كما يلي: cosθ = 0.49923‎ θ = arc cos 0.49923 = 60.051‎ درجة إذا حسبتَ أشعة الإزاحة بالطريقة المعاكسة (بالطرح من ‎(0, 3, 0)‎)، فلن يؤثر ذلك على الإجابة، إذ يمكن أن يؤشّر شعاعان إلى اتجاهين متعاكسين، ولكن حاصل جدائهما النقطي سيكون نفسه. بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد، وسنتعرّف في المقال التالي على إسقاط شعاعٍ ما على شعاع آخر. ترجمة -وبتصرُّف- للفصل The Angle between 3D Vectors من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثنائي الأبعاد 2D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D
  16. من المفيد معرفة وحدات بايثون 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
  17. ستصادف ملفات الصور الرقمية طوال الوقت إذا كان لديك كاميرا رقمية أو حتى إذا رفعتَ صورًا من هاتفك على حسابك على فيسبوك أو انستغرام مثلًا، وقد تعرف كيفية استخدام برامج الرسوميات الأساسية مثل الرسام 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 مكتبات بايثون تستخدم في المشاريع الصغيرة
  18. يمكنك كتابة برامج لإرسال رسائل البريد الإلكتروني والرسائل النصية القصيرة 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
  19. ناقشنا في المقال السابق بعض استخدامات الجداء النقطي والتي هي: حساب طول الشعاع: حيث يكون الجداء النقطي للشعاع مع نفسه = الطول2. اكتشاف تعامد شعاعين: حيث يكون الجداء النقطي لشعاعين متعامدين = 0. تساوي الزاوية بين الشعاع ونفسه 0 درجة في الحالة الأولى، وتساوي الزاوية بين الشعاعين 90 درجة في الحالة الثانية، وبالتالي يمكن القول أن الزاوية بين الشعاعين لها علاقة بالجداء النقطي، إذ قد يكون حاصل الجداء النقطي كبيرًا عندما تكون الزاوية بين الشعاعين قريبة من الصفر وصغيرًا عندما تكون الزاوية بينهما قريبة من الزاوية القائمة. سنناقش في هذا المقال المواضيع التالية: الخاصيتان المتعلقتان بالجداء النقطي للأشعة. صيغة لحساب الزاوية بين شعاعي وحدة Unit Vectors. عدد الأشعة التي تبعد عن شعاع معين بالمقدار الزاوي نفسه. معنى جيب التمام السالب للزاوية بين شعاعين. كيفية العثور على الزاوية بين الأشعة التي ليست أشعة وحدة. تأثير طول الأشعة على الجداء النقطي لنفكّر حاليًا في الأشعة في فضاء ثنائي الأبعاد مثل الشعاع a في الرسم البياني السابق، حيث يُحتمَل أن يكون جداؤه النقطي مع الشعاع c الذي له منحى Orientation مقداره 60 درجة أكبر من جدائه النقطي مع الشعاع b الذي له منحى مقداره 30 درجة، لأن الشعاع c أطول بكثير من الشعاع b، ولنتأكد الآن من ذلك الآن. ولتكن لدينا الأشعة التالية: a = (6, 0)T b = (3, 2)T c = (5, 9)T ويكون الجداء النقطي لها هو: a · b = 18.0 a · c = 30.00 توجد خاصيتان متعلقتان بحاصل جداء الأشعة النقطي كما ذكرنا سابقًا وهما: الزاوية بين أشعة الإدخال. طول أشعة الإدخال. ولكننا نرغب الآن في إبقاء تأثير الزاوية فقط من خلال إزالة تأثير الطول بطريقة أو بأخرى، ويمكننا إزالة تأثير الطول باستخدام توحيد Normalize الأشعة، أي إنشاء أشعة الوحدة Unit Vectors في الاتجاه نفسه كما سنوضح في الفقرة التالية. توحيد الأشعة يوضّح الرسم البياني السابق كل شعاع بعد توحيده، أي إنشاء شعاع وحدة باتجاه الشعاع الأصلي نفسه. ملاحظة: تذكّر أن الحرف u المنخفض يشير إلى شعاع الوحدة: ‎bu‎. إلغاء تأثير الطول على جداء الأشعة النقطي لنتأكد الآن من الجداء النقطي الأكبر من بين الجداءين ‎au · bu‎ و ‎au · cu‎ بعد أن وحّدنا الأشعة لإلغاء تأثير الطول على الجداء النقطي. بما أن: u · v = | u | | v | cos θ، وجميع الأشعة طولها يساوي 1.0، فإن الجداء النقطي ‎u · v = cos θ، وبالتالي فإن الجداء النقطي ‎au · bu‎ يساوي جيب تمام الزاوية بين الشعاعين ‎au‎ و ‎bu‎، والذي يمكن قراءته من الرسم البياني على أنه 0.866، بينما يساوي الجداء النقطي ‎au · cu‎ جيب تمام الزاوية بين ‎au‎ و ‎cu‎، والذي يمكن قراءته من الرسم البياني على أنه 0.500. منحى الشعاع ‎bu‎ أقرب إلى الشعاع ‎au‎، لذلك يكون ‎au · bu‎ هو الأكبر حيث: au = (1, 0): شعاع الوحدة عند الزاوية صفر درجة bu = (0.866, 0.5): شعاع الوحدة عند الزاوية 30 درجة cu = (0.5, 0.866): شعاع الوحدة عند الزاوية 60 درجة تذكر أن: cos 30 = 0.866 و sin 30 = 0.5 و cos 60 = 0.5 و sin 60 = 0.866. وبالتالي فإن الجداء النقطي هو: au · bu = 0.866 au · cu = 0.500 لذا يمكن إزالة تأثير الطول باستخدام أشعة طولها 1، ويكون حاصل الجداء النقطي أكبر عندما تفصل زاوية صغيرة بين الأشعة، وإذا كانت الأشعة في اتجاهات متعاكسة مثل ‎(1, 0)T‎ و ‎(-1, 0)T‎، فسيكون مقدار الجداء النقطي سالبًا. مجال قيمة الجداء النقطي لشعاعي وحدة سنوضّح فيما يلي عينة لشعاع الوحدة ‎bu‎ وحاصل الجداء النقطي له مع الشعاع ‎au = (1.0, 0)T‎ لزوايا مختلفة: الزاوية الشعاع b النتيجة صورة توضيحية الزاوية 000 درجة ‎(1.000, 0.000)T‎ تساوي 1.000 الزاوية 015 درجة ‎(0.966, 0.259)T‎ تساوي 0.966 الزاوية 030 درجة ‎(0.866, 0.500)T‎ تساوي 0.866 الزاوية 045 درجة ‎(0.707, 0.707)T‎ تساوي 0.707 الزاوية 060 درجة ‎(0.500, 0.866)T‎ تساوي 0.500 الزاوية 075 درجة ‎(0.259, 0.966)T‎ تساوي 0.259 الزاوية 090 درجة ‎(0.000, 1.000)T‎ تساوي 0.000 الزاوية 105 درجة ‎(-0.259, 0.966)T‎ تساوي ‎-0.259 ‫ الزاوية 120 درجة ‎(-0.500, 0.866)T‎ تساوي ‎-0.500 ‫ الزاوية 135 درجة ‎(-0.707, 0.707)T‎ تساوي ‎-0.707 ‫ الزاوية 150 درجة ‎(-0.866, 0.500)T‎ تساوي ‎-0.866 ‫ الزاوية 165 درجة ‎(-0.966, 0.259)T‎ تساوي ‎-0.966 ‫ الزاوية 180 درجة ‎(-1.000, 0.000)T‎ تساوي ‎-1.000 ‫ ‎bu‎ هو شعاع الوحدة الذي تمثله المصفوفة ‎(cos θ, sin θ )T‎ في كل حالة من الحالات. إذًا مجال قيم حاصل الجداء النقطي لشعاعي الوحدة ‎au · bu‎ هو ‎-1.0 … 1.0. صيغة حساب الزاوية بين شعاعين مجال قيم حاصل الجداء النقطي لشعاعي الوحدة ‎au · bu‎ هو ‎-1.0 … 1.0، لأن كل جداء نقطي في الفقرة السابقة هو: ‎(1, 0)T · ( cos θ, sin θ)T = cos θ وهذا صحيح، فعندما يكون ‎au‎ شعاع وحدة فإنه سيؤشر إلى أيّ اتجاه، وبالتالي يمكن حساب الزاوية بين شعاعي الوحدة كما يلي: au · bu = cos θ‎ حيث θ هي الزاوية بين الشعاعين، وتتضمن هذه الصيغة تلقائيًا حقيقة أن الجداء النقطي للأشعة المتعامدة هو صفر، لأن جيب تمام cos الزاوية 90 يساوي الصفر. أوجد جيب تمام الزاوية بين شعاعي الوحدة اللذين تمثلهما المصفوفتان ‎(0.7071, 0.7071)T‎ و ‎(0.5, 0.866)T‎: cos θ = (0.7071, 0.7071)T · (0.5, 0.866)T = 0.7071 · 0.5 + 0.7071 · 0.866 = 0.9659‎ تدريب عملي تدريب 1: يوضّح الرسم البياني السابق شعاعين هما: الشعاع الأول ‎(0.7071, 0.7071)T‎ وهو شعاع وحدة عند الزاوية 45 درجة، والشعاع الثاني ‎(0.5, 0.866)T‎ وهو شعاع وحدة عند الزاوية 60 درجة، وبالتالي يجب أن يكون حاصل الجداء النقطي لهما هو جيب تمام الزاوية 15. لنوضّح ذلك باستخدام الرموز كما يلي: الشعاع الأول هو ‎(cos 45, sin 45)T‎. الشعاع الثاني هو ‎(cos 60, sin 60)T‎. حاصل الجداء النقطي لهما هو: cos 45 cos 60 + sin 45 sin 60 ‎= cos 45 cos(45 + 15) + sin 45 sin(45+15)‎ ‎= cos 45 (cos 45 cos 15 - sin 45 sin 15) + sin 45 (cos 45 sin 15 + cos 15 sin45)‎ ‎= cos 45 cos 45 cos 15 - cos 45 sin 45 sin 15 + sin 45 cos 45 sin 15 + sin 45 cos 15 sin 45 ‎= cos2 45 cos 15 + sin245 cos 15 ‎= (cos245 + sin245)cos15 ‎= cos 15 استخدامنا في الخطوة رقم 5 القاعدتين التاليتين: cos(x+y) = cos(x) cos(y) - sin(x) sin(y)‎ sin(x+y) = sin(x) cos(y) + cos(x) sin(y)‎ واستخدامنا في الخطوة رقم 9 القاعدة التالية: sin2x + cos2x = 1‎ أوجد جيب تمام Cosine الزاوية بين شعاعي الوحدة المُمثَّلان بالمصفوفتين ‎‎qu = (0.0, 1)T‎ و ‎ru = (0.5, 0.866)T‎: qu · ru = cos θ = 0.866‎ وبالتالي: θ =‫ arc cos 0.866 ‫= 30 درجة. تدريب 2: يظهِر الشكل التالي الشعاعان ‎qu‎ و ‎ru‎ والزاوية بينهما: تذكّر أن: qu · ru = ru · qu‎ يجب أن تكون الزاوية بين الشعاعين 180 درجة أو أقل (حسب التعريف)، ولا يهم ترتيب الأشعة في الجداء النقطي. ملاحظة: من المفيد في بعض الأحيان رسم صورة للتأكد من أن إجابتك منطقية خاصةً عندما تؤشر الأشعة إلى أرباع مختلفة. أوجد جيب تمام الزاوية بين شعاعي الوحدة المُمثَّلان بالمصفوفتين ‎qu = (0.0, 1)T‎ و ‎wu = (-0.5, 0.866)T‎: cos x =‫ 0.866، إذًا يجب أن تكون x =‫ arc cos 0.866 =‫ 30 درجة. لاحظ أن كلا الشعاعين ‎ru‎ و ‎wu‎ يعطيان الإجابة cos 30 عند تطبيق الجداء النقطي عليهما مع الشعاع ‎qu‎، بالرغم من أنهما يقعان على جانبي هذا الشعاع. يوجد شعاعا وحدة يبعدان بمقدار 30 درجة عن الشعاع المعطَى في الفضاء ثنائي الأبعاد، وسيعطي كلاهما النتيجة نفسها للجداء النقطي مع الشعاع المُعطى، لذا يجب أن تكون حذرًا في رسم الأشعة في الحالات الغامضة. الزوايا بين شعاعين التي تكون أكبر من 90 درجة أوجد جيب تمام الزاوية بين شعاعي الوحدة المُمثَّلين بالمصفوفتين ‎qu = (0.0, 1)T‎ و ‎zu = (-0.5, -0.866)T‎: الزاوية بين هذين الشعاعين أكبر من 90 درجة، وبالتالي فإن جيب تمام الزاوية بينهما سالبة، حيث cos θ =‫ ‎-0.866، لأن: θ = arc cos -0.866 = 150‎ الزاوية نفسها تعطي الجداء النقطي نفسه لنحاول الآن رسم الشعاع الآخر الذي جداؤه النقطي مع الشعاع ‎qu‎ يساوي ‎-0.866 كما يلي: الشعاعان اللذان يكون ناتج الجداء النقطي لهما مع الشعاع ‎qu = (0.0, 1)T‎ يساوي ‎-0.866 هما: zu = (-0.5, -0.866)T vu = (0.5, -0.866)T‎ وضعنا في الرسم البياني السابق ذيل هذين الشعاعين عند نقطة الأصل، ولكن تذكّر أن الأشعة ليست ثابتة في أي موقع معين ويمكن رسمها في أيّ مكان مناسب، وأن الجداء النقطي يعطي الزاوية بين منحى كلٍّ من هذين الشعاعين. أوجد الزاوية بين الشعاعين: ‎du = 0.7071(1,1)T‎ و ‎eu = -0.7071(1,1)T‎، وارسم هذين الشعاعين لتوضيح الحالة: 0.7071‎(1, 1)T · -0.7071(1, 1)T = 0.7071(-0.7071)(1+1) = -0.70712 * 2 = -0.5*2 = 1 إذًا الزاوية هي: cos( -1 )‎ =‫ 180 درجة، حيث يوجد شعاع واحد فقط يبعد 180 درجة عن الشعاع المعطى. تدريب عملي أكثر واقعية نادرًا ما يمنحك العالم الحقيقي زوايا سهلة تبلغ 30 درجة و 45 درجة و 60 درجة وما إلى ذلك، لذا إليك تدريبًا أكثر واقعية. أوجد الزاوية المحصورة بين الشعاعين: fu = (0.6, 0.8)T gu = (0.8, 0.6)T قد ترغب أولًا في التأكد من أن هذه الأشعة هي أشعة وحدة فعلًا، لذا ارسم هذه الشعاعين، ثم احسب ناتج الجداء النقطي لهما، واستخدم الدالة arc cos في الآلة الحاسبة لحاسوبك (أو الآلة الحاسبة الحقيقية) للعثور على الزاوية، ولا تنسَ حساب الإجابة بالدرجات وليس بالراديان كما يلي: ناتج الجداء النقطي هو: ‎( 0.6, 0.8 ) · ( 0.8, 0.6 ) = 0.6*0.8 + 0.8*0.6 = 0.48 + 0.48 = 0.96 والزاوية بينهما هي: arc cos( 0.96 )‎ =‫ 16.26 درجة. الأشعة التي ليست أشعة وحدة ليكن لدينا الشعاعان التاليان، ونريد إيجاد الزاوية المحصورة بينهما: j = (3, 4)T k = (0, 2)T نلاحظ أن هذه الأشعة ليست أشعة وحدة، وبالتالي لا يمكنك العثور على الزاوية بينهما دون مزيد من العمل، لذا دعونا نرسم رسمًا بيانيًا لهما أولًا. يمكننا إيجاد الزاوية بين هذين الشعاعين من خلال توحيد Normalize كل منهما وهذا لا يغير منحاهما، وبالتالي أصبح لدينا الآن شعاعا وحدة ويمكن إيجاد جيب تمام الزاوية بينهما باستخدام الجداء النقطي. الزاوية بين الأشعة التي ليست أشعة وحدة لنوجد الآن الزاوية بين الشعاعين j و k: نوجد أولًا طول كل شعاع: | j | = 5.0 | k | = 2.0 ثم نوحّد كلًا منهما، ولكن تذكّر أنه من الحكمة عدم إجراء عملية القسمة في هذه الخطوة في كثير من الأحيان: ju = (3, 4)T / 5.0‎ ku = (0, 2)T / 2.0‎ نحسب الآن الجداء النقطي لهما: ju · ku = (3, 4)T / 5.0 · (0, 2)T / 2.0 = (1/10)(3, 4)T · (0, 2)T = (0.1)(8) = 0.8‎ أخيرًا، نستخدم دالة arc cos في الآلة الحاسبة: arc cos 0.8 = 36.87 درجة لنحسب الآن الزاوية المحصورة بين الشعاعين: ‎a = (10, 5)T‎ و ‎b = (8,12)T‎ مثلًا. نحسب الأطوال أولًا: | a | = √125 | b | = √208 ثم نحسب الجداء النقطي: ‎(10, 5)T / √125 · (8, 12)T / √208 = (108 + 512) / ( √125 √208) = 140 / ( √125 √208) = 140 / √(125*208) = 140 / √26000 = 0.86824 وبالتالي فإن الزاوية هي: arc cos ( 0.86824 ) = 29.745 درجة وصلنا إلى نهاية هذا المقال الذي يحتوي على قدرٍ كبير من الحسابات لإيجاد الزاوية بين شعاعين في فضاء ثنائي الأبعاد، وسنتعرّف في المقال التالي على كيفية إيجاد الزاوية بين شعاعين في فضاء ثلاثي الأبعاد. ترجمة -وبتصرُّف- للفصل The Angle between Two Vectors من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D خاصية الاتجاه Direction للأشعة الهندسية في التصاميم ثلاثية الأبعاد خاصية الطول Length للأشعة الهندسية في التصاميم ثلاثية الأبعاد
  20. يحتاج التحقق من البريد الإلكتروني والرد عليه الكثير من الوقت، ولا يمكنك كتابة برنامج للتعامل مع جميع رسائل بريدك الإلكتروني، إذ تتطلب كل رسالة ردًا خاصًا بها، ولكن يمكنك أتمتة الكثير من المهام الأخرى المتعلقة بالبريد الإلكتروني بعد أن تعرف كيفية كتابة البرامج التي يمكنها إرسال واستقبال رسائل البريد الإلكتروني. قد يكون لديك مثلًا جدول بيانات يحتوي على الكثير من سجلات العملاء وتريد إرسال رسالة تحتوي على استمارة 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
  21. تعرّفنا في المقال السابق على كيفية الحصول على الوقت باستخدام الوحدتين 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 مشاريع بايثون عملية تناسب المبتدئين
  22. قد تشغّل برامجك أثناء جلوسك أمام الحاسوب، ولكنك ستفضّل تشغيلها دون إشرافك المباشر، إذ يمكن لساعة حاسوبك جدولة البرامج لتشغيل الشيفرة البرمجية في وقت وتاريخ محدَّد أو على فترات زمنية منتظمة، فمثلًا يمكن أن يستخلص 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 في بايثون مصطلحات بايثون البرمجية
  23. سنوضّح في هذا المقال حقيقتين مهمتين، أولهما أن الجداء النقطي لمصفوفة عمودية مع نفسها يعطي مربع طول الشعاع الذي تمثله، وثانيهما أن الجداء النقطي لمصفوفتين عموديتين تمثلان شعاعين متعامدين يساوي الصفر. سنوضح في هذا المقال المواضيع التالية: الجداء النقطي وطول الشعاع. شعاع الوحدة Unit Vector. شعاع الوحدة الناظم Unit Normal. الجداء النقطي للأشعة المتعامدة ومصفوفاتها العمودية. كيفية بناء شعاع متعامد مع شعاع معين. تعريف ميل Slope الخط ثنائي الأبعاد. العلاقة بين ميول الخطوط المتعامدة ثنائية الأبعاد. صيغة الجداء النقطي المتعلقة بطول الشعاع لنفترض أن لدينا ‎v = ( 3, 4 )T‎، فأوجد ناتج الجداء النقطي v · v وطول الشعاع v: ‎v · v = ( 3, 4 )T · ( 3, 4 )T = 32 + 42 = 9 + 16 = 25 = 52‎ طول v ‫= 5 لأنه جزء من المثلث قائم الزاوية 3/4/5. رأينا في المقال السابق أن الجداء النقطي لشعاع مع نفسه هو: ‎(x, y, z)‎T · (x, y, z)T = x2 + y2 + z2 وتوجد طريقة أخرى لكتابة ذلك وهي: v · v = | v |2‎ ناتج الجداء النقطي لمصفوفة عمودية مع نفسها هو عدد حقيقي (مقدار سلمي) Scalar، وهو مربع طول الشعاع الذي تمثله. ملاحظة: إذا استخدمتَ الإحداثيات المتجانسة Homogeneous Coordinates، فستحتاج هذه العملية الحسابية إلى التعديل بعض الشيء، وتذكّر أن الطول هو خاصية للشعاع الهندسي، وليس خاصية للمصفوفة العمودية التي يمكن استخدامها لتمثيل الشعاع. لنوجد الآن طول الشعاع الذي تمثله المصفوفة العمودية ‎( 2, 1, -1)T‎: ناتج الجداء النقطي هو 4+1+1 = 6، وبالتالي فإن الطول هو ‎√6. خاصيات الجداء النقطي المتعلقة بطول الشعاع إليك بعض الخاصيات الجداء النقطي المتعلقة بطول الشعاع: إذا غيّرتَ حجم شعاع أو ضربته مع عدد حقيقي k، فإنك تضرب طوله بهذه القيمة k: ‎(kv)·(kv) = k2 v 2 وبالتالي | k v | = k| v | طول الشعاع الصفري هو صفر: ‎| 0 | = 0 ناتج الجداء النقطي للشعاع الصفري مع أي شعاع آخر هو صفر: 0‎ · v = 0 لا يغيّر عكس اتجاه الشعاع من طوله: | -v | = | v | أوجد طول الشعاع ‎(30, 40)T‎: ‎(30, 40)T = 10(3, 4)T‎، وبالتالي يكون الطول 10*5 = 50. أو يمكنك حسابه بطريقة أصعب كما يلي: ‎|(30,40)|2 = 900 + 1600 = 2500 والطول = ‎√2500 ‫= 50 أشعة الوحدة Unit Vectors تذكّر أن شعاع الوحدة هو شعاع بطول 1، وتسمَّى عملية إنشاء شعاع وحدة له اتجاه الشعاع المُعطَى نفسه بالتوحيد Normalizing التي تُعطَى بالصيغة التالية (كما ناقشنا في مقال سابق): vu = v / | v | تُطبَّق هذه الصيغة على الأشعة والمصفوفات العمودية التي تمثلها، إذ يمكن حساب الطول | v | باستخدام الجداء النقطي للمصفوفات العمودية. ملاحظة: لاحظ أننا استخدمنا مصطلح "Normal" ، ولكن يجب الانتباه إلى أن لها معانٍ متعددة، إذ يعني مصطلح التوحيد Normalizing إنشاءُ شعاع الوحدة في اتجاه الشعاع الأصلي نفسه، بينما يُطلَق على الشعاع العمودي على سطح معين أحيانًا اسم "الشعاع الناظم Normal Vector"، ولكنه ليس شعاع الوحدة بالضرورة، لذا احرص على ألّا تقول أبدًا "الشعاع الناظم" عندما تقصد "شعاع الوحدة". إبقاء طول الشعاع صريحًا عند حساب شعاع الوحدة جرّب إجراء عملية التوحيد للشعاع الذي تمثله المصفوفة العمودية ‎( 1.2, -4.2, 3.5 )T‎: ناتج الجداء النقطي هو: ‎( 1.2, -4.2, 3.5 )T · ( 1.2, -4.2, 3.5 )T = 1.22 + (-4.2)2 + 3.52 = 31.33 ونمثّل شعاع الوحدة كما يلي: ‎( 1.2, -4.2, 3.5 )T / √31.33. = (0.2144, -0.7504, 0.6253)T يجب عليك ترك الإجابة بالصيغة الأولى السابقة دون إجراء عملية القسمة عند الحل بالورقة والقلم، إذ ستوفر لك الحسابات اللاحقة التي ستجريها في كثير من الأحيان فرصة لإلغاء الطول، إذا تركته صريحًا كما في الصيغة الأولى. تمثيل أشعة الوحدة للاتجاه أجب عن الاسئلة التالية (قد ترغب في التفكير في الأشعة ثنائية الأبعاد أثناء إجابتك، بالرغم من أن الإجابات ستكون صحيحة بالنسبة للأشعة ثلاثية الأبعاد أيضًا): كم عدد الأشعة التي يمكن أن تكون في اتجاه معين؟ متعددة بصورة لا نهائية. كم عدد أشعة الوحدة التي يمكن أن توجد في اتجاه معين؟ شعاع واحد فقط. يمكن أن يكون هناك عدد لا نهائي من الأشعة في اتجاه معين لأن عناصر الشعاع هي أعداد حقيقية، لذلك يوجد عدد لا نهائي من الأشعة التي تحاذي المحور x في الفضاء ثنائي الأبعاد مثلًا، وتبدو المصفوفات العمودية لهذه الأشعة بالصورة ‎(s, 0)T‎. يؤشّر شعاع الوحدة لشعاع معين إلى اتجاه هذا الشعاع نفسه، ولا يوجد سوى شعاع وحدة واحد فقط في اتجاه معين، وتوجد قيمة واحدة فقط للتعبير ‎v / | v |‎، ويوجد شعاع وحدة واحد فقط في اتجاه x الموجب، وتمثله المصفوفة العمودية ‎( +1, 0)T‎. ينطبق الشيء نفسه على المناحي Orientations والأبعاد الأخرى. يظهِر الشكل التالي أشعة لها أطوال مختلفة والمنحى نفسه، ولكن يكون أحدها فقط هو شعاع الوحدة. تذكّر الصيغة التالية لحساب زاوية الشعاع: الزاوية = arc tan( y/x )‎ ولكن ليست هذه الصيغة مفيدة جدًا في الفضاء ثلاثي الأبعاد، إذ لا يكفي تحديد الزاوية بين الشعاع وأحد المحاور فقط عندما يكون هناك ثلاثة محاور. يُستخدَم شعاع الوحدة لشعاع معين في أغلب الأحيان للتعبير عن اتجاه هذا الشعاع، حيث يوجد شعاع وحدة واحد فقط، لذا فإن تعبيره عن الاتجاه فريد من نوعه، وينطبق الشيء نفسه على جميع الأبعاد. ليكن لدينا الشعاع ‎v = ( -3, 2, 4)T‎ مثلًا، حيث يمكننا معرفة اتجاهه من خلال حساب شعاع الوحدة كما يلي: v| v |2 = 9 + 4 + 16 = 29 ‎vu = v / √29 الجداء النقطي للأشعة التي بينهما زاوية قائمة إذا كان لدينا شعاعان موجَّهان بحيث يكون بينهما زاوية مقدارها 90 درجة مثل أن يكون أحدهما يحاذي المحور x والآخر يحاذي المحور y كما في الشكل التالي، فستكون نتيجة جدائهما النقطي هي 0: إذًا لنوجد الجداء النقطي للشعاعين ‎r = (6, 0)T‎ و ‎s = (0, 😎T‎ كما يلي: ‎(6, 0)T · (0, 😎T = 6*0 + 0*8 = 0 لنفترض أننا دوّرنا الشعاعين السابقين بزاوية 45 درجة كما في الشكل السابق ليصبحا كما يلي (لا تقلق بشأن كيفية تدوير الأشعة، إذ سنوضّحه في مقال لاحق): r' = ( 6cos(45), 6sin(45) )T s' = ( -8sin(45), 8cos(45) )T ستبقى الزاوية بينهما بعد تدويرها تساوي 90 درجة، وبالتالي سيكون الجداء النقطي لهما 0 أيضًا. أوجد الجداء النقطي للشعاعين: r' = ( 2cos(45), 2sin(45) )T s' = ( -3sin(45), 3cos(45) )T نطبّق الجداء النقطي كما يلي: r' · s' = 2cos(45) * (-3)sin(45) + 2sin(45) * 3cos(45) = -2cos(45) * (3)sin(45) + 2sin(45) * 3cos(45) = -6cos(45) * sin(45) + 6cos(45) * sin(45) = 0‎ الجداء النقطي للأشعة المتعامدة يبدو من المعقول أن يكون حاصل الجداء النقطي لشعاعين هو نفسه بعد تدويرهما بالمقدار نفسه، وبالتالي نستنتج أن الجداء النقطي لشعاعين متعامدين هو صفر، أي أن الجداء النقطي للمصفوفتين العموديتين اللتين تمثلهما هو الصفر. يهمنا فقط المنحى النسبي، حيث إذا كان الشعاعان متعامدين، فسيكون حاصل الجداء النقطي لهما صفرًا، بغض النظر إن تقاطعا أم لا، إذ ليس من الضروري أن يتقاطع شعاعان ليكونا متعامدين، فمن غير المنطقي التحدث عن تقاطع شعاعين بما أن الأشعة ليس لها موقع محدد، وهذه هي النتيجة نفسها التي رأيناها مع الأشعة الهندسية. يمكننا القول أن الشعاعين التاليين متعامدان لأن حاصل الجداء النقطي لهما يساوي الصفر: q = ( -5, 3 )T‎ r = ( 3, 5 )T عدم اعتماد الجداء النقطي للأشعة المتعامدة على الطول يمكننا تشكيل مصفوفة عمودية ثنائية الأبعاد متعامدة مع مصفوفة أخرى من خلال تبديل العناصر وتغيير إشارة أحدها، ولكن تعمل هذه الطريقة فقط في الفضاء ثنائي الأبعاد، حيث يمكنك العثور على عدد لا حصر له من الأشعة المتعامدة مع شعاع محدَّد مثل الأشعة التالية المتعامدة مع الشعاع ‎( -5, 3)T‎: ( 3, 5 )T ( -3, -5)T ( 1.5, 2.5)T +( 6, 10)T …. وغيرها إذا كان الشعاع u متعامدًا مع الشعاع v، فإن الجداء النقطي ‎u · v = 0، وبالتالي ‎(k u) · v = k(u · v)= 0 لأي عدد حقيقي k، إذًا هناك عدد لا نهائي من الأشعة (k u‎) المتعامدة مع الشعاع v. قد نرغب في العثور على شعاع الوحدة الناظم Unit Normal لشعاع معين الذي هو شعاع: متعامد (ناظم) مع الشعاع المعطى. يبلغ طوله 1. ملاحظة: تذكر ألّا تخلط بين مفهومي التوحيد Normalizing (إنشاء شعاع وحدة في اتجاه الشعاع نفسه)، وحساب شعاع الوحدة الناظم (جعل شعاع الوحدة في اتجاه متعامد مع الشعاع). لنحسب الآن شعاع الوحدة الناظم للشعاع ‎(-5, 3)T‎: ‎(3, 5)/( √34)‎‎، أو يمكنك اختيار الخيار الآخر: ‎(-3, -5)/( √34)‎. عدد لا نهائي من الأشعة الناظمة الأشعة في الفضاء ثلاثي الأبعاد لها أيضًا أشعة وحدة ناظمة، ولكن لا يُعَد شعاع الوحدة الناظم لشعاع معين فريدًا، فمثلًا تكون جميع القضبان متعامدةً مع المحور في عجلة العربة كما في الشكل السابق، أي أنّ جميع الخطوط في مستوي هذه القضبان متعامدة مع المحور، وبالتالي هناك عدد لا نهائي منها. لنوجد عددًا من الأشعة الناظمة للشعاع الذي تمثله المصفوفة العمودية ‎(1, 2, 2)T‎ كما يلي: ( 0, 0, 0 ) ( -2, 1, 0 ) ( 0, -1, 1 ) ( -4, 1, 1 ) ( 0, 1, -1 ) … وغيرها الكثير بعدد لا نهائي جميع الأشعة السابقة هي أشعة ناظمة للشعاع المعطى، لأن حاصل الجداء النقطي لكل منها مع هذا الشعاع يساوي الصفر، ولكنها ليست أشعة وحدة، وإن أردتَ إيجاد أشعة الوحدة الناظمة، فسيكون هناك عدد لا نهائي منها أيضًا. الشعاع الصفري متعامد مع جميع الأشعة لاحظ الشعاع الناظم الأول الذي هو الشعاع الصفري في المثال السابق، فحاصل الجداء النقطي للشعاع الصفري مع الشعاع المُعطَى هو الصفر، لذلك يجب أن يكون الشعاع الصفري متعامدًا مع هذا الشعاع. تستخدم كتب الرياضيات في أغلب الأحيان حقيقة أن الشعاع الصفري متعامد مع جميع الأشعة (من النوع نفسه)، ولكن لن نحتاج إلى هذه الحقيقة حاليًا، بل يمكن أن نحتاجها في المقالات اللاحقة. طول الشعاع الصفري هو صفر، لذا فهو ليس شعاع وحدة ناظم. قد تتذكر من مادة الهندسة التحليلية الصيغة التالية لميل خطٍ في فضاء ثنائي الأبعاد وغير موازٍ للمحور Y: m‎‎ = (التغير في قيمة y) / (التغير في قيمة x) هذه هي الصيغة نفسها المُستخدَمة في ظل الزاوية مع المحور x، وترتبط هذه الصيغة بالسبب الذي يجعل الجداء النقطي للأشعة المتعامدة ثنائية الأبعاد يساوي صفرًا. ميول الخطوط المتعامدة لنحاول الإجابة على الأسئلة التالية: ما هو الميل m1‎ للشعاع ‎( 2, 5 )T‎؟ m1 = 5/2 ما هو الميل m2‎ للشعاع ‎( -5, 2 )T‎؟ m2 = -2/5 ما هو ناتج الجداء النقطي لهما؟ ‎(2)(-5) + (5)(2) = 0 ما هو ناتج ضرب m1‎ و m2‎؟ 5/2‎ * -2/5 = -1 قد تتذكر الحقيقة التالية من دروس الرياضيات السابقة: إذا كان المستقيمان متعامدين فإن حاصل ضرب ميليهما يساوي ‎-1. إليك ما يحدث عندما ننشئ شعاعًا ناظمًا لشعاع ثنائي الأبعاد من خلال تبديل العناصر وتغيير إشارة أحدها: إذا كان ‎v = ( x, y )T‎. فإن الشعاع المعامد له هو ‎v' = ( -y, x )T‎. لأن الجداء النقطي ‎( x, y)T · ( -y, x )T‎ هو ‎-xy + yx = 0. ميل الشعاع ‎( x, y )T‎ هو y/x. ميل ‎( -y, x )T‎ هو ‎-x/y. حاصل ضرب الميلين هو: y/x * -x/y = -(xy)/(xy) = -1. إذًا هل يُعَد الشعاعان ‎( -1.5, 6)T‎ و ‎(2, 2)T‎ متعامدين؟ لا، لأن ناتج الجداء النقطي لهما هو ‎-3 + 12 = 9. ناتج الجداء النقطي ليس صفرًا، لذا فإن الشعاعين ليسا متعامدين، وقد توصّلنا إلى هذه النتيجة بغض النظر عن الأطوال، إذ يمكن اكتشاف تعامد الأشعة بغض النظر عن طولها. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على علاقة الجداء النقطي للأشعة مع طولها وتعامدها، وسنناقش في المقال التالي الجداء النقطي غير الصفري لشعاعين. ترجمة -وبتصرُّف- للفصل Length and the Dot Product من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D خاصية الاتجاه Direction للأشعة الهندسية في التصاميم ثلاثية الأبعاد خاصية الطول Length للأشعة الهندسية في التصاميم ثلاثية الأبعاد
  24. تعلّمنا في المقال السابق كيفية استخراج النص من مستندات 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
  25. تُعَد مستندات بي دي إف 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
×
×
  • أضف...