Python: от данных до своей сводки Урок 12 из 56
CSV: запятая внутри поля и метка, которую ставит Excel
Двенадцатый урок курса по Python. `split(",")` на строке `2022,15.0,"шок, война, логистика"` даёт пять кусков вместо трёх — измерено. Модуль `csv` даёт три. Плюс `DictReader`, обязательный `newline=""` и метка из Excel, из-за которой `row["year"]` отвечает `KeyError`, хотя колонка на месте.
Зачем это нужно
В прошлом уроке мы разбирали строку через partition(";"), и это работало, пока данные были простые. Настоящая выгрузка простой не бывает.
В поле попадает запятая: «шок, война, логистика». Появляются кавычки, а внутри кавычек — снова запятая. Иногда в ячейку попадает перевод строки, и одна запись занимает две строки файла.
Всё это — законный CSV, и всё это ломает split. Модуль csv знает правила формата целиком, поэтому выгрузку читают им, а не руками.
Сразу целиком
Файл csvdemo.py. Запуск: python csvdemo.py из окружения.
Обязательное — запись через csv.writer и чтение через csv.DictReader. Средний блок — доказательство: там видно, чем split отличается от чтения по правилам.
"""Урок 12: CSV — не «строка с запятыми», а формат со своими правилами.
Запятая внутри поля, кавычки, перевод строки в ячейке — всё это законно.
Поэтому выгрузку читают модулем csv, а не split(",").
"""
import csv
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "vygruzka.csv"
rows = [
["year", "value", "note"],
["2021", "8.0", "обычный год"],
["2022", "15.0", "шок, война, логистика"],
["2023", "n/a", "данных нет"],
]
# newline="" обязателен: перевод строки внутри файла ставит сам модуль csv.
with data.open("w", encoding="utf-8", newline="") as target:
csv.writer(target).writerows(rows)
print("== что лежит в файле")
print(data.read_text(encoding="utf-8"), end="")
print()
print("== split против csv.reader на третьей строке")
line = data.read_text(encoding="utf-8").splitlines()[2]
print("split(','): ", line.split(","))
with data.open(encoding="utf-8", newline="") as source:
print("csv.reader: ", list(csv.reader(source))[2])
print()
print("== DictReader: строка как словарь")
total = 0.0
count = 0
with data.open(encoding="utf-8", newline="") as source:
for row in csv.DictReader(source):
if row["value"] == "n/a":
print(f"{row['year']}: пропуск — {row['note']}")
continue
total += float(row["value"])
count += 1
print(f"{row['year']}: {row['value']}% — {row['note']}")
print(f"строк взято: {count}, среднее {total / count:.2f}%")
data.unlink()
Выводит:
== что лежит в файле
year,value,note
2021,8.0,обычный год
2022,15.0,"шок, война, логистика"
2023,n/a,данных нет
== split против csv.reader на третьей строке
split(','): ['2022', '15.0', '"шок', ' война', ' логистика"']
csv.reader: ['2022', '15.0', 'шок, война, логистика']
== DictReader: строка как словарь
2021: 8.0% — обычный год
2022: 15.0% — шок, война, логистика
2023: пропуск — данных нет
строк взято: 2, среднее 11.50%
Разбор
Доказательство в одну строку
split(','): ['2022', '15.0', '"шок', ' война', ' логистика"']
csv.reader: ['2022', '15.0', 'шок, война, логистика']
Пять кусков вместо трёх, обрывки кавычек по краям и пробелы в начале. Дальше по программе это поедет как «значение» и «примечание», и никакая проверка типов не спасёт: строки-то настоящие.
csv.reader знает правило: запятая внутри кавычек — часть значения, а не разделитель. Он же снимает сами кавычки, потому что они принадлежат формату, а не данным.
Образ. Адрес «Алматы, ул. Абая, 10» в анкете. Человек видит один адрес; программа, режущая по запятым, — три поля. Кавычки в CSV и есть та скобка, которой в анкете нет.
Кавычки ставит писатель, а не вы
Посмотрите на файл: третью строку csv.writer записал в кавычках сам, остальные — без. Правило простое: кавычки появляются там, где без них файл нельзя было бы прочитать однозначно — если внутри значения есть разделитель, кавычка или перевод строки.
Отсюда практический вывод: не собирайте CSV склейкой строк. ",".join(values) даст файл, который сломается на первом же значении с запятой — и сломается не у вас, а у того, кто его откроет.
newline="" — не украшение
with data.open("w", encoding="utf-8", newline="") as target:
Модуль csv сам решает, чем заканчивать строку, и ждёт, что файл ему не помешает. Без newline="" Python добавит собственный перевод строки поверх — в Windows между записями появятся пустые строки, а файл, открытый в Excel, будет через одну.
Правило запоминают целиком: файл для csv открывают с newline="" — и на запись, и на чтение. Это написано в документации модуля первым же примечанием.
DictReader: колонка по имени, а не по номеру
for row in csv.DictReader(source):
... row["value"] ...
csv.reader отдаёт список: row[1] — второе поле. Стоит источнику вставить колонку в середину — и весь разбор поедет молча.
DictReader берёт первую строку как заголовок и отдаёт словарь: row["value"] найдёт колонку, где бы она ни стояла. Для чужих выгрузок это правильный выбор по умолчанию: они меняются без предупреждения.
Рядом живёт csv.DictWriter — он пишет словари обратно и требует заранее назвать fieldnames, то есть договориться о порядке колонок.
Разделитель бывает не запятая
В Казахстане и России выгрузка из Excel чаще приходит с точкой с запятой: в локали, где десятичный разделитель — запятая, 1,5 иначе развалилось бы на две ячейки. Модуль об этом знает:
csv.reader(source, delimiter=";")
csv.DictReader(source, delimiter=";")
Есть и csv.Sniffer, который пытается угадать разделитель по началу файла. Угадывает он неплохо, но на маленьком или странном файле ошибается — а молчаливая ошибка в разборе хуже явной. Надёжнее посмотреть на файл глазами один раз и написать разделитель прямо.
Метка Excel, из-за которой пропадает первая колонка
Excel сохраняет CSV с невидимой меткой в начале файла:
байты начала: b'\xef\xbb\xbfyea'
Эти три байта — BOM, «метка порядка байтов». Прочитайте такой файл как обычный utf-8 — и она приклеится к первому имени колонки:
ключи как utf-8: ['year', 'value']
row['year'] → KeyError 'year'
Колонка на месте, глазами всё видно, а программа её не находит. Лечится одной буквой в имени кодировки:
ключи как utf-8-sig: ['year', 'value'] | row['year'] = 2021
utf-8-sig — та же UTF-8, но метку в начале она съедает. Для файлов, пришедших из Excel, её и берут; при записи, наоборот, оставляют обычный utf-8, чтобы не добавлять метку самим.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
split(",")разобрал строку на пять кусков вместо трёх? - Чем
DictReaderлучшеreader, когда файл пришёл из чужого источника? - Что такое BOM и почему из-за него
row["year"]даётKeyError?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Сколько кусков получится?
line = '2022,15.0,"шок, война, логистика"'
print(len(line.split(",")))
2. Заполните пропуск. Файл сохранён Excel. Вместо ... поставьте кодировку, при которой имя первой колонки не приедет с меткой.
import csv
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "excel.csv"
data.write_bytes("year,value\n2021,8.0\n".encode("utf-8-sig"))
with data.open(encoding=..., newline="") as source:
for row in csv.DictReader(source):
print(row["year"])
data.unlink()
3. Почините. Строка собрана склейкой, и при чтении полей стало больше, чем записывали. Соберите её так, как положено.
import csv
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "vygruzka.csv"
row = ["2022", "15.0", "шок, война, логистика"]
data.write_text(",".join(row) + "\n", encoding="utf-8")
with data.open(encoding="utf-8", newline="") as source:
for back in csv.reader(source):
print("полей:", len(back))
data.unlink()
Задание
Обязательное. Дано:
rows = [
["month", "price", "note"],
["январь", "520", "обычный месяц"],
["февраль", "546,5", "шок, спрос, логистика"],
["март", "n/a", "цены нет"],
]
Запишите это в vygruzka.csv через csv.writer с newline="". Напечатайте, сколько кусков даёт split(",") на третьей строке файла и сколько полей видит csv.reader. Затем пройдите файл DictReader: месяц с n/a печатайте как пропуск с примечанием, остальные — с ценой и примечанием, а в конце напечатайте, сколько месяцев взято, и среднее с двумя знаками (запятая в числе считается точкой). Файл за собой уберите.
Ожидаемый вывод:
split(','): 6 кусков
csv.reader: 3 поля
январь: 520 — обычный месяц
февраль: 546,5 — шок, спрос, логистика
март: пропуск — цены нет
месяцев взято: 2, среднее 533.25
Готово, когда: вывод совпадает построчно; файл собран csv.writer, а не склейкой строк; колонки взяты по именам, а не по номерам; newline="" стоит и на записи, и на чтении.
На своих данных. Соберите csv.writer файл из своих данных, где в одном поле обязательно есть запятая. Прочитайте его двумя способами: split(",") и csv.reader — и напечатайте оба результата рядом. Затем пройдите файл DictReader и посчитайте среднее по числовой колонке, пропуская n/a.
Всё это собрано в step-5 — сверьтесь после того, как напишете сами.
По желанию.
- Запишите файл с
delimiter=";"и откройте его в Excel или LibreOffice. - Сохраните из Excel файл с кириллицей и прочитайте его сначала как
utf-8, потом какutf-8-sig. - Уберите
newline=""при записи и посмотрите на файл в редакторе, который показывает невидимые символы.
Куда это встанет в проекте
Сводка перестаёт зависеть от того, кто и как выгрузил данные. Файл читается DictReader по именам колонок, разделитель назван явно, метка Excel не ломает первую колонку, а строка с n/a уходит в пропуски, а не в среднее.
Долги. Мы читаем весь файл в цикле, но храним разобранное в памяти. Для выгрузки на сотни тысяч строк это уже много — к тому, как читать такое, вернёмся в уроке про генераторы.
Ответы
Показать ответы
На вопросы
- Потому что в поле была запятая, а
splitне знает про кавычки: он режет по каждому символу-разделителю.csv.readerзнает правило формата — запятая внутри кавычек принадлежит значению. - Он берёт первую строку как заголовок и отдаёт словарь, поэтому колонку находят по имени. Если источник вставит колонку в середину, разбор по номерам сломается молча, а по именам продолжит работать.
- Это три невидимых байта в начале файла, которые Excel пишет как метку кодировки. Прочитанные как обычный
utf-8, они приклеиваются к имени первой колонки, и та перестаёт находиться по своему настоящему имени. Читают такие файлы какutf-8-sig.
К разминке
- Пять. Разделяющих запятых две, а внутри примечания — ещё две, и
splitрежет по всем четырём: он не знает про кавычки.csv.readerна этой же строке даёт три поля.
5
"utf-8-sig". Обычныйutf-8оставит метку Excel приклеенной к имени первой колонки, иrow["year"]ответитKeyErrorпри живой колонке.
import csv
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "excel.csv"
data.write_bytes("year,value\n2021,8.0\n".encode("utf-8-sig"))
with data.open(encoding="utf-8-sig", newline="") as source:
for row in csv.DictReader(source):
print(row["year"])
data.unlink()
2021
",".join(row)записал запятые примечания как разделители, и при чтении полей стало пять вместо трёх. Кавычки ставит писатель —csv.writer:
import csv
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "vygruzka.csv"
row = ["2022", "15.0", "шок, война, логистика"]
with data.open("w", encoding="utf-8", newline="") as target:
csv.writer(target).writerow(row)
with data.open(encoding="utf-8", newline="") as source:
for back in csv.reader(source):
print("полей:", len(back))
data.unlink()
полей: 3
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.