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 читал все данные. Для крупных загрузок и отзывчивых превью выбирайте путь, который не читает лишнего.