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

التعامل مع الملفات في البرمجة


أسامة دمراني

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

الدخل والخرج بالملفات

لنطبق مثالًا عمليًا، ولنفترض وجود ملف اسمه menu.txt، يحتوي على قائمة من الوجبات:

steak & eggs
steak & chips
steak & steak

لنكتب برنامجًا يقرأ الملف ويعرض الخرج كما يفعل أمر cat في صدفة يونكس، وأمر type في صدفة ويندوز.

# (r) افتح الملف للقراءة
inp = open("menu.txt","r")
# اقرأ الملف سطرًا سطرًا
for line in inp:
    print( line )
# أغلق الملف مرة أخرى
inp.close()

تأخذ open()‎ وسيطين، الأول هو اسم الملف الذي يمكن أن يكون متغيرًا أو سلسلةً نصيةً مجردةً literal string كما فعلنا هنا، والوسيط الثاني هو الوضع الذي نفتح به الملف، حيث يمكن فتحه للقراءة فقط r أو للكتابة w، كما نحدد هل هو للاستخدام النصي أم للاستخدام الثنائي binary، وذلك بإضافة محرف b إلى r أو w في حالة الاستخدام الثنائي، كما يلي:

open(fn,"rb")

لاحظ أننا نقرأ الملف في حلقة for، التي تتصرف في بايثون مثل حلقة foreach في تجميعة، إذ تعيد كل عنصر في التجميعة، ويمكن النظر إلى الملف النصي على أنه تجميعة من الأسطر، وهكذا تقرأ الحلقة كل سطر على حدة.

ثم نغلق الملف باستخدام دالة مسبوقة بمتغير ملف، وتُعرف هذه الصياغة باستدعاء التابع method invocation كما شرحنا في المقال السابق حول البرمجة باستخدام الوحدات، ويمكن تقريب مفهوم متغير الملف بأنه وحدة تحتوي الدوال التي تعمل على الملفات، ونستوردها في كل مرة ننشئ متغيرًا من نوع ملف.

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

كما أننا لم نحدد المسار الكامل للملف الموجود في الشيفرة أعلاه، لذا سنتعامل مع الملف على أنه موجود في المجلد الحالي، لكن يمكن تمرير الاسم الكامل للمسار إلى open()‎ بدلًا من اسم الملف مجردًا. قد نواجه مشكلةً بسيطةً في هذا الشأن في نظام ويندوز، إذ يُستخدم المحرف \ لفصل المجلدات في مسارات ويندوز، لكن نفس المحرف له معنىً خاص آخر في سلاسل بايثون، لذلك يفضل استخدام المحرف / عند تحديد المسارات في بايثون لضمان عملها على أي نظام تشغيل بما فيها ويندوز.

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

توجد طريقة أخرى لقراءة الملفات باستخدام حلقة while وتابع لكائن الملف اسمه readline()‎، وتمتاز هذه الطريقة بأننا نستطيع التوقف عن معالجة الملف بمجرد العثور على البيانات التي نريدها، مما يسرع الأداء كثيرًا إذا كنا نتعامل مع ملفات طويلة، لكنها أعقد، لذا سننظر في مثالنا السابق باستخدام هذه الطريقة لنشرحها:

# افتح الملف للقراءة
inp = open("menu.txt","r")
# اقرأ الملف واطبع كل سطر
while True:
    line = inp.readline()
    if not line: break
    print( line )
# أغلق الملف
inp.close()

لاحظ أننا استخدمنا تقنية break التي ذكرناها في مقال مقدمة في البرمجة الشرطية، وذلك للخروج من الحلقة إذا كان السطر فارغًا، إذ يَعُد السطر الفارغ بالاصطلاح البولياني قيمة false، ثم طبعنا كل سطر وكررنا الحلقة مرةً أخرى، ثم أغلقنا الملف بعد الخروج من حلقة while. وإذا رغبنا في التوقف عند نقطة ما في الملف، فسيكون ذلك بشرط فرع branch condition داخل حلقة while، فإذا اكتشفنا شرط الإيقاف فسنستدعي break أيضًا لتنتهي الحلقة.

هذا كل ما يتعلق بفتح الملف وقراءته والتعامل معه بالطريقة التي تريدها مما سبق، لكن في المثال السابق ملاحظة صغيرة، فالأسطر المقروءة من الملف لها محرف سطر جديد في نهايتها، لذا سيكون لدينا أسطر فارغة إذا استخدمنا print()‎ التي تضيف محرف السطر الجديد الخاص بها، ولتجنب هذا الأمر وفّرت بايثون تابع سلسلة نصية اسمه strip()‎ يحذف المسافات البيضاء أو المحارف التي لا تُطبع من كلا طرفي السلسلة النصية، كما توجد توابع مثل rstrip وlstrip اللذين يُحذفان المسافات من جانب واحد فقط، وبناءً عليه نستطيع إصلاح مشكلة المسافات إذا استبدلنا السطر التالي بسطر print()‎ السابق:

    print( line.rstrip() )  # احذف الجانب الأيمن فقط
    end

إذا أردنا إنشاء أمر نسخ في بايثون، فإننا نفتح ملفًا جديدًا في وضع الكتابة ثم نكتب الأسطر إليه بدلًا من طباعتها، سننشئ ملفًا اسمه MENU.BAK يكافئ الملف MENU.TXT:

