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

برمجة عملاء ويب باستخدام بايثون


أسامة دمراني

سنغطي في هذا المقال من سلسلة تعلم البرمجة ما يلي:

  • إرسال الطلبات إلى خادم الويب.
  • إرسال البيانات إلى الخادم.
  • معالجة HTML بواسطة html.parser.
  • الخيارات الأخرى.

برمجة عملاء الويب

يمكن وصف برمجة عملاء الويب في تعريفين رئيسيين هما:

  1. استخدام جافاسكربت لتعديل محتوى صفحة الويب داخل المتصفح، بتغيير ألوان العناصر وتحريك اللوحات حولها، وإظهار العناصر أو إخفائها، وجلب أجزاء من البيانات الأولية من الخادم، مثل حالة البحث الحي live search مثلًا. وهي ليست معقدةً، لكنها تتطلب معرفةً عميقةً بكل من HTML و CSS، ومن ثم فهي خارج نطاق شرحنا، فإذا أردت معرفة المزيد عن مثل هذا النوع من برمجة عملاء الويب فانظر سلسلة مدخل إلى html5 مثلًا في أكاديمية حسوب، وكتاب نحو فهم أعمق لتقنيات HTML5، كما توجد عدة مكتبات وأطر عمل لجافاسكربت يجب البحث فيها وتعلمها، لعل أشهرها jQuery، والتي يمكن القراءة عنها في توثيق jQuery في موسوعة حسوب، وغيرها من السلاسل والمواد العلمية في الأكاديمية والموسوعة.
  2. إنشاء برنامج يعمل على حاسوب ويتصل بخادم ويب متظاهرًا بأنه برنامج متصفح ويب، حيث يجلب هذا البرنامج بعض البيانات للتحليل، وهو ما يسمى بالبوت bot -اختصار robot-، أو يتبع بعض الروابط من موقع لآخر بحثًا عن بيانات تتعلق بموضوع ما، وهو ما يسمى بعناكب الويب web spyders أو زاحفات الوب، وسندرس بعض هذه الأنواع من برامج عملاء الويب في هذا المقال.

التعلم بالتطبيق

سنخالف في هذا المقال النهج الذي كنا نتبعه في شرحنا، إذ سنستخدم مثالًا عمليًا لتوضيح كيفية كتابة برنامج عميل ويب، وسنبدأ ببساطة في البداية، ثم نضيف المزايا لإبراز بعض المشاكل المعقدة بالتدريج، لكنه سيظل برنامجًا بسيطًا للغاية عند تمامه، لكن القصد منه أن يعطيك فكرةً عما يمكن تنفيذه بمثل تلك البرامج.

مهمة هذا التدريب هي بناء برنامج يولد صفحة ويب جديدةً، تحتوي على تجميعة من جميع محتويات قائمة "سنغطي في هذا المقال (What will we cover?‎)" ونحتاج إلى تنفيذ بضعة أمور لبناء ذلك البرنامج:

  1. قراءة محتوى HTML لملف الفهرس.
  2. تحليل محتويات الصفحة، واستخراج جميع الروابط الموجودة في الجزء الأيسر إلى قائمة.
  3. جلب محتوى HTML الخاص بكل رابط من روابط تلك القائمة.
  4. استخراج قائمة النقاط الموجودة في قسم "What will we cover" إلى قائمة جديدة.
  5. توليد صفحة HTML باستخدام البيانات التي جمعناها.

جلب المحتوى

رأينا سابقًا كيفية جلب صفحة HTML بسيطة من خادم باستخدام الوحدة urllib.request:

import urllib.request as url
site = url.urlopen('http://www.alan-g.me.uk/l2p2/index.htm')
page = site.read()

لدينا الآن سلسلة نصية تحتوي على شيفرة HTML الخاصة بالصفحة الرئيسية، أو صفحة المستوى الأعلى للنسخة الأجنبية من الفصول، وسنلاحظ صعوبة قراءة الخرج، لهذا نستخدم خيار عرض المصدر view source الموجود في المتصفح لننظر في شيفرة HTML، لنرى أن جدول المحتويات موجود في زوج من وسوم nav -اختصار للتنقل navigation- وأن كل قائمة من قوائم الفصول الموجودة في كل قسم تُجمع في قائمة غير مرتبة unordered list، تحمل الوسم ul، ونميز كل عنصر فيها بوجود وسم li فيه، وتمثل جميع عناصر القائمة روابط تشعبيةً إلى ملفات الفصول، لهذا نجدها محاطةً بوسم الرابط التشعبي <a>.

