Python批量导出数据库表到Excel:自动化脚本实战指南
2026/9/9 15:47:58 网站建设 项目流程

做数据支持或者报表开发的朋友,大概率都遇到过这种需求:业务部门跑过来说“帮我把XX系统里这几张表的数据导成 Excel 发我”,你打开数据库客户端,连上库、写几个查询、挨个导出。一次两次还能忍,等表多了、数据量上来了、或者每周都要重复做一遍,手动操作就非常折磨人——漏表、格式不统一、导到一半客户端卡死,这些都是家常便饭。

用 Python 写一个批量导出脚本解决这类问题,是我这几年一直在用的方案。它能把“连库→查询→导出→命名→归档”整条链路自动化,适合数据运营、测试开发、运维、刚入门 Python 的办公自动化需求者参考。这篇文章我会把完整的实现思路、代码、踩坑点都摊开讲,照着改就能用。

1. 这个需求藏在哪些场景里:先想清楚再动手

1.1 被业务方追着要 Excel 的日常

很多人以为“数据库导出 Excel”就是把查询结果存成文件这么简单,但真正工作里它往往不是一次性动作,而是周期性、批量性的需求。

我见过两种非常典型的场景。第一种是给业务方做数据交付,比如电商运营要每月导出订单明细、商品销售汇总、用户画像标签表,一张两张表还好,十几张表一起要就麻烦;第二种是数据迁移或数据备份,需要把库里某些历史表完整导出来归档,这时候通常还带着时间范围、状态条件等各种过滤逻辑。

还有一种场景容易被忽略——导出不是给人看,而是给下游系统用的。比如把数据库里的数据导成 Excel 后,再交给另一个同事跑 VBA 宏或者导入到某个业务平台。这种场景下,Excel 的格式就比内容更敏感:列顺序不能乱、字段名不能改、数字必须是文本格式,否则下游程序直接报错。

我之所以强调“先想清楚场景”,是因为导出脚本的写法完全取决于你是给“人看”还是给“系统用”。给人看的要处理格式、列宽、可读性;给系统用的要保证字段类型、Sheet 名称、文件编码严格可控。需求没理清就写代码,后面大概率返工。

1.2 手动导出为什么不可靠:三个真实痛点

拿 Navicat、DBeaver 这类数据库客户端手动导出,确实能解决问题,但只在数据量小、次数少的时候成立。手动导出至少有三个绕不开的痛点。

第一个痛点是漏表和漏数据。表一多,你很难保证每次导出的都是同一批表。今天导了 5 张,明天业务说还要加 2 张,后天又有人说某个 sheet 少了几行数据——版本漂移就是这么来的。手动查询很难复现完全一致的结果,尤其是涉及时间范围筛选时,差一分钟数据就可能不一致。

第二个痛点是格式不统一。不同的人导出的 Excel 可能列宽不一样、行高不一样、日期格式有的是文本有的是时间、空值有的显示 NULL 有的是空单元格。这些细节在单独看文件时没感觉,一旦下游要合并处理或者做数据校验,问题就全暴露了。

第三个痛点是效率瓶颈。几十万行数据手动导出,客户端很容易卡死或者内存飙升;超过 100 万行,Excel 本身也要炸。所以很多人最后发现,最好的方式不是“手动导出大文件”,而是“让脚本自动按条件分文件导出”。这几个痛点,就是 Python 脚本要解决的核心问题。

1.3 方案选型:pandas 为主,xlsxwriter 兜底

Python 导出 Excel 的常见组合我列一下:pandas + openpyxl、pandas + xlsxwriter、裸用 openpyxl、裸用 xlsxwriter。很多人一上来就懵,不知道该选哪套。

我的习惯是“数据在 DataFrame 里处理用 pandas,写 Excel 引擎用 xlsxwriter”。pandas 的read_sql可以直接从数据库读取查询结果,生成 DataFrame 后to_excel一行就能写出文件,代码量最少。而 xlsxwriter 作为写入引擎,好处是支持流式写入、可以精细控制格式和布局,特别适合大数据量和小细节要求高的场景。