# افتح الملفين للقراءة والكتابة على الترتيب
inp = open("menu.txt","r")
outp = open("menu.bak","w")

# اقرأ الملف ناسخًا كل سطر إلى الملف الجديد
for line in inp:
    outp.write(line)

print( "1 file copied..." )

# أغلق الملفات
inp.close()
outp.close()

لاحظ أننا أضفنا تعليمة print()‎ في النهاية كي نطمئن المستخدم بحدوث شيء ما، لأن التغذية الراجعة للمستخدم تنفع ولا تضر، ولم نتعرض هنا لمشاكل مع محارف الأسطر الجديدة لأننا كتبنا نفس السطر line الذي قرأناه، لكن إذا كنا نكتب سلاسل نصيةً ننشئها بأنفسنا، أو سلاسل حذفنا منها محارف الأسطر الجديدة من قبل، فسيكون علينا إضافة سطر جديد إلى نهاية سلسلة الخرج، كما يلي:

outp.write(line + '\n') # \n => سطر جديد

لننظر في كيفية تطبيق هذا في برنامج النسخ. سنضيف تاريخ اليوم في الأعلى بدلًا من مجرد نسخ القائمة، وبهذا نستطيع إنشاء قائمة يومية من ملف نصي للوجبات يسهل تعديله، وكل ما علينا فعله هو كتابة بعض الأسطر في بداية الملف الجديد قبل نسخ ملف menu.txt، كما يلي:

import time
# MENU.TXT أنشئ القائمة اليومية وفقًا لـ
# افتح الملفات للقراءة والكتابة على الترتيب
inp = open("menu.txt","r")
outp = open("menu.prn","w")

# أنشئ سلسلة تاريخ اليوم 
today = time.localtime(time.time())
theDate = time.strftime("%A %B %d", today)

# أضف نص الإعلان وسطرًا فارغًا
outp.write("Menu for %s\n\n" % theDate) 

# إلى ملف جديد menu.txt انسخ كل سطر من 
for line in inp:
    outp.write(line)

print( "Menu created for %s..." % theDate )

# أغلق الملفات
inp.close()
outp.close()

لاحظ أننا نستخدم وحدة time للحصول على تاريخ اليوم time.time()‎، وتحويله إلى صف tuple من القيم ‎(time.localtime())‎ التي تُستخدم لاحقًا بواسطة time.strftime()‎ -انظر توثيق الوقت والتاريخ في بايثون من موسوعة حسوب- لإنتاج سلسلة نصية تبدو بالشكل التالي عندما ندخلها في رسالة عنوان باستخدام تنسيق السلاسل النصية:

Menu for Sunday September 19

Spam & Eggs
Spam &...

لم يُطبع إلا سطر واحد فارغ رغم إضافتنا لمحرفين ‎\n في نهاية السلسلة النصية، وذلك لأن أحدهما كان السطر الجديد عند نهاية العنوان نفسه، وهذا يظهر جانبًا مزعجًا في إدارة عملية إنشاء محارف الأسطر الجديدة وحذفها.

إلحاق البيانات

ثمة أمر آخر في معالجة الملفات، إذ قد نرغب في إلحاق بيانات بنهاية ملف موجود، ويمكن فعل هذا بفتح الملف للإدخال، وقراءة البيانات إلى قائمة، ثم إلحاق البيانات إليها، ثم كتابة القائمة كلها إلى إصدار جديد من الملف القديم، ولن يُحدث هذا مشكلةً إذا كان الملف قصيرًا؛ أما إذا كان كبيرًا -أكبر من 100 ميجا بايت مثلًا-، فستنفد الذاكرة التي يجب أن تحتفظ بالقائمة، وسيستغرق الأمر زمنًا طويلًا. لحسن الحظ لدينا وضع يسمى "a" والذي نستطيع تمريره إلى open()‎، فيسمح لنا بإلحاق البيانات مباشرةً إلى ملف موجود بمجرد كتابتها، وإذا لم يكن الملف موجودًا، فسيُفتح ملف جديد كما لو كنا قد حددنا الوضع "w".

لنفترض أن لدينا ملف سجل نستخدمه لالتقاط رسائل الخطأ، ولا نريد أن نحذف رسائل الخطأ الموجودة كلما جاءت رسالة جديدة، لذا يمكن إلحاق الخطأ في الملف كما يلي:

def logError(msg):
   err = open("Errors.log","a")
   err.write(msg)
   err.close()

قد نرغب -من الناحية العملية- بتقييد حجم الملف بشكل ما، والتقنية الشائعة لذلك هي إنشاء اسم ملف مبني على التاريخ، فإذا تغير التاريخ فسننشئ ملفًا جديدًا، مما يسهل على مشرفي النظام أن يجدوا أخطاء يوم بعينه، وأرشفة ملفات الأخطاء القديمة إذا لم نعد بحاجة إليها. تذكر أن وحدة time من مثال القائمة أعلاه يمكن استخدامها لإيجاد التاريخ الحالي.

بنية With في بايثون

قدم الإصدار الثالث من بايثون طريقةً جديدةً سهلةً للعمل مع الملفات، لا سيما عند التكرار على محتوياتها، وتستخدم هذه الطريقة بنيةً جديدةً اسمها with، بالشكل التالي:

with open('Errors.log',"r") as inp:
    for line in inp:
        print( line )   

