Python: от данных до своей сводки Урок 27 из 56
Чтение и запись: CSV, JSON и SQL одной строкой
Двадцать шестой урок курса по Python. Чужой файл — это разделитель, запятая в дробях, BOM и своя метка пропуска; в цикле про каждую мелочь надо помнить самому, в `read_csv` это аргументы. Разбираем `read_csv`, `to_csv`, `read_sql` и `to_sql`, а заодно то, чего файл про таблицу не помнит.
Зачем это нужно
Двенадцатый урок научил читать CSV модулем csv: строка за строкой, каждое число из текста руками. Это правильный инструмент, когда файл один и небольшой.
Но чужие файлы приходят разными. Из Excel — с точкой с запятой вместо запятой и с запятой внутри дробей. Из бухгалтерии — с пробелом в тысячах и с «н/д» вместо пустоты. Из старой системы — с BOM в начале. Каждая из этих мелочей в цикле превращается в отдельную строчку кода, которую надо помнить, а забытая — в тихо неверный ответ.
В pandas всё это — аргументы одной функции. Урок про то, какие именно.
Сразу целиком
Файл formaty.py. Одна таблица уезжает в три формата и возвращается обратно.
"""Урок 26: чтение и запись — CSV, JSON, SQL.
Одна и та же таблица уезжает в три формата и возвращается обратно. Смотрим, что
переживает дорогу, а что теряется по пути.
"""
import sqlite3
from pathlib import Path
import pandas as pd
HERE = Path(__file__).resolve().parent
# Инфляция, % за год. Данные Всемирного банка; 2025 год ещё не посчитан.
df = pd.DataFrame(
{"KZ": [8.0, 15.0, 14.5, 8.7, None], "UZ": [10.8, 11.4, 10.0, 9.6, None]},
index=[2021, 2022, 2023, 2024, 2025],
)
df.index.name = "year"
print(df)
print()
print("== CSV: одна строка вместо цикла")
csv_path = HERE / "inflation.csv"
df.to_csv(csv_path)
print(csv_path.read_text(encoding="utf-8"), end="")
print()
print("== и обратно")
plain = pd.read_csv(csv_path)
print("столбцы:", list(plain.columns), "| подписи:", plain.index.tolist())
back = pd.read_csv(csv_path, index_col="year")
print("столбцы:", list(back.columns), "| подписи:", back.index.tolist())
print("совпало с исходной:", back.equals(df))
print()
print("== пропуск остался пропуском")
print("пустых ячеек:", int(back.isna().sum().sum()), "| в файле это:", repr(csv_path.read_text(encoding="utf-8").splitlines()[-1]))
print()
print("== чего файл не помнит: тип")
rates = pd.DataFrame({"day": pd.to_datetime(["2026-01-15", "2026-02-15"]), "rate": [512.3, 519.8]})
rates_path = HERE / "rates.csv"
rates.to_csv(rates_path, index=False)
print(rates_path.read_text(encoding="utf-8"), end="")
print("просто прочитали:", dict(pd.read_csv(rates_path).dtypes.astype(str)))
print("с parse_dates: ", dict(pd.read_csv(rates_path, parse_dates=["day"]).dtypes.astype(str)))
print()
print("== JSON: тот же ряд, другой конверт")
json_path = HERE / "inflation.json"
df.to_json(json_path, orient="index")
text = json_path.read_text(encoding="utf-8")
print("длина:", len(text), "знаков, начало:")
print(text[:88], "…")
print()
print("== SQL: таблица уходит в базу и возвращается запросом")
db = sqlite3.connect(HERE / "digest.db")
df.reset_index().to_sql("inflation", db, if_exists="replace", index=False)
high = pd.read_sql("SELECT year, KZ FROM inflation WHERE KZ > 10 ORDER BY year", db)
print(high)
db.close()
Выводит:
KZ UZ
year
2021 8.0 10.8
2022 15.0 11.4
2023 14.5 10.0
2024 8.7 9.6
2025 NaN NaN
== CSV: одна строка вместо цикла
year,KZ,UZ
2021,8.0,10.8
2022,15.0,11.4
2023,14.5,10.0
2024,8.7,9.6
2025,,
== и обратно
столбцы: ['year', 'KZ', 'UZ'] | подписи: [0, 1, 2, 3, 4]
столбцы: ['KZ', 'UZ'] | подписи: [2021, 2022, 2023, 2024, 2025]
совпало с исходной: True
== пропуск остался пропуском
пустых ячеек: 2 | в файле это: '2025,,'
== чего файл не помнит: тип
day,rate
2026-01-15,512.3
2026-02-15,519.8
просто прочитали: {'day': 'str', 'rate': 'float64'}
с parse_dates: {'day': 'datetime64[us]', 'rate': 'float64'}
== JSON: тот же ряд, другой конверт
длина: 143 знаков, начало:
{"2021":{"KZ":8.0,"UZ":10.8},"2022":{"KZ":15.0,"UZ":11.4},"2023":{"KZ":14.5,"UZ":10.0}," …
== SQL: таблица уходит в базу и возвращается запросом
year KZ
0 2022 15.0
1 2023 14.5
Разбор
Одна строка туда, одна обратно
df.to_csv(путь) пишет таблицу целиком: заголовок из имён столбцов, подписи строк первым столбцом, пропуски — пустыми ячейками. pd.read_csv(путь) читает обратно. Никакого writer.writerow в цикле и никакого int(row["year"]) — типы pandas определяет сам по содержимому.
Сравните с двенадцатым уроком: там на то же самое уходило пятнадцать строк, и каждая была местом, где можно ошибиться.
Что файл не помнит
Файл — это текст. Он не помнит две вещи, и обе стоят ошибок.
Индекс. read_csv без подсказки считает первый столбец обычным столбцом: подписи строк стали столбцом year, а вместо них появились номера [0, 1, 2, 3, 4]. Скажите index_col="year" — и год снова подпись. Проверка back.equals(df) в примере печатает True именно после этого.
Тип. 2026-01-15 в файле — строка. pandas прочитает её строкой, и day.dt.month не сработает: у строки нет месяца. parse_dates=["day"] превращает её в дату, и в выводе видно, как str становится datetime64[us]. То же с кодами: dtype={"kod": str} спасает ведущий ноль, который иначе исчезнет вместе с превращением в число.
Пропуск в этом примере дорогу пережил: пустая ячейка прочиталась как NaN, а NaN записался пустой ячейкой — потому и пустых ячеек ровно две, те же самые. Но это свойство не столбца, а везения: в текстовом столбце пустая ячейка и пустая строка неразличимы, а слова NA, N/A, nan и ещё десяток read_csv по умолчанию считает пропусками — включая настоящее сокращение страны NA, Намибии. Чем считать пропуском, задают явно: na_values=[...] и keep_default_na=False.
Аргументы, которые закрывают почти любой чужой файл
| Аргумент | Когда нужен |
|---|---|
sep=";" |
Excel в наших краях пишет через точку с запятой |
decimal="," |
512,3 — это число, а не строка |
thousands=" " |
1 200,50 — тоже число |
encoding="utf-8-sig" |
BOM в начале файла из Excel |
na_values=["н/д", "-"] |
у отправителя своя метка пропуска |
usecols=[...] |
не тащить двадцать столбцов ради двух |
index_col="year" |
подпись вместо лишнего столбца |
parse_dates=["day"] |
текст превратить в дату |
dtype={"kod": str} |
сохранить ведущий ноль |
nrows=5 |
заглянуть в гигабайтный файл, не читая его целиком |
Последний стоит привычки: прежде чем читать чужой файл целиком, прочитайте пять строк и посмотрите на dtypes. Это тридцать секунд и половина будущих недоразумений.
При записи важны три:
index=False— если подписи строк это просто номера, им нечего делать в файле;float_format="%.2f"— чтобы в отчёте не было8.041471000000001;na_rep="н/д"— если получателю нужна метка, а не пустота.
JSON: когда read_json, а когда json.load
to_json и read_json работают, когда JSON — плоская таблица, и главный вопрос там — orient: как именно разложить строки и столбцы. В примере orient="index" дал словарь «год → строка».
Но ответ банка из восемнадцатого урока — не таблица: там вложенные объекты, служебная обёртка и метаданные. Такое проще разобрать json.load, как мы и делали, а таблицу построить из готового списка словарей. Для вложенных структур есть pd.json_normalize, который раскладывает их по столбцам.
Правило по сути, а не по владельцу: плоский JSON, который и так таблица, читайте read_json; вложенный — разбирайте json.load или json_normalize. Чужой API чаще отдаёт второе, свой to_json — первое, но решает структура, а не то, кто прислал файл.
SQL: to_sql и read_sql
Соединение берётся оттуда же, откуда в двадцать первом уроке, — из sqlite3. to_sql кладёт таблицу в базу, read_sql возвращает результат запроса уже таблицей.
Это не замена двадцать второму уроку, а его продолжение. Группировку, соединение и фильтр по-прежнему лучше отдавать базе: она умеет это на данных, которые в память не влезут, и вернёт вам маленький результат вместо большого. read_sql — это дверь между двумя мирами, а не повод разлюбить GROUP BY.
Два предупреждения. Значения в запрос подставляют через params=, а не форматированием строки — почему, показано там же, в уроке про SQLite. И про соединение: sqlite3 pandas принимает как есть, для PostgreSQL и прочих подойдёт SQLAlchemy или ADBC — вызов при этом выглядит одинаково, меняется только то, что вы передали в con.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Что теряется при записи таблицы в CSV и как это вернуть при чтении?
- Когда JSON лучше читать
json.load, а неread_json? - Зачем оставлять
GROUP BYбазе, если pandas тоже умеет группировать?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Файл пришёл из Excel, а прочитали его без аргументов. Что напечатает программа?
import io
import pandas as pd
TEXT = "year;rate\n2025;512,3\n2026;519,8\n"
df = pd.read_csv(io.StringIO(TEXT))
print(df)
print("форма:", df.shape, "| столбцы:", list(df.columns))
2. Заполните пропуск. Вместо ... поставьте аргументы, чтобы получилось два столбца и настоящие числа.
# точка с запятой — разделитель, запятая — дробная часть
import io
import pandas as pd
TEXT = "year;rate\n2025;512,3\n2026;519,8\n"
df = pd.read_csv(io.StringIO(TEXT), ...)
print("форма:", df.shape, "| столбцы:", list(df.columns))
print(dict(df.dtypes.astype(str)))
print(df["rate"].sum())
3. Почините. После круга «записали — прочитали» в таблице появился лишний столбец. Уберите его одним аргументом.
# откуда взялся Unnamed: 0
import io
import pandas as pd
df = pd.DataFrame({"kind": ["еда", "связь"], "amount": [1200, 4000]})
buffer = io.StringIO()
df.to_csv(buffer)
print("прочитали:", pd.read_csv(io.StringIO(buffer.getvalue())).columns.tolist())
Задание
Обязательное. Программа сама пишет «выгрузку от бухгалтера» — файл в кодировке utf-8-sig, с точкой с запятой, пробелом в тысячах, запятой в дробях, меткой н/д и лишним столбцом примечание:
год;категория;сумма;примечание
2024;еда;1 200,50;
2024;связь;4 000,00;тариф сменился
2025;еда;1 380,75;
2025;связь;н/д;счёт не пришёл
Прочитайте её одним вызовом read_csv так, чтобы: сумма стала числом, н/д — пропуском, примечание не читалось вовсе. Напечатайте таблицу, типы столбцов и число пропусков. Затем запишите чистый CSV без индекса, положите таблицу в SQLite и получите запросом сумму по годам.
Ожидаемый вывод:
прочитано:
год категория сумма
0 2024 еда 1200.50
1 2024 связь 4000.00
2 2025 еда 1380.75
3 2025 связь NaN
типы: {'год': 'int64', 'категория': 'str', 'сумма': 'float64'}
пропусков: 1
чисто:
год,категория,сумма
2024,еда,1200.5
2024,связь,4000.0
2025,еда,1380.75
2025,связь,
по годам:
год сумма
2024 5200.50
2025 1380.75
Готово, когда: вывод совпадает построчно; всё разобрано аргументами read_csv, а не заменами в строках; сумма имеет тип float64, а не str; в чистом файле нет столбца с номерами строк; сумма по годам получена запросом, а не groupby.
На своих данных. Возьмите любой файл, который вам присылали, — выписку, выгрузку, отчёт. Прочитайте пять строк (nrows=5), посмотрите dtypes и подберите аргументы так, чтобы числа стали числами, а даты датами. Запишите результат чистым CSV.
По желанию.
- Сохраните ту же таблицу в JSON тремя разными
orientи сравните файлы. - Прочитайте свой файл с
dtype={"kod": str}и убедитесь, что ведущий ноль на месте. - Положите таблицу в SQLite и задайте ей запрос с
WHEREиGROUP BY— результат придёт таблицей.
Куда это встанет в проекте
Восьмой шаг: диск начинает говорить таблицами. saqtau.save — это to_csv с float_format и пустой ячейкой вместо пропуска, saqtau.load — read_csv с index_col="year", и с диска приходит сразу таблица, а не словарь, из которого её надо собирать. Отчёт тоже пишется to_csv(index=False).
Столбец note исчез: он существовал, чтобы словами объяснить метку n/a, а пустая ячейка говорит то же самое, и read_csv сама превращает её в NaN.
Сеть не тронута: банк отвечает одним небольшим JSON, и json.load для него по-прежнему правильный инструмент.
Долги. База у сводки пока не заведена: to_sql в проекте не используется, хотя данные для этого уже готовы. Дойдём до неё там же, где до сервера.
Ответы
Показать ответы
На вопросы
- Теряются индекс и типы. Индекс возвращается аргументом
index_col, типы —parse_datesдля дат иdtypeдля тех столбцов, где важно, чтобы текст остался текстом. Пропуски, в отличие от них, переживают дорогу сами. - Когда JSON — не таблица: вложенные объекты, служебная обёртка, метаданные вокруг данных. Тогда его разбирает
json.load, а таблица строится из готового списка словарей или черезpd.json_normalize. - Потому что база работает с данными, которые не обязаны помещаться в память, и вернёт маленький результат вместо большого. Тащить миллион строк в pandas, чтобы получить пять, — это оплатить дорогу тому, что можно было оставить дома.
К разминке
- Разделителем по умолчанию считается запятая, поэтому строка
2025;512,3разрезана по запятой на2025;512и3. Первый кусок стал подписью строки, второй — значением, и все числа в таблице неверные. Программа не упала: она «успешно» прочитала чужой файл неправильно.
year;rate
2025;512 3
2026;519 8
форма: (2, 1) | столбцы: ['year;rate']
sep=";", decimal=",". Первый аргумент разрезает строку по правильному знаку, второй объясняет, что запятая внутри числа — дробная часть.
import io
import pandas as pd
TEXT = "year;rate\n2025;512,3\n2026;519,8\n"
df = pd.read_csv(io.StringIO(TEXT), sep=";", decimal=",")
print("форма:", df.shape, "| столбцы:", list(df.columns))
print(dict(df.dtypes.astype(str)))
print(df["rate"].sum())
форма: (2, 2) | столбцы: ['year', 'rate']
{'year': 'int64', 'rate': 'float64'}
1032.1
to_csv(buffer, index=False). Подписи строк здесь — просто номера, и в файле им делать нечего; попав туда, они читаются как столбец без имени и получают его:Unnamed: 0.
import io
import pandas as pd
df = pd.DataFrame({"kind": ["еда", "связь"], "amount": [1200, 4000]})
buffer = io.StringIO()
df.to_csv(buffer)
print("с индексом:", pd.read_csv(io.StringIO(buffer.getvalue())).columns.tolist())
clean = io.StringIO()
df.to_csv(clean, index=False)
print("без индекса:", pd.read_csv(io.StringIO(clean.getvalue())).columns.tolist())
с индексом: ['Unnamed: 0', 'kind', 'amount']
без индекса: ['kind', 'amount']
К заданию
Все четыре особенности файла закрываются аргументами одного вызова: sep=";", decimal=",", thousands=" ", encoding="utf-8-sig", na_values=["н/д"] и usecols для трёх нужных столбцов. Соблазн вычистить файл заменами в тексте — ошибка: замена , на . испортит любую запятую внутри примечания, а метка пропуска у следующего отправителя будет другой.
Сумма по годам считается запросом SELECT год, SUM(сумма) ... GROUP BY год, потому что данные уже в базе, а база это умеет.
Источники
- Ввод и вывод в pandas — все форматы разом, с примерами.
- Полный список аргументов read_csv — там их больше пятидесяти; знать нужно десяток.
- Модуль sqlite3 — соединение, которое принимает
read_sql.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.