简介:这份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. 创建 workbook,encoding 必须设,否则中文表头乱码 wb = xlwt.Workbook(encoding='utf-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, start=1): 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_type='application/vnd.ms-excel' ) response['Content-Disposition'] = ( 'attachment; filename="orders.xls"' ) wb.save(response) return response这段代码的逻辑说明:xlwt.Workbook(encoding='utf-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_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="orders.xlsx"' wb.save(response) return response逻辑说明:Workbook()创建后默认有一个活动 sheet,ws.append()接受一个列表,按行写入,比 xlwt 的write(row, col, value)更符合直觉。wb.save(response)同样支持直接写入 HttpResponse 对象,openpyxl 内部会处理流式写入。
参数说明:openpyxl 的Workbook没有 encoding 参数,因为它默认就是 UTF-8。如果数据量很大,可以用write_only=True模式创建 workbook,这种模式下不能随机访问单元格,但内存占用显著降低。用法是wb = Workbook(write_only=True),然后ws = wb.create_sheet(),之后只能用ws.append()逐行写。
提示:如果导出数据超过十万行,建议用
write_only=True配合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, name='export_orders_xls'), path('export/orders/xlsx/', views.export_orders_xlsx, name='export_orders_xlsx'), ]前端触发下载最简单的方式是用一个普通链接,而不是 Ajax:
<!-- templates/order_list.html --> <a href="{% url 'export_orders_xlsx' %}" class="btn 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_only=True) ws = wb.create_sheet('订单列表') ws.append(['订单号', '客户名称', '金额', '创建时间']) # 用 iterator() 避免一次性加载所有行 for row in Order.objects.all().values_list( 'order_no', 'customer_name', 'amount', 'created_at' ).iterator(chunk_size=2000): 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_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="orders.xlsx"' return response逻辑说明:iterator(chunk_size=2000)让 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; filename="xxx.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没有设置encoding='utf-8',或者写入的字符串本身不是 unicode。解决:创建 workbook 时显式传encoding='utf-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_size=2000)分批取数,openpyxl 用write_only=True模式。如果还是不够,考虑把导出改成异步任务,生成文件后存到对象存储,再给用户一个下载链接。
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, filename='export.xlsx', sheet_title='Sheet1'): """ headers: list[str] rows: iterable of list/tuple filename: 下载文件名 """ wb = Workbook(write_only=True) 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_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) from urllib.parse import quote response['Content-Disposition'] = ( "attachment; filename*=UTF-8''{}".format(quote(filename)) ) return response视图里只需要准备 headers 和 rows:
def export_orders(request): headers = ['订单号', '客户名称', '金额', '创建时间'] rows = Order.objects.all().values_list( 'order_no', 'customer_name', 'amount', 'created_at' ).iterator(chunk_size=2000) 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 时区偏移」的问题。导出功能不像页面渲染那样直观,数据错了用户可能过很久才发现,所以自己先验证一遍最省心。
希望帮到你。
本文还有配套的精品资源,点击获取