لاحظ أننا لم نستخدم close()‎، إذ تضمن with إغلاق الملف في نهايتها، وهكذا يكون التعامل مع الملفات أكثر موثوقيةً، وهذه الطريقة هي التي يُنصح بها لفتح الملفات في الإصدار الثالث من بايثون، وقد اخترنا استخدام الأسلوب القديم open/close لأن أغلب لغات البرمجة تستخدمه، وهو أكثر وضوحًا وصراحةً من أسلوب with، لكن إذا أردت أن تستخدم بايثون خاصةً فمن الأفضل استخدام with.

بعض العثرات في أنظمة التشغيل

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

الأسطر الجديدة

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

  1. ‎\r: محرف إعادة العربة (CR: Carriage Return).
  2. ‎\n: محرف تغذية السطر (LF: Line Feed).
  3. ‎\r\n زوج CR/LF.

تُستخدم التقنيات الثلاث في أنظمة تشغيل مختلفة، فنظام MS DOS مثلًا -وويندوز بالتبعية- يستخدم التقنية الثالثة، أما يونكس -بما في ذلك لينكس- فيستخدم الطريقة الثانية، بينما تستخدم أبل الطريقة الأولى في نظام ماك أو إس القديم، وتستخدم الطريقة الثانية في نظام MacOS X وما بعده بما أن هذا النظام ما هو إلا يونكس. وعلى المبرمج أن يجري اختبارات كثيرةً ويتخذ إجراءات مختلفةً لكل نظام تشغيل، ليتعامل مع هذا التعدد لنهايات الأسطر. لكن اللغات الحديثة -بما في ذلك بايثون- توفر تسهيلات للتعامل مع هذه الفوضى بالنيابة عنا، وتحل بايثون هذه المشكلة في وحدة os التي تعرّف متغيرًا اسمه linesep يُضبط على محرف السطر الجديد الذي يستخدمه نظام التشغيل، مما يسهل عملية إضافة الأسطر الجديدة، كما ينتبه التابع rstrip()‎ إلى نظام التشغيل عندما يحذف هذه المحارف، وهكذا نستخدم هذا التابع لنريح أنفسنا من عناء التفكير في الأسطر الجديدة التي تُحذف من الأسطر المقروءة من الملف، كما نضيف os.linesep إلى السلاسل النصية التي تُكتب إلى الملف.

فإذا أنشأنا ملفًا على نظام تشغيل ثم عالجناه على نظام تشغيل آخر غير متوافق مع الأول، فلا نستطيع إلا أن نوازن نهاية السطر مع os.linesep لنعرف الفرق.

تحديد المسارات

هذه المشكلة تخص مستخدمي ويندوز أكثر من غيرهم، فقد ذكرنا سابقًا أن كل نظام تشغيل يحدد مسارات الملفات باستخدام محارف مختلفة لفصل الأقراص والمجلدات والملفات عن بعضها، وأن الحل العام لهذا هو استخدام وحدة os التي توفر متغير os.sep لتعريف محرف فصل المسار الخاص بالمنصة الحالية، ولن نحتاج إلى ذلك كثيرًا في المواقف العملية، لأن المسار سيختلف لكل حاسوب على أي حال، لذا سنُدخِل المسار الكامل مباشرةً في سلسلة نصية -ربما سلسلة لكل نظام تشغيل نعمل عليه-، لكن لهذا عقبة كبيرة بالنسبة لمستخدمي ويندوز، فقد رأينا في القسم السابق أن بايثون تتعامل مع السلسلة ‎'\n'‎ على أنها محرف السطر الجديد، أي أنها تأخذ محرفين وتعاملهما مثل محرف واحد، ولدينا كثير من مثل هذه التسلسلات الخاصة من المحارف التي تبدأ بشرطة مائلة خلفية \، ومنها:

  • ‎\n: سطر جديد.
  • ‎\r: إعادة العربة.
  • ‎\t: جدول أفقي.
  • ‎\v: جدول رأسي، يعني أحيانًا صفحةُ جديدة.
  • ‎\b: زر backspace.
  • ‎\0nn: أي شيفرة ثمانية عشوائية، مثل ‎\033 التي تشير إلى زر الهروب Esc.

فإذا كان لدينا ملف اسمه test.dat ونريد فتحه في بايثون من خلال تحديد مسار ويندوز كامل، فستكون الشيفرة:

>>> f = open('C:\test.dat')

لكن ما يحدث هو أن بايثون سترى الزوج ‎\t على أنه محرف جدول وستخبرنا أنها لا تستطيع إيجاد ملف باسم ? est.dat، ولحل هذه المشكلة لدينا ثلاث طرق، هي:

  1. نضع r أمام السلسلة النصية، لنخبر بايثون أن تتجاهل أي شرطة مائلة خلفية، وتعاملها على أنها سلسلة نصية خام.
  2. نستخدم شرطات مائلة أمامية / بدلًا من الخلفية، وستتوافق بايثون مع ويندوز ليخرجا لنا المسار، وهذا الحل يجعل الشيفرة قابلةً للعمل على أنظمة التشغيل الأخرى.
  3. استخدام شرطتين خلفيتين \\ بما أن بايثون ترى محرفي الشرطتين المزدوجتين على أنهما شرطة خلفية واحدة.

وعلى ذلك سيفتح أي سطر مما يلي ملف البيانات الخاص بنا بشكل سليم:

