简介:这是一款面向Windows用户的批量Excel转CSV小工具,解决需要将多个xls/xlsx文件快速转为csv格式以进行数据处理或归档的重复劳动问题。压缩包共包含33个文件,主要涉及C++工程源码(头文件、实现文件、工程配置)、示例Excel与csv文件以及可直接运行的ExcelToCsv.exe,整体体积仅94KB,轻量易用,适合需要快速转换又不想配置编译环境的普通办公人员,也适合对VC++工程感兴趣、想研究Excel解析逻辑的开发者。该工具基于CSDN上某版源码修改而来,原作者代码与Windows SDK存在Font类重定义问题,这里已更正并成功编译。解压后可直接在bin目录运行exe,作者已在Windows下实测可用;源码工程也为学习ExcelCOM接口调用、批量文件遍历等知识提供了完整参考。资源已有2682人学习下载,实用性和参考价值都较为明确。
1. 批量Excel转csv:为什么说“另存为”不是方案,而是一堆坑的开端
某个周五下午,某公司的运营同事把一个月的销售明细拆成了几十个Excel文件,让我帮忙导成csv供数据分析用。我第一个念头是“这有什么难的”,结果她补充了一句:“之前手工另存为导过一次,三十多个文件里有一半在Excel里打开是乱码,还有几个日期列变成了一串数字,数据串位……”这就是批量Excel转csv工具要解决的典型场景:不是“转”这个动作难,而是Excel本身的格式多样性、编码默认值和多Sheet结构,会让一板一眼的转换变成玄学。这篇文章会讲清楚转换前必须想明白的三个关键分岔口,给一套可以直接跑的Python方案,再把增量转换、错误隔离和几个高频翻车现场拆给你看。适合谁读?手头有成百上千个Excel要处理的数据开发、测试和业务分析,以及不想在数据导入环节反复填坑的任何人。
2. 动手前先看懂Excel到csv的三个关键分岔口:编码、分隔符和“到底转哪个表单”
很多人一上来就写循环、调库,结果转出来的csv要么乱码,要么行数对不上,要么一个文件只出了第一张表。这些问题几乎都能追溯到三个前置决策:编码怎么定、分隔符用什么、多Sheet文件怎么拆。这三件事没想清楚,后面写多少代码都是在给Bug打工。
2.1 编码:UTF-8 BOM是给Excel留的后门,不是给Python留的
csv文件本身不携带编码元信息,打开它的软件只能靠猜。Excel的Windows版本默认按ANSI(也就是GBK)去解码,所以当你用Python输出一份标准的UTF-8无BOM文件,Excel会把它当GBK读,中文全部变成乱码。反过来,如果下游程序需要的是UTF-8,你按GBK输出,换一个环境就又是一次翻车。
常见的做法是给三个角色分别定编码:给Excel人工打开看的文件用utf-8-sig(带BOM,Excel能正确识别);给程序做自动化处理的用纯utf-8(不带BOM,避免某些Linux下的数据处理框架把BOM当字符);给老旧的业务系统导入时,明确问对方要什么,通常逃不开GBK。写进代码里就是一行参数的事,但选错就是“操作半小时,返工一下午”。
# 用file命令检查已有csv的编码,别靠肉眼猜 file --mime-encoding input.csv2.2 分隔符:你的csv可能被“真正的csv”拒绝
CSV全称是Comma-Separated Values,理论上用逗号,但现实里Excel的“CSV UTF-8 格式”用的其实是逗号,而“CSV(逗号分隔)”在中文版Excel里保存时可能受区域设置影响变成分号。更麻烦的是字段本身包含逗号、换行和引号,如果自己用split(',')去解析,结果必然惨烈。
Python标准库csv模块会负责引号和转义,这才是可靠做法。你要决定的只有分隔符本身:给通用数据处理工具直接用逗号;给某些老系统导数据,对方明确要求制表符或分号,那就换一个参数,代码逻辑完全不用动。转换工具里把分隔符做成可配置项,而不是写死在代码里,是我吃过亏之后养成的习惯。
2.3 多Sheet文件:一个Excel文件转出来往往是“一批”csv
现实中的Excel几乎没有单Sheet的。我一个做财务的朋友给过来的对账单,一个文件里有“1月”“2月”“汇总”三个Sheet,她期望的是每个Sheet独立成一个csv。如果转换脚本只取active,那永远只能拿到打开文件时停在上次保存位置的那张表。
决定Sheet策略就三条路:全部Sheet各出一个csv;只出指定Sheet;按Sheet名拼接规则输出。命名规则也得提前定,常见的是“原文件名_Sheet名.csv”,这个方案最大的坑是重名覆盖:文件A里有Sheet“汇总”,文件B里也有“汇总”,不处理就互相覆盖,静默丢数据。后面第3章的代码里会给出处理方案。
| 决策点 | 默认选型 | 什么时候换 |
|---|---|---|
| 输出编码 | utf-8-sig | 下游是Linux程序时换utf-8;老系统指定GBK时换gbk |
| 分隔符 | 逗号 | 下游系统要求分号/制表符时换 |
| Sheet范围 | 全部 | 只关心指定Sheet时用--sheet参数限制 |
| 文件命名 | 原文件名_Sheet名.csv | Sheet名重复时追加序号 |
3. 用Python搭一个最小可用的批量转换工具:核心代码与参数拆解
理论收住,开始动手。这个章节给出一套可以直接落地的脚本,覆盖目录遍历、多Sheet拆分、编码控制和参数配置,先跑通,再去考虑增量、日志这些工程化能力。
3.1 依赖选型:为什么用openpyxl而不是一上来就pandas
处理Excel到csv,最常见的两条路:用pandas的read_excel加to_csv,或者用openpyxl读再配标准库csv写。pandas确实省事,但如果你只想做格式转换,引入它意味着多背一个庞大的依赖树,而且pandas在读取时会有隐式的类型推断,日期变数字这类问题在pandas那一层就已经发生了,排查起来反而更绕。
我的习惯是openpyxl来读,配合csv标准库来写。openpyxl支持read_only模式,占内存小,data_only=True能拿到公式计算后的值而不是公式本身,这两个选项对本场景是刚需。依赖只需要一个库:
pip install openpyxl选openpyxl只覆盖.xlsx和.xlsm,如果是老式.xls,需要额外加装xlrd,但xlrd目前只支持.xls,这属于另一个兼容性话题,后面第5章会提到。
3.2 核心转换脚本:遍历目录、多Sheet拆分、一行参数控编码
直接给可用的脚本。这个版本已经处理了章节2里提到的命名冲突和Sheet遍历问题,带注释,可以直接存成batch_excel_to_csv.py。
# batch_excel_to_csv.py import csv import argparse from pathlib import Path from openpyxl import load_workbook def excel_to_csv(src_path: Path, dst_dir: Path, encoding: str, delimiter: str, sheet_names: list[str] | None) -> list[str]: """单个Excel转csv,返回实际生成的csv文件名列表""" # read_only=True降低内存占用,data_only=True取公式计算结果 workbook = load_workbook(src_path, read_only=True, data_only=True) generated = [] # 决定要转哪些sheet sheets = sheet_names if sheet_names else workbook.sheetnames for sheet_name in sheets: if sheet_name not in workbook.sheetnames: print(f"[跳过] {src_path.name} 中不存在Sheet: {sheet_name}") continue ws = workbook[sheet_name] # 输出文件名用"原文件名_Sheet名.csv",保证多文件时不会互相覆盖 dst_path = dst_dir / f"{src_path.stem}_{sheet_name}.csv" with open(dst_path, 'w', newline='', encoding=encoding) as f: writer = csv.writer(f, delimiter=delimiter) # values_only=True让每行变成元组而不是Cell对象 for row in ws.iter_rows(values_only=True): writer.writerow(row) generated.append(dst_path.name) workbook.close() return generated def main(): parser = argparse.ArgumentParser(description='批量将Excel文件转换为CSV') parser.add_argument('input_dir', help='存放Excel文件的目录') parser.add_argument('--out', default='csv_out', help='输出目录,默认csv_out') parser.add_argument('--encoding', default='utf-8-sig', help='输出编码,默认utf-8-sig') parser.add_argument('--delimiter', default=',', help='分隔符,默认逗号') parser.add_argument('--sheet', action='append', help='只转指定Sheet,可传多次;不传则转所有Sheet') args = parser.parse_args() src_dir = Path(args.input_dir) dst_dir = Path(args.out) dst_dir.mkdir(parents=True, exist_ok=True) # 输出目录不存在时自动创建 excel_exts = {'.xlsx', '.xlsm'} # 只处理这两种后缀,避免误伤其他文件 failed_count = 0 for src_path in src_dir.iterdir(): if src_path.suffix.lower() not in excel_exts: continue try: generated = excel_to_csv(src_path, dst_dir, args.encoding, args.delimiter, args.sheet) for name in generated: print(f"[成功] {src_path.name} -> {name}") except Exception as exc: failed_count += 1 print(f"[失败] {src_path.name}: {exc}") print(f"完成,失败 {failed_count} 个文件") if __name__ == '__main__': main()这段代码的逻辑不复杂,但有几个参数值得单独说。--sheet用action='append',意味着你可以传多次,比如--sheet 汇总 --sheet 明细,只转这两个;不传就是全转。--encoding默认utf-8-sig,是因为大多数场景下csv最终要交给Excel打开;如果你要喂给Linux上的程序,改成utf-8。newline=''这一行是Python写csv的老规矩,不写的话Windows下每行后面多一个空行。read_only=True这个参数在文件特别大的时候能救命,第6章会单独展开。
3.3 命令行参数设计:让不懂Python的同事也能上手
脚本写出来不是给自己一个人用的,命令行入口的参数设计决定了团队里其他人愿不愿意用。我的原则是:必选参数只有输入目录,其余全部有合理默认值。
# 最简用法:转当前目录下所有Excel,输出到csv_out python batch_excel_to_csv.py ./data # 指定输出目录和编码 python batch_excel_to_csv.py ./data --out ./converted --encoding utf-8 # 只转两个sheet,用分号做分隔符(给老系统用) python batch_excel_to_csv.py ./data --sheet 汇总 --sheet 明细 --delimiter ';'--out不传就输出到当前目录下的csv_out,脚本会自己建文件夹。这种设计能避免同事在自己电脑上一顿操作把csv直接撒在Excel旁边,把原始文件搞得一团乱。
4. 从“能跑”到“敢用”:增量转换、错误隔离和无人值守的必做细节
脚本能跑通只是第一步。真正拿到生产环境,你会发现全量重转几千个文件慢得让人怀疑人生,一个坏文件能让整个批次中断,半夜跑批没人守着看控制台。这一章解决的就是这些问题。
4.1 增量转换:别让机器重复做已经做完的事
批处理场景里最常见的需求是“只转新增和修改过的文件”。实现方案不外乎三种:对比文件名、对比修改时间、对比文件哈希。文件名对比最简单但不可靠——内容改了文件名没变就漏掉;哈希对比最准但每个文件都要全量读取,大文件场景很耗时;折中方案是看源文件和目标csv的修改时间。
# incremental_check.py from pathlib import Path def should_skip(src: Path, dst: Path) -> bool: """如果目标csv已存在,且比源文件新,则跳过转换""" if not dst.exists(): return False # 没有产出过,必须转 return dst.stat().st_mtime >= src.stat().st_mtime把这几个函数接到第3章的主循环里,就变成了增量版本:每次只处理新文件和被改过的Excel文件。注意st_mtime的粒度问题:有些文件系统时间戳精度是秒级,同一次编辑里源文件和目标csv的时间可能一样,我一般会在比较时加一个小阈值,或者干脆用>而不是>=,宁可在边界上多转一次,也不要漏转。
4.2 单文件失败不打断整体:把错误汇总成报告
批处理的第二原则是“一个坏文件不能拖垮整个批次”。第3章的代码里已经用了try/except包住单文件转换,但只打印到控制台还不够——你需要一份能事后拿来看的错误清单。
# error_report.py import traceback failures = [] for src_path in src_dir.iterdir(): if src_path.suffix.lower() not in excel_exts: continue try: excel_to_csv(src_path, dst_dir, args.encoding, args.delimiter, args.sheet) except Exception: failures.append({ "file": src_path.name, "error": traceback.format_exc(), }) if failures: with open("转换失败清单.txt", "w", encoding="utf-8") as f: for item in failures: f.write(f"{item['file']}\n{item['error']}\n---\n")这里用traceback.format_exc()记录完整的堆栈,而不是只记一句str(exc)。只记一句话的坏处是,比如“zipfile.BadZipFile”这类错误你根本不知道是文件损坏还是后缀伪装,带了堆栈才能定位。另外,失败清单要单独落盘,不要让失败信息混在标准输出里,否则定时任务跑了三天你再翻终端日志,眼睛会瞎。
4.3 和定时任务、CI集成:无人值守才是批处理的归宿
批量转换工具最终要挂在计划任务里跑。Windows上就是“任务计划程序”,Linux上就是cron,或者挂在流水线里作为数据预处理的第一步。
# 每天凌晨1点跑一次增量转换,日志落盘 0 1 * * * cd /path/to/project && python batch_excel_to_csv.py ./data --out ./csv_out >> ./logs/convert.log 2>&1如果增量逻辑已经写好了,这个cron任务就是安全的——每天跑,只转当天新增的文件,输出的csv目录保持最新。有个小坑提醒:定时任务的环境变量往往和手动执行不一样,Python脚本在cron里找不到模块是常见问题。要么用绝对路径的venv里的Python解释器,要么在脚本开头自己sys.path.insert。我习惯用绝对路径的python,省事少踩坑。
5. 批量转换避坑指南:乱码、日期变数字、内存被吃光的四个高频现场
写转换工具的人八成时间不是在写代码,而是在处理“为什么转出来不对”。这一章整理四个我实际遇到过的现场,都是现象、原因、解决三步说清。
5.1 现象:转换后的csv在Excel里打开全是乱码
原因基本只有一个:Pyton输出了不带BOM的UTF-8,Excel默认按ANSI解码。这是编码决策错误,不是数据坏了。解决方式是在打开输出文件时看一眼文件头有没有BOM,或者直接在脚本里把默认编码设为utf-8-sig。
顺带说一个反直觉的事:如果你用utf-8-sig生成的csv带给下游Python程序处理,程序的csv.reader不指定编码会默认用utf-8,此时文件头会有个\ufeff字符,导致第一列第一行多出不可见前缀。所以给程序用的csv用不带BOM的utf-8,给Excel用utf-8-sig,两个都要给的时候,脚本里加个编码参数切换。
5.2 现象:Excel里的日期,转出来变成“44786”这种数字
这不是转换工具写错了,是Excel日期本质上是序列数。openpyxl在data_only=True时读到日期单元格,返回的是datetime对象;但如果你用的是某些带转换的库,或者源文件里的日期是文本存储的,就会出现数字。解决的关键是转换时统一拦截所有datetime对象。
# format_cell.py from datetime import datetime, date def safe_str(value): """把Excel单元格值转成适合写csv的字符串""" if isinstance(value, datetime): return value.strftime('%Y-%m-%d %H:%M:%S') if isinstance(value, date): return value.strftime('%Y-%m-%d') return value在writer.writerow之前对每行做一次safe_str映射,日期列就不会再变成神秘数字。如果你想让下游自由选择日期格式,可以把strftime的格式也做成参数,但我的经验是YYYY-MM-DD和YYYY-MM-DD HH:MM:SS两种格式能覆盖90%的需求,别过度设计。
5.3 现象:一个文件明明有六个Sheet,转出来只剩一个
原因几乎都是脚本取的是workbook.active而不是遍历sheetnames。active是文件上次保存时激活的那个Sheet,用户最后看过哪个就停在哪,所以表现就是你永远只会得到“用户最后看的那一页”。
另一个隐蔽一点的原因:有些Excel文件里存在“不可见Sheet”,sheetnames和“工作簿里肉眼能看到的表”不是一回事。如果业务上有需求连隐藏Sheet一起转,代码不用改,sheetnames本来就包含隐藏表;如果不想转隐藏表,需要额外读sheet_state属性做过滤。
5.4 现象:几百MB的Excel一转换,内存占用飙满,机器卡死
这个问题在第3章的脚本里其实已经规避了一半——用了read_only=True。剩下那一半是写入端的:用DataFrame.to_csv一类的方案会先在内存里组装完整数据再写盘,文件一大就吃不消。csv.writer配合iter_rows是逐行读、逐行写的流式过程,内存占用和文件大小关系不大。
如果你是从.xls老文件读取,事情就麻烦一点:xlrd不支持流式读取,大文件照样吃内存,解决办法是先让用户把.xls批量转成.xlsx再用这个工具,或者换开源社区维护的其他解析库。这个问题没有完美解,现实点就是分两步走,没那么痛。
6. 把工具再往前推一步:大文件流式转换、列裁剪和转换后的核对习惯
最后这章讲一个值得现在就改进到工具里的技巧:大Excel文件的流式写法。第3章的代码里虽然已经用了read_only=True,但把整个Sheet里的所有行都原样写进了csv——还没到最省内存的状态。实际上openpyxl的read_only模式还有一个配合招式,同时处理“大文件”和“只要一部分列”的场景。
先看核心代码:
# streaming_convert.py import csv from openpyxl import load_workbook def convert_large(src_path, dst_path, columns=None): """流式读取xlsx并写出csv,columns控制列裁剪""" from openpyxl.cell.cell import Cell workbook = load_workbook(src_path, read_only=True, data_only=True) ws = workbook[workbook.sheetnames[0]] # 默认只处理第一个Sheet with open(dst_path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) for row in ws.iter_rows(values_only=True): if columns: row = tuple(row[i] for i in columns if i < len(row)) writer.writerow(row) workbook.close() # 用法:只导出第0列和第2列,适合大宽表裁剪 # convert_large("big.xlsx", "big_selected.csv", columns=[0, 2])这个写法的关键是在iter_rows循环里做列裁剪,内存里同时存在的始终只有“一行的数据”,而不是整张表。注意,read_only模式对同一个workbook对象切换Sheet会浪费资源,所以如果是多Sheet大文件,最可靠的写法还是每个Sheet单独处理,或者干脆在循环里每次都重新load_workbook,大文件场景这点开销值得。
进阶技巧说完,再讲一个我的固定习惯:批量转换完成后,不要直接走人,抽三个文件对行数。
# 源文件Sheet总行数 vs csv总行数,两者差1是因为表头 wc -l converted/文件名_Sheet1.csv python -c "from openpyxl import load_workbook; wb=load_workbook('源文件.xlsx', read_only=True); print(wb['Sheet1'].max_row)"行数对得上,数据基本就没丢;对不上就回去看是哪个Sheet少了尾巴——这个问题多发生在源文件里有合并单元格、自动筛选或分页导致的空行,iter_rows会原样带出这些空行,反而让你核对行数时产生误差。所以核对时多看数据头尾,别只看总数。批量转换这条路没有太多黑匣子,把编码、Sheet策略和流式这三件事控制住,工具就从“勉强能用”变成了“可以托付”。希望帮到你。
本文还有配套的精品资源,点击获取