一、前言
由于 Office Word 文档在 07版开始,对于文档结构做了较大的改进,连文件后缀都改为 docx 了。因此,在文档的读写解析上,没法重用 HWPF 模块的 API。 POI 组件对于 07版之后的 Word 文档解析处理上,他们设计了一个全新的模块 XWPF。
XWPF 模块设计了一个类 org.apache.poi.xwpf.usermodel.XWPFDocument 来处理 docx 格式文档的解析。它大体上将 docx 格式的 word 文档分为几个类型对象的集合,如下图:

二、关于文本替换符
关于模板文件中的文本替换符,大家可以参考上一篇文章 《笨方法学 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();
}运行结果如下图:

四、替换处理(提供的样例代码,可能有些小问题。如果用于生产环境,请自行修改)
对于 XWPFDocument 的处理,API 并没有提供默认的替换方法。那么我们需要自己进行替换符的替换处理。
这时,有些读者可能会想到,能不能在段落(XWPFParagraph)上进行替换?这个根据本人在研究API的信息来看,是不可行的。因为,段落并没有字符串的设置方法。而且,文字的样式也不是在段落上设置的。可以参考下图的API信息:

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

我们的段落1,被拆分成了4个 XWPFRun 对象。
因此,如果我们需要对内容进行合并,我们需要把多个XWPFRun对象,合并为1个。这样才可以成功替换字符串。
并且,我们需要注意一个问题:关于文字替换后的样式问题。我们在设计一个模板的时候,往往会在替换符上增加样式,比如:字体加粗、增加下划线。并且,希望在替换内容后,这些样式,依然保留。
对此,我们有一个很简单的解决方案,那就是,当遇到替换符前缀“{{”时,把后面的变量名,以及替换符后缀“}}”合并到替换符前缀所属的 XWPFRun 对象。这样既可以保留样式,也能保证内容替换的准确性。样例代码如下:
/**
* >> 在处理 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 方法。如下:


其中,不带pos参数的setText会以追加的形式设置字符串内容。我们需要的是第 2 个,带 pos 参数的方法。将 pos设置为0,即可替换掉原有内容。参考代码如下:
/**
* >> 在处理 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 文本替换(续)》