纯 Python 实现:百万级数据高效导出 Excel/CSV(可直接运行的完整示例,仅供参考)

132 阅读6分钟

只用 Python,手把手实现 低内存、可流式 的大数据导出。涵盖:

  • openpyxlwrite_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 持续写出;
  • 稳定优先:对外暴露导出接口时,建议后台任务生成 + 链接下载,更抗压