简介这份PDF资料面向Django后端开发者与Web全栈学习者聚焦在Django项目中导出数据到Excel并实现浏览器下载这一常见需求。内容围绕xlwt库的使用展开涵盖视图层HttpResponse响应类型与Content-Disposition头的设置、工作簿与工作表创建、表头及数据逐行写入、BytesIO内存输出等关键环节并延伸讲解前端通过XMLHttpRequest发起POST请求、利用Blob与createObjectURL触发下载的完整链路。资源包共1个PDF文件大小约77KB篇幅紧凑但知识点集中适合作为快速查阅的技术参考。针对百万、千万级数据量场景资料还补充了FileResponse与StreamingHttpResponse的对比指出HttpResponse易引发MemoryError与nginx超时并给出流式传输的优化思路与PyMySQL取数示例。目前已有1498人学习适合需要落地导出下载功能或排查大数据下载性能问题的开发者参考。1. 从一次运营催报表说起Django 导出 Excel 并下载到底难在哪后台管理页面上线三个月运营终于忍不住了订单列表能不能一键导出成 Excel他们不想登录数据库也不想让我每次跑一段脚本再发文件。这个需求听起来简单——Django 里查数据、写 Excel、返回下载三步而已。但真动手就会发现坑集中在三个地方数据量一大内存直接爆、中文文件名在浏览器里变成乱码、前端用 Ajax 请求根本触发不了下载。这几个问题不解决功能上线当天就会被投诉。这篇笔记讲的就是在 Django 项目里把数据库查询结果导出成 Excel 文件并让用户点击下载的完整落地路径。适合正在做 Django 项目实战的新手也适合已经写过导出但被内存和编码问题折腾过的熟手。我会从选型开始把 xlwt、openpyxl 两条路都走一遍给出可直接抄的最小实现再把参数、边界和排查方法讲透。核心词 django、excel、xlwt、下载会贯穿始终但不会为了堆词牺牲可读性。2. 选 xlwt 还是 openpyxl先看你的 Excel 要写到什么程度2.1 两个库的能力边界与选型判断在 Django 里生成 Excel绕不开两个库xlwt 和 openpyxl。热搜里 xlwt 出现频率很高很多老教程也用它但它有个硬限制——只能写 .xls 格式单表最多 65536 行、256 列。openpyxl 写的是 .xlsx行数上限约 104 万行支持更多样式和公式。如果你的导出场景是后台报表、订单明细这类可能超过六万行的数据xlwt 会在写入时报错这不是配置能绕过去的。选型判断可以按这个顺序走数据量稳定在五万行以内、只需要简单表头和单元格、项目里已经装了 xlwt那就继续用 xlwt代码最短数据量可能突破六万行、需要合并单元格或设置列宽、未来要支持 .xlsx直接上 openpyxl。我一般会默认选 openpyxl因为它的 API 更现代和 Python 3 配合更顺而且不用在数据量增长后再迁移一次。还有一个容易被忽略的点xlwt 对中文的支持需要显式设置编码openpyxl 默认就是 UTF-8省掉一层心智负担。热搜里有人搜「python写入excel」如果只搜到 xlwt 的老文章很容易在中文表头上翻车。2.2 用 xlwt 写一个最小可用的导出视图先看 xlwt 的写法因为很多存量项目还在用它。下面是一个 Django 视图查询订单数据并返回 .xls 文件下载# views.py import xlwt from django.http import HttpResponse from .models import Order def export_orders_xls(request): # 1. 创建 workbookencoding 必须设否则中文表头乱码 wb xlwt.Workbook(encodingutf-8) ws wb.add_sheet(订单列表) # 2. 写表头style 可选这里用默认 headers [订单号, 客户名称, 金额, 创建时间] for col, title in enumerate(headers): ws.write(0, col, title) # 3. 查询数据用 values_list 减少 ORM 开销 rows Order.objects.all().values_list( order_no, customer_name, amount, created_at ) for row_idx, row in enumerate(rows, start1): for col_idx, value in enumerate(row): # datetime 需要转成字符串xlwt 不认 datetime 对象 if hasattr(value, strftime): value value.strftime(%Y-%m-%d %H:%M:%S) ws.write(row_idx, col_idx, value) # 4. 写入 HttpResponse设置 content-type 和 disposition response HttpResponse( content_typeapplication/vnd.ms-excel ) response[Content-Disposition] ( attachment; filenameorders.xls ) wb.save(response) return response这段代码的逻辑说明xlwt.Workbook(encodingutf-8)里的 encoding 参数决定了字符串写入时的编码不设的话中文会变成乱码。values_list比all()再取属性更省内存因为它返回的是元组而不是模型实例。ws.write(row, col, value)的行列都是从 0 开始表头占第 0 行数据从第 1 行开始。wb.save(response)直接把 workbook 写进 HttpResponse不需要先在磁盘上生成临时文件。参数说明content_type用application/vnd.ms-excel对应 .xls如果用 openpyxl 写 .xlsx 则用application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。Content-Disposition的 filename 如果包含中文需要额外处理后面避坑章节会讲。2.3 用 openpyxl 写 .xlsx 并支持大数据量openpyxl 的写法和 xlwt 类似但有几个关键差异。下面是对应的视图# views.py from openpyxl import Workbook from django.http import HttpResponse from .models import Order def export_orders_xlsx(request): wb Workbook() ws wb.active ws.title 订单列表 headers [订单号, 客户名称, 金额, 创建时间] ws.append(headers) # append 按行追加比逐格 write 简洁 rows Order.objects.all().values_list( order_no, customer_name, amount, created_at ) for row in rows: # openpyxl 对 datetime 支持较好但统一转字符串更稳 cleaned [ v.strftime(%Y-%m-%d %H:%M:%S) if hasattr(v, strftime) else v for v in row ] ws.append(cleaned) response HttpResponse( content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) response[Content-Disposition] attachment; filenameorders.xlsx wb.save(response) return response逻辑说明Workbook()创建后默认有一个活动 sheetws.append()接受一个列表按行写入比 xlwt 的write(row, col, value)更符合直觉。wb.save(response)同样支持直接写入 HttpResponse 对象openpyxl 内部会处理流式写入。参数说明openpyxl 的Workbook没有 encoding 参数因为它默认就是 UTF-8。如果数据量很大可以用write_onlyTrue模式创建 workbook这种模式下不能随机访问单元格但内存占用显著降低。用法是wb Workbook(write_onlyTrue)然后ws wb.create_sheet()之后只能用ws.append()逐行写。提示如果导出数据超过十万行建议用write_onlyTrue配合values_list的迭代器避免一次性把所有行加载到内存。3. 把下载链路跑通URL、前端触发与文件名处理3.1 配置 URL 和前端下载入口视图写好后需要在 urls.py 里挂上路由# urls.py from django.urls import path from . import views urlpatterns [ path(export/orders/xls/, views.export_orders_xls, nameexport_orders_xls), path(export/orders/xlsx/, views.export_orders_xlsx, nameexport_orders_xlsx), ]前端触发下载最简单的方式是用一个普通链接而不是 Ajax!-- templates/order_list.html -- a href{% url export_orders_xlsx %} classbtn btn-primary 导出 Excel /a这里的关键点是浏览器遇到Content-Disposition: attachment的响应会自动触发下载不需要前端做任何处理。如果用 Ajax 请求这个 URL响应会被 JavaScript 接收浏览器不会弹出下载框这是很多人踩过的坑。如果确实需要 Ajax 触发下载得用 Blob 和URL.createObjectURL手动构造下载代码量翻倍且容易在移动端出问题。我一般直接用a标签或window.location.href。3.2 中文文件名不乱码的两种处理方式如果文件名是中文比如「订单导出.xlsx」直接写进Content-Disposition会在部分浏览器里变成乱码或截断。标准做法是用filename*UTF-8语法from urllib.parse import quote filename 订单导出.xlsx response[Content-Disposition] ( attachment; filename*UTF-8{}.format(quote(filename)) )quote会把中文转成百分号编码filename*UTF-8告诉浏览器用 UTF-8 解码。这种方式在 Chrome、Firefox、Edge 上都正常。如果还要兼容老版本 IE可以同时写filename和filename*浏览器会优先用带星号的那个。另一种做法是文件名用英文加时间戳比如orders_20250101.xlsx避开编码问题。这不是偷懒很多后台系统就是这么做的因为运营拿到文件后通常会重命名。如果产品经理坚持要中文名就用上面的filename*方案。3.3 用 StreamingHttpResponse 降低内存占用当数据量到几十万行时即使 openpyxl 的write_only模式也会在内存里保留一部分结构。更彻底的做法是用StreamingHttpResponse配合生成器边查边写边发from django.http import StreamingHttpResponse from openpyxl import Workbook from .models import Order def export_orders_stream(request): def generate(): wb Workbook(write_onlyTrue) ws wb.create_sheet(订单列表) ws.append([订单号, 客户名称, 金额, 创建时间]) # 用 iterator() 避免一次性加载所有行 for row in Order.objects.all().values_list( order_no, customer_name, amount, created_at ).iterator(chunk_size2000): cleaned [ v.strftime(%Y-%m-%d %H:%M:%S) if hasattr(v, strftime) else v for v in row ] ws.append(cleaned) # 把 workbook 写入一个临时缓冲区再 yield import io buf io.BytesIO() wb.save(buf) yield buf.getvalue() response StreamingHttpResponse( generate(), content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) response[Content-Disposition] attachment; filenameorders.xlsx return response逻辑说明iterator(chunk_size2000)让 Django 每次只从数据库取 2000 行而不是一次性把整个查询集加载到内存。生成器函数generate()在StreamingHttpResponse消费时才会执行wb.save(buf)把 Excel 写入内存缓冲区后一次性 yield。严格来说这个方案并没有完全流式输出 Excel因为 openpyxl 的 write_only 模式仍然需要在最后 save 时组装文件结构但它避免了把全部数据行同时放在内存里。参数说明chunk_size根据数据库和内存情况调整2000 到 5000 之间比较常见。如果数据库连接池有限不要设太小否则查询次数过多反而慢。注意StreamingHttpResponse不适合需要设置Content-Length的场景因为总长度在生成器执行完之前是未知的。如果前端需要进度条得换用分片导出或异步任务方案。4. 导出功能上线前后最容易翻车的五个地方4.1 现象浏览器直接显示乱码而不是下载文件原因content_type设成了text/html或没设Content-Disposition浏览器把响应当成页面内容渲染了。解决确保content_type是 Excel 对应的 MIME 类型并且Content-Disposition以attachment开头。用attachment; filenamexxx.xlsx而不是inline。4.2 现象xlwt 写入时报ValueError: row index was 65536, not allowed by .xls format原因数据行数超过了 xlwt 的 65536 行上限。解决换 openpyxl 写 .xlsx或者对数据做分页导出每页一个文件。如果业务上确实需要单文件超过六万行xlwt 没有绕过去的办法这是 .xls 格式本身的限制。4.3 现象中文表头在 Excel 里显示为乱码原因xlwt 的Workbook没有设置encodingutf-8或者写入的字符串本身不是 unicode。解决创建 workbook 时显式传encodingutf-8并确保从数据库取出的字符串在 Python 3 下已经是 str 类型。如果是从 Python 2 迁移过来的老代码需要检查是否有str()强制转换导致编码丢失。4.4 现象Ajax 请求导出接口后端返回 200 但浏览器没下载原因Ajax 的响应由 JavaScript 处理浏览器不会自动触发下载。解决改用a标签、window.location.href或window.open直接请求导出 URL。如果必须用 Ajax需要在前端把响应转成 Blob 再手动创建下载链接但这种方式在文件较大时容易失败。4.5 现象导出几十万行时服务器内存飙升甚至 OOM原因Order.objects.all()会一次性把所有模型实例加载到内存加上 Excel 写入过程中的数据结构内存占用是数据量的数倍。解决用values_list减少字段开销用.iterator(chunk_size2000)分批取数openpyxl 用write_onlyTrue模式。如果还是不够考虑把导出改成异步任务生成文件后存到对象存储再给用户一个下载链接。5. 把导出做成可复用的工具函数与异步方案5.1 抽一个通用的 Excel 导出工具每次写视图都重复一遍 workbook 创建和 response 构造很烦我一般会抽一个工具函数# utils/excel.py import io from openpyxl import Workbook from django.http import HttpResponse def export_to_excel(headers, rows, filenameexport.xlsx, sheet_titleSheet1): headers: list[str] rows: iterable of list/tuple filename: 下载文件名 wb Workbook(write_onlyTrue) ws wb.create_sheet(sheet_title) ws.append(headers) for row in rows: cleaned [ v.strftime(%Y-%m-%d %H:%M:%S) if hasattr(v, strftime) else v for v in row ] ws.append(cleaned) buf io.BytesIO() wb.save(buf) buf.seek(0) response HttpResponse( buf.getvalue(), content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) from urllib.parse import quote response[Content-Disposition] ( attachment; filename*UTF-8{}.format(quote(filename)) ) return response视图里只需要准备 headers 和 rowsdef export_orders(request): headers [订单号, 客户名称, 金额, 创建时间] rows Order.objects.all().values_list( order_no, customer_name, amount, created_at ).iterator(chunk_size2000) return export_to_excel(headers, rows, filename订单导出.xlsx)这个工具函数把文件名编码、content_type、write_only 模式都封装好了新增加一个导出接口只需要改 headers 和查询集。参数上filename默认是英文传中文时会自动走filename*编码。sheet_title对应 Excel 底部的工作表名称如果包含特殊字符如[]:*?/\需要提前替换否则 openpyxl 会报错。5.2 数据量再大就上异步任务如果导出数据到百万行级别同步请求会超时用户等几十秒也容易重复点击。这时候把导出改成异步任务更合适用 Celery 或 Django 的 management command 在后台生成文件存到文件系统或对象存储数据库里记录任务状态前端轮询或跳转到下载页。核心改动是把export_to_excel里的HttpResponse换成FileResponse或直接写文件视图只负责创建任务和查询状态。验证异步方案是否生效可以看三个指标请求响应时间是否降到毫秒级、后台任务是否在独立进程中执行、生成的文件是否能被正确下载且内容完整。我一般会在本地用time命令跑一次同步导出记录行数和耗时再对比异步方案的响应时间。如果同步导出 10 万行需要 8 秒异步方案应该让用户几乎无感。5.3 一个我反复用的验证习惯每次写完导出功能我不会只看代码有没有报错而是会做三件事用 100 行、1 万行、10 万行三档数据各跑一次看内存和耗时曲线用 Chrome 和 Firefox 各下载一次确认文件名和内容正常把导出的 Excel 用 pandas 读回来检查行数和列数是否和数据库一致。这个习惯帮我提前发现过好几次「表头写错列」和「datetime 时区偏移」的问题。导出功能不像页面渲染那样直观数据错了用户可能过很久才发现所以自己先验证一遍最省心。希望帮到你。本文还有配套的精品资源点击获取