CSV و JSON
در فصل هجدهم فایل متنی ساده نوشتیم. ولی دنیای واقعی دو قالب استاندارد دارد: CSV برای جدولها و صفحهگستردهها، و JSON برای وب و ارتباط بین برنامهها.
۱. فایل CSV چیست
CSV یعنی «مقدارهای جداشده با ویرگول». در عمل فقط یک فایل متنی ساده است: هر خط یک سطر جدول، و مقدارها با ویرگول از هم جدا شدهاند. خط اول معمولاً سرستون است.
StudentID,Name,Age,Grade,Math_Score,Science_Score,English_Score 1,Alice,14,9,85,90,88 2,Bob,15,10,78,82,80 3,Charlie,14,9,92,88,95
اکسل و گوگلشیت هر دو این قالب را میفهمند. پس CSV پل بین برنامه شما و دنیای صفحهگسترده است.
وسوسه میشوید که فایل را خط به خط بخوانید و با
split(',') بشکنید. برای فایلهای ساده کار میکند، ولی
اگر مقداری خودش ویرگول داشته باشد (مثل «رشت، گیلان»)، همه چیز به هم
میریزد. ماژول csv این حالتها را با گیومهگذاری درست
مدیریت میکند. همیشه از آن استفاده کنید.
۲. خواندن CSV به شکل لیست
import csv
def read_as_lists() -> None:
'''csv.reader gives every row as a LIST of strings.'''
with open('students.csv', newline='', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader) # the first line is the header
print('Header:', header)
rows = []
for row in reader: # the reader continues after the header
rows.append(row)
print(f'Total students: {len(rows)}')
for row in rows[:3]:
print(row)
خواندن به شکل لیست: نمونه 01_read_csv
هنگام باز کردن فایل CSV، حتماً newline='' بنویسید.
بدون آن، هم ماژول csv و هم سیستمعامل کاراکتر پایان
خط اضافه میکنند و در ویندوز بین هر دو سطر یک خط خالی میافتد.
این یکی از عجیبترین باگهایی است که تازهکارها با آن روبهرو
میشوند.
تابع next(reader) اولین سطر یعنی سرستون را میخواند و
مکاننما را جلو میبرد، پس حلقه بعدی از سطر داده شروع میشود.
۳. خواندن CSV به شکل دیکشنری
def read_as_dicts() -> None:
'''csv.DictReader gives every row as a DICTIONARY: much more readable.'''
with open('students.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f) # the header becomes the keys
students = []
for row in reader:
# Everything read from a CSV file is TEXT: convert what is numeric.
row['StudentID'] = int(row['StudentID'])
row['Age'] = int(row['Age'])
row['Grade'] = int(row['Grade'])
row['Math_Score'] = int(row['Math_Score'])
row['Science_Score'] = int(row['Science_Score'])
row['English_Score'] = int(row['English_Score'])
students.append(row)
for s in students[:3]:
# Use DOUBLE quotes outside when the keys use single quotes.
print(f" {s['Name']} (Grade {s['Grade']}): "
f"Math={s['Math_Score']}, Science={s['Science_Score']}")
خواندن به شکل دیکشنری: نمونه 01_read_csv
csv.reader | csv.DictReader | |
|---|---|---|
| هر سطر | لیستی از رشتهها | دیکشنری با کلید نام ستون |
| دسترسی | row[1] | row['Name'] |
| سرستون | باید دستی رد شود | خودکار به کلید تبدیل میشود |
| اگر ستونها جابهجا شوند | کد خراب میشود | کد سالم میماند |
حتی اگر ستون شبیه عدد باشد، آنچه میخوانید رشته است. '85' + 10 خطا ایجاد میکند و '85' > '9' نادرست است، چون مقایسه متنی انجام میشود. همیشه ستونهای عددی را صریحاً تبدیل کنید. این هم ساختار داده فصل پانزدهم را میسازد: لیستی از دیکشنریها.
۴. پردازش دادههای CSV
def best_student() -> None:
'''Find the student with the best total, keeping the file open only once.'''
with open('students.csv', newline='', encoding='utf-8') as f:
rows = list(csv.DictReader(f))
def total(row: dict) -> int:
return (int(row['Math_Score'])
+ int(row['Science_Score'])
+ int(row['English_Score']))
best = max(rows, key=total)
print(f" {best['Name']} with {total(best)} points")
def filter_by_grade(grade: int) -> None:
'''Show the names of the students of one grade.'''
with open('students.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
if int(row['Grade']) == grade:
print(f" {row['Name']}")
پردازش: نمونه 01_read_csv
نکته مهم: list(csv.DictReader(f)) کل فایل را در حافظه
میریزد. برای فایل کوچک عالی است، ولی برای فایل چندگیگابایتی باید خط به
خط پردازش کنید، همانطور که در filter_by_grade شده است.
مولدهای فصل قبل دقیقاً برای همین ساخته شدهاند.
۵. نوشتن CSV
FIELDNAMES = [
'StudentID', 'Name', 'Age', 'Grade',
'Math_Score', 'Science_Score', 'English_Score',
]
def write_list_data() -> None:
'''csv.writer writes rows given as LISTS.'''
rows = [
[11, 'Liam', 14, 9, 77, 81, 79],
[12, 'Mia', 15, 10, 88, 92, 90],
]
with open('students_from_lists.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(FIELDNAMES) # one row
writer.writerows(rows) # many rows at once
def write_dict_data() -> None:
'''csv.DictWriter writes rows given as DICTIONARIES.'''
students = [
{'StudentID': 14, 'Name': 'Olivia', 'Age': 14, 'Grade': 9,
'Math_Score': 95, 'Science_Score': 97, 'English_Score': 93},
]
with open('students_from_dicts.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
writer.writeheader() # the header comes from fieldnames
writer.writerows(students)
نوشتن: نمونه 02_write_csv
شرح مثال
writerowیک سطر وwriterowsچند سطر مینویسد.DictWriterبایدfieldnamesبگیرد تا بداند ترتیب ستونها چیست.writeheader()سطر سرستون را از روی همانfieldnamesمینویسد. فراموش کردنش یعنی فایلی بدون سرستون.- به خاطر داشته باشید که حالت
'w'فایل قبلی را پاک میکند.
۶. افزودن و تغییر جداکننده
def append_to_file() -> None:
'''Mode "a" adds a row without touching what is already there.'''
new_student = [17, 'Ruby', 14, 9, 89, 91, 86]
with open('students_from_lists.csv', 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(new_student)
def write_with_semicolon() -> None:
'''Some spreadsheets expect ';' instead of ','.'''
with open('students_semicolon.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f, delimiter=';')
writer.writerow(['Name', 'City'])
writer.writerow(['Ali', 'Rasht'])
writer.writerow(['Sara, the second', 'Lahijan']) # the comma is quoted
افزودن و جداکننده: نمونه 02_write_csv
در بسیاری از کشورها، از جمله وقتی ویندوز فارسی است، اکسل انتظار
نقطهویرگول دارد نه ویرگول. پارامتر
delimiter این را حل میکند. و به سطر آخر دقت کنید: مقداری
که خودش ویرگول دارد، خودکار داخل گیومه گذاشته میشود.
۷. JSON چیست
JSON قالب استاندارد تبادل داده در وب است. تقریباً هر وبسرویسی در دنیا با JSON جواب میدهد. خوبیاش برای ما این است که ساختارش تقریباً عین دیکشنری و لیست پایتون است.
| در JSON | در پایتون |
|---|---|
| object | dict |
| array | list |
| string | str |
| number | int یا float |
true / false | True / False |
null | None |
تفاوتهای مهم با پایتون:
- در JSON کلیدها حتماً باید رشته و داخل جفتگیومه باشند. تکگیومه مجاز نیست.
trueوfalseوnullبا حرف کوچک نوشته میشوند.- ویرگول اضافه بعد از آخرین عضو مجاز نیست، برخلاف پایتون.
- JSON توضیح ندارد.
| تابع | از | به |
|---|---|---|
json.load(f) | فایل | شیء پایتون |
json.dump(obj, f) | شیء پایتون | فایل |
json.loads(s) | رشته | شیء پایتون |
json.dumps(obj) | شیء پایتون | رشته |
حرف s در انتها یعنی string. این را حفظ کنید تا هیچوقت
اشتباهشان نگیرید.
۸. خواندن JSON
import json
def load_products(filename: str) -> list[dict]:
'''Load the product list from a JSON file.'''
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f) # load() reads from a FILE
return data
def filter_by_tag(products: list[dict], tag: str) -> list[dict]:
'''Return the products carrying a given tag.'''
result = []
for p in products:
if tag in p['tags']: # 'tags' is a nested list
result.append(p)
return result
def group_by_manufacturer(products: list[dict]) -> dict[str, list[str]]:
'''Return {manufacturer: [product names]}.'''
groups: dict[str, list[str]] = {}
for p in products:
groups.setdefault(p['manufacturer'], []).append(p['name'])
return groups
خواندن JSON: نمونه 03_read_json
بعد از json.load، آنچه دارید یک ساختار داده معمولی پایتون
است: لیستی از دیکشنریها که بعضی مقدارهایشان خودشان لیستاند. تمام
مهارتهای فصل پانزدهم اینجا به کار میآیند. متد
setdefault هم از فصل چهاردهم آمده و گروهبندی را یک خطی
میکند.
۹. نوشتن JSON
def save_news_to_json(news: list[dict], filename: str) -> None:
'''Write the articles into a JSON file.
indent=2 -> a readable, indented file
ensure_ascii=False-> keep the Persian letters instead of \\u06cc escapes
'''
with open(filename, 'w', encoding='utf-8') as f:
json.dump(news, f, indent=2, ensure_ascii=False)
print(f'{len(news)} news articles saved to {filename}')
نوشتن JSON: نمونه 04_write_json
بدون ensure_ascii=False، پایتون هر حرف فارسی را به
کدی مثل ی تبدیل میکند. فایل از نظر فنی درست
است ولی هیچ انسانی نمیتواند بخواندش. و بدون
indent=2، کل فایل یک خط بیپایان میشود. هر دو را
همیشه بنویسید. این هم یکی از باگهایی بود که در فایلهای اصلی این
دوره اصلاح شد.
JSON با ساختارهای تودرتو طبیعی کار میکند: لیستی از مقالهها که هر کدام لیست برچسب و لیست دیکشنری نظرها دارند. با فایل متنی ساده، ذخیره چنین چیزی کابوس است.
۱۰. JSON به شکل رشته
import json
student = {
'name': 'Ali',
'age': 20,
'scores': [18, 20, 15],
'active': True,
'address': None,
'city': 'رشت',
}
# dumps() -> a Python object becomes a JSON string
text = json.dumps(student)
print(text)
print(type(text))
# The options make it readable.
print(json.dumps(student, indent=2, ensure_ascii=False, sort_keys=True))
# loads() -> a JSON string becomes a Python object
back = json.loads(text)
print(back['name'], back['scores'][0])
print(json.loads('{"a": true, "b": null, "c": [1, 2.5]}'))
# A broken text raises JSONDecodeError (a subclass of ValueError).
try:
json.loads('{not json}')
except json.JSONDecodeError as e:
print('Invalid JSON:', e)
رشته JSON: نمونه 05_json_strings
نسخه رشتهای دقیقاً همان چیزی است که در فصل بعد، هنگام کار با
وبسرویسها، لازمتان میشود: سرور یک رشته JSON میفرستد و شما با
loads بازش میکنید. همیشه JSONDecodeError را
بگیرید، چون دادهای که از اینترنت میآید ممکن است خراب باشد.
۱۱. چیزهایی که JSON نمیتواند ذخیره کند
from datetime import date
# JSON cannot store a date, a set or an object.
data = {'day': date.today(), 'tags': {'a', 'b'}}
try:
json.dumps(data)
except TypeError as e:
print('Cannot serialise:', e)
# Solution 1: convert by hand.
print(json.dumps({'day': str(date.today()), 'tags': list({'a', 'b'})}))
# Solution 2: give dumps() a 'default' function for the unknown types.
def convert(value):
if isinstance(value, date):
return value.isoformat()
if isinstance(value, set):
return sorted(value)
raise TypeError(f'{type(value)} is not serialisable')
print(json.dumps(data, default=convert))
محدودیتها: نمونه 05_json_strings
JSON فقط شش نوع دارد: رشته، عدد، بولی، تهی، آرایه و شیء. تاریخ، مجموعه، تاپل و اشیاء کلاسهای شما جزوشان نیستند. دو راه حل وجود دارد: دستی تبدیل کنید، یا تابعی به default بدهید که پایتون هر وقت به نوع ناشناختهای رسید صدایش بزند. اگر به خاطر داشته باشید، در فصل بیست و دوم تابع asdict برای dataclass همین کار را ساده میکرد.
۱۲. پروژه: تبدیل CSV به گزارش JSON
'''A small complete tool: read a CSV file, analyse it, save the result as JSON.'''
import csv
import json
import os
CSV_FILE = 'students.csv'
JSON_FILE = 'report.json'
NUMERIC_FIELDS = ['StudentID', 'Age', 'Grade',
'Math_Score', 'Science_Score', 'English_Score']
def read_students(path: str) -> list[dict]:
'''Read the CSV file and convert the numeric columns.'''
if not os.path.exists(path):
raise FileNotFoundError(f'{path} was not found')
students: list[dict] = []
with open(path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
for field in NUMERIC_FIELDS:
try:
row[field] = int(row[field])
except (KeyError, ValueError):
raise ValueError(f'the column {field} is missing or invalid')
students.append(row)
return students
def add_computed_fields(students: list[dict]) -> None:
'''Add the total and the average to every student.'''
for student in students:
scores = [student['Math_Score'],
student['Science_Score'],
student['English_Score']]
student['Total'] = sum(scores)
student['Average'] = round(sum(scores) / len(scores), 2)
student['Passed'] = student['Average'] >= 70
خواندن و محاسبه: نمونه 06_project_csv_to_json
def build_report(students: list[dict]) -> dict:
'''Build the summary that will be saved as JSON.'''
by_grade: dict[str, list[str]] = {}
for student in students:
by_grade.setdefault(str(student['Grade']), []).append(student['Name'])
return {
'source': CSV_FILE,
'count': len(students),
'class_average': round(
sum(s['Average'] for s in students) / len(students), 2),
'best': max(students, key=lambda s: s['Total'])['Name'],
'worst': min(students, key=lambda s: s['Total'])['Name'],
'passed': [s['Name'] for s in students if s['Passed']],
'failed': [s['Name'] for s in students if not s['Passed']],
'by_grade': by_grade,
'students': students,
}
def main() -> None:
try:
students = read_students(CSV_FILE)
except (FileNotFoundError, ValueError) as e:
print('Error:', e)
return
if not students:
print('The file contains no student.')
return
add_computed_fields(students)
report = build_report(students)
with open(JSON_FILE, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
ساخت گزارش: نمونه 06_project_csv_to_json
شرح پروژه
- سه مرحله روشن: خواندن، محاسبه، گزارش. هر کدام تابع خودش را دارد.
- تابع خواندن، خطای معنادار میاندازد و
mainآن را میگیرد؛ همان تقسیم مسئولیت فصل نوزدهم. - در
build_reportتقریباً همه ابزارهای فصل بیست و پنجم و بیست و ششم دیده میشوند: درک لیستی، عبارت مولد داخلsum، وmaxباkey. - خروجی JSON هم خلاصه دارد و هم داده کامل. این الگوی رایج گزارشسازی است: کسی که فقط خلاصه میخواهد بالای فایل را میخواند، و کسی که جزئیات میخواهد پایین را.
- کلیدهای دیکشنری
by_gradeباstr()به رشته تبدیل شدهاند، چون در JSON کلید حتماً باید رشته باشد.
پوشه S27-csv-and-json شامل شش پوشه نمونه، از
01_read_csv تا
06_project_csv_to_json، بهعلاوه فایلهای داده
students.csv و products.json.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
newline='' | هنگام باز کردن CSV، اجباری |
csv.reader | هر سطر یک لیست |
csv.DictReader | هر سطر یک دیکشنری. معمولاً بهتر |
csv.writer و writerows | نوشتن سطرها از روی لیست |
csv.DictWriter و writeheader | نوشتن از روی دیکشنری |
delimiter=';' | تغییر جداکننده |
| داده CSV | همیشه متن است. عددها را تبدیل کنید |
json.load و dump | کار با فایل |
json.loads و dumps | کار با رشته |
indent=2 | فایل خوانا |
ensure_ascii=False | حفظ حروف فارسی |
default=func | ذخیره نوعهایی که JSON نمیشناسد |
JSONDecodeError | خطای متن JSON خراب |
تمرینهای فصل بیست و هفت
students.csvرا بخوانید و فقط دانشجویانی که میانگینشان بالای ۸۰ است را چاپ کنید.- فایل CSV تازهای بنویسید که نام و میانگین هر دانشجو را داشته باشد.
- ستون
Resultبا مقدارpassیاfailبه فایل CSV اضافه کنید. products.jsonرا بخوانید و کالاهای یک سازنده خاص را چاپ کنید.- کالاهای یک دستهبندی را در فایل JSON تازهای ذخیره کنید.
products.jsonرا به CSV تبدیل کنید، با برچسبهایی که با نقطهویرگول به هم چسبیدهاند.- برنامهای بنویسید که فایل JSON مخاطبان را بخواند و به کاربر اجازه دهد مخاطب اضافه کند.
- توضیح دهید چرا مجموعه را نمیشود در JSON ذخیره کرد و دو راه حل نشان دهید.