So Sánh Văn Bản
So sánh hai đoạn văn bản cạnh nhau và đánh dấu sự khác biệt từng dòng
Văn Bản A
Văn Bản B
Text Diff là gì?
Text diff (viết tắt của "difference" — sự khác biệt) là kết quả của việc so sánh hai khối văn bản và xác định những dòng nào được thêm, xóa hoặc giữ nguyên. Khái niệm này bắt nguồn từ tiện ích Unix diff, được phát hành lần đầu vào năm 1974 trong Version 5 Unix. Ngày nay, text diff là nền tảng của các hệ thống quản lý phiên bản như Git, nơi mỗi commit lưu trữ một diff thay vì toàn bộ bản sao của từng tệp.
Thuật toán diff tìm Dãy Con Chung Dài Nhất (Longest Common Subsequence — LCS) giữa hai chuỗi dòng. Các dòng có trong LCS được đánh dấu là không thay đổi. Các dòng trong văn bản gốc nhưng không có trong LCS được đánh dấu là đã xóa. Các dòng trong văn bản đã sửa đổi nhưng không có trong LCS được đánh dấu là đã thêm. Kết quả là tập hợp các thay đổi tối thiểu cần thiết để chuyển đổi văn bản này thành văn bản kia.
Đầu ra diff có nhiều định dạng. Unified diff (mặc định cho git diff) thêm dấu trừ trước các dòng bị xóa và dấu cộng trước các dòng được thêm. Side-by-side diff sắp xếp cả hai văn bản trong các cột song song. Công cụ này sử dụng so sánh từng dòng với đầu ra được tô màu: xanh lá cho các dòng thêm, đỏ cho các dòng xóa, và màu trung tính cho các dòng không đổi. Các dòng không đổi được hiển thị mặc định nhưng có thể ẩn đi để tập trung vào những gì đã thay đổi.
Tại sao dùng công cụ Text Diff online?
So sánh văn bản trong terminal đòi hỏi phải cài đặt tiện ích diff và xử lý các cờ dòng lệnh. Công cụ diff trên trình duyệt loại bỏ hoàn toàn sự phức tạp đó.
Các trường hợp sử dụng Text Diff
So sánh các định dạng đầu ra Diff
Các công cụ diff tạo ra đầu ra ở nhiều định dạng khác nhau. Bảng dưới đây tóm tắt các định dạng phổ biến nhất, công cụ nào tạo ra chúng và khi nào mỗi định dạng hữu ích.
| Định dạng | Công cụ / Nguồn | Mô tả |
|---|---|---|
| Unified diff | diff -u / git diff | Prefixes lines with + / - / space; includes @@ hunk headers |
| Side-by-side | diff -y / sdiff | Two columns, changed lines aligned horizontally |
| Context diff | diff -c | Shows changed lines with surrounding context, marked with ! / + / - |
| HTML diff | Python difflib | Color-coded HTML table with inline change highlights |
| JSON Patch | RFC 6902 | Array of add/remove/replace operations on a JSON document |
Cách Line Diff hoạt động: Thuật toán LCS
Hầu hết các công cụ line-diff, bao gồm công cụ này, đều sử dụng thuật toán Longest Common Subsequence (LCS). LCS tìm tập hợp dòng lớn nhất xuất hiện trong cả hai văn bản theo cùng thứ tự tương đối, mà không yêu cầu chúng liền kề nhau. Các dòng không có trong LCS chính là sự khác biệt thực sự.
Thuật toán LCS chuẩn sử dụng quy hoạch động và chạy trong thời gian O(m x n), trong đó m và n là số dòng của hai văn bản. Đối với các tệp lớn, các biến thể tối ưu hóa như thuật toán Myers' diff (được Git sử dụng) giảm xuống còn O(n + d^2) trong đó d là số lượng sự khác biệt, làm cho nó nhanh khi hầu hết các dòng được chia sẻ.
Ví dụ Code
Cài đặt so sánh văn bản từng dòng trong JavaScript, Python, Go và dòng lệnh. Mỗi ví dụ tạo ra đầu ra theo kiểu unified diff.
// Line-by-line diff using the LCS algorithm
function diffLines(a, b) {
const linesA = a.split('\n')
const linesB = b.split('\n')
// Build LCS table
const m = linesA.length, n = linesB.length
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = linesA[i-1] === linesB[j-1]
? dp[i-1][j-1] + 1
: Math.max(dp[i-1][j], dp[i][j-1])
// Backtrack to produce diff
const result = []
let i = m, j = n
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && linesA[i-1] === linesB[j-1]) {
result.unshift({ type: 'equal', text: linesA[i-1] }); i--; j--
} else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) {
result.unshift({ type: 'add', text: linesB[j-1] }); j--
} else {
result.unshift({ type: 'remove', text: linesA[i-1] }); i--
}
}
return result
}
const diff = diffLines("alpha\nbeta\ngamma", "alpha\nbeta changed\ngamma\ndelta")
// → [
// { type: 'equal', text: 'alpha' },
// { type: 'remove', text: 'beta' },
// { type: 'add', text: 'beta changed' },
// { type: 'equal', text: 'gamma' },
// { type: 'add', text: 'delta' }
// ]import difflib
text_a = """alpha
beta
gamma""".splitlines()
text_b = """alpha
beta changed
gamma
delta""".splitlines()
# Unified diff (same format as git diff)
for line in difflib.unified_diff(text_a, text_b, fromfile='a.txt', tofile='b.txt', lineterm=''):
print(line)
# --- a.txt
# +++ b.txt
# @@ -1,3 +1,4 @@
# alpha
# -beta
# +beta changed
# gamma
# +delta
# HTML side-by-side diff
d = difflib.HtmlDiff()
html = d.make_file(text_a, text_b, fromdesc='Original', todesc='Modified')package main
import (
"fmt"
"strings"
)
// Minimal LCS-based line diff
func diffLines(a, b string) {
la := strings.Split(a, "\n")
lb := strings.Split(b, "\n")
m, n := len(la), len(lb)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if la[i-1] == lb[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else if dp[i-1][j] >= dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
var result []string
i, j := m, n
for i > 0 || j > 0 {
if i > 0 && j > 0 && la[i-1] == lb[j-1] {
result = append([]string{" " + la[i-1]}, result...)
i--; j--
} else if j > 0 && (i == 0 || dp[i][j-1] >= dp[i-1][j]) {
result = append([]string{"+" + lb[j-1]}, result...)
j--
} else {
result = append([]string{"-" + la[i-1]}, result...)
i--
}
}
for _, line := range result {
fmt.Println(line)
}
}
// Output:
// alpha
// -beta
// +beta changed
// gamma
// +delta# Compare two files with unified diff (3 lines of context) diff -u original.txt modified.txt # Git diff between working tree and last commit git diff HEAD -- file.txt # Git diff between two branches git diff main..feature -- src/ # Side-by-side diff in the terminal diff -y --width=120 original.txt modified.txt # Color-coded diff (requires colordiff) diff -u original.txt modified.txt | colordiff