قراءة الملفات وكتابتها
قراءة الملفات وكتابتها باستخدام with open، الفروق بين أوضاع r و w و a و x، السطر الجديد في نهاية كل سطر، لماذا نكتب encoding دائماً، واستخدام pathlib للمهام السريعة.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
كل ما بنيناه على مدار الفصول السابقة كان يختفي تماماً بمجرد انتهاء تشغيل البرنامج.
basket = []
basket.append("pen")
print(basket)['pen']شغّل البرنامج مرة أخرى وستجد السلة فارغة من جديد. القوائم، والقواميس، والدوال — كل شيء يعيش في الذاكرة العشوائية (RAM)، وحياة الذاكرة تنتهي تماماً بانتهاء عمل البرنامج.
كان الفصل السابق يدور حول مشاركة الكود البرمجي بين البرامج المختلفة. أما هذا الفصل فيدور حول البيانات — كيف يترك تشغيلٌ ما نتاجه للتشغيل التالي، أو لشخص آخر تماماً.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineانتهى البرنامج من العمل، وما زال ملف notes.txt موجوداً ومحفوظاً على القرص.
في نهاية هذا الدرس ستكون قادراً على
- قراءة الملفات وكتابتها باستخدام
with open(...)وشرح سبب تفضيلwith - التمييز بدقة بين أوضاع الفتح
"r"و"w"و"a"و"x"— وخاصة ما يمحوه الوضع"w" - قراءة الملف سطراً بسطر ومعالجة رمز السطر الجديد
\nفي نهاية كل سطر - شرح سبب كتابة
encoding="utf-8"دائماً وبلا استثناء - استخدام
pathlib.Pathلإنجاز المهام البسيطة والسريعة - قراءة وفهم أخطاء
FileNotFoundErrorوUnicodeDecodeError
المتطلبات السابقة: الوحدات النمطية والاستيراد.
لماذا نستخدم with؟
فتح أي ملف هو بمثابة استعارة مورد من نظام التشغيل، وأي استعارة يجب أن تُرد. إذا لم يُغلق الملف، فقد لا تصل البيانات التي كتبتها إلى القرص مطلقاً، كما أن تراكم الملفات المفتوحة دون إغلاق قد يؤدي إلى توقف البرنامج بأكمله عن العمل.
كتلة with تتكفل بإعادة هذا المورد تلقائياً — حيث يُغلق الملف فور انتهاء الكتلة، حتى لو حدث استثناء أو خطأ في داخلها.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.المتغير fh يظل موجوداً بعد انتهاء الكتلة — فنطاق المتغيرات يعمل تماماً كما تعلمنا سابقاً — ولكن الملف نفسه يصبح مغلقاً. وفي هذه الدورة التدريبية، تُكتب دالة open() دائماً بصحبة with.
قراءة الملفات
لقراءة محتوى الملف كاملاً دفعة واحدة، نستخدم التابع .read():
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23استخدمنا repr هنا عمداً، لأن print العادية تخفي فواصل الأسطر. الملف النصي هو مجرد نص واحد طويل، وتستقر فواصل الأسطر بداخله على هيئة رمز \n — وتذكر هذه الحقيقة هو نصف فهم هذا الفصل.
قراءة الملف سطراً بسطر
استخدام .read() ليس خياراً مناسباً للملفات الضخمة، لأنه يحمّل الملف بأكمله دفعة واحدة في الذاكرة. بدلاً من ذلك، فإن التكرار عبر كائن الملف نفسه يجعل بايثون يقرأ سطراً واحداً في كل دورة:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'كل سطر ينتهي بالرمز \n. وهذا هو مصدر الالتباس الأكثر شيوعاً على الإطلاق — فالأمر int(line) لن ينجح، والمقارنة line == "pen" لن تتحقق، والسبب خفي عن العين للوهلة الأولى.
لذا فإن الخطوة الأولى في الغالبية العظمى من الحالات هي استخدام .strip():
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00استخدام .strip() ثم .split(",") ثم float() و int() — هذا التسلسل سيتكرر معك مراراً وتكراراً عند قراءة الملفات. كل ما يخرج من الملف هو نص (String) وليس أرقاماً، والتحويل الذي تعلمناه في الفصل السابع مطلوب هنا أيضاً.
إذا كنت ترغب في الحصول على جميع الأسطر دفعة واحدة في قائمة، فهناك طريقتان:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']التابع .splitlines() يحذف نهايات الأسطر تلقائياً. هناك أيضاً التابع .readlines()، ولكنه يُبقي على رموز \n في نهاية كل سطر — وهو الفخ الذي يتعثر فيه الكثيرون.
الكتابة وحرف واحد بالغ الخطورة
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")إذا كان الملف يحتوي سابقاً على old content، فالنتيجة الآن هي:
newالوضع "w" يفرغ الملف ويمحو محتواه تماماً — وذلك في لحظة الفتح ذاتها، وقبل كتابة أي شيء فيه. ولا يوجد أي تحذير ولا سبيل للتراجع.
للحفاظ على المحتوى الحالي والإضافة إلى نهايته، استخدم الوضع "a" (Append):
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newأما إذا أردت رفض الكتابة تماماً في حال كان الملف موجوداً بالفعل، فاستخدم الوضع "x" (Exclusive creation):
FileExistsError: [Errno 17] File exists: 'notes.txt'الأوضاع الأربعة في سطر واحد: "r" للقراءة فقط، و "w" لمحو القديم والكتابة من جديد، و "a" للإضافة في النهاية، و "x" لإنشاء ملف جديد حصراً.
تريث قليلاً قبل كتابة الحرف"w". لقد فقد الكثير من المبرمجين أعمالاً حقيقية بسبب هذا الحرف الواحد. إذا كان الملف الذي تفتحه لم ينشئه برنامجك بنفسه، ففكر جيداً قبل استخدام"w".
أنت المسؤول عن إضافة \n
دالة print تنزل سطراً جديداً تلقائياً نيابة عنك. أما دالة write فلا تفعل ذلك.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'لذا فإن أنظف طريقة لكتابة قائمة من الأسطر هي:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'تضع "\n".join(rows) فواصل أسطر بين العناصر، وتنهي الإضافة + "\n" الملف بسطر جديد — وهو العرف الاصطلاحي القياسي للملفات النصية وما تتوقعه العديد من الأدوات.
encoding="utf-8" — دائماً وبلا استثناء
بدون تحديد encoding، يعتمد بايثون على الترميز الافتراضي لنظام التشغيل، وهو ما يختلف من جهاز لآخر. والنتيجة هي برنامج يعمل بسلاسة على جهازك ويتعطل تماماً على جهاز شخص آخر.
القراءة بالترميز الخاطئ تؤدي إلى:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteلذا اكتب encoding="utf-8" في كل مرة تكتب فيها open() — سواء للقراءة أو للكتابة على حد سواء. الاستثناء الوحيد هو الوضع الثنائي ("rb" و "wb")، حيث لا يطرح السؤال أصلاً لعدم وجود ترميز نصي.
pathlib — مسار مختصر للمهام البسيطة
بالنسبة للملفات الصغيرة، قد تبدو كتابة كتلة with بأكملها عبئاً طويلاً.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneالتابعان read_text و write_text يتكفلان بفتح الملف وإغلاقه داخلياً، فلا حاجة لكتابة with. ويوفر كائن Path ميزات أخرى متعددة — مثل .exists() و .name و .suffix و .parent — وتُدمج مسارات المجلدات باستخدام علامة /، كما في: Path("data") / "orders.txt".
متى تستخدم كل منهما؟ استخدم Path عندما تحتاج قراءة الملف بأكمله دفعة واحدة، واستخدم with open(...) عند القراءة سطراً بسطر. وتمتلك write_text ميزة إضافية: إنها تستبدل الملف افتراضياً وبوضوح، لذا فإن خطأ كتابة "w" دون قصد لن يتكرر هنا.
مثال متكامل
ملف orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2ملف main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3أربعة أمور جديرة بالانتباه:
ثلاث دوال بثلاث وظائف منفصلة. الدالة read_order تقرأ من القرص، و report تقتصر على الحسابات فقط، و main تكتب النتيجة. لم تُعطَ report أي ملف على الإطلاق — بل أُعطيت قائمة وأرجعت قائمة. لذا فاختبارها لا يتطلب وجود ملف، وفي الفصل الثامن والعشرين سنكون ممتنين تحديداً لهذه الميزة.
تمت معالجة الأسطر الفارغة مسبقاً وبحذر. وجود سطر جديد فارغ في نهاية الملف أمر طبيعي جداً، وبدون معالجته سيفشل raw.split(",") في العثور على ثلاثة عناصر وسيلقي خطأ ValueError. السطر if not raw.strip(): continue يحل المشكلة في سطر واحد.
تحويل float(price) و int(quantity) ليس خياراً بل ضرورة، لأن كل ما يخرج من الملف يصل كنص. إن نسيتهما، فسيقوم price * quantity بتكرار النص بصمت دون أي تنبيه — وهو أسوأ أنواع الأخطاء البرمجية: الخطأ الذي لا يوقف البرنامج.
يُكتب التقرير في ملف منفصل تماماً. لم نفتح orders.txt بوضع "w" إطلاقاً — وظل ملف المدخلات الأصلي سليماً كما هو. هذه عادة برمجية ممتازة: لا تكتب فوق الملف الذي تقرأ منه.
حالات الخطأ الشائعة
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' الملف غير موجود، أو أنك تشغل البرنامج من مجلد مختلف. المسار يُحدد بناءً على المجلد الذي شغلت منه الأمر، وليس مجلد السكربت نفسه. اطبع Path("orders.txt").resolve() لتعرف أين يبحث البرنامج بالضبط.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... الملف كُتب بترميز مختلف، أو أنه ليس ملفاً نصياً على الإطلاق. حدد الترميز الصحيح عبر encoding، أو افتحه بوضع القراءة الثنائية "rb" إذا كان ملفاً ثنائياً.
ValueError: I/O operation on closed file. تمت محاولة استخدام كائن الملف بعد انتهاء كتلة with. اقرأ ما تحتاجه من بيانات داخل الكتلة.
ValueError: not enough values to unpack أحد الأسطر لا يحتوي على عدد الفواصل المتوقع — وغالباً ما يكون سطراً فارغاً في النهاية. تجاهل الأسطر الفارغة، وعند الشك اطبع print(repr(raw)).
كتبت الملف لكنه فارغ تماماً تتم قراءة الملف قبل اكتمال كتلة with. تصل عمليات الكتابة إلى القرص فعلياً عند إغلاق الملف.
التصقت جميع الأسطر في سطر واحد طويل الدالة write لا تفصل بين الأسطر تلقائياً. أنت المطالب بإضافة رمز \n.
اختفت كل محتويات الملف القديمة تم فتح الملف باستخدام الوضع "w". استخدم الوضع "r" للقراءة والوضع "a" للإضافة في النهاية.
Step 4 of 6 — Predict
Check your understanding
repr is used, so nothing is hidden. What is printed?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- B'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- C'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
The file is opened with "w" but nothing is written. What is printed?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened ''
- Bopened 'important work\n'
- Copened 'opened\n'
- DA `FileExistsError`
Two things are written separately. What is in the file?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'penbag'
- B'pen\nbag\n'
- C'pen bag'
- D'pen\nbag'
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.
دورك الآن
أنشئ ملفاً باسم people.txt يحتوي على اسم ورقم في كل سطر مفصولين بفاصلة — على الأقل أربعة أسطر، واترك سطراً فارغاً في المنتصف عن عمد.
ثم اكتب سكربت summary.py يحتوي على ثلاث دوال: دالة تقرأ الملف إلى قائمة من الأزواج، ودالة تحول تلك القائمة إلى أسطر نصية منسقة (توضح من يملك أكبر رقم، والمجموع الإجمالي)، ودالة main لكتابة الناتج في ملف summary.txt. لا تنسَ إضافة if __name__ == "__main__":.
ثم أجرِ هذه التجارب الخمس:
- احذف
.strip(). ما الخطأ الذي سيظهر، ولماذا؟ - احذف الرقم من أحد الأسطر بحيث تبقى الفاصلة دون رقم بعدها. اقرأ رسالة الخطأ الناتجة.
- غيّر
"w"إلى"a"في دالةmain، ثم شغّل البرنامج ثلاث مرات متتالية. ماذا حدث لمحتوىsummary.txt؟ - جرّب فتح
people.txtباستخدام الوضع"w"... توقف، لا تفعل ذلك في الملف الأصلي! انسخ الملف أولاً وجرّب على النسخة، ثم انظر إلى ما تبقى منها. - اطبع
Path("people.txt").resolve()، ثم شغّل البرنامج من مجلد آخر. هل يتغير المسار؟
التجربة الأخيرة تفسر خطأ يقع فيه الجميع تقريباً: البرنامج يعمل بنجاح، ثم يُشغّل من مكان آخر فيتوقف ملقياً FileNotFoundError.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz