先把话说在前头:如果你手里堆着几十个Excel要合并、清洗、转成CSV,或者反过来,隔三差五就要接手乱七八糟的CSV文件,别急着一个个打开手工弄。用Python批量处理Excel和CSV文件,是我这几年做数据支持工作里复现率最高的一套活。它解决的从来不是“能不能处理”的问题,而是“多久能处理完”和“会不会有人手滑改错数据”的问题。
上个月部门收到20多个分公司的销售月报,有的叫“销售表”,有的叫“数据汇总”,列名还五花八门,甚至有几个文件带了合并单元格。我花了半小时写了个脚本,几分钟跑完,之后每个月只需要双击运行一次。接触过各种方案之后,我的体会是:批量处理Excel和CSV这件事,真正卡住新手的往往不是Python本身,而是“不知道用哪个库”“读取时有哪些坑”“编码怎么选”这些细节。这篇我把完整方案和你可能踩的坑一次性说清楚,内容偏实操,代码可以直接抄。
1. 先选对工具再动手:pandas、openpyxl和csv模块的取舍
1.1 三套方案各有各的适用边界
很多人一上来就装pandas,这没错,但你要知道pandas不是唯一的答案。我自己的习惯是,动手前先想清楚这次处理的核心需求是什么,然后从三套工具里挑一套合适的。
| 场景 | 推荐工具 | 理由 |
|---|---|---|
| 只做CSV读写、字段拼接、转义处理 | Python内置csv模块 | 零依赖,速度最快,环境兼容性最好 |
| 需要保留Excel样式、修改单元格、处理公式 | openpyxl | 精确到单元格级别的控制 |
| 批量读取、筛选、合并、透视、格式转换 | pandas | 表格思维,一行代码覆盖大部分需求 |
具体来说,标准库csv适合那种“从A系统导出CSV,调整后导进B系统”的轻量场景,它不需要安装任何第三方包,也不存在版本兼容问题。openpyxl则适合改格式类需求,比如批量把某个Sheet的字体颜色改掉、在指定行插入内容、修改批注。但如果你的目标是“把100个Excel读进来,筛出销售额大于一万的行,合在一起输出”,那直接用pandas是最省事的,它的read_excel、read_csv、to_excel几乎就是为这种场景设计的。
我一般建议把pandas和openpyxl都装上,两者配合使用:pandas负责批量读取和运算,需要精细操作单元格时再绕回openpyxl。标准库csv不需要装,但要会读会写,因为某些特殊环境里pandas可能装不上。
1.2 安装与验证:一次把环境配好
环境准备这块,先把几个高频问题按顺序排好,照着做基本不会翻车。
安装Python时,Windows平台一定记得勾选“Add Python to PATH”,否则后面命令窗口输入python会提示找不到命令。装完之后用VS Code写代码的话,装好Python扩展,再在右下角或命令面板里选中正确的解释器。很多所谓“环境问题”,最后查出来都是解释器没选对。
依赖安装很简单:
pip install pandas openpyxl国内网络环境下,换镜像源会快很多:
pip install pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple装完验证一下:
python -c "import pandas, openpyxl; print(pandas.__version__, openpyxl.__version__)"这里有个特别容易踩的坑:pandas装了,openpyxl没装,运行read_excel会直接报“Missing optional dependency 'openpyxl'”。字面意思已经说得很清楚了,但我见过不止一个同事为了这个报错把pandas卸载重装好几次,最后才反应过来是openpyxl缺失。所以装依赖的时候,两个一起装最省事。
1.3 版本和文件格式的兼容问题
再避一个坑:不要觉得“装了最新版就万事大吉”。pandas对Excel的读写依赖openpyxl或xlrd,但两者支持的格式不一样。
- openpyxl支持.xlsx,不支持老的.xls;
- xlrd 2.0以上版本只支持.xls,不支持.xlsx,我之前用xlrd读xlsx就吃过亏;
- pandas 2.x之后,有些版本的默认引擎选择也会影响读取结果。
如果你的源文件是.xls结尾,处理方案有两个:一是让文件提供方另存为.xlsx,这最省事;二是额外装一个旧版xlrd并显式指定engine='xlrd'。我的建议是优先让文件变成.xlsx,因为.xls本身是旧格式,后续各种工具兼容性都差。别小看这点,实际处理大批量文件时,“文件格式和代码预期不一致”是报错的重灾区。
2. 第一批文件怎么进来:目录遍历、文件筛选与类型兜底
2.1 用pathlib遍历目录,不要再用os.listdir
批量处理的第一步,是拿到所有目标文件的路径。我推荐用pathlib的glob方法,比老派的os.listdir简洁得多,而且返回的是Path对象,拼接路径时不用再管反斜杠和正斜杠的问题。
from pathlib import Path data_dir = Path("data") xlsx_files = sorted(data_dir.glob("*.xlsx")) csv_files = sorted(data_dir.glob("*.csv"))注意我用了sorted,这一步不是可有可无。不同操作系统上glob返回的文件顺序可能不一样,如果不排序,合并出来的行顺序每次可能都不同,排查时也很容易被误导。排一下序,每次跑出来的结果一致,逻辑上更可控。
如果文件放在多级子目录里,用rglob递归遍历:
all_files = sorted(data_dir.rglob("*.*")) files = [p for p in all_files if p.suffix in (".xlsx", ".xls", ".csv")]这种写法会把当前目录下所有符合条件的文件全部捞出来,适合文件归档比较深的场景。
2.2 按文件名模式筛选目标文件
实际业务经常只要“包含销售回款”的文件,或者只要某几个日期段的数据。在列表推导式里直接过滤就行:
targets = [p for p in xlsx_files if "销售" in p.stem]p.stem是去掉后缀的文件名。如果文件名本身有规律,比如“2025-01-销售.xlsx”,还可以用正则把日期提取出来,再按日期区间过滤。这块看具体需求,但思路是一致的:优先在路径层面缩小处理范围,而不是把文件全读进来再筛选,省内存也省时间。
2.3 Excel读取时强制指定类型:防丢精度的一招
这一步是我最想强调的。Excel里很多列看着是数字,实际上是文本,比如订单编号、身份证号、电话号,甚至经纬度。pandas默认会自动推断类型,经常把“001”读成“1”,把超过15位的数字尾部变成0,而这些数据一旦读错,后面怎么清洗都救不回来。
解决办法是读取时用dtype强制指定列的类型:
df = pd.read_excel( file_path, engine="openpyxl", dtype={"订单编号": str, "客户编码": str, "经纬度": str, "创建时间": str} )把容易出问题的列统一按字符串读进来,再根据实际需要做后续转换,这是“先读取,后清洗”思路的关键一步。很多坐标点错位、编号溢出的问题,源头就在这个读取阶段。
2.4 CSV读取时关于编码和分隔符的兼容方案
CSV文件最折磨人的两件事,一是编码,二是分隔符。中文环境的CSV常见三种编码:utf-8、utf-8-sig(带BOM)、gbk。乱码往往不是文件坏了,是编码猜错了。最稳妥的办法是尝试多个编码直到成功:
for enc in ("utf-8-sig", "gbk", "utf-8"): try: df = pd.read_csv(file_path, encoding=enc) break except UnicodeDecodeError: continue分隔符问题也一样。有些系统导出的CSV用逗号,有些用分号,有些用制表符,甚至同一个文件里既有逗号又有分号。想兼容多种情况,可以直接用正则:
df = pd.read_csv(file_path, sep=r"\s*[,;]\s*", engine="python")这招在接手来历不明的CSV时特别管用。不管你是处理电力负荷数据、气象CSV,还是某个手机价格预测.csv的数据集,读取阶段把编码和分隔符这两关过了,后面的分析才有意义。
3. 真正折磨人的是单元格细节:公式、文本数字和坐标精度
3.1 data_only=True只是看起来简单,其实有条件
如果你处理的Excel里有公式,比如每张分表里“合计”列是算出来的,用openpyxl直接读通常拿不到值。`load_workbook`默认把公式作为字符串读进来;只有当文件之前被Excel或其他表格软件打开并保存过、缓存了计算结果,`load_workbook(data_only=True)`才读得到数值。
from openpyxl import load_workbook wb = load_workbook(file_path, data_only=True) ws = wb.active print(ws["H10"].value)如果发现data_only=True读出来是None,别慌,八成是文件生成时没有计算过公式。这种情况在程序自动生成的Excel里特别常见,因为生成工具只写了公式,没有触发重算。最稳妥的方案是:能用pandas的场景尽量不用openpyxl读公式值,必须读缓存值时,先用Excel把源文件打开保存一次,或者要求源头直接输出数值版本。
处理公式还有个思路是直接从源头上绕开:如果你是脚本生成Excel,写入的本来就是计算后的数值,那别人读取时就不会遇到公式问题。这条经验在处理“由程序导出、再由程序导入”的表格时很实用。
3.2 文本型数字、千分符和科学计数法
Excel里左上角带绿三角的“文本数字”,到pandas里就是字符串。对这种字符串转数字,不能直接astype,得先去分隔符和单位。
def clean_numeric(series): s = series.astype(str).str.replace(",", "", regex=False) s = s.str.replace("元", "", regex=False).str.strip() return pd.to_numeric(s, errors="coerce")errors="coerce"表示转不过去的变成NaN,这样后续能直观看到哪行数据有问题,而不是直接让整个脚本崩掉。
这种“数字列里混了千分符”的情况,在财务导出文件里简直是大头。我经常看到有人把列转成字符串之后直接去掉逗号,但忘了同一列里可能还有“元”“¥”这种单位后缀。清洗的时候多看一眼实际数据长什么样,比无脑replace要靠谱。
科学计数法也是一个高频问题:身份证号、长数字读进Excel会显示成1.23457E+17。根源是Excel的单元格格式和浮点数精度限制。Python这边把列指定为字符串通常能缓解,但如果最终输出Excel给别人,建议把这类列在Excel里设成文本格式再写入,否则表格一打开又是科学计数法,对方还是会来问你。
3.3 经纬度和ID列精度丢失:一次坐标点错位的排查
有几次做地图相关数据时,我发现Excel里存的经纬度被pandas读出来以后,最后几位全变了,比如106.714328读成106.71432799999999。原因是Excel数字默认按双精度浮点存储,15位有效数字内一般没事,但经纬度要精确到6位小数时,浮点误差就可能让坐标点位置不对。
如果你在arcmap或GIS工具里导入Excel坐标点,发现点位偏移、聚集到一个奇怪的位置,经常不是软件操作问题,而是导入时经纬度列被当作数值、发生了精度舍入。解决方案很简单:读取时把经纬度列指定为字符串,或者用Decimal保存,不让pandas做浮点数推断。
df = pd.read_excel( "points.xlsx", dtype={"lng": str, "lat": str} )这样坐标点的值才会原样保留,后续要算距离、画图时再转float也不迟。记住一个原则:像编号、坐标、电话号码这种“看起来像数字但不能丢精度”的列,一律先当字符串读。
3.4 合并单元格和“提取拼音”这类文本操作
批量清洗数据时,合并单元格是隐藏大坑。pandas读Excel不会自动把合并单元格展开,会得到一部分NaN。遇到连续的合并单元格,可以用前向填充处理:
df[["区域"]] = df[["区域"]].fillna(method="ffill")这个办法只对连续区域的合并有效。如果表格结构本身很乱,我建议让业务方提供标准的二维表,而不是自己花半小时去猜合并规则。批量处理的终极价值是省时间,不是用来跟糟糕的表格结构较劲。
至于提取拼音、拼音首字母这类需求,用pypinyin三行代码就能解决,常在姓名、单位名整理时用到。不过它和表格批处理是两件事,这里不展开。真正高频的文本处理还是字段拆分,比如把“北京市朝阳区XX路”拆成省市区:
df[["省", "市", "区"]] = df["地址"].str.split(n=2, expand=True).iloc[:, :3]这种操作配合批量读取,能省掉大量手工复制粘贴。
4. Excel和CSV互转的边界问题:编码、分隔符与导库前置准备
4.1 写CSV后在Excel打开乱码,根源是编码标记
一个经典场景:用Python把处理结果写成CSV,客户双击打开Excel,所有中文变成乱码。原因不是保存错了,而是Excel默认用系统ANSI代码页打开CSV,Windows中文环境默认是GBK,你保存成标准UTF-8,Excel没识别出来。
解决方案:保存CSV时用utf-8-sig编码,也就是带BOM,Excel就能正确识别。
df.to_csv("result.csv", index=False, encoding="utf-8-sig")另一个细节是index=False,不然第一列会多出一列行号,几乎人人踩过。这个顺手写成习惯,就不会忘。
4.2 给DBeaver、SQL Server导入CSV时的前置准备
如果你要把CSV导入到SQL Server、DBeaver、MySQL这类系统,情况又不太一样。数据库连接工具通常按你指定的编码和分隔符读CSV,导入报错时优先检查三样:编码、首行标题、分隔符。
| 工具或场景 | 常见问题 | 对策 |
|---|---|---|
| SQL Server导入CSV | 中文乱码 | 指定Codepage=65001,对应UTF-8 |
| DBeaver导入CSV | 列错位、列名变第一行 | 检查Header和Delimiter设置是否匹配 |
| 各种ETL工具 | 长文本/换行破坏字段结构 | 用csv模块的writer生成标准转义格式 |
SQL Server导入时,如果中文是UTF-8,要选Codepage=65001;乱码时换936重试。DBeaver导入对话框里“Header”和“Delimiter”选项要跟文件实际情况一一对应,错一个导进去就会乱。有些系统一定要分号或制表符当分隔符,导出时用sep=";"或sep="\t"生成,导入成功率会高很多。
有些CSV里包含长文本和换行,常规的字符串拼接会破坏字段结构,最好用csv模块的writer,它会正确处理引号转义:
import csv with open("export.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerows(rows)4.3 大批量CSV合并进同一个Excel文件
把几百个CSV合并到一个Excel,很多人想的是循环逐个to_excel,其实更高效的是先逐个读进DataFrame,统一列名后,写入同一个Excel的不同Sheet:
import pandas as pd from pathlib import Path out_path = "merged.xlsx" with pd.ExcelWriter(out_path, engine="openpyxl") as writer: for i, f in enumerate(sorted(Path("csv_dir").glob("*.csv")), 1): df = pd.read_csv(f, encoding="utf-8-sig") df.to_excel(writer, sheet_name=f"表{i}", index=False)这个模式在处理“一个文件夹几十个CSV,想集中到一个Excel里发给别人”时特别好用。Sheet名直接用表1、表2,别人打开也不用一个个找文件。
4.4 “外部表不是预期的格式”到底是怎么回事
还有一类非常常见的报错,叫“外部表不是预期的格式”,通常出现在你试图用数据库工具或外部程序加载一个Excel文件时。最常见的原因有两个。
第一个是扩展名和实际文件格式不一致:文件内容其实是CSV或HTML表格,但扩展名被改成了.xlsx,工具按Excel格式去解析自然失败。第二个是xls和xlsx混用:工具预期xlsx,你给的是老xls。解决思路很简单,不要只看扩展名,先打开文件确认真实格式,或者用Python读一遍,看pandas能不能识别。识别不了就别强行导入,先转换格式。
我自己的习惯是,遇到这种问题先写两行代码:
with open("problem.xlsx", "rb") as f: print(f.read(8))如果是xlsx,文件头是PK;如果是CSV,开头直接就是文本内容。这一步十秒钟就能判断文件真实格式,比在工具里反复换配置快得多。
5. 批量合并与拆分:高频需求的两种标准写法
5.1 多Excel文件按列合并,关键是列名统一
批量处理里出现频率最高的需求之一,就是多个Excel合并成一个。直接看代码:
import pandas as pd from pathlib import Path standard_cols = ["日期", "区域", "销售额", "备注"] all_parts = [] for f in sorted(Path("reports").glob("*.xlsx")): df = pd.read_excel(f, engine="openpyxl", dtype=str) df.columns = [c.strip() for c in df.columns] # 统一列名:缺失的列补上,多出的列丢弃 for c in standard_cols: if c not in df.columns: df[c] = pd.NA df = df[standard_cols] # 把销售额统一成数值 df["销售额"] = pd.to_numeric(df["销售额"], errors="coerce") all_parts.append(df) result = pd.concat(all_parts, ignore_index=True) result.to_excel("汇总.xlsx", index=False)关键点有两个。一是先做列名统一:如果20个文件里有两个列名不一致,concat会直接多出两列,而不是报错,最后汇总表结构就乱了。二是所有文件统一用dtype=str读进来,避免同一列在这个文件是数字、在那个文件是文本时类型合并出错。这两点做到了,合并基本不会出大问题。
5.2 按列值拆分,文件名的非法字符必须处理
反过来,把一个“总表”按区域、按月份拆成多个文件,用groupby就行。
by_area = result.groupby("区域", dropna=False) for area, group in by_area: safe = str(area).replace("/", "_").replace("\\", "_") group.to_excel(f"分表_{safe}.xlsx", index=False)文件名的清洗必须做。因为你根本不知道区域名里会出现什么字符,Windows的文件名不允许/、\、:、*、?、"、<、>、|,任何一个都有可能导致to_excel报错。更稳的做法是用列表推导式把所有非法字符一并替换,而不是只替换一两个。
5.3 一个Excel拆成多个CSV:sheet_name=None真的省事
还有一种常见需求:一个Excel里有12个月,每个Sheet一个月度数据,需要批量拆成12个CSV。用sheet_name=None可以一次性把所有Sheet读成一个字典:
excel_file = "2025年数据.xlsx" sheets = pd.read_excel(excel_file, sheet_name=None, engine="openpyxl") for name, df in sheets.items(): safe_name = str(name).strip().replace(" ", "_") df.to_csv(f"by_sheet_{safe_name}.csv", index=False, encoding="utf-8-sig")pd.read_excel(sheet_name=None)返回的字典,key是Sheet名,value是DataFrame,这个接口在处理多Sheet场景时真的省事。配合前面的按列拆分,基本可以覆盖日常70%以上的合并拆分需求。
6. 让脚本稳定跑完:报错速查、日志追踪与函数封装
6.1 高频报错与对策速查
批量处理过程中,有几个报错我遇到的频率最高,整理成一张表方便你对照。
| 报错现场 | 可能原因 | 处理建议 |
|---|---|---|
| Missing optional dependency 'openpyxl' | pandas读Excel缺引擎 | pip install openpyxl |
| UnicodeDecodeError | CSV编码猜错 | 依次尝试utf-8-sig、gbk |
| Bad line / Error tokenizing data | 分隔符选错或脏数据多列 | 用sep正则自动识别或先看文件头部 |
| 外部表不是预期的格式 | 文件格式和扩展名不一致 | 确认真实格式,先另存为xlsx再导入 |
| data_only=True读到None | 公式没有缓存值 | 用Excel打开保存一次,或要求源头给值 |
拿到报错时,先别急着改代码,打印前几行数据看看:
print(df.head()) print(df.dtypes)大部分问题在数据层面就能看出来,比如列错位、值变成NaN、类型不对。定位到具体问题再改逻辑,比盲目重跑更高效。
6.2 批量处理必须加日志,不然出事没法查
批量处理最忌讳的是:跑到第50个文件报错,前面的49个白做,又不知道是哪个文件出的问题。所以脚本一开头就要加日志。
import logging logging.basicConfig( filename="batch.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", encoding="utf-8" ) for f in files: try: df = pd.read_excel(f, engine="openpyxl") # 处理逻辑 logging.info(f"完成: {f.name}, 行数={len(df)}") except Exception as e: logging.exception(f"失败: {f.name}, 原因={e}") continuelogging.exception会在日志里自动带上堆栈信息,排查时非常有用。单个文件失败不中断整体,是稳定批量脚本的底线要求。任何批量处理,都要让程序具备“坏一个文件不影响其他文件”的能力。
6.3 把脚本封装成可复用函数
处理同样一批文件达到三次以上,就该考虑封装了。我通常把“读取—清洗—输出”变成三个函数,再写一个main串起来。
def load_file(path): if path.suffix == ".csv": return pd.read_csv(path, encoding="utf-8-sig", dtype=str) return pd.read_excel(path, engine="openpyxl", dtype=str) def clean_data(df): # 统一列名、清理文本、类型转换 return df def save_output(df, out_path): df.to_excel(out_path, index=False) def main(): for path in get_target_files(): try: df = load_file(path) df = clean_data(df) save_output(df, out_path_for(path)) logging.info(f"完成: {path.name}") except Exception as e: logging.exception(f"失败: {path.name}, 原因={e}") if __name__ == "__main__": main()这样后面清洗规则变了,只改clean_data一个函数,其他部分不用动。批量处理Excel和CSV的脚本,想要长期复用,这个封装习惯越早养成越好。
最后再分享一个小细节:我习惯在脚本输出目录里放一个readme.txt,记录这份数据是什么时候生成的、脚本在哪里、清洗逻辑是什么。三个月后再打开这个目录,你会感谢当初的自己。批量处理Excel和CSV的脚本,从来不是跑完就结束的东西,能维护、能复现、能解释,才算真正交付。