رفتن به محتوا
پایتون از صفرپایتون در کار واقعیدرس ۶ از ۱۰

فایل و JSON

داده را روی دیسک بخوان و بنویس: open با with و حالت‌های r و w و a، encoding=utf-8، خواندن خط به خط، تحلیل یک لاگ واقعی، pathlib برای مسیرها، فایل CSV، و JSON با json.dumps و json.load و ensure_ascii=False؛ همراه خطاهای FileNotFoundError و PermissionError.

  • حدود ۲۱ دقیقه مطالعه
  • ۸ مثال اجراشده
  • ۳ تمرین با جواب
  • مقدماتی تا متوسط
  • به‌روزرسانی: ۲۹ مهر ۱۴۰۵

توی این درس یاد می‌گیری برنامه‌ات داده را نگه دارد: در فایل بنویسد و بعداً بخواند. با open و دستور with فایل را باز می‌کنی، فرق حالت‌های r (خواندن)، w (نوشتن از نو) و a (اضافه به آخر) را می‌فهمی و یاد می‌گیری چرا برای فارسی باید encoding="utf-8" بدهی. یک لاگ وب‌سرور واقعی را خط به خط تحلیل می‌کنی، با pathlib مسیرها را تمیز می‌سازی، و دو قالب پرکاربرد داده را یاد می‌گیری: CSV (جدول) و JSON (همان زبانی که APIها و فایل‌های تنظیمات با آن حرف می‌زنند).

مسئله: همه‌چیز با بسته شدن برنامه پاک می‌شود

Section titled “مسئله: همه‌چیز با بسته شدن برنامه پاک می‌شود”

هر چه تا اینجا در متغیرها گذاشتی، در حافظه (RAM) بود؛ برنامه که تمام شود، همه می‌رود. برنامه‌ی واقعی باید داده را ماندگار کند (روی دیسک) و داده‌ای را که برنامه‌های دیگر ساخته‌اند بخواند: لاگ‌ها، فایل‌های تنظیمات، خروجی یک API، گزارش اکسل.

تشبیه: دفترچه‌ای که باید باز و بسته شود

Section titled “تشبیه: دفترچه‌ای که باید باز و بسته شود”

کار با فایل مثل کار با یک دفترچه است: باز می‌کنی (open)، می‌خوانی یا می‌نویسی، و می‌بندی. اگر دفترچه را باز رها کنی، ممکن است آخرین نوشته‌هایت ثبت نشود. with مثل دستیاری است که بعد از کارت همیشه دفترچه را می‌بندد، حتی اگر وسط کار حادثه‌ای (خطا) پیش بیاید. حالت هم مثل این است که بگویی «فقط می‌خوانم» (r)، «پاکش کن و از نو بنویس» (w) یا «به صفحه‌ی آخر اضافه کن» (a).

with فایل را باز می‌کند، بلوک تورفته با آن کار می‌کند، و با تمام شدن بلوک (یا بروز خطا) فایل خودکار بسته می‌شود.

همه را در ~/py اجرا کن.

مثال ۱: نوشتن، اضافه کردن، خواندن

Section titled “مثال ۱: نوشتن، اضافه کردن، خواندن”
notes.py
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("خط اول\n")
f.write("خط دوم\n")
with open("notes.txt", "a", encoding="utf-8") as f:
f.write("خط سوم (اضافه‌شده)\n")
with open("notes.txt", encoding="utf-8") as f:
content = f.read()
print(content)
print(repr(content))
خروجی
خط اول
خط دوم
خط سوم (اضافه‌شده)
'خط اول\nخط دوم\nخط سوم (اضافه‌شده)\n'
  • open(اسم, حالت, encoding=...) فایل را باز می‌کند و as f یک اسم کوتاه به آن می‌دهد.
  • "w" فایل را از نو می‌سازد (اگر بود، محتوایش پاک می‌شود). "a" به آخرش اضافه می‌کند. بدون حالت، پیش‌فرض "r" (فقط خواندن) است.
  • write برخلاف print خودش به خط بعد نمی‌رود؛ \n را خودت بگذار.
  • read() کل فایل را یک متن برمی‌گرداند. repr نشان می‌دهد واقعاً داخلش چیست: \n همان خط جدید است و ‌ همان نیم‌فاصله‌ی فارسی (درس متن و encoding در دوره‌ی مبانی).

دستورهای ترمینال (cat notes.txt) هم همین فایل را می‌بینند؛ فایلی که پایتون می‌نویسد یک فایل متنی معمولی است.