مهمتنا التالية استخراج جميع عناصر <a> داخل لوحة nav من الصفحة.

استخراج محتوى الوسوم

ذكرنا في المقدمة أننا نستطيع استخدام عمليات البحث النصية البسيطة للعثور على الوسوم وغيرها في صفحات الويب، لكن يفضل استخدام محلل HTML مناسب، لذا سنستخدم الصنف HTMLParser الموجود في وحدة المكتبة القياسية html.parser، وهو محلل لغوي مجرَّد abstract، مدفوع بالأحداث أو حدَثي event-driven، ويجب أن نقسمه إلى أصناف فرعية subclasses لتوفير المزايا التي نريدها، مع ملاحظة أنه يستدعي تابعين، الأول هو handle_starttag()‎ عند كل وسم HTML افتتاحي، والثاني هو handle_endtag()‎ عند كل وسم إغلاق، ويجب أن نوفر النسخ الخاصة بنا من تلك التوابع لتنفذ الإجراءات المناسبة عند العثور على الوسوم التي نريدها، ونريد في حالتنا هذه أن نجد كل الروابط الموجودة في لوحة nav، لذا يجب تعيين راية flag نسميها in_nav في كل مرة نعثر فيها على وسم nav، فإذا وجدنا وسم a وكانت الراية True فسنحفظ خاصية href في قائمة، حيث تُمرَّر الخاصيات إلى التابع في صف tuple من ثنائيات المفتاح/القيمة، ونريد أخيرًا التقاط وسم الإغلاق ‎/nav، وإعادة تعيين الراية إلى False، لضمان أننا لا نجمع أي روابط من خارج لوحة المحتويات، وعليه سنعِدّ المحلل اللغوي، ونتأكد من قدرته على تعرّف الوسوم الثلاثة المطلوبة باستخدام تعليمات الطباعة كما يلي:

import urllib.request as url
import html.parser

