Выгрузка в Excel из Django: что учесть на реальном проекте
Экспорт данных в Excel кажется задачей на десять минут, но на большом отчёте выедает память и вешает воркер. Разбираем стриминг, форматы xlsx против csv и фоновую генерацию.

Клиент нажимает «Выгрузить в Excel», ждёт двадцать секунд, потом видит 504 от Nginx, а в логах Gunicorn — WORKER TIMEOUT. Отчёт на 80 000 строк собрал в память список объектов, отрендерил их в openpyxl и выел 1,5 ГБ на одном запросе. На двух параллельных выгрузках воркер умер. Это типичная история любой Django-админки, которая доросла до реальных объёмов. Разберём, где выгрузка ломается и какой подход выбрать под ваш размер данных.
Статья про Django 4.2 LTS и 5.x. Смежные темы — построение самих отчётов и права доступа к выгрузке — здесь не рассматриваем, только механику отдачи файла.
Три задачи, которые называют одним словом «экспорт»
Прежде чем брать библиотеку, ответьте на один вопрос: сколько строк и как часто. От этого зависит всё остальное.
- До нескольких тысяч строк, редко — простой синхронный ответ, любой формат подойдёт.
- Десятки тысяч строк, регулярно — здесь важна память: нельзя держать весь набор в списке, нужен курсор по QuerySet и стриминг.
- Сотни тысяч и больше, тяжёлые отчёты — синхронный HTTP-ответ не годится в принципе. Генерация уходит в фоновую задачу, пользователь получает ссылку на готовый файл.
Ошибка новичка — писать код для первого случая, а запускать на третьем.
Форматы: csv, xlsx и когда что
«Excel» для пользователя — это иконка, а не формат. Excel открывает и то, и другое, но цена генерации разная.
| Критерий | CSV | XLSX (openpyxl) |
|---|---|---|
| Память на строку | Минимальная, пишется потоком | Высокая в обычном режиме, средняя в write_only |
| Форматирование, стили, формулы | Нет | Есть |
| Несколько листов | Нет | Есть |
| Стриминг из коробки | Да | Нет, файл собирается целиком |
| Проблема с кодировкой в Excel | Есть (нужен BOM) | Нет |
Если данные плоские и нужен только столбец за столбцом — берите CSV, он на порядок дешевле по ресурсам. XLSX оправдан, когда нужны стили, ширина колонок, несколько листов или пользователь прямо ждёт «настоящий» файл Excel с форматированием.
Подводный камень CSV: кодировка
Excel на Windows по умолчанию открывает CSV в системной кодировке, а не UTF-8. Кириллица превращается в кракозябры. Лечится добавлением BOM в начало файла — используйте кодировку utf-8-sig, тогда Excel распознаёт UTF-8 корректно.
Стриминг CSV: не держим данные в памяти
Django отдаёт большой CSV через StreamingHttpResponse. Ключевой приём — не собирать строки в список, а итерироваться по QuerySet с iterator(), чтобы ORM не кешировал все объекты разом.
# Django 4.2 / 5.x
import csv
from django.http import StreamingHttpResponse
class Echo:
"""Псевдо-буфер: csv.writer пишет в него, а мы отдаём результат наружу."""
def write(self, value):
return value
def export_orders_csv(request):
queryset = Order.objects.all().values_list("id", "created_at", "total")
def rows():
writer = csv.writer(Echo())
# заголовок с BOM, чтобы Excel не ломал кириллицу
yield "\ufeff" + writer.writerow(["ID", "Дата", "Сумма"])
# iterator() отключает кеширование QuerySet
for order_id, created_at, total in queryset.iterator(chunk_size=2000):
yield writer.writerow([order_id, created_at.isoformat(), total])
response = StreamingHttpResponse(rows(), content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="orders.csv"'
return responseЗдесь values_list вместо полных объектов экономит и память, и время: ORM не создаёт экземпляры моделей. chunk_size у iterator() управляет тем, сколько строк вытягивается из БД за раз. Ответ начинает уходить клиенту сразу, ещё до того как последняя строка прочитана из базы.
Правило простое: если вы где-то написали list(queryset) перед выгрузкой — вы уже проиграли по памяти на больших данных. Список материализует всё разом.XLSX без обжорства памяти: write_only
Обычный режим openpyxl держит всю книгу в памяти как дерево объектов. На десятках тысяч строк это гигабайты. У библиотеки есть режим только на запись — он сбрасывает строки на диск по мере добавления и не хранит их все.
# openpyxl 3.1.x, Django 4.2 / 5.x
from openpyxl import Workbook
from django.http import HttpResponse
def export_orders_xlsx(request):
# write_only=True — режим потоковой записи
workbook = Workbook(write_only=True)
sheet = workbook.create_sheet("Заказы")
sheet.append(["ID", "Дата", "Сумма"])
queryset = Order.objects.values_list("id", "created_at", "total")
for order_id, created_at, total in queryset.iterator(chunk_size=2000):
# даты без таймзоны — Excel не понимает tz-aware datetime
sheet.append([order_id, created_at.replace(tzinfo=None), total])
response = HttpResponse(
content_type=(
"application/vnd.openxmlformats-officedocument."
"spreadsheetml.sheet"
)
)
response["Content-Disposition"] = 'attachment; filename="orders.xlsx"'
workbook.save(response) # пишет прямо в поток ответа
return responseВажное ограничение write_only: строки пишутся строго по порядку, вернуться и поправить ячейку выше нельзя, автофильтр и часть операций со стилями ограничены. Для отчёта, который просто выгружает таблицу, это не мешает.
Дата и таймзона
openpyxl не умеет писать tz-aware datetime — на такой ячейке будет исключение. Если у вас USE_TZ = True (по умолчанию с Django 4.x), приводите даты через timezone.localtime и убирайте таймзону перед записью, либо форматируйте в строку.
Когда синхронно нельзя: фоновая генерация
HTTP-запрос не должен жить дольше таймаута прокси — обычно 30–60 секунд. Отчёт на 300 000 строк с расчётами в него не влезет, а StreamingHttpResponse здесь не спасёт, если строки требуют долгих вычислений между отдачами.
Схема для тяжёлых выгрузок:
- Пользователь запускает выгрузку — вьюха ставит задачу в очередь (Celery, RQ или что у вас есть) и сразу отвечает «файл готовится».
- Воркер генерирует файл, сохраняет его в хранилище (например, в приватный бакет через
django-storagesили вMEDIA_ROOT). - Пользователь получает ссылку на скачивание — по готовности через уведомление, письмо или опрос статуса задачи.
Отдавать сам файл лучше через выдачу временной подписанной ссылки на хранилище или через X-Accel-Redirect у Nginx, чтобы не гонять гигабайты через Python-воркер повторно.
Что ещё ломается на проде
- Ответ буферизуется целиком. Стриминг работает, только если между Django и клиентом ничего не собирает ответ в буфер. Проверьте, что у прокси не включена полная буферизация тела ответа.
- ATOMIC_REQUESTS. Если у вас включены атомарные запросы, длинная выгрузка держит транзакцию открытой всё время генерации. Выносите тяжёлый экспорт в отдельную вьюху без транзакции или в фон.
- Дубли из-за пагинации по OFFSET. Если данные пишутся во время выгрузки, срез по
LIMIT/OFFSETможет дать пропуски и повторы. Для больших наборов лучше курсорная пагинация по возрастающему ключу. - Кодировка имени файла. Кириллица в
filenameломается в части браузеров. Используйтеfilename*=UTF-8''в заголовкеContent-Disposition.
Что делать дальше
Начните с замера: сколько памяти и секунд уходит на вашу самую большую реальную выгрузку. Снимите max RSS воркера под нагрузкой в один и два параллельных запроса — это покажет, упрётесь ли вы в память при обычной работе. Если выгрузка укладывается в секунды и мегабайты — оставляйте синхронный CSV со стримингом. Если растёт — переводите в write_only, а самые тяжёлые отчёты уводите в очередь задач и отдавайте готовым файлом по ссылке.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Django: StreamingHttpResponse (документация), openpyxl: Optimised modes (write-only)
Частые вопросы
У меня старая версия Django, 3.2. Код с iterator и StreamingHttpResponse сработает?
Стриминг настроил, а браузер всё равно ждёт весь файл и только потом качает. Почему?
openpyxl падает с ошибкой на дате при записи. Что не так?
Кириллица в CSV превращается в мусор при открытии в Excel. Как исправить?
Отчёт всё равно не укладывается в таймаут, даже со стримингом. Что делать?
Использовать ли pandas.to_excel для выгрузки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.