如何用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




