Excel批量转CSV:Python脚本实战与避坑指南

发布时间:2026/10/20 15:51:23
Excel批量转CSV:Python脚本实战与避坑指南 简介面向Windows用户的批量格式转换工具主要解决多个Excel文件逐个手工另存为csv的低效问题适合办公文员、数据分析人员及需要批量处理表格的开发者使用。压缩包共33个文件整体仅94KB核心是bin目录下已编译好的ExcelToCsv.exe解压后可直接运行同时附带VC工程完整源码包括h/cpp头文件与实现、dsp/sln/dsw等工程配置以及xls/xlsx/csv测试数据便于核验转换结果。源码原始版本存在Excel Font类与Windows SDK重定义导致的编译问题资源包已修复该冲突并重新编译对研究Excel文件解析、CSV输出和编译排错思路的读者也有参考价值。已有2682人学习下载。1. 批量Excel转CSV到底解决什么问题从手工另存为到脚本批处理如果你手上攒了几百个Excel文件需要定期转成CSV交给下游系统你很快就明白手工“另存为”有多折磨人。批量Excel转CSV工具就是把这一堆.xlsx/.xls按统一规则变成.csv同时管好编码、分隔符、多Sheet拆分这些没人愿意手工检查的细节。它解决的不仅是“省时间”而是让每一次转换结果都一致——手工操作这次可能用了UTF-8下次可能忘了去空格结果就是下游数据对不上还查不到原因。这个工具最典型的用户是三类人给数据库或数仓做前置清洗的数据工程师被业务部门要求“每周导一份文本文件”的运维以及经常交付报表给外部系统的分析岗。他们共同的痛点不是“不会转”而是“转得不可控”。CSV乱码这件事十次里有八次不是转换工具的锅是编码选择的问题批量工具的价值恰恰是把编码、分隔符、类型处理这些黑匣子逻辑固定下来让任何人都能一键复现同样的结果。2. 批量转换的工具选型脚本、命令行还是GUI先想清楚需求边界直接写代码之前先花十分钟把需求边界想清楚能省掉后面很大一部分返工。批量转CSV听起来是纯体力活但“转成什么样、给谁用、字段里藏没藏逗号”这三个问题决定了你该用哪个库、该写多少防御代码。2.1 先拆需求文件来源、消费方和字段规则我一般会先列一张需求清单把模糊的“转一下就行”变成具体规则。第一条是文件来源这批Excel是同一个报表模板导出的还是散落在不同系统里的历史文件模板统一的话列名和字段顺序是固定的脚本可以写死不统一的话就要按“检测到的列”动态输出否则下游对不上表头。第二条是CSV的消费方。给MySQL的LOAD DATA用和给Python pandas用对编码和分隔符的容忍度完全不同。老一些的系统可能只认GBK或ANSILinux上的服务则几乎默认UTF-8如果消费方会用Excel直接打开CSV那UTF-8 with BOM基本是唯一不会乱码的答案。还有文件头有的系统要求带列名的表头有的要求必须没有表头。第三条严重被低估字段内容。Excel单元格里可以合法地包含逗号、换行、双引号你自己手工另存为转出来的CSV没问题是因为Excel按标准规则加了引号脚本如果用手拼字符串的方式构造一行遇到这种单元格就翻车。建议按下面这个表把需求过一遍需求维度要问的问题对实现的影响扩展名只有.xlsx还是有老.xls决定用哪个读取库Sheet数量转第一个还是全部决定要不要按Sheet拆分文件名表头需要列名还是纯数据决定是否带header输出编码下游用什么系统读决定UTF-8-SIG还是GBK分隔符逗号还是制表符决定sep参数字段规则有没有长数字、日期格式决定是否强制文本读取顺便说一下原理。Excel的.xlsx本质是一个zip压缩包里面是多个XML文件单元格值、样式、公式分开存放而CSV就是纯文本一行一条记录字段用分隔符切开。转换这个动作本质上不是“另存为”而是把XML里的单元格解析出来按纯文本规则重新序列化。理解了这一点你就能明白为什么有时直接改扩展名从xlsx改成csv是没用的——内容还是zip格式任何文本编辑器打开都是一堆乱码需要通过真正的解析器来读。2.2 三条路线怎么选Python脚本、现成命令行、图形工具常见的落地路线有三条写Python脚本、调用办公套件的无头命令行、用图形化小工具。办公套件的无头命令行好处是零代码一个命令就能把指定文档转成CSV坏处是对编码和格式的控制很弱转出来的文件经常带着一堆样式遗留的空白而且多Sheet策略、列类型处理基本不可控适合一次性救急不适合成为长期自动化的一环。图形化工具适合完全不懂命令行的业务同事手工拖拽文件但批量场景下没人愿意每天打开窗口点按钮更没法接定时任务。所以大多数团队最终都会走到Python脚本这条路上来。原因很直接pandas和openpyxl对Excel格式的覆盖够全脚本既能批量遍历目录也能精确控制编码、分隔符、Sheet选择和输出命名出了问题时还能写日志记录这些能力是图形界面给不了的。我个人的判断标准是这样如果转换过程需要顺带做数据清洗比如删列、过滤行、改日期格式、合并多个Sheet那就直接用pandas它的DataFrame操作比逐行处理舒服太多如果只是“原样搬运格式”一个文件几十MB甚至上百MBpandas反而累赘用openpyxl逐行读再配合csv.writer逐行写内存占用能低一个数量级。2.3 为什么把pandas当主力、什么时候不用它pandas读Excel的核心是pd.read_excel()底层会根据文件扩展名自动选择解析器。对.xlsx常见做法是走openpyxl对老.xls依赖xlrd。这一层对使用者是透明的但有个隐藏代价pandas会做类型推断。比如一列全是数字的物料编码读进来可能变成int64再写回CSV时前导0没了超过15位的数字变成科学计数法。所以pandas适合的场景是“我要对数据做加工”的转换而不是“原样搬运”的转换。什么时候不用pandas第一文件超大——读Excel会把整个Sheet载入内存一个200MB的xlsx可能吃掉1GB以上的RAM。第二列类型必须完全原样保留——pandas的推断机制总会尝试“帮你做决定”而openpyxl逐行读取能拿到单元格的原始值。第三转换脚本要跑在内存受限的容器里逐行流式处理更稳妥。选型结论可以压缩成一句话要加工、要过滤选pandas要快、要省内存、要原样选openpyxl逐行写CSV。老.xls文件没有太多选择余地基本只能靠xlrd把单元格读出来再手动写文本行。下面进入实现环节我会先给一个能直接跑的最小脚本再逐步加参数和防御逻辑。3. 用Python落地批量转换最小脚本、多Sheet拆分与CLI入口这一章直接给能抄的代码。先约定目录结构一个输入目录放所有Excel文件一个输出目录放生成的CSV脚本放在同层或任意位置都可以。我会从最小单Sheet版本开始然后扩展成多Sheet版本最后加一个命令行入口让它能被定时任务调用。3.1 最小批量脚本遍历目录、单Sheet转储与输出目录下面的脚本是常见做法的简化版用glob扫描输入目录下所有.xlsx文件pandas读取第一个Sheet输出到指定目录。# 批量转CSV的最小脚本单Sheet版本 import glob import os import pandas as pd INPUT_DIR ./excel_files # 放Excel的目录 OUTPUT_DIR ./csv_files # CSV输出目录 os.makedirs(OUTPUT_DIR, exist_okTrue) for xlsx_path in glob.glob(os.path.join(INPUT_DIR, *.xlsx)): # dtypeobject 让所有列按文本读取避免数字类型推断 df pd.read_excel(xlsx_path, dtypeobject) base os.path.splitext(os.path.basename(xlsx_path))[0] out_path os.path.join(OUTPUT_DIR, f{base}.csv) df.to_csv(out_path, indexFalse, encodingutf-8-sig) print(f已转换: {xlsx_path} - {out_path})逻辑说明glob.glob负责匹配目录下所有.xlsx结尾的文件os.path.splitext把文件名和扩展名拆开保证输出文件和输入文件同名不同后缀df.to_csv的indexFalse很关键——不关掉的话CSV第一列会多出一行行号。编码用了utf-8-sig这是带BOM的UTF-8Excel双击打开不会乱码下游Python程序也能正常识别。参数说明dtypeobject表示所有列都按Python对象读入不做数值推断这样物料编码、订单号这类长数字不会变成科学计数法代价是数值计算需要手动转类型。如果输入目录里混着.xls这个脚本会跳过下一章会处理。3.2 多Sheet拆分与编码选择utf-8-sig为什么是默认答案实际业务里一个Excel文件经常带着“汇总”“明细”“辅助表”多个Sheet而下游系统往往只想要其中一个。更常见的是“每个Sheet转成一个CSV”文件名加上Sheet名来区分。下面这个版本处理的就是这种情况。# 多Sheet拆分每个Sheet独立CSV文件名加Sheet名 import glob import os import pandas as pd INPUT_DIR ./excel_files OUTPUT_DIR ./csv_files os.makedirs(OUTPUT_DIR, exist_okTrue) for xlsx_path in glob.glob(os.path.join(INPUT_DIR, *.xlsx)): # sheet_nameNone 返回一个dictkey是Sheet名value是DataFrame sheets pd.read_excel(xlsx_path, sheet_nameNone, dtypeobject) base os.path.splitext(os.path.basename(xlsx_path))[0] for sheet_name, df in sheets.items(): # Sheet名里的空格换成下划线避免文件名里出现空格 safe_sheet sheet_name.replace( , _) out_path os.path.join(OUTPUT_DIR, f{base}_{safe_sheet}.csv) df.to_csv(out_path, indexFalse, encodingutf-8-sig) print(fSheet [{sheet_name}] - {out_path})逻辑说明sheet_nameNone会让pandas把文件里所有Sheet都读进内存返回一个字典键是Sheet名。遍历字典时每个Sheet单独写一个CSV输出文件名用“原文件名_Sheet名.csv”的形式防止多个Sheet互相覆盖。safe_sheet这步处理了Sheet名带空格或特殊字符的情况中文Sheet名会原样保留文件名允许中文只是一些老旧系统可能不识别需要留意。讲到编码这里给一个我日常使用的选择表输出编码适用场景注意事项utf-8-sig下游会用Excel打开文件开头带BOMPython用utf-8读会看到\ufeffutf-8下游是Linux/Python/数据库Excel打开可能乱码gbk老Windows系统、某些国产软件跨平台兼容差说得直接一点不知道下游用什么就用utf-8-sig。它是最不容易翻车的默认值Excel和多数文本工具都能正常显示唯一的代价是文件开头多了3个字节的BOM标记绝大多数场景可以忽略。3.3 CLI入口与自动化参数、默认值和定时任务对接脚本写死目录和参数只能自己用要交给别人或挂上定时任务就需要一个命令行入口。下面这个版本用argparse提供参数默认值保持和前面脚本一致让“不带参数直接跑”依然成立。# 命令行版本可指定输入输出目录、编码、分隔符、Sheet策略 import argparse import glob import os import pandas as pd parser argparse.ArgumentParser(description批量Excel转CSV工具) parser.add_argument(--input, default./excel_files, helpExcel文件所在目录) parser.add_argument(--output, default./csv_files, helpCSV输出目录) parser.add_argument(--encoding, defaultutf-8-sig, help输出CSV编码) parser.add_argument(--delimiter, default,, helpCSV分隔符默认逗号) parser.add_argument(--sheet, defaultall, helpall每个Sheet一个文件或指定Sheet名) args parser.parse_args() os.makedirs(args.output, exist_okTrue) patterns [*.xlsx, *.xls, *.xlsm] for ext in patterns: for xlsx_path in glob.glob(os.path.join(args.input, ext)): # xls老文件由pandas自动选择解析器读法一致 sheets pd.read_excel(xlsx_path, sheet_nameNone, dtypeobject) base os.path.splitext(os.path.basename(xlsx_path))[0] for sheet_name, df in sheets.items(): if args.sheet ! all and sheet_name ! args.sheet: continue safe_sheet sheet_name.replace( , _) out_path os.path.join(args.output, f{base}_{safe_sheet}.csv) df.to_csv(out_path, indexFalse, encodingargs.encoding, separgs.delimiter) print(f已转换: {xlsx_path} Sheet [{sheet_name}] - {out_path})典型的调用方式python batch_xlsx2csv.py --input ./data --output ./out --encoding utf-8-sig --sheet all参数说明--input和--output控制目录--encoding覆盖默认编码--delimiter可以改成\t输出制表符分隔的文件某些数据库导入场景需要--sheet指定只转某个Sheet默认all是全部转出。注意到这里把扩展名匹配扩展成了三种*.xls会命中老格式文件pandas会自动选解析器但前提是环境里装了能读xls的库。和定时任务对接时关键是不要让脚本依赖“当前工作目录”。我在调度里一般写绝对路径python /opt/scripts/batch_xlsx2csv.py --input /data/excel --output /data/csv并在脚本里把os.makedirs(args.output, exist_okTrue)放在目录创建前面避免第一次运行时输出目录不存在直接报错。到这里一个能用的批量转换工具已经成型但真正让它可靠需要把下一章的坑全部踩一遍才知道补哪里。4. 批量转换避坑指南编码、大文件与数据类型的5个典型翻车点这一章全是实际用下来容易翻车的地方。很多问题看起来像“玄学”比如同一个文件在A机器转出来正常在B机器就乱码几万行的文件转换好好的几百万行的直接内存溢出。剥开看每一个都有明确原因和对应解法。4.1 乱码分不清是谁的锅CSV在Excel里乱码、在代码里乱码现象用脚本转出来的CSV用Excel打开全是“锟斤拷”之类的乱码但用Python或VS Code打开正常反过来的场景是Python读别人给的CSV乱码Excel打开却正常。原因这是编码不一致不是数据损坏。脚本写入用的是不带BOM的UTF-8Excel在Windows上默认按本地代码页通常是GBK打开读不到正确字节就会显示乱码。反过来别人用Excel另存为的CSV常常是GBK编码Python默认以UTF-8去读一样乱码。解决输出端用utf-8-sig输入端先检查编码再读取。具体来说批量工具的输出编码统一设置为utf-8-sig读取来源CSV时用chardet或先拿前几个字节判断BOM不能用utf-8硬读。这个判断逻辑要提前做好不要等到下游提工单了才想起来排查。4.2 长数字丢精度和“0”被吞现象身份证号、物料编码这类长数字列转成CSV后变成了1.23457E17以0开头的工号“0157”变成了“157”。列内容看起来丢了但Excel里明明是对的。原因pandas的read_excel默认会对列做类型推断数字列被读成int64或float64写CSV时自然按数值格式输出。Excel单元格里存的文本“0157”一旦被当成数值前导0就无法保留超过15位的整数会转成科学计数法精度已经丢了。解决读取的时候强制按文本。两个办法全列文本或指定列文本# 指定列按文本读取避免长数字转科学计数法 import pandas as pd # 方案一所有列都按object读最省事 df_all_text pd.read_excel(input.xlsx, dtypeobject) # 方案二只对特定列强制文本其余列保持自动推断 df_partial pd.read_excel( input.xlsx, converters{物料编码: str, 身份证号: str} )参数说明dtypeobject的副作用是日期列也会变成字符串日期格式化需要另做处理converters接收一个字典键是列名值是转换函数可以精确只处理出问题的列。注意身份证号这类超过15位的数字即使用str转回文本如果pandas内部已经把它读成float再转str会得到“1.23456789012346e17”所以必须在读取阶段就指定不能事后补救。4.3 大Excel内存爆炸现象一个几十MB的xlsx脚本跑着跑着内存占用飙到2GB以上进程被系统杀掉小文件正常大文件必挂。原因pd.read_excel默认一次性把整个Sheet的数据读进内存DataFrame在内存里的开销远大于文件体积。几十MB的xlsx解压后XML内容可能到几百MB再加上DataFrame的对象引用轻松翻倍。解决改用openpyxl的只读模式逐行读取、逐行写入CSV内存占用控制在几十MB。代码如下# 大文件逐行转CSV避免内存溢出 import csv from openpyxl import load_workbook wb load_workbook(big.xlsx, read_onlyTrue, data_onlyTrue) ws wb.active with open(big.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) # values_onlyTrue 让每一行直接返回单元格的值不返回Cell对象 for row in ws.iter_rows(values_onlyTrue): writer.writerow(row) wb.close()参数说明read_onlyTrue让openpyxl以只读流模式解析不会把整个Sheet建在内存里data_onlyTrue表示取计算后的值而不是公式字符串如果你的Excel里有公式列不加这个参数会把公式文本写进CSV。csv.writer默认会正确处理字段里的逗号、换行和引号。循环结束后调用wb.close()释放文件句柄在跑批任务里这步不写文件多了会报“无法访问”之类的错误。4.4 字段含逗号、换行导致列错位现象地址栏、备注栏里含有英文逗号或换行的Excel转出来的CSV用文本编辑器看着是完整的但导入数据库时列数对不上经常报错“数据列数不匹配”。原因CSV的真正规则是——只有当逗号是“分隔符”时才分割列如果字段本身含逗号整个字段必须用双引号包起来字段里的换行同理。问题大多出在用手拼字符串构造CSV行比如,.join(row)它不会自动加引号于是字段里的逗号成了多余的分隔符。解决永远不要自己拼CSV行用csv.writer或pandas的to_csv。两者都实现了标准引用规则。细节上的差别看这张表引用模式行为适用场景QUOTE_MINIMAL只有字段含分隔符/引号/换行时才加引号通用默认QUOTE_ALL所有字段都加引号下游要求统一引号pandas.to_csv默认就是QUOTE_MINIMAL大多数情况不用改用csv.writer时也可以显式指定quotingcsv.QUOTE_ALL来让每一列都带引号某些数据库导入工具更喜欢这种格式。还有一点需要注意如果用的分隔符不是逗号而是制表符但字段内容里恰好也有制表符同样会有这种问题规则一致。4.5 多Sheet只转出一条、xls老格式读不出现象一个Excel文件里明明有“1月”“2月”“3月”三个Sheet脚本只生成了一个CSV或者遇到.xls扩展名的老文件报错“File format not supported”。原因pd.read_excel不带sheet_name参数时默认读取第一个Sheet这是新手最常见的沉默问题脚本不报错但输出不完整。.xls读不出则更复杂老格式依赖xlrd库而xlrd的新版本已经不支持xlsx如果你用最新环境跑老文件可能连依赖都装不上或者刚装上就发现解析失败。解决读取时显式指定sheet_nameNone拿全部Sheet或者用sheet_name具体名只取目标Sheet。.xls文件则要确认环境里装了兼容xls的读取库版本并且不要把.xls和.xlsx混在一个解析器下处理。我的建议是老文件先在Excel里另存为xlsx再进批处理虽然多一步但能绕开整个xls生态的兼容泥潭如果文件量太大没法手工转就把xls解析单独做一个脚本不跟xlsx混跑这样某个文件挂了不影响主链路。5. 让批量转换具备增量与校验能力按文件状态跳过、失败续跑全量转换跑一次没问题但每天跑就会觉得浪费几百个Excel大部分没变全部重新读、重新写既耗时间还会让下游系统反复收到内容完全相同的文件。增量处理的核心是“先判断文件变没变再决定转不转”。判断依据我用“修改时间文件大小内容哈希”三者都不变就跳过有一个变化就重新转换。下面这个片段可以直接嵌到上一章的命令行版本里# 增量转换记录文件状态只有变化才重新生成CSV import hashlib import json import os STATE_FILE ./convert_state.json def file_hash(path, chunk_size65536): h hashlib.md5() with open(path, rb) as f: while chunk : f.read(chunk_size): h.update(chunk) return h.hexdigest() def should_convert(path, state): stat os.stat(path) key os.path.basename(path) record state.get(key, {}) return ( record.get(size) ! stat.st_size or record.get(mtime) ! stat.st_mtime or record.get(hash) ! file_hash(path) )逻辑说明should_convert对每个Excel文件计算三个特征任意一个与上次记录不同就认为文件发生了变化。mtime和size做快速判断hash兜底内容变化如果只改了文件权限没改内容前两者可能会误判这时哈希结果能拦下来。执行完转换后把最新的三个值写回STATE_FILE供下次运行时加载。失败的转换不要写状态这样同一批文件下次可以重试。实际部署的时候我还会在批量循环里加一层try/except单个文件转换失败就记日志、继续处理下一个而不是让整个批次中断。这个习惯救过我很多次——某个文件锁定了、某个Sheet里混入了非法字符都只是日志里多一行不影响其他文件正常输出。最后说一个我自己的教训早期做批量转换只追求“快”全量直接覆盖输出后来下游系统开始按文件时间做增量同步才发现固定文件内容、不产生无意义的重复变更比转换本身更重要。现在的习惯是任何一次批量执行都先用空目录跑一小批样本确认分隔符、编码、表头、文件名都符合约定再全量执行。转换工具做得越“笨”、越可预期用起来越省心。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询