برای فایل‌های بزرگ (یک لاگ چندگیگابایتی) read() کل فایل را در حافظه می‌ریزد. بهتر است خط به خط بخوانی؛ for روی فایل همین کار را می‌کند:

lines.py
with open("notes.txt", encoding="utf-8") as f:
for i, line in enumerate(f, start=1):
print(i, line.rstrip("\n"))
خروجی
1 خط اول
2 خط دوم
3 خط سوم (اضافه‌شده)

هر line شامل \n آخرش است؛ rstrip("\n") آن را برمی‌دارد تا print دو خط جدید چاپ نکند. (strip() بدون آرگومان فاصله‌های اول و آخر را هم برمی‌دارد.)

مثال ۳: تحلیل یک لاگ واقعی

Section titled “مثال ۳: تحلیل یک لاگ واقعی”

یک فایل access.log با شکل لاگ nginx بساز (با nano، یا اگر nginx داری، از /var/log/nginx/access.log کپی کن):

access.log
10.0.0.1 - - [21/Oct/2026:10:00:01 +0000] "GET / HTTP/1.1" 200 512
10.0.0.2 - - [21/Oct/2026:10:00:02 +0000] "GET /login HTTP/1.1" 200 1024
10.0.0.1 - - [21/Oct/2026:10:00:03 +0000] "GET /admin HTTP/1.1" 403 128
10.0.0.3 - - [21/Oct/2026:10:00:04 +0000] "GET /old HTTP/1.1" 404 64
10.0.0.1 - - [21/Oct/2026:10:00:05 +0000] "POST /login HTTP/1.1" 500 32
10.0.0.2 - - [21/Oct/2026:10:00:06 +0000] "GET /missing HTTP/1.1" 404 64
logstat.py
counts = {}
ips = set()
with open("access.log", encoding="utf-8") as f:
for line in f:
parts = line.split()
ip, code = parts[0], parts[8]
ips.add(ip)
counts[code] = counts.get(code, 0) + 1
print("کدها:", counts)
print("IPهای متفاوت:", len(ips))
خروجی
کدها: {'200': 2, '403': 1, '404': 2, '500': 1}
IPهای متفاوت: 3

split() هر خط را با فاصله می‌شکند؛ IP کلمه‌ی اول ([0]) و کد وضعیت کلمه‌ی نهم ([8]) است (بشمار!). بعد همان الگوهای درس لیست و دیکشنری: شمارش با get و حذف تکراری با set. این همان کاری است که در دوره‌ی لینوکس با awk '{print $9}' | sort | uniq -c می‌کردی.

⚡ بررسی سریع

open("data.txt", "w") روی فایلی که از قبل هست چه می‌کند؟

برای کار با مسیرها (وجود داشتن، اسم، پسوند، ساختن پوشه) ماژول pathlib تمیزترین راه است:

paths.py
from pathlib import Path
p = Path("notes.txt")
print(p.exists(), p.name, p.suffix, p.stat().st_size)
Path("out").mkdir(exist_ok=True)
Path("out/hello.txt").write_text("سلام\n", encoding="utf-8")
print(Path("out/hello.txt").read_text(encoding="utf-8").strip())
for f in sorted(Path(".").glob("*.py")):
print(f)
print(Path("/var/log") / "nginx" / "access.log")
خروجی (بخش فایل‌های .py به پوشه‌ی تو بستگی دارد)
True notes.txt .txt 58
سلام
lines.py
logstat.py
notes.py
paths.py
/var/log/nginx/access.log
کد کار
p.exists() آیا وجود دارد؟
p.name / p.suffix / p.parent اسم / پسوند / پوشه‌ی بالایی
p.stat().st_size اندازه به بایت (۵۸: حرف‌های فارسی هر کدام دو بایت‌اند)
Path("x").mkdir(exist_ok=True) ساختن پوشه (mkdir -p)
p.read_text() / p.write_text(s) خواندن / نوشتن کل فایل در یک خط
Path(".").glob("*.py") پیدا کردن فایل‌ها با الگو (مثل ls *.py)
Path("/var/log") / "nginx" چسباندن مسیرها با / (بدون نگرانی از اسلش اضافه)

from pathlib import Path یعنی «از ماژول pathlib فقط Path را بیاور» (درس بعد). write_text و read_text خودشان فایل را باز و بسته می‌کنند، پس برای فایل‌های کوچک لازم نیست with بنویسی.

مثال ۵: فایل‌های تنظیمات سیستم

Section titled “مثال ۵: فایل‌های تنظیمات سیستم”

پایتون هر فایل متنی را می‌خواند؛ مثلاً همان /etc/os-release درس اول لینوکس:

osinfo.py
info = {}
with open("/etc/os-release") as f:
for line in f:
line = line.strip()
if "=" in line:
key, value = line.split("=", 1)
info[key] = value.strip('"')
print(info["PRETTY_NAME"], "|", info["VERSION_CODENAME"])
خروجی
Ubuntu 24.04.5 LTS | noble

split("=", 1) فقط در اولین = می‌شکند (اگر مقدار خودش = داشت، خراب نمی‌شود)، و strip('"') کوتیشن‌های دور مقدار را برمی‌دارد. حالا کل فایل در یک دیکشنری است.

CSV (مقدارهای جداشده با ویرگول) ساده‌ترین قالب جدول است و اکسل هم بازش می‌کند:

cart.py
import csv
rows = [["name", "price", "qty"], ["کتاب", "120000", "2"], ["خودکار", "15000", "5"]]
with open("cart.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(rows)
total = 0
with open("cart.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
total += int(row["price"]) * int(row["qty"])
print(f"جمع: {total:,}")
خروجی
جمع: 315,000
Terminal window
cat cart.csv
خروجی
name,price,qty
کتاب,120000,2
خودکار,15000,5

csv.DictReader خط اول را سرستون می‌گیرد و هر سطر را یک دیکشنری با همان کلیدها می‌دهد (row["price"]). همه‌ی مقدارها متن‌اند، پس int() لازم است. چرا ماژول csv و نه split(",")؟ چون اگر یک مقدار خودش ویرگول داشته باشد (مثلاً "تهران، خیابان آزادی")، csv آن را درست داخل کوتیشن می‌گذارد و درست می‌خواند. newline="" را ماژول csv توصیه می‌کند تا خط‌ها روی همه‌ی سیستم‌ها درست باشند.

مثال ۷: JSON، زبان مشترک برنامه‌ها

Section titled “مثال ۷: JSON، زبان مشترک برنامه‌ها”

JSON شکل متنی همان لیست‌ها و دیکشنری‌های پایتون است، و تقریباً هر API و ابزاری (از داکر تا GitHub) با آن حرف می‌زند:

json1.py
import json
server = {"name": "web1", "ip": "10.0.0.1", "ports": [80, 443], "active": True, "owner": "علی"}
text = json.dumps(server)
print(text)
print(json.dumps(server, ensure_ascii=False, indent=2))
back = json.loads(text)
print(back["ports"][1], type(back))
خروجی
{"name": "web1", "ip": "10.0.0.1", "ports": [80, 443], "active": true, "owner": "\u0639\u0644\u06cc"}
{
"name": "web1",
"ip": "10.0.0.1",
"ports": [
80,
443
],
"active": true,
"owner": "علی"
}
443 <class 'dict'>
  • json.dumps (dump string) داده‌ی پایتون را به متن JSON تبدیل می‌کند؛ json.loads برعکس.
  • دقت کن True پایتون در JSON می‌شود true (کوچک)، و None می‌شود null.
  • پیش‌فرض، حرف‌های غیرانگلیسی به شکل \u0639... نوشته می‌شوند (درست است ولی ناخوانا). ensure_ascii=False فارسی را همان‌طور نگه می‌دارد و indent=2 خروجی را مرتب و خوانا می‌کند.
JSON و پایتون تقریباً یک ساختار دارند؛ dumps و dump به JSON می‌برند و loads و load برمی‌گردانند.

مثال ۸: ذخیره و بازیابی JSON در فایل

Section titled “مثال ۸: ذخیره و بازیابی JSON در فایل”
json2.py
import json
servers = [{"name": "web1", "disk": 72}, {"name": "db1", "disk": 91}]
with open("servers.json", "w", encoding="utf-8") as f:
json.dump(servers, f, ensure_ascii=False, indent=2)
with open("servers.json", encoding="utf-8") as f:
data = json.load(f)
for s in data:
print(s["name"], s["disk"])
خروجی
web1 72
db1 91
Terminal window
cat servers.json
خروجی
[
{
"name": "web1",
"disk": 72
},
{
"name": "db1",
"disk": 91
}
]

json.dump و json.load (بدون s) مستقیم با فایل کار می‌کنند. این ساده‌ترین «دیتابیس» است: برنامه وضعیتش را در یک فایل JSON ذخیره می‌کند و دفعه‌ی بعد از همان‌جا ادامه می‌دهد. (برای داده‌ی بزرگ یا چندکاربره، دوره‌ی SQL.)

پشت پرده: بافر و «بسته نشدن» فایل

Section titled “پشت پرده: بافر و «بسته نشدن» فایل”

وقتی write می‌کنی، پایتون داده را فوراً روی دیسک نمی‌نویسد؛ اول در یک بافر (حافظه‌ی موقت) جمع می‌کند و وقتی بافر پر شد یا فایل بسته شد، یک‌جا می‌نویسد، چون نوشتن روی دیسک کند است. اگر فایل را نبندی و برنامه ناگهان بمیرد، ممکن است آخرین نوشته‌ها هیچ‌وقت به دیسک نرسند. برای همین with مهم است: با تمام شدن بلوک، حتی اگر خطایی رخ داده باشد، فایل بسته و بافر خالی می‌شود.

و encoding چه می‌کند؟ فایل روی دیسک فقط بایت است (درس بیت و بایت). encoding="utf-8" به پایتون می‌گوید این بایت‌ها را با قاعده‌ی UTF-8 به حرف تبدیل کند؛ اگر قاعده‌ی اشتباهی بدهی، همان بایت‌ها حرف‌های عجیب (یا خطا) می‌شوند. اندازه‌ی ۵۸ بایتی notes.txt هم از همین‌جاست: حرف‌های فارسی در UTF-8 دو بایت‌اند و نیم‌فاصله سه بایت.

حالت معنی اگر فایل نباشد اگر باشد
"r" (پیش‌فرض) خواندن FileNotFoundError از اول می‌خواند
"w" نوشتن می‌سازد پاک می‌کند و از نو
"a" اضافه می‌سازد به آخرش اضافه می‌کند
"x" ساختن انحصاری می‌سازد FileExistsError (برای جلوگیری از پاک شدن)
کار کد
کل فایل به متن f.read() یا Path(p).read_text()
خط به خط for line in f:
نوشتن f.write("متن\n")
JSON به داده json.load(f) / json.loads(s)
داده به JSON json.dump(d, f, ensure_ascii=False, indent=2) / json.dumps(d)
CSV csv.reader(f)، csv.DictReader(f)، csv.writer(f)
with open("nofile.txt") as f:
print(f.read())
خروجی
Traceback (most recent call last):
File "/home/ali/py/nf.py", line 1, in <module>
with open("nofile.txt") as f:
^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'nofile.txt'

همان «No such file or directory» دوره‌ی لینوکس. اسم نسبی (nofile.txt) نسبت به پوشه‌ای که برنامه را از آن اجرا کرده‌ای حساب می‌شود، نه پوشه‌ی فایل .py. راه‌حل: با pwd ببین کجایی، یا مسیر کامل بده. (در درس بعد یاد می‌گیری این خطا را بگیری.)

خروجی
PermissionError: [Errno 13] Permission denied: '/etc/shadow'

برنامه‌ی پایتون با کاربر تو اجرا می‌شود و همان دسترسی‌های دوره‌ی لینوکس را دارد. راه‌حل: فقط فایل‌هایی را بخوان که اجازه داری؛ برای فایل‌های سیستمی، یک کپی بگیر یا (فقط اگر واقعاً لازم است) sudo python3.

یک لاگ را با "w" باز کنی، هر اجرا لاگ قبلی را پاک می‌کند. راه‌حل: برای «اضافه کن» همیشه "a".

with open("notes.txt", "w", encoding="utf-8") as f:
f.write(42)
خروجی
TypeError: write() argument must be str, not int

راه‌حل: f.write(str(42)) یا f.write(f"{n}\n").

import json
json.loads("{'name': 'web1'}")
خروجی (خط آخر)
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

JSON فقط کوتیشن دوتایی (") را قبول می‌کند؛ {'name': 'web1'} شکل چاپ دیکشنری پایتون است، نه JSON. راه‌حل: JSON را با json.dumps بساز، نه دستی؛ و برای بررسی یک فایل JSON در ترمینال: python3 -m json.tool file.json.

تمرینآسان

برنامه‌ای بنویس که سه خط درباره‌ی خودت در about.txt بنویسد، بعد فایل را بخواند و تعداد خط‌ها و تعداد کل حرف‌ها را چاپ کند.

دیدن جواب
about.py
with open("about.txt", "w", encoding="utf-8") as f:
f.write("اسمم علی است.\n")
f.write("در تهران زندگی می‌کنم.\n")
f.write("پایتون یاد می‌گیرم.\n")
with open("about.txt", encoding="utf-8") as f:
lines = f.readlines()
print("تعداد خط:", len(lines))
print("تعداد حرف:", sum(len(line) for line in lines))
خروجی
تعداد خط: 3
تعداد حرف: 57

readlines() همه‌ی خط‌ها را در یک لیست می‌دهد. تعداد حرف‌ها شامل \nهای آخر خط و نیم‌فاصله‌ها هم هست.

تمرینمتوسط

تمرین اصلی: از همان access.log مثال ۳، IPهایی را پیدا کن که دست‌کم یک خطای ۴xx یا ۵xx (کد ۴۰۰ و بالاتر) داشته‌اند، و برای هر IP تعداد خطاهایش را در errors.json (با فارسی خوانا و indent=2) ذخیره کن. بعد فایل را بخوان و چاپ کن.

دیدن جواب
errors.py
import json
errors = {}
with open("access.log", encoding="utf-8") as f:
for line in f:
parts = line.split()
ip, code = parts[0], int(parts[8])
if code >= 400:
errors[ip] = errors.get(ip, 0) + 1
with open("errors.json", "w", encoding="utf-8") as f:
json.dump(errors, f, ensure_ascii=False, indent=2)
with open("errors.json", encoding="utf-8") as f:
print(f.read())
خروجی
{
"10.0.0.1": 2,
"10.0.0.3": 1,
"10.0.0.2": 1
}

int(parts[8]) کد را عدد می‌کند تا بشود با >= 400 مقایسه‌اش کرد. 10.0.0.1 یک ۴۰۳ و یک ۵۰۰ داشت.

تمرینسخت

یک «دفترچه‌ی کار» ماندگار بساز: برنامه‌ی todo.py که اگر todo.json نبود، با یک لیست خالی شروع کند، یک کار جدید (از input) به آن اضافه کند، فهرست را با شماره چاپ کند و دوباره در فایل ذخیره کند. دو بار اجرا کن و ثابت کن کار اول بعد از بسته شدن برنامه نگه داشته شده.

دیدن جواب
todo.py
import json
from pathlib import Path
path = Path("todo.json")
if path.exists():
tasks = json.loads(path.read_text(encoding="utf-8"))
else:
tasks = []
tasks.append(input("کار جدید: "))
for i, t in enumerate(tasks, start=1):
print(f"{i}. {t}")
path.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
صفحه (اجرای اول؛ جواب: خرید نان)
کار جدید: خرید نان
1. خرید نان
صفحه (اجرای دوم؛ جواب: تمرین پایتون)
کار جدید: تمرین پایتون
1. خرید نان
2. تمرین پایتون

در اجرای دوم «خرید نان» از فایل خوانده شد. این الگو (بخوان، تغییر بده، ذخیره کن) پایه‌ی هر برنامه‌ای است که باید چیزی را به خاطر بسپارد.

آزمونک
  1. چرا with open(...) as f بهتر از open تنهاست؟

  2. برای اضافه کردن یک خط به آخر لاگ، کدام حالت؟

  3. json.dumps(d, ensure_ascii=False) چه فایده‌ای دارد؟

  4. True پایتون در JSON چه می‌شود؟

  5. مقدارهایی که csv.DictReader می‌دهد چه نوعی‌اند؟

  6. FileNotFoundError با اینکه فایل کنار اسکریپت است. علت محتمل؟

  • with open(اسم, حالت, encoding="utf-8") as f: و بلوک تورفته؛ with فایل را همیشه می‌بندد.
  • حالت‌ها: "r" خواندن، "w" از نو (پاک می‌کند!)، "a" اضافه.
  • f.read() کل فایل، for line in f: خط به خط (برای فایل‌های بزرگ)، f.write("...\n") نوشتن متن.
  • pathlib.Path برای مسیرها: exists، mkdir(exist_ok=True)، glob، read_text، write_text، و / برای چسباندن.
  • CSV با ماژول csv (مقدارها متن‌اند)؛ JSON با json.dump/load و ensure_ascii=False, indent=2.
  • خطاها: FileNotFoundError (مسیر)، PermissionError (دسترسی)، JSONDecodeError (JSON نامعتبر).
برگه‌ی تقلب این درس
دستورکاری که می‌کند
with open("f.txt", encoding="utf-8") as f:باز کردن برای خواندن
open("f.txt", "w", encoding="utf-8")نوشتن از نو
open("f.txt", "a", encoding="utf-8")اضافه به آخر
for line in f: line.rstrip()خط به خط
Path("x").exists() / .mkdir(exist_ok=True)وجود / ساختن پوشه
json.load(f) / json.dump(d, f, ensure_ascii=False, indent=2)خواندن / نوشتن JSON
json.loads(s) / json.dumps(d)JSON از متن / به متن
csv.DictReader(f)خواندن CSV با سرستون
python3 -m json.tool f.jsonبررسی و مرتب‌کردن JSON در ترمینال