-->

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

背景

为实现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后两位数字

创建实体类

    @Data
    public 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,用于设置parentId
                String[] 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;
                        }
                        // 构造ID
                        StringBuilder 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);
                        // 设置parentId
                        if (styleIndex > 1) {
                            // 如果不是第二级标题,则parentId为上一级的ID
                            node.setParentId(lastLevelIds[styleIndex-1]);
                        } else {
                            // 如果是第二级标题,则parentId为根节点ID
                            node.setParentId("10000000000");
                        }
                        // 记录当前级别的ID
                        lastLevelIds[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,用于设置parentId
                String[] 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;
                        }
                        // 构造ID
                        StringBuilder 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);
                        // 设置parentId
                        if (styleIndex > 1) {
                            // 如果不是第二级标题,则parentId为上一级的ID
                            node.setParentId(lastLevelIds[styleIndex-1]);
                        } else {
                            // 如果是第二级标题,则parentId为根节点ID
                            node.setParentId("10000000000");
                        }

                        // 记录当前级别的ID
                        lastLevelIds[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中提取所有父级ID
        String 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;
                }
            }

            // 从找到的位置开始,按照每两位一级生成父级ID
            for (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);

            // 收集所有相关的父节点ID
            Set<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为搜索关键词,有则模糊搜索,没有则查询全部

posted @ 2025-12-26 14:55  ꧁ʚ星月天空ɞ꧂  阅读(2)  评论(0)    收藏  举报