You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何用Java正则表达式替换DOCX模板文本?解决Apache POI Run拆分问题

我太懂这种崩溃感了——用Apache POI处理docx模板时,明明是完整的%SOME_TEXT%标记,却被拆成好几个Run,导致替换代码要么找不到标记,要么替换后乱码、抛异常。之前踩过不少坑,分享几个亲测有效的解决方案:


解决方案1:合并段落内的Run再替换

Word会根据输入习惯、格式变化自动拆分Run,最简单的思路就是先把同一段落里的所有Run合并成一个,统一替换标记后再设置回去。这种方法代码简洁,适合模板标记没有混合格式的场景。

工具方法:合并段落Run

private static void mergeParagraphRuns(XWPFParagraph paragraph) {
    List<XWPFRun> runs = paragraph.getRuns();
    if (runs.size() <= 1) return;

    // 收集所有Run的文本内容
    StringBuilder mergedText = new StringBuilder();
    for (XWPFRun run : runs) {
        String runText = run.getText(0);
        if (runText != null) {
            mergedText.append(runText);
        }
    }

    // 删除原有所有Run
    int totalRuns = runs.size();
    for (int i = totalRuns - 1; i >= 0; i--) {
        paragraph.removeRun(i);
    }

    // 创建新Run并设置合并后的文本,同时保留原段落的基础格式
    XWPFRun newRun = paragraph.createRun();
    newRun.setText(mergedText.toString());
    XWPFRun originalFirstRun = runs.get(0);
    newRun.setFontFamily(originalFirstRun.getFontFamily());
    newRun.setFontSize(originalFirstRun.getFontSize());
    newRun.setBold(originalFirstRun.isBold());
    newRun.setItalic(originalFirstRun.isItalic());
}

使用示例

// 打开docx文档
XWPFDocument doc = new XWPFDocument(new FileInputStream("模板文件.docx"));

// 遍历所有段落进行替换
for (XWPFParagraph paragraph : doc.getParagraphs()) {
    mergeParagraphRuns(paragraph);
    String paragraphText = paragraph.getText();
    if (paragraphText.contains("%SOME_TEXT%")) {
        // 替换所有匹配的标记
        String replacedText = paragraphText.replaceAll("%SOME_TEXT%", "你的替换内容");
        paragraph.getRuns().get(0).setText(replacedText, 0);
    }
}

// 保存修改后的文档
try (FileOutputStream out = new FileOutputStream("替换后的文档.docx")) {
    doc.write(out);
}
doc.close();

注意:这种方法会把段落内所有Run合并成一个,所以如果段落里有不同格式的文本(比如部分加粗、变色),合并后会统一成第一个Run的格式。如果你的模板有复杂格式,建议用下面的方法。


解决方案2:逐Run拼接查找并替换(保留格式)

如果需要保留原文档的格式细节,可以通过逐Run拼接文本缓冲区,实时检查是否包含完整的模板标记,找到后精准替换对应Run的内容,同时保留其他部分的格式。

工具方法:精准替换模板标记

private static void replaceTemplateWithFormat(XWPFParagraph paragraph, Map<String, String> replacementMap) {
    List<XWPFRun> runs = paragraph.getRuns();
    StringBuilder textBuffer = new StringBuilder();
    int startRunIndex = -1;

    for (int i = 0; i < runs.size(); i++) {
        XWPFRun currentRun = runs.get(i);
        String runText = currentRun.getText(0);
        if (runText == null) continue;

        textBuffer.append(runText);

        // 检查缓冲区是否包含任意模板标记
        for (Map.Entry<String, String> entry : replacementMap.entrySet()) {
            String placeholder = entry.getKey();
            String replacement = entry.getValue();

            int placeholderPos = textBuffer.indexOf(placeholder);
            if (placeholderPos != -1) {
                // 拆分缓冲区内容:标记前、标记、标记后
                String beforePlaceholder = textBuffer.substring(0, placeholderPos);
                String afterPlaceholder = textBuffer.substring(placeholderPos + placeholder.length());

                // 设置起始Run的文本为标记前内容
                runs.get(startRunIndex == -1 ? i : startRunIndex).setText(beforePlaceholder, 0);

                // 删除中间多余的Run(如果标记跨了多个Run)
                if (startRunIndex != -1 && startRunIndex != i) {
                    for (int j = startRunIndex + 1; j <= i; j++) {
                        paragraph.removeRun(startRunIndex + 1);
                    }
                    i = startRunIndex; // 调整索引,避免遗漏后续Run
                }

                // 插入替换文本的Run,并复制原Run的格式
                XWPFRun replacementRun = paragraph.insertNewRun(i + 1);
                replacementRun.setText(replacement);
                copyRunStyle(currentRun, replacementRun);

                // 处理标记后的剩余文本
                if (!afterPlaceholder.isEmpty()) {
                    runs.get(i).setText(afterPlaceholder, 0);
                } else {
                    paragraph.removeRun(i);
                    i--; // 索引回退,避免跳过下一个Run
                }

                // 重置缓冲区和起始索引
                textBuffer.setLength(0);
                startRunIndex = -1;
                break;
            }
        }

        // 如果缓冲区还没找到完整标记,记录起始Run索引
        if (startRunIndex == -1 && textBuffer.length() > 0) {
            startRunIndex = i;
        }
    }
}

// 复制Run的格式属性
private static void copyRunStyle(XWPFRun source, XWPFRun target) {
    target.setFontFamily(source.getFontFamily());
    target.setFontSize(source.getFontSize());
    target.setBold(source.isBold());
    target.setItalic(source.isItalic());
    target.setUnderline(source.getUnderline());
    target.setColor(source.getColor());
}

使用示例

XWPFDocument doc = new XWPFDocument(new FileInputStream("模板文件.docx"));

// 定义所有需要替换的标记和内容
Map<String, String> replacements = new HashMap<>();
replacements.put("%SOME_TEXT%", "Заказчик Иванов Иван Иванович");
replacements.put("%ANOTHER_PLACEHOLDER%", "Доверенность №123 от 01.01.2024");

// 处理所有段落
for (XWPFParagraph paragraph : doc.getParagraphs()) {
    replaceTemplateWithFormat(paragraph, replacements);
}

// 保存文档
try (FileOutputStream out = new FileOutputStream("带格式的替换文档.docx")) {
    doc.write(out);
}
doc.close();

额外处理:表格中的模板标记

如果模板里的标记在表格单元格中,别忘了遍历表格进行处理:

for (XWPFTable table : doc.getTables()) {
    for (XWPFTableRow row : table.getRows()) {
        for (XWPFTableCell cell : row.getTableCells()) {
            for (XWPFParagraph paragraph : cell.getParagraphs()) {
                // 这里可以用上面任意一种替换方法
                mergeParagraphRuns(paragraph);
                String text = paragraph.getText();
                if (text.contains("%SOME_TEXT%")) {
                    text = text.replace("%SOME_TEXT%", "表格内的替换内容");
                    paragraph.getRuns().get(0).setText(text, 0);
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者kozmo

火山引擎 最新活动