java使用Apache POI 操作word文档

项目背景:

当我们对一些word文档(该文档包含很多的标题比如 1.1 ,1.2 , 1.2.1.1, 1.2.2.3)当我们删除其中一项或者几项时,需要手动的对后续的进行补充。该功能主要是对标题进行自动的补充。

具体步骤:

导入依赖:

java 复制代码
<dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>5.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.2</version>
        </dependency>

官网网址:(觉得麻烦不可也许)

Apache Poi 官方链接 可以看官方文档,其实更方便的可以直接导入依赖后,下载源代码,直接看源码的注释也许

跑一下代码熟悉一下

首先把下面的代码复制到编译器跑一下,看看是否正常运行,顺便了解基本使用

java 复制代码
package codeByZyc;

import org.apache.poi.xwpf.usermodel.*;

import java.io.FileInputStream;
import java.io.IOException;

public class rederWordTest {
    public static void main(String[] args) throws IOException {
            FileInputStream file = new FileInputStream("输入你的word文档地址");
            XWPFDocument document = new XWPFDocument(file);
            // 获取word中的段落,无法获取表格
            System.out.println("获取到的段落");
            for (XWPFParagraph paragraph : document.getParagraphs()) {
                System.out.println(paragraph.getText());
            }
            //  这是只能获取word中的表格
            System.out.println("获取到的表给内容");
            for (XWPFTable table : document.getTables()) {
                for (XWPFTableRow row : table.getRows()) {
                    for (XWPFTableCell cell : row.getTableCells()) {
                        System.out.print(cell.getText() + " \t");
                    }
                    System.out.println();
                }
            }
            document.close();
            file.close();

        
    }

}

api说明

通过上面的代码,我们可以知道poi是用过XWPFDocument这个类去获取的word内容。 下面从段落和表格两部分进行代码说明

段落api说明

对于word中的段落他的操作如下:

java 复制代码
XWPFDocument(最大的模块).getParagraphs->Paragraph(负责每一个段落).getRuns->Run(这是最小处理的元素)

下面是进行调式的图片,配合图片更好理解:

XWPFDocument:就是最大的那个模块 信息很大
Paragraphs:这是所有的段落集合
Paragraph:存放的就是每一段了 里面的runs 是按照格式进行分割的
Runs run的集合

具体看下面

Run(最基础的元素)

这是最重要的那个元素,他是构成所有段落和表格的最小单位。
一个段落他是如何划分成几个run的?

他是按照每个字的前后 是否同一个格式(字体,加粗否,大小等)必须完全一样才能分到一个run里面。具体分割还得调式看

下图是一些run的切割:

这个就是特殊的符号会被划开

这个更离谱 注意 1.3.1后面的空格没有 ,这个的空格是被划开的。是因为空格的格式和标题不一样

段落代码:里面有注释 应该算比较清楚了 有问题可以下面评论

文件路径自己填写一下

java 复制代码
package codeByZyc;

import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class word {


    // 匹配数字开头,小数点隔开 空格后接内容
   static Pattern headerPattern = Pattern.compile("^(\\d+(?:\\.\\d+)*)(\\s+.*)");
    //用于计算层级
   static ArrayList<Integer> headerlist= new ArrayList();
    //用于存放段落开头有单个特殊符号的
    static ArrayList<String> kaitou =new ArrayList<>();

    // 匹配中文括号列表项(如 (1))
//   static Pattern listItemPattern = Pattern.compile("^((\\d+))(.*)$");

    public static void main(String[] args) {

        String path="文件路径";

        //初始化计算器
        for (int i = 0; i < 10; i++) {
            headerlist.add(0);
        }
        //初始化开头特殊字符

        kaitou.add("★");
        kaitou.add("*");

        //执行代码
        updateWord(path);
    }
    public static void updateWord(String path){
        try {
            // 1. 读取 .docx 文件
            FileInputStream fis = new FileInputStream(path);
            XWPFDocument document = new XWPFDocument(fis);


            //获取word每一个段落元素(不包含表格的)
            List<XWPFParagraph> paragraphs = document.getParagraphs();


            //  //一个段落一个段落的处理
            for (int i = 0; i < paragraphs.size(); i++) {
                wordDuanluo(paragraphs.get(i));
            }
            

            //开始村存回去
            FileOutputStream out = new FileOutputStream("文件路径代码生成.docx");
            document.write(out);

            // 4. 关闭流
            document.close();
            fis.close();
            out.close();

        } catch (Exception e) {
            e.printStackTrace();
        }


    }


    

    //处理段落
    private static void wordDuanluo(XWPFParagraph paragraph) {
        //每个段落下面还有更小的元素,叫run 所以处理run
        List<XWPFRun> runs = paragraph.getRuns();
        //直接匹配第一个 是那就是标题 不是那就是正文

       if (runs.size()==1){
            wordRun(runs.get(0));
        }else if (runs.size()==2){
           //只有两个识别开头是不是包含特殊  时走一个通道 不是走二通道
           //标志是否匹配
           boolean flagkaitou= false;
           for (int i = 0; i < kaitou.size(); i++) {
                if (kaitou.get(i).equals(runs.get(0).text())){
                    flagkaitou=true;
                    break;
                }
           }
           if (flagkaitou==true){
               //证明开头匹配走一同到
               wordRun(runs.get(1));
           }else {
               //不匹配
               wordRun(runs.get(0),runs.get(1));
           }
       }else if (runs.size()>2){
           //数量在三个及以上
           //标志是否匹配
           boolean flagkaitou= false;
           for (int i = 0; i < kaitou.size(); i++) {
               if (kaitou.get(i).equals(runs.get(0).text())){
                   flagkaitou=true;
                   break;
               }
           }
           if (flagkaitou==true){
               //证明开头匹配
               wordRun(runs.get(1),runs.get(2));
           }else {
               //不匹配
               wordRun(runs.get(0),runs.get(1));
           }

       }


    }



// 处理非常特殊的 标题后面跟的空格字体和标题不一样分开了 进行拼接
    private static void wordRun(XWPFRun run1, XWPFRun run2) {
        //run是作为操作word的非常小的元素了,他会把每个段落换分成几个run组成  具体划分规则我也不知道 (我看案列 是按格式进行 格式一样的情况大概率是在一起)
        //采用正则表达式进行匹配
        Matcher matcher = headerPattern.matcher(run1.text()+run2.text());
        if (matcher.find()) {
            //匹配成功
            //保存序号后面的文章用于拼接
            String contex = matcher.group(2);
            //按照.进行切割
            String[] originalParts = matcher.group(1).split("\\.");
            //根据长度判断层级 一个就一级
            int length = originalParts.length;
            //文档按照顺序1  1.1  1.1.1

            //将子层级覆盖掉
            for (int i = length; i < headerlist.size(); i++) {
                headerlist.set(i,0);
            }
            headerlist.set(length - 1, (headerlist.get(length - 1) + 1));

            StringBuffer result = new StringBuffer();
            //拼接正确的序号
            for (int i = 0; i < length; i++) {
                result.append(headerlist.get(i));
                result.append(".");
            }
            //多出一个. 进行删除
            result.deleteCharAt(result.length()-1);


            //序号放到run1  空格+正文放到run2
            run1.setText(result.toString(),0);
            run2.setText(contex,0);



        }
    }

    private static void wordRun(XWPFRun run) {
        //run是作为操作word的非常小的元素了,他会把每个段落换分成几个run组成  具体划分规则我也不知道 (我看案列 是按格式进行 格式一样的情况大概率是在一起)
        //采用正则表达式进行匹配
        Matcher matcher = headerPattern.matcher(run.text());
        if (matcher.find()) {
            //匹配成功
            //保存序号后面的文章用于拼接
            String contex = matcher.group(2);
            //按照.进行切割
            String[] originalParts = matcher.group(1).split("\\.");
            //根据长度判断层级 一个就一级
            int length = originalParts.length;
            //文档按照顺序1  1.1  1.1.1
            //将子层级覆盖掉
            for (int i = length; i < headerlist.size(); i++) {
                headerlist.set(i,0);
            }
            headerlist.set(length - 1, (headerlist.get(length - 1) + 1));

            StringBuffer result = new StringBuffer();
            //拼接正确的序号
            for (int i = 0; i < length; i++) {
                result.append(headerlist.get(i));
                result.append(".");
            }
            //多出一个. 进行删除
            result.deleteCharAt(result.length()-1);

            result.append(contex);

            //将内容替换到run
            run.setText(result.toString(),0);


        }
    }



}
表格api说明 基本上和段落一样 有一点点不一样
复制代码
XWPFDocument(最大的模块).getTables->XWPFTable(负责每一个表格).getRows->Row(代表一行).getTableCells->XWPFTableCell(每一格子)[由于cell无法更改具体看下图] 需要深入到 run

