Java批量转换XLS为TXT/CSV:XLS2TXT工具解析与实战

发布时间:2026/10/24 0:18:16
Java批量转换XLS为TXT/CSV:XLS2TXT工具解析与实战

简介:这份资源是一个基于 Java 的 XLS 转换示例程序,面向需要将 Excel 电子表格批量导出为 TXT 或 CSV 的开发者,尤其适合正在学习 Apache POI 读写 Excel 的初中级 Java 程序员。它通过一个可运行的示例,演示如何借助 Apache POI 的 API 逐行读取 XLS 内容并输出为纯文本或逗号分隔文件,帮助读者理解单元格遍历、数据类型处理与文件写出等核心环节。压缩包内共 2 个文件,包含 1 个 Java 源码文件和 1 个 Markdown 说明文档,源码承载转换逻辑,说明文档交代使用方式与注意事项,整体仅约 2KB,轻量易读。作者在描述中特别指出,示例针对特定格式的 XLS 编写,存在部分硬编码的列号,读者可通过不指定具体列号来规避这一限制,这一提示对实际改造很有参考价值。目前已有 191 人学习,适合作为 Excel 转文本工具的入门参考与二次开发起点。

1. 从一堆“打不开”的 XLS 说起:这个 Java 小工具到底能干什么

手里攒了几十上百个 XLS 文件,来源五花八门——老系统导出的、别人邮件发来的、爬虫落盘的,格式横跨 xls、xlsx,甚至还有改了后缀名的伪 Excel。想批量喂给下游脚本做清洗,第一步就卡住:Python 的 pandas 读老式 xls 要装 xlrd,读 xlsx 要 openpyxl,遇到加密或格式错乱直接抛异常,一个文件崩掉整批任务。XLS2TXT 就是冲着这个场景来的:一个用 Java 写的转换工具,把任意 XLS 文件统一转成 TXT 和 CSV,屏蔽掉底层格式差异,让下游只面对纯文本。它适合做数据预处理、ETL 前置清洗、批量报表归档的人,也适合不想在每台机器上折腾 Python 依赖的 Java 后端。核心价值一句话:把“格式兼容”这件脏活收进一个 jar 里,你只管拿结果。

2. 拆开看:XLS2TXT 的转换链路与 Java 依赖选型

2.1 为什么用 Java 而不是脚本语言

批量文件转换这类活,脚本语言写起来快,但部署时容易翻车。目标机器上 Python 版本、pip 源、C 扩展编译环境,任何一环出问题都得现场救火。Java 的优势在于打包成可执行 jar 后,只要目标机器有 JRE,java -jar就能跑,依赖全在 jar 内部,不存在“我这能跑你那不能跑”的玄学。XLS2TXT 走的就是这条路:把 Apache POI 这类解析库连同业务逻辑一起打进 fat jar,分发时只给一个文件。

另一个理由是并发。批量转换几百个文件时,Java 的线程池和 NIO 文件操作比脚本语言更可控,尤其是要限制内存占用、避免一次性把大表读进堆的情况。常见做法是用固定大小线程池加阻塞队列,生产者扫目录、消费者解析写文件,内存曲线比单线程顺序处理平稳得多。

2.2 解析层:POI 的 HSSF 与 XSSF 怎么选

XLS 分两代:老式.xls是二进制格式,对应 POI 的 HSSF;新式.xlsx是 OOXML 压缩包,对应 XSSF。XLS2TXT 要“通吃”,就得在入口处判断文件真实类型,而不是看后缀。判断方法读文件头魔数:.xls开头是D0 CF 11 E0,.xlsx开头是50 4B 03 04(也就是 ZIP 的 PK 头)。只看后缀会把改了名的文件送错解析器,直接抛OfficeXmlFileException或NotOLE2FileException。

