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

Syncfusion Word文档对象对比:差异未完全检测的问题求助

Syncfusion Word文档文本对比功能优化方案

问题背景

我通过Syncfusion Word编辑器生成了两个SFDT格式的文档对象,需要对比文本的变更情况,但当前实现的对比功能仅能检测约50%的改动,无法识别全部差异,寻求可行的解决方案。

现有对比代码

const diff = require('deep-diff');

export function DocumentDiffer(sfdtNewest: string, sfdtOlder: string): string {
  if (!sfdtNewest || !sfdtOlder) return JSON.stringify(sfdtNewest);

  const newest = JSON.parse(sfdtNewest);
  const older = JSON.parse(sfdtOlder);

  let newestSections = newest.sections;
  let olderSections = older.sections;
  newestSections.map((newestSection: any, indexSection: number) => {

    newestSection.blocks.map((block: any, indexBlock: number) => {
      let addedIndexes: number = 0;

      block.inlines.map((inline: any, indexInline: number) => {
        let finalIndex = indexInline + addedIndexes;

        const lineToCompare = newestSections[indexSection].blocks[indexBlock].inlines[finalIndex];

        // TODO: compare in older inlines
        const oldSectionExist: boolean = olderSections.length >= indexSection;
        const oldBlockExist: boolean = olderSections[indexSection].blocks.length >= indexBlock;
        const oldInlineExist: boolean = olderSections[indexSection].blocks[indexBlock].inlines.length >= finalIndex;
        const oldInlines: boolean = olderSections[indexSection].blocks[indexBlock].inlines[finalIndex] != undefined;
        if (oldSectionExist && oldBlockExist && oldInlineExist && oldInlines && lineToCompare) {

          const oldLine = olderSections[indexSection].blocks[indexBlock].inlines[finalIndex];
          const nextLine = newestSections[indexSection].blocks[indexBlock].inlines.length > finalIndex ? newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] : null;
          const next2ndLine = newestSections[indexSection].blocks[indexBlock].inlines.length > (finalIndex + 1) ? newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 2] : null;
          const nextLineInOld = olderSections[indexSection].blocks[indexBlock].inlines.length > finalIndex ? olderSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] : null;
          const changes = diff(oldLine, lineToCompare);

          if (nextLine && !nextLineInOld) {
            // text added at the end
            nextLine.characterFormat.fontColor = "#45b464"
            newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] = nextLine;
          } else {
            if (changes) {
              changes.map((change: any) => {
                if (change.kind == 'E') {

                  if (change.path[0] == 'text') {
                    if (change.lhs != " " && change.lhs != "") {
                      // removed
                      if (nextLine && nextLine.text == oldLine.text || (next2ndLine && next2ndLine.text == oldLine.text)) {
                        addedIndexes += (nextLine && nextLine.text == oldLine.text ? 1 : 2);
                      } else {
                        oldLine.characterFormat.fontColor = "#b44545"
                        oldLine.characterFormat.strikethrough = "SingleStrike"

                        const inlines = newestSections[indexSection].blocks[indexBlock].inlines;
                        const iinlines = [
                          ...inlines.slice(0, finalIndex),
                          oldLine,
                          ...inlines.slice(finalIndex)
                        ];
                        newestSections[indexSection].blocks[indexBlock].inlines = iinlines;
                      }
                    }
                    if (change.rhs != " " && change.rhs != "") {
                      // added
                      lineToCompare.characterFormat.fontColor = "#00fff6"
                    }
                  }
                }
              })
            }
          }

        } else {}
      });

    });
  });

  newest.sections = newestSections;
  return JSON.stringify(newest);
}

现有代码的核心问题

  • 索引逻辑混乱:手动维护addedIndexes处理插入行的方式,无法应对文本移动、多行连续修改等复杂场景,极易出现索引错位,导致大量差异漏检。
  • 仅匹配同位置节点:严格按章节→块→行内元素的相同索引位置对比,完全忽略文本块的新增、删除、顺序调整(比如段落移动),一旦文档结构变化就会失效。
  • diff逻辑覆盖不全:依赖deep-diff但只处理了文本字段的修改(kind: 'E'),完全忽略了节点新增(kind: 'N')、删除(kind: 'D'),以及单行文本内的局部修改场景。
  • 特殊场景处理缺失:空格、空文本的判断逻辑混乱,多行连续新增/删除的情况完全未覆盖。

