עבודה עם קבצי טקסט: open, with, קריאה וכתיבה

מטרות השיעור
  • לפתוח קובץ במצב המתאים — r, w או a — ולהבין מה כל מצב עושה לתוכן הקיים
  • להשתמש ב-with כדי שהקובץ ייסגר תמיד, גם כשיש שגיאה
  • לקרוא קובץ כולו, שורה אחר שורה, ולכתוב ולהוסיף אליו
  • לבנות נתיב לקובץ ביחס למיקום הסקריפט עם pathlib, ולטפל ב-FileNotFoundError

מה נלמד?

  • פתיחת קובץ עם open ומצבי פתיחה.
  • למה חייבים לסגור קובץ, ואיך with עושה את זה אוטומטית.
  • קריאה: כל הקובץ, שורה אחר שורה.
  • כתיבה והוספה.
  • נתיב עמיד, כזה שלא תלוי במקום שממנו מריצים (הבטחנו בנושא מחשב, מערכת הפעלה ורשת!).

למה צריך את זה?

נושא מחשב, מערכת הפעלה ורשת: “כל מה שב-RAM נעלם כשהתוכנית נגמרת”. קבצים הם הדרך הבסיסית לשמור מידע על ה-Storage: הגדרות, דוחות, היסטוריה, נתונים לייבוא וייצוא. הספירה שתמיד חזרה ל-1 ב-memory_test.py? היום נגרום לה לשרוד בין הרצות.

ובפרויקט של נושא OOP תשמרו את כל המידע של המערכת לקובץ.

הסבר מהבסיס

פתיחת קובץ

open(path, mode) מבקשת ממערכת ההפעלה (System Call!) גישה לקובץ, ומחזירה אובייקט קובץ שדרכו קוראים וכותבים.

מצבי פתיחה (Modes)

מצבשםאם הקובץ קייםאם לא קייםפעולות
"r"Read (ברירת מחדל)פותח לקריאהFileNotFoundErrorקריאה בלבד
"w"Writeמוחק את כל התוכן!יוצרכתיבה
"a"Appendשומר את התוכן, וכותב בסוףיוצרכתיבה לסוף
מצב w מוחק!

open("data.txt", "w") מרוקן את הקובץ ברגע הפתיחה, עוד לפני שכתבתם תו אחד. אם התכוונתם להוסיף, המידע הקודם כבר נמחק. להוספה תמיד "a".

למה צריך לסגור קובץ?

  • כתיבה לקובץ נאספת קודם בחוצץ (Buffer) בזיכרון, ונכתבת לדיסק בפועל רק בסגירה או כשהחוצץ מתמלא. בלי סגירה, חלק מהמידע עלול לא להישמר.
  • מערכת ההפעלה מגבילה כמה קבצים תהליך יכול להחזיק פתוחים. קבצים שלא נסגרים “דולפים”.
  • ב-Windows, קובץ פתוח לפעמים נעול, ותוכנות אחרות לא יכולות לגשת אליו.

with — סגירה אוטומטית

with open("notes.txt", "r") as file:
    content = file.read()
# here the file is already closed - always

with מבטיח שהקובץ ייסגר ברגע שיוצאים מהבלוק: בסוף רגיל, ב-return, ואפילו כשנזרקת שגיאה. זוכרים את finally מנושא Exceptions ו-Logging? with עושה בדיוק את זה, בצורה קצרה. תמיד פותחים קבצים עם with.

אנלוגיה: ספר בספרייה

open = לקחת ספר מהמדף. close = להחזיר אותו. בלי with, אתם צריכים לזכור להחזיר, ואם מתקשרים אליכם באמצע (שגיאה), הספר נשאר על השולחן. with = ספרנית שמחזירה את הספר בשבילכם ברגע שאתם קמים מהשולחן, לא משנה למה קמתם.

דרכי קריאה

דרךמחזירהמתי
file.read()מחרוזת אחת עם כל הקובץקבצים קטנים
file.readlines()רשימה של שורות (כל אחת עם \n בסוף)כשצריך את כל השורות כרשימה
for line in file:שורה אחר שורההדרך המומלצת. עובדת גם לקבצים ענקיים, כי לא טוענת הכול לזיכרון

\n הוא תו ירידת שורה (Newline). כל שורה בקובץ מסתיימת בו. line.strip() מסירה אותו (ואת הרווחים בקצוות).

