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

C# OpenXML合并Word文档:编号列表混乱问题求助

C# OpenXML合并Word文档列表格式问题修复方案

问题根源

  1. 列表样式覆盖:合并时未完全隔离不同文档的<w:abstractNum>和<w:num>定义,ID冲突导致后添加的列表样式覆盖之前的。
  2. 编号延续:未正确为每个源文档的列表设置独立的起始编号,且段落关联的NumId未同步更新到新的列表定义。

核心修复步骤

1. 完全隔离每个源文档的列表定义

为每个源文档的所有<w:abstractNum>和<w:num>生成全新的ID,复制到目标文档的numbering.xml中,同时生成新的nsid和name属性避免冲突。

2. 强制重置编号起始值

针对每个<w:num>条目,确保添加或重置<w:startOverride>为1,保证每个文档的编号从1开始。

3. 同步段落的列表ID映射

遍历源文档所有段落,将原NumId替换为目标文档中生成的新NumId,确保段落关联正确的列表定义。

示例代码实现

复制并重建编号定义

private static Dictionary<string, string> CopyAndRebuildNumbering(NumberingPart targetNumPart, NumberingPart sourceNumPart)
{
    var numIdMap = new Dictionary<string, string>();
    var abstractNumIdMap = new Dictionary<string, string>();
    var targetDefs = targetNumPart.NumberingDefinitionsPart ?? new NumberingDefinitionsPart();

    // 处理AbstractNum,生成新ID
    foreach (var sourceAbstractNum in sourceNumPart.NumberingDefinitionsPart.AbstractNumElements)
    {
        int newAbstractId = targetDefs.AbstractNumElements.Select(a => int.Parse(a.AbstractNumId.Value)).DefaultIfEmpty(0).Max() + 1;
        var newAbstractNum = (AbstractNum)sourceAbstractNum.CloneNode(true);
        newAbstractNum.AbstractNumId = new StringValue(newAbstractId.ToString());
        newAbstractNum.Nsid = new StringValue(Guid.NewGuid().ToString("N").Substring(0, 8));
        newAbstractNum.Name = new StringValue($"MergedList_{newAbstractId}");

        targetDefs.AppendChild(newAbstractNum);
        abstractNumIdMap[sourceAbstractNum.AbstractNumId.Value] = newAbstractId.ToString();
    }

    // 处理Num条目,生成新ID并关联新AbstractNumId
    foreach (var sourceNum in sourceNumPart.NumberingDefinitionsPart.NumElements)
    {
        int newNumId = targetDefs.NumElements.Select(n => int.Parse(n.NumId.Value)).DefaultIfEmpty(0).Max() + 1;
        var newNum = (Num)sourceNum.CloneNode(true);
        newNum.NumId = new StringValue(newNumId.ToString());
        
        // 替换关联的AbstractNumId
        string oldAbstractId = newNum.AbstractNumId.Val.Value;
        newNum.AbstractNumId.Val = new StringValue(abstractNumIdMap[oldAbstractId]);

        // 强制设置起始编号为1
        var startOverride = newNum.Descendants<StartOverride>().FirstOrDefault();
        if (startOverride == null)
        {
            startOverride = new StartOverride { Val = 1 };
            newNum.AppendChild(startOverride);
        }
        else
        {
            startOverride.Val = 1;
        }

        targetDefs.AppendChild(newNum);
        numIdMap[sourceNum.NumId.Value] = newNumId.ToString();
    }

    targetNumPart.NumberingDefinitionsPart = targetDefs;
    targetNumPart.NumberingDefinitionsPart.Save();
    return numIdMap;
}

更新段落的NumId映射

private static void UpdateParagraphNumIds(Body sourceBody, Dictionary<string, string> numIdMap)
{
    foreach (var para in sourceBody.Descendants<Paragraph>())
    {
        var numPr = para.Descendants<NumberingProperties>().FirstOrDefault();
        if (numPr != null && numPr.NumId != null && numIdMap.ContainsKey(numPr.NumId.Val.Value))
        {
            numPr.NumId.Val = new StringValue(numIdMap[numPr.NumId.Val.Value]);
        }
    }
}

合并文档主逻辑

public static void MergeWordDocs(string targetPath, List<string> sourcePaths)
{
    using (var targetDoc = WordprocessingDocument.Open(targetPath, true))
    {
        var targetBody = targetDoc.MainDocumentPart.Document.Body;
        var targetNumPart = targetDoc.MainDocumentPart.NumberingPart ?? targetDoc.MainDocumentPart.AddNewPart<NumberingPart>();

        foreach (var sourcePath in sourcePaths)
        {
            using (var sourceDoc = WordprocessingDocument.Open(sourcePath, false))
            {
                var sourceBody = sourceDoc.MainDocumentPart.Document.Body;
                var sourceNumPart = sourceDoc.MainDocumentPart.NumberingPart;

                if (sourceNumPart != null)
                {
                    var numIdMap = CopyAndRebuildNumbering(targetNumPart, sourceNumPart);
                    UpdateParagraphNumIds(sourceBody, numIdMap);
                }

                // 复制源文档内容到目标文档
                foreach (var elem in sourceBody.Elements())
                {
                    targetBody.AppendChild(elem.CloneNode(true));
                }
            }
        }

        targetDoc.MainDocumentPart.Document.Save();
    }
}

验证要点

  • 合并后检查numbering.xml,确认不同文档的<w:abstractNum>和<w:num>都有独立的ID。
  • 检查每个<w:num>下是否存在<w:startOverride w:val="1">。
  • 查看段落的<w:numId>是否对应到目标文档中新生成的ID。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 02:28:09