openpyxl 也不是不能用,它在读写已存在的 Excel 文件上很强,比如要修改模板、保留已有格式。但如果你是“从零生成一个全新 Excel”,xlsxwriter 性能更好,功能也更贴近需求,比如设置单元格格式、冻结窗格、自动筛选、条件格式这些都能直接调 API。所以我的结论是:日常批量导出,优先pandas.to_excel(engine='xlsxwriter');遇到超大表,直接裸用 xlsxwriter 流式写入。

2. 环境准备:装对库,连接参数一步到位

2.1 需要安装的依赖与各自职责

先把依赖列出来,少装一个都会踩坑。我常规用的是这几组库:

  • pandas:负责 SQL 查询、数据处理、批量写 Excel 的入口。
  • pymysql:连接 MySQL 的驱动。如果你用的是 PostgreSQL,那改成psycopg2;SQL Server 用pymssql;Oracle 用cx_Oracle。核心逻辑都一样,只是连接参数不同。
  • sqlalchemy:创建数据库引擎的推荐方式。可以直接用pandas.read_sql(sql, engine),比传 pymysql 连接对象更稳,尤其是在需要分块读取时。
  • xlsxwriter:写入 Excel 的核心引擎,负责格式控制和大文件流式写入。

安装命令很简单:

pip install pandas pymysql sqlalchemy xlsxwriter

如果你只想写最小脚本,pandas + pymysql 就够了;但当你开始做批量、分块、格式化导出时,sqlalchemy 和 xlsxwriter 基本是标配。

2.2 连接参数的几个坑:字符集、超时、SSL

连接数据库这个步骤看起来简单,但参数配不对,各种幺蛾子都会冒出来。

第一个坑是字符集。如果你不显式指定charset='utf8mb4',很多 MySQL 库默认用latin1或者utf8mb3连接,中文读出来就可能乱码。utf8mb4是 MySQL 8 之后的推荐字符集,能存 emoji 和生僻字,建议直接固定。

第二个坑是超时时间。数据库连接默认的超时时间不一定够你用,尤其是大查询跑几分钟的时候,连接可能提前断开,脚本报MySQL server has gone away。我一般在连接参数里把connect_timeoutread_timeoutwrite_timeout都设成 60 秒以上,避免大查询中途断连。

第三个坑是 SSL。如果公司数据库开启了 SSL 连接,pymysql 默认行为可能连不上,需要在连接串里追加 SSL 相关配置。这个看实际情况,我建议先用客户端确认能连上,再用 Python 脚本连,能省去很多排查时间。

用 SQLAlchemy 创建连接串时,我的写法长这样:

from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://user:password@127.0.0.1:3306/dbname?charset=utf8mb4", connect_args={ "connect_timeout": 60, "read_timeout": 120, "write_timeout": 120, }, pool_pre_ping=True, pool_recycle=3600, )

重点说下pool_pre_ping。它会在每次从连接池拿连接之前先 ping 一下,发现连接已断就重建。这个参数救过我很多次,尤其是跑批量导出任务时,连接池里的连接很容易被数据库服务端回收,不加这个参数会突然报错。

2.3 先跑通一个最小导出脚本

环境配好之后,我建议先写一个最小化的导出脚本,确认整条链路是通的,再往上堆功能。最小脚本大概这个样子:

import pandas as pd from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://user:password@127.0.0.1:3306/dbname?charset=utf8mb4", connect_args={"connect_timeout": 60} ) df = pd.read_sql("SELECT * FROM orders LIMIT 1000", engine) df.to_excel("orders_preview.xlsx", index=False, engine="xlsxwriter") print("导出成功,共", len(df), "行")

这段代码做的事很简单:连接数据库、查前 1000 行、写 Excel。跑通这一步,说明你的库装对了、连接参数没问题、excel 写入也正常。然后再去处理完整表、批量表、大表这些复杂场景。

我特别建议在正式导出前先用LIMIT 1000做一次预览,把字段名、数据样例、类型都过一眼,确认没问题再全量导出。很多格式问题,在这种小样本里就能提前发现。

3. 核心实现:从单表导出到批量多表导出

3.1 单表导出:pandas 五步完成

单表导出是最基础的能力,逻辑上就五步:建引擎、写 SQL、查数据、写 Excel、收尾。

我用一个具体例子来说明。假设订单表orders有这些字段:order_iduser_idproduct_nameamountstatuscreated_at,我想把 2024 年 1 月的订单导出来:

