上周业务方的同事跑来找我说能不能把库里近一年的订单明细导成 Excel 他们要自己拉透视表。我第一反应是这还不简单pandas.read_sql接to_excel几行代码就收工。等真拿到生产环境的数据量一看一张表几千万行直接全量读取内存根本扛不住Excel 单 Sheet 的行数上限也摆在那根本不是一个to_excel能解决的问题。所以专门写了这个 Python 批量导出工具把数据库里的数据按批次、按分表、按业务维度拆成多个 Excel 文件或者在一个文件里分多个 Sheet 导出整个过程带进度、带日志、带断点续传。这篇文章就把我的完整实现思路、代码、还有踩过的坑一次说清楚给要做报表导出、数据分发、运营取数的朋友一个可以直接落地的参考。1. 为什么简单的 Excel 导出落地时却最容易翻车先说结论单机、小数据量、一次性脚本用 pandas 一把梭确实没问题。但数据量一旦上来或者任务变成每天定时跑问题就一串串往外冒。1.1 数据量和 Excel 格式本身的硬限制Excel 的.xlsx格式有个让人头疼的单 Sheet 上限1048576 行。很多业务表动辄几百万行直接全量导出必然失败。就算没到上限超过 20 万行的 Excel 文件打开和刷新都会变得很卡业务方的电脑稍微旧一点打开一次要等半分钟。另外还有一个大家容易忽略的限制单元格内文本不能超过 32767 个字符。数据库里如果有大字段、备注、JSON 配置之类的长文本超长部分会被 Excel 静默截断业务方拿到手才发现数据不对这时候再排查就尴尬了。所以在导出逻辑里必须预留字段长度检测和截断策略。再加上 Excel 对日期时间格式、超长数字比如雪花 ID、手机号的自动科学计数法问题整个导出任务要考虑的细节一点不比业务功能少。1.2 常见方案对比为什么我选了 SQLAlchemy pandas xlsxwriter写这个工具之前我对比了三种主流方案方案优点缺点适用场景pandas.read_sqlDataFrame.to_excel代码短上手快全量加载内存大表直接 OOM无法精细控制 Sheet、列宽、格式小数据量一次性导出openpyxl直接操作单元格可精确控制格式和样式逐单元格写入千万级数据写入速度慢到怀疑人生报表美化、模板填充SQLAlchemy流式查询 xlsxwriter写入内存可控、写入快、支持格式控制代码相对多需要自己管理批次和类型大数据量批量导出生产环境我最终选择的是第三种的组合但把查询层包了一层pandas而不是完全手动fetchmany。原因是pandas的分块读取、类型推断、空值处理已经足够成熟没必要重复造轮子真正需要自己控制的是分块大小写入方式文件切分策略这些工程层面的东西。1.3 批量导出到底在批量什么这个标题里的批量其实包含三个维度批量导出多张表比如一个库里有订单表、用户表、商品表一次任务全部导出成独立文件或者合成一个多 Sheet 的 Excel。单张表数据量过大批量分块导出把 500 万行拆成 5 个文件每个 100 万行避免触发 Excel 上限。增量任务每天跑只导出当天新增或变化的数据而不是每天全量扫全表。这三个维度在代码设计上是互相叠加的下面第二、三章会分别展开。2. 核心实现从数据库读取到 Excel 落盘的完整代码这一章直接给完整的可运行代码并解释每一处关键设计的原因。我会用虚拟的表结构order_records做例子表里有id、trade_date、user_id、product_name、remark等字段覆盖了常规数字、日期、文本、长文本几种典型类型。2.1 数据库连接用 SQLAlchemy 统一管理第一步是建立数据库连接。我习惯用 SQLAlchemy 的create_engine而不是直接用某个数据库驱动裸连原因有两个换数据库不用改代码MySQL、PostgreSQL、SQLite 都能用同一套导出代码。pandas.read_sql原生支持 SQLAlchemy 引擎参数占位符也更安全。from sqlalchemy import create_engine def build_engine(db_type: str mysql, host: str 127.0.0.1, port: int 3306, username: str your_user, password: str your_pass, database: str your_db) - object: 构建 SQLAlchemy 数据库引擎。 如果 db_type 是 sqlite只需传 database 路径即可。 if db_type sqlite: return create_engine(fsqlite:///{database}, pool_pre_pingTrue) url f{db_type}://{username}:{password}{host}:{port}/{database}?charsetutf8mb4 return create_engine(url, pool_pre_pingTrue, pool_size5, pool_recycle3600)注意charsetutf8mb4这个对 MySQL 很重要。utf8mb4才能完整支持 emoji 和生僻字如果漏掉数据里带个 emoji 符号写到 Excel 里就会变成乱码。这是我在真实项目中踩过的坑。pool_pre_pingTrue这个参数的意思是每次从连接池取连接之前先做一次轻量探测防止拿到一个已经失效的连接。定时任务里数据库连接经常被服务端主动断开加上这个参数能省掉很多莫名其妙的MySQL server has gone away报错。2.2 主函数按配置批量导出多张表到同一个 Excel这个设计的核心思路是把导出什么表导出哪些列按什么条件过滤都放到配置字典里代码本身保持通用。这样业务上新增一张表的导出需求只需要加一段配置不需要改逻辑。import pandas as pd from sqlalchemy import text def export_tables_to_excel(engine, tables_config: list, output_path: str, chunksize: int 50000, sheet_limit: int 500000): tables_config: [ { sheet_name: 订单表, query: SELECT id, trade_date, user_id, product_name, remark FROM order_records WHERE trade_date :start_date, params: {start_date: 2024-01-01}, order_by: id, }, ... ] writer pd.ExcelWriter(output_path, enginexlsxwriter) for cfg in tables_config: sheet_name cfg[sheet_name][:31] # Excel sheet 名最长 31 字符 query cfg[query] params cfg.get(params, {}) order_by cfg.get(order_by, ) # 构造带分页包裹的查询后面解释为什么这么做 paged_query wrap_query_with_row_number(query, order_by) total_exported 0 sheet_row 0 # 当前 sheet 已写入行数 # 把分页条件逐段传入 offset 0 while True: page_query paged_query f LIMIT {chunksize} OFFSET {offset} df pd.read_sql(text(page_query), engine, paramsparams) if df.empty: break # 达到单个 sheet 的行数上限换下一个 sheet if sheet_row len(df) sheet_limit: # 先处理不满一个 sheet 的剩余部分... pass df.to_excel(writer, sheet_namesheet_name, indexFalse, startrowsheet_row 1, header(sheet_row 0)) sheet_row len(df) total_exported len(df) offset chunksize print(f[{sheet_name}] 已导出 {total_exported} 行偏移 {offset}) writer.close() print(f导出完成{output_path})这段代码里wrap_query_with_row_number是我封装的一个分页函数下面单独说明。先解释几个关键点indexFalse必须写DataFrame自带的行索引如果不关掉会以多出来一列的方式写进 Excel业务方会看到一列没名字的序号非常不专业。header(sheet_row 0)控制列名只写一次分块导出到同一个 sheet 时只有第一块数据需要写列名后面续写只需要追加数据行。这个判断非常关键不写的话每个分块都会重复一行字段名。startrowsheet_row 1跳过列名行第一块从第 1 行开始写0 是列名行后续块从已有数据行之后续写。2.3 稳定分页用主键排序代替 OFFSETLIMIT ... OFFSET ...在数据量小的时候没问题但偏移量越大数据库扫描越慢。更严重的是如果trade_date有重复值而分页排序不稳定同一批数据可能被重复导出或漏掉。我在这里用了基于排序键的键集分页稳定且性能好def stable_page_query(base_query: str, order_column: str, last_value, page_size: int): if last_value is None: return f{base_query} ORDER BY {order_column} LIMIT {page_size} return f{base_query} AND {order_column} :last_value ORDER BY {order_column} LIMIT {page_size}每次记录当前页最后一条order_column的值下一页用WHERE order_column last_value取数据。这样即使数据在导出过程中被并发写入也不会出现 OFFSET 分页那种页与页之间数据错位的问题。当然前提是order_column有唯一性或者至少业务上能接受排序键相同的数据被一起跨越的边缘情况。实际操作中我会要求配置里必须带一个order_by字段并且优先用主键id作为排序键这是最稳妥的方案。2.4 数据类型预处理防止写进 Excel 就变味儿数据库里的数据类型跟 Excel 单元格格式不是一一对应的下面这几个处理必须在写入前完成def prepare_dataframe(df: pd.DataFrame, datetime_cols: list None, long_text_cols: list None) - pd.DataFrame: datetime_cols: 需要显式保留日期时间的列名列表 long_text_cols: 可能超过 32767 字符的长文本列 if datetime_cols: for col in datetime_cols: if col in df.columns: df[col] pd.to_datetime(df[col]).dt.strftime(%Y-%m-%d %H:%M:%S) if long_text_cols: for col in long_text_cols: if col in df.columns: df[col] df[col].astype(str).str.slice(0, 32000) # 把 id 这类超长数字列显式转成字符串避免科学计数法 for col in df.select_dtypes(include[int64, float64]).columns: if col.lower().endswith((id, no, num)): df[col] df[col].astype(str) return df这里有三件事值得展开日期转字符串pandas 写 datetime 到 Excel 时如果 xlsxwriter 的格式化没配对单元格显示的是一串数字Excel 日期本质是序列号。我选择直接在 SQL 查询或 pandas 里把日期格式化成标准字符串一劳永逸。超长文本截断截断到 32000 而不是 32767留出一点余量避免多字节字符在边界被截断产生乱码。长数字转字符串这招防的是手机号、雪花 ID 这类超过 15 位的纯数字如果不转字符串它们在 Excel 里会变成科学计数法看起来像4.10217E18精度还丢了。2.5 完整调用示例把上面的函数串起来实际使用长这样if __name__ __main__: engine build_engine(databasebusiness_db) tables [ { sheet_name: 订单明细, query: SELECT id, trade_date, user_id, product_name, remark FROM order_records WHERE trade_date :start_date , params: {start_date: 2024-01-01}, order_by: id, }, { sheet_name: 用户信息, query: SELECT id, user_name, register_date, level FROM user_info, params: {}, order_by: id, }, ] export_tables_to_excel( engine, tables_configtables, output_path/data/export/business_2024.xlsx, chunksize50000, sheet_limit400000, )这里我把sheet_limit设成 40 万而不是 Excel 上限 104 万是有意的。超过 40 万行的 sheetExcel 打开已经明显卡顿与其交给业务方一个能开但很慢的大文件不如在导出时就按更保守的阈值切分。3. 数据量特别大时的优化分块、并发与内存控制当单表数据达到千万级别前面那个单进程、串行分块的方案还是不够快。这一章说三个优化手段按性价比排序。3.1 内存估算为什么不能一次性全量读取很多人写导出脚本的第一版都是pd.read_sql(query, engine)一把梭几百万行数据全读进内存。我们来粗算一下一行订单记录假设有 20 个字段平均每个字段算 20 字节500 万行就是500万 × 20 × 20 2亿字节约 200 MB。看着还好但 pandas 为了类型推断会临时复制多份副本实际峰值可能是这个数的 3 到 4 倍。再加上 DataFrame 索引、字符串对象本身需要的内存一个 500 万行的 DataFrame 吃 1 GB 内存很正常。如果服务器上还跑着其他服务这基本就是 OOM 的前奏了。所以我坚持在任何生产导出脚本里都做分块读取既为了控制内存也为了能在每块之间打印进度。3.2 分块大小怎么定分块大小chunksize是个需要平衡的参数。我自己的经验公式行平均宽度小比如只有几个整数列可以设 10 万到 20 万。行平均宽度大有长文本、JSON、多字符串列建议设 2 万到 5 万。宁可块数多也不要让单块内存超过 200 MB。实际压测时我会在分块读取的循环里监控一下内存变化import tracemalloc tracemalloc.start() # 分块循环读取... current, peak tracemalloc.get_traced_memory() print(f当前内存: {current / 1024 / 1024:.1f} MB, 峰值: {peak / 1024 / 1024:.1f} MB)这里的核心原则是单块读完了、写完了内存就该释放。如果发现内存只增不减优先检查是不是有人把每个分块都append到了一个总 DataFrame 里——这是最常见的隐性内存泄漏。3.3 多进程并行不同表并行单表按 ID 范围切分如果你的数据库服务器 CPU 核数多可以并行导出多张独立的表。Python 的多线程在 IO 密集型任务上有用但这里的主要瓶颈是数据库查询后的 pandas 内存处理属于 CPU 和内存密集型所以用multiprocessing更合适。多进程最简单的写法是concurrent.futures.ProcessPoolExecutorfrom concurrent.futures import ProcessPoolExecutor, as_completed def export_one_table(cfg: dict): 导出单张表独立进程执行 engine build_engine() # 每个进程单独建连接 return export_tables_to_excel( engine, [cfg], cfg[output_path], chunksizecfg.get(chunksize, 50000), sheet_limitcfg.get(sheet_limit, 400000), ) def parallel_export(all_configs: list): with ProcessPoolExecutor(max_workers4) as pool: futures [pool.submit(export_one_table, cfg) for cfg in all_configs] for future in as_completed(futures): print(future.result())注意每个子进程必须自己创建数据库连接和ExcelWriter不能把父进程的 engine 直接传进去。数据库连接不能跨进程共享这是很多人踩过坑的地方。如果是单张表特别大多进程思路要反过来按主键id的范围切成 N 段每个进程导出[id_min, id_max)区间。切分方式先查一下表的最小和最大主键SELECT MIN(id), MAX(id), COUNT(*) FROM order_records;然后根据总数和期望的每段行数算出切割点再按切割点生成各自的WHERE id BETWEEN ... AND ...查询。这个方案的好处是天然支持断点续传——某个区间跑挂了重跑那个区间即可。3.4 写入引擎的选择xlsxwriter 的 write_only 模式pandas.DataFrame.to_excel支持传入不同的 engineopenpyxl和xlsxwriter对同一份数据的写入性能差距明显。xlsxwriter有一个 write-only 模式专门为大数据量设计workbook xlsxwriter.Workbook(output_path, {constant_memory: True}) worksheet workbook.add_worksheet(订单明细)开启这个模式后xlsxwriter 不会在内存里维护完整的行数据而是边写边落盘内存占用大幅下降。唯一的不方便是不能回改已经写过的单元格——但导出任务根本不需要回改所以完全适配。在pandas.ExcelWriter里启用方式也简单writer pd.ExcelWriter(output_path, enginexlsxwriter, engine_kwargs{options: {constant_memory: True}})我之前做过一次对比测试同样是 80 万行、15 列的数据openpyxl写入耗时接近 3 分钟xlsxwriter的 constant_memory 模式只要 40 秒左右。这个差距在生产任务里非常可观。4. 我把这些 Excel 发给业务方后踩过的那些坑代码能跑通只是第一步。真实世界里能把文件生成出来和业务方能正常使用之间隔着好几个大坑。这一章记录我实际交付过程中踩过的坑和对应的处理办法。4.1 单 Sheet 超过 104 万行不是报错但比报错更麻烦有一次我导出一张日志表数据量 130 多万行我满怀信心地等着文件生成结果确实生成了但 Excel 打开时报文件损坏原因是超过了单 Sheet 上限。这个坑最坑的地方在于文件层面已经写坏了但导出脚本没有任何异常提示。xlsxwriter 会把写到 1048577 行之后的逻辑线强行掐掉但文件打开前你永远不知道。我的处理办法是在写入循环里做一个硬检查MAX_EXCEL_ROWS 1048576 if sheet_row len(df) MAX_EXCEL_ROWS: # 当前 sheet 已满另起一个 sheet 或另起文件 ...更稳妥的做法是在方案设计阶段就按业务数据的增长趋势预估未来的行数提前把一个大表的数据按时间维度分文件导出。比如日志表就按天导出一天一个文件既避免行数问题业务方拿数据也更方便。4.2 数字被显示成科学计数法的三个来源这个问题太常见了常见到每个做导出的人都会遇到。超长 ID19 位的雪花 IDExcel 显示成4.10217E18而且后几位被当成 0原始精度丢失。手机号11 位数字理论上不超过 Excel 的 15 位精度限制但很多手机号第 11 位附近会出现奇怪的浮点误差导出后末尾变成12300这种。带前导零的编码比如商品编码001234Excel 会自作主张把前导零省略。解决思路分两层第一层在 SQL 查询阶段就转成字符串。比如 MySQL 用CAST(id AS CHAR)PostgreSQL 用id::text让数据库返回字符串而不是数字pandas 拿到手就是字符串再写入 Excel 就不会触发科学计数法。第二层如果数据已经以数字类型到了 pandas 里就用df[col] df[col].astype(str)转一次。注意此时如果数字已经是浮点型直接转字符串会带.0后缀要先转int64再转str或者用小数点后 0 位格式化。4.3 日期显示成数字或 Unix 时间戳pandas 写 datetime 到 xlsxwriter 时如果不设置单元格格式日期会显示成45087.3456这种序列数。我一开始没做特殊处理业务方拿到文件后发给我一堆问号这个数字是什么后来我总结出一个最省心的方案所有日期列在写入前统一格式化成YYYY-MM-DD HH:MM:SS字符串。df[trade_date] pd.to_datetime(df[trade_date]).dt.strftime(%Y-%m-%d %H:%M:%S)字符串日期写入 Excel 后是左对齐的文本格式虽然不能直接在 Excel 里做日期运算但对于导出给业务方看这个场景完全够用。如果有分析师需要日期参与计算他们自己用分列功能转一下批量也很方便。4.4 乱码问题谁都可能遇到但原因各不同乱码的排查路径我这几年走得很熟练了按位置分成三种数据库读出来就是乱码大概率是连接串没加charsetutf8mb4或者表字段本身的字符集不对。pandas 阶段乱码检查拿到的 DataFrame 里打印出来的值是正常的但写出去是乱码多半是 ExcelWriter 的编码设置被动了或者用了 CSV 输出时没指定utf-8-sig。Excel 打开乱码但代码输出正常这个最常见就是 CSV 文件没有 BOM 头。Excel 默认用 GBK 编码读 CSV没有 BOM 的 UTF-8 文件它识别不出来。CSV 导出时一定要用df.to_csv(path, indexFalse, encodingutf-8-sig)utf-8-sig会自动带上 BOMExcel 就能正确识别。4.5 一个慢得无法忍受的文件性能瓶颈可能在单元格格式我遇到过这么一次数据量不大才 30 万行但导出耗时非常长一看代码循环里对每个单元格调用了worksheet.write_number()后面还跟了一堆set_format()。naive 写法的性能杀手是逐单元格操作 格式对象绑定。优化办法是能用pandas的批量写入就不要自己循环写单元格。如果要设置列宽、表头样式只对列名行做格式化数据行保持默认。表头样式可以用worksheet.set_row(0, None, header_format)这种批量方式而不是每个单元格单独设置。这个优化做完同样的 30 万行数据耗时从 4 分多钟降到了 20 秒以内效果立竿见影。5. 让导出任务变成可运维的定时任务断点续传与失败重试一次性的手工导出很简单真正的难点在于把这个脚本变成每天自动跑、挂了能恢复、跑完有日志的生产级定时任务。这一章是我在交付给其他团队后根据使用反馈迭代出的工程化改进。5.1 断点续传以已导出的最大主键为锚点最朴素的断点续传方案是记录上一次导出的最大主键值。下次任务启动时SQL 条件从id last_max_id开始查而不是从全表开头查。def get_last_checkpoint(state_file: str) - int: try: with open(state_file, r) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return 0 def save_checkpoint(state_file: str, last_id: int): with open(state_file, w) as f: f.write(str(last_id))这个方案有几个前提表必须有单调递增的主键或者至少有一个递增的业务时间字段。每次任务只追加新数据不修改历史数据。增量导出的数据范围是连续的区间不存在越过空白段后还有新数据的情况。如果表结构不满足这些前提断点续传的意义就大打折扣这种情况建议改为按时间窗口全量重导或者把增量标志位交给业务系统维护。5.2 失败重试指数退避而不是立即硬冲数据库连接抖动、锁竞争、网络闪断这些都是定时任务最常遇到的故障类型。如果失败后立即重试很可能在同一瞬间再次失败。我习惯用带指数退避的重试逻辑import time def retry_call(func, retries3, base_delay2): for attempt in range(retries): try: return func() except Exception as exc: if attempt retries - 1: raise delay base_delay * (2 ** attempt) print(f第 {attempt 1} 次失败{delay}s 后重试: {exc}) time.sleep(delay)每次失败的间隔按 2 秒、4 秒、8 秒指数增长给数据库留出恢复窗口。同时记录最后失败位置让下一次任务可以从断点继续。5.3 日志是导出任务的灵魂我接手过别人的导出脚本代码结尾只有一行print(done)。出问题时完全不知道数据导到哪一步了、出了多少行、卡在哪个表上。我的日志规范很简单每个表开始导出前记录表名、预计数据量、开始时间。每个分块导出后记录已导出行数、耗时、当前主键边界。任务结束或异常退出统一写一个 summary每个表导出了多少行、总耗时、是否有跳过和重试。import logging import time logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[logging.FileHandler(/data/export/export.log)] ) logger logging.getLogger(exporter) def export_with_log(engine, cfg): start time.time() total 0 logger.info(f开始导出表 {cfg[sheet_name]}) # ... 分块导出循环 ... logger.info(f表 {cfg[sheet_name]} 完成共 {total} 行耗时 {time.time() - start:.2f}s)有了这个日志任何一次任务异常都能快速定位是数据库查询太慢、某一行的数据格式有问题、还是写 Excel 时文件被占用。日志不求花哨但必须能让接手的同事在 5 分钟内定位到问题。5.4 定时调度的落地姿势我用 cron 表达式做调度核心命令很简单0 2 * * * /usr/bin/python3 /opt/scripts/exporter.py /data/export/cron.log 21凌晨两点跑业务低谷数据库压力小文件生成后也不影响上班时间取数。这里有个细节不要直接用python exporter.py而是写一个启动脚本先激活虚拟环境再跑。项目依赖如果装在全局环境某天其他人动了依赖版本整个定时任务就无声无息地挂了。我之前为此定了一条规矩任何导出任务的启动命令都固定为cd /opt/scripts source .venv/bin/activate python exporter.pycron 里写全路径不要依赖默认环境的任何东西。定时任务跑成功后还可以加一个可选的发送通知环节把导出的文件上传到公司的文件平台或对象存储然后把下载链接发给业务群。这一步看团队基础设施有空实现一下价值很大因为业务方不用每次找运维或者开发要文件了。6. 一些值得再抠一抠的细节格式美化与文件命名导出任务做到这里已经是可用的生产工具了但要让业务方用得舒服还有几个小细节值得花时间优化。6.1 列宽自适应别让业务方每次手动拉列宽xlsxwriter 里可以给 Sheet 设置默认列宽或者针对特定列设置更宽的值worksheet.set_column(A:A, 12) # ID 列 worksheet.set_column(B:B, 20) # 日期列 worksheet.set_column(C:C, 30) # 名称列如果列很多不想一个个指定可以按列名的最大字符长度估算for i, col in enumerate(df.columns): max_len max(df[col].astype(str).str.len().max(), len(col)) 2 worksheet.set_column(i, i, min(max_len, 50))这个操作对使用者体验提升非常明显值得做。6.2 表头加粗与冻结首行冻结首行这个功能几乎是业务方最喜欢的一个。表数据一多往下拉的时候完全不知道每一列是什么冻结首行后这个痛点直接消失。header_format workbook.add_format({bold: True, bg_color: #D9E1F2, border: 1}) worksheet.set_row(0, None, header_format) worksheet.freeze_panes(1, 0)一次设置永久生效整个文件的专业度立刻上一个档次。6.3 文件命名规则时间戳 业务标识文件命名看起来小但影响的是整个交付链路。我经常看到有人导出文件叫export.xlsx结果第二天再导出就把昨天的文件覆盖了或者业务方同时拿到 5 个export.xlsx根本分不清哪个是哪个。我的命名规范是业务日期_数据范围_生成时间戳.xlsx比如order_records_20240101_20240630_20240701020000.xlsx保证文件名里能看出数据的时间范围、生成的时间点且不会互相覆盖。如果是定时增量任务这个习惯能帮你少背很多锅。6.4 空数据表的处理宁可给空模板也别少一个 Sheet业务方用 Excel 最怕的情况是今天导出数据恰好是 0 行脚本直接跳过了这个 sheet业务方以为程序出 bug 了实际上只是没数据。我的处理逻辑是如果查询结果为空也创建 Sheet只写入列名和一行无数据说明。这样业务方一眼就明白这个时间段确实没有数据而不是工具坏了。if df.empty: empty_df pd.DataFrame(columnscfg[columns]) empty_df.to_excel(writer, sheet_namesheet_name, indexFalse)小小的一个坚持省掉了很多不必要的需求沟通。7. 从能跑到好用的个人经验总结最后说说我做完这个导出工具后的几点体会。导出这个功能看起来是人人都能做的最小功能但真正能稳定服务业务的导出工具一定不是pd.read_sql to_excel几行代码就算完成的。数据量边界、格式兼容性、异常恢复、可观测性、使用体验缺一不可。尤其是在数据量大、任务频繁的场景下一个没有分块、没有日志、没有断点的导出脚本迟早会变成半夜的告警电话来源。我自己的习惯是任何导出任务都先问三个问题——数据量级是多少多久跑一次谁在用这个文件数据量决定要不要分块频率决定要不要断点续传和定时调度使用者的技术水平决定我要不要在格式上多花心思。这三个问题问清楚了方案就自然浮出水面。如果看完这篇你还是只想抄一个最小可用的版本那至少记得导出前先做数据类型预处理导出时分块控制内存导出后确认 sheet 行数没超上限。这三件事做到位你的导出脚本就能超过大多数临时脚本成为别人愿意持续使用的工具。
阅读完成 · 觉得有帮助?