Diff 文字比對工具完整教學:算法、視覺化、版本控制應用

寫給:每天 git diff 看半天的工程師、想對比兩版合約文字的法務、需要找出兩份報告差異的編輯。

「找出兩個字串 / 檔案的差異」聽起來簡單,實際是 1970 年代電腦科學重要議題。Doug McIlroy 在 1976 年寫的 diff 命令至今仍是 Unix 標配。背後的演算法(Myers 1986、Patience 1993、Histogram 2008)支撐了 Git、Mercurial、SVN、Code Review 工具。本文 13 分鐘把 Diff 演算法、視覺化、Git 設定、合併衝突一次講透。

1. Diff 是什麼

Diff(differential)是計算兩個序列差異的演算法。輸入兩個文字檔,輸出最小編輯距離(minimum edit distance)的編輯腳本(add / delete / unchanged)。

原文 A:                修改 B:
1: apple              1: apple
2: banana             2: blueberry
3: cherry             3: cherry
4: date               4: date
                      5: elderberry

Diff 輸出:

- banana
+ blueberry
+ elderberry

1.1 為什麼是「最小編輯距離」

兩個字串的差異有無限種表示。例如 A→B 可說「刪 banana、加 blueberry」也可說「全刪全加」。Diff 演算法找最少 add/delete 操作的版本,因為:

  1. 對人類最易讀
  2. 對版本控制最節省空間
  3. 對 merge 衝突偵測最準

1.2 應用領域

  • 版本控制(Git / SVN)
  • Code Review(GitHub PR)
  • 文字編輯器(VS Code 比較模式)
  • 同步工具(rsync)
  • 合約比對(法務)
  • 期刊投稿(差異追蹤)

2. 5 個核心觀念

2.1 三大演算法

演算法 年份 時間複雜度 空間 特色
Myers 1986 O((N+M)D) O(D²) 經典預設,多數系統用
Patience 1993 O(NlogN+M) O(N+M) 對重排序友善
Histogram 2008 接近 O(N+M) O(N+M) Git 2.0+ 預設

Git 預設用 Histogram(速度 + 結果可讀性平衡):

git config --global diff.algorithm histogram

2.2 行 diff vs 字 diff vs 字元 diff

粒度 用途
行 程式碼(diff -u)
字(word) 文章 / 文件
字元 短句 / 命名比對

Git 預設行 diff。要 word diff:

git diff --word-diff
git diff --word-diff-regex='\w+'   # 中文需自訂 regex

對中文文章 word diff 需自訂分詞器(jieba)。

2.3 Unified diff 格式

--- a/file.txt    2026-06-05 10:00:00
+++ b/file.txt    2026-06-05 10:05:00
@@ -1,5 +1,5 @@
 apple
-banana
+blueberry
 cherry
 date
+elderberry

組件:

  • --- / +++ 標頭:兩檔路徑與時間
  • @@ -X,Y +A,B @@ hunk header:原文第 X 行起 Y 行 / 新文第 A 行起 B 行
  • 開頭:unchanged(context)
  • -:刪除
  • +:新增

git diff / GitHub PR / patch 都用此格式。

2.4 Side-by-side(並排)

| Original              | Modified              |
| 1: apple              | 1: apple              |
| 2: banana          -> | 2: blueberry          |
| 3: cherry             | 3: cherry             |
| 4: date               | 4: date               |
|                       | 5: elderberry         |

並排適合:

  • 看大量重排
  • 跨欄位 align(如表格)
  • 非程式碼文件

2.5 三方合併(3-way merge)

兩人從共同祖先各自修改 → 合併時需 3-way diff:

        common ancestor
         /          \
    branch A    branch B
         \          /
          merged

merge 演算法:

  1. 算 A vs ancestor → patch_A
  2. 算 B vs ancestor → patch_B
  3. 兩 patch 套用到 ancestor,衝突區段標出
<<<<<<< HEAD
Branch A 的修改
=======
Branch B 的修改
>>>>>>> feature

衝突解決後 git add . && git commit 完成 merge。

3. 實戰範例

範例 1:bash diff 命令

# 基本比對
diff file1.txt file2.txt

# Unified format
diff -u file1.txt file2.txt

# 忽略空白
diff -w file1.txt file2.txt
diff --ignore-all-space file1.txt file2.txt

# 比對目錄
diff -r dir1/ dir2/

# 只列檔名差異
diff -rq dir1/ dir2/

範例 2:Python difflib

import difflib

a = ["apple", "banana", "cherry"]
b = ["apple", "blueberry", "cherry", "date"]

# 行 diff
for line in difflib.unified_diff(a, b, lineterm=""):
    print(line)

# 字元 diff
diff = difflib.SequenceMatcher(None, "banana", "blueberry")
print(f"相似度: {diff.ratio():.2%}")  # 38.46%