// 通过文件头魔数判断真实格式,避免被错误后缀误导 public static WorkbookType detectType(File file) throws IOException { try (FileInputStream fis = new FileInputStream(file)) { byte[] header = new byte[8]; int n = fis.read(header); if (n < 4) return WorkbookType.UNKNOWN; // xls 的 OLE2 复合文档头 if (header[0] == (byte) 0xD0 && header[1] == (byte) 0xCF && header[2] == (byte) 0x11 && header[3] == (byte) 0xE0) { return WorkbookType.XLS; } // xlsx 本质是 zip,PK 头 if (header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04) { return WorkbookType.XLSX; } return WorkbookType.UNKNOWN; } }

这段逻辑的关键在header数组的前四字节比对。0xD0CF11E0是 OLE2 复合文档的固定签名,0x504B0304是 ZIP 本地文件头。参数上唯一要注意的是读取长度:至少读 4 字节,读 8 字节是为了留余量。如果文件小于 4 字节,直接判为 UNKNOWN,不要硬解析,否则 POI 会抛底层异常,堆栈很难看。

2.3 写出层:TXT 与 CSV 的编码和分隔符陷阱

转换的终点是文本,但文本有两个隐形坑:编码和分隔符。TXT 输出建议统一 UTF-8,带不带 BOM 取决于下游。如果下游是 Windows 上的 Excel 直接打开,不带 BOM 的 UTF-8 中文会乱码,这时加 BOM 是常见做法;如果下游是 Linux 脚本或数据库导入,BOM 反而会污染第一列字段名。XLS2TXT 一般把编码做成可配参数,默认 UTF-8 无 BOM。

CSV 的坑更集中。字段里如果本身含逗号、换行、双引号,必须按 RFC 4180 转义:整个字段用双引号包起来,字段内的双引号写成两个双引号。很多手写转换器只做join(","),遇到地址栏里的逗号就列错位,这种翻车在批量数据里特别隐蔽,往往到下游才发现。

// CSV 字段转义:处理逗号、换行、双引号 public static String escapeCsv(String field) { if (field == null) return ""; boolean needQuote = field.contains(",") || field.contains("\n") || field.contains("\r") || field.contains("\""); if (!needQuote) return field; // 字段内双引号翻倍,再整体加引号 return "\"" + field.replace("\"", "\"\"") + "\""; }

needQuote判断四种情况:逗号、换行、回车、双引号。只要命中一种就加引号包裹。replace("\"", "\"\"")把单个双引号替换成两个,这是 RFC 4180 的硬性要求。参数上没什么可调的,但要注意顺序:先判断再替换,不能先替换后判断,否则原本不含引号的字段会被误加引号。

2.4 空单元格与合并单元格的处理策略

Excel 里空单元格和合并单元格是转换质量的分水岭。POI 遍历行时,row.getCell(i)对不存在的单元格返回 null,直接toString()会 NPE。合并单元格更麻烦:只有左上角那个格子有值,其余返回空。如果直接按行列输出,合并区域会变成一堆空字段,下游根本还原不出原表结构。

常见做法是先把合并区域读出来,建一个坐标到值的映射,遍历时优先查映射,查不到再取单元格本身的值。空单元格统一输出空字符串,而不是"null"或跳过整行——跳过会导致行号错位,下游对不上原始行。

// 预读合并区域,建立 (row,col) -> value 映射 Map<String, String> mergedMap = new HashMap<>(); for (int i = 0; i < sheet.getNumMergedRegions(); i++) { CellRangeAddress region = sheet.getMergedRegion(i); Row firstRow = sheet.getRow(region.getFirstRow()); if (firstRow == null) continue; Cell firstCell = firstRow.getCell(region.getFirstColumn()); String val = firstCell == null ? "" : getCellString(firstCell); // 把整个合并区域每个坐标都填上左上角的值 for (int r = region.getFirstRow(); r <= region.getLastRow(); r++) { for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) { mergedMap.put(r + "," + c, val); } } }

CellRangeAddress给出合并区域的起止行列。mergedMap的 key 用"行,列"拼接,简单但够用。填充时把整个矩形区域每个坐标都映射到左上角的值,这样遍历时无论走到哪个格子都能拿到正确内容。代价是合并区域大时内存占用上升,一般表格规模下可以接受。

