# עבודה עם קבצי טקסט: open, with, קריאה וכתיבה<no value>

## מה נלמד?

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

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

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

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

## הסבר מהבסיס

### פתיחת קובץ

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

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

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

{{< box "warn" "מצב w מוחק!" >}}
`open("data.txt", "w")` **מרוקן את הקובץ ברגע הפתיחה**, עוד לפני שכתבתם תו אחד. אם התכוונתם להוסיף, המידע הקודם כבר נמחק. להוספה תמיד `"a"`.
{{< /box >}}

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

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

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

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

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

{{< box "analogy" "אנלוגיה: ספר בספרייה" >}}
`open` = לקחת ספר מהמדף. `close` = להחזיר אותו. בלי `with`, אתם צריכים לזכור להחזיר, ואם מתקשרים אליכם באמצע (שגיאה), הספר נשאר על השולחן. `with` = ספרנית שמחזירה את הספר בשבילכם ברגע שאתם קמים מהשולחן, לא משנה למה קמתם.
{{< /box >}}

### דרכי קריאה

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

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

### כתיבה

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

### נתיב עמיד עם `pathlib`

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

```python
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` מצביע לאותו קובץ **לא משנה מאיפה מריצים**.

## תחביר

```python
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`:

```python
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))
```

**פלט:**

```text
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`:

```python
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()
```

**שלוש הרצות:**

```text
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`:

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

Rina,64
```

`week-2/files/grades_report.py`:

```python
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()
```

**פלט במסך:**

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

**`report.txt`:**

```text
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` בגלל נתיב יחסי.**

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

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

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

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

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

```python
file.write(42)
```

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

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

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

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

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

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

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

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

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

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

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

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

**פלט צפוי:**

```text
Day 1: started
Day 2: networks
```

{{< solution >}}
```python
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="")
```

```text
Day 1: started
Day 2: networks
```

- `with` סוגר את הקובץ אוטומטית, גם אם יש שגיאה.
- `write` לא מוסיף ירידת שורה בעצמו — צריך `\n`.
- `end=""` — כי התוכן כבר נגמר ב-`\n`.
{{< /solution >}}

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

```python
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")
```

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

{{< solution >}}
```python
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="")
```

```text
second
third
```

- `"w"` **מוחק** את הקובץ ומתחיל מחדש — ולכן `first` נמחק כשנפתח ב-`"w"` בפעם השנייה.
- `"a"` **מוסיף** לסוף, ולכן `third` נוסף אחרי `second`.
{{< /solution >}}

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

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

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

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

**פלט צפוי:**

```text
dana 90
avi 75
noa 88
```

{{< solution >}}
```python
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))
```

```text
dana 90
avi 75
noa 88
```

- `for line in file` — עובר על הקובץ שורה אחרי שורה.
- `strip()` — מוריד את ה-`\n` ורווחים מהקצוות.
- `split(",")` — מפרק את השורה, ו-Unpacking מכניס לשני משתנים.
{{< /solution >}}

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

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

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

**פלט צפוי:**

```text
line 2 skipped: avi,abc
Average: 85.0
```

{{< solution >}}
```python
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))
```

```text
line 2 skipped: avi,abc
Average: 85.0
```

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

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

1. מה יקרה אם ננסה לפתוח לקריאה קובץ שלא קיים?
2. כתבו פונקציה `load_tasks(path)` שמחזירה את שורות הקובץ כרשימה, ורשימה ריקה אם הקובץ לא קיים. השתמשו ב-`Path(path).exists()`.

```python
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"))
```

**פלט צפוי:**

```text
[]
['buy milk', 'call mom']
```

{{< solution >}}
1. תיזרק **`FileNotFoundError`**.
2.

```python
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"))
```

```text
[]
['buy milk', 'call mom']
```

בודקים **לפני** הפתיחה אם הקובץ קיים. בהרצה הראשונה של תוכנית עוד אין קובץ — וזה מצב תקין, לא שגיאה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- `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`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [Reading and Writing Files](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files) — open ו-with
- [Methods of File Objects](https://docs.python.org/3/tutorial/inputoutput.html#methods-of-file-objects) — read, readline ו-write
- [open()](https://docs.python.org/3/builtins/functions.html#open) — mode ו-encoding

<!-- docs:end -->