# HTML side-by-side
html = difflib.HtmlDiff().make_file(a, b)
with open("diff.html", "w") as f:
    f.write(html)

範例 3:JavaScript(diff-match-patch)

import { diff_match_patch } from "diff-match-patch";

const dmp = new diff_match_patch();
const diffs = dmp.diff_main("apple banana cherry", "apple blueberry cherry");
dmp.diff_cleanupSemantic(diffs);

console.log(diffs);
// [[0, "apple "], [-1, "ba"], [1, "blue"], [0, "berry cherry"], [-1, "na"]]
// 0 = unchanged, -1 = delete, 1 = insert

const html = dmp.diff_prettyHtml(diffs);
document.getElementById("output").innerHTML = html;

範例 4:Git diff 進階

# 比對兩個 commit
git diff abc123..def456

# 比對某檔在 main 分支與當前
git diff main -- src/app.js

# 只看 stat
git diff --stat

# 顯示 word diff
git diff --color-words

# 忽略空白
git diff -w

# 用 vscode 當 diff tool
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
git difftool

範例 5:產 patch 並套用

# 產 patch
git diff main..feature > my.patch

# 套用 patch
git apply my.patch

# 套用並 commit
git am my.patch

# 用 patch 命令(非 git 環境)
patch -p1 < my.patch

4. 進階技巧

4.1 自訂 diff driver(特定檔案類型)

.gitattributes:

*.json diff=json
*.png  diff=image
*.docx diff=word

.gitconfig:

[diff "json"]
    textconv = jq .

[diff "image"]
    textconv = identify    # imagemagick

[diff "word"]
    textconv = pandoc --to=plain

git diff 自動先用對應 textconv 轉成可讀形式再比對。例如 docx 變成 plain text diff,不再是二進位 binary diff。

4.2 Patience algorithm(重排序友善)

Myers 在大量重排時會「拗」出怪 diff。Patience 對重排序友善:

git diff --patience
git config --global diff.algorithm patience

特別適合:refactor 後函式順序變化大的場景。

4.3 跳過某段 diff(gitattributes)

package-lock.json -diff
*.min.js -diff

-diff 標記檔案不參與 diff(顯示 "Binary files differ")。適合自動生成的大檔。

4.4 語意化 diff(semantic diff)

普通 diff 只比文字,無法理解語意。語意 diff 工具(difftastic、GumTree)解析 AST:

# 安裝
brew install difftastic

# 用作 git difftool
git config --global diff.external difft
git diff

效果:函式重排不顯示 diff、變數改名標清楚。

4.5a 二進位檔案 diff

Git 預設對二進位檔顯示 "Binary files differ"。若是文字導向二進位(如 SQLite、Excel),可設 textconv:

[diff "sqlite"]
    textconv = sqlite3 .dump
[diff "xlsx"]
    textconv = xlsx2csv

sqlite DB 變動可看 SQL diff、xlsx 可看 CSV diff。

4.5b Patch 衝突的解法層級

merge conflict 解決有三層工具:

  1. 手動編輯:適合衝突小、邏輯清楚
  2. GUI merge tool(VS Code、Beyond Compare、Meld):適合多處衝突
  3. rerere(reuse recorded resolution):Git 記住你之前的解法,下次同樣衝突自動套用
git config --global rerere.enabled true

長期分支 rebase 重複解同樣衝突時神器。

4.5 結構化資料 diff

JSON / YAML 比文字 diff 更適合用專用工具:

# jq + diff
diff <(jq -S . a.json) <(jq -S . b.json)

# 或用 json-diff
json-diff a.json b.json
# YAML
dyff between a.yaml b.yaml

對巢狀資料的 diff 比 git 預設清晰得多。

5. 常見錯誤與解法

錯誤 1:diff 完全打亂(看起來改一堆但其實只改一行)

症狀:改一行 import 但 diff 顯示整檔重寫。 根因:CRLF 與 LF 行尾不一。 解法:git config core.autocrlf input,並在 .gitattributes 統一行尾。

錯誤 2:中文文章 diff 亂跳

症狀:改一個字,整段被標 deleted + inserted。 根因:預設 word diff regex 不支援中文,整行被視為一個 word。 解法:用 difftastic 或自訂 regex:

git diff --word-diff-regex='[一-龥]|[A-Za-z]+|[0-9]+|[^ ]'

錯誤 3:merge 衝突解錯破壞別人的修改

症狀:解衝突時誤刪別人那邊的程式碼。 根因:沒看清三方差異。 解法:用 3-way merge tool(VS Code、Beyond Compare)查看 base / yours / theirs 三版。

錯誤 4:大檔 diff 卡死

症狀:git diff 一個 1MB 的 JSON 卡 30 秒。 根因:Myers 在大檔效能差。 解法:

git config diff.algorithm histogram
git config diff.renames true

