Конвертация форматов файлов в Python: разбор на живом проекте
Скрипт конвертировал 40 тысяч CSV в Parquet и упал на файле, где в числовом столбце попалась строка «н/д». Разбираем, где ломается наивная конвертация и как её чинить.

Задача звучит просто: взять папку с CSV-выгрузками, привести их к Parquet, чтобы аналитики читали данные вдесятеро быстрее. Первый прогон на тестовой выборке отработал за минуту. На боевых 40 тысячах файлов процесс упал на 12-й тысяче с ArrowInvalid: Could not convert 'н/д' to int64. Один продавец в одном столбце вместо цены поставил текст — и весь батч встал.
Это типичная история любой конвертации форматов: проблема не в том, как вызвать библиотеку, а в том, что делать с данными, которые не помещаются в целевую схему. Разберём на связке CSV → Parquet и CSV → Excel, где эти грабли самые частые. Версии: Python 3.12, pandas 2.2, pyarrow 16.x, openpyxl 3.1.
Почему очевидное решение подводит
Очевидное решение выглядит так:
import pandas as pd
df = pd.read_csv("prices.csv")
df.to_parquet("prices.parquet")Три проблемы, которые всплывают не на тесте, а в проде:
- Типы. pandas угадывает тип столбца по содержимому. Если в числовой колонке хоть одна ячейка со строкой — весь столбец станет
object, и Parquet либо сохранит его как строку, либо упадёт при явной схеме. - Кодировка. CSV из 1С или Excel часто в
cp1251, аread_csvпо умолчанию ждёт UTF-8. ПолучитеUnicodeDecodeErrorили кракозябры. - Память.
read_csvтянет весь файл в оперативку. Файл на 3 ГБ на машине с 4 ГБ RAM убьёт процесс без внятной ошибки — простоKilledот OOM-киллера.
Конвертация — это не одна строка, а три решения: как читать, во что приводить типы, как писать не взрывая память.
Подход 1: pandas с явной схемой
Годится, когда файлы влезают в память и структура известна заранее. Ключевое — не отдавать угадывание типов на откуп pandas, а задать их руками через dtype и заранее разобраться с «мусорными» значениями.
import pandas as pd
# Читаем всё как строки, чтобы pandas ничего не угадывал
df = pd.read_csv(
"prices.csv",
encoding="cp1251", # выгрузка из 1С
dtype=str,
keep_default_na=False, # чтобы 'н/д' не превратилось в NaN раньше времени
)
# Числовой столбец приводим сами, мусор -> NaN
df["price"] = pd.to_numeric(df["price"], errors="coerce")
# Строки, которые не сконвертировались, теперь видны
bad = df[df["price"].isna()]
if not bad.empty:
print(f"Не сконвертировано строк: {len(bad)}")
df.to_parquet("prices.parquet", engine="pyarrow", index=False)Параметр errors="coerce" — центр всей конструкции. Он не роняет процесс на первой плохой ячейке, а превращает её в NaN, оставляя вам возможность посчитать и залогировать брак. Цена: вы сами отвечаете за типы, автоматика больше не помогает.
Подход 2: pyarrow напрямую для больших файлов
Когда файл не влезает в память, pandas не вариант. pyarrow умеет читать CSV блоками и писать Parquet потоково, не собирая всё целиком.
import pyarrow as pa
import pyarrow.csv as pv
import pyarrow.parquet as pq
# Явная схема на входе: pyarrow не будет угадывать
convert_options = pv.ConvertOptions(
column_types={"price": pa.float64(), "sku": pa.string()},
null_values=["н/д", "", "NULL"], # что считать пропуском
)
read_options = pv.ReadOptions(block_size=64 * 1024 * 1024) # блок 64 МБ
# open_csv возвращает потоковый ридер
with pv.open_csv(
"big_prices.csv",
read_options=read_options,
convert_options=convert_options,
) as reader:
writer = None
for batch in reader:
table = pa.Table.from_batches([batch])
if writer is None:
writer = pq.ParquetWriter("big_prices.parquet", table.schema)
writer.write_table(table)
if writer:
writer.close()Здесь null_values решает ту самую проблему с «н/д»: значения из списка становятся null ещё на чтении, до попытки привести к float64. Память под контролем — в оперативке одновременно живёт один блок, а не весь файл.
Подход 3: CSV → Excel через openpyxl
Excel — отдельная боль. У формата жёсткий лимит: 1 048 576 строк на лист. CSV на два миллиона строк в один лист не влезет физически, и никакая библиотека это не обойдёт — придётся резать на листы или файлы.
import pandas as pd
df = pd.read_csv("report.csv", encoding="utf-8")
max_rows = 1_000_000 # запас до лимита Excel
chunks = [df[i:i + max_rows] for i in range(0, len(df), max_rows)]
with pd.ExcelWriter("report.xlsx", engine="openpyxl") as writer:
for idx, chunk in enumerate(chunks, start=1):
chunk.to_excel(writer, sheet_name=f"part_{idx}", index=False)Второй подводный камень Excel — автоприведение. Артикул 00123 Excel покажет как 123, а длинный числовой код превратит в экспоненту 1,23E+11. Если такие столбцы должны остаться текстом, держите их строками в DataFrame (dtype=str при чтении), тогда openpyxl запишет их как текст.
Конвертация форматов на 80% состоит из работы с типами и мусором в данных. Сам вызовto_parquetилиto_excel— последняя и самая простая строка.
Что выбрать под задачу
| Ситуация | Подход | Чем платите |
|---|---|---|
| Файл влезает в RAM, структура известна | pandas + явный dtype | Ручное управление типами |
| Файл больше памяти | pyarrow потоково | Больше кода, меньше удобств pandas |
| Нужен .xlsx для людей | pandas + openpyxl | Лимит строк, автоприведение типов |
| Много мелких файлов пачкой | pandas в цикле с try/except на файл | Скорость, но проще изолировать сбой |
Что ломается на практике
- Смешанные типы в столбце. Самая частая причина падения при записи Parquet с явной схемой. Лечится
to_numeric(errors="coerce")илиnull_valuesв pyarrow. - Кодировка cp1251. Если не знаете кодировку заранее, определите её через
charset-normalizer(ставится вместе с requests) вместо того, чтобы гадать. - Разделитель. В русских выгрузках он часто
;, а не,. Задайтеsep=";"явно, иначе весь CSV прочитается как один столбец. - Даты. pandas по умолчанию НЕ парсит даты — они остаются строками. Указывайте
parse_dates=["order_date"], если нужен тип datetime в Parquet. - Тихий OOM. Процесс с сообщением
Killedи без трейсбека — почти всегда нехватка памяти. Переходите на потоковое чтение.
Что делать дальше
Оберните конвертацию каждого файла в отдельный try/except и пишите битые файлы в лог, а не роняйте весь батч — один плохой CSV не должен стоить вам 12 тысяч успешно обработанных. После первого прогона проверьте схему результата: pyarrow.parquet.read_schema("out.parquet") покажет, какие типы реально записались — часто там сюрпризы вроде string вместо ожидаемого double. Если конвертация регулярная, замерьте, сколько строк отсеивается в брак от прогона к прогону: рост этой доли — сигнал, что в источнике поменялся формат.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: pyarrow — Reading and writing the Apache Parquet format, pandas — IO tools (read_csv, to_parquet, to_excel)
Частые вопросы
У меня pandas 1.x, а не 2.2 — код сработает?
Скрипт падает с UnicodeDecodeError — что делать?
Почему после конвертации в Parquet числовой столбец стал строкой?
Файл больше оперативной памяти, процесс просто убивается — как быть?
Как конвертировать миллион с лишним строк в один Excel-файл?
Как не потерять ведущие нули в артикулах при выгрузке в Excel?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.