优化解决方案

1. 改用文本内容级别的diff算法

放弃按节点索引硬匹配的思路,先提取两个文档的纯文本结构(保留段落/块层级),用成熟的文本diff库(比如diff-match-patch,Google开源的文本对比算法)对比内容,再将差异映射回SFDT节点结构。

2. 分层处理文档结构变化

  • 先对比章节、块的结构:新增的章节/块直接标记为绿色(新增样式),删除的章节/块插入到新文档对应位置并标记红色删除线。
  • 结构一致的块,再深入对比行内文本的差异。

3. 修复索引处理逻辑

不再手动维护addedIndexes,修改SFDT节点后重新遍历更新后的节点列表,从根源避免索引错位问题。

4. 完善全类型diff处理

使用deep-diff时,除了处理修改类型(E),还要覆盖:

  • N:新增的行内节点,直接标记为新增样式。
  • D:删除的行内节点,插入到新文档对应位置并标记删除样式。

优化后的示例代码

import diff from 'deep-diff';
import DiffMatchPatch from 'diff-match-patch';

export function DocumentDiffer(sfdtNewest: string, sfdtOlder: string): string {
  if (!sfdtNewest || !sfdtOlder) return JSON.stringify(sfdtNewest);

  const newest = JSON.parse(sfdtNewest);
  const older = JSON.parse(sfdtOlder);
  const dmp = new DiffMatchPatch();

  // 遍历所有章节
  newest.sections.forEach((newSection, sectionIdx) => {
    const oldSection = older.sections?.[sectionIdx];
    if (!oldSection) {
      // 整个章节为新增,标记所有内容
      newSection.blocks.forEach(block => {
        block.inlines.forEach(inline => {
          inline.characterFormat = inline.characterFormat || {};
          inline.characterFormat.fontColor = "#45b464";
        });
      });
      return;
    }

    // 遍历章节内的块
    newSection.blocks.forEach((newBlock, blockIdx) => {
      const oldBlock = oldSection.blocks?.[blockIdx];
      if (!oldBlock) {
        // 整个块为新增,标记所有行内元素
        newBlock.inlines.forEach(inline => {
          inline.characterFormat = inline.characterFormat || {};
          inline.characterFormat.fontColor = "#45b464";
        });
        return;
      }

      // 提取块内文本做内容级diff
      const newFullText = newBlock.inlines.map(i => i.text).join('');
      const oldFullText = oldBlock.inlines.map(i => i.text).join('');
      const diffs = dmp.diff_main(oldFullText, newFullText);
      dmp.diff_cleanupSemantic(diffs);

      // 将diff结果映射回行内节点
      let textCursor = 0;
      newBlock.inlines.forEach(inline => {
        inline.characterFormat = inline.characterFormat || {};
        const inlineLen = inline.text.length;
        
        for (const [diffType, diffText] of diffs) {
          if (textCursor >= inlineLen) break;
          if (inline.text.startsWith(diffText, textCursor)) {
            if (diffType === 1) {
              // 标记新增文本
              inline.characterFormat.fontColor = "#45b464";
            } else if (diffType === -1) {
              // 插入删除的文本节点
              const deletedInline = JSON.parse(JSON.stringify(inline));
              deletedInline.text = diffText;
              deletedInline.characterFormat.fontColor = "#b44545";
              deletedInline.characterFormat.strikethrough = "SingleStrike";
              const insertPos = newBlock.inlines.indexOf(inline);
              newBlock.inlines.splice(insertPos, 0, deletedInline);
            }
            textCursor += diffText.length;
          }
        }
      });

      // 处理旧块存在但新块已删除的行内节点
      oldBlock.inlines.forEach(oldInline => {
        const exists = newBlock.inlines.some(newInline => newInline.text === oldInline.text);
        if (!exists) {
          oldInline.characterFormat = oldInline.characterFormat || {};
          oldInline.characterFormat.fontColor = "#b44545";
          oldInline.characterFormat.strikethrough = "SingleStrike";
          newBlock.inlines.push(oldInline);
        }
      });
    });
  });

  return JSON.stringify(newest);
}

内容的提问来源于stack exchange,提问作者Daniel Díaz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 05:59:59