或對大資料檔加 -diff 跳過。

錯誤 5:空白 diff 噪訊

症狀:trailing space、indent 改動爆滿 diff。 根因:editor / formatter 不一致。 解法:

git diff -w     # 忽略空白
# 或設定 EditorConfig 統一團隊

.editorconfig:

[*]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

6. 用 SuperTools 線上版完成相同任務

不想開 terminal 對兩段文字?打開 SuperTools Diff 工具:

  • 雙欄並排 / 上下對比:自由切換
  • 顆粒度:行 / 詞 / 字元
  • 演算法:Myers / Patience / Histogram
  • 語法 highlight:自動偵測 JS / Python / JSON / YAML
  • 忽略選項:空白 / 大小寫 / 中文標點
  • 檔案上傳:拖檔比對
  • 匯出:unified diff / patch / HTML

工具 client-side,內容不上傳,可比公司文件。

實際使用流程:

  1. 開 supertools.tw/text/diff
  2. 左貼舊版合約、右貼新版
  3. 選「Word level」+「Ignore whitespace」
  4. 標紅刪除 / 標綠新增
  5. 點「Export patch」下載 unified diff 給法務

立即使用 SuperTools Diff 工具:https://supertools.tw/text/diff

7. 其他語言實作

Python

import difflib
list(difflib.unified_diff(a_lines, b_lines))
difflib.SequenceMatcher(None, a, b).ratio()

JavaScript

import { diffLines } from "diff";
const changes = diffLines(oldText, newText);

Go

import "github.com/sergi/go-diff/diffmatchpatch"
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(text1, text2, false)

Rust(similar crate)

use similar::TextDiff;
let diff = TextDiff::from_lines("a\nb\n", "a\nc\n");
for change in diff.iter_all_changes() { println!("{:?}", change); }

Bash

diff -u a.txt b.txt
git diff --no-index a.txt b.txt    # git 也可比非 repo 檔案

8. FAQ

Q1:Myers vs Patience vs Histogram 該選哪個? Git 2.0+ 預設 Histogram 通常最佳。重排場景試 Patience。

Q2:可以比較圖片嗎? 可以用 ImageMagick compare 命令,或設 textconv = identify。

Q3:比 PDF 怎麼辦? 先用 pdftotext 轉純文字再 diff,或用 DiffPDF 視覺化工具。

Q4:merge 衝突怎麼避免? 小 PR、頻繁 rebase、避免長期分支、團隊用一致 formatter。

Q5:能比較目錄整體嗎? diff -r dir1 dir2 或 meld dir1 dir2(GUI)。

9. 實戰案例研究

案例 1:refactor 大 PR 用 difftastic 救命

某團隊 refactor 把單檔 1500 行的 service 拆成 5 個 module。普通 git diff 顯示「整檔刪除 + 新增 5 檔」,看不出邏輯哪裡改了。

換用 difftastic:

brew install difftastic
git config --global diff.external difft
git diff main..refactor

difftastic 解析 AST 後,對「移動」的函式不再標 deleted+inserted,而是標 "moved to new file"。Reviewer 從一頭霧水變成 5 分鐘讀完。

教訓:大型 refactor 必備 semantic diff 工具。

案例 2:法務合約比對

某律師事務所每月對比 100+ 份合約版本。原本人工逐行對照,每份 30 分鐘。

導入 SuperTools Diff 工具:

  1. 兩個 docx 用 pandoc 轉純文字
  2. 貼進 Diff 工具設「word level + ignore whitespace」
  3. 紅綠標註所有更動
  4. 匯出 patch 給律師審閱

每份從 30 分鐘降到 5 分鐘,月省 50+ 小時。

案例 3:自動化測試的 snapshot diff

某前端團隊用 Jest snapshot testing:

expect(component).toMatchSnapshot();

當 snapshot 改變時,Jest 顯示 diff。某次改動 100+ snapshot 一起變化,但團隊不確定改動是預期的還是 bug。

寫了個小工具:

# 過濾出「只是 className 順序變化」的 diff(無實質影響)
git diff --no-color snapshots/ \
  | grep -E "^[+-]" \
  | grep -vE 'className="[^"]+"' \
  | head -100

過濾後只剩 5 個真實的結構變化,比逐個檢查省 95% 時間。

10. 結論 + 延伸閱讀

Diff 工具是工程師日常生活的隱形 superpower。理解 Myers / Patience / Histogram 三演算法、unified format、3-way merge 原理後,你可以更快讀 PR、解 conflict、追歷史改動。下次面對複雜 diff 時,先到 SuperTools 線上工具切換粒度與演算法,找出最易讀的呈現方式。三條黃金法則:重 refactor 用 semantic diff、結構化資料用對應專用工具、減少 noise diff(空白、自動生成)。

延伸閱讀: