17  صياغة البيانات

سَلْسَلة البيانات (Data Serialization) تشير لعملية تحويل البيانات في ذاكرة بايثون (مثل القائمة [] والقاموس {}) من صيغتها الثنائية الخاص باللغة إلى تمثيلٍ ليس خاصًّا بلغةٍ معيَّنة؛ بل يتبع صيغةً متفقًا عليها؛ فإما أن يكون:

  1. تمثيلاً نصيًّا (Plain Text Serialization) متفقًا عليه مثل: xml أو json أو csv ونحوها (وقد تقدَّم عرضُها في باب النص).
  2. أو تمثيلاً ثنائيًّا (Binary Serialization) متفقًا عليه كذلك مثل: pickle أو protobuf أو parquet.

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

وعملية تحويل البيانات المُسَلسَلة (أي المكتوبة بإحدى هذه الصيغ) وقراءَتها في برنامجٍ ما تسمى فك التسلسل (Deserialization). فالتحويل يكون مثلاً من النصي إلى الثنائي في الذاكرة. أو من الثنائي المسلسل إلى الثنائي في الذاكرة.

فالسلسلة وفكها ما هي إلا صياغة لا تغير المكنون بل شكله.

ويختلف شكل البيانات من ثلاثة أوجه:

  1. الصف: [v1, v2, ...]
  2. الربط: {k1: v1, k2: v2, ...}
  3. التضمين: وهو جعل العنصر نفسه صفًّا أو ارتباطًا
    • تضمين الصف لصفوف: [[v1, v2, ...], [v1, v2, ...], ...]
    • تضمين الصف لروابط: [{k1: v1, k2: v2, ...}, {k1: v1, k2: v2, ...}, ...]
    • تضمين الروابط لصفوف: {k1: [v1, v2, ...], k2: [v1, v2, ...], ...}
    • تضمين الروابط لروابط: {k1: {k2: v2, k3: v3, ...}, k2: {k4: v4, k5: v5, ...}, ...}

فالصفوف تتميز بكون كل صفٍّ فيها تدوينًا لمجموعة خصائص تشترك في العمود كله. أما الارتباط ففيه مرونة؛ فمجموعة الارتباطات لا تُلزِم كل ارتباط أن يكون له ذات خصائص الارتباط الذي قبله.

الارتباط والتضمين

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

import json

لنفترض أن لدينا user_preferences محفوظًا في القاموس التالي، ونريد حفظه في ملف json:

user_preferences = {
    'theme': 'dark',
    'language': 'Arabic',
    'notifications': {
        'email': True,
        'sms': False,
        'push': True
    },
    'last_updated': '2021-09-01',
    'emails': ['example1@domain.com', 'example2@domain.com']
}

لنكتبها في الملف نستعمال json.dump على النحو التالي:

with open('../../datasets/user_preferences.json', mode='w') as file:
    json.dump(user_preferences, file)

فإذا أردنا قراءتها نستعمل json.load على النحو التالي:

with open('../../datasets/user_preferences.json') as file:
    data = json.load(file)
print(data)
{'theme': 'dark', 'language': 'Arabic', 'notifications': {'email': True, 'sms': False, 'push': True}, 'last_updated': '2021-09-01', 'emails': ['example1@domain.com', 'example2@domain.com']}

الصف

تأتي البيانات الجدولية في صيغ متعددة، مثل:

  • CSV وهي صيغة يكون فيها الصف في سطر، وتكون عناصره مفصولة بعلامة الفاصلة ","
  • TSV وهي مثل CSV إلا أن الفاصلة علامة "\t"

وغيرها كثير.

في هذا القسم، سنركز على ملفات (Comma Separated Values) CSV؛ وتعني حرفيًّا: القيَم المفصولة بالفاصلة.

توجد في بايثون وحدة csv فيها إجراءات للقراءة والكتابة على طريقة csv. فلدينا:

  • كائن reader لعمليات القراءة
  • وكائن آخر منفصل اسمه writer يحوي عمليات الكتابة
import csv

لنكتب قائمة من الطلاب إلى ملف CSV. لاحظ، لدينا قائمة من قوائم، حيث تمثل كل قائمة داخلية صفًا لوحدها:

header = ['Name', 'Age', 'Grade', 'Done']
rows = [
    ['Adam', 22, 90, 'F'],
    ['Belal', 23, 92, 'F'],
    ['Camal', 24, 91, 'T'],
    ['Dawod', 8, 99, 'F'],
    ['Emad', 9, 98, 'F'],
]

نكتبها على النحو التالي:

with open('../../datasets/students.csv', mode='w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(header)
    writer.writerows(rows)

ملاحظة، يمكنك محاولة فتح الملف مباشرة من مستكشف الملفات. حاول فتحه باستخدام Excel أو Google Sheet أو أي برنامج جداول بيانات آخر. إذا فتحته باستخدام محرر نصوص، سترى البيانات كملف CSV؛ حرفيًا قيم مفصولة بفواصل.

الآن، دعنا نقرأه كهيكل بيانات في بايثون: كقائمة من القوائم.

with open('../../datasets/students.csv') as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)
['Name', 'Age', 'Grade', 'Done']
['Adam', '22', '90', 'F']
['Belal', '23', '92', 'F']
['Camal', '24', '91', 'T']
['Dawod', '8', '99', 'F']
['Emad', '9', '98', 'F']

لنحاول حساب متوسط درجات الطلاب.

students = []
with open('../../datasets/students.csv') as file:
    reader = csv.reader(file)
    next(reader) # skip the header
    for row in reader:
        students.append(row)

الآن بعد أن حفظناها في القائمة students، دعونا نقوم ببعض العمليات الحسابية.

grades = [s[2] for s in students]
avg = sum(grades) / len(grades)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 2
      1 grades = [s[2] for s in students]
----> 2 avg = sum(grades) / len(grades)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

هذا الخطأ متوقع عند قراءة الملفات لأنها دائمًا تعتبر من نوع str، ولذلك نضطر لتحويل القيَم العددية إلى int لفعل عمليات رياضية:

grades = [int(s[2]) for s in students]
avg = sum(grades) / len(grades)
avg
94.0

يمكنك أيضًا قراءة وكتابة البيانات في شكل قاموس باستخدام الكائنات DictReader وDictWriter.

with open('../../datasets/students.csv') as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row)
{'Name': 'Adam', 'Age': '22', 'Grade': '90', 'Done': 'F'}
{'Name': 'Belal', 'Age': '23', 'Grade': '92', 'Done': 'F'}
{'Name': 'Camal', 'Age': '24', 'Grade': '91', 'Done': 'T'}
{'Name': 'Dawod', 'Age': '8', 'Grade': '99', 'Done': 'F'}
{'Name': 'Emad', 'Age': '9', 'Grade': '98', 'Done': 'F'}

والطريقة الموصى بها للتعامل مع البيانات الجدولية (مثل ملفات CSV) هي استخدام مكتبة مثل pandas. توفر هذه المكتبة هيكل بيانات سريع ومرن لمعالجة البيانات وتحليلها. ولا بأس أن تطلع على دليل البداية في مكتبة pandas.

  • إذا كنت تريد قراءة الملفات وكتابتها بشكل بسيط انظر: open().
  • وإذا كنت تريد التعامل مع الملفات المؤقتة فانظر: tempfile.
  • وكثير من عمليات التعامل مع الملفات والأدلة تجدها في: shutil.