برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۷

CSV و JSON

در فصل هجدهم فایل متنی ساده نوشتیم. ولی دنیای واقعی دو قالب استاندارد دارد: CSV برای جدول‌ها و صفحه‌گسترده‌ها، و JSON برای وب و ارتباط بین برنامه‌ها.

در این فصل می‌خوانید
  1. فایل CSV چیست
  2. خواندن CSV به شکل لیست
  3. خواندن CSV به شکل دیکشنری
  4. پردازش داده‌های CSV
  5. نوشتن CSV
  6. افزودن و تغییر جداکننده
  7. JSON چیست
  8. خواندن JSON
  9. نوشتن JSON
  10. JSON به شکل رشته
  11. چیزهایی که JSON نمی‌تواند ذخیره کند
  12. پروژه: تبدیل 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 نخوانیمش

وسوسه می‌شوید که فایل را خط به خط بخوانید و با 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

پارامتر newline='' اجباری است

هنگام باز کردن فایل 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.readercsv.DictReader
هر سطرلیستی از رشته‌هادیکشنری با کلید نام ستون
دسترسیrow[1]row['Name']
سرستونباید دستی رد شودخودکار به کلید تبدیل می‌شود
اگر ستون‌ها جابه‌جا شوندکد خراب می‌شودکد سالم می‌ماند
همه چیز در CSV متن است

حتی اگر ستون شبیه عدد باشد، آنچه می‌خوانید رشته است. '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

شرح مثال

۶. افزودن و تغییر جداکننده

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در پایتون
objectdict
arraylist
stringstr
numberint یا float
true / falseTrue / False
nullNone

تفاوت‌های مهم با پایتون:

تابعازبه
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

شرح پروژه

فایل‌های تمرینی این فصل

پوشه 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 خراب

تمرین‌های فصل بیست و هفت

  1. students.csv را بخوانید و فقط دانشجویانی که میانگینشان بالای ۸۰ است را چاپ کنید.
  2. فایل CSV تازه‌ای بنویسید که نام و میانگین هر دانشجو را داشته باشد.
  3. ستون Result با مقدار pass یا fail به فایل CSV اضافه کنید.
  4. products.json را بخوانید و کالاهای یک سازنده خاص را چاپ کنید.
  5. کالاهای یک دسته‌بندی را در فایل JSON تازه‌ای ذخیره کنید.
  6. products.json را به CSV تبدیل کنید، با برچسب‌هایی که با نقطه‌ویرگول به هم چسبیده‌اند.
  7. برنامه‌ای بنویسید که فایل JSON مخاطبان را بخواند و به کاربر اجازه دهد مخاطب اضافه کند.
  8. توضیح دهید چرا مجموعه را نمی‌شود در JSON ذخیره کرد و دو راه حل نشان دهید.
فصل قبل: کد پایتونی، بخش دوم فصل بعد: بسته‌ها، REST API و XML