Word 文档比对系统技术文档
原创文章,转载必究
原创文章,转载必究
原创文章,转载必究
版本信息
当前版本: v2.0更新日期: 2024-11核心算法: 字符级 Diff + DOM 映射定位
效果预览(目前仅支持docx格式资源)

一、使用方式
1.1 基本使用流程
1. 上传文档 → 自动预览
├─ 方式1: 点击"选择文档1/2"按钮上传本地 .docx 文件
└─ 方式2: 在URL输入框输入在线文档地址
2. 开始比对 → 差异高亮
└─ 点击"开始比对"按钮,系统自动高亮文档差异
3. 查看结果
├─ 预览区域: 可视化查看文档差异(红色=删除,绿色=添加)
├─ 相似度指标: 实时显示文档相似度百分比
└─ 变更列表: 点击右侧"变更"按钮查看详细变更清单
1.2 功能说明
| 功能 | 说明 |
|---|---|
| 自动预览 | 文档上传后立即渲染预览,无需等待比对 |
| URL 加载 | 支持输入在线 .docx 文档 URL,自动下载并加载 |
| 精准比对 | 使用字符级 Diff 算法,对中文友好 |
| 同步滚动 | 左右文档联动滚动,可通过开关控制 |
| 相似度计算 | 基于 LCS(最长公共子序列)算法计算相似度 |
| 变更列表 | 详细列出所有新增、删除、修改的内容 |
二、核心技术架构
2.1 技术栈
// 核心依赖
- Vue 3 (Composition API)
- Element Plus (UI 组件库)
- docx-preview (Word 文档渲染)
- mammoth.js (Word 文档解析)
- diff (文本差异算法)
2.2 系统架构图
┌─────────────────────────────────────────────────────┐
│ 上传层 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 本地文件上传 │ │ URL 远程加载 │ │
│ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────────┼──────────────────┘
│ │
└──────────┬───────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 解析层 │
│ ┌──────────────────────────────────────┐ │
│ │ mammoth.js (提取纯文本) │ │
│ │ docx-preview (渲染 HTML 预览) │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 比对层 │
│ ┌──────────────────────────────────────┐ │
│ │ 1. 行级 Diff (diffLines) │ │
│ │ 2. 字符级 Diff (diffChars) │ │
│ │ 3. 词级 Diff (diffWords) │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 高亮定位层 │
│ ┌──────────────────────────────────────┐ │
│ │ DOM 映射表构建 (buildTextToNodeMap) │ │
│ │ 精准定位算法 (highlightByTextMap) │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 展示层 │
│ ┌──────────────────────────────────────┐ │
│ │ 预览模式 (精准高亮) │ │
│ │ 变更列表 (详细信息) │ │
│ │ 相似度展示 (实时计算) │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
三、关键技术方案
3.1 文档解析方案
3.1.1 双引擎解析
// 引擎1: mammoth.js - 提取纯文本用于比对
const parseWordDocument = async (file: File): Promise<string> => {
const arrayBuffer = await file.arrayBuffer()
const result = await mammoth.extractRawText({ arrayBuffer })
return result.value
}
// 引擎2: docx-preview - 渲染可视化预览
const renderDocxPreview = async (file: File, container: HTMLElement) => {
const arrayBuffer = await file.arrayBuffer()
await renderAsync(arrayBuffer, container, undefined, {
className: 'docx-wrapper',
inWrapper: true,
ignoreWidth: false,
ignoreHeight: false,
ignoreFonts: false,
breakPages: true
})
}
方案优势:
mammoth.js: 提取的纯文本准确,适合文本比对docx-preview: 完整保留 Word 格式(表格、样式、图片等)
3.2 精准比对算法
3.2.1 三级 Diff 策略
// 第一级: 行级 Diff (定位变更区域)
const lineDiffs = diffLines(text1, text2)
// 第二级: 词级 Diff (并排视图使用)
const wordDiff = diffWords(oldLine, newLine)
// 第三级: 字符级 Diff (预览模式使用 - 对中文更精准)
const charDiff = diffChars(doc1Text, doc2Text)
3.2.2 为什么预览模式使用字符级 Diff?
问题: 词级 Diff (diffWords) 对中文处理不准确
// 示例
diffWords("合同不含", "合修改同含")
// 可能结果: 删除"合" + 添加"合" + 删除"同" + 添加"修改同" (混乱)
diffChars("合同不含", "合修改同含")
// 精确结果: 删除"同不" + 添加"修改同" (准确)
解决方案: v1.1 版本从 diffWords 改为 diffChars
// v1.0 (词级 - 对中文不友好)
const fullDiff = diffWords(doc1Map.fullText, doc2Map.fullText)
// v1.1 (字符级 - 对中文友好)
const fullDiff = diffChars(doc1Map.fullText, doc2Map.fullText)
四、预览模式精准高亮算法 (核心创新)
4.1 问题背景
挑战: docx-preview 渲染的 HTML 结构非常复杂
<!-- 一行文本可能分散在多个节点 -->
<p>
<span>1.2本合</span>
<span>同</span>
<span>不含增值税</span>
</p>
传统方案的问题:
简单的文本搜索容易误匹配(重复文本)上下文匹配在复杂 HTML 中不可靠跨节点的文本难以精准定位
4.2 创新方案: 文本-DOM 映射表
4.2.1 核心思想
将整个文档视为连续的字符流,为每个字符建立与 DOM 节点的映射关系
4.2.2 数据结构设计
interface TextPosition {
node: Text // 所属的 DOM 文本节点
nodeOffset: number // 在节点内的偏移量
globalOffset: number // 在整个文档中的全局偏移量
}
interface TextNodeMap {
fullText: string // 文档的完整文本
positions: TextPosition[] // 每个字符的位置信息
}
4.2.3 映射表构建算法
const buildTextToNodeMap = (element: HTMLElement): TextNodeMap => {
const positions: TextPosition[] = []
let fullText = ''
// 使用 TreeWalker 按文档顺序遍历所有文本节点
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null
)
let node: Node | null
while (node = walker.nextNode()) {
const textNode = node as Text
const text = textNode.textContent || ''
if (!text) continue
// 为文本节点中的每个字符建立映射
for (let i = 0; i < text.length; i++) {
positions.push({
node: textNode,
nodeOffset: i,
globalOffset: fullText.length + i
})
}
fullText += text
}
return { fullText, positions }
}
时间复杂度: O(n),n 为文档字符总数
空间复杂度: O(n)
4.3 精准定位高亮算法
4.3.1 核心流程
const highlightByTextMapAtPosition = (
map: TextNodeMap,
globalOffset: number, // 从 diff 算法得到的精确位置
length: number, // 要高亮的字符长度
className: string
): boolean => {
// 1. 从映射表中获取起始和结束位置
const startPos = map.positions[globalOffset]
const endIndex = globalOffset + length - 1
const endPos = map.positions[endIndex]
if (!startPos || !endPos) return false
// 2. 情况A: 文本在同一节点内
if (startPos.node === endPos.node) {
highlightTextNode(startPos.node, startPos.nodeOffset, length, className)
return true
}
// 3. 情况B: 文本跨越多个节点
const nodesToHighlight = new Map<Text, { start: number, end: number }>()
for (let i = globalOffset; i <= endIndex; i++) {
const pos = map.positions[i]
const existing = nodesToHighlight.get(pos.node)
if (existing) {
existing.end = pos.nodeOffset // 扩展范围
} else {
nodesToHighlight.set(pos.node, {
start: pos.nodeOffset,
end: pos.nodeOffset
})
}
}
// 4. 对每个节点进行高亮
nodesToHighlight.forEach((range, node) => {
const nodeLength = range.end - range.start + 1
highlightTextNode(node, range.start, nodeLength, className)
})
return true
}
4.3.2 高亮执行流程
// 主流程: 对整个文档执行字符级 diff
const fullDiff = diffChars(doc1Map.fullText, doc2Map.fullText)
let doc1Offset = 0
let doc2Offset = 0
fullDiff.forEach((part) => {
if (part.removed) {
// 在文档1中高亮删除的文本
highlightByTextMapAtPosition(
doc1Map,
doc1Offset, // 精确的全局位置
part.value.length, // 精确的长度
'diff-word-deleted'
)
doc1Offset += part.value.length
} else if (part.added) {
// 在文档2中高亮添加的文本
highlightByTextMapAtPosition(
doc2Map,
doc2Offset,
part.value.length,
'diff-word-added'
)
doc2Offset += part.value.length
} else {
// 未改变的文本,更新偏移量
doc1Offset += part.value.length
doc2Offset += part.value.length
}
})
4.4 算法优势分析
| 特性 | 传统方案 | 映射表方案 (本方案) |
|---|---|---|
| 精准度 | ❌ 易误匹配 | ✅ 100% 精准 |
| 跨节点支持 | ❌ 需要复杂逻辑 | ✅ 自动支持 |
| 性能 | ⚠️ O(n*m) 搜索 | ✅ O(1) 查找 |
| 可维护性 | ❌ 复杂上下文逻辑 | ✅ 清晰简洁 |
| 中文支持 | ⚠️ 一般 | ✅ 完美 |
核心优势:
绝对精准: 使用全局偏移量,不会误匹配性能优秀: O(1) 时间复杂度定位任意位置跨节点无缝: 自动处理跨多个 DOM 节点的文本对中文友好: 配合 diffChars 完美支持中文
五、相似度计算算法
5.1 LCS (最长公共子序列) 算法
const longestCommonSubsequence = (str1: string, str2: string): number => {
const m = str1.length
const n = str2.length
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
}
}
}
return dp[m][n]
}
const calculateSimilarity = (str1: string, str2: string): number => {
if (str1 === str2) return 1
if (str1.length === 0 || str2.length === 0) return 0
const lcs = longestCommonSubsequence(str1, str2)
return (2 * lcs) / (str1.length + str2.length)
}
相似度分级:
🟢 高相似度: ≥ 90%🟡 中等相似度: 70% – 90%🔴 低相似度: < 70%
六、同步滚动优化方案
6.1 技术实现
// 1. 节流函数 (限制执行频率)
const throttle = (func: Function, delay: number) => {
let lastCall = 0
return (...args: any[]) => {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
func(...args)
}
}
}
// 2. RAF (requestAnimationFrame) 平滑滚动
const syncScrollPosition = (source: HTMLElement, target: HTMLElement) => {
if (!syncScroll.value || isScrolling.value) return
isScrolling.value = true
// 计算滚动比例
const ratio = source.scrollTop / (source.scrollHeight - source.clientHeight)
const targetTop = ratio * (target.scrollHeight - target.clientHeight)
// 使用 RAF 确保流畅
requestAnimationFrame(() => {
target.scrollTop = targetTop
})
// 16ms 后解锁 (60fps)
setTimeout(() => { isScrolling.value = false }, 16)
}
// 3. 节流滚动事件 (60fps)
const onPreview1Scroll = throttle((event: Event) => {
if (previewContainer2.value && syncScroll.value) {
syncScrollPosition(event.target, previewContainer2.value)
}
}, 16)
性能优化:
节流控制频率 (60fps)RAF 与浏览器刷新同步避免无限循环的锁机制
七、版本演进历史
v1.0 (初始版本)
✅ 基础文档比对功能✅ 并排视图 + 预览模式⚠️ 使用词级 diff (中文支持不佳)⚠️ 上下文匹配定位 (不够精准)
v1.1 (字符级优化)
✅ 改用字符级 diff (diffChars)✅ 完善中文支持✅ Unicode 诊断功能
v2.0 (映射表方案 – 当前版本)
🚀 创新的文本-DOM映射表算法🚀 100% 精准定位🚀 移除并排视图,专注预览模式🚀 添加相似度显示🚀 自动预览功能🚀 支持 URL 加载🚀 丝滑同步滚动优化
八、性能指标
8.1 性能测试数据
| 文档大小 | 字符数 | 映射表构建 | Diff 计算 | 高亮渲染 | 总耗时 |
|---|---|---|---|---|---|
| 小型 | 5K | 10ms | 20ms | 15ms | ~50ms |
| 中型 | 50K | 80ms | 150ms | 100ms | ~350ms |
| 大型 | 200K | 300ms | 800ms | 400ms | ~1.5s |
8.2 优化建议
针对大型文档:
考虑分块处理 (chunk-based processing)使用 Web Worker 进行后台计算实现虚拟滚动减少 DOM 节点
九、已知限制与未来规划
9.1 当前限制
格式差异: docx-preview 可能无法完美还原所有 Word 格式大文档性能: 超大文档 (>500K 字符) 可能卡顿复杂表格: 表格内文本跨单元格时定位可能不准确
9.2 未来规划
支持 PDF 文档比对 导出比对报告 (Word/PDF格式) 支持文档版本管理 添加评论和批注功能 支持多文档批量比对
十、开发者指南
10.1 本地开发
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev
# 构建生产版本
pnpm build
10.2 核心文件说明
src/views/betWord3/
├── index.vue # 主组件 (3600+ 行)
├── README.md # 本技术文档
└── 核心函数说明:
├── parseWordDocument() # 文档解析
├── compareTexts() # 文本比对
├── buildTextToNodeMap() # 映射表构建 ⭐
├── highlightByTextMapAtPosition() # 精准定位 ⭐
├── highlightDifferencesInPreview() # 高亮执行
└── syncScrollPosition() # 同步滚动
10.3 调试技巧
// 开启详细日志
console.log('[映射] 文档1:', doc1Map.fullText.length, '字符')
console.log('[映射] 文档2:', doc2Map.fullText.length, '字符')
console.log('[完成] 高亮', highlightCount.deleted, '处删除')
// Unicode 诊断 (用于排查字符编码问题)
getUnicodeInfo(text) // 输出: "U+4E0D U+0074"
十一、FAQ
Q1: 为什么有些字符被标记为删除又添加?
A: v1.0 使用词级 diff,对中文支持不佳。v2.0 已改用字符级 diff,问题已解决。
Q2: 如何提高大文档的比对速度?
A:
使用 URL 加载避免本地读取开销考虑升级到支持 Web Worker 的版本减少文档格式复杂度
Q3: 能否自定义高亮颜色?
A: 可以通过修改 CSS 实现:
:deep(.diff-word-deleted) {
background-color: #your-color;
}
附录 A: 参考资料
diffChars API – npm diffdocx-preview – GitHubTreeWalker API – MDN最长公共子序列 – Wikipedia
文档维护: iuleon97(妖怪不慌不张)
最后更新: 2024年11月


