ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django大数据导出Excel/CSV:从HttpResponse到StreamingHttpResponse的完整方案

Django大数据导出Excel/CSV:从HttpResponse到StreamingHttpResponse的完整方案 简介这是一份面向Django开发者的实用资料讲解在项目中导出数据到Excel并实现浏览器下载的完整方案。内容基于xlwt库从后端视图编写到前端Ajax交互均有覆盖并揭示用HttpResponse直接返回大文件可能引发的MemoryError与nginx超时问题进而介绍StreamingHttpResponse的流式传输优化思路。资源包为单个PDF文档整体大小77KB便于快速阅读与按代码实践。文档中包含后台view.py示例、Excel表头与数据写入逻辑、前端XMLHttpRequest触发下载的完整代码以及针对百万、千万级数据量下载的排错与优化方法适合需要为管理后台增加导出功能或希望提升下载性能的读者参考。目前已有1498人学习下载值得作为Django文件导出需求的速查材料。1. 给 Django 配上 Excel 导出下载一份能直接抄的落地方案只要做后台管理系统几乎逃不掉“把列表数据导成 Excel”这个需求。这篇文章要解决的是从 Django 后端生成 .xls 文件、通过浏览器触发下载到前端拿到 Blob 落盘的完整链路并且会单独讲清楚百万元素级数据导出时的内存与超时问题。适合刚接触 Django 的初学者照着一步步搭通也适合已经写好普通导出、但一跑大数据就 MemoryError 的熟手直接把方案升级过去。这里不会只贴代码还会把每个参数和每个坑位都标出来毕竟这类功能写起来不难翻车的点全藏在细节里。2. 基础版后端用 xlwt 生成工作簿并用 HttpResponse 返回2.1 先装依赖xlwt 的适用边界与安装方式Excel 导出的 Python 库有很多常见的是 xlwt、xlrd、openpyxl、pandas 这几类。这里用的 xlwt 专门负责生成 .xls 格式的旧版 Excel 文件不支持 .xlsx也不支持读取它的定位很纯粹写老格式够用、轻量、无额外依赖。如果你的表结构里有公式、图表、数据透视表这类高级对象xlwt 做不了需要换 openpyxl但普通的地名、次数、经纬度这类数据列表xlwt 完全够用。安装只有一条命令建议先在虚拟环境里执行pip install xlwt装完可以在 Django 的 views.py 里import xlwt验证一下没有报错就说明环境就绪了。这里要特别提醒一点xlwt 的单元格写入方法write()是零索引的行和列都从 0 开始数新手写表头时容易在坐标上搞混后面代码里会反复看到这个规律。2.2 视图函数从 POST 参数到 Excel 文件流的完整链路先看后端代码这是整个导出功能的地基。下面的导出函数接收前端传过来的城市名按城市筛选数据库里的 place 表然后把这个结果集写入 Excel 工作簿返回给浏览器from django.http import HttpResponse from io import BytesIO import xlwt def export_excel(request): city request.POST.get(city) list_obj place.objects.filter(citycity) # 响应类型声明为 Excel 文件浏览器收到后会按附件处理 response HttpResponse(content_typeapplication/vnd.ms-excel) response[Content-Disposition] attachment;filename city .xls if list_obj: ws xlwt.Workbook(encodingutf-8) w ws.add_sheet(sheet1) # 写入表头四个字段分别对应地名、次数、经度、纬度 w.write(0, 0, u地名) w.write(0, 1, u次数) w.write(0, 2, u经度) w.write(0, 3, u纬度) excel_row 1 for obj in list_obj: name obj.place sum_data obj.sum lng obj.lng lat obj.lat w.write(excel_row, 0, name) w.write(excel_row, 1, sum_data) w.write(excel_row, 2, lng) w.write(excel_row, 3, lat) excel_row 1 # 工作簿保存到内存缓冲区再写入响应对象 output BytesIO() ws.save(output) output.seek(0) response.write(output.getvalue()) return response这里的逻辑分四层第一层取前端参数并过滤数据第二层声明 HTTP 响应类型为老版 Excel 的 MIME 类型第三层建工作簿、加 sheet、写表头、遍历 ORM 结果集写数据行第四层把工作簿保存到 BytesIO再通过response.write()输出。注意output.seek(0)这行代码BytesIO 的游标在save()之后停在末尾不重新定位到开头getvalue()返回的可能不是完整文件流这个动作不能省。Content-Disposition的attachment;filename是最关键的两个头参数之一attachment告诉浏览器这是下载附件而不是内联展示。文件名拼的是city变量如果 city 是中文这里会踩坑后面避坑章节专门讲。2.3 小数据量场景的选型理由什么时候可以放心用 HttpResponse上面的方案在数据量不超过几千到几万行的场景下非常稳。HttpResponse的工作方式是视图函数一次性把完整的内容拼装好再返回数据小的时候响应快、代码简单完全没必要上流式方案。很多教程一上来就推StreamingHttpResponse实际是过度设计——你导出的可能只是一个月的订单列表几千行数据用流式反而让代码复杂度上升。什么时候该换方案呢我一般以 5 万行为分界线。超过这个量HttpResponse的内存峰值就不太好看了如果服务器是 1G 内存的小机器20 万行基本就是极限这在后面的百万级章节会展开。3. 前端下载链路XHR 拿 Blob 再触发浏览器保存3.1 用 XMLHttpRequest 发 POST请求头、CSRF、响应类型前端这件事听起来简单点按钮、发请求、浏览器下载但直接用window.location或a标签跳转是不行的因为后端接收的是 POST 参数。常规做法用XMLHttpRequest构造异步请求把响应体按 Blob 处理。$(#export_excel).click(function () { var csrf $(input[namecsrfmiddlewaretoken]).val(); const req new XMLHttpRequest(); req.open(POST, /export_excel/, true); req.responseType blob; req.setRequestHeader(Content-Type, application/x-www-form-urlencoded); req.send(city $(#city).val() csrfmiddlewaretoken csrf); req.onload function() { const data req.response; const blobUrl window.URL.createObjectURL(data); download(blobUrl); }; });这段代码有两个关键设置。第一是req.responseType blob没有这行响应体拿到的是一个字符串你把它塞进a标签后可能变成乱码或打不开的文件。第二是 CSRF token 的处理Django 默认开启 CSRF 校验POST 请求必须带 token。这里选择把 token 拼进请求体Content-Type是application/x-www-form-urlencoded所以格式是 key-value 拼接。注意原代码里用了两个这是笔误标准写法是单个传多个参数时别写错。3.2 触发下载临时a标签与download属性的配合拿到 Blob 后不能直接a.click()完事要先创建一个对象 URL然后用隐藏的a元素触发点击function download(blobUrl) { var city $(input[namecity]).val(); const a document.createElement(a); a.style.display none; a.download city .xls; a.href blobUrl; a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(blobUrl); }window.URL.createObjectURL会为 Blob 生成一个内存中的临时地址a.download属性必须显式指定文件名否则浏览器可能用 URL 里的字符串当名字。revokeObjectURL这行很重要临时 URL 不释放会积累内存。另外创建出来的a元素要removeChild清理掉不然页面上残留不可见节点反复点击导出后 DOM 会越来越脏。这里还有一个很容易忽略的细节a.download里的文件名后缀应该和后端返回的文件名一致后端拼的是city .xls前端也应该对应。如果前端不指定、后端 Content-Disposition 里的文件名中文乱码了用户下载下来的文件就是一串不知所云的字符。4. 升级百万级下载从 HttpResponse 切到 StreamingHttpResponse 加流式游标4.1 为什么 20 万行就崩了fetchall 的隐形成本前面的方案能跑但撑不住大数据量。问题出在两个地方一是HttpResponse本身一次性把整个文件内容放在内存里返回二是 ORM 的filter().objects查询结果也是整体加载到内存再遍历。如果你的查询结果是 15 万行、每行有几十个字段内存占用轻松上 500M。我实测过一台 2G 内存的云服务器导出 20 万行时 Django 进程直接被系统杀掉。FileResponse和StreamingHttpResponse都是 Django 内置的流式响应方案。FileResponse适合返回已经存在的静态文件它内部用迭代器分块读文件StreamingHttpResponse适合动态生成内容你给它一个生成器它边生成边返给客户端。数据库内容实时导出显然属于后者所以切入点选StreamingHttpResponse。4.2 流式游标替代普通游标SSDictCursor 与 fetchone 的配合用StreamingHttpResponse只能解决响应侧的内存问题数据侧的瓶颈还在。你如果用cursor.fetchall()一次性把所有行取到 Python 进程里生成器再有耐心也没用。这里要用 PyMySQL 的流式游标SSDictCursor它不会把结果集一次性拉到客户端而是每次fetchone()时才从 MySQL 服务器取一行import pymysql from django.http import StreamingHttpResponse def export_big_excel(request): city request.POST.get(city) conn pymysql.connect( host127.0.0.1, port3306, databasedemo, userroot, passwordroot, cursorclasspymysql.cursors.SSDictCursor ) cursor conn.cursor() sql SELECT place, sum, lng, lat FROM place WHERE city%s cursor.execute(sql, (city,)) response StreamingHttpResponse(generate_csv(cursor)) response[Content-Type] application/octet-stream response[Content-Disposition] attachment;filenameplaces_ city .csv return response def generate_csv(cursor): cols [place, sum, lng, lat] yield ,.join(cols) \n row cursor.fetchone() while row is not None: yield ,.join(str(row[col]) for col in cols) \n row cursor.fetchone() cursor.close() conn.close()这个版本的要点是while row is not None这个循环模式。fetchone()每次只从数据库服务器取一行行取完后返回None循环结束。注意游标和连接对象的关闭时机必须在生成器内部关闭因为 Django 拿到响应件后才会迭代生成器你放在视图函数里return之前关闭生成器还会继续用直接抛异常。这是个很典型的翻车点。4.3 为什么输出改成了 CSV大数据导出别执着于 .xls细心的话会发现上面的代码返回的是 CSV 而不是 .xls。这里有个非常现实的原因xlwt 是内存型库工作簿的所有数据都要在内存里组织好才能save()输出数据量到几十万行时就算不炸也会慢到让人怀疑服务器死机。而 CSV 是纯文本流格式可以一行一行的yield天生适配StreamingHttpResponse的迭代模型。如果你的业务方一定要 .xls 或 .xlsx有两条路一条是让运维装个 LibreOffice后端生成 CSV 后调用libreoffice --headless --convert-to xls做离线转换另一条是后端用openpyxl的write_only模式它能边写边刷磁盘内存占用比 xlwt 低一个数量级。但这两条路都有额外成本我通常的做法是先问业务方要的是数据还是格式——只要数据能打开CSV 的兼容性其实更好Excel 双击 CSV 也不会乱码只是中文列名要注意编码。5. 避坑指南六个真实翻车现场与排查方法5.1 CSV 中文乱码现象导出的 CSV 用 Excel 打开中文全变成乱码。原因Python 默认用 UTF-8 编码文件内容Excel 老版本默认按 GBK 解析 CSV。解决在 CSV 内容头部写入 UTF-8 BOM。在生成器里输出\ufeff前缀即可yield \ufeff ,.join(cols) \n。BOM 是字节序标记Excel 看到它就会用 UTF-8 解码。5.2 Content-Disposition 文件名中文变下划线现象浏览器下载时文件名变成_____或一串编码。原因HTTP 头默认只支持 ASCII中文文件名直接拼进去会被浏览器丢弃或替换。解决用 RFC 5987 标准格式传文件名response[Content-Disposition] attachment;filename*UTF-8 quote(city .xls)Python 的urllib.parse.quote会把中文做百分号编码浏览器能正常还原。5.3 MemoryError现象导出超过 20 万行时报 MemoryError或者 Django 进程被杀页面直接 502。原因HttpResponse整体拼装、ORM 结果集整体加载、fetchall()全部取回三个环节叠加内存峰值无法控制。解决按第 4 章方案切换StreamingHttpResponseSSDictCursorfetchone()。注意 ORM 的.iterator()也有类似效果可以在部分场景替换。5.4 nginx 504 Gateway Timeout现象大数据导出时请求处理超过 nginx 默认超时时间浏览器收到 504。原因nginx 默认proxy_read_timeout是 60 秒导出高清数据时等不到完就被掐断。解决先确认加了流式方案然后调长超时时间。在 nginx 站点配置的 location 里加proxy_read_timeout 300s;和proxy_send_timeout 300s;改完nginx -s reload。这只是缓解核心还是在代码层面减少单次请求处理耗时。5.5 Blob 下载出的文件打不开现象下载成功但文件损坏Excel 提示格式不对。原因前端responseType没设置成blob或者后端返回了错误信息比如 CSRF 失败返回 403 页面前端把 HTML 错误页当成 Excel 落盘。解决后端返回前打印response.status_code确认是 200前端在onload里加状态判断req.status 200才执行下载逻辑。另外把Content-Type设置正确错误页的 HTML 和 Excel 二进制流的 MIME 类型完全不同可以很快区分。5.6 多次导出后浏览器越来越卡现象页面连续导出几次后内存占用飙升甚至浏览器崩溃。原因createObjectURL生成的临时 URL 没有及时释放。解决在a.click()后立即调用window.URL.revokeObjectURL(blobUrl)。这是前端最容易漏的一步代码见 3.2 节。6. 从 15 万行到千万行我把这套流程压进生产环境的实测心得这套方案我最早是在一个地理信息平台的数据导出功能里落地。当时有个页面要导出一个城市两个月内的 POI 点位数据量大致在 300 万行左右表里有地名、经纬度、分类、来源、更新时间等十几个字段。一开始用的HttpResponse加xlwt测试环境导出 1 万行没问题我就高估了实际环境。上线首日运营点了一次全量导出Django 进程直接 C 掉nginx 那边报了 504。那是半夜两点我被电话叫起来查日志才认真把第 4 章的方案完整改造完。那次血的教训让我总结了一个验证习惯写完全新的导出代码第一件事不是看功能通没通而是拿生产环境的最大数据量压一轮——开终端跑time curl测总耗时时长同时用psutil或系统top盯 Django 进程的内存峰值。正常情况百万行导出时进程常驻内存的浮动应该控制在 20M 以内如果内存曲线随数据量线性上升那说明代码里还有哪里在偷偷攒全量数据。我后来每次给导出功能加字段都要检查一遍ORM 查询是否用了.values()而不是整对象加载字段列表是否有多余的关联关系被select_related拉进来。另外一个我说过无数次但每次都有人翻车的地方是游标关闭。写完while fetchone循环后别忘了生成器最后要cursor.close()和conn.close()。这个操作不会立刻归还 MySQL 连接但能释放服务器端游标持有的结果集不然数据库那边每个导出请求都会挂着一个开着的结果集连接池很快被耗尽。从那以后我每次做 Django 的数据导出需求都强制自己走一遍这三步先量数据量级5000 行以内用HttpResponse方案图省事5 万行以上直接上StreamingHttpResponse百万级再叠加 SSDictCursor。前端一律用XMLHttpRequest Blob下载完立刻revokeObjectURL。这套流程已经稳定跑过好几个项目希望帮到你。本文还有配套的精品资源点击获取
返回列表