>>> f = open(r'C:\test.dat')
>>> f = open('C:/test.dat')
>>> f = open('C:\\test.dat')

لاحظ أن هذه المشكلة مقصورة على السلاسل المجردة التي نكتبها في شيفرة برنامجنا، أما إذا قرئت سلاسل المسار من ملف أو من المستخدم، فستفسر بايثون محارف \ وتستخدمها كما هي دون مشاكل.

دليل جهات الاتصال

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

  • إضافة مدخل في دليل جهات الاتصال.
  • حذف مدخل منه.
  • البحث عن مدخل موجود من قبل وعرضه.
  • الخروج من البرنامج.

تحميل دليل جهات الاتصال

import os
filename = "addbook.dat"

def readBook(book):
    if os.path.exists(filename):
       with open(filename,'r') as store:
          for line in store:
             name = line.rstrip()
             entry = next(store).rstrip()
             book[name] = entry

نلاحظ هنا أننا نستورد الوحدة os التي نستخدمها للتحقق من أن مسار الملف موجود قبل فتحه، ونعرِّف اسم الملف مثل متغير مستوى وحدة module level variable لنستخدمه في تحميل البيانات وحفظها.

سنستخدم أيضًا rstrip()‎ لحذف محرف السطر الجديد من نهاية السطر، ودالة next()‎ التي تجلب السطر التالي من الملف إلى داخل الحلقة، وهذا يعني أننا نقرأ سطرين في نفس الوقت أثناء عمل الحلقة، ودالة next هي جزء من خاصية في بايثون تسمى بالمكرر، وهي خاصية لن نشرحها في هذه السلسلة بما أنها خاصة ببايثون كلغة برمجة، لكننا سنقول أن كل تجميعات بايثون وملفاتها وبعض الأمور الأخرى يُنظر إليها على أنها مكرَّرات أو أنواع قابلة للتكرار، ويمكن معرفة المزيد عن هذه الخاصية في توثيق بايثون.

حفظ دليل جهات الاتصال

def saveBook(book):
    with open(filename, 'w') as store:
       for name,entry in book.items():
         store.write(name + '\n')
         store.write(entry + '\n')

لاحظ أننا نحتاج إلى إضافة محرف السطر الجديد ‎'\n'‎ عندما نكتب البيانات، وأننا نكتب سطرين لكل إدخال، فهذا يعكس حقيقة أننا عالجنا سطرين عند قراءة الملف.

الحصول على مدخلات المستخدم

def getChoice(menu, length):
    print( menu )
    prompt = "Select a choice(1-%d): "  % length
    choice = int( input(prompt) )
    return choice

نلاحظ أننا نستقبل معامِل طول يخبرنا عدد المداخل الموجودة، مما يسمح لنا بإنشاء محث يحدد نطاق الأعداد المناسب.

إضافة مدخل

def addEntry(book):
    name =  input("Enter a name: ")
    entry = input("Enter street, town and phone number: ")
    book[name] = entry

حذف مدخل

def removeEntry(book):
    name = input("Enter a name: ")
    del(book[name])

العثور على مدخل

def findEntry(book):
    name = input("Enter a name: ")
    if name in book:
       print( name, book[name] )
    else: print( "Sorry, no entry for: ", name )

الخروج من البرنامج

لن نكتب دالةً مستقلةً للخروج من البرنامج، بل سنجعل خيار الإنهاء اختبارًا في حلقة while الخاصة بالقائمة، لذا سيكون البرنامج الرئيسي كما يلي:

def main():
    theMenu = '''
    1) إضافة مدخل
    2) حذف مدخل
    3) العثور على مدخل
    4) الخروج والحفظ
    '''
    theBook = {}
    readBook(theBook)
    while True:
        choice = getChoice(theMenu, 4)
        if choice == 4: break

        if choice == 1:
            addEntry(theBook)
        elif choice == 2:
            removeEntry(theBook)
        elif choice == 3:
            findEntry(theBook)
        else: print( "Invalid choice, try again" )
    saveBook(theBook)

لم يبق الآن إلا استدعاء main()‎ عند تشغيل البرنامج، وهنا سنستخدم القليل من سحر بايثون:

if __name__ == "__main__":
    main()

تسمح لنا هذه الشيفرة الغامضة باستخدام أي ملف بايثون مثل وحدة، وذلك باستيراده import أو مثل برنامج عبر تشغيله، والفرق هنا هو أن بايثون تضبط المتغير الداخلي __name__ عند استيراد البرنامج على اسم الوحدة، لكن إذا شغّلنا الملف مثل برنامج، فستُضبط قيمة __name__ على "__main__"، هذا يعني أن دالة main()‎ لا تُستدعى إلا إذا شغلنا الملف مثل برنامج وليس عند استيراده.

إذا كتبنا هذه الشيفرة في ملف نصي وحفظناه باسم addressbook.py، فيجب أن يكون قابلًا للتشغيل في أي محث لنظام تشغيل بكتابة ما يلي:

C:\PROJECTS> python addressbook.py

أو بالنقر المزدوج عليه مثل أي ملف في مدير الملفات في ويندوز، ليشغَّل في نافذة CMD خاصة به، تُغلَق عند تحديد خيار الإغلاق، أو باستخدام ما يلي في لينكس:

$ python addressbook.py