import pandas as pd from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://user:password@127.0.0.1:3306/dbname?charset=utf8mb4", connect_args={"connect_timeout": 60} ) sql = """ SELECT order_id, user_id, product_name, amount, status, created_at FROM orders WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-02-01 00:00:00' """ df = pd.read_sql(sql, engine) with pd.ExcelWriter("orders_202401.xlsx", engine="xlsxwriter") as writer: df.to_excel(writer, sheet_name="orders", index=False) print("导出行数:", len(df))

这里的pd.ExcelWriter作为上下文管理器使用,写完会自动关闭文件,避免文件占用问题。index=False一定要加上,否则默认会把 DataFrame 的行索引写进 A 列,Excel 里多出一列莫名的数字,很容易被下游误会成数据。

3.2 批量导出:表清单 + 循环,改造成本很低

批量导出最常见的做法就是维护一张表清单,然后用循环逐个处理。我通常用两种方式维护表清单:代码里的列表,或者配置文件。

先用最简单的列表方式:

import pandas as pd from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://user:password@127.0.0.1:3306/dbname?charset=utf8mb4", connect_args={"connect_timeout": 60} ) tables = ["users", "orders", "products", "order_items", "payments"] for table in tables: try: df = pd.read_sql(f"SELECT * FROM {table}", engine) file_name = f"export_{table}.xlsx" with pd.ExcelWriter(file_name, engine="xlsxwriter") as writer: df.to_excel(writer, sheet_name=table, index=False) print(f"{table} 导出成功,共 {len(df)} 行 -> {file_name}") except Exception as e: print(f"{table} 导出失败: {e}")

这个脚本结构够用了,但我必须提醒一句:SELECT *在批量导出时并不是好习惯。它会把表里所有列都导出来,包括一些业务上不想暴露的内部字段,比如is_deletedupdate_time、内部备注之类的。更稳的做法是把每张表需要导出的列显式定义在配置里。

所以到了稍微复杂一点的需求,我习惯把表清单升级成字典结构,把“表名”和“导出列”绑定:

export_config = { "users": ["id", "username", "email", "created_at"], "orders": ["order_id", "user_id", "amount", "status", "created_at"], } for table, columns in export_config.items(): col_str = ", ".join(columns) df = pd.read_sql(f"SELECT {col_str} FROM {table}", engine) df.to_excel(f"export_{table}.xlsx", index=False, engine="xlsxwriter")

这样配置和逻辑分离,后续业务方要加列减列,你只改 dict,不需要改代码。

3.3 导出带格式的 Excel:列宽、冻结行、文本格式

光把数据导出来只是及格,真正让业务方觉得“你这脚本专业”的,是导出的 Excel 打开以后不用再手动调格式。我做交付时会重点处理三个细节:列宽、冻结首行、文本型数字。

列宽不设置的话,很多列内容会被截断,看起来非常乱。xlsxwriter 支持按列设置宽度,我通常根据该列最大内容长度估算,稍微加一点余量:

import pandas as pd df = pd.read_sql("SELECT * FROM orders", engine) with pd.ExcelWriter("orders_with_format.xlsx", engine="xlsxwriter") as writer: df.to_excel(writer, sheet_name="orders", index=False) worksheet = writer.sheets["orders"] worksheet.freeze_panes(1, 0) # 冻结首行 for i, col in enumerate(df.columns): max_len = max( df[col].astype(str).map(len).max(), # 该列最大内容长度 len(str(col)) # 列名本身长度 ) + 2 worksheet.set_column(i, i, max_len)

freeze_panes(1, 0)是冻结首行,让业务方往下翻数据时能一直看到列名。这是我很喜欢的一个功能,因为数据一多,没有冻结首行的话翻到第 500 行根本不知道某列是什么。

文本格式这个点,身份证号、银行卡号、订单号这种长数字最容易出问题。Excel 默认会把超过 15 位的数字转成科学计数法,而且后几位直接变成 0,数据被损坏了业务方还不一定第一时间发现。解决办法是在写入时把这类列设置成文本格式,pandas 里先把字段转成字符串,再在 xlsxwriter 里对单元格设置文本格式:

