2025, Nov 21 06:01
Память-эффективное превью CSV и XLSX: pandas, polars и openpyxl
Как сделать превью CSV и XLSX без лишней памяти: сравниваем pandas и polars, openpyxl и fastexcel; показываем nrows/n_rows и примеры для первых N строк.
Сделать легковесный предварительный просмотр пользовательских таблиц кажется простым, пока на реальных файлах не раздувается память. Когда в CSV порядка 30 тыс. строк, а в XLSX — до миллиона, выбор средства чтения и его настроек решает, получите ли вы живое превью или процесс, упирающийся в сотни мегабайт. Ниже — практическое руководство: что происходит в pandas и polars, как это воспроизвести и какие опции стабильно удерживают потребление памяти в рамках при показе «первых N строк».
Воспроизводим проблему
Следующий тестовый файл предназначен для сравнения pandas и polars на входах CSV и XLSX под pytest-memray. В нём есть полные чтения и частичные — для наглядного сопоставления. Логика не менялась относительно базовой версии; имена изменены для ясности.
import io
import pytest
import pandas as pd
import polars as pl
@pytest.fixture
def csv_path() -> str:
return "files/rows.csv"
@pytest.fixture
def xlsx_path() -> str:
return "files/bankdataset.xlsx"
def test_pandas_csv_full(csv_path: str):
pd.read_csv(csv_path)
def test_polars_csv_full(csv_path: str):
pl.scan_csv(csv_path, low_memory=True).collect()
def test_pandas_xlsx_full(xlsx_path: str):
pd.read_excel(xlsx_path, sheet_name=None)
def test_polars_xlsx_full(xlsx_path: str):
pl.read_excel(xlsx_path, sheet_name=None)
def test_pandas_csv_head(csv_path: str):
frame = pd.read_csv(csv_path, nrows=20)
assert len(frame) == 20
def test_polars_csv_head(csv_path: str):
frame = pl.scan_csv(csv_path).head(n=20).collect()
assert len(frame) == 20
def test_pandas_xlsx_head(xlsx_path: str):
frame = pd.read_excel(xlsx_path, nrows=20)
assert len(frame) == 20
def test_polars_xlsx_head(xlsx_path: str):
frame = pl.read_excel(xlsx_path).head(n=20)
assert len(frame) == 20
def test_polars_csv_head_via_buffer(csv_path: str):
with io.BytesIO() as buf:
pl.scan_csv(csv_path).limit(20).sink_csv(buf)
frame = pl.read_csv(buf.getvalue())
assert len(frame) == 20
В зафиксированных прогонах pandas показал меньшие аллокации, чем polars, как для CSV, так и для XLSX в этих конкретных сценариях. С XLSX разница была особенно заметна: polars с движком Excel по умолчанию достигал примерно 473 MiB на пике, тогда как pandas — около 426 MiB. Результаты по CSV были ближе, но и там у pandas цифры вышли лучше в этих тестах.
Что происходит на самом деле
Если цель — превью первых N строк, ключевое — останавливается ли читатель преждевременно, как только данных достаточно. Для CSV обе экосистемы умеют эффективно стримить, но важны детали, когда и где происходит материализация. Для Excel критичны выбор движка и его способность рано выйти.
Pandas read_excel по умолчанию использует openpyxl и прекращает разбор раньше, когда задан nrows. Этот ранний выход виден в исходниках pandas и именно поэтому библиотека не вытягивает весь лист, если вам нужна только выборка. Соответствующие строки здесь: pandas/io/excel/_openpyxl.py, где есть короткое замыкание:
if file_rows_needed is not None and len(data) >= file_rows_needed:
break
Polars read_excel использует fastexcel как движок по умолчанию. Он поддерживает собственный n_rows через read_options — например, pl.read_excel(..., read_options={"n_rows": 20}) — однако в тестах это не снизило расход памяти. В polars можно переключиться на engine="openpyxl", но, согласно реализации, он читает все данные, что сводит на нет оптимизацию раннего выхода для превью. Поведение можно посмотреть здесь: polars/io/spreadsheet/functions.py.
Сама openpyxl предоставляет эффективный способ ограничить чтение на уровне листа. Использование iter_rows(max_row=...) сокращает объём проходящих данных и, по измерениям, потребляет чуть меньше памяти, чем остановка через выход из цикла после того, как читатель уже продвинулся дальше.
Практичный путь к экономным по памяти превью
Если вам нужно вывести первые N строк, два решения в тестах стабильно минимизировали аллокации. Для CSV опирайтесь на модуль стандартной библиотеки csv, чтобы прочитать ровно нужное число строк. Для Excel используйте openpyxl напрямую и ограничивайте итерацию через max_row.
Вот минимальный шаблон для превью CSV на стандартной библиотеке:
import csv
def preview_csv_head(csv_file: str, n: int = 20):
rows = []
with open(csv_file, newline="", encoding="utf-8") as fh:
reader = csv.reader(fh)
for idx, r in enumerate(reader):
rows.append(r)
if idx + 1 >= n:
break
return rows
Для Excel режим только для чтения openpyxl с iter_rows(max_row=...) держит память в узде и избегает чтения всего листа. Пример ниже повторяет подход, который показал немного меньший расход памяти, чем ранний выход:
from openpyxl import load_workbook
def preview_xlsx_head(xlsx_file: str, limit: int = 20):
opts = {"read_only": True, "data_only": True, "keep_links": False}
wb = load_workbook(xlsx_file, **opts)
previews = {}
for ws_name in wb.sheetnames:
ws = wb[ws_name]
sample = []
for row in ws.iter_rows(max_row=limit + 1):
sample.append([cell.value for cell in row])
previews[ws_name] = sample
return previews
Если удобнее оставаться в экосистеме pandas, можно задействовать ранний выход через nrows:
import pandas as pd
def preview_xlsx_with_pandas(xlsx_file: str, n: int = 20):
df = pd.read_excel(xlsx_file, nrows=n)
return df
В polars для полноты можно попробовать параметр n_rows в fastexcel. Вызов выглядит так, хотя в тестах это не улучшило память в данном сценарии:
import polars as pl
def preview_xlsx_with_polars(xlsx_file: str, n: int = 20):
frame = pl.read_excel(xlsx_file, read_options={"n_rows": n})
return frame
Почему это важно для рабочих превью
Функции превью запускаются часто и на границах размеров пользовательского ввода. Небольшие неэффективности накапливаются в более высокую нагрузку на память и увеличивают время отклика. XLSX особо чувствителен, потому что некоторые читатели могут разбирать весь лист до того, как вы урежете до первых строк, — чего превью как раз не требует. Выбор читателя, который останавливается, как только получил запрошенные строки, — самое простое и заметное улучшение для этой задачи.
Выводы и рекомендации
Когда цель — экономный по памяти показ «первых N строк», самые безопасные настройки — это стандартный читатель csv для CSV и openpyxl с iter_rows(max_row=...) для файлов Excel. Если вы уже используете pandas, read_excel(..., nrows=...) выигрывает за счёт раннего выхода openpyxl и решает ту же задачу. В polars помните, что путь по умолчанию через fastexcel и движок openpyxl отличаются по поведению раннего выхода; в тестах параметр n_rows у fastexcel не менял потребление памяти, а движок openpyxl читал все данные. Для крупных загрузок и отзывчивых превью выбирайте путь, который не читает лишнего.