3. 动手跑起来:命令行参数、批量模式与输出配置

3.1 环境准备与最小可运行示例

跑 XLS2TXT 的前提是目标机器有 JRE 8 或以上。先确认版本:

java -version # 期望输出类似 openjdk version "1.8.0_xxx" 或更高

如果版本低于 8,POI 较新版本会直接报UnsupportedClassVersionError。确认后,最小运行命令是单文件转换:

# 单文件转换:输入 xls,输出同名 txt 和 csv java -jar xls2txt.jar -i ./data/report.xls -o ./out -f txt,csv

参数说明:-i指定输入文件,可以是单个文件也可以是目录;-o指定输出目录,不存在会自动创建;-f指定输出格式,逗号分隔,支持txt、csv,不传默认两个都出。这里有个细节:输出文件名默认沿用输入文件名,只换后缀,所以report.xls会生成report.txt和report.csv,不会覆盖原始文件。

3.2 批量转换与并发参数怎么设

批量模式把-i指向目录即可,工具会递归扫描。并发相关参数一般有三个:线程数、批大小、是否跳过已存在输出。

# 批量转换:4 线程,每批 50 个文件,跳过已转换的 java -jar xls2txt.jar -i ./data -o ./out -f csv \ --threads 4 --batch-size 50 --skip-existing

--threads控制解析线程数,经验值是 CPU 核数的 1 到 2 倍。设太大反而因为磁盘 IO 争抢变慢,设太小又吃不满多核。--batch-size是每批提交给线程池的任务数,影响内存峰值:批越大,同时驻留的 Workbook 对象越多。--skip-existing在断点续跑时特别有用,输出文件已存在就跳过,避免重复劳动。如果内存吃紧,把--batch-size降到 10 到 20,配合-Xmx限制堆大小。

# 限制堆内存 512M,适合小内存机器跑大批量 java -Xmx512m -jar xls2txt.jar -i ./data -o ./out --threads 2 --batch-size 10

-Xmx512m是 JVM 参数,必须放在-jar之前,放后面会被当成程序参数。这是新手最容易踩的坑之一,命令不报错但参数没生效。

3.3 输出格式的细粒度控制

TXT 和 CSV 的输出行为可以通过参数微调。常见的有分隔符、编码、是否输出表头、空行处理。

参数作用默认值建议
--delimiterCSV 列分隔符,数据含逗号多时改\t
--encoding输出编码UTF-8Windows 下游可改GBK
--bom是否写 BOMfalseExcel 直接打开时设true
--header是否输出首行表头true无表头数据设false
--sheet指定工作表全部多 sheet 时按名或索引选
# 输出制表符分隔、带 BOM、只转第一个 sheet java -jar xls2txt.jar -i ./data -o ./out -f csv \ --delimiter "\t" --bom true --sheet 0

--delimiter "\t"在 shell 里要写成转义形式,实际传入的是制表符。--sheet 0表示只转索引为 0 的工作表,多 sheet 文件默认会全部转出,文件名加 sheet 名后缀,如果 sheet 名含非法字符会被替换成下划线。

3.4 转换结果怎么验证

转完不能直接信,得抽验。最直接的办法是比对行数和抽样字段。行数用wc -l快速看:

# 统计输出 CSV 行数,和原始表预期行数比对 wc -l ./out/report.csv

行数对不上通常有三种原因:空行被跳过、合并单元格展开导致行数变化、或者原表有隐藏行。抽样验证建议挑含特殊字符的字段,比如地址、备注,看 CSV 里是否正确转义。如果下游是数据库,直接建临时表导入,让数据库的严格模式帮你抓格式错误,比肉眼靠谱。

4. 避坑指南:批量转换里最容易翻车的五个点

4.1 现象:转换报 NotOLE2FileException,但文件后缀是 xls

原因:文件实际是 xlsx 或 HTML 伪装的,只是后缀被改成了 xls。POI 按后缀选了 HSSF 解析器,读到 ZIP 头直接抛异常。

