Django 数据导出 Excel 实战:从 HttpResponse 到 StreamingHttpResponse 流式改造

发布时间:2026/10/6 18:02:54
Django 数据导出 Excel 实战:从 HttpResponse 到 StreamingHttpResponse 流式改造 简介这份PDF资料面向Django后端开发者聚焦在Web项目中导出数据库数据到Excel并触发浏览器下载这一常见需求。内容以xlwt库为核心讲解如何安装依赖、在view.py中编写export_excel视图通过HttpResponse设置application/vnd.ms-excel类型与Content-Disposition附件头将查询结果写入工作表并保存到BytesIO返回同时给出前端XMLHttpRequest发送POST请求、用Blob与createObjectURL模拟点击下载的完整交互思路。资料还延伸讨论了百万、千万级数据量下HttpResponse易触发MemoryError与nginx超时的问题对比FileResponse、StreamingHttpResponse的流式传输特性与适用场景并附PyMysql取数导出CSV的示例。资源包为1个PDF文件约77KB篇幅精炼、代码密度高适合需要快速落地导出下载功能或优化大数据下载方案的中级开发者参考已有1498人学习。1. 从一次导出翻车说起Django 数据导出 Excel 到底该怎么落地去年帮一个做本地生活数据的团队排查问题他们的运营后台有个「按城市导出地点数据」的功能上线三个月一直没人管直到某天运营点了一次导出页面转了四十秒最后甩回来一个 502。我上去一看place.objects.filter(citycity)把整城十几万条记录一次性拉进内存再用xlwt逐行写、BytesIO全量缓存、HttpResponse一把返回——这条链路里每一环都在赌数据量小。这个场景其实特别典型Django 项目里导出 Excel 并触发浏览器下载代码不到五十行就能跑通但真正决定它能不能上生产的是数据量、响应方式和前端触发这三件事的配合。这篇笔记就围绕一份可直接复现的导出实现展开从xlwt写表、HttpResponse设头到前端XMLHttpRequest拿 blob 触发下载再到百万级数据下StreamingHttpResponse配合流式游标绕开 memoryerror 和 nginx timeout。适合正在写 Django 后台、需要给运营或客户提供「一键导出」能力的同学新手能照着抄熟手能对着边界参数改。2. 最小可用导出链路xlwt 写表 HttpResponse 设头 前端 blob 下载先把能跑通的那一版讲透。这条链路分三段后端查数据并生成 Excel 二进制流、HTTP 响应头声明这是附件、前端拿到二进制后伪造点击触发下载。三段任何一段没对齐表现就是「点了没反应」或者「下载下来是乱码」。2.1 依赖与数据模型前提xlwt是纯 Python 写.xls注意是老的 BIFF 格式不是.xlsx的库安装就一行pip install xlwt它只负责写不负责读也不依赖 Excel 环境所以在服务器上跑没有额外负担。数据模型这边原文用的是place字段有place地名、sum次数、lng经度、lat纬度查询条件是city。你换成自己的模型时只要保证filter出来的 QuerySet 里每个对象能取到要写的列就行。这里有个容易忽略的点xlwt写入的单元格值类型是它自己推断的字符串、数字、日期处理方式不同经纬度这种浮点数直接写没问题但如果你的字段是Decimal建议先float()转一下否则某些版本会写成文本。2.2 后端视图从 QuerySet 到 Excel 字节流核心视图长这样我按原文结构补了必要的异常和空值处理from django.http import HttpResponse from django.shortcuts import render from io import BytesIO import xlwt from .models import place def export_excel(request): # 前端用 POST 传城市名GET 也能取但导出这种带副作用的操作建议 POST city request.POST.get(city) if not city: return HttpResponse(city 参数缺失, status400) list_obj place.objects.filter(citycity) # 声明响应类型为 Excel浏览器识别后会走下载而不是渲染 response HttpResponse(content_typeapplication/vnd.ms-excel) # attachment 表示作为附件下载filename 后面拼城市名 response[Content-Disposition] attachment;filename city .xls if list_obj: # 创建工作簿encoding 指定 utf-8否则中文表头会乱码 ws xlwt.Workbook(encodingutf-8) # 新建一个 sheet名字叫 sheet1 w ws.add_sheet(sheet1) # 写表头write(行, 列, 值)行列都从 0 开始 w.write(0, 0, u地名) w.write(0, 1, u次数) w.write(0, 2, u经度) w.write(0, 3, u纬度) # 从第 1 行开始写数据第 0 行已经被表头占了 excel_row 1 for obj in list_obj: w.write(excel_row, 0, obj.place) w.write(excel_row, 1, obj.sum) w.write(excel_row, 2, obj.lng) w.write(excel_row, 3, obj.lat) excel_row 1 # 写到内存 IO不要写磁盘避免并发时文件名冲突和清理问题 output BytesIO() ws.save(output) # save 之后指针在末尾必须 seek(0) 回到开头否则 response 写出去是空的 output.seek(0) response.write(output.getvalue()) return response逻辑说明HttpResponse默认content_type是text/html这里改成application/vnd.ms-excel浏览器才知道这是 Excel 文件。Content-Disposition的attachment是关键没有它浏览器可能直接在页面里尝试打开。BytesIO作为中间缓冲ws.save(output)把工作簿序列化进去seek(0)这一步是血泪经验——漏了它下载下来的文件大小是 0排查半天以为是权限问题。参数上encodingutf-8必须显式给xlwt默认编码在某些环境下写中文会出问题。2.3 前端触发XMLHttpRequest 拿 blob 再伪造 a 标签点击前端不能直接用window.location跳转因为要带 POST 参数和 CSRF token所以走XMLHttpRequest$(#export_excel).click(function () { // Django 的 CSRF token 藏在表单隐藏域里POST 必须带上 var csrf $(input[namecsrfmiddlewaretoken]).val(); const req new XMLHttpRequest(); req.open(POST, /export_excel/, true); // 关键声明响应类型为 blob否则拿到的是字符串二进制会损坏 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 blob new Blob([data]); const blobUrl window.URL.createObjectURL(blob); download(blobUrl); }; }); function download(blobUrl) { var city $(input[namecity]).val(); const a document.createElement(a); a.style.display none; // download 属性决定保存的文件名这里用城市名拼 .xls a.download city .xls; a.href blobUrl; a.click(); document.body.removeChild(a); }逻辑说明responseType blob是整段前端代码的命门不设它req.response会按文本解析Excel 的二进制字节被当字符串处理下载下来必然打不开。createObjectURL把 blob 转成一个临时 URLa.download指定文件名a.click()触发下载。注意download函数里原文写的是文件命名占位实际要替换成真实文件名否则浏览器会用默认名。参数上Content-Type设成application/x-www-form-urlencoded是为了让 Django 能正常解析 POST body如果你改用FormData这个头要让浏览器自己设别手动覆盖。提示Content-Disposition里的 filename 如果含中文部分浏览器会乱码稳妥做法是用filename*UTF-8加 URL 编码或者干脆用英文名前端a.download再改成中文。3. 数据量上到十万级StreamingHttpResponse 与流式游标改造上一章那套在几千条数据下没问题但一旦单城数据过十万list_obj全量进内存、xlwt全量写、BytesIO全量缓存三个「全量」叠加memoryerror 和 nginx timeout 是迟早的事。这一章讲怎么把它改成流式。3.1 为什么 HttpResponse 会崩三个全量点先定位问题。place.objects.filter(citycity)返回的是惰性 QuerySet但for obj in list_obj一旦开始迭代Django 会把整个结果集取回内存除非你用.iterator()。这是第一个全量点。xlwt的Workbook对象在内存里维护整个表格结构ws.save(output)之前所有行都在内存这是第二个。output.getvalue()把整个字节流复制一份给 response这是第三个。原文提到「超过 15 万行大概就报 memoryerror」1G 内存下这个数字是合理的。nginx 那边默认proxy_read_timeout60 秒后端生成时间一超连接被掐前端看到的就是 502 或 timeout。3.2 StreamingHttpResponse 的适用边界Django 提供三种返回方式HttpResponse、FileResponse、StreamingHttpResponse。FileResponse适合「服务端已有文件、直接传」它内部也是迭代器可以指定 chunk 大小但它没法实时从数据库生成内容——你总不能让数据库先落成文件再传。StreamingHttpResponse接受一个迭代器边生成边发内存占用只跟单次 chunk 有关这才是数据库导出的正解。它的代价是一旦开始发送就不能再改响应头也没法在出错时返回一个漂亮的错误页所以生成逻辑里的异常要提前处理好。3.3 用 SSDictCursor fetchone 改造取数逻辑光换StreamingHttpResponse不够因为如果取数还是fetchall内存照样爆。原文给的方案是pymysql的SSDictCursor服务端流式游标配合fetchone逐条取import pymysql from django.http import StreamingHttpResponse field_types {1: tinyint, 2: smallint, 3: int} # 字段类型映射按需补全 def export_stream(request): city request.POST.get(city) conn pymysql.connect(host127.0.0.1, port3306, databasedemo, userroot, passwordroot) # 关键改动SSDictCursor 让结果集留在服务端客户端逐条拉 cursor conn.cursor(cursorpymysql.cursors.SSDictCursor) cursor.execute(SELECT place, sum, lng, lat FROM place WHERE city%s, [city]) # 从 cursor.description 拿列名构造表头 cols {} for i, row in enumerate(cursor.description): cols[row[0]] field_types.get(row[1], str(row[1])) response StreamingHttpResponse(get_result_format(cursor, cols)) response[Content-Type] application/octet-stream response[Content-Disposition] attachment;filename{0}.csv.format(city) return response def get_result_format(cursor, cols): # 先 yield 表头 tmp_str for col in cols: tmp_str %s, % col yield tmp_str.strip(,) \n # 再逐条 yield 数据行fetchone 每次只取一条 while True: row cursor.fetchone() if row is None: break tmp_str for col in cols: tmp_str %s, % str(row[col]) yield tmp_str.strip(,) \n逻辑说明SSDictCursor和普通DictCursor的区别在于普通游标执行execute时就把全部结果拉到客户端内存SSDictCursor把结果留在 MySQL 服务端fetchone每次只取一条内存占用恒定。get_result_format是个生成器yield出去的内容会被StreamingHttpResponse逐块发送客户端边收边写文件。参数上Content-Type用application/octet-stream是通用二进制流配合.csv后缀Excel 也能打开。注意这里输出的是 CSV 不是 xls因为流式场景下xlwt那种「先建完整工作簿再保存」的模式和流式天然冲突CSV 是纯文本行式结构最适合边取边发。注意SSDictCursor要求连接在整个迭代期间保持打开别在生成器外面conn.close()否则迭代到一半连接断了。常见做法是在生成器的finally里关连接。3.4 百万级数据下的参数调优数据量再往上走光靠流式还不够几个参数要调。MySQL 侧SSDictCursor每次fetchone是一次网络往返百万行就是百万次往返慢得离谱可以用fetchmany(size1000)批量取内存和速度折中。nginx 侧proxy_read_timeout和proxy_send_timeout要调大比如 300 秒否则流式传到一半照样被掐。Django 侧如果用StreamingHttpResponse配合 gunicorn注意 worker 的超时--timeout也要同步放大。还有一个容易翻车的点流式响应一旦开始中间抛异常客户端拿到的是半截文件所以取数前该做的校验city 是否存在、表是否存在要全部前置。参数默认值大数据场景建议作用nginx proxy_read_timeout60s300s等待后端响应的最长时间nginx proxy_send_timeout60s300s向后端发送请求的最长时间gunicorn --timeout30s300sworker 处理单请求超时fetchmany size无1000每次从服务端拉取的行数4. 导出功能避坑清单从乱码到超时的五条实战记录这一章把我在实际项目里踩过的坑集中列一下每条按「现象 → 原因 → 解决」写都是导出场景高频出现的。4.1 下载下来的 Excel 中文全是乱码现象文件能下载用 Excel 打开表头「地名」「次数」显示成方块或问号。原因xlwt.Workbook()没指定encodingutf-8或者指定了但写入的字符串本身不是 unicode。解决创建 Workbook 时显式传encodingutf-8写入的字符串统一用u中文或确保是strPython 3 默认 unicode。如果是 CSV 方案还要在文件开头写 BOM\ufeff否则 Excel 打开 CSV 也会乱码。4.2 文件下载下来是 0 字节现象点击导出浏览器确实下载了但文件大小 0打开报错。原因BytesIO在ws.save(output)之后指针停在末尾直接response.write(output.getvalue())拿到的是空内容。解决save之后必须output.seek(0)回到开头。这个坑我在两个项目里各踩过一次后来养成习惯凡是BytesIO写完要读先seek(0)。4.3 前端拿到的是乱码字符串而不是文件现象请求返回 200但下载的文件内容是一堆乱码文本或者浏览器直接把二进制当页面渲染了。原因XMLHttpRequest没设responseType blob默认按text解析二进制被破坏。解决req.open之后、req.send之前设req.responseType blob。另外后端Content-Type要和前端处理方式匹配用 blob 方式的话后端返回application/octet-stream最稳。4.4 数据量一大就 502 或 memoryerror现象小数据量正常超过十几万行就报 500memoryerror或 502nginx timeout。原因HttpResponsefetchallxlwt全量内存模式。解决换StreamingHttpResponseSSDictCursorfetchone/fetchmany输出格式从 xls 改 csv同时调大 nginx 和 gunicorn 超时。这个改造不是可选项是数据量上来的必选项。4.5 CSRF 校验失败导致 403现象前端点击导出控制台报 403后端日志显示 CSRF verification failed。原因POST 请求没带csrfmiddlewaretoken或者带了但 key 不对。解决从页面隐藏域input[namecsrfmiddlewaretoken]取值拼进 POST body。如果用FormData把 token 作为其中一个字段 append 进去。别图省事用csrf_exempt导出是写操作豁免 CSRF 是给自己挖坑。5. 进阶把导出做成可复用工具与自检习惯走到这里基本功能和大数据量改造都覆盖了。最后分享两个我后来固定下来的做法一个是把导出逻辑抽成可复用的工具函数一个是上线前的自检清单。先说复用。每个模型都写一遍export_excel视图太蠢我一般抽一个通用函数接收 QuerySet、列定义、文件名返回响应def build_excel_response(queryset, columns, filename): queryset: 已 filter 好的 QuerySet columns: [(字段名, 表头), ...] filename: 下载文件名不含扩展名 response HttpResponse(content_typeapplication/vnd.ms-excel) response[Content-Disposition] attachment;filename{}.xls.format(filename) ws xlwt.Workbook(encodingutf-8) w ws.add_sheet(sheet1) for col_idx, (_, header) in enumerate(columns): w.write(0, col_idx, header) for row_idx, obj in enumerate(queryset.iterator(), start1): for col_idx, (field, _) in enumerate(columns): w.write(row_idx, col_idx, getattr(obj, field)) output BytesIO() ws.save(output) output.seek(0) response.write(output.getvalue()) return response这里queryset.iterator()是另一个省内存的点它让 Django 分批从数据库取而不是一次性加载。列定义用(字段名, 表头)元组列表调用方传[(place, 地名), (sum, 次数)]就行。参数上filename不含扩展名函数内部拼.xls避免调用方重复写。再说自检。导出功能上线前我现在固定跑这几项小数据量10 行验证文件能打开、中文不乱码边界数据量0 行验证不报错、返回空文件而不是 500大数据量构造 20 万行验证内存和超时前端在 Chrome 和 Edge 各点一次确认文件名和内容对得上CSRF 在登录态和未登录态各测一次。这套清单帮我拦下过至少三次「本地好好的、线上就崩」的问题。从那以后我每次写导出都强制先问自己三个问题数据量上限是多少、响应走的是全量还是流式、前端拿的是 blob 还是文本。这三个问题答清楚坑基本就绕开了。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询