כתיבה

file.write(text) כותבת בדיוק את הטקסט, בלי לרדת שורה אוטומטית (שלא כמו print). רוצים שורה חדשה? מוסיפים "\n" בעצמכם. ו-write מקבלת רק מחרוזת: מספרים ממירים קודם עם str().

נתיב עמיד עם pathlib

בנושא מחשב, מערכת הפעלה ורשת ראינו שנתיב יחסי נמדד מהתיקייה שממנה הרצתם. הפתרון המקצועי: לבנות את הנתיב ביחס למיקום הקובץ .py עצמו.

from pathlib import Path

BASE_DIR = Path(__file__).parent      # the folder this .py file is in
DATA_FILE = BASE_DIR / "data.txt"     # "/" joins path parts, on every OS
  • __file__ — משתנה מובנה: הנתיב של הקובץ הנוכחי.
  • Path(...).parent — התיקייה שמכילה אותו.
  • / בין Path למחרוזת — מחבר חלקי נתיב בצורה נכונה בכל מערכת הפעלה (\ ב-Windows, / במק).

עכשיו DATA_FILE מצביע לאותו קובץ לא משנה מאיפה מריצים.

תחביר

from pathlib import Path

path = Path(__file__).parent / "notes.txt"

# write (creates or overwrites)
with open(path, "w", encoding="utf-8") as file:
    file.write("first line\n")

# append (adds to the end)
with open(path, "a", encoding="utf-8") as file:
    file.write("another line\n")

# read everything
with open(path, "r", encoding="utf-8") as file:
    content = file.read()