يُعَد هذا البرنامج المكون من ستين سطرًا نموذجًا لما يجب أن تكون قادرًا على كتابته الآن بنفسك، وهو أداة مفيدة على حالته تلك، رغم أننا سنضيف إليه بعض الأشياء التي ستحسّنه في المقال التالي.

جافاسكربت وVBScript

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

لربما يجب أن نشرح نموذج الكائن FileSystem قبل أن ننظر في الشيفرة، فنموذج الكائن هو مجموعة من الكائنات المرتبطة ببعضها، والتي يمكن للمبرمج استخدامها. يتكون نموذج كائن FileSystem من كائن FSO وعدد من كائنات File، بما في ذلك كائن TextFile الذي سنستخدمه، كما توجد بعض الكائنات المساعدة مثل TextStream.

ما سنفعله هنا هو إنشاء نسخة من كائن FSO ثم نستخدمها لإنشاء كائنات TextFile، ثم ننشئ كائنات TextStream كي نستطيع قراءة النصوص فيها وكتابتها أيضًا، كذلك فكائنات TextStream نفسها هي التي نقرؤها من الملفات أو نكتبها.

اكتب الشيفرة أدناه في ملف باسم testFiles.js وشغله باستخدام cscript كما ذكرنا في قسم WSH من المقال السابق.

فتح ملف

يجب أن ننشئ كائن FSO أولًا، ثم ننشئ كائن TextFile منه كي نتمكن من فتح ملف في WSH:

var fileName, fso, txtFile, outFile, line;

// احصل على اسم الملف
fso = new ActiveXObject("Scripting.FileSystemObject");
WScript.Echo("What file name? ");
fileName = WScript.StdIn.Readline();

// للقراءة inFile افتح 
// للكتابة outFile افتح
inFile = fso.OpenTextFile(fileName, 1); // mode 1 = Read
fileName = fileName + ".BAK"
outFile = fso.CreateTextFile(fileName);

إغلاق الملفات

inFile.close();
outFile.close();

شرح المثال في VBScript

احفظ ما يلي في testFiles.ws وشغله باستخدام:

cscript testfiles.ws

أو ضع الجزء الذي بين وسوم script في ملف باسم testFile.vbs وشغله، حيث تسمح صيغة ws. بدمج شيفرة جافاسكربت وVBScript في نفس الملف باستخدام عدة وسوم script:

<?xml version="1.0"?>

<job>
  <script type="text/vbscript">
      Dim fso, inFile, outFile, inFileName, outFileName
      Set fso = CreateObject("Scripting.FileSystemObject")

      WScript.Echo "Type a filename to backup"
      inFileName = WScript.StdIn.ReadLine
      outFileName = inFileName & ".BAK"

      ' open the files
      Set inFile = fso.OpenTextFile(inFileName, 1)
      Set outFile = fso.CreateTextFile(outFileName)

      ' read the file and write to the backup copy
      Do While not inFile.AtEndOfStream
         line = inFile.ReadLine
         outFile.WriteLine(line)
      Loop

      ' close both files
      inFile.Close
      outFile.Close

      WScript.Echo inFileName & " backed up to " & outFileName
  </script>
</job>

التعامل مع الملفات غير النصية

يُعَد التعامل مع الملفات النصية أكثر ما يفعله المبرمج، غير أننا قد نحتاج إلى معالجة بيانات ثنائية أحيانًا، وسنشرح هذا الجزء في بايثون فقط لندرة حدوثه في جافاسكربت وVBScript.

الترميز الثنائي للبيانات

يجب أن ننظر في كيفية تمثيل البيانات وتخزينها على الحاسوب قبل أن نتحدث عن كيفية الوصول إليها داخل ملف ثنائي، حيث تُخزَّن البيانات في هيئة تسلسلات من الأرقام الثنائية أو البتات، والتي تُجمع في مجموعات من 8 بتات تسمى البايت، أو 16بتًا تسمى الكلمة، في حين أن المجموعة التي تتكون من 4 بتات تسمى أحيانًا بالحلمة. قد يكون للبايت الواحد نمط من 256 نمط للبتات، وتعطى هذه الأنماط قيمًا من 0 إلى 255، كما يجب أن تُحوَّل المعلومات التي نعالجها في برامجنا من سلاسل نصية وأعداد وغيرها إلى سلاسل من تلك البايتات، لذا يخصَّص نمط بايت معين لكل محرف نستخدمه في السلاسل النصية، ورغم وجود عدة نظم ترميز للبيانات، إلا أن أشهرها هو ترميز آسكي ASCII، لكن هذا الترميز لا يحوي إلا 128 محرفًا فقط، وهذا بالكاد يكفي للغة الإنجليزية فقط، لذا طوِّر معيار ترميز جديد سمي بالترميز الموحد أو اليونيكود Unicode، والذي يَستخدم كلمات البيانات لتمثيل المحارف بدلًا من البايتات، ويشتمل على أكثر من مليون محرف، ثم يمكن بعد ذلك ترميز تلك المحارف في مجرى بيانات مضغوط أكثر.

