ملفات JSON و CSV
قراءة الجداول المحاطة بعلامات اقتباس وكتابتها باستخدام وحدة csv، لماذا نحتاج `newline=""`، حفظ البيانات المهيكلة باستخدام json، وأي الأنواع البرمجية تتغير أثناء التحويل.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
منذ الفصل الثالث والعشرين ونحن نقسم الأسطر باستخدام .split(",")، وكان ذلك يفي بالغرض بنجاح. لكن ماذا لو كان أحد الحقول في الملف يحتوي على فاصلة في داخله؟
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(line.strip().split(","))['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', '"large', ' sturdy"', '850.0']السطر الأخير خرج بأربعة حقول بدلاً من ثلاثة، وأصبحت علامات الاقتباس جزءاً من البيانات نفسها!
وضع علامات الاقتباس ليس خطأ — بل هو القاعدة الرسمية لملفات CSV، وقد وُجدت تحديداً للتعامل مع هذا الموقف. الخطأ يكمن في طريقة قراءتنا اليدوية. وكتابة كود يعالج هذه القواعد بنفسك تعني معالجة الاقتباسات داخل الاقتباسات ونزول الأسطر داخل الحقول؛ وهي مهمة تبدأ صغيرة وتتعقد سريعاً.
ولكن لحسن الحظ، هذه المهمة مُنجزة ومحلولة بالفعل.
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.reader(fh):
print(row)['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', 'large, sturdy', '850.0']يتناول هذا الفصل تنسيقين أساسيين — CSV للجداول المسطحة، و JSON للبيانات ذات البنية الهيكلية — وتأتي بايثون مجهزة بوحدة قياسية لكل منهما.
في نهاية هذا الدرس ستكون قادراً على
- قراءة ملفات CSV باستخدام
csv.readerوcsv.DictReader، والكتابة إليها باستخدامcsv.writer - شرح سبب كتابة المعامل
newline=""دائماً - التمييز بين
json.dumps/loadsوjson.dump/load - معرفة أنواع بايثون التي تعود كما هي من JSON وتلك التي تتغير أو تفشل
- اتخاذ القرار الصحيح بالمفاضلة بين CSV و JSON
- قراءة وفهم خطأ
JSONDecodeErrorوتحديد موقعه
المتطلبات السابقة: الاستثناءات — التعامل مع الأخطاء والانهيارات.
قراءة ملفات CSV
تُرجع الدالة csv.reader كل سطر على هيئة قائمة (List). لكن هذا يتطلب منك حفظ مواقع الأعمدة في الذاكرة — ماذا كان يمثل row[2] مجدداً؟
أما csv.DictReader، فإنه يعتبر السطر الأول رؤوساً للأعمدة (Headers)، ويحوّل كل سطر بيانات إلى قاموس (Dictionary):
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
print(row["name"], "-", row["price"], type(row["price"]))pen - 15.0 <class 'str'>
bag - 850.0 <class 'str'>لاحظ أمرين مهمين:
الوصول إلى البيانات يتم بالاسم، لذا فإن تغيير ترتيب الأعمدة في الملف لن يفسد الكود. يُعد DictReader الخيار الأفضل والأنسب في الغالبية العظمى من الحالات.
لكن price ما زال نصاً (String). وحدة csv تفصل الحقول عن بعضها، لكنها لا تحوّل أنواع البيانات — فلا يوجد شيء داخل ملف CSV يخبر بايثون أن هذا العمود عبارة عن رقم. لذا فإن استدعاء float() و int() يقع على عاتقك أنت، تماماً كما في الفصل الثالث والعشرين.
كتابة ملفات CSV
import csv
rows = [["pen", "blue ink", 15.0], ["bag", "large, sturdy", 850.0]]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(["name", "note", "price"])
writer.writerows(rows)name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0لاحظ أن النص large, sturdy وُضع بين علامتي اقتباس تلقائياً — بينما لم يُحط blue ink باقتباس، لعدم الحاجة لذلك. ما كان يصعب علينا قراءته يدوياً، لم نعد مضطرين للقلق بشأن كتابته أيضاً.
ولكتابة قائمة من القواميس، نستخدم DictWriter:
import csv
rows = [
{"name": "pen", "price": 15.0},
{"name": "bag", "price": 850.0},
]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["name", "price"])
writer.writeheader()
writer.writerows(rows)name,price
pen,15.0
bag,850.0يحدد المعامل fieldnames ترتيب الأعمدة أيضاً، ودون استدعاء writeheader() لن يُكتب سطر العناوين الرئيسي.
لماذا نكتبnewline=""؟ تتكفل وحدةcsvبنفسها بتحديد رمز نهاية السطر. وفتح الملف بالطريقة العادية يجعل بايثون يترجم نهايات الأسطر مرة ثانية في بعض أنظمة التشغيل (مثل ويندوز)، مما يترك سطراً فارغاً بعد كل صف. تمريرnewline=""يوقف هذه الترجمة الإضافية. احرص على كتابتها عند القراءة والكتابة على السواء.
JSON — بنية هيكلية تتجاوز حدود الجداول
ملفات CSV تشبه الجداول: صفوف وأعمدة، وكل شيء فيها مسطح. ولكن ماذا لو كان الطلب يحتوي على قائمة عناصر، وكل عنصر يحتوي على قائمة وسوم (Tags)؟ لا يمكن للجدول البسيط استيعاب ذلك.
import json
order = {"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": True, "note": None}
print(json.dumps(order)){"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": true, "note": null}تحوّل الدالة json.dumps كائن بايثون إلى نص (String). يبدو الشكل قريباً جداً من قواميس بايثون، مع اختلافين: تحولت True إلى true، وتحولت None إلى null. فـ JSON لغة مستقلة مشتركة بين العديد من لغات البرمجة، ولها قواعدها الإملائية الخاصة.
ولإنتاج نص منسق يسهل على الإنسان قراءته:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}ولإعادة تحويل النص إلى كائن بايثون:
import json
text = '{"name": "pen", "price": 15.0, "stocked": true, "note": null}'
order = json.loads(text)
print(order)
print(type(order["price"]), type(order["stocked"]), order["note"] is None){'name': 'pen', 'price': 15.0, 'stocked': True, 'note': None}
<class 'float'> <class 'bool'> Trueهذا هو الفارق الجوهري الأكبر بينه وبين CSV. عادت القيمة price كرقم وعادت stocked كـ True بوليني، دون الحاجة لأي دوال تحويل يدوية على الإطلاق. يحمل JSON أنواع البيانات الأصلية معه، بينما لا يفعل CSV ذلك.
يسهل حفظ الفروق بين الدوال الأربع: dumps و loads تتعاملان مع النصوص، بينما dump و load تتعاملان مع الملفات مباشرة. حرف s الأخير يرمز إلى string.
import json
from pathlib import Path
order = {"name": "pen", "price": 15.0}
Path("order.json").write_text(json.dumps(order, indent=2) + "\n", encoding="utf-8")
with open("order.json", "r", encoding="utf-8") as fh:
back = json.load(fh)
print(back){'name': 'pen', 'price': 15.0}ما لا يعيده JSON كما كان
لا يمكن لكل كائن في بايثون أن يتحول إلى JSON، وما يدخل فيه لا يعود بالضرورة متطابقاً تماماً.
import json
data = {"pair": (1, 2), "numbers": {3, 4}}
print(json.dumps({"pair": data["pair"]}))
print(json.dumps(data)){"pair": [1, 2]}
TypeError: Object of type set is not JSON serializableيتحول التوبل (Tuple) بصمت إلى قائمة (List). لا يوجد مفهوم التوبل في JSON، لذا فالكتابة لا تطلق أي خطأ — ولكن ما يعود إليك هو قائمة عادية. فوعد الفصل الرابع عشر بأن التوبل غير قابل للتعديل يضيع أثناء رحلة التحويل.
المجموعات (Sets) ترفض التحويل تماماً، وهذا في الواقع سلوك أفضل — فالتوقف المباشر بخطأ أفضل بكثير من التحويل الصامت الخاطئ. وإذا أردت حفظها، مررها كقائمة مرتبة عبر sorted(...).
وهناك فخ إضافي أكثر تأثيراً:
import json
data = {1: "pen", 2: "bag"}
text = json.dumps(data)
print(text)
print(json.loads(text)){"1": "pen", "2": "bag"}
{'1': 'pen', '2': 'bag'}مفاتيح JSON نصية دائماً وأبداً. إذا مررت مفاتيح عددية، فإنها تتحول بهدوء إلى نصوص، وتعود إليك كنصوص. لذا إن كان data[1] يعمل سابقاً، فستحتاج إلى data["1"] بعد القراءة من JSON.
القائمة التي يجدر بك حفظها قصيرة: القواميس، والقوائم، والنصوص، والأرقام، و True/False و None آمنة تماماً. التوبلات تتغير إلى قوائم. أما المجموعات، والتواريخ، والكائنات المخصصة التي تنشئها بنفسك فلا تُحوّل مباشرة.
JSON صارم ودقيق في قواعده
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)في بايثون، علامات الاقتباس المفردة والمزدوجة متكافئة تماماً. أما في JSON فليست كذلك — علامات الاقتباس المزدوجة حصراً هي المقبولة، ويُمنع وضع فاصلة زائدة بعد العنصر الأخير.
تحدد رسالة الخطأ رقم السطر والعمود بدقة، مما يسهل عليك العثور على موضع الخطأ حتى في الملفات الضخمة. والخطأ JSONDecodeError هو في الأصل ابن لـ ValueError — لذا فإن except ValueError تلتقطه أيضاً، وفقاً لقاعدة عائلات الاستثناءات التي تعلمناها في الفصل السابق.
أيهما تختار ومتى؟
اختر CSV عندما تكون البيانات على شكل جدول مسطح بسيط ويحتاج مستخدم إلى فتحه في برامج الجداول الحسابية (مثل Excel). ومع كثرة الصفوف، يبقى حجم الملف أصغر نسبياً.
اختر JSON عندما تحتوي البيانات على بنية هيكلية — قوائم متداخلة، قواميس داخل قواميس — أو عندما تحتاج إلى استعادة أنواع البيانات دون تحويل يدوي. ملفات الإعدادات وواجهات برمجة التطبيقات على الويب (APIs) تعتمد على JSON في كل مكان تقريباً.
مثال متكامل
ملف items.csv:
name,note,price,quantity
pen,blue ink,15.0,3
bag,"large, sturdy",850.0,1
ink,,120.0,2
clip,broken,,4ملف main.py:
"""Read a CSV of items, total it, and write the result as JSON."""
import csv
import json
from pathlib import Path
TAX_RATE = 0.15
def read_items(path):
"""Returns the usable rows and the complaints. csv handles the quoting."""
items = []
problems = []
with open(path, "r", encoding="utf-8", newline="") as fh:
for number, row in enumerate(csv.DictReader(fh), start=2):
try:
items.append({
"name": row["name"],
"note": row["note"],
"amount": round(
float(row["price"]) * int(row["quantity"]) * (1 + TAX_RATE), 2),
})
except ValueError as err:
problems.append(f"line {number}: {err}")
return items, problems
def main():
items, problems = read_items(Path("items.csv"))
summary = {
"items": items,
"total": round(sum(item["amount"] for item in items), 2),
"skipped": problems,
}
Path("summary.json").write_text(
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(Path("summary.json").read_text(encoding="utf-8"), end="")
if __name__ == "__main__":
main(){
"items": [
{
"name": "pen",
"note": "blue ink",
"amount": 51.75
},
{
"name": "bag",
"note": "large, sturdy",
"amount": 977.5
},
{
"name": "ink",
"note": "",
"amount": 276.0
}
],
"total": 1305.25,
"skipped": [
"line 5: could not convert string to float: ''"
]
}أربعة أمور جديرة بالدراسة:
ملف CSV دخل ومدخلات JSON خرجت، وهذا نابع من طبيعة كل تنسيق. المدخلات عبارة عن جدول مسطح يسهل فتحه في الجداول الحسابية. أما المخرجات فليست مسطحة — بل هي قائمة عناصر يمثل كل منها قاموساً، وبجوارها قائمة ثانية بالملاحظات والأخطاء. بنية كهذه كان يستحيل تمثيلها بنقاء في ملف CSV.
النص large, sturdy اجتاز الرحلة بنجاح تام. قرأته وحدة csv بشكل صحيح وكتبته json باقتباس دقيق. والمشكلة التي بدأ بها هذا الفصل لم يعد لها أي أثر.
الحقل الفارغ note والحقل الفارغ price كلاهما نصوص فارغة، لكن مصيرهما اختلف تماماً. الحقل الفارغ في CSV هو "" وليس None. الحقل note ظل نصاً ولم يسبب أي مشكلة، بينما float("") أطلقت استثناء ValueError، فتخطى البرنامج سطر clip وأضافه إلى قائمة الملاحظات. لا يوجد في CSV مفهوم "قيمة مفقودة"، بل "نص فارغ" فقط — وتفسير معنى هذا الفراغ هو قرارك أنت.
بدء الترقيم بـ start=2 لم يكن عشوائياً. يستهلك DictReader السطر الأول كرؤوس للأعمدة، لذا فإن أول سطر بيانات يقدمه هو السطر الثاني في الملف الحقيقي. والرسالة التي سيتحقق منها إنسان في الملف الأصلي يجب أن تعد الأسطر بنفس طريقة الملف.
حالات الخطأ الشائعة
سطر فارغ إضافي بعد كل صف في ملف CSV نسيت تمرير newline="". احرص على إضافتها عند فتح الملف للكتابة.
KeyError: 'price' — مع أن العمود موجود في الملف بوضوح اسم رأس العمود غير مطابق، سواء في التهجئة أو بوجود مسافات بيضاء زائدة. اطبع reader.fieldnames لترى ما قرأه بايثون فعلياً.
TypeError عند إجراء عمليات حسابية على الأرقام وحدة csv تُرجع كل شيء كنصوص. يجب عليك استدعاء float() أو int() يدوياً.
معاملة السطر الأول في الملف كبيانات عادية أنت تستخدم csv.reader الذي لا يعلم شيئاً عن رؤوس الأعمدة. استخدم DictReader بدلاً منه، أو احذف السطر الأول بنفسك.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes استخدام علامات اقتباس مفردة، أو وجود فاصلة إضافية بعد آخر عنصر. لغة JSON ليست كود بايثون.
TypeError: Object of type set is not JSON serializable مررت مجموعة أو تاريخاً أو كائناً مخصصاً. حوّله إلى قائمة أو نص أولاً قبل التحويل.
عادت التوبلات كقوائم عادية هذا هو السلوك الطبيعي المتوقع — فلا توجد توبلات في JSON. استدعِ tuple(...) بنفسك بعد القراءة إذا كنت بحاجة لتوبل.
كان data[1] يعمل، وبعد حفظه وقراءته ألقى KeyError مفاتيح JSON نصية دائماً. أصبح المفتاح الآن data["1"].
Step 4 of 6 — Predict
Check your understanding
A three-column file, split by hand. How many fields come out?
# items.csv
# name,note,price
# pen,blue ink,15.0
# bag,"large, sturdy",850.0
with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(len(line.strip().split(",")))- A3 3 4
- B3 3 3
- C3 3 5
- DA `ValueError`
DictReader read the row. What happens on the second line?
# items.csv
# name,price
# pen,15
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
row = next(csv.DictReader(fh))
print(row)
print(row["price"] + 1)- AThe dictionary prints, then a `TypeError` — `price` is still text
- BThe dictionary prints, then `16`
- CThe dictionary prints, then `151`
- DA `KeyError`
json.dumps is given a Python dictionary. What is printed?
import json
print(json.dumps({"name": "pen", "stocked": True, "note": None}))- A{"name": "pen", "stocked": true, "note": null}
- B{"name": "pen", "stocked": True, "note": None}
- C{'name': 'pen', 'stocked': True, 'note': None}
- D{"name": "pen", "stocked": true}
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
أنشئ ملفاً باسم books.csv بالأعمدة: title,author,pages,year وضع فيه ستة أسطر على الأقل. ضع فاصلة داخل عنوان أحد الكتب على الأقل (بحيث يُحاط باقتباس)، واترك عمود pages فارغاً في أحد الصفوف، وضع أحرفاً بدلاً من الرقم في صف آخر.
ثم اكتب سكربت library.py يقوم بما يلي:
- قراءة الملف باستخدام
DictReader، مع جمع الأخطاء المرفقة برقم السطر لكل صف معيب - إنشاء ملخص بحسب الكاتب — كم كتاباً لكل كاتب، وإجمالي عدد الصفحات
- كتابة النتيجة بالكامل في ملف
library.jsonمع تنسيقindent=2 - قراءة الملف من جديد والتأكد من تطابق الإجمالي تماماً
ثم أجرِ هذه التجارب الخمس:
- استخدم
line.split(",")بدلاً منDictReader. ماذا يحدث للعنوان الذي يحتوي على فاصلة؟ - احذف
newline=""عند الكتابة، ثم افتح ملف CSV في محرر نصوص وتفحص الأسطر. - أضف مجموعة (Set) في الملخص (مجموعة أسماء الكُتّاب مثلاً) وحاول حفظ ملف JSON. اقرأ رسالة الخطأ ثم قم بحلها.
- افتح ملف
library.jsonيدوياً وغيّر علامة اقتباس مزدوجة واحدة إلى اقتباس مفرد، ثم حاول قراءته بالكود. هل تظهر رسالة الخطأ رقم السطر والعمود بدقة؟ - استخدم السنة كمفتاح عدد في القاموس — مثل
{2020: [...]}— ثم احفظه واقرأه من جديد. بماذا يجب أن تبحث عنه في القاموس الآن؟
التجربتان الأخيرتان توضحان جوهر هذا الفصل: الكتابة في ملف هي عملية ترجمة، وكل ترجمة تغيّر شيئاً في طبيعة البيانات. معرفة ما يتغير تمنحك حرية اختيار التنسيق؛ والجهل به يجعل التنسيق يفرض قيوده عليك.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz