笨方法学 Java -30-综合练习2-实现分析-docx 文本替换
作者:谎言诞行    发布时间:2024年03月23日

一、前言

由于 Office Word 文档在 07版开始,对于文档结构做了较大的改进,连文件后缀都改为 docx 了。因此,在文档的读写解析上,没法重用 HWPF 模块的 API。 POI 组件对于 07版之后的 Word 文档解析处理上,他们设计了一个全新的模块 XWPF。

XWPF 模块设计了一个类 org.apache.poi.xwpf.usermodel.XWPFDocument 来处理 docx 格式文档的解析。它大体上将 docx 格式的 word 文档分为几个类型对象的集合,如下图:

image.png


二、关于文本替换符

关于模板文件中的文本替换符,大家可以参考上一篇文章  《笨方法学 Java -29-综合练习2-实现分析-doc 文本替换》


三、docx格式文档的结构

其实对于我们这种模板替换的处理,只需要了解最简单的文档结构即可。最简单的文档结构就是:文档->段落->文字。对应到 XWPF 模块就是:XWPFDocument->XWPFParagraph->XWPFRun。大家是不是觉得,这个结构跟HWPF 也是类似的。

测试的参考代码如下:

public static void main(String[] args) throws Exception {
    //操作系统用户主目录
    String userDir = System.getProperties().getProperty("user.home");
    //文件所在文件夹
    String fileDir = userDir+File.separator+"examproj";
    //word文档路径1
    String templateFilePath = fileDir+File.separator+"请假单模板(新).docx";
    //
    XWPFDocument docx = new XWPFDocument(new FileInputStream(templateFilePath));
    //
    List<XWPFParagraph> paragraphs = docx.getParagraphs();
    //
    if(paragraphs!=null && paragraphs.size()>0) {
        for(int i=0;i<paragraphs.size();i++) {
            XWPFParagraph pr = paragraphs.get(i);
            System.out.println("段落"+(i)+":"+pr.getText());
        }
        System.out.println("=================================");
        for(int i=0;i<paragraphs.size();i++) {
            XWPFParagraph pr = paragraphs.get(i);
            //获取相同样式的字符串
            List<XWPFRun> runs = pr.getRuns();
            if(runs!=null && runs.size()>0) {
                for(int j=0;j<runs.size();j++) {
                    System.out.println("段落"+i+",字符串"+j+",内容:"+runs.get(j).text());
                }
            }else {
                System.out.println("段落"+(i)+",没有字符串内容");
            }
        }
    }else {
        System.out.println("获取不到段落信息");
    }
    //关闭文档对象
    docx.close();
}

运行结果如下图:

image.png


四、替换处理(提供的样例代码,可能有些小问题。如果用于生产环境,请自行修改)

对于 XWPFDocument 的处理,API 并没有提供默认的替换方法。那么我们需要自己进行替换符的替换处理。

这时,有些读者可能会想到,能不能在段落(XWPFParagraph)上进行替换?这个根据本人在研究API的信息来看,是不可行的。因为,段落并没有字符串的设置方法。而且,文字的样式也不是在段落上设置的。可以参考下图的API信息:

image.png

那么,按照“文档->段落->文字”的层次结构,我们只能在文字(XWPFRun)上努力了。但是,有一点,大家要注意,文字是按照同属性分组的。如下图:

image.png

我们的段落1,被拆分成了4个 XWPFRun 对象。


因此,如果我们需要对内容进行合并,我们需要把多个XWPFRun对象,合并为1个。这样才可以成功替换字符串。

并且,我们需要注意一个问题:关于文字替换后的样式问题。我们在设计一个模板的时候,往往会在替换符上增加样式,比如:字体加粗、增加下划线。并且,希望在替换内容后,这些样式,依然保留


对此,我们有一个很简单的解决方案,那就是,当遇到替换符前缀“{{”时,把后面的变量名,以及替换符后缀“}}”合并到替换符前缀所属的 XWPFRun 对象。这样既可以保留样式,也能保证内容替换的准确性。样例代码如下:

/**
 * >>&nbsp;在处理 office word 文档中,docx 类型的文档时,
 * 由于字符处理时,需要替换的变量字符串可能分割在几个对象中,需要合并
 * @param paragrah 段落对象
 * @return List<XWPFRun> 
 */
