6  الملفات النصية

لم يقتصر عمل الحاسب على العمليات الحسابية بين الأرقام فحسب، بل امتدَّ ليصل إلى معالجة النصوص (Text Processing) التي تأتي بأشكال مختلفة منها: محادثات وسائل التواصل الاجتماعي، ورسائل البريد الإلكتروني، ومقالات وكتب وموسوعات وصفحات ومواقع الشبكة.

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

وتوجد ثلاثة طرق لإنشاء النص في بايثون:

  1. بعلامة اقتباس مفردة: 'السلام عليكم!'
  2. أو بعلامة اقتباس مزدوجة: "السلام عليكم!" وليس بينهما فرق.
  3. أو بعلامة اقتاس مكررة ثلاثة مرات: """السلام عليكم!""" للنص الجاري على أكثر من سطر.

على سبيل المثال:

name = 'Adam'
address = "Makkah, Saudi Arabia"

تأمل إنشاء هذا النص الذي يبتدئ في السطر الأول ويمتد لأربعة أسطر:

message = """السلام عليكم ورحمة الله وبركاته,
أبشرك بأنك قطعت شوطًا كبيرًا.

شكرًا لك.
أخوك آدم.
"""
print(message)
السلام عليكم ورحمة الله وبركاته,
أبشرك بأنك قطعت شوطًا كبيرًا.

شكرًا لك.
أخوك آدم.

حقيقة النص

يتم تمثيل النص في بايثون بالنوع str (من String وتعني خيط) وهو سلسلة رموز (وهي التي نضعها بين علامتي التنصيص "") لكن الذي يحصل في الواقع أن مفسِّر لغة بايثون يترجمها إلى أرقام تقابلها في جدول الترميز العالمي Unicode لتكون قيمة المتغيِّر في الحقيقة.

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

لاحظ في النص البرمجي أن الدالة chr(i) تحوِّل الرقم i إلى رمز Unicode المقابل له. ونحن نستعملها في حلقة لكر الأرقام من 32 إلى 127 ونقسمها لمجموعات بحيث يكون كل سطر فيه مجموعة من الحروف:

for i in range(32, 127):
    if i in [48, 58, 65, 91, 97, 123]:
        print()
    print(chr(i), end=' ')
  ! " # $ % & ' ( ) * + , - . / 