إن أكثر ترميزات اليونيكود شيوعًا هو UTF-8، ويتوافق توافقًا كبيرًا مع آسكي، بحيث أن أي ملف متوافق مع آسكي يتوافق أيضًا مع UTF-8، رغم أن العكس قد لا يكون صحيحًا. يوفر اليونيكود عددًا من الترميزات التي يعرِّف كل منها البايت الذي يمثل قيمةً عدديةً من اليونيكود، أو "نقطةً رمزيةً" وفق اصطلاح اليونيكود. وهذا النظام المعقد هو الثمن الذي كان يجب دفعه من أجل بناء شبكة حواسيب عالمية تعمل بمختلف اللغات التي يستخدمها البشر، لكن المتحدثين بالإنجليزية لم يكن عليهم القلق بشأن اليونيكود إلا عند قراءة البيانات من ملف ثنائي، على الرغم من أن الإصدار الثالث لبايثون يَعُد السلاسل النصية سلاسل يونيكود، لذا يجب أن نعلم الترميز المستخدم من أجل تفسير البيانات الثنائية تفسيرًا صحيحًا.

تدعم بايثون نصوص اليونيكود دعمًا كاملًا، فتنظر إلى سلسلة المحارف المرمَّزة على أنها سلسلة بايتات لها النوع bytes، بينما يكون للسلسلة غير المرمزة النوع str، ويكون الترميز الافتراضي هو UTF-8، ولهذا بعض شواذ، نظريًا على الأقل، ولن نشرح استخدام المحارف التي ليست UTF-8 في هذه السلسلة، لكن يمكن مراجعة مستند How-To في موقع بايثون.

ما نريد الإشارة إليه من كل هذا هو أن المجرى الثنائي لنص اليونيكود المرمَّز يعامَل مثل سلسلة من البايتات، وتوفر بايثون دوالًا لتحويل (أو فك ترميز) قيم bytes لتكون قيم str، بالمثل يجب أن تحوَّل الأعداد إلى ترميزات ثنائية أيضًا، فعلى الرغم من أن قيم البايتات تكفي في حالة الأعداد الصغيرة، إلا أن الأعداد الأكبر من 255 أو الأعداد السالبة أو الكسور تحتاج إلى عمل آخر، وقد ظهرت عدة ترميزات معيارية للبيانات العددية، والتي تستخدمها أغلب لغات البرمجة ونظم التشغيل، فمثلًا: يعرِّف المعهد الأمريكي للهندسة الكهربية والإلكترونية IEEE عدة ترميزات للأعداد ذات الفاصلة العائمة floating point numbers، وتهدف هذه الجهود إلى حل مشكلة التفسير الصحيح للبيانات الثنائية، فمن المهم للغاية أن نحولها إلى النوع الصحيح عند قراءتها، بما أنه يتعين علينا تفسير أنماط البتات الخام إلى النوع الصحيح المناسب للبرنامج الذي نعمل عليه، وعلى ذلك من الممكن أن نفسر مجرى بيانات كُتب أصلأ على أنه سلسلة نصية في صورة مجموعة من الأعداد ذات الفاصلة العائمة، ورغم أن المعنى الأصلي له سيُفقد، إلا أن أنماط البتات يمكن أن تمثل أي واحد منهما.

فتح الملفات الثنائية وإغلاقها

يتمثل الفرق الجوهري بين الملفات النصية والثنائية في أن الملفات النصية تتكون من ثمانيات octets -جمع ثُماني، وهو الشيء المكون من ثمانية أجزاء-، لكن الاسم الأشهر لها هو بايتات، ويمثل كل بايت حرفًا. تُحدَّد نهاية الملف بنمط بايت خاص يُعرف باسم EOF، وهو اختصار لعبارة نهاية الملف End Of File؛ بينما يحتوي الملف الثنائي على بيانات ثنائية عشوائية، ومن ثم لا يمكن استخدام قيمة بعينها لتحديد نهاية الملف، لذا نحتاج إلى وضع عمليات مختلف لقراءة تلك الملفات، فعند فتح ملف ثنائي في بايثون أو في أي لغة أخرى، فيجب أن نحدد أنه يُفتَح في الوضع الثنائي، أو المخاطرة بقطع البيانات التي نقرؤها عند أول محرف eof تجده بايثون في تلك البيانات. يمكن تنفيذ ذلك في بايثون بإضافة b إلى معامِل الوضع mode parameter كما يلي:

binfile = open("aBinaryFile.bin","rb")

لا يختلف هذا عن فتح ملف نصي إلا في قيمة الوضع "rb"، ونستطيع استخدام أي وضع آخر، لكن مع إضافة حرف b، فنستخدم "wb" للكتابة، و"ab" للإلحاق، كما لا يختلف إغلاق الملف الثنائي عن الملف النصي، فنستدعي التابع close()‎ لكائن الملف المفتوح:

binfile.close()

وبما أننا فتحنا الملف في الوضع الثنائي، فلا داعي لإعطاء بايثون أي معلومات إضافية، فهي تعرف كيف تغلق الملف.

الوحدة struct

توفّر بايثون وحدةً اسمها struct لترميز البيانات الثنائية وفك ترميزها، وهي تتصرف مثل سلاسل التنسيق التي استخدمناها لطباعة البيانات المختلطة، إذ توفر سلسلةً تمثل البيانات التي نقرؤها وتطبقها على مجرى البايتات الذي نحاول تفسيره، ومن الممكن استخدام هذه الوحدة لتحويل مجموعة من البيانات إلى مجرى البايت للكتابة، إما إلى ملف ثنائي أو حتى خط اتصالات communications line.