private static List<XWPFRun> getRunInfoFromParagrah(XWPFParagraph paragrah){
    //结果对象
    List<XWPFRun> result = null;
    //参数正常才开始处理
    if(paragrah!=null && paragrah.getRuns()!=null && paragrah.getRuns().size()>0) {
        //结果初始化
        result = paragrah.getRuns();
        //循环遍历,将字符串合并
        StringBuffer buffer = new StringBuffer();
        XWPFRun targetRun = null;
        for(int i=0;i<result.size();i++) {
            XWPFRun tmp = result.get(i);
            if(tmp.text().contains(MyWordUtil.PARAM_PREFFIX)) {
                //如果遇到带替换变量的字符串,放入buffer中(开始符号的run对象,作为基础对象)
                buffer.append(tmp.text());
                targetRun = tmp;
            }else if(tmp.text().contains(MyWordUtil.PARAM_SUFFIX)) {
                //如果遇到带替换变量结束符的字符串,放入buffer中,删除原有run对象
                buffer.append(tmp.text());
                paragrah.removeRun(i);
                paragrah.insertNewRun(i);//========删掉之后数组的序号就变了,需要插一个空的进去
                //将变量全部字符串合并
                targetRun.setText(buffer.toString(), 0);
                //重置buffer
                buffer.setLength(0);
                //重置目标run
                targetRun = null;
            }else if(buffer.length()>0) {
                //如果是变量中间的内容,放入buffer中,删除原有run对象
                buffer.append(tmp.text());
                paragrah.removeRun(i);
                paragrah.insertNewRun(i);//========删掉之后数组的序号就变了,需要插一个空的进去
            }else {
                //其它直接跳过
                continue;
            }
        }
    }
    return result;
}

 

到这里,我们就已经把替换符所设计的几个 XWPFRun 对象,合并为一个了。那么我们要怎样才能把 XWPFRun 对象的字符串替换为我们目标内容呢?其实很简单,XWPFRun对象有2个 setText 方法。如下:

image.png

image.png

其中,不带pos参数的setText会以追加的形式设置字符串内容。我们需要的是第 2 个,带 pos 参数的方法。将 pos设置为0,即可替换掉原有内容。参考代码如下:

/**
 * >>&nbsp;在处理 office word 文档中,docx 类型的文档时,替换内容中的变量
 * @param runs 内容列表
 * @param paramsMap 翻译用的map信息
 */
private static void replaceParams(List<XWPFRun> runs, Map<String, String> paramsMap) {
    if(runs!=null && paramsMap!=null) {
        for(XWPFRun run: runs) {
            if(run!=null && run.text()!=null && run.text().contains(PARAM_PREFFIX)) {
                //字符串中的原始内容(替换后也是它)
                String newStr = run.text();
                //开始替换
                for(String key : paramsMap.keySet()) {
                    //前缀的出现位置
                    int indexOfPre = newStr.indexOf(PARAM_PREFFIX);
                    //后缀的出现位置
                    int indexOfSuf = newStr.indexOf(PARAM_SUFFIX);
                    //如果内部还有需要替换的内容
                    if(indexOfPre>=0 && indexOfSuf>=0 && indexOfPre<indexOfSuf) {
                        newStr = newStr.replaceAll(PARAM_PRE_REGEXP+key+PARAM_SUF_REGEXP, paramsMap.get(key));
                    }else {
                        //否则,已经替换完这个 run 了,跳出
                        break;
                    }
                }
                run.setText(newStr, 0);
            }
        }
    }
}


在下一篇文章,我们讨论下,更加特殊的情况。《笨方法学 Java -31-综合练习2-实现分析-docx 文本替换(续)》

填写一种颜色的字符(不区分大小写),1分钟有效,点击图片刷新
谎言诞行
2024-03-25 13:26:53
程序已经优化完毕,可以参考 第 31 篇文章。
谎言诞行
2024-03-24 19:25:36
合并字符串的程序优化中,暂未发布新内容!!!
谎言诞行
2024-03-23 17:11:19
修改了 replaceParams 方法的实现代码。大家可以参考一下。使用了 replaceAll 处理,因为 run 对象中可能有几个变量需要替换。