
简介这份PDF资料面向Django后端开发者聚焦在项目中导出数据到Excel并实现浏览器下载这一常见需求。内容围绕xlwt库的使用展开讲解如何通过HttpResponse设置application/vnd.ms-excel响应类型与Content-Disposition头将查询结果写入工作表并返回给前端同时涉及前端XMLHttpRequest发起POST请求、Blob与createObjectURL触发下载的完整链路。资源还补充了百万、千万级数据量下载时应对MemoryError与nginx超时的思路对比FileResponse、StreamingHttpResponse与HttpResponse的适用场景并给出StreamingHttpResponse配合PyMySQL分块返回数据的示例。压缩包共1个PDF文件约77KB篇幅精炼适合需要快速掌握导出下载实现细节与大数据优化策略的开发者查阅。目前已有1498人学习可作为项目实战中的参考手册。1. 导出 Excel 这件事坑不在写文件而在“下载”那一步后台列表页跑得好好的运营突然提需求把筛选出来的订单、用户、日志导成 Excel点一下就能下载。很多 Django 新手第一反应是HttpResponse塞个文件路径结果浏览器要么把二进制流当 HTML 渲染成乱码要么下载下来的文件名是一串 URL 编码要么数据量一大内存直接飙红。这个标题真正要解决的不是“怎么用 Python 写 Excel”而是在 Django 的请求-响应模型里把内存里生成的文件流安全地推给浏览器并触发下载。它适合正在做 django 项目实战新手阶段、需要交付导出功能的开发者也适合已经会用 xlwt 或 openpyxl 但被中文文件名、大文件内存、并发下载搞过的熟手。下面按“选型 → 生成 → 响应 → 避坑 → 进阶”的顺序把这条链路拆开讲透。2. 选 xlwt 还是 openpyxl先看你的 Django 版本和 Excel 格式2.1 三个库的边界xlwt、openpyxl、xlsxwriter热搜词里 xlwt 出现频率很高但它是上一个时代的产物。选型之前先明确一个硬约束xlwt 只能写.xlsBIFF8 格式单表上限 65536 行、256 列且早已停止维护。如果你的 Django 项目还在用 Python 2 或者历史包袱重xlwt 能跑但新项目用 Python 3 Django 3/4/5直接上 openpyxl 或 xlsxwriter。库支持格式写入方式内存表现适用场景xlwt.xls全量内存差大表易 OOM老项目、小数据量openpyxl.xlsx常规 / write_only中等write_only 可优化需要读写、需要样式xlsxwriter.xlsx流式常量内存好纯导出、大数据量、图表我一般会这样判断只导出、数据可能上万行、不需要回头读这个文件选 xlsxwriter需要保留模板、要读回校验、要复杂样式选 openpyxl维护十年前的老系统才碰 xlwt。热搜里“python写入excel”这个需求在 Django 场景下 90% 是纯导出所以本文主线用 openpyxl 演示生态最稳、文档最全并在进阶章给出 xlsxwriter 的常量内存写法。2.2 用 openpyxl 生成工作簿的最小代码先不碰 Django单独把“数据 → Excel 二进制流”这一步跑通。核心是Workbooksave到一个BytesIO而不是存磁盘。# excel_utils.py from io import BytesIO from openpyxl import Workbook from openpyxl.styles import Font, Alignment def build_workbook(headers, rows, sheet_nameSheet1): headers: list[str] 表头 rows: iterable[list] 每行数据 返回: BytesIO指针已回到开头 wb Workbook() ws wb.active ws.title sheet_name # 写表头并加粗 ws.append(headers) for cell in ws[1]: cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) # 写数据行 for row in rows: ws.append(row) # 关键写入内存缓冲区不落磁盘 buffer BytesIO() wb.save(buffer) buffer.seek(0) # 必须回绕否则读出来是空 return buffer逻辑说明Workbook()在内存里建工作簿ws.append逐行追加wb.save(buffer)把整个 xlsx 序列化进BytesIO。参数上唯一容易翻车的是buffer.seek(0)——save之后指针停在末尾直接交给HttpResponse会得到一个 0 字节文件浏览器下载下来打不开这是血泪经验里最高频的一条。sheet_name不要超过 31 个字符且不能含[]:*?/\否则 openpyxl 直接抛异常。2.3 把 queryset 喂给生成函数Django 的QuerySet是惰性的别一次性list(qs)再遍历数据量大时内存翻倍。用.values_list()配合iterator()# views.py 片段 from .models import Order from .excel_utils import build_workbook def export_orders_qs(statusNone): qs Order.objects.all() if status: qs qs.filter(statusstatus) # values_list 只取需要的列iterator 分批取 qs qs.values_list(order_no, customer, amount, created_at).iterator(chunk_size2000) headers [订单号, 客户, 金额, 创建时间] return build_workbook(headers, qs, sheet_name订单)chunk_size2000是经验值太小数据库往返多太大内存收益下降。values_list返回元组ws.append能直接吃省掉构造字典的开销。注意created_at是datetime对象openpyxl 会自动识别成日期格式不需要手动strftime手动转字符串反而会丢失 Excel 的日期排序能力。3. 让浏览器弹出下载框Content-Disposition 与中文文件名3.1 HttpResponse 的正确拼装方式生成好BytesIO之后用HttpResponse指定 content_type 和 Content-Disposition。xlsx 的 MIME 类型是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet写错浏览器可能不认。# views.py from urllib.parse import quote from django.http import HttpResponse def export_orders_view(request): status request.GET.get(status) buffer export_orders_qs(status) filename 订单导出.xlsx # 中文文件名必须 URL 编码否则部分浏览器乱码 encoded quote(filename) response HttpResponse( buffer.getvalue(), content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet, ) response[Content-Disposition] fattachment; filename*UTF-8{encoded} return response逻辑说明attachment告诉浏览器“这是下载不是预览”filename*UTF-8是 RFC 5987 规定的编码文件名写法Chrome、Edge、Firefox 都认。只写filename不带*时中文会变成乱码或被截断这是“excel加载项被禁用”之外另一个高频投诉点。buffer.getvalue()返回完整字节小文件没问题大文件见第 5 章的流式方案。3.2 用 StreamingHttpResponse 处理大文件当导出几万行时buffer.getvalue()会把整个文件复制一份到内存峰值翻倍。这时改用StreamingHttpResponse配合生成器边生成边发。from django.http import StreamingHttpResponse def export_large_view(request): def row_gen(): yield b # 占位实际由 openpyxl write_only 模式产出 # 真实场景见 5.2 的 xlsxwriter 流式写法 response StreamingHttpResponse( row_gen(), content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet, ) response[Content-Disposition] attachment; filenamelarge.xlsx return response注意StreamingHttpResponse一旦开始发送就无法再改状态码所以权限校验、参数校验必须在返回它之前做完。另外它默认不设Content-Length浏览器下载进度条可能不显示这是正常现象不是 bug。3.3 前端触发下载的两种方式最简单的是a href/export/orders/?statuspaid导出/a浏览器直接处理。如果导出前要带 POST 参数或 CSRF用 fetch 拿 blobasync function downloadExcel() { const resp await fetch(/export/orders/?statuspaid, { method: GET, headers: { X-Requested-With: XMLHttpRequest }, }); if (!resp.ok) { alert(导出失败); return; } const blob await resp.blob(); const url window.URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 订单导出.xlsx; a.click(); window.URL.revokeObjectURL(url); // 释放内存 }revokeObjectURL不调用会一直占着内存批量导出时容易积累。a.download的值会被浏览器优先使用但服务端的Content-Disposition仍是兜底。4. 导出功能避坑五个真实翻车现场4.1 下载下来是 0 字节或打不开现象点击导出文件下载成功但大小 0KBExcel 提示“文件格式或扩展名无效”。 原因BytesIO在save后指针停在末尾getvalue()之前没seek(0)或者直接传了buffer对象而非buffer.getvalue()。 解决wb.save(buffer)后立刻buffer.seek(0)传给HttpResponse时用buffer.getvalue()或buffer.read()。4.2 中文文件名变成%E8%AE%A2%E5%8D%95.xlsx现象下载文件名是一串百分号编码。 原因只用了filename而没有filename*UTF-8或者编码时用了quote但没指定safe。 解决统一用filename*UTF-8{quote(filename)}并确保quote的默认safe/不会把斜杠留下文件名里本来也不该有斜杠。4.3 数据量一大就 502 或内存爆掉现象导出 5 万行时 Nginx 返回 502或服务器内存飙升。 原因list(qs)全量加载 getvalue()全量复制 HttpResponse再缓冲三份数据同时在内存。 解决iterator(chunk_size2000)分批取改用StreamingHttpResponse或换 xlsxwriter 的constant_memory模式。4.4 并发导出时文件串了现象A 用户下载到 B 用户的数据。 原因把文件写到了固定的临时路径如/tmp/export.xlsx两个请求互相覆盖。 解决永远不要用固定磁盘路径做导出中转直接用BytesIO或StreamingHttpResponse让每个请求持有独立缓冲区。4.5 时间字段变成一串数字现象Excel 里创建时间显示45123.456。 原因datetime被 openpyxl 识别为日期序列号但单元格没设数字格式。 解决写入后设置cell.number_format YYYY-MM-DD HH:MM:SS或者干脆在values_list里用strftime转成字符串牺牲排序换可读性。5. 进阶常量内存导出与导出任务化5.1 用 xlsxwriter 的 constant_memory 模式当行数到十万级openpyxl 常规模式仍会吃几百 MB。xlsxwriter 提供{constant_memory: True}它按行刷写临时文件内存占用基本恒定。import xlsxwriter from io import BytesIO def build_large_workbook(headers, rows): buffer BytesIO() wb xlsxwriter.Workbook(buffer, {constant_memory: True, in_memory: True}) ws wb.add_worksheet(数据) bold wb.add_format({bold: True}) for col, h in enumerate(headers): ws.write(0, col, h, bold) for r, row in enumerate(rows, start1): for c, val in enumerate(row): ws.write(r, c, val) wb.close() # 必须 close否则文件不完整 buffer.seek(0) return bufferconstant_memory模式下只能按行顺序写不能回头改前面的单元格所以样式要在写之前定义好。in_memory: True让它写进BytesIO而不是磁盘临时文件配合StreamingHttpResponse就能做到低内存 不落盘。wb.close()是必须的不调用文件尾部元数据不写入Excel 会报损坏。5.2 导出任务化超过 30 秒就别同步等同步导出有个硬上限Nginx 默认proxy_read_timeout60 秒超过就 504。十万行以上建议改成异步任务请求进来先落一条导出记录Celery 后台生成文件存对象存储前端轮询状态完成后给下载链接。# tasks.py from celery import shared_task from django.core.files.storage import default_storage shared_task def export_task(export_id): record ExportRecord.objects.get(idexport_id) buffer build_large_workbook(record.headers, record.iter_rows()) path fexports/{export_id}.xlsx default_storage.save(path, buffer) record.file_path path record.status done record.save()这样导出接口本身只做“创建任务”这一件事响应时间稳定在毫秒级。代价是用户不能立刻拿到文件需要前端配合轮询或 WebSocket 通知。判断标准很简单预估行数 × 单行耗时 10 秒就上异步。5.3 验证导出结果是否正确的三个检查点写完别急着交付我一般会做三步验证。第一用 pandas 读回文件比对行数和列名pd.read_excel(buffer)看shape是否和 queryset 的count()一致。第二抽查首行、末行和一条中间记录确认没有错位或漏列。第三用 Excel 打开确认中文不乱码、日期格式正常、表头加粗生效。这三步能拦住 90% 的低级错误。我自己的习惯是任何导出功能上线前先用 1 行、1000 行、10 万行三档数据各跑一遍观察内存和响应时间曲线。1 行验证逻辑1000 行验证常规路径10 万行验证流式和超时。这个习惯帮我省过好几次半夜被叫起来处理 502 的后悔药。希望帮到你。本文还有配套的精品资源点击获取