1. 概述

本教程将演示如何在Word文档的不同位置替换指定模式。我们将同时处理*.doc.docx*两种格式的文件。

2. Apache POI库

Apache POI库提供了Java API,用于操作Microsoft Office应用程序使用的各种文件格式,例如Excel电子表格、Word文档和PowerPoint演示文稿。它支持以编程方式读取、写入和修改此类文件。

要编辑*.docx文件,我们需要在pom.xml中添加最新版本的poi-ooxml*依赖:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

此外,我们还需要最新版本的*poi-scratchpad来处理.doc*文件:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>5.2.5</version>
</dependency>

3. 文件处理

我们需要创建示例文件、读取文件、替换文本并保存结果文件。下面先讨论所有与文件处理相关的内容。

3.1 示例文件

创建一个Word文档。我们将把文档中的Baeldung替换为Hello 因此,我们将在文件的不同位置(特别是表格、不同文档段落和节)写入Baeldung,并使用多种格式样式,包括单词内部有格式变化的情况。我们将使用同一个文档,分别保存为*.doc.docx*格式:

原始文档

3.2 读取输入文件

首先需要读取文件。我们将文件放在resources文件夹中,使其在类路径中可用 这样我们可以获取InputStream。对于*.doc文档,我们将基于此InputStream创建POIFSFileSystem对象,最后获取要修改的HWPFDocument对象。使用try-with-resources确保InputStreamPOIFSFileSystem自动关闭。但由于要修改HWPFDocument*,我们将手动关闭它:

public void replaceText() throws IOException {
    String filePath = getClass().getClassLoader()
      .getResource("baeldung.doc")
      .getPath();
    try (InputStream inputStream = new FileInputStream(filePath); POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream)) {
        HWPFDocument doc = new HWPFDocument(fileSystem);
        // 替换文档中的文本并保存更改
        doc.close();
    }
}

处理*.docx文档时更简单,可以直接从InputStream派生XWPFDocument*对象:

public void replaceText() throws IOException {
    String filePath = getClass().getClassLoader()
      .getResource("baeldung.docx")
      .getPath();
    try (InputStream inputStream = new FileInputStream(filePath)) {
        XWPFDocument doc = new XWPFDocument(inputStream);
        // 替换文档中的文本并保存更改
        doc.close();
    }
}

3.3 写入输出文件

我们将输出文档写入同一文件。修改后的文件将位于target文件夹中。*HWPFDocument和*XWPFDocument类都提供了write()方法,用于将文档写入OutputStream 例如,对于*.doc*文档:

private void saveFile(String filePath, HWPFDocument doc) throws IOException {
    try (FileOutputStream out = new FileOutputStream(filePath)) {
        doc.write(out);
    }
}

4. 替换*.docx*文档中的文本

尝试替换*.docx文档中的Baeldung*,并分析过程中遇到的挑战。

4.1 朴素实现

已将文档解析为XWPFDocument对象。XWPFDocument由多个段落组成。文件核心部分的段落可直接访问,但表格中的段落需要遍历所有行和单元格。暂不讨论*replaceTextInParagraph()*方法的具体实现,先展示如何将其应用到所有段落:

private XWPFDocument replaceText(XWPFDocument doc, String originalText, String updatedText) {
    replaceTextInParagraphs(doc.getParagraphs(), originalText, updatedText);
    for (XWPFTable tbl : doc.getTables()) {
        for (XWPFTableRow row : tbl.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                replaceTextInParagraphs(cell.getParagraphs(), originalText, updatedText);
            }
        }
    }
    return doc;
}

private void replaceTextInParagraphs(List<XWPFParagraph> paragraphs, String originalText, String updatedText) {
    paragraphs.forEach(paragraph -> replaceTextInParagraph(paragraph, originalText, updatedText));
}

在Apache POI中,段落被划分为XWPFRun对象。作为初步尝试,遍历所有run:如果检测到目标文本,就更新run的内容:

private void replaceTextInParagraph(XWPFParagraph paragraph, String originalText, String updatedText) {
    List<XWPFRun> runs = paragraph.getRuns();
    for (XWPFRun run : runs) {
        String text = run.getText(0);
        if (text != null && text.contains(originalText)) {
            String updatedRunText = text.replace(originalText, updatedText);
            run.setText(updatedRunText, 0);
        }
    }
}

最后,更新*replaceText()*方法包含所有步骤:

public void replaceText() throws IOException {
    String filePath = getClass().getClassLoader()
      .getResource("baeldung-copy.docx")
      .getPath();
    try (InputStream inputStream = new FileInputStream(filePath)) {
        XWPFDocument doc = new XWPFDocument(inputStream);
        doc = replaceText(doc, "Baeldung", "Hello");
        saveFile(filePath, doc);
        doc.close();
    }
}

通过单元测试运行此代码,查看更新后的文档截图:

docx朴素替换结果

4.2 局限性

如截图所示,大部分Baeldung已被替换为Hello,但仍有两处未被替换。

深入理解XWPFRun每个run代表具有相同格式属性的连续文本序列。 格式属性包括字体、大小、颜色、粗体、斜体、下划线等。每当格式变化时,就会创建新的run。因此表格中包含多种格式的Baeldung未被替换——其内容分散在多个run中。

底部蓝色Baeldung也未被替换。实际上,Apache POI不保证相同格式的字符一定在同一个run中。简单来说,朴素实现仅适用于最简单场景。✅ 在简单场景下使用此方案是合理的,因为它不涉及复杂决策。但若遇到此限制,则需要其他解决方案。

4.3 处理跨多个run的文本

为简化实现,我们做以下假设:当段落中包含Baeldung时,可以接受丢失该段落的格式。 因此,我们可以移除段落中的所有run,并用单个新run替换。重写*replaceTextInParagraph()*方法:

private void replaceTextInParagraph(XWPFParagraph paragraph, String originalText, String updatedText) {
    String paragraphText = paragraph.getParagraphText();
    if (paragraphText.contains(originalText)) {
        String updatedParagraphText = paragraphText.replace(originalText, updatedText);
        while (paragraph.getRuns().size() > 0) {
            paragraph.removeRun(0);
        }
        XWPFRun newRun = paragraph.createRun();
        newRun.setText(updatedParagraphText);
    }
}

查看结果文件:

docx完整替换结果
如预期,所有Baeldung都被替换。但大部分格式丢失了(最后一种格式未丢失,似乎Apache POI对其处理方式不同)。

⚠️ 根据实际需求,也可以选择保留原始段落的部分格式。此时需要遍历所有run,按需保留或更新格式属性。

5. 替换*.doc*文档中的文本

处理*.doc文件要简单得多。我们可以直接获取整个文档的Range对象。**然后通过其replaceText()方法修改范围内容:*

private HWPFDocument replaceText(HWPFDocument doc, String originalText, String updatedText) {
    Range range = doc.getRange();
    range.replaceText(originalText, updatedText);
    return doc;
}

运行此代码得到更新后的文件:

doc替换结果

可见替换发生在整个文件中。对于跨多个run的文本,默认行为是保留第一个run的格式。

6. 总结

本文介绍了如何在Word文档中替换文本模式。在*.doc文档中操作非常直接,但在.docx*中,简单实现会遇到限制。我们通过简化假设展示了如何克服此限制。

完整代码可在GitHub获取。


原始标题:Replacing Variables in a Document Template with Java