You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apache POI Java替换docx文本变量并保留格式的问题求助

Apache POI替换Docx文档变量时的格式保留问题

我正在实现一个用Apache POI替换docx文档中变量的功能,文档里的变量用<<变量名>>标记(比如“在<<日期>>,<<姓名>>完成了<<任务>>。”)。目标是编写一个接收XWPFParagraph和Map<String, String>参数的方法,遍历段落的run,把变量替换成对应的值。

现有最新代码及问题

以下是当前实现的核心替换方法:

private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, String> variableValues) {
        List<XWPFRun> runs = paragraph.getRuns();
        StringBuilder paragraphText = new StringBuilder();

        // Step 1: Gather all text from the paragraph
        for (XWPFRun run : runs) {
            String text = run.getText(0);
            if (text != null) {
                paragraphText.append(text);
            }
        }

        // Step 2: Perform replacements on the complete text
        String replacedText = paragraphText.toString();
        for (Map.Entry<String, String> entry : variableValues.entrySet()) {
            replacedText = replacedText.replace("<<" + entry.getKey() + ">>", entry.getValue());
        }

        // Step 3: Clear all existing runs
        for (XWPFRun run : runs) {
            run.setText("", 0);
        }

        // Step 4: Add the replaced text back to the paragraph
        if (!replacedText.isEmpty()) {
            XWPFRun newRun = paragraph.createRun();
            newRun.setText(replacedText);
        }
    }

这个版本能完成基本替换,但存在两个核心格式问题:

  • 字体大小异常变更(比如从11号变为10号)
  • 若变量本身带加粗格式,替换后的值无法正确继承该格式,同一文档内部分段落完全加粗、部分段落格式混乱

文档遍历代码

以下是遍历整个文档(正文、页眉、页脚、表格)的方法:

public void replaceVariables(XWPFDocument document, Map<String, String> variableValues) {
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replaceTextInParagraph(paragraph, variableValues);
        }
        for (XWPFHeader header : document.getHeaderList()) {
            for (XWPFParagraph paragraph : header.getParagraphs()) {
                replaceTextInParagraph(paragraph, variableValues);
            }
            processTables(header.getTables(), variableValues);
        }
        for (XWPFFooter footer : document.getFooterList()) {
            for (XWPFParagraph paragraph : footer.getParagraphs()) {
                replaceTextInParagraph(paragraph, variableValues);
            }
            processTables(footer.getTables(), variableValues);
        }
        processTables(document.getTables(), variableValues);
    }
    private void processTables(List<XWPFTable> tables, Map<String, String> variableValues) {
        for (XWPFTable table : tables) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    for (XWPFParagraph paragraph : cell.getParagraphs()) {
                        replaceTextInParagraph(paragraph, variableValues);
                    }
                }
            }
        }
    }

尝试过的其他方案

以下是两个目前最接近需求的尝试版本,但仍存在问题:

方案一

private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, String> variableValues) {
        List<XWPFRun> runs = paragraph.getRuns();
//        List<XWPFRun> newRuns = new ArrayList<>();
        StringBuilder paragraphText = new StringBuilder();

        for (XWPFRun run : runs) {
//            XWPFRun newRun = run;
//            newRun.setText("", 0);
//            newRuns.add(newRun);

            String text = run.getText(0);
            if (text != null) {
                paragraphText.append(text);
            }
        }

        String replacedText = paragraphText.toString();
        for (Map.Entry<String, String> entry : variableValues.entrySet()) {
            replacedText = replacedText.replace("<<" + entry.getKey() + ">>", entry.getValue());
        }

//        while (!paragraph.getRuns().isEmpty()) {
//            paragraph.removeRun(0);
//        }
        String [] textParts = replacedText.split(" ");

        for (int i = 0; i < runs.size(); i++) {
            int j = 0;
            runs.get(i).setText(textParts[j], 0);

            paragraph.addRun(runs.get(i));
        }
//
//        XWPFRun newRun = paragraph.createRun();
//        newRun.setText(replacedText);
    }

方案二

private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, String> variableValues) {
        StringBuilder paragraphText = new StringBuilder();
        List<XWPFRun> runs = paragraph.getRuns();

        // First, get the entire text of the paragraph
        for (XWPFRun run : runs) {
            String text = run.getText(0);
            if (text != null) {
                paragraphText.append(text);
            }
        }

        // Perform the replacement on the entire paragraph text
        String replacedText = paragraphText.toString();
        for (Map.Entry<String, String> entry : variableValues.entrySet()) {
            replacedText = replacedText.replace("<<" + entry.getKey() + ">>", entry.getValue());
        }

        // Split the replaced text back into runs
        int currentIndex = 0;
        for (XWPFRun run : runs) {
            String text = run.getText(0);
            if (text != null) {
                int remainingLength = replacedText.length() - currentIndex; // Calculate remaining length in the replaced text
                String subText;

                if (text.length() <= remainingLength) {
                    // Safe to use the entire length of the run
                    subText = replacedText.substring(currentIndex, currentIndex + text.length());
                } else {
                    // Limit the length to the remaining text length
                    subText = replacedText.substring(currentIndex);
                }

                run.setText(subText, 0);  // Update text in each run
                currentIndex += subText.length();
            }
        }
    }

核心需求约束

  • 必须保证文档格式一致性:移除现有run并创建新run会丢失文档中的图片
  • 不能出现文本截断问题(比如姓名、日期被截断)
  • 不允许使用Bookmarks实现

内容的提问来源于stack exchange,提问作者Vihren Pironski - Sb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:28:11