简介一份面向 Java 开发者的轻量示例代码聚焦用 Apache POI 将 XLS 电子表格转换为 TXT 或 CSV 的常见需求适合需要快速了解表格解析与文本导出流程的初中级开发者参考。压缩包共 2 个文件包含 1 个 Java 源码文件与 1 个 Markdown 说明文件整体约 2KBJava 文件给出核心转换示例Markdown 文件用于补充使用说明与背景信息体量小巧便于直接查阅。示例代码针对某些特定格式的 XLS 编写因此存在少量硬编码数字描述中也提示可通过不指定特定列号来规避限制这一细节对改造适配不同表格结构有参考意义。已有 191 人学习读者可从中了解基于 POI API 读取工作簿、处理单元格并输出 TXT/CSV 的基本思路并借助源码与说明文档快速定位可复用的实现片段。1. 从 300 个旧 XLS 说起XLS2TXT 到底替谁省时间某公司数据迁移前夜业务同事丢来 300 多个 .xls结构横七竖八目标系统只认 TXT 和 CSV。手点 Excel 另存为既慢又容易把日期、前导零和中文编码弄丢。XLS2TXT 就是冲这个场景来的一个基于 Java 的 XLS 转 TXT/CSV 工具能把单文件或整目录的旧表格批量落成文本适合做数据清洗前的第一道粗加工。它不是 ETL 平台别指望它建表、洗脏数据它的价值是把 Excel 这层壳剥掉让后面的脚本、数据库导入和比对工具能接手。2. 拆开 XLS2TXT 的 Java 底座BIFF8 记录、POI 与单元格类型2.1 .xls 不是改了后缀的表格OLE2、BIFF8 与 HSSF 的边界很多人拿到 .xls 第一反应是改后缀为 .xlsx再用新库读结果直接报 Invalid header signature。原因不复杂.xls 是 OLE2 复合文档内部按 BIFF8 记录流存工作表、格式、公式.xlsx 则是 ZIP 包加 XML。文件头就能区分OLE2 开头通常是D0 CF 11 E0 A1 B1 1A E1ZIP 开头是50 4B 03 04。XLS2TXT 这类 Java 工具底层常见做法是 Apache POIHSSF 读 .xlsXSSF 读 .xlsxSXSSF 只解决写大文件。HSSF 的边界要提前知道一个 sheet 最多 65536 行、256 列BIFF5 的老文件支持不完整加密文件不额外处理就会在打开阶段失败。标题里写“任何 XLS”实际落地时要分几种情况标准 BIFF8 的 .xls 最稳从旧系统导出的 BIFF5 可能丢格式把 HTML 表格或 XML 内容直接改成 .xls 后缀的“假 Excel”POI 会在文件头校验时拒绝带打开密码的文件要先让业务方另存为无密码版本别在转换脚本里硬啃解密。选型上如果只处理 .xlsHSSF 足够如果同一批目录里混着 .xls 和 .xlsx直接用WorkbookFactory按文件头自动分派比按后缀判断可靠。XLS2TXT 如果提供的是源码包先看它依赖的是 POI 还是 jxljxl 对 BIFF8 够用但对新格式和复杂公式的支持面窄后期二次开发容易卡住。还有一个容易忽略的点Excel 的“空行”不一定是空行。行对象存在、单元格对象不存在、单元格存在但类型是 BLANK这三种状态在转换时行为不同。XLS2TXT 输出 TXT 时如果直接按row.getLastCellNum()截断可能把中间的空列吃掉如果按固定列数补齐又可能给下游多塞空字段。我的做法是先用最小读取器把 sheet 名、有效行数、每行列数和单元格类型打印出来确认源文件没有“假空行”和“合并单元格残影”再决定批量转换的列策略。2.2 先写一个最小读取器把单元格类型、日期和公式看清楚在把整批文件交给 XLS2TXT 之前我一般会先用二十行 Java 写一个读取器确认这个文件到底能不能被 POI 正常打开、日期列是不是真日期、公式列有没有缓存值。下面这段代码不依赖 XLS2TXT 的具体 API只做“体检”import org.apache.poi.ss.usermodel.*; import java.io.File; public class XlsInspector { public static void main(String[] args) throws Exception { File file new File(args[0]); // WorkbookFactory 按文件头选 HSSF 或 XSSF避免手动判断后缀 try (Workbook wb WorkbookFactory.create(file)) { DataFormatter formatter new DataFormatter(); FormulaEvaluator evaluator wb.getCreationHelper().createFormulaEvaluator(); for (Sheet sheet : wb) { System.out.println(SHEET sheet.getSheetName()); for (Row row : sheet) { StringBuilder line new StringBuilder(); for (Cell cell : row) { // 公式单元格先看缓存结果类型避免直接读成公式字符串 if (cell.getCellType() CellType.FORMULA) { CellType cached cell.getCachedFormulaResultType(); if (cached CellType.NUMERIC DateUtil.isCellDateFormatted(cell)) { line.append(formatter.formatCellValue(cell, evaluator)).append(\t); continue; } } // DataFormatter 尽量按单元格显示格式输出日期和数字都受益 String text formatter.formatCellValue(cell, evaluator); line.append(text).append(\t); } System.out.println(line); } } } } }逻辑说明WorkbookFactory.create(File)是入口它根据文件头而不是后缀选择 HSSF 或 XSSFDataFormatter负责把单元格按 Excel 里看到的格式转成字符串日期、百分比、千分位都靠它FormulaEvaluator用来计算公式但公式单元格本身可能带缓存值所以先看getCachedFormulaResultType()避免把公式文本直接写进 TXT。参数上args[0]传单个 .xls 或 .xlsx 路径输出用制表符分隔方便先肉眼比对如果源文件有公式且缓存值为空evaluator可能抛异常这时要在外层 catch 里记录文件名和 sheet 名不要静默跳过。这段代码跑通至少说明三件事文件头合法、POI 能打开、单元格类型可枚举。后面用 XLS2TXT 批量转换时如果某个文件输出异常我会先拿它跑一遍这个读取器。读取器也报错问题在源文件读取器正常而 XLS2TXT 输出错问题在转换参数或输出编码。这个顺序能省掉大量“到底是文件坏还是工具坏”的拉扯。3. 把单元格落成文本UTF-8/GBK 编码、分隔符与转义三件事3.1 编码选型UTF-8、GBK、GB18030 与 BOM 的取舍XLS2TXT 把内容从 Workbook 里读出来之后最终要落到字节流。Java 里常见写法是OutputStreamWriter包FileOutputStream如果不显式传 charset就会吃平台默认编码。Windows 上默认可能是 GBKLinux 容器里可能是 UTF-8同一份转换脚本在两台机器上跑出两种结果这种翻车现场我见过不止一次。稳妥做法是在配置里把输出编码写成显式参数TXT 和 CSV 分别可配日志里打印实际使用的 charset 名称。落地场景建议编码关键动作Windows Excel 双击打开 CSVGBK 或 GB18030不加 BOM避免首列带隐藏字符Linux/容器里的导入脚本UTF-8显式指定 charset不依赖file.encoding跨系统交换、含生僻字UTF-8 或 GB18030先确认接收端字符集别只看文件能打开老数据库客户端GBK用Charset.forName(GBK)别用平台默认BOM 要单独说。给 Excel 双击用的 UTF-8 CSV加 BOM 能减少中文乱码但很多解析器、数据库导入工具、Spark 读 CSV 时会把 BOM 当成第一列列名的一部分导致“字段名对不上”。我的习惯是面向人看的 CSV 可以加 BOM面向程序导入的 CSV 不加TXT 一般不加。XLS2TXT 如果提供“编码”和“BOM”两个选项不要只改编码不检查 BOM最好在输出后用十六进制看一眼文件头是不是EF BB BF。注意-Dfile.encodingUTF-8只影响 JVM 默认字符集不能替代代码里的显式 charset。转换工具内部如果用了new String(bytes)或FileReader仍然可能按平台默认解码排查时要看源码里有没有硬编码。3.2 分隔符与转义逗号、制表符、换行和引号怎么处理CSV 不是“用逗号把字段拼起来”这么简单。单元格里出现逗号、双引号、换行时必须按 RFC 4180 的思路处理含分隔符、引号或换行的字段用双引号包起来字段内部的双引号写成两个双引号。TXT 如果用制表符分隔规则类似只是分隔符换成\t。XLS2TXT 的输出配置里通常会有分隔符、引号字符、是否全部加引号这几个参数别嫌麻烦先把转义逻辑跑通再批量。import java.io.*; import java.nio.charset.*; import java.util.*; public class TextExporter { public static void writeRow(BufferedWriter writer, ListString cells, char delimiter, char quote, boolean quoteAll) throws IOException { StringBuilder line new StringBuilder(); for (int i 0; i cells.size(); i) { if (i 0) line.append(delimiter); String value cells.get(i) null ? : cells.get(i); boolean needQuote quoteAll || value.indexOf(delimiter) 0 || value.indexOf(quote) 0 || value.indexOf(\n) 0 || value.indexOf(\r) 0; if (needQuote) { line.append(quote); line.append(value.replace(String.valueOf(quote), String.valueOf(quote) quote)); line.append(quote); } else { line.append(value); } } writer.write(line.toString()); writer.write(\n); // 固定 LF避免 Windows/Linux 混跑时行尾不一致 } public static void main(String[] args) throws Exception { Charset charset StandardCharsets.UTF_8; // 给 Windows Excel 双击时可换成 Charset.forName(GBK) try (BufferedWriter writer new BufferedWriter( new OutputStreamWriter(new FileOutputStream(out.csv), charset))) { writeRow(writer, Arrays.asList(订单号, 金额, 备注), ,, , false); writeRow(writer, Arrays.asList(A001, 12.50, 含逗号,和\引号\), ,, , false); } } }逻辑说明writeRow负责一行的拼接和转义delimiter是分隔符CSV 用,TXT 用\tquote是引号字符通常用双引号quoteAll为 true 时所有字段都加引号适合字段内容不可控、下游解析器又比较挑剔的场景。value.indexOf(delimiter)检查字段里有没有分隔符value.indexOf(quote)检查有没有引号\n和\r检查换行。替换时把单个引号变成两个引号这是 CSV 的标准转义。writer.write(\n)固定 LF不让平台默认行尾混进来如果下游明确要求 CRLF再改成\r\n。参数之外还有两个细节。第一数值格式如果单元格是数值型DataFormatter可能输出12.5而不是12.50金额列要在转换前确认单元格格式或者在导出后统一补小数位。第二前导零订单号000123如果被 Excel 存成数值转出来就是123这不是 XLS2TXT 的锅是源文件列格式的问题要么让业务方把列设成文本要么在转换配置里按列强制字符串读取。批量转换前先抽 20 行看这两类字段比整批跑完再后悔便宜得多。4. 批量转换流水线目录扫描、命名隔离与失败重试4.1 先规划输出目录相对路径、同名文件和覆盖策略单文件转换用 GUI 点两下就行几百个文件再手点就是体力活。XLS2TXT 如果提供命令行入口可以用 shell 循环如果只提供 Java 调用入口就写一个小批量调度器。无论哪种第一步都是把输入目录和输出目录分开输出路径按输入文件的相对路径映射避免所有 CSV 平铺在一个目录里互相覆盖。下面这段代码只做文件发现和输出路径计算import java.nio.file.*; import java.util.*; import java.util.stream.*; public class BatchPlanner { public static ListPath findXls(Path root) throws IOException { try (StreamPath stream Files.walk(root)) { return stream.filter(Files::isRegularFile) .filter(p - { String name p.getFileName().toString().toLowerCase(Locale.ROOT); return name.endsWith(.xls) || name.endsWith(.xlsx); }) .sorted() .collect(Collectors.toList()); } } public static Path toOutput(Path root, Path outputRoot, Path xls, String ext) { Path relative root.relativize(xls); String name relative.toString().replaceAll((?i)\\.(xls|xlsx)$, . ext); return outputRoot.resolve(name); } }逻辑说明Files.walk(root)递归扫描try-with-resources保证流关闭过滤条件同时接受.xls和.xlsx大小写用Locale.ROOT避免土耳其语环境下的I问题sorted()让输出顺序稳定方便两次转换做校验和比对。toOutput用root.relativize(xls)保留子目录结构replaceAll把扩展名换成csv或txt。参数上root是输入根目录outputRoot是输出根目录ext不带点传csv或txt。这里有个坑同一目录下如果同时有a.xls和a.xlsx按上面的规则都会映射到a.csv后跑的覆盖先跑的。解决方式有三种输出名加源扩展名a.xls.csv、加短哈希、或者按输入子目录再分一层。我一般选第一种名字丑一点但排查时一眼能看出源文件是谁。覆盖策略也要显式化批量任务默认REPLACE_EXISTING方便重跑但第一次跑之前先确认输出目录是空的别把上一次的结果和这一次混在一起。4.2 失败重试与日志让坏文件停在原地不拖垮整批批量转换最怕一个坏文件抛异常整个循环中断前面跑完的不知道跑到哪后面没跑的也没人知道。正确做法是每个文件独立 try-catch失败记录到日志必要时重试一次。重试只对临时性错误有意义比如文件被占用、磁盘瞬时满、网络盘抖动文件头损坏、密码保护、格式不支持这类错误重试一百次也没用日志里要能区分。import java.nio.file.*; import java.util.concurrent.TimeUnit; public class RetryRunner { public static boolean convertWithRetry(Path input, Path output, int maxAttempts) { for (int attempt 1; attempt maxAttempts; attempt) { try { convertOnce(input, output); // 替换成 XLS2TXT 的转换入口 return true; } catch (Exception e) { System.err.println(FAIL input input attempt attempt msg e.getMessage()); if (attempt maxAttempts) { try { TimeUnit.MILLISECONDS.sleep(300L * attempt); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); return false; } } } } return false; } private static void convertOnce(Path input, Path output) throws Exception { // 这里调用 XLS2TXT 的单文件转换方法若是命令行包可用 ProcessBuilder 调 java -jar throw new UnsupportedOperationException(替换为实际转换入口); } }逻辑说明maxAttempts是最大尝试次数建议设为 2 或 3不要无限重试300L * attempt是线性退避第一次失败等 300ms第二次等 600ms给文件锁和网络盘一点恢复时间InterruptedException要恢复中断位不要吞掉。convertOnce是占位方法实际使用时替换成 XLS2TXT 的 service 方法或者用ProcessBuilder调它的命令行入口。日志至少记四个字段输入路径、输出路径、尝试次数、异常摘要批量跑完再按输入路径排序和源目录做差集就知道哪些文件没转成功。如果 XLS2TXT 只提供 GUI没有命令行也可以用 Java 的ProcessBuilder调java -jar但参数要提前在单文件上验证别在循环里猜。批量任务最好串行跑不要一上来就开 8 个线程POI 读 .xls 是内存密集型并发太高容易 OOM而且日志交错后很难定位。我的习惯是先用 5 个文件试跑确认输出目录结构、编码、分隔符都正确再放开整批每批控制在 50 到 100 个文件跑完做一次行数抽查。5. 转换翻车现场5 个 XLS2TXT 高频问题排查5.1 输出全是乱码编码没对齐还是 BOM 在捣乱现象XLS2TXT 转出来的 CSV 用 Excel 打开正常用 Linux 的less或数据库导入工具打开全是问号或者反过来Linux 下正常Windows Excel 打开乱码。原因通常是输出编码和接收端编码不一致工具按 UTF-8 写Excel 按 GBK 解或者工具按平台默认 GBK 写下游按 UTF-8 解。解决先确认 XLS2TXT 的输出编码参数显式设成 UTF-8 或 GBK不要留空再用file -i out.csv或十六进制看文件头UTF-8 BOM 是EF BB BF有 BOM 而下游不认就去掉。给 Excel 双击的 CSV 用 GBK 最省事给程序导入的 CSV 用 UTF-8 且不加 BOM两边都别靠猜。5.2 日期变成 45000单元格格式没被读取现象源表里是2024-01-01转出来变成45292或45292.0。原因Excel 的日期在文件里存的是序列号显示成日期靠的是单元格格式XLS2TXT 如果只读数值、不读格式就会把序列号直接落成文本。解决转换前用DateUtil.isCellDateFormatted(cell)判断配合DataFormatter.formatCellValue按格式输出如果工具本身不处理日期就在后处理脚本里按列做序列号转日期注意 Excel 的 1900 闰年 bug 和 1904 日期系统。更稳的做法是让业务方在源文件里把日期列统一成文本yyyy-MM-dd再跑转换别把格式问题留到下游。5.3 长数字变科学计数法double 精度和显示格式的锅现象身份证号、订单号、银行卡号转出来变成1.23457E17或者后三位变成000。原因Excel 数值型单元格底层是 double超过 15 位有效数字就会丢精度XLS2TXT 如果按数值读取再转字符串科学计数法和精度丢失都会出现。解决转换前检查源列格式能改文本就改文本不能改的话用DataFormatter按显示格式读或者按列配置“强制字符串”模式。批量转换后抽一批长数字做长度校验18 位变 15 位、末位变 0 的直接打回源文件不要在输出端做补救。5.4 公式单元格为空或结果旧缓存值没刷新现象源表里公式列明明有值转出来是空字符串或者还是上一次计算的旧值。原因POI 默认不重新计算公式只读文件里缓存的公式结果如果文件由程序生成、缓存值没写或者公式引用了外部工作簿缓存值就可能为空或过期。解决用FormulaEvaluator在读取时计算公式或者让业务方在 Excel 里打开、全量重算、另存为值外部引用和循环引用别硬算先在源文件里拆掉。XLS2TXT 如果提供“计算公式”开关先拿一个包含SUM、VLOOKUP、跨 sheet 引用的样本试确认结果和 Excel 里一致再批量。5.5 大文件 OOMHSSF 全量加载与 65536 行上限现象几十兆的 .xls 转到一半报OutOfMemoryError或者超过 65536 行的部分直接消失。原因HSSF 把整个工作簿加载进内存.xls 本身又限制单 sheet 65536 行、256 列文件里如果有很多样式、合并单元格、图片内存占用还会更高。解决先调 JVM 参数-Xmx2g或更高确认不是内存太小再拆文件按 sheet 或按行段拆成多个 .xls如果源文件其实是 .xlsx换 XSSF 或流式读取别用 HSSF 硬扛。转换日志里记录每个文件的 sheet 数和行数超过 6 万行的提前预警别等跑完才发现少了后半截。6. 交付前最后一公里抽样比对、行数校验与校验和批量转换跑完不等于能交付。我一般会做三层校验第一层看行数和列数第二层看关键字段的汇总值第三层抽 20 行做逐列比对。行数用wc -l和源表有效行对比少一行可能是空行被吞多一行可能是表头重复列数看首行分隔符数量多一列少一列通常是转义或空列处理出了问题。金额列做sum日期列取min/max长数字列检查长度分布这三项能抓住大部分格式性错误。校验项做法失败信号行数wc -l对比源表有效行少一行可能是空行/合并单元格列数抽查首行分隔符数量多一列少一列通常是转义金额合计对源表与输出做 sum差异说明数值格式或精度日期范围取 min/max变成序列号或空值抽样 20 行diff 关键列中文、引号、换行校验和重复转换后 sha256不稳定说明排序/时间戳# 输出文件行数与校验和用于两次转换结果比对 wc -l out.csv sha256sum out.csv # 抽样前 21 行检查中文、逗号和引号 sed -n 1,21p out.csvwc -l给的是物理行数如果单元格里有换行CSV 的物理行数会大于逻辑行数所以它只能做粗筛不能当唯一标准sha256sum适合验证“同一批输入、同一套参数”下输出是否稳定如果两次转换校验和不同先查文件排序、时间戳、随机 ID 这些不稳定因素sed -n 1,21p是抽样重点看中文有没有乱码、含逗号的字段有没有被引号包住、引号有没有双写。校验脚本不要写得太复杂能重复跑、能输出差异文件路径就行。从那以后我每次批量转换前都强制走一遍“单文件体检、20 行抽样、行数与金额汇总”这三步哪怕业务方催得再急也不跳过。XLS2TXT 能把重复劳动压下去但源文件的格式债不会自动消失早发现比晚返工便宜。希望帮到你。本文还有配套的精品资源点击获取