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

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

  1. 基于jacob控件的实现
    jacob一个Java-com中间件,通过这个组件利用Java程序调用win上面的dll来操作office实现文件的处理,这个因为是基于office本身的功能,效果上还可以,不过受限了操作系统,如果你的程序要放在linux上部署,这个就无法实现了,可以参考一下这位仁兄的 参考案例
  2. 基于pageoffice的实现
    上面jacob是基于java后端的,那这个就是基于前端的, pageoffice是一个基于类似于ActiveX类的控件,它是将office以控件的形式加载到浏览器端来操作,当然我们只需要懂js,通过js api来调用就可以,这个方案解决了跨平台的问题,但pageoffice是一款商业软件,是需要收费的,具体也可参考一下这位仁兄的参考案例
  3. 基于docx4j的实现
    docx4j还是挺强大的,我挺看好它,所以我认为这个实现是比较好的一种方案了,但实际上我自己没怎么用这个方案,因为在我自己折腾出办法以后才发现了它,我也就懒得去改了,并且我比较信赖自己的劳动成果(请允许我自恋一下),有兴趣可以看看这位仁兄的 docx的拆分和合并 还有这个 参考案例
  4. 基于POI的实现
    这个方案可能是网上使用比较多的,我曾经也用过这个方案, 但这个方案似乎对于复杂的文档并不那么友好,比如有图片的文档似乎合并后图片会有问题,当然不排除个人对它的了解层度不够深或许也有别的办法能处理好,只是我不知道, 具体可以自行尝试一下,参考案例
  5. 其他实现方案
    似乎像itext这类库也可以、还有一些偏门的独家秘方的我了解的就不多了, 我能想到的还有比如通过python、.NET等一些语言把这个功能单独写成一个独立的服务,然后通过java去调用这个服务来实现也是可以的,只是看你愿不愿意这么做,值不值得在这个事上大展身手了。

三、基于Open Xml WordprocessingML的word合并

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

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

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

	<xml-fragment 
		xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" 			  
		xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/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="http://schemas.openxmlformats.org/drawingml/2006/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.docx source2.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="http://schemas.microsoft.com/office/word/2018/wordml" ,转换为 键值对,
        //schems 的结构为:  xmlns:w16 = { xmlns:w16  : "http://schemas.microsoft.com/office/word/2018/wordml" }
        Map<String, NameValuePair<String>> schemas = new HashMap<>();
        Matcher matcher = Pattern.compile("^<\\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<>();
            // 记录图片合并前及合并后的ID
            for (XWPFPictureData picture : allPictures) {
                String before = source2.getRelationId(picture);
                //将原文档中的图片加入到目标文档中
                //一张图片对应一个<w:drawing>标签,图片存在缓存中  ,并由一个ID来标签图片ID  <a:blip r:embed="rId4"/>
                //合并xml时,图片不会带过来,所以先添加图片,将生成的ID替换原来的ID
                String 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();

在这里插入图片描述

整点总结显得流弊

似乎这一堆的东西看起来有点避轻就重的感脚,为什么网上那么多简便的方法不用,非整的这么复杂,这个问题老头我想,即然你能看到这里也就不用我过多解释了。
我个人认为主要是了解它的原理,其次就是为了根据自己的需要去折腾它了,比如系列文章的后续章节,你将会看到更有趣的东西.

更多折腾可前往围观

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
合并多个 Word 文件,你可以使用 poi-tl 提供的方法来实现。以下是一个简单的示例: ```java import org.apache.poi.xwpf.usermodel.*; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import java.io.*; import java.util.List; public class WordMergeExample { public static void main(String[] args) { try { // 创建一个新的空白文档 XWPFDocument mergedDocument = new XWPFDocument(); // 读取第一个 Word 文件 XWPFDocument doc1 = new XWPFDocument(new FileInputStream("file1.docx")); // 复制第一个 Word 文件的内容到合并文档中 copyContent(doc1, mergedDocument); // 读取第二个 Word 文件 XWPFDocument doc2 = new XWPFDocument(new FileInputStream("file2.docx")); // 复制第二个 Word 文件的内容到合并文档中 copyContent(doc2, mergedDocument); // 保存合并后的文档 FileOutputStream outputStream = new FileOutputStream("merged.docx"); mergedDocument.write(outputStream); outputStream.close(); System.out.println("合并完成!"); } catch (IOException | InvalidFormatException e) { e.printStackTrace(); } } private static void copyContent(XWPFDocument sourceDoc, XWPFDocument targetDoc) { List<XWPFParagraph> paragraphs = sourceDoc.getParagraphs(); for (XWPFParagraph paragraph : paragraphs) { targetDoc.createParagraph().createRun().setText(paragraph.getText()); } } } ``` 在上面的示例中,我们首先创建了一个空白的目标文档 `mergedDocument`,然后使用 `copyContent` 方法将每个源文档的内容复制到目标文档中。最后,将目标文档保存为一个新的合并后的 Word 文件。 你需要将示例中的 `"file1.docx"` 和 `"file2.docx"` 替换为你要合并的实际文件路径。根据你的需求,你可以调整代码来处理更多的 Word 文件
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值