Shanraq.org Shanraq.org
CSV: запятая внутри поля и метка, которую ставит Excel
IT

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, чтобы не добавлять метку самим.

Карта урока

Карта урока: разделитель, кавычки и заголовок

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему split(",") разобрал строку на пять кусков вместо трёх?
  2. Чем DictReader лучше reader, когда файл пришёл из чужого источника?
  3. Что такое 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 уходит в пропуски, а не в среднее.

Долги. Мы читаем весь файл в цикле, но храним разобранное в памяти. Для выгрузки на сотни тысяч строк это уже много — к тому, как читать такое, вернёмся в уроке про генераторы.

Ответы

Показать ответы

На вопросы

  1. Потому что в поле была запятая, а split не знает про кавычки: он режет по каждому символу-разделителю. csv.reader знает правило формата — запятая внутри кавычек принадлежит значению.
  2. Он берёт первую строку как заголовок и отдаёт словарь, поэтому колонку находят по имени. Если источник вставит колонку в середину, разбор по номерам сломается молча, а по именам продолжит работать.
  3. Это три невидимых байта в начале файла, которые Excel пишет как метку кодировки. Прочитанные как обычный utf-8, они приклеиваются к имени первой колонки, и та перестаёт находиться по своему настоящему имени. Читают такие файлы как utf-8-sig.

К разминке

  1. Пять. Разделяющих запятых две, а внутри примечания — ещё две, и split режет по всем четырём: он не знает про кавычки. csv.reader на этой же строке даёт три поля.
5
  1. "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
  1. ",".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

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.