只用 Python,手把手实现 低内存、可流式 的大数据导出。涵盖:
openpyxl的 write_only 流式写 XLSX- 标准库
csv的超轻量导出- Flask/FastAPI 边写边下 HTTP 下载
- 从数据库分页读取的 生产-消费 管道
- 常见性能/内存坑位与避坑指南
快速结论
- CSV 首选:如果不需要样式/合并/公式,标准库
csv写入最快、最省内存,适合千万级行数。 - XLSX 必用
write_only:用openpyxl.Workbook(write_only=True)+ws.append流式写,大幅降低内存。 - 端到端流式:数据库分页/游标 → 逐批写入 → (可选)HTTP 直接下载;中途可取消、失败可重试。
- 避免逐格设样式:越少越好;只给表头来一点点样式就行。
最小可跑:openpyxl 流式写 100 万行 XLSX
依赖:
pip install openpyxl
# file: export_xlsx_stream.py
from openpyxl import Workbook
from openpyxl.styles import Font
from time import perf_counter
def export_xlsx(path: str, rows: int = 1_000_000):
t0 = perf_counter()
# 关键:write_only 模式,避免在内存中维护整张表
wb = Workbook(write_only=True)
ws = wb.create_sheet("Sheet1")
# 表头 + 轻量样式(仅一次即可)
header = ["ID", "Name", "Amount", "CreatedAt"]
ws.append([h for h in header]) # write_only 下不能直接设样式到单元格
# 提示:如果一定要给表头加粗,可在非 write_only 下先创建模板,再追加数据;或仅导出数据不加样式
from datetime import datetime, timedelta
base = datetime(2024, 1, 1, 0, 0, 0)
for i in range(1, rows + 1):
ws.append([
i,
f"User_{i}",
i % 10_000,
(base + timedelta(seconds=i)).isoformat()
])
# 可选:每 N 行打印一次进度(避免频繁打印影响性能)
if i % 100_000 == 0:
print(f"[progress] rows={i}")
wb.save(path)
print(f"done: {path}, rows={rows}, elapsed={perf_counter() - t0:.2f}s")
if __name__ == "__main__":
export_xlsx("bigdata.xlsx", rows=1_000_00) # 示例:10 万行先试跑
# 生产可调到 1_000_000 或更多(注意磁盘 IO)
要点
- write_only=True 才是低内存关键;ws.append 是批量行写入的正确姿势。
- openpyxl 可把 Workbook.save() 写到文件路径或文件对象(BytesIO/socket 不建议:XLSX 需要一次性打包,内存/网络易抖)
极致轻量:标准库 CSV 流式导出
零依赖、速度快、最稳;Excel/Numbers/Sheets 都能打开。
# file: export_csv_stream.py
import csv
def export_csv(path: str, rows: int = 1_000_000, flush_every: int = 100_000):
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
# 表头
w.writerow(["ID", "Name", "Age"])
for i in range(1, rows + 1):
w.writerow([i, f"User_{i}", 18 + (i % 30)])
if i % flush_every == 0:
# 适度 flush,降低缓冲占用,增强容错
f.flush()
print(f"[progress] rows={i}")
if __name__ == "__main__":
export_csv("bigdata.csv", rows=1_000_00) # 先 10 万行试跑
小贴士
- Windows 用户如果遇到 Excel 乱码,可考虑写入 BOM:f.write('\ufeff') 再创建 csv.writer。
- 分隔符可改为 csv.writer(..., delimiter=';') 以适应不同语言区设置。
HTTP 直接下载(无中间文件)
方案 A:CSV 边写边下(最推荐)
使用 Flask:pip install flask
# file: app_csv_stream.py
from flask import Flask, Response
import csv
from io import StringIO
app = Flask(__name__)
def iter_csv(rows=500_000):
# 使用生成器一边写一边产出
sio = StringIO()
w = csv.writer(sio)
w.writerow(["ID", "Name", "Age"])
yield sio.getvalue()
sio.seek(0); sio.truncate(0)
for i in range(1, rows + 1):
w.writerow([i, f"User_{i}", 18 + i % 30])
if i % 2000 == 0:
# 每 2000 行刷新一次缓冲(调优参数)
yield sio.getvalue()
sio.seek(0); sio.truncate(0)
# 末尾未刷新的部分
yield sio.getvalue()
@app.get("/export/csv")
def export_csv():
headers = {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": 'attachment; filename="report.csv"',
"Cache-Control": "no-cache",
}
return Response(iter_csv(), headers=headers)
if __name__ == "__main__":
app.run("0.0.0.0", 8080, threaded=True)
方案 B:XLSX 下载(建议落盘或临时文件)
XLSX 是压缩打包结构,通常需要一次性写完。推荐:
- 用 tempfile.NamedTemporaryFile(delete=False) 先写盘,再 send_file。
- 或者后台任务生成后提供下载链接。
# file: app_xlsx_tempfile.py
from flask import Flask, send_file
from tempfile import NamedTemporaryFile
from openpyxl import Workbook
app = Flask(__name__)
@app.get("/export/xlsx")
def export_xlsx():
tmp = NamedTemporaryFile(delete=False, suffix=".xlsx")
tmp.close()
wb = Workbook(write_only=True)
ws = wb.create_sheet("Sheet1")
ws.append(["ID", "Name"])
for i in range(1, 200_001):
ws.append([i, f"User_{i}"])
wb.save(tmp.name)
return send_file(
tmp.name,
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
as_attachment=True,
download_name="report.xlsx",
max_age=0
)
if __name__ == "__main__":
app.run("0.0.0.0", 8080, threaded=True)
数据库分页:生成器 + 管道式写出
示例用 sqlite3;换成 MySQL/PostgreSQL 只需改驱动与 SQL。 核心:不要把结果一次性读进内存,用 生成器 逐批产出。
# file: db_stream_pipeline.py
import sqlite3
from contextlib import closing
from typing import Iterator, Tuple
def init_demo_db(db=":memory:", rows=100_000):
conn = sqlite3.connect(db)
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, amount INT)")
conn.executemany("INSERT INTO t (name, amount) VALUES (?, ?)",
[(f"User_{i}", i % 1000) for i in range(1, rows + 1)])
conn.commit()
return conn
def stream_rows(conn: sqlite3.Connection, page_size=5000) -> Iterator[Tuple[int, str, int]]:
offset = 0
while True:
with closing(conn.execute(
"SELECT id, name, amount FROM t ORDER BY id LIMIT ? OFFSET ?",
(page_size, offset)
)) as cur:
batch = cur.fetchall()
if not batch:
break
for row in batch:
yield row
offset += len(batch)
def export_rows_to_csv(conn: sqlite3.Connection, path: str):
import csv
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["ID", "Name", "Amount"])
for rid, name, amt in stream_rows(conn, page_size=10_000):
w.writerow([rid, name, amt])
def export_rows_to_xlsx(conn: sqlite3.Connection, path: str):
from openpyxl import Workbook
wb = Workbook(write_only=True)
ws = wb.create_sheet("Sheet1")
ws.append(["ID", "Name", "Amount"])
for rid, name, amt in stream_rows(conn, page_size=10_000):
ws.append([rid, name, amt])
wb.save(path)
if __name__ == "__main__":
conn = init_demo_db(rows=200_000)
export_rows_to_csv(conn, "db_export.csv")
export_rows_to_xlsx(conn, "db_export.xlsx")
生产建议:深分页可改 keyset 分页(基于上次最大 ID): WHERE id > :last_id ORDER BY id LIMIT :page_size,性能更稳。
样式/列宽/冻结窗格(适度即可)
write_only 模式下,不要逐格设置样式;如果一定要样式,建议两种方式:
- 模板法:先准备一个带样式的模板(非 write_only),数据区仅写值;
- 仅表头加样式:减少样式指令数量。
# 非 write_only 场景才建议这样做;大数据建议样式极简
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
wb = Workbook()
ws = wb.active
ws.title = "Styled"
ws.append(["ID", "Name", "Amount"])
for cell in ws[1]:
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center")
ws.freeze_panes = "A2" # 冻结首行
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 12
wb.save("styled_small.xlsx")
常见坑与优化清单
1.内存暴涨
- XLSX 必开 write_only=True;CSV 直接流式即可。
- 避免把全量数据拉成大列表,再一次性写入。
2.导出超慢
- 移除逐格样式/公式;改用行级 append;减少 Python 层频繁 I/O(适度批量 flush)。
- 数据库端只查必要列,建好索引,使用 keyset 翻页。
3.浏览器下载中断
- 选择 CSV 边写边下;XLSX 建议先落盘后 send_file。
- 记录 Broken pipe 当作正常中断处理,及时回收资源。
4.Excel 打开 CSV 乱码
-
指定 charset=utf-8;Windows 环境可写 BOM:f.write('\ufeff')。
-
分隔符与地区设置冲突时,可改 delimiter=';'。
5.大文件传输困难
- 生成完压缩(CSV→ZIP)再提供下载;或异步生成后提供临时链接。
- HTTP 端设置合理超时与断点续传(Nginx/对象存储支持)。
6.并发顺序错乱
- 多协程/多线程并发查,单消费者顺序写;或多 Sheet 并行,每个 Sheet 一个 writer。
总结
- 能用 CSV 就用 CSV:更快、更省内存、适合海量数据;
- 必须 XLSX 就开 write_only:按行追加,样式从简;
- 端到端流式:数据库分页/游标 → 生成器迭代 → 文件/HTTP 持续写出;
- 稳定优先:对外暴露导出接口时,建议后台任务生成 + 链接下载,更抗压