尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

java实现Apache POI提取word文档标题(类目录)

java实现Apache POI提取word文档标题(类目录)
📅 发布时间:2026/6/20 7:23:51

背景

为实现F1帮助文档的自动化管理(并非写死的文档内容),故由后端提取帮助文档目录,将目录存入数据库统一管理,并通过开发查询目录树接口,返回给前端目录,由前端跳转到指定的HTML界面(由拆分的word文档转化的)

导包

在pom文件中导入Apache POI的包

        <!-- Apache POI核心库 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.4</version></dependency><!-- Apache POI OOXML支持(.docx) --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.4</version></dependency><!-- Apache POI OLE2支持(.doc) --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-scratchpad</artifactId><version>5.2.4</version></dependency>

这里的实体类的id为个人自定义,最多6级标题,每级最多10多个标题,所以根节点为1000000000000,每级决定1后两位数字

创建实体类

    @Datapublic static class HeadingNode {private String id;private String title;private int level;private String parentId;public HeadingNode() {}}

使用Apache POI解析Word文档,提取标题

    // 使用Apache POI解析Word文档public static List<HeadingNode> parseWordHeadings(File wordFile) throws IOException {List<HeadingNode> headings = new ArrayList<>();HeadingNode headingNode = new HeadingNode();if (language.equals("zh")){headingNode.setTitle("用户使用手册");} else {headingNode.setTitle("User Manual");}headingNode.setLevel(1);headingNode.setId("10000000000");headingNode.setParentId("0");headings.add(headingNode);try (FileInputStream fis = new FileInputStream(wordFile)) {if (wordFile.getName().toLowerCase().endsWith(".docx")) {// 处理 .docx 文件XWPFDocument document = new XWPFDocument(fis);// 计数器数组,记录各级标题的序号int[] counters = new int[7]; // 索引0不用,1-5对应五个级别// 记录上一级别的ID,用于设置parentIdString[] lastLevelIds = new String[7];for (XWPFParagraph paragraph : document.getParagraphs()) {System.out.println("paragraph:"+paragraph.getText());String style = paragraph.getStyle();String styleID = paragraph.getStyleID();System.out.println("style:"+style);System.out.println("styleID:"+styleID);if (style != null && style.matches("\\d+")) {int levelNum = Integer.parseInt(style);int styleIndex = 0;if (levelNum>0&&levelNum<=10){styleIndex = 1;} else if (levelNum>10&&levelNum<=20){styleIndex = 2;} else if (levelNum>20&&levelNum<=30) {styleIndex = 3;} else if (levelNum>30&&levelNum<40) {styleIndex = 4;} else if (levelNum>40&&levelNum<=50) {styleIndex = 5;} else if (levelNum>50&&levelNum<=60) {styleIndex = 6;}// 更新计数器counters[styleIndex]++;// 重置更低级别的计数器for (int j = styleIndex + 1; j <= 5; j++) {counters[j] = 0;}// 构造IDStringBuilder idBuilder = new StringBuilder("1");for (int level = 1; level <= 5; level++) {idBuilder.append(String.format("%02d", counters[level]));}HeadingNode node = new HeadingNode();node.setTitle(paragraph.getText());node.setLevel(styleIndex + 1);String currentId = idBuilder.toString();node.setId(idBuilder.toString());headings.add(node);// 设置parentIdif (styleIndex > 1) {// 如果不是第二级标题,则parentId为上一级的IDnode.setParentId(lastLevelIds[styleIndex-1]);} else {// 如果是第二级标题,则parentId为根节点IDnode.setParentId("10000000000");}// 记录当前级别的IDlastLevelIds[styleIndex] = currentId;}}} else if (wordFile.getName().toLowerCase().endsWith(".doc")) {// 处理 .doc 文件HWPFDocument document = new HWPFDocument(fis);Range range = document.getRange();// 计数器数组,记录各级标题的序号int[] counters = new int[6]; // 索引0不用,1-5对应五个级别// 记录上一级别的ID,用于设置parentIdString[] lastLevelIds = new String[6];for (int i = 0; i < range.numParagraphs(); i++) {org.apache.poi.hwpf.usermodel.Paragraph paragraph = range.getParagraph(i);Short styleIndex = paragraph.getStyleIndex();if (styleIndex >= 1 && styleIndex <= 5) {// 更新计数器counters[styleIndex]++;// 重置更低级别的计数器for (int j = styleIndex + 1; j <= 5; j++) {counters[j] = 0;}// 构造IDStringBuilder idBuilder = new StringBuilder("1");for (int level = 1; level <= 5; level++) {idBuilder.append(String.format("%02d", counters[level]));}HeadingNode node = new HeadingNode();node.setTitle(paragraph.text().trim());node.setLevel(styleIndex + 1);String currentId = idBuilder.toString();node.setId(idBuilder.toString());headings.add(node);// 设置parentIdif (styleIndex > 1) {// 如果不是第二级标题,则parentId为上一级的IDnode.setParentId(lastLevelIds[styleIndex-1]);} else {// 如果是第二级标题,则parentId为根节点IDnode.setParentId("10000000000");}// 记录当前级别的IDlastLevelIds[styleIndex] = currentId;}}} else {throw new IllegalArgumentException("不支持的文件格式");}}return headings;}