解决:入口处用 2.2 节的魔数判断真实类型,不要信后缀。如果魔数既不是 OLE2 也不是 ZIP,尝试按纯文本或 HTML 表格解析,仍失败就记录到错误清单,不要中断整批任务。

4.2 现象:中文全部乱码,英文正常

原因:输出编码和下游读取编码不一致。工具默认 UTF-8,下游用 GBK 打开,中文必然乱码。

解决:确认下游编码。Windows Excel 直接打开就加--bom true并保持 UTF-8;老系统只认 GBK 就设--encoding GBK。不要两头都改,改一头对齐即可。

4.3 现象:CSV 列错位,某些行字段数对不上

原因:字段内含逗号或换行,没做 RFC 4180 转义,被下游按分隔符切碎了。

解决:确保写出层走了 2.3 节的escapeCsv。已经转好的文件可以用脚本补救,但更稳的是重转。验证方法是统计每行字段数,出现不一致就说明有未转义字段。

4.4 现象:大批量跑到一半 OOM

原因:--batch-size太大,或线程数太多,同时驻留的 Workbook 对象撑爆堆内存。POI 的 XSSF 尤其吃内存,因为它会把整个 xlsx 解压进内存。

解决:降--batch-size到 10 到 20,降--threads,并显式设-Xmx。如果单个文件本身就巨大,考虑换 POI 的流式 API(SXSSF 用于写,XSSFReader 用于读),但那是改造工具本身的活。

4.5 现象:合并单元格转出来一片空白

原因:只取了左上角单元格,其余坐标返回 null 或空,没有做区域填充。

解决:按 2.4 节预读合并区域建映射。注意映射要在遍历前建好,遍历中动态查 POI 的getMergedRegion性能很差,大表会明显变慢。

5. 进阶:把 XLS2TXT 嵌进自己的 Java 流水线

工具用命令行跑只是入门,真正省事的是把它当库嵌进现有 Java 项目。核心是把转换逻辑抽成一个服务类,输入File,输出ConversionResult,里面带输出路径、行数、耗时和错误信息。这样你可以用自己的调度框架控制并发,用日志框架收集错误,而不是解析命令行输出。

// 把转换封装成可复用服务,便于嵌入现有流水线 public class XlsConversionService { public ConversionResult convert(File input, File outDir, ConvertOptions opts) { long start = System.currentTimeMillis(); ConversionResult result = new ConversionResult(); result.setInput(input); try { WorkbookType type = TypeDetector.detectType(input); if (type == WorkbookType.UNKNOWN) { result.setError("无法识别的文件格式"); return result; } try (Workbook wb = WorkbookFactory.create(input)) { // 按配置逐 sheet 写出,统计行数 int rows = SheetWriter.write(wb, outDir, opts); result.setRowCount(rows); } } catch (Exception e) { result.setError(e.getMessage()); } result.setCostMs(System.currentTimeMillis() - start); return result; } }

WorkbookFactory.create会自动根据文件内容选 HSSF 还是 XSSF,省去手动分支。ConvertOptions封装编码、分隔符、sheet 选择等参数,和命令行参数一一对应。ConversionResult把错误吞进对象而不是抛异常,这样批量处理时单个文件失败不影响整批,错误集中收集后统一处理。

验证嵌入效果有个实用技巧:拿一批已知结果的样本文件跑回归,比对输出文件的 MD5。转换逻辑稳定的话,同样输入同样参数,输出应该逐字节一致。一旦某次改动导致 MD5 变化,说明写出行为变了,得查是转义逻辑动了还是编码默认值改了。

我自己的习惯是,任何批量转换任务上线前,先拿 20 个覆盖各种边界(空表、合并单元格、含逗号字段、超大表、伪后缀)的样本跑一遍,把结果和预期比对通过再放量。这个习惯帮我拦下过好几次“看着能跑、实际列错位”的问题。从那以后我每次改写出逻辑都强制走一遍样本回归,不敢省。希望帮到你。

本文还有配套的精品资源,点击获取