هناك الكثير من رموز تنسيق التحويل المختلفة، لكننا لن نستخدم إلا رموز الأعداد الصحيحة integers والسلاسل النصية strings هنا؛ أما البقية فيمكن قراءة المزيد عنها في توثيق بايثون الرسمي. إن رمز تنسيق التحويل للأعداد الصحيحة هو i، ورمز السلاسل النصية s، وتتكون سلاسل تنسيق الوحدة struct من سلاسل من الرموز فيها أرقام مسبقة التعليق pre-pended، حيث توضح عدد العناصر التي نحتاج إليها، مع استثناء لرمز s الذي يشير العدد مسبق التعليق فيه إلى طول السلسلة النصية، حيث تعني 4s مثلًا سلسلةً من أربعة محارف (أربعة محارف وليس أربعة سلاسل نصية).

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

'i34s' # نفترض وجود 34 محرف في العنوان

ولكي نعالج مسألة الأطوال المختلفة للعناوين، فسنكتب دالةً تنشئ السلسلة الثنائية كما يلي:

def formatAddress(address): 
    fields = address.split()
    number = int(fields[0])
    rest = bytes(' '.join(fields[1:],'utf8') # أنشئ سلسلة بايت
    format = "i%ds" % len(rest) # أنشئ سلسلة التنسيق
    return struct.pack(format, number, rest)

لقد استخدمنا تابع السلسلة split()‎ أعلاه لتقسيم سلسلة العنوان النصية إلى أجزائها، وقسمناها في حالتنا إلى قائمة من الكلمات، ثم استخرجنا الجزء الأول ليكون رقم الشارع، بعد ذلك استخدمنا تابع سلسلةٍ نصية آخر هو join لدمج الحقول المتبقية معًا مع الفصل بينها بمسافة. كذلك نحتاج إلى تحويل السلسلة النصية إلى مصفوفة bytes لأن هذا هو ما تستخدمه وحدة srtuct، ويكون طول تلك السلسلة هو ما نحتاج إليه في سلسلة تنسيق struct، لذا نستخدم دالة len()‎ بالتوازي مع سلسلة تنسيق عادية لبناء سلسلة تنسيق struct.

ستعيد formatAddress()‎ تسلسلًا من البايتات يحتوي على التمثيل الثنائي لعنواننا، وبما أن لدينا البيانات الثنائية فسنرى كيف نستطيع كتابتها إلى ملف ثنائي، ثم قراءتها مرةً أخرى.

القراءة والكتابة باستخدام struct

لننشئ ملفًا ثنائيًا يحتوي على سطر عنوان واحد باستخدام الدالة formatAddress()‎ المعرَّفة أعلاه، وهنا سنحتاج إلى فتح الملف للكتابة في وضع 'wb'، وترميز البيانات وكتابتها إلى الملف، ثم إغلاق الملف.

import struct
f = open('address.bin','wb')
data = "10 Some St, Anytown, 0171 234 8765"
bindata = formatAddress(data)
print( "Binary data before saving: ", repr(bindata) )
f.write(bindata)
f.close()

نستطيع التحقق من أن البيانات في صورة ثنائية بفتح address.bin في محرر نصي، وسنرى حينها أن المحارف لا زالت قابلةً للقراءة لكن الرقم اختفى، فإذا فتحنا الملف في محرر نصي يدعم الملفات الثنائية مثل ViM أو Emacs، فسنجد أن بداية الملف فيها 4 بايت، حيث سيبدو الأول منها مثل محرف سطر جديد، أما البقية فتبدو أصفارًا، وذلك لأن القيمة العددية للسطر الجديد هي 10، كما نرى هنا باستخدام بايثون:

>>> ord('\n')
10

تعيد الدالة ord()‎ في هذا المثال القيمة العددية لأي محرف، لذا فإن أول أربعة بايتات هي 10,0,0,0 في النظام العشري، أو 0xA,0x0,0x0,0x0 في النظام الست عشري، وهو النظام المستخدم عادةً في عرض البيانات الثنائية بما أنه أكثر اختصارًا من استخدام الأرقام الثنائية الخالصة.

يُأخذ العدد الصحيح في حاسوب ذي معمارية 32 بت أربعة بايتات، لذا فإن القيمة العددية "10" قد حُولت بواسطة وحدة struct إلى تسلسل من 4 بايتات هو 10,0,0,0. وتضع معالجات إنتل البايت الأقل أهميةً في البداية، وهنا سنحصل على القيمة الثنائية الحقيقية من خلال قراءة التسلسل عكسيًا 0,0,0,10، وهي القيمة الصحيحة 10 ممثلةً بأربعة بايتات عشرية؛ أما بقية البيانات فهي السلسلة النصية الأصلية، لذا تظهر بصيغتها المحرفية العادية.

يجب الانتباه إلى عدم حفظ الملف من داخل محرر Notepad، فرغم أنه يستطيع تحميل بعض الملفات الثنائية، إلا أنه لا يستطيع حفظ الملفات الثنائية، لذا سيحول القيم الثنائية إلى نصوص، مما سيؤدي إلى تخريب البيانات الحقيقية.

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

نحتاج إلى أن فتح الملف في وضع rb لقراءة بياناتنا الثنائية مرةً أخرى، فنقرأ البيانات في تسلسل من البايتات ثم نغلق الملفK ونفك البيانات باستخدم سلسلة تنسيق struct. سيبقى السؤال هنا حول كيفية معرفة شكل سلسلة التنسيق، والإجابة هنا هي أننا نحتاج إلى إيجاد السلسلة الثنائية من تعريف الملف، وتوفر عدة مواقع على الويب هذه المعلومات، فشركة أدوبي مثلًا تنشر تعريف تنسيقها الثنائي لصيغة PDF الخاصة بها، وفي حالتنا هذه سنعرف أنها يجب أن تكون السلسلة التي أنشأناها في formatAddress()‎، وهي 'iNs'، حيث N رقم متغير.

توفر وحدة struct بعض الدوال المساعدة التي تعيد حجم كل نوع من أنواع البيانات، لذلك فإن شغلنا محث بايثون وأجرينا بعض التجارب، فسنستطيع معرفة عدد بايتات البيانات التي سنحصل عليها لكل نوع بيانات، وهكذا نستطيع تحديد قيمة N:

>>> import struct
>>> print struct.calcsize('i')
4
>>> print struct.calcsize('s')
1

وبما أننا نعرف أن بياناتنا ستترك 4 بايتات للعدد وبايتًا واحدًا لكل محرف، فستكون N هي إجمالي طول البيانات ناقصًا منه 4، لنجرب استخدام هذا لقراءة ملفنا:

import struct
f = open('address.bin','rb')
data = f.read()
f.close()

fmtString = "i%ds" % (len(data) - 4)
number, rest = struct.unpack(fmtString, data)
rest = rest.decode('utf8') #convert bytes to string
address = ' '.join((str(number),rest))

print( "Address after restoring data:", address )

لاحظ أننا اضطررنا إلى تحويل rest إلى سلسلة نصية باستخدام دالة decode()‎ لأن بايثون رأتها من النوع bytes الذي لن يعمل مع join()‎.

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

الوصول العشوائي إلى الملفات

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

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

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

أين أنا؟

لتحديد مكاننا في ملف ما نستخدم التابع tell()‎ الخاص بالملف، فإذا فتحنا ملفًا وقرأنا ثلاثة أسطر، فعندها سنستطيع أن نسأل الملف حينها كم قرأنا من الملف حتى الآن.

لننظر في مثال ليتضح المعنى، حيث سننشئ ملفًا بخمسة أسطر نصية لها نفس الطول -وتساوي الطول هنا ليس ضروريًا وإنما هو لتوضيح المثال-، بعدها سنقرأ ثلاثة أسطر ونسأل أين نحن، ثم نعود إلى البداية ونقرأ سطرًا واحدًا ونقفز إلى السطر الثالث ونطبعه، ثم نعود إلى السطر الثاني، كما يلي:

# (+ \n) أنشئ 5 أسطر من عشرين محرفًا 
testfile = open('testfile.txt','w')
for i in range(5):
    testfile.write(str(i) * 20 + '\n')
testfile.close()

# اقرأ ثلاثة أسطر واسأل أين نحن
testfile = open('testfile.txt','r')
for line in range(3):
    print( testfile.readline().strip() )
position = testfile.tell()
print( "At position: ", position, "bytes" )

# عد إلى البداية
testfile.seek(0)
print( testfile.readline().strip() ) # كرر السطر الأول
lineLength = testfile.tell()
testfile.seek(2*lineLength)  # اذهب إلى نهاية السطر 2
print( testfile.readline().strip() ) # السطر الثالث
testfile.close()

لقد استخدمنا الدالة seek()‎ لتحريك المؤشر، والعملية الافتراضية هنا هي تحريكه إلى رقم البايت المحدد كما رأينا هنا، لكن يمكن إضافة وسطاء آخرين لتغيير تابع الفهرسة المستخدم.

لاحظ أيضًا أن القيمة التي طبعتها tell()‎ الأولى تعتمد على طول السطر الجديد على نظامك، فنظام وندوز 10 مثلًا يطبع 66 ليشير إلى أن تسلسل السطر الجديد طوله 2 بايت، لكن بما أن هذه القيمة تتوقف على نظام التشغيل ونحن نريد أن نجعل الشيفرة محمولةً قدر الإمكان؛ فقد استخدمنا tell()‎ مرةً ثانيةً بعد قراءة سطر واحد لحساب طول كل سطر، وسترى أن مثل تلك الحيل ضرورية عند التعامل مع مشاكل المنصات المختلفة.

خاتمة

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

  • ضرورة فتح الملفات قبل استخدامها.
  • قراءة الملفات أو الكتابة فيها، إذ لا يمكن تنفيذ العمليتين معًا.
  • الدالة readlines()‎ في بايثون التي تقرأ جميع الأسطر الموجودة في ملف ما، والدالة readline()‎ التي تقرأ سطرًا واحدًا في كل مرة، وهذا يوفر في الذاكرة.
  • عدم الحاجة إلى استخدام أي من الدالتين السابقتين بما أن دالة open الخاصة ببايثون تعمل مع حلقات for.
  • ضرورة غلق الملفات بعد استخدامها.
  • ضرورة إضافة b إلى نهاية راية الوضع mode flag الخاصة بالملفات الثنائية، وتحتاج البيانات إلى تفسير بعد قراءتها، وذلك بواسطة وحدة struct عادةً.
  • تفعّل كل من tell()‎ وseek()‎ الوصول شبه العشوائي pseudo-random إلى الملفات متسلسلة الوصول sequential files.

ترجمة -بتصرف- للفصل الثاني عشر: Handling Files من كتاب Learning 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.


×
×
  • أضف...