注意,我原来以为在cell就可以更改他的文本

但是看源码可以知道 他是在尾部追加并不是覆盖,所以还是只能追到run去覆盖。

表格代码:
复制代码
import org.apache.poi.xwpf.usermodel.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class TestWord {
    // 匹配表格的 结尾开头是数字 空格几个都行
     static  Pattern tablePattern = Pattern.compile("^(\\d+)(\\s*)$");
     static  Integer tableCount=0;
    //用于存放段落开头有单个特殊符号的
    static ArrayList<String> kaitou =new ArrayList<>();
    public static void main(String[] args) {
        String path="测试table.docx";


        //初始化开头特殊字符

        kaitou.add("★");
        kaitou.add("*");

        readWord(path);

    }

    public static void readWord(String filePath){
        try {
            // 1. 读取 .docx 文件
            FileInputStream fis = new FileInputStream(filePath);
            XWPFDocument document = new XWPFDocument(fis);
            //直接一层一层找一下找到 run  试过在cell进行修改 但是cell的修改是 原来的基础上进行了新的增加 不能进行替换 直接找到run进行替换
            for (XWPFTable table : document.getTables()) {
                //一个table 清除一次计数器
                tableCount=0;
                for (XWPFTableRow row : table.getRows()) {
                    for (XWPFTableCell cell : row.getTableCells()) {
                        for (XWPFParagraph paragraph : cell.getParagraphs()) {
                            List<XWPFRun> runs = paragraph.getRuns();
                            //直接匹配第一行
                            if (runs.size()==1){
                                tableup(runs.get(0));

                            }else if (runs.size()>1){
                                //这边就是 考虑到前面有个特殊符号的情况 就需要判断了
                                XWPFRun run1 = runs.get(0);
                                //第一个不是特殊那就直接走第一通道
                                boolean flag=false;
                                for (int i = 0; i < kaitou.size(); i++) {
                                    if (kaitou.get(i).equals(run1.text())){
                                        flag=true;
                                        break;
                                    }
                                }
                                if (flag){
                                    tableup(runs.get(1));
                                }else {
                                    tableup(run1);
                                }

                            }



                        }

                    }
                }
            }
            System.out.println("测试完成");

            FileOutputStream out = new FileOutputStream("生成的table.docx");
            document.write(out);
            // 4. 关闭流
            document.close();
            fis.close();
            out.close();

        } catch (Exception e) {
            e.printStackTrace();
        }


    }


    public  static void  tableup(XWPFRun run){
        Matcher matcher = tablePattern.matcher(run.text());
        if (matcher.find()){
            //匹配成功  开始更换层级
            String content = matcher.group(2);
            String originalParts = matcher.group(1);
            //开始覆盖掉序号 并拼接后面的内容
            tableCount++;
            StringBuffer result=new StringBuffer();
            result.append(tableCount+content);
            //写入
            run.setText(result.toString(),0);
        }
    }
}

总结

难点:

我感觉最大的难点就是对Api的熟悉,需要看看源码或者文档。以及利用正则表达式对标题进行匹配

相关推荐
达文汐5 分钟前
【困难】力扣算法题解析LeetCode332:重新安排行程
java·数据结构·经验分享·算法·leetcode·力扣
培风图南以星河揽胜6 分钟前
Java版LeetCode热题100之零钱兑换:动态规划经典问题深度解析
java·leetcode·动态规划
启山智软29 分钟前
【中大企业选择源码部署商城系统】
java·spring·商城开发
我真的是大笨蛋32 分钟前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怪兽源码1 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
恒悦sunsite1 小时前
Redis之配置只读账号
java·redis·bootstrap
梦里小白龙1 小时前
java 通过Minio上传文件
java·开发语言
人道领域1 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
sheji52612 小时前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Java Web的电子商务网站的用户行为分析与个性化推荐系统为例,包含答辩的问题和答案
java·开发语言