这里word分为了docx和doc两种处理方式,其中doc直接获取标题样式,即一级标题,getStyleIndex返回1,二级标题返回2,依次返回,因为我这里最小标题到6级标题,所以只对1-6级进行了提取;至于docx格式嘛,标题网上提取都是一级标题返回字符串Heading1,后续依次,但是实际体验感觉不太好提取,会出现Heading1NoNumber,较容易造成提取错误,所以我这里转为对getStyleID处理,与style区别不大,准确的标题会返回具体数字,例如,一级标题返回字符串10,所以处理了0-60,style都由数字组成的;这里我用的都是doc格式,docx格式只是简单测了一下,个人认为doc较易处理,所以docx我都是另存为doc处理的;注意:上述所有前提都是文档的标题样式严格为word的标题样式,否则肯定是提取不到的,所以体量很大的文档提取要保证格式,略微问题可以手动改,全改还是算了吧。 这里最后返回了所有的节点,如需存库可以使用mybatis-plus自带的saveBatch存储到数据库

添加查询功能

上述返回的headings在存入到数据库或内存中后,需要添加查询与搜索功能给前端,代码如下

    private void collectParentIds(HeadingNode node, Set<String> relatedIds) {relatedIds.add(node.getId());// 从当前节点ID中提取所有父级IDString currentId = node.getId();if (currentId != null && currentId.startsWith("1") && currentId.length() == 13) {// 从后往前每两位检查,找到第一个非"00"的位置int lastIndex = currentId.length();for (int i = currentId.length() - 2; i > 1; i -= 2) {String suffix = currentId.substring(i, i + 2);if (!"00".equals(suffix)) {lastIndex = i;break;}}// 从找到的位置开始,按照每两位一级生成父级IDfor (int i = lastIndex; i > 1; i -= 2) {String parentId = currentId.substring(0, i) + "0000000000000".substring(i);if (!"1000000000000".equals(parentId)) {relatedIds.add(parentId);}}}// 添加根节点relatedIds.add("1000000000000");}public List<Tree<String>>  getHeadingsByOperation(String keyWord){List<HeadingNode> headings;if (keyWord != null && !keyWord.trim().isEmpty()) {// 模糊搜索LambdaQueryWrapper<HeadingNode> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(HeadingNode::getTitle, keyWord);List<HeadingNode> searchResults = list(queryWrapper);// 收集所有相关的父节点IDSet<String> relatedIds = new HashSet<>();for (HeadingNode node : searchResults) {collectParentIds(node, relatedIds);}// 查询所有相关节点(包括父节点)if (!relatedIds.isEmpty()) {LambdaQueryWrapper<HeadingNode> fullQuery = new LambdaQueryWrapper<>();fullQuery.in(HeadingNode::getId, relatedIds);headings = list(fullQuery);} else {headings = searchResults;}} else {// 查询全部headings = list();}
//        log.info("keyWord:{}", keyWord);
//        log.info("成功查询数据库中的数据,数据:{}", headings.size());
//        log.info("成功构建树形结构,数据:{}", headings);if (headings.isEmpty()){return null;} else {TreeNodeConfig treeNodeConfig = new TreeNodeConfig();treeNodeConfig.setIdKey("id");treeNodeConfig.setWeightKey("level");treeNodeConfig.setDeep(6);List<Tree<String>> treeNodes = TreeUtil.build(headings, "1000000000000", treeNodeConfig,(treeNode, tree) -> {tree.setId(treeNode.getId());tree.setWeight(treeNode.getLevel());tree.setParentId(treeNode.getParentId());tree.setName(treeNode.getTitle());});return treeNodes;}}

这里collectParentIds方法为模糊搜索到对应节点后,添加其所有父节点;list为mybatis-plus自带查询方法;最后将数据以树形结构返回;keyWord为搜索关键词,有则模糊搜索,没有则查询全部

相关新闻

  • NMN牌子哪个好?2026十大NMN品牌排名与权威口碑对比 - 速递信息
  • 【Open-AutoGLM使用全攻略】:从零入门到实战精通的5大核心步骤
  • 告别人工干预!Open-AutoGLM让大模型真正“自己动起来”

最新新闻

  • 从提示词到生产代码:SDD(Specification-Driven Development)范式下的智能研发实践
  • 钦州市本地2026年最新黄金回收靠谱门店TOP排行榜+白银回收+铂金回收+彩金回收及联系方式+地址+电话+诚信店铺推荐 - 盛世金银回收
  • Gemini 1.5 Pro 实战指南:Android与API集成合规方案
  • MC9S08SH32硬件断点与调试系统深度解析
  • Java CompletableFuture 异步编排实战
  • DeepTutor:你的智能学习伙伴,让AI辅导无处不在

日新闻

  • 信任的进化:技术实现详解——如何用JavaScript构建博弈论模拟器
  • Terrakube自定义工作流:如何集成OPA、Infracost等工具扩展IaC能力
  • grunt-concurrent快速入门:5分钟学会并行运行Grunt任务

周新闻

  • 3步解锁iOS设备:applera1n激活锁绕过完全指南
  • 39 2026 人工智能证书终极盘点,普通人选 AI 证书可以从这些方向入手
  • Redis 暴露公网有多危险?从端口检查到补救步骤

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号