# read line by line
with open(path, "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

path.exists()     # True / False

encoding="utf-8" — תמיד מציינים. בלי זה, ב-Windows ברירת המחדל היא קידוד אחר, וטקסט בעברית (או אימוג’י) ייכתב כג’יבריש או יזרוק UnicodeDecodeError.

דוגמת קוד ראשונה

week-2/files/diary.py:

from pathlib import Path

DIARY_FILE = Path(__file__).parent / "diary.txt"

with open(DIARY_FILE, "w", encoding="utf-8") as file:
    file.write("Day 1: started the course\n")
    file.write("Day 2: learned the terminal\n")

with open(DIARY_FILE, "a", encoding="utf-8") as file:
    file.write("Day 3: networks!\n")

with open(DIARY_FILE, "r", encoding="utf-8") as file:
    content = file.read()

print(content)
print("File size in characters:", len(content))

פלט:

Day 1: started the course
Day 2: learned the terminal
Day 3: networks!

File size in characters: 71

ובתיקייה נוצר הקובץ diary.txt. פתחו אותו ב-VS Code.

פירוק הקוד שורה אחר שורה

שורההסבר
Path(__file__).parent / "diary.txt"נתיב מלא לקובץ שליד הסקריפט. עובד מכל תיקייה.
open(DIARY_FILE, "w", ...)מצב כתיבה: יוצר את הקובץ, או מרוקן אותו אם קיים.
file.write("...\n")כותב טקסט. ה-\n יוצר ירידת שורה.
סוף בלוק ה-with הראשוןהקובץ נסגר והתוכן נכתב לדיסק.
open(..., "a", ...)מצב הוספה: התוכן הקיים נשמר, והכתיבה מתבצעת בסוף.
file.read()כל התוכן כמחרוזת אחת.
שורה ריקה בפלטהקובץ מסתיים ב-\n, ו-print מוסיף \n משלו.

נסו: הריצו שוב. האם הקובץ גדל? לא, כי מצב "w" בתחילת הסקריפט מרוקן אותו בכל הרצה. שנו את הפתיחה הראשונה ל-"a" והריצו פעמיים. מה קורה?

דוגמה נוספת

מונה הרצות שבאמת שורד (הבעיה מנושא מחשב, מערכת הפעלה ורשת!) + עיבוד קובץ נתונים שורה אחר שורה.

week-2/files/run_counter.py:

from pathlib import Path

COUNTER_FILE = Path(__file__).parent / "counter.txt"


def read_count():
    if not COUNTER_FILE.exists():
        return 0
    with open(COUNTER_FILE, "r", encoding="utf-8") as file:
        return int(file.read().strip())


def save_count(count):
    with open(COUNTER_FILE, "w", encoding="utf-8") as file:
        file.write(str(count))


def main():
    count = read_count() + 1
    save_count(count)
    print("This program has run", count, "times")


if __name__ == "__main__":
    main()

שלוש הרצות:

This program has run 1 times
This program has run 2 times
This program has run 3 times

עכשיו זה עובד, כי המספר נשמר על ה-Storage בין הרצות, ולא רק ב-RAM.

עיבוד קובץ ציונים. צרו week-2/files/grades.txt:

Dana,90
Avi,72
Noa,abc
Tom,85

Rina,64

week-2/files/grades_report.py:

from pathlib import Path

GRADES_FILE = Path(__file__).parent / "grades.txt"
REPORT_FILE = Path(__file__).parent / "report.txt"


def load_grades(path):
    grades = {}
    with open(path, "r", encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            line = line.strip()
            if line == "":
                continue
            name, grade_text = line.split(",")
            try:
                grades[name] = int(grade_text)
            except ValueError:
                print("Skipping line", line_number, "- bad grade:", repr(grade_text))
    return grades


def write_report(grades, path):
    with open(path, "w", encoding="utf-8") as file:
        for name, grade in grades.items():
            status = "PASS" if grade >= 65 else "FAIL"
            file.write(name + ": " + str(grade) + " " + status + "\n")
        average = sum(grades.values()) / len(grades)
        file.write("Average: " + str(round(average, 1)) + "\n")


def main():
    grades = load_grades(GRADES_FILE)
    write_report(grades, REPORT_FILE)
    print("Report written to", REPORT_FILE.name)


if __name__ == "__main__":
    main()

פלט במסך:

Skipping line 3 - bad grade: 'abc'
Report written to report.txt

report.txt:

Dana: 90 PASS
Avi: 72 PASS
Tom: 85 PASS
Rina: 64 FAIL
Average: 77.8

פירוק:

  • for line_number, line in enumerate(file, start=1) — מעבר על הקובץ שורה אחר שורה, עם מספר שורה, כדי לדווח איפה הבעיה.
  • line.strip() — מסירה את ה-\n. בלעדיה, int("90\n") דווקא עובד, אבל השם היה מגיע עם רווחים מיותרים.
  • if line == "": continue — מדלגים על שורות ריקות.
  • name, grade_text = line.split(",") — פיצול לפי פסיק, ו-Unpacking לשני משתנים.
  • "PASS" if grade >= 65 else "FAIL" — ביטוי תנאי (Conditional Expression): if/else קצר בשורה אחת, שמחזיר אחד משני ערכים.
  • REPORT_FILE.name — רק שם הקובץ, בלי הנתיב המלא.
  • Path לא רק “נתיב”. יש לו מתודות שימושיות כמו .exists() ו-.name.

טעות נפוצה

הטעות 1: "w" במקום "a" — ומחיקת נתונים. ראינו למעלה. לפני כל open(..., "w"), שאלו את עצמכם: “האם אני בסדר עם זה שהתוכן הקודם יימחק?”

הטעות 2: FileNotFoundError בגלל נתיב יחסי.

with open("grades.txt") as file:

עובד רק כשמריצים מאותה תיקייה. מכפתור Run ב-VS Code, או מתיקייה אחרת:

FileNotFoundError: [Errno 2] No such file or directory: 'grades.txt'

הפתרון: Path(__file__).parent / "grades.txt". ולקבצים שאולי לא קיימים, לבדוק .exists() או לתפוס FileNotFoundError.

הטעות 3: לכתוב מספר עם write.

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

תיקון: file.write(str(42)).

הטעות 4: לשכוח \n.

file.write("line one")
file.write("line two")

בקובץ: line oneline two, בשורה אחת. write לא מוסיף ירידת שורה.

הטעות 5: לקרוא פעמיים מאותו קובץ פתוח.

with open(path, encoding="utf-8") as file:
    first = file.read()
    second = file.read()
print(repr(second))   # ''

הקובץ “זוכר” מאיפה הוא קורא (מצביע קריאה). אחרי read() המצביע בסוף, ולכן הקריאה השנייה מחזירה מחרוזת ריקה.

תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

תרגיל 1: כתיבה וקריאה

כתבו לקובץ diary.txt שתי שורות: Day 1: started ו-Day 2: networks. אחר כך קראו את הקובץ והדפיסו את התוכן שלו.

פלט צפוי:

Day 1: started
Day 2: networks
הצג פתרון
with open("diary.txt", "w", encoding="utf-8") as file:
    file.write("Day 1: started\n")
    file.write("Day 2: networks\n")

with open("diary.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
Day 1: started
Day 2: networks
  • with סוגר את הקובץ אוטומטית, גם אם יש שגיאה.
  • write לא מוסיף ירידת שורה בעצמו — צריך \n.
  • end="" — כי התוכן כבר נגמר ב-\n.

תרגיל 2: w מול a

with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")

מה יהיה בקובץ בסוף? הסבירו.

הצג פתרון
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")

with open("log.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
second
third
  • "w" מוחק את הקובץ ומתחיל מחדש — ולכן first נמחק כשנפתח ב-"w" בפעם השנייה.
  • "a" מוסיף לסוף, ולכן third נוסף אחרי second.

תרגיל 3: שורה אחרי שורה

בקובץ grades.txt כל שורה היא name,grade. כתבו קוד שקורא את הקובץ שורה אחרי שורה, ומדפיס כל שם עם הציון שלו. דלגו על שורות ריקות.

צרו את הקובץ כך:

with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")

פלט צפוי:

dana 90
avi 75
noa 88
הצג פתרון
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")

with open("grades.txt", "r", encoding="utf-8") as file:
    for line in file:
        line = line.strip()
        if not line:
            continue
        name, grade_text = line.split(",")
        print(name, int(grade_text))
dana 90
avi 75
noa 88
  • for line in file — עובר על הקובץ שורה אחרי שורה.
  • strip() — מוריד את ה-\n ורווחים מהקצוות.
  • split(",") — מפרק את השורה, ו-Unpacking מכניס לשני משתנים.

תרגיל 4: קלט פגום

אותו קובץ ציונים, אבל עכשיו יש בו שורה פגומה. קראו את הקובץ: שורה שהציון בה אינו מספר — דלגו עליה והדפיסו הודעה עם מספר השורה. בסוף הדפיסו את הממוצע של הציונים התקינים.

with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")

פלט צפוי:

line 2 skipped: avi,abc
Average: 85.0
הצג פתרון
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")

grades = []
with open("grades.txt", "r", encoding="utf-8") as file:
    for number, line in enumerate(file, start=1):
        line = line.strip()
        name, grade_text = line.split(",")
        try:
            grades.append(int(grade_text))
        except ValueError:
            print("line", number, "skipped:", line)

print("Average:", sum(grades) / len(grades))
line 2 skipped: avi,abc
Average: 85.0

enumerate(file, start=1) נותן גם את מספר השורה. try/except מהשיעורים הקודמים מאפשר לדלג על שורה פגומה בלי לקרוס.

תרגיל 5: קובץ שלא קיים

  1. מה יקרה אם ננסה לפתוח לקריאה קובץ שלא קיים?
  2. כתבו פונקציה load_tasks(path) שמחזירה את שורות הקובץ כרשימה, ורשימה ריקה אם הקובץ לא קיים. השתמשו ב-Path(path).exists().
print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))

פלט צפוי:

[]
['buy milk', 'call mom']
הצג פתרון
  1. תיזרק FileNotFoundError.
from pathlib import Path


def load_tasks(path):
    if not Path(path).exists():
        return []
    tasks = []
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            tasks.append(line.strip())
    return tasks


print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))
[]
['buy milk', 'call mom']

בודקים לפני הפתיחה אם הקובץ קיים. בהרצה הראשונה של תוכנית עוד אין קובץ — וזה מצב תקין, לא שגיאה.

סיכום

  • open(path, mode, encoding="utf-8"): "r" קריאה (ברירת מחדל), "w" כתיבה שמוחקת, "a" הוספה לסוף.
  • תמיד with: הקובץ נסגר אוטומטית, גם כשיש שגיאה.
  • קריאה: read() (הכול), for line in file (שורה אחר שורה, מומלץ). strip() מסירה את \n.
  • write כותבת רק str, ובלי ירידת שורה. מוסיפים "\n" בעצמכם.
  • נתיב עמיד: Path(__file__).parent / "file.txt". בודקים קיום עם .exists() או תופסים FileNotFoundError.

דוקומנטציה רשמית