JSON: קריאה, כתיבה ושמירת נתונים
- להסביר מהו JSON ולמה הוא עדיף על קובץ טקסט “חופשי” לשמירת נתונים
- להכיר את ההתאמה בין טיפוסי JSON לטיפוסי Python
- לשמור ולטעון נתונים מקובץ עם
json.dumpו-json.load - להמיר בין מחרוזת JSON לאובייקט Python עם
json.dumpsו-json.loads, ולטפל בקובץ חסר או פגום
מה נלמד?
- מהו JSON ואיך הוא נראה.
- איך JSON מתאים ל-dict ול-list של Python.
- שמירה לקובץ וטעינה ממנו.
- המרה בין מחרוזת JSON לנתונים.
למה צריך את זה?
בשיעור הקודם שמרנו טקסט, ולשם כך המצאנו “פורמט” משלנו: Dana,90 בכל שורה. עכשיו נניח שלכל סטודנט יש רשימת ציונים, כתובת עם עיר ורחוב, ודגל is_active. איך שומרים את זה בשורת טקסט? כל אחד ימציא פורמט אחר, ונצטרך לכתוב קוד פענוח מסובך.
JSON הוא פורמט סטנדרטי לנתונים מובנים. כל שפת תכנות יודעת לקרוא ולכתוב אותו. ובעיקר:
- נושא OOP — הפרויקט יישמר כ-JSON.
- נושא Backend ומסדי נתונים — כל בקשה ותשובה ב-API נשלחת כ-JSON. FastAPI ממיר אוטומטית dict של Python ל-JSON ולהפך.
הסבר מהבסיס
מהו JSON?
JSON = JavaScript Object Notation. נולד ב-JavaScript, אבל היום הוא פורמט כללי. קובץ JSON הוא קובץ טקסט עם מבנה מוגדר, שנראה כמעט בדיוק כמו dict ו-list של Python:
{
"name": "Dana",
"age": 24,
"is_active": true,
"grades": [90, 85, 100],
"address": {"city": "Haifa", "street": "Herzl 10"},
"phone": null
}התאמה בין JSON ל-Python
| JSON | Python |
|---|---|
object { } | dict |
array [ ] | list |
string "text" | str |
number 42, 3.5 | int, float |
true / false | True / False |
null | None |
כללים חשובים של JSON (נבדלים מ-Python!)
- מחרוזות ומפתחות רק במירכאות כפולות
"...". מירכאות בודדות'...'לא חוקיות. true,false,nullבאותיות קטנות.- אין פסיק אחרי האיבר האחרון.
- אין הערות.
- מפתחות של object הם תמיד מחרוזות.
Serialization ו-Deserialization
| מונח | כיוון | פונקציה |
|---|---|---|
| Serialization (סריאליזציה) | נתוני Python ← טקסט JSON | json.dump / json.dumps |
| Deserialization | טקסט JSON ← נתוני Python | json.load / json.loads |
איך זוכרים: ה-s בסוף (dumps, loads) = string, עבודה מול מחרוזת. בלי s (dump, load) = עבודה מול קובץ.
ספה בסלון (dict בזיכרון) אי אפשר להעביר כמו שהיא בדלת. Serialization = לפרק אותה ולארוז בקרטון שטוח עם הוראות הרכבה (טקסט JSON), שאפשר לשמור במחסן (קובץ) או לשלוח במשאית (רשת). Deserialization = להרכיב אותה מחדש בבית החדש, ולקבל ספה זהה.
תחביר
import json
data = {"name": "Dana", "grades": [90, 85]}
# file
with open(path, "w", encoding="utf-8") as file:
json.dump(data, file, indent=2, ensure_ascii=False) # save
with open(path, "r", encoding="utf-8") as file:
data = json.load(file) # load
# string
text = json.dumps(data) # dict -> JSON string
data = json.loads(text) # JSON string -> dictindent=2— כותב JSON “יפה”, עם ירידות שורה והזחה. בלי זה, הכול בשורה אחת.ensure_ascii=False— טקסט שאינו באנגלית (עברית למשל) נשמר כמו שהוא, ולא כקודים כמוד.
דוגמת קוד ראשונה
week-2/files/save_profile.py:
import json
from pathlib import Path
PROFILE_FILE = Path(__file__).parent / "profile.json"
profile = {
"name": "Dana",
"age": 24,
"is_active": True,
"grades": [90, 85, 100],
"address": {"city": "Haifa", "street": "Herzl 10"},
"phone": None,
}
with open(PROFILE_FILE, "w", encoding="utf-8") as file:
json.dump(profile, file, indent=2, ensure_ascii=False)
with open(PROFILE_FILE, "r", encoding="utf-8") as file:
loaded = json.load(file)
print("Loaded type:", type(loaded))
print("City:", loaded["address"]["city"])
print("Average:", sum(loaded["grades"]) / len(loaded["grades"]))
print("Same as original?", loaded == profile)פלט:
Loaded type: <class 'dict'>
City: Haifa
Average: 91.66666666666667
Same as original? Trueprofile.json שנוצר:
{
"name": "Dana",
"age": 24,
"is_active": true,
"grades": [
90,
85,
100
],
"address": {
"city": "Haifa",
"street": "Herzl 10"
},
"phone": null
}פירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
import json | מודול סטנדרטי. אין צורך בהתקנה. |
json.dump(profile, file, indent=2, ...) | Serialization: הופך את ה-dict לטקסט JSON וכותב לקובץ. True הפך ל-true, ו-None ל-null. |
json.load(file) | Deserialization: קורא את הטקסט ובונה ממנו dict חדש. |
type(loaded) | dict רגיל לגמרי. אפשר לעבוד איתו כמו עם כל dict. |
loaded["address"]["city"] | גישה למבנה מקונן, כמו בנושא מבני נתונים ופונקציות. |
loaded == profile | True: הנתונים שרדו את “הסיבוב” לקובץ וחזרה בשלמותם. (אבל loaded is profile הוא False, כי זה אובייקט חדש.) |
דוגמה נוספת
“מסד נתונים” קטן בקובץ JSON — טעינה עם ברירת מחדל, הוספה, שמירה וטיפול בקובץ פגום. בדיוק התבנית שתשמש בפרויקט של נושא OOP. week-2/files/contacts_db.py:
import json
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
DB_FILE = Path(__file__).parent / "contacts.json"
def load_contacts():
if not DB_FILE.exists():
logging.info("No database file yet - starting empty")
return []
try:
with open(DB_FILE, "r", encoding="utf-8") as file:
return json.load(file)
except json.JSONDecodeError:
logging.error("Database file is corrupted - starting empty")
return []
def save_contacts(contacts):
with open(DB_FILE, "w", encoding="utf-8") as file:
json.dump(contacts, file, indent=2, ensure_ascii=False)
logging.info("Saved %s contacts", len(contacts))
def add_contact(contacts, name, phone):
contact = {"id": len(contacts) + 1, "name": name, "phone": phone}
contacts.append(contact)
return contact
def main():
contacts = load_contacts()
new_contact = add_contact(contacts, "Dana", "050-1234567")
print("Added:", new_contact)
save_contacts(contacts)
print("Total contacts in file:", len(contacts))
if __name__ == "__main__":
main()הרצה ראשונה:
INFO No database file yet - starting empty
Added: {'id': 1, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 1 contacts
Total contacts in file: 1הרצה שנייה:
Added: {'id': 2, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 2 contacts
Total contacts in file: 2פירוק:
- הקובץ מחזיק list של dicts: בדיוק “טבלה” של רשומות.
load_contactsמטפלת בשני מקרים: אין קובץ (הרצה ראשונה) ← רשימה ריקה. קובץ פגום (json.JSONDecodeError) ← רישום שגיאה ורשימה ריקה. בכל מקרה התוכנית לא קורסת."id": len(contacts) + 1— מזהה ייחודי פשוט. (שימו לב: אם נמחק איש קשר, המזהה הבא יכול לחזור על מזהה קיים. ב-Database אמיתי בנושא Backend ומסדי נתונים, המסד ייתן מזהים בעצמו, וזו בדיוק אחת הסיבות שמשתמשים בו.)- טעינה ← שינוי בזיכרון ← שמירה: התבנית הבסיסית של עבודה עם קובץ נתונים.
מחרוזת JSON — dumps / loads
לא תמיד יש קובץ. ברשת, למשל, מקבלים מחרוזת JSON:
import json
response_text = '{"id": 7, "name": "Dana", "active": true, "tags": ["new", "vip"]}'
student = json.loads(response_text)
print(student["name"], student["active"], student["tags"][1])
reply = {"status": "ok", "count": 3, "error": None}
print(json.dumps(reply))Dana True vip
{"status": "ok", "count": 3, "error": null}זה בדיוק מה ש-FastAPI יעשה בשבילנו בנושא Backend ומסדי נתונים: loads על הבקשה הנכנסת, ו-dumps על התשובה היוצאת.
טעות נפוצה
הטעות 1: JSON לא חוקי שנכתב ביד.
json.loads("{'name': 'Dana'}")json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)ב-JSON רק מירכאות כפולות. הסיבה השכיחה: מעתיקים פלט של print(dict) מ-Python, שמשתמש במירכאות בודדות, ולכן הוא לא JSON תקין. הדרך הנכונה ליצור JSON היא תמיד json.dumps, ולא str(dict).
הטעות 2: לבלבל בין dump ל-dumps.
with open(path, "w", encoding="utf-8") as file:
json.dumps(data, file)TypeError: dumps() takes 1 positional argument but 2 were givenלקובץ משתמשים ב-dump (בלי s). dumps מחזיר מחרוזת, ולא כותב לשום מקום.
הטעות 3: לשמור משהו ש-JSON לא מכיר.
json.dumps({"tags": {"a", "b"}})TypeError: Object of type set is not JSON serializableל-JSON אין set (ואין tuple: tuple נשמר כ-array ונטען בחזרה כ-list). ממירים קודם: list(my_set). אותו דבר יקרה בנושא OOP עם אובייקטים של מחלקות, ושם נלמד להמיר אותם ל-dict.
הטעות 4: מפתחות מספריים חוזרים כמחרוזות.
data = {1: "one"}
loaded = json.loads(json.dumps(data))
print(loaded) # {'1': 'one'}
print(loaded[1]) # KeyError: 1ב-JSON מפתחות הם תמיד מחרוזות, ולכן 1 הפך ל-"1". ניגשים עם loaded["1"], או מתכננים מראש מפתחות טקסט.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: מ-Python ל-JSON
לכל ערך Python — איך הוא ייראה ב-JSON?
True None "Dana" [1, 2] {"age": 24}הצג פתרון
| Python | JSON |
|---|---|
True | true |
None | null |
"Dana" | "Dana" — תמיד מירכאות כפולות |
[1, 2] | [1, 2] — מערך |
{"age": 24} | {"age": 24} — אובייקט; המפתחות תמיד מחרוזות |
תרגיל 2: שמירה וטעינה
שמרו את המילון לקובץ profile.json (עם הזחה, ובלי לקודד עברית), טענו אותו בחזרה, והדפיסו את השם ואת הציון הגבוה.
profile = {"name": "Dana", "grades": [90, 85, 100]}פלט צפוי:
Dana
100הצג פתרון
import json
profile = {"name": "Dana", "grades": [90, 85, 100]}
with open("profile.json", "w", encoding="utf-8") as file:
json.dump(profile, file, indent=2, ensure_ascii=False)
with open("profile.json", "r", encoding="utf-8") as file:
loaded = json.load(file)
print(loaded["name"])
print(max(loaded["grades"]))Dana
100dump/load— מול קובץ.indent=2— קובץ קריא לבני אדם.ensure_ascii=False— עברית נשמרת כמו שהיא.
תרגיל 3: מחרוזת JSON
התשובה הבאה הגיעה משרת כמחרוזת. המירו אותה למילון והדפיסו את השם ואם הסטודנט פעיל. אחר כך המירו את המילון reply למחרוזת JSON.
text = '{"id": 7, "name": "Dana", "active": true}'
reply = {"status": "ok", "error": None}פלט צפוי:
Dana True
{"status": "ok", "error": null}הצג פתרון
import json
text = '{"id": 7, "name": "Dana", "active": true}'
student = json.loads(text)
print(student["name"], student["active"])
reply = {"status": "ok", "error": None}
print(json.dumps(reply))Dana True
{"status": "ok", "error": null}loads / dumps (עם s — string) עובדות מול מחרוזת, לא מול קובץ. כך נראית תקשורת עם שרת.
תרגיל 4: הגדרות שנשמרות
כתבו load_settings(path) שמחזירה את ברירות המחדל {"theme": "light", "font_size": 14}, ואם הקובץ קיים — מעדכנת אותן לפי מה ששמור בו. כתבו גם save_settings(path, settings).
print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))פלט צפוי:
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}הצג פתרון
import json
from pathlib import Path
DEFAULTS = {"theme": "light", "font_size": 14}
def load_settings(path):
settings = DEFAULTS.copy()
if Path(path).exists():
with open(path, "r", encoding="utf-8") as file:
saved = json.load(file)
for key, value in saved.items():
settings[key] = value
return settings
def save_settings(path, settings):
with open(path, "w", encoding="utf-8") as file:
json.dump(settings, file, indent=2)
print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json")){'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}DEFAULTS.copy() — כדי לא לשנות את ברירות המחדל עצמן (זוכרים את ההפניות מהשיעור על Mutability?).
תרגיל 5: רשימת אנשי קשר
כתבו שתי פונקציות: add_contact(path, name, phone) שמוסיפה איש קשר לרשימה ששמורה בקובץ JSON (ויוצרת את הקובץ אם אינו קיים), ו-find_phone(path, name) שמחזירה את הטלפון או None.
add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))פלט צפוי:
052-2222222
Noneהצג פתרון
import json
from pathlib import Path
def load_contacts(path):
if not Path(path).exists():
return []
with open(path, "r", encoding="utf-8") as file:
return json.load(file)
def add_contact(path, name, phone):
contacts = load_contacts(path)
contacts.append({"name": name, "phone": phone})
with open(path, "w", encoding="utf-8") as file:
json.dump(contacts, file, indent=2, ensure_ascii=False)
def find_phone(path, name):
for contact in load_contacts(path):
if contact["name"] == name:
return contact["phone"]
return None
add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))052-2222222
Noneהקובץ מחזיק רשימה של מילונים — בדיוק המבנה מהשיעור על dict. כל שינוי: טוענים, משנים, שומרים.
סיכום
- JSON = פורמט טקסט סטנדרטי לנתונים מובנים. object ↔
dict, array ↔list,true/false/null↔True/False/None. - כללים: מירכאות כפולות בלבד, בלי פסיק אחרון, בלי הערות. מפתחות הם תמיד מחרוזות.
- קובץ:
json.dump(data, file)/json.load(file). מחרוזת:json.dumps(data)/json.loads(text). indent=2לקריאוּת,ensure_ascii=Falseלעברית.- תבנית: טען (עם ברירת מחדל) ← שנה בזיכרון ← שמור. מטפלים ב-קובץ חסר וב-
JSONDecodeError. - set ואובייקטים צריך להמיר לפני השמירה. JSON הוא השפה של כל API בנושא Backend ומסדי נתונים.
דוקומנטציה רשמית
- Saving structured data with json — JSON בקצרה
- json.dump() — כתיבה לקובץ
- json.load() — קריאה מקובץ