1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > (五) JAVA基于OPENXML的word文档插入 合并 替换操作系列之word文件合并[支持多文件]

(五) JAVA基于OPENXML的word文档插入 合并 替换操作系列之word文件合并[支持多文件]

时间:2019-09-27 04:02:51

相关推荐

(五) JAVA基于OPENXML的word文档插入 合并 替换操作系列之word文件合并[支持多文件]

(五)、JAVA基于OPENXML的word文档插入、合并、替换操作系列之word文件合并[支持多文件]

二、word合并的多种方案简单比较三、基于Open Xml WordprocessingML的word合并step 0、准备工作step 1、命名空间合并step 2、文档内图片合并step 3、内容区域合并step 4、合并整合输出整点总结显得流弊

二、word合并的多种方案简单比较

基于jacob控件的实现

jacob一个Java-com中间件,通过这个组件利用Java程序调用win上面的dll来操作office实现文件的处理,这个因为是基于office本身的功能,效果上还可以,不过受限了操作系统,如果你的程序要放在linux上部署,这个就无法实现了,可以参考一下这位仁兄的 参考案例基于pageoffice的实现

上面jacob是基于java后端的,那这个就是基于前端的, pageoffice是一个基于类似于ActiveX类的控件,它是将office以控件的形式加载到浏览器端来操作,当然我们只需要懂js,通过js api来调用就可以,这个方案解决了跨平台的问题,但pageoffice是一款商业软件,是需要收费的,具体也可参考一下这位仁兄的参考案例基于docx4j的实现

docx4j还是挺强大的,我挺看好它,所以我认为这个实现是比较好的一种方案了,但实际上我自己没怎么用这个方案,因为在我自己折腾出办法以后才发现了它,我也就懒得去改了,并且我比较信赖自己的劳动成果(请允许我自恋一下),有兴趣可以看看这位仁兄的 docx的拆分和合并 还有这个 参考案例基于POI的实现

这个方案可能是网上使用比较多的,我曾经也用过这个方案, 但这个方案似乎对于复杂的文档并不那么友好,比如有图片的文档似乎合并后图片会有问题,当然不排除个人对它的了解层度不够深或许也有别的办法能处理好,只是我不知道, 具体可以自行尝试一下,参考案例其他实现方案

似乎像itext这类库也可以、还有一些偏门的独家秘方的我了解的就不多了, 我能想到的还有比如通过python、.NET等一些语言把这个功能单独写成一个独立的服务,然后通过java去调用这个服务来实现也是可以的,只是看你愿不愿意这么做,值不值得在这个事上大展身手了。

三、基于Open Xml WordprocessingML的word合并

前面在word基础篇时提过,word解压后核心的一个文件document.xml它里面就是WordprocessingML的结构,这算是word的老底了,我们的合并就是基于这货的,所以我这个方法比较粗鲁,但是透彻呀!

广告插播: 建议你往下看之前,如果你不了解WordprocessingML也没看过我前面的几篇笔记,强烈建议你先点上面传送过去看看,尤其是系列之二《图片在word结构中的存放、插入、替换图片》,广告完毕!

还记得我前面系列笔记之一《基础篇》中那个word文档中的document.xml文件的结构吗,它是一个有着特殊格式的xml文件,精简一下它是这样的:

<xml-fragment xmlns:w="/wordprocessingml//main" xmlns:m="/officeDocument//math" …… 省略更多 ……><!-- 以上为内容区、截取一部分 --><w:p><w:pPr><w:spacing w:after="450"/><w:ind w:left="120"/><w:jc w:val="center"/></w:pPr><w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="8466666" cy="5872268"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="0" name="" descr=""/><wp:cNvGraphicFramePr><a:graphicFrameLocks noChangeAspect="true"/></wp:cNvGraphicFramePr><a:graphic><a:graphicData uri="/drawingml//picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="1" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rId4"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="8466666" cy="5872268"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p><w:sectPr><w:pgSz w:w="11907" w:h="16839" w:code="9"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></xml-fragment>

如果单纯的把上面的东西当一个xml看的话,xml-fragment应该叫根节点,它里面定义了许多的xmlns:xx,我们把它叫作命名空间,每个文件所包含的命名空间都不一样,所以整体的文件合并都要包含命名空间、内容、图片三块,合并大致分作以下五个步骤进行

step 0、准备工作

首先要加载文件,并把它转换为上面的xml文件格式,这里我也用到了docx4j、poi、dom4j中的一些东西,来对word、WordprocessingML的快速解析与处理.

在这里我准备了两个文件,source1.docxsource2.docx,它们的内容如下:

然后通过代码读入source1.docxsource2.docx如下:

// 本例借用dom4j来操作xml (WordprocessingML)SAXReader saxReader = new SAXReader();//取source1的内容,并转换 document来操作XWPFDocument source1 = new XWPFDocument(OPCPackage.open("/Users/tenney/Desktop/source1.docx"));CTBody templateBody = source1.getDocument().getBody();org.dom4j.Document document1 = saxReader.read(new ByteArrayInputStream(templateBody.xmlText().getBytes(Charset.defaultCharset())));//读取 source2的内容XWPFDocument source2 = new XWPFDocument(OPCPackage.open("/Users/tenney/Desktop/source2.docx"));CTBody templateBody2 = source1.getDocument().getBody();org.dom4j.Document document2 = saxReader.read(new ByteArrayInputStream(templateBody.xmlText().getBytes(Charset.defaultCharset())));

step 1、命名空间合并

我们知道每个文档转成WordprocessingML格式xml后,根节点xml-fragment下面都有xmlns:xx的“命名空间”,要确保合并后的文档所有内容能被识别,这些xmlns:xx也必须被合并,这一步我们要做的事就是这个。

我们以source1.docx为目标文件,将source2.docx中的内容读取过来:

//通过正则,解析形如: xmlns:w16="/office/word//wordml" ,转换为 键值对,//schems 的结构为: xmlns:w16 = { xmlns:w16 : "/office/word//wordml" }Map<String, NameValuePair<String>> schemas = new HashMap<>();Matcher matcher = pile("^<\\S+?\\s+([^><]+\\=[^><]+)>").matcher(templateBody2.xmlText());if (matcher.find()){String[] schemaArr = matcher.group(1).split("\\s+");for (String attr : schemaArr){String[] s = attr.split("=");if(s.length > 1){schemas.put(s[0], new NameValuePair<>(s[0], s[1]));}else {//logger.warn("不符合要求的xmlns定义:{}", attr);}}}/*** 执行合并, 通过dom4j的方法获取 source1的根节点, 也就是 xml-fragment,然后 通过 addAttribute() 来将 source2中的所有命名空间合并进去* 这个地方也可以通过字符的操作,在xml中直接合并到,但需要自己行处理重复的 命名空间, 通过dom4j的方法,不需要考虑,它会自动处理*/Element root = document1.getRootElement();schemas.values().forEach(s->root.addAttribute(s.getName(), s.getValue().replace("\"","")));

step 2、文档内图片合并

图片的合并分两步,第一步是将图片资源拷贝并追加到目标文档中, 第二步是处理图片的资源关联标识rId(如果不记得了这是什么东东,请回去看系列文章之二篇)代码如下:

//得到文档2中的所有图片List<XWPFPictureData> allPictures = source2.getAllPictures();final String PICRID_PREFIX = "_PIC_ID_PREFIX";if(allPictures != null && !allPictures.isEmpty()){//步骤一、 将文档1的图片复制到文档2中//这个对象用来存储图片在原文档中的关联的rID,以及合并到新文档中产生的新的rId关联关系Map<String,String> picMap = new HashMap<>();// 记录图片合并前及合并后的IDfor (XWPFPictureData picture : allPictures) {String before = source2.getRelationId(picture);//将原文档中的图片加入到目标文档中//一张图片对应一个<w:drawing>标签,图片存在缓存中 ,并由一个ID来标签图片ID <a:blip r:embed="rId4"/>//合并xml时,图片不会带过来,所以先添加图片,将生成的ID替换原来的IDString after = null;try {if(picture.getData() == null || picture.getData().length < 1){logger.warn("图片数据丢失:{} - {}", before, picture.getFileName());}after = source1.addPictureData(picture.getData(), picture.getPictureType());picMap.put(before, after.replace("rId",PICRID_PREFIX)); //操作_1} catch (InvalidFormatException e) {logger.warn("提取文档图片失败:{}", e.getMessage(), e);}}//步骤二、处理图片在新文档的关联关系, 此时内容尚未合并到新文档,只是为合并做做准备 // 将文档2直接转成xml,并处理掉图片rId关联关系String IMG_PATTERN = "r:embed=\"%s\"";String richText = source2.getDocument().getBody().xmlText();for (Map.Entry<String, String> set : picMap.entrySet()) {// 防止图片数量超10时,出现如 rId1 替换掉了 rId1, rId1x, rId1xxx 等,导致ID引用不正确, richText = richText.replace(String.format(IMG_PATTERN, set.getKey()), String.format(IMG_PATTERN, set.getValue()));// richText = richText.replace(set.getKey(), set.getValue());}richText = richText.replace(PICRID_PREFIX, "rId");//操作_2}

本小节可能有点绕,稍微解释一下:

关于rId: 假设文档1、文档2中各有一张图片, 那么文档1中的图片1的rId=“rId1”同理文档2中的图片1也是rId=“rId1”,如果将文档2中的图片1合并到文档1中,此时在文档1中生成的标识就变成了rId="rId2",因为文档1之前已经存在了一张图片, 所以合并内容时,也需要将该标识进行相应的替换,否则合并后的文档就是会因为资源引用错乱而无法打开, 也就是上面的步骤一PICRID_PREFIX: 此操作是为了避免图片数量巨多时, 在进行替换时,出现部分字串替换的问题,比如直接用rId5替换目标文档中的内容,将会把rId5rId50rId5x等所有ID都替换而出错,所以定义一个比较复杂的前缀来处理这个问题。

一般情况下建议先处理图片后,再处理内容,因为处理完图片后,在被合并的文档中一并处理掉图片关联rId, 然后再将处理好的内容一块合并至目标文档,避免合并到目标文档后再去替换rId不小心替换掉了不该替换的内容。

step 3、内容区域合并

这部分内容就比较简单的,简单到就是xml文件或者说字符串的合并,代码如下:

//内容合并 (这块合并比较简单,方法也比较多,反正就是xml文件的合并而已,我这里采用了dom4j来操作)org.dom4j.Document newDoc = saxReader.read(new ByteArrayInputStream(richText.getBytes(Charset.defaultCharset())));//得到待合并的文档根节点以下的所有元素List<Element> appends = newDoc.getDocument().getRootElement().elements();//sectPr 节点是用来定义文档的背景、宽度等设置的, 同一文档不能有多个,所以合并的时候忽略该节点final Set<String> mergeIgnoreNodes = new HashSet<>(Arrays.asList("sectPr"));//获取文档1的元素列表List<Element> elements = document1.getDocument().getRootElement().elements();for (Element e: appends){if(!mergeIgnoreNodes.contains(e.getName())){//从当前位置之后添加新元素,看到这行代码,是不是有点别的想法啊, 对的, 你想在什么位置插入都可以,并不一定要是追加到最后//elements.add(++idx, (Element) e.detach()); elements.add((Element) e.detach());}}

step 4、合并整合输出

没什么可说的,看输出结果吧。

//文档输出FileOutputStream fos = new FileOutputStream("/Users/tenney/Desktop/merge.docx");//将最终的内容重新设置到 word的CTBody中CTBody makeBody = CTBody.Factory.parse( document1.asXML());templateBody.set(makeBody);source1.write(fos);fos.close();

整点总结显得流弊

似乎这一堆的东西看起来有点避轻就重的感脚,为什么网上那么多简便的方法不用,非整的这么复杂,这个问题老头我想,即然你能看到这里也就不用我过多解释了。

我个人认为主要是了解它的原理,其次就是为了根据自己的需要去折腾它了,比如系列文章的后续章节,你将会看到更有趣的东西.

更多折腾可前往围观

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。