0 1 2 3 4 5 6 7 8 9 
: ; < = > ? @ 
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
[ \ ] ^ _ ` 
a b c d e f g h i j k l m n o p q r s t u v w x y z 
{ | } ~ 

ولو أردت العكس: أن تعرف ما هو الرقم للرمز، فإنك تستعمل دالة ord(c) على النحو التالي:

print(ord("A"), ord("Z"), ord("a"), ord("z"), ord("0"), ord("9"))
print(ord("!"), ord("?"), ord("."), ord(","), ord(":"), ord(";"))
print(ord("ب"), ord("ي"), ord("ة"))
print(ord("😄"), ord("🚀"))
65 90 97 122 48 57
33 63 46 44 58 59
1576 1610 1577
128516 128640

لذلك فإن المقارنة بين السلاسل النصيَّة هي في الحقيقة مقارنة بأرقامها. فالحرف الصغير والحرف الكبير رقمان مختلفان:

print("A" == "a")
False

والحاصل في الواقع هو أن بايثون تقارنها بعد التحويل إلى الأرقام:

print(ord("A"), ord("a"))
print(ord("A") == ord("a"))
65 97
False

لذلك لا تعجب من أن الحرف الكبير أصغر من الصغير بهذا الاعتبار؛ وذلك: لأنه يأتي قبله في جدول الترميز العالمي، فرقمه أقل من رقم الصغير:

print("A" > "a")
False

والسبب في ابتدائنا من 32 هو أن الأرقام التي قبل ذلك تسىمى أحرف تحكم (Control Characters). بل إن رقم 32 نفسه هو حرف تحكُّم وظيفته تحريك رأس الكتابة بمقدار خطوة واحدة لتكوين مسافة.

print(chr(32) == " ")
print(ord(" ") == 32)
True
True

وهو من أحرف الفراغات (Whitespace Characters)، ويهمنا منها:

  • المسافة (Space): " " ويعني الانتقال بمقدار خطوة واحدة لإحداث فراغ بين الكلمات
  • السطر (Line Feed): "\n" ويعني الانتقال للسطر التالي
  • البادئة (Tab): "\t" وهي التي تكون في بداية الفقرة، أو التي تكون بين الأعمدة في تنسيق الجداو. وقد نستعملها في كتابة بايثون للمحاذاة

ولاحظ أن الشرطة مع الحرف، يفسران كرقم واحد، وإنما وُضِعَت لها رموز لكثرة استعمالها، ويمكننا معرفة الأرقام الدالة على كل من هذه الرموز هكذا:

print(ord(" "), ord("\t"), ord("\n"))
32 9 10

ونحن في كل مرة نستعمل إجراء الطباعة: print() فالقيمة الافتراضيَّة للمعطى end='\n'، وهذا يعني أن الطباعة يتبعها علامة الانتقال لسطر جديد. فإذا أردت أن تكون الطباعة بلا سطرٍ جديد، بل تكون مثلاً، مسافة، فإننا نعيِّن قيمة المعطى عند الاستدعاء:

print("Hello", end=" ")
print("World")
Hello World

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

إخراج النص

كثيرًا ما نحتاج لصياغة البيانات بقالب معيَّن، حتى يُرسَل ويفسَّر في الجهة المقابلة من برنامج آخر باعتبار نفس القالب، فتسهل قراءته بهذا الوجه.

ومن ذلك مثلاً رسائل البريد الإلكتروني حيث تكتب:

email_template = """Assalamu Alaikum {candidate_name},

We're pleased to invite you for an interview for the {job_title} position.

Details:

- Date: {interview_date}
- Time: {interview_time} {time_zone}
- Method: {interview_method}

Please confirm your availability by {rsvp_date}.

We look forward to speaking with you, Insha'Allah!

With sincere regards,
The {company_name} Hiring Team
"""

وعملية الضرب تنتج تكرارًا للنص:

print('-' * 10)
print('=' * 10)
print('*' * 10)
----------
==========
**********

فنستطيع أن نطرِّز الرسالة به:

decor = "=" * 80
email_template = decor + "\n" + email_template + "\n" + decor

فهذا القالب يُمكن ملؤه بحسب المرسل والمرسل إليه، وذلك على النحو التالي:

filled_email = email_template.format(
    company_name="Tech Solutions Inc.",
    job_title="Software Engineer",
    candidate_name="Adam Ben Saad",
    interview_date="July 15, 2045",
    interview_time="10:00 AM",
    time_zone="GMT+3",
    interview_method="Online video call",
    rsvp_date="July 10, 2045"
)

فإذا ما طبعنا هذا النص فسيظهر أن المتغير الآن يحتفظ بنسخة تمَّ تعبئة القالب فيه بالقيَم المعطاة:

print(filled_email)
================================================================================
Assalamu Alaikum Adam Ben Saad,

We're pleased to invite you for an interview for the Software Engineer position.

Details:

- Date: July 15, 2045
- Time: 10:00 AM GMT+3
- Method: Online video call

Please confirm your availability by July 10, 2045.

We look forward to speaking with you, Insha'Allah!

With sincere regards,
The Tech Solutions Inc. Hiring Team

================================================================================

كتابة النص

وتتم كتابة النص في الملفات باستعمال الإجراء write() على النحو التالي:

file = open('email.txt', 'w')
file.write(filled_email)
file.close()

إدخال النص

والغالب أن وجود النص في البرنامج يكون ناتجًا عن قراءة ملف نصي بالإجراء open() أي: في نفس المجلد الذي يوجد فيه البرنامج:

file = open('email.txt')
content = file.read()
file.close()
print(content)
================================================================================
Assalamu Alaikum Adam Ben Saad,

We're pleased to invite you for an interview for the Software Engineer position.

Details:

- Date: July 15, 2045
- Time: 10:00 AM GMT+3
- Method: Online video call

Please confirm your availability by July 10, 2045.

We look forward to speaking with you, Insha'Allah!

With sincere regards,
The Tech Solutions Inc. Hiring Team

================================================================================

وإذا كان الملف في مسارٍ آخر وأردنا قراءته، فوجب تعيين المسار بحسبه.

السياق

وهنا إشارة إلى أن عملية فتح الملف open() يجب أن تتبع بعملية .close() لإغلاقه. وهذا النمط يتكرر كثيرًا. ولذلك جعلت بايثون لهذا النمط كلمة with بحيث يتم الإغلاق بعند انتهاء جسد السياق:

with open('email.txt') as file:
    content = file.read()
print(content)
================================================================================
Assalamu Alaikum Adam Ben Saad,

We're pleased to invite you for an interview for the Software Engineer position.

Details:

- Date: July 15, 2045
- Time: 10:00 AM GMT+3
- Method: Online video call

Please confirm your availability by July 10, 2045.

We look forward to speaking with you, Insha'Allah!

With sincere regards,
The Tech Solutions Inc. Hiring Team

================================================================================

استخراج القيَم من النص

من أهم عمليات النصوص، استخراج المعلومات منها، فمثلاً، نريد استخراج:

  • تاريخ المقابلة
  • الوقت
  • وطريقة المقابلة

وإذا لاحظنا هذه الرسالة، فإنها تعلمنا بأن أحد القوالب التي قد تستعمل هي:

- Date: {interview_date}
- Time: {interview_time} {time_zone}
- Method: {interview_method}

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

lines = content.splitlines()
print(len(lines))
19

ثم نكرها ونبحث عن الأنماط التي نريد، ولاحظ أننا نستعمل الإجراء .split() لفصل النص إلى قائمة عند الفاصل الذي نريده، وهو النقطات الرأسيتان (:) حتى يكون فيه جزءان، فنأخذ الجزء الثاني [1] منها وهو القيمة. ونستعمل الإجراء .strip() لإزالة الفراغات المتبقية من النص (المسافات الزوائد في البداية والنهاية).

for line in lines:
    if "Date:" in line:
        date = line.split(":")[1].strip()
    elif "Time:" in line:
        time = line.split(":")[1].strip()
    elif "Method:" in line:
        method = line.split(":")[1].strip()

print("time =", time)
print("date =", date)
print("method =", method)
time = 10
date = July 15, 2045
method = Online video call

ويُراجع باب التعبير النمطي.

النص نوع ما لا نوع له

تذكر: النص نوع ما لا نوع له.

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

numbers = [1000, 500, 250, 750]
total = sum(numbers)
with open('output.txt', 'w') as file:
    file.write(total)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[234], line 2
      1 with open('output.txt', 'w') as file:
----> 2     file.write(total)

TypeError: write() argument must be str, not int

ويحصل تحويل العدد إلى نص باستعمال الدالة str(i):

content = str(total)
with open('output.txt', 'w') as file:
    file.write(content)

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

with open('output.txt') as file:
    content = file.read()
print(content + 2500)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[237], line 1
----> 1 print(content + 2500)

TypeError: can only concatenate str (not "int") to str

ويحصل تحويل النص إلى عددية باستعمال الدالة int(s) أو float(s):

total = int(content)
print(total + 2500)
5000

صياغة البيانات في قوالب جيسون (JSON)

وكما أن القالب المستعمل في الرسالة البريدية صالح لقراءة البشر، فكذلك البرمجيات تستعمل قوالب للتواصل فيما بينها، وكذلك تستعمل لتخزين المعلومات وقراءتها لاحقًا. ومن أشهرها قالب JSON لأنه بسيط جدًّا؛ فما هو إلا أزواج من المفاتيح والقيَم المقابلة لها (بعد النقطيتن الرأسيتين). وهي تشبه إلى حد كبير ما يسمى القاموس (dict) في بايثون. وسنبيِّنُه إن شاء الله.

فمن المكتبة الأساسية نستورِد وحدة json لصياغة البيانات من شكلها البايثوني إلى هذه الصيغة ثم كتابتها، أو لتحويلها من صيغتها تلك إلى القيمة البايثونية المقابلة لها.

import json

transactions = [1200, -500, 300, -200, 450, -1000, 800]

with open('transactions.json', 'w') as file:
    json.dump(transactions, file)

وعند القراءة نستعمل الإجراء json.load() لتحويل تلك الصياغة النصيَّة إلى قائمة بايثونية:

with open('transactions.json') as file:
    transactions = json.load(file)
print(sum(transactions))
1050

وأما إذا كانت قيَمًا متعددة، فالأفضل وضعها في قاموس (dict) بحيث تكون أسماء المتغيرات مفاتيح (Keys)، وتكون قيمها قيَمًا لها (Values). ثم تُصاغ بصيغة JSON وتُحفظ في ملف:

data = {
    "expenses": [1200, 300, 450, 800],
    "revenues": [-500, -200, -1000],
}

with open('data.json', 'w') as file:
    json.dump(data, file)

ثم يمكن قراءتُها:

with open('data.json') as file:
    data = json.load(file)
print(data)
{'expenses': [1200, 300, 450, 800], 'revenues': [-500, -200, -1000]}

وكذلك يمكن استخراج القيمة منها، بالاسم الذي حُفِظَ به أولاً في القاموس:

revenues = data["revenues"]
print(sum(revenues) / len(revenues))

expenses = data["expenses"]
print(sum(expenses) / len(expenses))
-566.6666666666666
687.5

وقد تكون البيانات متضمنة بعضها في بعض، كتفضيلات المستخدم (user):

user = {
    "name": "Adam",
    "language": "Arabic",
    "phone": "966xxxxxxxxx",
    "last_updated": "2021-09-01",
    "age": 25,
    "notifications": {
        "email": "monthly",
        "sms": "weekly",
        "push": "daily"
    },
    "emails": ["example1@domain.com", "example2@domain.com"]
}

فالمفتاح (Key) عادةً ما يكون نصًّا كما ترى. وأما القيمة (Value) فقد تكون نصًّا أو عددًا أو قائمة أو حتى قاموسًا!

وللوصول إلى قيمة مضمَّنة، قد تكتب:

a = user['notifications']
print(a)
{'email': 'monthly', 'sms': 'weekly', 'push': 'daily'}
b = a['sms']
print(b)
weekly

وذلك أن نوع قيمة المتغير a هو قاموس:

type(a)
dict

أو تأتي بها مرة واحدة:

c = user['notifications']['sms']
print(c)
weekly

وكذا الأمر في الوصول للقائمة المضمَّنة (emails) كما يلي:

e = user['emails']
print(e)
['example1@domain.com', 'example2@domain.com']

ثم الوصول إلى عنصرٍ من هذه القائمة:

print(e[0])
example1@domain.com

وذلك أن نوع قيمة المتغير c هو قائمة:

type(e)
list

أو الوصول إليها مرة واحدة:

f = user['emails'][0]
print(f)
example1@domain.com

ومن الأنماط المستعملة بكثرة: استعمال القواميس كعناصر لقائمة. ولذلك وجب علينا التدرب على قراءتها.

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

data = [
    {
        'name': 'Ahmad Hamada',
        'experiences': [
            {
                'company': 'Geo Space',
                'start': '2038-01-01',
                'end': '2039-01-01',
                'role': 'Junior Software Engineer',
            },
            {
                'company': 'Space Roots',
                'start': '2039-01-01',
                'end': '2041-01-01',
                'role': 'Senior Software Engineer',
            },
        ],
    },
    {
        'name': 'Belal Banana',
        'experiences': [
            {
                'company': 'Banana Tech',
                'start': '2041-01-01',
                'end': '2042-01-01',
                'role': 'Smoothie Operator',
            },
            {
                'company': 'BugSquash Labs',
                'start': '2042-02-01',
                'end': '2043-08-01',
                'role': 'Code Pest Control Specialist',
            },
            {
                'company': 'Caffeinated Circuits Inc.',
                'start': '2043-09-01',
                'end': '2045-05-01',
                'role': 'Espresso-Driven Engineer',
            }
        ]
    },
]

ولنفترض أننا نريد آخر وظيفة شغلها المرشح الثاني، فإننا نكتبها في بايثون بهذا الشكل:

data[1]['experiences'][-1]['role']
'Espresso-Driven Engineer'

وبايثون تقيِّمها (أي: تحسبها أو تفسِّرها) من اليسار إلى اليمين، على النحو التالي:

من المتغير data أريد العنصر الثاني، ومنه أريد مقابل المفتاح experiences، ومنه أريد العنصر الأخير، ومنه أريد مقابل المفتاح role.

وقد يكون فصلها أوضَح هكذا:

  • من المتغير data (هو قائمة)
  • منه: العنصر الثاني (1) (هو قاموس)
  • منه: مقابل المفتاح experiences (هو قائمة)
  • منه: العنصر الأخير (هو قاموس)
  • منه: مقابل المفتاح role (هو نص)

فإذا أردت أن تقرأها بالعكس من اليمين إلى اليسار، فتقول:

  • مقابل المفتاح role
  • في العنصر الأخير من
  • مقابل المفتاح experiences
  • في العنصر الثاني (1) من
  • المتغير data

وتقول أيضًا: أريد وظيفةَ آخر خبرات الثاني من المرشحين.

نصيحة

قراءة صيَغ الوصول هي من مهارات المبرمج الذي يحتاجها كثيرًا.

ويُحفظ هذا النص في ملف data.json بالصيغة التالية:

with open('data.json', 'w') as file:
    json.dump(data, file)

ثم يقرأ لاحقًا:

with open('data.json') as file:
    data = json.load(file)
print(data[0]['name'])
Ahmad Hamada