with pd.ExcelWriter("users_with_idcard.xlsx", engine="xlsxwriter") as writer: df["id_card"] = df["id_card"].astype(str) # 强制转字符串 df.to_excel(writer, sheet_name="users", index=False) worksheet = writer.sheets["users"] text_format = writer.book.add_format({"num_format": "@"}) # 假设 id_card 是第 3 列,索引为 2 worksheet.set_column(2, 2, 20, text_format)

这里的num_format: "@"是 xlsxwriter 里的文本格式,设置后单元格内容会被当成文本处理,不会再变科学计数法。列索引可以从 DataFrame 列名里动态找,比如df.columns.get_loc("id_card"),这样更不容易写死。

4. 大数据量下的性能优化:分块读取与流式写入

4.1 全表读进内存会发生什么事

小表怎么导都没事,但一旦表里有几百万行、几十个字段,直接把全表塞进 DataFrame 就有问题了。内存占用会飙升,可能 8G 内存的电脑直接卡死,甚至触发 OOM 进程被杀。

我印象很深的一次,是导一张 500 万行的日志表,每个字段还有不少长文本。我一开始图省事,直接pd.read_sql("SELECT * FROM logs", engine),然后眼睁睁看着内存占用从 1G 一路涨到 7G,最后进程被系统杀掉,数据库连接也断了。那次之后我总结了一个原则:能用 SQL 过滤的绝不在 Python 里过滤,能分块读的绝不一次性全读。

优化思路无非两个方向:一是控制每次读取的数据量,分块处理;二是用流式写入,不要让数据积压在内存里。这两招配合起来,几百 MB 甚至几 GB 的数据导出到 Excel 也不是问题。

4.2 fetchmany 分块 + xlsxwriter 流式写入

处理超大表的时候,我不再用 pandas 一次读全表,而是改用原始 SQL 游标 +fetchmany分批取数,然后用 xlsxwriter 逐行写入。

