Diff:無處不在的差異計算
從 Git 版本管理到程式碼審查,從設定變更到資料同步——Diff 是軟體開發的基礎操作。
| 場景 |
Diff 類型 |
工具 |
| 程式碼變更 |
行級 Diff |
Git |
| 設定對比 |
鍵值 Diff |
線上對比工具 |
| JSON 資料 |
結構化 Diff |
json-diff |
| 法律文件 |
語意 Diff |
專用工具 |
| 翻譯校對 |
字元級 Diff |
線上對比 |
一、Myers Diff 演算法原理
核心思想
Myers 演算法尋找將來源文字轉換為目標文字的最短編輯腳本(SES)——最少插入/刪除操作。
來源: ABCABBA
目標: CBABAC
編輯腳本:
- A (刪除 A)
B (保留 B)
- C (刪除 C)
C (保留 C → 在前面插入 C)
A (保留 A)
B (保留 B)
- B (刪除 B)
A (保留 A)
+ C (插入 C)
演算法圖示
0 1 2 3 4 5 6 7
┌───┬───┬───┬───┬───┬───┬───┬───┐
0 │ → │ ↘ │ │ │ │ │ │ │ A
├───┼───┼───┼───┼───┼───┼───┼───┤
1 │ ↓ │ │ ↘ │ │ → │ ↘ │ │ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
2 │ │ ↓ │ │ ↘ │ ↓ │ │ ↘ │ │ C
├───┼───┼───┼───┼───┼───┼───┼───┤
3 │ │ │ ↓ │ │ │ ↓ │ │ ↘ │ A
├───┼───┼───┼───┼───┼───┼───┼───┤
4 │ │ │ │ ↓ │ ↘ │ │ ↓ │ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
5 │ │ │ │ │ ↓ │ │ │ ↓ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
6 │ │ │ │ │ │ ↓ │ ↘ │ │ A
└───┴───┴───┴───┴───┴───┴───┴───┘
C B A B A C B A
→ = 右移(刪除) ↓ = 下移(插入) ↘ = 對角(保留)
時間複雜度
| 情況 |
複雜度 |
說明 |
| 最優 |
O(ND) |
N=文字長度,D=差異數 |
| 最差 |
O((N+M)D) |
完全不同的文字 |
| 典型 |
O(N+D²) |
少量差異 |
二、使用工具庫文字對比
操作步驟
- 開啟 文字對比工具
- 在左側貼上原始文字
- 在右側貼上修改後文字
- 查看差異結果:
- 支援字元級差異高亮,精確到每個字元變化
對比模式
| 模式 |
粒度 |
適用場景 |
| 行級 |
按行對比 |
程式碼變更 |
| 字級 |
按詞對比 |
文件修訂 |
| 字元級 |
按字元對比 |
精確校對 |
三、JSON Diff 特殊技巧
使用 JSON Diff 工具
- 開啟 JSON Diff 工具
- 貼上兩份 JSON 資料
- 查看結構化差異:
- 新增/刪除/修改的鍵
- 值變化(含型別變化)
- 巢狀路徑
JSON Diff vs 文字 Diff
// 原始
{ "name": "张三", "age": 25, "tags": ["a", "b"] }
// 修改
{ "name": "张三", "age": 26, "tags": ["a", "c"], "email": "z@test.com" }
| 對比方式 |
結果 |
| 文字 Diff |
按行顯示差異,難以理解 |
| JSON Diff |
age: 25→26, tags[1]: "b"→"c", +email: "z@test.com" |
四、Unified Diff 格式
格式規範
--- a/config.json
+++ b/config.json
@@ -1,5 +1,6 @@
{
"name": "my-project",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "A tool",
- "private": true
+ "private": true,
+ "license": "MIT"
}
| 符號 |
含義 |
--- |
原始檔案 |
+++ |
修改檔案 |
@@ -a,b +c,d @@ |
差異位置(原始第 a 行起 b 行,修改第 c 行起 d 行) |
- |
刪除的行 |
+ |
新增的行 |
(空格) |
上下文行 |
五、補丁生成與應用
生成補丁
import * as Diff from 'diff';
const oldText = 'Hello World\nGoodbye World';
const newText = 'Hello World\nGoodbye Cruel World';
// 生成補丁
const patch = Diff.createPatch('file.txt', oldText, newText, 'old', 'new');
console.log(patch);
輸出:
--- old/file.txt
+++ new/file.txt
@@ -1,2 +1,2 @@
Hello World
-Goodbye World
+Goodbye Cruel World
應用補丁
// 正向應用
const result = Diff.applyPatch(oldText, patch);
// 反向應用(回滾)
const reversed = Diff.applyPatch(newText, Diff.reversePatch(patch));
三向合併
const base = '原始文字';
const branchA = 'A 的修改';
const branchB = 'B 的修改';
const merged = Diff.mergeDiff3(base, branchA, branchB);
// 自動合併,衝突時標記 <<<<<<< / ======= / >>>>>>>
六、實際應用場景
設定檔變更偵測
const oldConfig = JSON.parse(fs.readFileSync('config.old.json', 'utf-8'));
const newConfig = JSON.parse(fs.readFileSync('config.new.json', 'utf-8'));
const diff = Diff.diffJson(oldConfig, newConfig);
diff.forEach(part => {
if (part.added) console.log('新增:', JSON.stringify(part.value, null, 2));
if (part.removed) console.log('刪除:', JSON.stringify(part.value, null, 2));
});
翻譯檔案校對
// 對比中英文翻譯鍵的一致性
const zhKeys = Object.keys(flatten(zhTranslation));
const enKeys = Object.keys(flatten(enTranslation));
const missingInEn = zhKeys.filter(k => !enKeys.includes(k));
const missingInZh = enKeys.filter(k => !zhKeys.includes(k));
API 回應回歸測試
const expected = await fs.readFile('fixtures/expected.json', 'utf-8');
const actual = await fetch('/api/data').then(r => r.json());
const diff = Diff.diffJson(JSON.parse(expected), actual);
if (diff.some(d => d.added || d.removed)) {
throw new Error('API 回應與預期不符');
}
七、效能優化
| 優化策略 |
效果 |
適用 |
| 限制上下文行數 |
減少輸出體積 |
大檔案 |
| 雜湊預處理 |
跳過相同行 |
超大檔案 |
| 分塊對比 |
平行處理 |
超長文字 |
| 行級先於字元級 |
減少計算量 |
程式碼檔案 |
// 限制上下文行數
const patch = Diff.createPatch('file', old, new, '', '', { context: 3 });