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

Apache POI替换Word占位符后文本加粗混合格式丢失问题

问题根因

格式丢失完全是现有替换逻辑的实现缺陷导致的,核心问题有两点:

  • Word的docx格式中,所有文本格式(加粗、斜体、字体、字号等)都是绑定在w:r(即XWPFRun对象)上的,同一段落内不同格式的文本会被拆成多个独立的run存储。你处理正文段落时,把所有run的文本拼接成一个字符串,删除多余run只留第一个,再把整段替换后的文本全塞到这一个run里,自然所有非第一个run的格式全部丢失,整段只剩第一个run的默认格式。
  • 处理文本框内的内容时,你通过CTR.Factory.parse重新解析生成了新的CTR对象副本,没有直接操作原文档内的run节点,后续全量替换文本回写时也没有保留原run的格式关联,同样会导致格式丢失。
修复方案

核心原则:禁止跨run拼接整段文本后回写到单个run,替换操作尽量在单个run内完成,仅在占位符被Word自动拆分到相邻run时做最小范围的合并,全程保留原run的格式属性。
具体修改步骤:

  1. 提前把所有占位符和替换值存入Map,避免写重复的replace逻辑
  2. 封装通用的段落替换方法,正文、文本框、页眉页脚内的段落都复用这套逻辑,不要分开写不同的替换实现
  3. 处理文本框内的CTR节点时,直接做类型转换,不要通过parse新建对象副本
  4. 废弃原有拼接全段文本、删除多余run的逻辑

核心实现代码如下:

// 提前构造占位符映射
Map<String, String> placeholderMap = new HashMap<>();
placeholderMap.put("[RECEPIENT_NAME]", name);
placeholderMap.put("[ADDRESS1]", address1);
placeholderMap.put("LETTER_GEN_DT", date);
placeholderMap.put("[ID]", sbsbid);
placeholderMap.put("MEMR_RX_ID", memrid);
placeholderMap.put("MEMR_MCTR_RX_GROUP", memrgroup);
placeholderMap.put("MEMR_MCTR_RXBIN", memrrxbin);
placeholderMap.put("MEMR_MCTR_PCN", memrpcn);
// 按原有的地址判断逻辑把ADDRESS2/ADDRESS3等占位符的值提前算好存入map,不要在替换循环里写分支判断

/**
 * 通用段落占位符替换,保留原文本格式
 */
private void replaceInParagraph(XWPFParagraph paragraph, Map<String, String> placeholderMap) {
    List<XWPFRun> runs = paragraph.getRuns();
    if (runs == null || runs.isEmpty()) {
        return;
    }

    // 第一步:单个run内的完整占位符直接替换,完全不改动原run格式
    for (XWPFRun run : runs) {
        String text = run.getText(0);
        if (text == null || text.isEmpty()) {
            continue;
        }
        String replaced = text;
        for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
            replaced = replaced.replace(entry.getKey(), entry.getValue());
        }
        run.setText(replaced, 0);
    }

    // 第二步:处理被Word自动拆分到相邻run的占位符(最多连续查5个run,覆盖绝大多数自动拆分场景)
    int runCount = runs.size();
    for (int i = 0; i < runCount; i++) {
        StringBuilder segBuffer = new StringBuilder();
        for (int j = i; j < Math.min(i + 5, runCount); j++) {
            XWPFRun segRun = runs.get(j);
            String segText = segRun.getText(0);
            segText = segText == null ? "" : segText;
            segBuffer.append(segText);
            String segStr = segBuffer.toString();

            boolean hit = false;
            for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
                if (segStr.contains(entry.getKey())) {
                    hit = true;
                    segStr = segStr.replace(entry.getKey(), entry.getValue());
                    break;
                }
            }

            if (hit) {
                // 替换后的文本写入起始位置的run,保留该run的格式,其余参与合并的run清空文本
                runs.get(i).setText(segStr, 0);
                for (int k = i + 1; k <= j; k++) {
                    runs.get(k).setText("", 0);
                }
                break;
            }
        }
    }
}

原有遍历逻辑修改点:

  • 正文段落直接调用replaceInParagraph(p, placeholderMap)即可,删掉原来isPrg分支下拼接runTxt、删除run、回写文本的全部逻辑
  • 处理文本框内容时,把原来的CTR ctr = CTR.Factory.parse(obj.newInputStream())改为CTR ctr = (CTR) obj;,拿到文本框内的段落对象后同样调用replaceInParagraph方法处理,不要单独写替换逻辑

注意:如果占位符本身被手动设置了跨格式(比如占位符一半是加粗、一半是普通文本),需要提前在模板里把占位符设置为统一格式,否则替换时会以占位符起始位置的run格式为准。正常模板里的占位符都是统一格式的,不会有这个问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 13:51:28