import xlsxwriter import pymysql conn = pymysql.connect( host="127.0.0.1", port=3306, user="user", password="password", database="dbname", charset="utf8mb4", cursorclass=pymysql.cursors.SSCursor, # 服务端游标,避免一次性加载全部结果 connect_timeout=60, read_timeout=600, ) workbook = xlsxwriter.Workbook("big_table.xlsx") worksheet = workbook.add_worksheet("data") cursor = conn.cursor() cursor.execute("SELECT order_id, user_id, amount, created_at FROM orders") # 先写表头 headers = [desc[0] for desc in cursor.description] for col_idx, header in enumerate(headers): worksheet.write(0, col_idx, header) row = 1 while True: rows = cursor.fetchmany(50000) # 每批 5 万行 if not rows: break for record in rows: for col_idx, value in enumerate(record): worksheet.write(row, col_idx, value) row += 1 print(f"已处理到第 {row} 行") cursor.close() conn.close() workbook.close()

这个脚本的核心是pymysql.cursors.SSCursor,也就是服务端游标。普通游标会把查询结果全部缓存在客户端内存里,等于你还是变相全表加载;而SSCursor是边查边取,配合fetchmany(50000)每次只取 5 万行,内存占用会非常稳定。

xlsxwriter 本身是流式写入的,数据一行一行写进去,不会在内存里堆整个 DataFrame。实测几百万行的数据,这个脚本内存占用可以控制在几百 MB 以内。相比 pandas 方案,这里牺牲了一点代码简洁度,但换来了在大数据量下的稳定性,值。

4.3 分列、按日期增量、多线程等进阶思路

如果表实在太大,比如上亿行,那就算用 fetchmany 流式写入,一个 Excel 文件也装不下。这时候就要考虑拆分了。

我常用的拆分策略有三种。第一种是“按 Sheet 拆分”,每 50 万行写一个 Sheet,Excel 文件里能放下多个 Sheet;第二种是“按日期拆分”,比如按月导出成orders_202401.xlsxorders_202402.xlsx,这种在业务上最常用,因为很多表都有created_at字段;第三种是“按主键范围拆分”,用WHERE id BETWEEN ? AND ?分段拉数据,适合没有时间字段的表。

另外还有几个进阶技巧。如果你有多个独立表要导出,可以用线程池并发处理,每个线程导一个表,能显著缩短总时长。但要注意数据库连接不能共用,每个线程要独立创建连接。业务量大的库,我建议给这些导出脚本设置一个“运行时间窗口”,比如业务低谷期跑批,避免影响线上业务。

5. 常见问题与排查技巧实录

5.1 身份证号变成科学计数法

这是导出 Excel 时遇到率最高的问题,没有之一。现象就是 Excel 里身份证号显示成4.10221E+17,点开看后几位全变成了 0。

根因是 Excel 对超过 15 位的数字默认用科学计数法存储,而身份证号是 18 位。解决办法我在前面已经提到过:pandas 读取时先把该字段astype(str),写入时用 xlsxwriter 把对应列设置为文本格式。

还有一条更保险的路:读取时就把该列强制转成字符串,甚至可以在 SQL 里用CAST(id_card AS CHAR),让数据源头就是字符串,后面不管怎么处理都不会变科学计数法。我一般两个方法叠加用,双保险。

5.2 中文乱码与 CSV 编码问题

如果你导出的是 Excel,大部分情况下中文乱码问题不大,因为 xlsxwriter 内部用 Unicode 存储,不乱码。真正容易出问题的是导出 CSV 的时候,用默认utf-8编码写的 CSV,用 Excel 打开会乱码。

解决办法是在 pandas 的to_csv里指定编码为utf-8-sig。这个编码会在文件开头加一个 BOM 头,Excel 识别后就能正确显示中文:

df.to_csv("data.csv", index=False, encoding="utf-8-sig")

如果你更喜欢用 Excel 原生格式,那就绕开 CSV 直接导 xlsx,少一层编码烦恼。

5.3 日期字段变成一串数字

很多人在 Excel 里看到日期字段显示成45000这类数字,第一反应是数据坏了,其实是 Excel 的日期序列号。pandas 导出的 datetime 类型一般能正确识别为日期,但如果你从数据库读出来的日期是字符串,写入 Excel 后可能被当普通文本,或者被你手动转换后变成序列号。

我建议在导出前统一处理日期格式。pandas 里可以这样:

df["created_at"] = pd.to_datetime(df["created_at"]).dt.strftime("%Y-%m-%d %H:%M:%S")

这样统一转成字符串格式,Excel 里显示的是2024-01-15 14:30:22这样的可读文本,用户不需要额外设置单元格格式。这属于“牺牲一点 Excel 日期计算能力,换可读性”的取舍,在交付场景里我更推荐。

5.4 SQL 超时、连接中断、数据错位

批量导出任务跑的时间长了,MySQL 端经常会主动断开空闲连接或者超时连接,报MySQL server has gone away

排查思路我建议按三步走:第一步检查连接参数里有没有设置connect_timeoutread_timeoutwrite_timeout,没有就补上;第二步检查 SQLAlchemy 引擎有没有pool_pre_ping=Truepool_recycle,没有就加上;第三步看是不是查询本身太慢,可以在 SQL 里加EXPLAIN分析有没有走索引。

还有一种数据错位的情况值得特别注意:如果你用 pandas 读表,又手动改列名,或者在 DataFrame 里增删列,然后直接to_excel,很可能会出现“数据对不上表头”的情况。原因大多是索引对齐问题。我的习惯是读出来之后先df.columns看一眼,确认列顺序;动过列之后,写文件前再确认一次最终列名列表和实际数据对得上。

最后再分享一个小习惯

做了这么多年数据导出相关的脚本,我最大的体会是:别把“导出”当成一次性交付,而是当成一个需要长期维护的小工具。我接到的需求基本都是多轮迭代的,第一版只有一个导出按钮,第二版可能要加一个时间范围筛选,第三版可能要求导出 Excel 的同时还要发一封带附件的邮件。

所以我在写脚本时会留两个口子:一个是表清单和字段配置用 dict 或 yaml 维护,不写死在业务逻辑里;另一个是每个导出步骤都尽量输出日志,谁导的、导了哪张表、多少行、花了多久,都留下 trace。这样即使出了线上问题,也能快速定位是数据问题还是脚本问题。

还有一个小技巧是导出完自动校验行数。我从数据库查一遍COUNT(*),和写入 Excel 的行数比对,不一致就告警。这个操作成本很低,但能拦住不少因为查询条件写错或者分块遗漏导致的数据缺失问题。批量导数据这件事,安全永远比速度重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询