做数据导出这件事,很多人一开始都觉得“简单”,不就是查一下表然后另存为 Excel 嘛。但真当你开始搞“批量导出数据库数据至 Excel 文件”的时候,会发现麻烦点全在那些没写在需求文档里的地方:表多到不想一个个点、数据量大到客户端卡死、日期格式导出来变成数字、发给业务那边又说打不开。我去年在做数据分析平台的时候就折腾过一轮这个东西,后来沉淀了一套 Python 批量导出的方案,从环境搭建到性能优化都走了一遍。这篇文章就把整个过程拆开讲,包含可直接复制的代码、参数说明和踩坑记录,给正在被导数据折磨的同学一个参考。
1. 批量导出到底在解决什么问题:场景拆解与方案选型
1.1 真实业务里的“批量导出”长什么样
你搜这个标题,大概率是遇到了下面这些场景之一:财务月初要对账,需要从业务库里导过去一个月的所有流水;运营要做用户行为分析,一口气要了 30 张表的明细数据;或者你正在做数据迁移,想把 A 库所有表完整备份成 Excel 交给下游团队。这些场景共同的特点是:不是导一张表,而是导一批表,而且往往要重复执行。
一开始我用的方案是 Navicat 里一个个表点导出,单张表确实挺快,但一旦表多,这个操作就变成了纯体力活。更要命的是,业务方过两周还会说“再导一次上周的”,你可能又得重新手点一遍。这让我意识到:导出 Excel 这个动作本身不难,难的是怎么把它变成一条命令就能跑完的事。批量二字,核心在于把重复劳动变成自动化操作。
1.2 为什么选 Python 而不是现成工具
市面上能导出数据的工具并不少,但我最后选择 Python 自建脚本,是做过对比的:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Navicat / DBeaver 导出向导 | 可视化操作,单表导出方便 | 多表批量要手动点,难以定时执行 |
| 数据库自带导入导出工具 | 适合大规模备份恢复 | 格式偏 SQL/CSV,Excel 定制能力弱 |
| Python + pandas + SQLAlchemy | 灵活批处理、定时执行、格式可控 | 需要写代码,有学习成本 |
Python 这条路的优势体现在三个地方:第一,可以遍历所有表名循环处理,天然适合“批量”;第二,pandas 的to_excel一行就能写出格式规范的 Excel,还能控制 sheet、列序、索引等细节;第三,封装成命令行脚本之后,丢到定时任务里就能每天自动生成报表。对于经常和数据打交道的人,这几条足够说服你投入半小时写个脚本。
2. 环境准备:先把依赖装对,后面省一半时间
2.1 Python 环境与虚拟环境隔离
做这类脚本,我不建议直接往系统 Python 里装一堆包,万一跟别的项目依赖冲突了,够你折腾半天。推荐用虚拟环境隔离,Python 3.8 以上都支持自带的venv。我自己习惯在项目根目录先建一个目录,然后创建虚拟环境再激活:
mkdir excel_exporter && cd excel_exporter python -m venv venv # Windows 下激活 venv\Scripts\activate # macOS/Linux 下激活 source venv/bin/activate这步做完,后续安装的所有依赖都只存在于这个项目的虚拟环境里,换机器或者删项目不会搞乱全局环境。如果你用的是 Anaconda,那conda create -n exporter python=3.9也是一样的效果,按个人习惯来。
提示:尽量别直接用系统自带的 Python 跑,尤其是 macOS 自带的 Python 版本往往比较旧,pandas 新版本对 Python 版本有硬性要求,装包时容易报
Failed to build之类的错。
2.2 核心依赖库安装与选型
整个方案依赖四个核心库:pandas、SQLAlchemy、数据库驱动、Excel 写入引擎。安装命令如下:
pip install pandas sqlalchemy pymysql openpyxl- pandas:负责查询结果和 Excel 文件之间的数据中转,是整条链路的枢纽。
- SQLAlchemy:统一不同数据库的连接方式,后面换库只需要改连接字符串。
- pymysql:MySQL 的纯 Python 驱动,安装最省事。如果连的是 PostgreSQL,就换成
psycopg2-binary;连 SQL Server 用pymssql;连 Oracle 用cx_Oracle。 - openpyxl:写 Excel 的引擎。新版 pandas 写
.xlsx默认就依赖它。
这里特别提一下 openpyxl 和另一个引擎 xlsxwriter 的选择。openpyxl 能处理 Excel 里的公式、已有模板文件,但写入速度较慢;xlsxwriter 写入速度快,还不依赖模板文件,适合从零生成新 Excel。如果追求性能,可以额外装一个 xlsxwriter,然后在to_excel里指定engine='xlsxwriter'。不过我们项目里因为需要边读边写分块数据,openpyxl 的 write_only 模式更合适,所以主用 openpyxl。
2.3 国内安装依赖的加速技巧
如果 pip 下载速度很慢,可以临时指定国内镜像源,实测速度快很多:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas sqlalchemy pymysql openpyxl也可以在用户目录下配置pip.conf永久换源,这些基础就不展开说了。总之环境准备好,后面写代码才不会被“装不上包”打断思路。
3. 核心代码实现:从数据库到 Excel 的完整链路
3.1 先用 SQLAlchemy 把数据库连接打通
连接数据库是第一步。为什么要用 SQLAlchemy 而不是直接pymysql.connect()?因为 SQLAlchemy 提供了连接池管理,还屏蔽了不同数据库的差异,以后迁移到别的数据库只需要改一个 URL。连接串的格式是:
数据库类型+驱动://用户名:密码@主机地址:端口号/数据库名?charset=utf8下面这段代码做了一个通用的连接函数,输入数据库类型和认证信息,返回一个 SQLAlchemy engine:
from sqlalchemy import create_engine def create_db_engine(db_type, host, port, user, password, database): if db_type == "mysql": url = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset=utf8mb4" elif db_type == "postgresql": url = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}" elif db_type == "sqlserver": url = f"mssql+pymssql://{user}:{password}@{host}:{port}/{database}" else: raise ValueError(f"暂不支持的数据库类型: {db_type}") engine = create_engine(url, pool_size=5, pool_recycle=3600) return engine3.2 单表导出 Excel:三行代码就能搞定
连接建好后,单表导出其实非常简单。最核心的就是三行代码:读数据、建 Excel 写入器、写 sheet。
import pandas as pd df = pd.read_sql_query("SELECT * FROM orders", engine) df.to_excel("orders.xlsx", index=False, sheet_name="orders")index=False一定要加,不然 pandas 会把行号写进 Excel 第一列,导出去的数据会多出一列没用的数字,很多人第一次导数据都栽在这。sheet_name是指定 Excel 里工作表的名字。这是最基础的形态,实际项目里需求不会这么简单,所以我们接着往下看批量怎么处理。
3.3 批量导出的核心逻辑:遍历表 + 自动组织输出结构
批量导出有两种常见形态,一种是“多张表写进同一个 Excel 的不同 Sheet”,另一种是“每张表单独生成一个 Excel 文件”。两种我都实现过,分别说下写法。
先看第一种,多表写进同一个文件,适合数据交付场景,一个文件打包全部:
import pandas as pd from sqlalchemy import inspect def export_tables_to_one_excel(engine, output_path): inspector = inspect(engine) table_names = inspector.get_table_names() # 拿到所有表名 with pd.ExcelWriter(output_path, engine="openpyxl") as writer: for table in table_names: df = pd.read_sql_query(f"SELECT * FROM `{table}`", engine) # sheet 名称不能超过 31 个字符,且不能包含特殊字符,这里简单截断 sheet_name = table[:31] df.to_excel(writer, sheet_name=sheet_name, index=False) print(f"已导出表 {table},共 {len(df)} 行")再看第二种,每张表单独一个文件,适合按表归档:
import os def export_tables_to_separate_files(engine, output_dir): inspector = inspect(engine) table_names = inspector.get_table_names() os.makedirs(output_dir, exist_ok=True) for table in table_names: df = pd.read_sql_query(f"SELECT * FROM `{table}`", engine) df.to_excel(os.path.join(output_dir, f"{table}.xlsx"), index=False)这里有几个细节要注意:表名拼接 SQL 时,MySQL 要用反引号括住,防止表名是关键字或包含特殊字符;Excel 的 sheet 名长度限制是 31 个字符,如果表名很长需要截断,否则写入会报错。我自己就遇到过一张叫tmp_user_order_detail_log_20240101的表,长度已经接近 31,幸好加了截断逻辑。
3.4 按条件导出和自定义查询,别只会全量
实际业务里,几乎不会有人傻乎乎每次导全表,更多地是“导最近 7 天的数据”或者“导某个状态的数据”。所以脚本里要支持自定义 SQL。这里要特别强调参数化查询,防止把传进来的查询条件直接拼进 SQL 里。我们看个例子:
def export_custom_query(engine, sql, params, output_path, sheet_name="data"): df = pd.read_sql_query(sql, engine, params=params) df.to_excel(output_path, index=False, sheet_name=sheet_name)调用时是这样的:
sql = "SELECT * FROM orders WHERE order_date >= %(start_date)s AND status = %(status)s" params = {"start_date": "2024-06-01", "status": "paid"} export_custom_query(engine, sql, params, "paid_orders.xlsx")params用字典传参,SQLAlchemy 会帮你做转义,避免 SQL 注入风险。注意不同数据库占位符写法略有差别,MySQL 的 pymysql 用%s或者%(name)s,PostgreSQL 的 psycopg2 用%(name)s可以直接用。这个细节如果你在项目里踩过,应该能明白我说的是什么。
4. 大数据量导出的性能优化与内存控制
4.1 Excel 的硬性限制,需要先知道
很多人第一次导几十万行数据,直接read_sql_query读完再to_excel,结果内存爆了,或者 Excel 写到最后报错。这里有几个硬性限制必须心里有数:
- Excel 单个工作表最多 1,048,576 行,也就是约 104 万行;
- 最多 16,384 列;
- 单格字符上限 32,767。
如果数据超过 104 万行,直接写进一个 sheet 是写不进去的,需要拆分到多个 sheet 或者拆成多个文件。这是 Excel 本身的限制,不是代码能绕过去的。
4.2 为什么一次读全表会内存爆炸
pandas 的read_sql_query会把所有查询结果一次性加载到内存里。假设你的表有 100 万行、30 列,在内存里的占用轻松超过 1GB。如果服务器本身内存只有 2GB,Python 进程可能直接被系统 kill 掉。特别是当你通过SELECT *全量导出时,根本不知道表到底多大,很容易就翻车。
4.3 分块读取 + 增量写入的正确姿势
正确的做法是:分批读取数据库,每批写完就释放内存。pandas 的read_sql_query支持chunksize参数,可以分批返回一个迭代器。然后用 openpyxl 的 write_only 模式,边读边写,避免一次性把所有数据堆在内存里。
直接上完整代码:
import pandas as pd from sqlalchemy import create_engine def export_large_table(engine, table_name, output_path, chunksize=10000): sql = f"SELECT * FROM `{table_name}`" # 设置 write_only=True,openpyxl 会进入流式写入模式 with pd.ExcelWriter(output_path, engine="openpyxl", mode="w") as writer: first_chunk = True for chunk in pd.read_sql_query(sql, engine, chunksize=chunksize): # 第一个 chunk 创建 sheet,后续 chunk 追加写 chunk.to_excel( writer, sheet_name=table_name[:31], index=False, header=first_chunk, startrow=writer.sheets[table_name[:31]].max_row if not first_chunk else 0, ) first_chunk = False print(f"已写入 {len(chunk)} 行,累计 {writer.sheets[table_name[:31]].max_row} 行")这段代码的核心思路是:第一个批次创建 sheet 并写入表头,后续批次从当前最大行数开始继续往下追加。writer.sheets[sheet_name].max_row能拿到当前 sheet 已经写到第几行了,用这个作为下一批的起始行,就能保证数据连续追加。header参数只在第一个批次写表头,后面批次不重复写。
我自己在项目里用这套逻辑导出过单表 60 万行的数据,生成一个 80MB 左右的 Excel,内存峰值控制在 500MB 以内,比起一次性读取的 2GB 内存占用,优化效果非常明显。
4.4 写入速度与文件大小的实测参考
不同环境跑出来会有差异,我拿一台 4 核 8GB 内存的机器做测试,MySQL 数据库,单表 30 万行、20 列,数据量约 120MB,几个指标大概是这样:
| 项目 | 一次性读取+写入 | 分块读取+追加写入 |
|---|---|---|
| 峰值内存 | 约 1.2GB | 约 300MB |
| 总耗时 | 约 40 秒 | 约 55 秒 |
| 生成文件大小 | 约 40MB | 约 40MB |
分块方案牺牲了一点点时间,换来了内存占用的大幅下降。在数据量超过 100 万行的场景下,这个取舍是必须的,因为一次性方案可能直接让程序崩溃。
5. 常见问题与排查技巧实录
5.1 中文乱码、中文表名报错
我第一次导出的时候,遇到最典型的问题就是中文乱码和中文表名导出报错。乱码的根源通常在于数据库连接串里没有指定字符集,MySQL 必须在连接 URL 里加?charset=utf8mb4,否则 pymysql 默认可能用了 latin1,导出来全是一堆问号。另外,如果表名是中文,SQL 拼接时务必加上反引号,比如:
sql = f"SELECT * FROM `{table_name}`"5.2 连接被拒绝、认证失败
MySQL 8.0 之后默认使用caching_sha2_password认证插件,老版本的 pymysql 可能连不上,报Authentication plugin 'caching_sha2_password' cannot be loaded之类的错误。解决办法是升级 pymysql 到最新版本,或者把连接 URL 里加上charset参数并确保密码正确。还有一种情况是远程连接被防火墙挡住,可以在连接串里加上connect_timeout=10,等待几秒后报错退出,比默认长时间挂起更容易定位问题。
5.2 时间日期导出来变成数字或格式不对
这是 Excel 导出的经典坑。当数据库字段是DATETIME时,pandas 一般能正确识别为datetime64类型,写入 Excel 后显示正常。但如果你用的是 SQLite 或者其他不支持 datetime 的数据库,读出来可能是字符串,写入 Excel 后会被识别成文本。更隐蔽的问题是,有些驱动会把时间读成datetime.datetime对象,但to_excel时如果代码里做了astype(str),就变成了一串2024-06-01 12:00:00文本,业务侧没法做日期筛选。我的建议是保持 pandas 原生类型,不要手动转字符串,除非业务明确要求。
5.3 金额等 Decimal 类型精度丢失
数据库里用DECIMAL(10,2)存的金额,如果直接读进 pandas,可能会变成float64,比如9.99变成9.990000000000002,这在财务数据里是致命的。解决办法是读取 SQL 时就把 Decimal 字段转成字符串或整数分转,用CAST(amount AS CHAR)或者使用 pandas 的dtype指定:
df = pd.read_sql_query("SELECT id, CAST(amount AS CHAR) AS amount FROM orders", engine)另一种做法是读出后用df["amount"] = df["amount"].astype(str),也能保住精度,但要注意空值处理。
5.4 导出的 Excel 打开时提示文件损坏
这个问题的排查方向不是我一开始想的数据问题,而是写入过程的完整性。用 openpyxl 写文件时,如果程序在写入中途崩溃、被强杀、或者磁盘满了,生成的.xlsx文件就会损坏。另外,使用with块管理ExcelWriter很重要,with退出时会自动调用save()和close(),如果你手写了writer.save()却忘了writer.close(),也可能留下不完整的文件。所以代码务必用with pd.ExcelWriter() as writer这种写法。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 导出中文变问号 | 连接串没指定 charset | URL 加?charset=utf8mb4 |
| 中文表名报 SQL 语法错误 | 表名没加反引号 | 拼接 SQL 时用反引号包裹表名 |
| MySQL 认证失败 | 驱动版本过旧 | 升级 pymysql |
| 金额精度丢失 | DECIMAL 转 float | 用 CAST 转字符串或 astype(str) |
| Excel 打开损坏 | 写入未正常 close | 使用 with 管理 ExcelWriter |
| 内存爆掉 | 一次读取数据量太大 | 分块读取 + 增量写入 |
| 超过 104 万行写不进去 | 超过 Excel 限制 | 拆分为多个 sheet 或多个文件 |
6. 进阶扩展:从脚本变成日常自动化小工具
6.1 支持命令行参数,让脚本可以传参调用
单纯在代码里改连接信息肯定不方便,把脚本封装成命令行工具有两个好处:一是换库、换表、换输出目录不需要改代码;二是定时任务可以直接调用命令,比如每天凌晨跑一次把当天数据导出。用 Python 自带的 argparse 就能实现,不依赖额外库:
import argparse def parse_args(): parser = argparse.ArgumentParser(description="批量导出数据库数据至 Excel") parser.add_argument("--host", required=True, help="数据库主机地址") parser.add_argument("--port", required=True, help="数据库端口") parser.add_argument("--user", required=True, help="数据库用户") parser.add_argument("--password", required=True, help="数据库密码") parser.add_argument("--database", required=True, help="数据库名") parser.add_argument("--tables", nargs="*", help="要导出的表名列表,不传则导出全部") parser.add_argument("--output", required=True, help="输出目录或文件路径") return parser.parse_args() if __name__ == "__main__": args = parse_args() engine = create_db_engine("mysql", args.host, args.port, args.user, args.password, args.database) if args.tables: for table in args.tables: export_large_table(engine, table, f"{args.output}/{table}.xlsx") else: export_tables_to_separate_files(engine, args.output)命令行调用方式:
python exporter.py --host 127.0.0.1 --port 3306 --user root --password 123456 \ --database mydb --tables orders users products --output ./exports这样不管是手动执行还是交给定时任务,都只需要一条命令。
6.2 定时执行和结果分发的思路
脚本做好之后,我建议接一个定时触发。Windows 上用任务计划程序、Linux 上用 crontab,都是一行配置的事。比如每天凌晨 2 点导出前一天数据:
0 2 * * * cd /data/excel_exporter && ./venv/bin/python exporter.py --host 127.0.0.1 --port 3306 --user root --password 123456 --database mydb --tables daily_orders --output /data/exports >> /data/logs/export.log 2>&1导出完成后的文件分发,可以再写一小段发送邮件的代码用 smtplib 发附件,或者直接推给企业微信/钉钉机器人,看团队习惯。这些属于自动化链路的延伸,核心的导出逻辑其实已经在前面搞定了。
6.3 还能继续扩展的方向
如果你对 Excel 格式有更多要求,还可以做这些扩展:用xlsxwriter给报表加列宽、冻结首行、加筛选器;导多个 sheet 时给不同 sheet 设置不同主题色;或者在导出前用 pandas 做一次聚合统计,再生成“明细+汇总”两层结构。我自己后来就在脚本里加了列宽自适应和表头加粗,导出的报表不再需要业务手工调整格式,使用体验好了不少。
最后分享一点我的实际体会
这套脚本在我手头已经跑了大半年,最忙的时候每天要导出几十张表,单次最大数据量到过 60 万行,基本没出过岔子。整个过程走下来,我觉得最重要的是先想清楚“会不会经常跑、数据量大概多大、文件发给谁”这三个问题,再决定用最简单的全量导出还是带分块写入的完整版本。最后再分享一个小技巧:批量导出之前,先拿一张数据量几百行的表跑一遍完整流程,确认连接、格式、文件都能正常打开,再放全量表,这个习惯能帮你省掉非常多在凌晨三点被导出失败告警吵醒的麻烦。