class LinkParser(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []  #list to store the links
        self.is_nav = False  # the flag

    def handle_starttag(self, name, attributes):
        if name == 'nav':
            print("We found a <nav> tag")
        elif name == 'a':
            print("We found an <a> tag")

    def handle_endtag(self, name):
        if name == 'nav':
            print("We found a </nav> tag")

site = url.urlopen('http://www.alan-g.me.uk/l2p2/index.htm')
page = site.read().decode('utf8') # convert bytes to str

parser = LinkParser()
parser.feed(page)

عند تشغيل الشيفرة السابقة ستجلب الصفحة، وتحللها، ثم تطبع الرسائل الموافقة لكل وسم تجده، وهي بهذا تثبت أن المحلل يعمل بكفاءة، وأننا بحاجة إلى راية is_nav لأن لدينا رابطًا خارج لوحة nav، نستطيع الآن أن نغير تعليمات الطباعة للشيفرة الحقيقية التي نريدها:

import urllib.request as url
import html.parser

class LinkParser(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []  # قائمة لتخزين الروابط
        self.is_nav = False  # الراية

    def handle_starttag(self, name, attributes):
        if name == 'nav':
            self.is_nav = True
        elif name == 'a' and self.is_nav:
            for key,val in attributes:
                if key == "href":
                    self.links.append(val)

    def handle_endtag(self, name):
        if name == 'nav':
            self.is_nav = False

site = url.urlopen('http://www.alan-g.me.uk/l2p2/index.htm')
page = site.read().decode('utf8') # حوِّل سلسلة البايت إلى سلسلة نصية

parser = LinkParser()
parser.feed(page)
print(parser.links)

نلاحظ هنا أن التغييرات التي أجريناها كانت في التابعين handle_starttag()‎ وhandle_endtag()‎، إضافةً إلى السطر الأخير في الشيفرة print(parser.links)‎، ويجب أن تصبح النتيجة كما يلي:

['tutintro.htm', 'tutneeds.htm', 'tutwhat.htm', 'tutstart.htm', 'tutseq1.htm', 
'tutdata.htm', 'tutseq2.htm', 'tutloops.htm', 'tutstyle.htm', 'tutinput.htm', 
'tutbranch.htm', 'tutfunc.htm', 'tutfiles.htm', 'tuttext.htm', 'tuterrors.htm', 
'tutname.htm', 'tutregex.htm', 'tutclass.htm', 'tutevent.htm', 'tutgui.htm', 
'tutrecur.htm', 'tutfctnl.htm', 'tutcase.htm', 'tutpractice.htm', 'tutdbms.htm', 
'tutos.htm', 'tutipc.htm', 'tutsocket.htm', 'tutweb.htm', 'tutwebc.htm', 
'tutwebcgi.htm', 'tutflask.htm', 'tutrefs.htm']

استخراج النقاط من الفصول

بعد أن حصلنا على قائمة الفصول نريد إنشاء دالة تستطيع استخراج النقاط التي في أول كل صفحة، لذا سنستخدم خاصية View Source الموجودة في المتصفح مرةً أخرى، إذ نفحص شيفرة HTML الخاصة بإطار الفصل، فنرى أننا ننظر في مجموعة من عناصر القائمة الموجودة داخل وسم div، مع تعيين الخاصية class على القيمة "todo"، وهذا يشبه تقريبًا ما فعلناه عند بحثنا عن الروابط، وسننشئ الآن دالةً تأخذ سلسلة HTML وتعيد قائمةً من سلاسل li، وسنضيف اسم الملف في الشيفرة ونثبته ليكون tutstart.htm.

import urllib.request as url
import html.parser

class BulletParser(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.in_todo = False
        self.is_bullet = False
        self.bullets = []

    def handle_starttag(self, name, attributes):
        if name == 'div':
           for key, val in attributes:
               if key == 'class' and val == 'todo':
                   self.in_todo = True
        elif name == 'li':
            if self.in_todo:
               self.is_bullet = True

    def handle_data(self, data):
        if self.is_bullet:
            self.bullets.append(data)
            self.is_bullet = False  # أعد تعيين الراية

    def handle_endtag(self, name):
        if name == 'div':
            self.in_todo = False

topic_url = "http://www.alan-g.me.uk/l2p2/tutstart.htm"

def get_bullets(aTopic):
    site = url.urlopen(aTopic)
    topic = site.read().decode('utf8')
    topic_parser = BulletParser()
    topic_parser.feed(topic)
    return topic_parser.bullets

print( get_bullets(topic_url) )

لاحظ أن لدينا تابع معالجة حدث إضافي سنعيد تعريفه، وهو handle_data()‎، لأننا نريد استخراج البيانات الموجودة في وسوم <li> بدلًا من الوسم نفسه أو خصائصه، وفيما عدا ذلك تشابه هذه الشيفرة المثال السابق، حيث نعين الراية in_todo لتوضيح متى نكون داخل صندوق المحتويات، وكذلك الراية is_bullet التي تشير إلى أننا وجدنا عنصر قائمة داخل الصندوق، ثم نعيد تعيين راية is_bullet بمجرد قراءتنا للبيانات، ونعيد تعيين is_todo عندما نترك الصندوق، أي عند ‎</div>‎.

يمكن دمج البرنامجين معًا، بإضافة الصنف والدالة الجديدين إلى الملف السابق، ويتبقى أن نكتب حلقة for للتكرار على الروابط من المحلل الأول وإرسالها إلى دالة get_bullets()‎، ثم تُجمع النتائج في قاموس عام global dictionary مرتب وفق الفصول، كما سننظمها أكثر من خلال إنشاء دالة get_topics()‎ التي تشبه get_bullets()‎، يمكن أن نضيف شيئًا لمعالجة الأخطاء هنا، ونحوله إلى تنسيق وحدة في نفس الوقت، كما يلي:

import urllib
import urllib.request as url
import html.parser

###### Link handling code  ####

class LinkParser(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []  #list to store the links
        self.is_nav = False  # the flag

    def handle_starttag(self, name, attributes):
        if name == 'nav':
            self.is_nav = True
        elif name == 'a' and self.is_nav:
            for key,val in attributes:
                if key == "href":
                    self.links.append(val)

    def handle_endtag(self, name):
        if name == 'nav':
            self.is_nav = False

def get_topics(aSite):
    try:
        site = url.urlopen(aSite)
        page = site.read().decode('utf8') # convert bytestring to str
        link_parser = LinkParser()
        link_parser.feed(page)
        return link_parser.links
    except urllib.error.HTTPError:
        return []


##### Bullet handling code

class BulletParser(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.in_todo = False
        self.is_bullet = False
        self.bullets = []

    def handle_starttag(self, name, attributes):
        if name == 'div':
           for key, val in attributes:
               if key == 'class' and val == 'todo':
                   self.in_todo = True
        elif name == 'li':
            if self.in_todo:
               self.is_bullet = True

    def handle_data(self, data):
        if self.is_bullet:
            self.bullets.append(data)
            self.is_bullet = False  # reset the flag

    def handle_endtag(self, name):
        if name == 'div':
            self.in_todo = False

def get_bullets(aTopic):
    try:
        site = url.urlopen(aTopic)
        topic = site.read().decode('utf8')
        topic_parser = BulletParser()
        topic_parser.feed(topic)
        return topic_parser.bullets
    except urllib.error.HTTPError:
        return []

#### driver code  ####
if __name__ == "__main__":
    summary = {}
    site_root = "http://www.alan-g.me.uk/l2p2/"

    the_topics = get_topics(site_root+'index.htm')

    for topic in the_topics:
        topic_url = site_root + topic
        summary[topic] = get_bullets(topic_url)

    print(summary['tutdata.htm'])

لا زال بإمكاننا إجراء المزيد من التنظيم والترتيب، من خلال أخذ اثنين من أصناف المحلل والدوال المرتبطة بهما إلى وحدات منفصلة، لكن نترك هذا تدريبًا لك، لذا أنشئ الوحدتين linkparser.py و bulletparser.py، واستوردهما إلى الشيفرة المتبقية التي تستطيع حفظها باسم topic_summary.py، ويجب أن يبدو الملف الأخير أشبه بما يلي:

import linkparser as LP
import bulletparser as BP

if __name__ == "__main__":
    summary = {}
    site_root = "http://www.alan-g.me.uk/l2p2/"

    the_topics = LP.get_topics(site_root+'index.htm')

    for topic in the_topics:
        topic_url = site_root + topic
        summary[topic] = BP.get_bullets(topic_url)

    print(summary['tutdata.htm'])

إنشاء صفحة الملخص

بقي لمشروعنا مهمة واحدة، وهي أخذ البيانات التي جمعناها وإنشاء صفحة ويب لعرضها فيها، وهي أكثر من مجرد إنشاء ملف بسيط ومعالجة نصوص، مثلما فعلنا في مثال القائمة الذي شرحناها في مقال التعامل مع الملفات في البرمجة، فالتعقيد الوحيد هنا هو الحاجة إلى استخدام وسوم HTML لتنسيق البيانات، والذي ننفذه بكتابة بعض ترويسات HTML الثابتة، ثم إنشاء حلقة تتكرر على بيانات الفصول لتطبع قائمةً من الفصول والنقاط bulletins، ثم نختم بوسم تذييل HTML ثابت أيضًا، ونغلق الملف، ثم نفتح الملف في المتصفح لرؤية الصفحة النهائية، كما يلي:

import linkparser as LP
import bulletparser as BP
import time

def create_summary(filename,data):
    with open(filename, 'w') as outf:
        # اكتب الترويسة
        outf.write('''<!Doctype htm>
<html><body>
<h1>Summary of tutor topics</h1>
<dl>''')

        # اكتب اسم كل فصل...
        for topic in data:
            outf.write('<dt>%s</dt>' % topic)
            # ...and its bullets
            for bullet in data[topic]:
                outf.write("<dd>%s</dd>" % bullet)

        # اكتب التذييل
        outf.write('''
</dl>
</body></html>''')

if __name__ == "__main__":
    summary = {}
    site_root = "http://www.alan-g.me.uk/l2p2/"
    summary_file = './topic_summary.htm'

    the_topics = LP.get_topics(site_root+'index.htm')

    for topic in the_topics:
        topic_url = site_root + topic
        summary[topic] = BP.get_bullets(topic_url)
        time.sleep(1) # DOS تعطيل رؤية الخادم له على أنه هجمة

    create_summary(summary_file, summary)
    print('OK')

يجب أن نحصل عند تشغيل الشيفرة على ملف اسمه topic_summary.htm نستطيع فتحه في المتصفح، ويجب أن يكون مثل هذه الصفحة.

إرسال البيانات في الطلب

نحتاج أحيانًا إلى تنفيذ أمور أكثر من مجرد جلب ملف ثابت من الخادم، فقد نرغب في تسجيل الدخول، ونحتاج إلى إرسال اسم المستخدم وكلمة المرور أو بيانات اعتماد أخرى، أو قد نستخدم وسيلة بحث لجلب بعض الروابط، وتوجد طريقتان لإرسال البيانات إلى الخادم وفقًا لنوع رسالة HTTP المتوقعة.

رأينا من قبل أن HTTP تحتوي على العديد من الرسائل التي يمكن إرسالها، وقلنا إن أكثر تلك الرسائل استخدامًا هي GET وPOST، حيث ترسل GET جميع بياناتها في سطر العنوان، ونستطيع رؤية ذلك في المتصفح في صورة المحارف الغريبة التي تحتوي على الكثير من علامات الاستفهام وإشارات التساوي، فهي قيم البيانات التي في طلب GET.

إرسال سلسلة بحث إلى GitHub

تنقسم برمجة عملاء الويب web clients عمليًا إلى جزء علمي وفق قواعد معروفة مسبقًا ونتوقع نتائجها، وجزء يأتي بالتجربة والخطأ، ويمكن تعلم الكثير عن موقع ويب بزيارته من خلال متصفح ويب، وإلقاء نظرة متفحصة في كل من شريط العنوان ومصدر الصفحات، فإذا زرنا مستودع الشيفرات مفتوحة المصدر في GitHub مثلًا، وبحثنا عن كلمة "python" فسنرى أن عنوان الصفحة المعادة يكون أشبه بما يلي:

https://github.com/search?utf8=%E2%9C%93&q=python&type=

يشير الجزء utf8=%E2%9C%93 إلى محرف يونيكود -هو '- الذي يخبرنا أن البحث يستخدم UTF-8، ونستطيع تجاهل هذا التفصيل وكذلك type=‎ الفارغة في النهاية، ونرسل ما يلي:

http://github.com/search?q=python

وسنرى أنها تعطينا نفس النتيجة.

نستطيع اكتشاف نوع هيكل HTML المُعاد من خلال عرض مصدر الصفحة، وخصائص العنصر element properties باستخدام أدوات الفحص الخاصة بالمتصفح inspection tools، ونستطيع إجراء البحث النصي لشاشة مصدر الصفحة في حالة GitHub ونحصل منه على أول نتيجة -والتي كانت geekcomputers/Python عند بحثنا-، والتي بدت كما يلي:

<div class="repo-list-item d-flex flex-justify-start py-4 public source">
  <div class="col-8 pr-3">
    <h3>
      <a href="/geekcomputers/Python" class="v-align-middle">geekcomputers/<em>Python</em></a>
    </h3>
...

نرى هنا كيفية استخراج الروابط، وصعوبته أكثر مما في مثال topic_summary.htm السابق.

لا شك أن المشاكل لا حصر لها، وسنجد للمواقع آليات تمنع كل شيء -عدا المتصفحات- من الوصول إلى بياناتها، أو تستخدم تقنيات جافاسكربت متطورةً لعرض الصفحة، ولن تفلح تقنيات تحليل HTML البسيطة معها، لكن بنظرةً متفحصةً في تلك المواقع سنرى أنها توفر واجهة برمجة تطبيقات API يمكن استخدامها بديلًا، وهي أفضل من أدوات تحليل HTML، وقد يطلب الموقع مالًا لقاء رخصة استخدامها إن كان تجاريًا لأنها الطريقة التي يمول بها الموقع نفسه.

وتوجد عدة مشاكل أخرى لا يتسع لها شرحنا، لكن نشير إلى أهمها والتي ينبغي البحث فيها، مثل معالجة محاولات تسجيل الدخول، واستخدام ملفات تعريف الارتباط cookies، ومعالجة اتصالات https المشفرة، فيمكن تنفيذ كل ذلك بقليل من الجهد، غير أنها خارج سياق شرحنا كما ذكرنا، ويمكن استخراج أغلب المعلومات من صفحة HTML باستخدام المحلل بدمج تباديل مختلفة من تلك التقنيات، إلا أننا قد نحصل على شيفرة HTML غير متقنة الكتابة أو التنسيق، وعندها سنحتاج إلى كتابة شيفرة خاصة لتصحيح الشيفرة، بل قد نضطر إلى كتابتها إلى ملف نصي واستخدام متحقق HTML خارجي -مثل HTMLtidy- لتصحيحها، إذا كان التشوه الحاصل فيها أكثر مما يمكن تعديله يدويًا، وذلك قبل أن نحللها، أو يمكن استخدام حزمة من طرف ثالث -مثل Beautiful Soup- التي تستطيع التعامل مع أغلب المشاكل الشائعة في HTML.

اكتشاف رموز الخطأ

يعيد الخادم أحيانًا أخطاءً بدلًا من صفحات الويب التي نتوقعها، ويجب أن نتمكن من التقاط تلك الأخطاء وقراءتها، والتي يعرضها المتصفح في صورة خطأ "Page Not Found" المشهور، أو صورة عبارات ألطف قليلًا، لكن إذا كنا نحن الذين نجلب البيانات بأنفسنا من الخادم فسنجد أن الخطأ يأتي في صورة رمز خطأ في ترويسة http، التي يحولها urllib.request إلى استثناء urllib.error.HTTPError، وبما أننا نعرف كيفية التقاط الاستثناءات باستخدام بنية try/except العادية، فنستطيع التقاط تلك الأخطاء بسهولة كما يلي:

import urllib.request as url
import urllib.error
try:
   site = url.urlopen("http://some.nonexistent.address/foo.html")
except urllib.error.HTTPError, e:
   print e.code

تأتي القيمة الموجودة في urllib.error.HTTPError.code من أول سطر من رد HTTP الخاص بخادم الويب، مباشرةً قبل بداية الترويسات، مثل "HTTP/1.1 200 OK" أو "HTTP/1.1 404 Not Found"، ويتكون من رقم الخطأ، ويمكن الاطلاع على أشهر رموز الخطأ التي يعيدها خادم الويب في هذه الصفحة من موقع w3، وما يهمنا هي الأخطاء التي تبدأ بالرقم 4 أو 5، وأغلب تلك الأخطاء ستكون:

  • 401: غير مصرح له Unauthorized.
  • 404: الصفحة غير موجودة Page not found.
  • 407: يُطلب توثيق الوكيل Proxy Authentication Required.
  • 500: خطأ داخلي في الخادم Internal Server Error.
  • 503: الخدمة غير متاحة Service Unavailable.
  • 504: انتهت المهلة الزمنية للبوابة Gateway Timeout.

قد يكون المطلوب في بعض تلك الأخطاء مثل -503 و504- مجرد إعادة المحاولة بعد بضعة دقائق، أما في الخطأ 407 فقد نحتاج إلى مزيد من العمل للوصول إلى صفحة الويب، ولمزيد من التفاصيل حول هذه الرموز انظر مقال رموز الإجابة في HTTP.

لننظر الآن في الجانب الآخر، فما الذي يحدث عند وصول طلباتنا إلى خادم الويب؟ وكيف ننشئ خادم ويب خاص بنا؟ هذا هو موضوع المقال التالي.

خاتمة

في نهاية هذا المقال نأمل أن تكون تعلمت ما يلي:

  • تُنشأ طلبات إلى خوادم الويب باستخدام طلبات http GET.
  • يرد خادم الويب بمستند HTML.
  • نحتاج إلى تحليل HTML لاستخراج البيانات المطلوبة.
  • توفر بايثون عدة وحدات للتحليل، وأبسطها وحدة html.parser.
  • توفر وحدات من طرف ثالث، مثل BeautifulSoup، وسائل تحليل أسهل وأفضل.

ترجمة -بتصرف- للفصل التاسع والعشرين: Web Client Programming من كتاب Learn To Program لصاحبه Alan Gauld.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...