简介:在Java企业级开发中,用程序解析Word文档并写入数据库是常见需求。这份资源恰好呈现了完整实现:基于Apache POI读取.docx文件内容,再通过JDBC持久化到数据库,适合希望提升数据录入效率、强化文件I/O与数据库编程能力的中级Java工程师。压缩包共41个文件,约13.24MB,核心包括11个jar依赖库、9个java示例源码,以及class编译产物、xml与properties配置和工程描述文件,结构清晰,可直接导入IDE运行调试。目前已有5138人学习下载。研读源码可掌握XWPFDocument遍历Word表格与文本、PreparedStatement预编译防SQL注入、批量插入优化及异常处理等关键写法;同时项目自带数据库配置与Eclipse工程设置,便于理解从文件解析到数据落库的完整链路。整体上,该资源既提供可复用的工具脚本思路,也展示了企业级数据处理的规范流程,对巩固Java后端技能有实际帮助。
1. Java 读取 Word 上传数据库:先把这条链路完整跑通
某部门把近三年的立项书 Word 文件堆在共享目录里,要求写个 Java 程序批量读取 Word 文件并上传到数据库,方便后续检索与统计。听起来简单,真正做起来才发现,“读取 Word”和“上传数据库”中间卡着一层解析稳定性:docx 能读到正文,doc 可能乱码,表格字段还会错位。这篇笔记记录我在模拟项目X里用 POI 拆解这套过程的完整方案,从解析选型、代码实现到落库,再到高频踩坑依次走通。适合要快速落地文档入库、或给知识库喂结构化文本的开发者参考。
2. Word 解析选型:POI、docx4j、Tika 谁更适合入库场景
做文档入库,第一步不是写代码,而是先想清楚用什么库去拆 Word。这里有个容易被低估的前提:Word 有两种完全不同的底层格式,选型一旦选错,后面所有人都得跟着返工。
2.1 两种 Word 格式:解析内核完全不同
.doc 是 OLE 复合文档(老式二进制容器),.docx 是 OOXML 标准下的 ZIP 包,里面装着多个 XML 文件。这意味着不能用同一个 API 通吃两种格式,扩展名只是第一层判断,文件内部结构才是解析器真正面对的东西。
拿一个 .docx 文件举例,它的基本骨架大致是这样的:
| 包内路径 | 存放内容 |
|---|---|
| word/document.xml | 正文主体:段落、表格、图片引用 |
| word/media/ | 图片二进制文件 |
| word/header1.xml / footer1.xml | 页眉、页脚 |
| word/styles.xml | 样式定义:标题、正文、默认字体 |
.doc 的 OLE 结构里没有这种清晰的文件树,而是通过目录流去定位各个组件,读取逻辑复杂得多。这也是为什么在 Java 生态里,POI 同时给出了两套 API:HWPF 管 .doc,XWPF 管 .docx。
我一般会先按文件名做一次初筛,再用文件头做二次确认:
String lowerName = fileName.toLowerCase(); try (InputStream in = new FileInputStream(file)) { if (lowerName.endsWith(".docx") || isZipHeader(in)) { XWPFDocument doc = new XWPFDocument(in); return parseDocx(doc); } else if (lowerName.endsWith(".doc") || isOleHeader(in)) { HWPFDocument doc = new HWPFDocument(in); return parseDoc(doc); } throw new IllegalArgumentException("不是 Word 文件: " + fileName); }这里有个血泪经验:只判断后缀名很不保险,很多 WPS 导出的文件后缀是 .doc,实际内容却是 OOXML 结构,或者反过来。常见做法是先读文件头几个字节,ZIP 格式以50 4B开头,OLE 复合文档以D0 CF 11 E0开头,判断命中后再交给对应解析器,能省掉后面一大半乱码和解体问题。
2.2 XWPF 与 HWPF 的分工与能力边界
XWPF 的处理对象是 ZIPPED XML,解析正文、表格、页眉页脚都相对成熟,而且仍在持续更新。HWPF 处理 .doc 的老式二进制结构,能力明显弱一截,尤其是表格遍历、图片抽取、复杂样式还原这类需求,API 多年没有大变化,遇到文件里嵌一个老式文本框就可能读不出完整内容。
我的策略是:入库场景优先把 .doc 里值得长期保存的内容转成 .docx 再做解析,而不是硬啃 HWPF。批量转换的常见做法是用 LibreOffice 无头模式,一条命令就能把整个目录里的旧格式转掉:
soffice --headless --convert-to docx --outdir /data/target /data/source转换结果取决于源文件本身的质量,字体编码、域代码、复杂文本框都可能出现偏差,所以转换完不要直接入库,先进入下一步的解析流程统一校验。
2.3 POI、docx4j、Tika 三套方案对比
我把这三个方案放在一起做过对比测试,结论比较明确:
| 方案 | 格式支持 | 最合适的场景 | 主要限制 |
|---|---|---|---|
| POI | .doc + .docx | 需要正文、表格、图片结构化入库 | API 偏底层,代码量稍大 |
| docx4j | 仅 .docx | 需要再生成 Word、操作文档部件 | 老 .doc 完全无法处理 |
| Apache Tika | .doc/.docx/PDF 等 | 只要纯文本,需要统一兜底解析 | 拿不到表格结构,丢失排版信息 |
如果目标只是“把文字抽出来塞进数据库某字段”,Tika 是最省事的选择;但如果入库后还要按表格行做统计、按图片数量做盘点,POI 几乎是唯一能把结构和内容一起保住的答案。实际项目中我经常两套一起用:POI 解析成功就直接走结构化流程,POI 抛异常时用 Tika 兜底只保文本,至少保证数据不丢。
2.4 入库场景还要考虑段落边界问题
选型时很多人只盯着格式支持,忽略了后续入库的结构设计。数据库表里通常需要区分标题、正文、表格说明,而 Word 文档里很多标题只是字体比正文大一号,并没有套用“标题 1”样式。
这种情况下,我会在解析层做一层启发式切分:读取每个段落时记录字体大小和是否加粗,超过正文基准字号 2 磅以上就标记为疑似标题,左右缩进和项目符号也一并记录,这样入库后的数据可以直接支撑搜索高亮和结构化展示。这个细节在纯文本抽取方案里是完全做不到的,也是我最终以 POI 为主力的原因之一。
3. 读取 Word 内容:段落、表格、图片的稳定提取写法
解析选型定下来之后,剩下的就是代码怎么写才稳定。“稳定”这个词在 Word 解析场景里意味着:能处理合并单元格、能拿到图片、能兼容老格式,而且内存别爆。
3.1 Maven 依赖与最小工程组织
POI 的依赖不是只有一个大包,而是按功能拆成了多个构件。解析 Word 文件需要同时引入 poi、poi-ooxml 和 poi-scratchpad 三件套,其中 poi-scratchpad 专门提供 HWPF 这类老格式支持。版本建议使用与你现有项目依赖没有冲突的最新稳定版。
<properties> <poi.version>使用最新稳定版</poi.version> </properties> <dependencies> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>${poi.version}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>${poi.version}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-scratchpad</artifactId> <version>${poi.version}</version> </dependency> </dependencies>三个依赖缺一个都会在运行期冒出奇怪的NoClassDefFoundError,比如只引入 poi-ooxml 而漏掉 poi,解析到表格样式时就会崩。工程结构上我习惯把“文件读取”和“内容解析”拆成两层:入口类只负责拿文件流并调用解析器,解析器返回一个带段落、表格、图片列表的结果对象,这样无论是扫目录、接 HTTP 上传还是读消息队列,复用同一套解析逻辑。
3.2 正文段落抽取:从 getText 到 run 级拼接
POI 里最直观的写法是遍历 XWPFDocument 的段落列表,直接调用getText():
XWPFDocument xDoc = new XWPFDocument(new FileInputStream("demo.docx")); for (XWPFParagraph paragraph : xDoc.getParagraphs()) { String text = paragraph.getText(); System.out.println(text); }这段代码能跑通,但拿到的文本可能比 Word 里看到的少东西。原因是paragraph.getText()只拼接了 XWPFRun 的文本节点,而 Word 里的软换行(Shift+Enter)、某些域代码生成的文本、嵌套在文本框里的段落,都可能被跳过。更稳的做法是手动遍历 run 并补上换行标记:
for (XWPFParagraph paragraph : xDoc.getParagraphs()) { StringBuilder sb = new StringBuilder(); for (XWPFRun run : paragraph.getRuns()) { String runText = run.getText(0); if (runText != null) { sb.append(runText); } if (run.getCTR().isSetBr()) { sb.append("\n"); } } System.out.println(sb.toString()); }参数说明:run.getText(pos)里的 pos 传 0 表示取当前 run 的第一个文本节点,新版本里多个文本节点合并存储的情况很少,传 0 是相对安全的取值方式。run.getCTR().isSetBr()用来判断 run 是否包含换行标志,加了这个判断,段落内部的软换行才能保留下来。处理这类问题我一般会先打印出一段原文对比,再决定要不要走 run 级拼接,避免过度设计。
3.3 表格读取与合并单元格的正确姿势
表格是 Word 解析里翻车率最高的区域。初版代码我直接用getTables()遍历,然后用row.getTableCells()拿单元格,结果发现解析出来的行列和 Word 里显示的不是一回事。
问题出在合并单元格上。Word 表格存在两种合并:横向合并由 gridSpan 记录跨列数,纵向合并由 vMerge 标记延续单元格。物理存储的 cell 数量少于视觉上的网格数量,直接用getTableCells()就会丢掉空位。
换成逻辑单元格读取方式:
for (XWPFTable table : xDoc.getTables()) { for (XWPFTableRow row : table.getRows()) { List<ICell> logicalCells = row.getTableICells(); for (ICell icell : logicalCells) { if (icell instanceof XWPFTableCell) { XWPFTableCell cell = (XWPFTableCell) icell; String cellText = cell.getText(); System.out.println("单元格内容: " + cellText); } } } }getTableICells()返回的是逻辑网格中的单元格列表,碰到横向合并时同一单元格会在多个位置出现,但能保证列数对得上。取到 cell 之后还要再遍历 cell 内嵌的段落,因为单元格里同样可能包含多个段落,只调getText()会丢掉段落结构。如果业务需要还原表格的原样排版,配合getCTTc().getTcPr().getGridSpan()读取合并跨度,才能把单元格坐标完整算出来。
3.4 图片抽取与文件扩展名判断
图片入库是另一个容易被忽略的点。XWPF 把所有图片统一存在包内 media 目录下,获取方式比较统一:
XWPFDocument xDoc = new XWPFDocument(new FileInputStream("demo.docx")); for (XWPFPictureData picture : xDoc.getAllPictures()) { byte[] data = picture.getData(); String ext = picture.suggestFileExtension(); String fileName = picture.getFileName(); // 按需保存 data 到磁盘或 OSS Files.write(Paths.get("/data/images/" + fileName), data); }suggestFileExtension()返回的是不带点的扩展名,比如png、jpeg或emf,保存时要用picture.getFileName()做文件名避免重名。这里有个常见误区:getAllPictures()拿到的图片不包含页眉页脚区域里引用的图片,如果业务需要全文档的图片资产,还要遍历 header 和 footer 里的关系数据,代码量会涨一截。
3.5 老 .doc 文件的兼容分支
处理 .doc 时,HWPF 的功能简陋到让人怀念 XWPF。如果目标只是拿正文文本,WordExtractor一行就能搞定:
WordExtractor extractor = new WordExtractor(new FileInputStream("old.doc")); String rawText = extractor.getText(); extractor.close();但 .doc 的表格读取很尴尬,POI 的老式表格 API 返回结果经常不稳定,尤其遇到合并单元格时统计数量和列宽都对不上。我的建议是把 .doc 统一走“ 2.2 节 提到的 LibreOffice 转 docx”方案,转完之后再进 XWPF 流程,比在 HWPF 里调来调去可靠得多。
4. 上传数据库:表结构、批量写入与存储方案怎么配
解析结果拿到之后,数据库设计就成了决定后续维护体验的关键环节。很多人一开始图省事把整篇文本塞进一个字段就完事,等要做全文检索和增量更新时才发现没有唯一标识、没有状态标识,数据没法对账。
4.1 表结构:为什么要拆元数据表和正文表
我采用的表结构是两张表配合:一张存文件元数据,用于列表展示、去重和任务状态跟踪;一张存解析后的结构化内容,按文件唯一 ID 关联:
CREATE TABLE document_meta ( id BIGINT PRIMARY KEY AUTO_INCREMENT, origin_file_name VARCHAR(255) NOT NULL, file_ext VARCHAR(10) NOT NULL, file_size BIGINT NOT NULL, file_sha256 CHAR(64) NOT NULL, parse_status TINYINT NOT NULL DEFAULT 0, parse_time DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_sha256 (file_sha256) ); CREATE TABLE document_content ( doc_id BIGINT PRIMARY KEY, full_text LONGTEXT NULL, table_count INT NULL, image_count INT NULL, parse_version VARCHAR(32) NULL, FOREIGN KEY (doc_id) REFERENCES document_meta(id) );拆分原因:第一,列表页只需要扫描 document_meta,不用拖着可能几百 KB 的 LONGTEXT 一起做索引扫描;第二,文件重复导入场景里,meta 表上的 file_sha256 唯一索引能直接挡住重复数据;第三,解析版本号字段放在 content 表,以后升级解析逻辑时可以对旧数据重算,不影响 meta 表的展示状态。
4.2 文件本身存 BLOB 还是存磁盘路径
很多初学者会把 Word 文件二进制直接塞进数据库的 BLOB 字段,这种做法在小项目里能跑,但数据库会迅速膨胀,备份和迁移都变成灾难。建议按文件大小和访问频率分策略:
| 存储方式 | 适合场景 | 主要问题 |
|---|---|---|
| MySQL BLOB | 文件极小、数量少、强事务要求 | 数据库膨胀,备份耗时长 |
| 本地磁盘 + 路径 | 内网系统、中等规模 | 迁移时要注意路径漂移 |
| OSS/MinIO + 路径 | 文件多、需要多端访问 | 需引入 SDK 依赖 |
我一般把原始文件放磁盘或对象存储,数据库只保存相对路径字段,document_meta 里对应的路径字段(示例 SQL 里没有包含,实际可按需追加)由上传模块统一维护。这样 document_content 里的 LONGTEXT 只有解析文本,数据库体积可控。
4.3 MyBatis 批量插入参数调优
批量插入解析结果时,最坑的是 MyBatis 的 foreach 动态 SQL 把所有值拼成一条超长 SQL,超过 max_allowed_packet 就报错。正确做法是控制每批数量,每条记录独立插入,并打开 JDBC 的批量重写开关。
@Insert({ "<script>", "INSERT INTO document_content(doc_id, full_text, table_count, image_count, parse_version) VALUES ", "<foreach collection='list' item='item' separator=','>", "(#{item.docId}, #{item.fullText}, #{item.tableCount}, #{item.imageCount}, #{item.parseVersion})", "</foreach>", "</script>" }) int batchInsert(@Param("list") List<DocContent> items);调用层分批提交:
// 每批 500 条,防止 SQL 过长 List<List<DocContent>> batches = partition(contentList, 500); for (List<DocContent> batch : batches) { mapper.batchInsert(batch); }这里有一个容易被忽略的 JDBC 参数:MySQL 驱动默认逐条执行addBatch的语句,并不会真正合并网络往返。需要在连接 URL 上加上rewriteBatchedStatements=true,批处理才能获得实际性能提升。这个参数对 MyBatis 同样生效,因为最终走的还是 JDBC Batch API。
4.4 事务边界与失败重试
批量入库一定要给每个批次独立事务,而不是整个目录一个事务。理由很简单:5000 个文件里只要有一个解析崩溃,整个事务回滚,前面 4999 个白跑。我实践中的做法是:document_meta 的写入单独提交,解析结果分批提交;失败的批次记录文件路径和异常类型,写进失败日志表;人工修复或升级解析逻辑后,根据失败文件清单重新进入解析队列。
status 字段按 0-待处理、1-解析中、2-成功、3-失败记录状态,重跑时只挑 0 和 3 的条目,避免重复消费机器资源。
5. 避坑排查:乱码、错位、OOM、超时四次现场还原
解析入库这个需求看着不复杂,实际跑批时各类问题轮番轰炸,这里挑四个我真实撞上的现场,按现象到原因再到解决写清楚。
5.1 老 doc 中文乱码:文本全变成问号
现象:读取 .doc 文件时,正文能正常解析出英文和数字,但中文标点全部变成问号,部分段落直接丢失。
原因:一是文件本身并非真正的 OLE Word 文档,而是某国产办公软件导出的伪 .doc 内容,编码方式与标准 OLE 不一致;二是 HWPF 对 GBK/GB2312 历史编码的推断在某些区域设置下失效。
解决:先按 2.1 节 的文件头判断方法确认真实文件类型。确认是老式 OLE 后,先用WordExtractor提取文本,再对提取结果做编码修复;如果修复后仍有乱码,直接走 Tika 兜底解析。从那以后我遇到老格式文件的处理顺序固定为:文件头识别、LibreOffice 转换、转换失败再进 Tika 纯文本通道。
5.2 表格行列错位:解析出的数据和 Word 对不上
现象:一个 4 列 10 行的表格,解析出来后前几行只有 3 个单元格,后几行出现 5 个,总数和 Word 显示完全对不上。
原因:表格里有横向合并单元格,POI 物理存储的 cell 数量小于视觉网格数,用getTableCells()读取时合并格只出现一次,导致后续行的列坐标计算混乱。
解决:改用row.getTableICells()读取逻辑网格,再配合CTTcPr.getGridSpan()获取合并跨度。合并单元格在逻辑网格里会占多个位置,做数据映射时按 GridSpan 补齐占位,表格内容就能和 Word 原样对应。
5.3 大文档 OOM:解析 200MB 的文档直接堆溢出
现象:一个包含大量高清图片的 docx 文件,在new XWPFDocument(fileInputStream)时直接抛出OutOfMemoryError,进程退出。
原因:XWPFDocument 会把整个 ZIP 包里的 XML 和媒体文件全部加载进内存,图片在抽取时还会再转一次字节数组,内存占用翻倍。
解决:分两步走。先只解析正文段落和表格,不调用getAllPictures(),减少图片回到内存的开销;确实需要图片时,把 JVM 堆调大并限制并发解析线程数。启动参数实测有效:
java -Xmx2g -Xms512m -jar doc-parser.jar --input=/data/docs如果单文件超过 500MB,建议先把文件切分到单篇文档级别再解析,或者直接换用 SAX 事件模式读 XML,但这属于极端场景,普通入库需求很少碰到。
5.4 批量入库事务锁等待:MySQL 报 lock wait timeout
现象:跑批 2000 个文档,插入到第 400 个时报Lock wait timeout exceeded,整个事务回滚。
原因:一个事务里执行了过多 INSERT,单条 SQL 长度接近 10MB,InnoDB 在做行锁等待和 binlog 写入时长事务持有资源过久;另一个诱因是连接 URL 没开rewriteBatchedStatements,批处理实际是逐条提交。
解决:批大小压到 500 条并每批独立提交事务;连接串末尾加rewriteBatchedStatements=true打开真正的批量写;如果表上有全文索引,先入库再建索引,避免插入时的索引维护代价过高。
5.5 解析结果和 Word 显示对不上:文本框和页眉页脚丢失
现象:Word 打开时整页内容完整,程序统计出的字符数比 Word 底部显示的字数少了 30% 以上。
原因:正文之外的内容分布在文本框、页眉、页脚、目录域里,getParagraphs()只会遍历 document.xml 中最外层的段落,这些区域默认不进入结果。
解决:额外遍历xDoc.getHeaderList()和xDoc.getFooterList(),把页眉页脚文本追加到结果尾部;文本框内容需要从 run 的 VML 图形节点里提取,单独走一段解析逻辑。做完整度校验时,对比的是“解析全量文本”和“Word 字数统计”的差值,而不是简单对比主文档段落数。
6. 让解析入库更可靠:完整度验证、增量去重与异步化
流程能跑通只是开始,生产环境里更重要的三个习惯:上线前跑验证、重跑时不重复、长任务不阻塞接口。
6.1 完整度验证:先跑一组真实样本的横向对比
我从不敢拿单个测试文档验证解析器。真实文件千奇百怪,有扫描转 PDF 再转 Word 的,有 WPS 兼容模式另存的,有嵌套表格套了三层的。规范化做法是抽 20 到 50 份真实样本,跑一轮对比输出:
java -jar doc-parser.jar --mode=verify --input=/data/samples --output=/data/verify_result.csv对比维度至少包含:源文件字符数、解析字符数、段落数、表格数、图片数。解析字符数低于源文件 70% 的文件逐个排查原因,把文本框、页眉页脚等缺失点补进代码再重跑。
6.2 增量去重:SHA-256 做唯一索引
document_meta 表上的uk_sha256唯一索引是重跑流程的后悔药。批量任务执行前先算一下文件的 SHA-256,已经存在就直接跳过;内容变更过的文件哈希值不同,会被重新解析并更新内容表,实现真正的增量同步。
public static String sha256(File file) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); try (InputStream in = Files.newInputStream(file.toPath())) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { digest.update(buffer, 0, len); } } return HexFormat.of().formatHex(digest.digest()); }摘要比较必须基于文件字节,不能基于文件名和最后修改时间,Maven 仓库里有现成的 Apache Commons Codec 封装,但用 JDK 原生类写也就十几行,不额外引入依赖。
6.3 异步任务队列与进度回传
一次性跑几千个文件时,同步接口会一直阻塞调用方。用线程池解析、解析进度写入内存变量或 Redis,前端轮询展示进度。线程数按 IO 密集型任务设置,8 核机器上 4 到 8 个线程足够,太多反而造成内存峰值叠加。
ExecutorService pool = Executors.newFixedThreadPool(4); for (File file : files) { pool.submit(() -> { try { String hash = sha256(file); if (docMetaMapper.exists(hash)) { return; } ParseResult result = documentParser.parse(file); documentService.save(file, result); progress.incrementAndGet(); } catch (Exception e) { failedFiles.add(file.getName()); } }); }线程池记得在应用关闭时调用shutdown(),并设置超时强制结束,否则容器下线时非守护线程会拖住进程。
从那以后,我每次接到文档入库需求,都强制先走一遍“真实样本验证 + SHA-256 去重 + 分批提交”的流程,解析完整性不再是个黑匣子。Word 解析不存在万能配方,但把选型、边界条件和重试机制想在前面,踩坑率能降下来一大半。希望帮到你。
本文还有配套的精品资源,点击获取