مقارنة النصوص
قارن بين نصّين جنبًا إلى جنب وأبرز الفروق سطرًا بسطر
النص أ
النص ب
ما هي أداة مقارنة النصوص (Text Diff)؟
مقارنة النصوص (text diff، اختصار لـ"difference") هي نتيجة مقارنة مقطعَي نص وتحديد الأسطر التي أُضيفت أو حُذفت أو بقيت دون تغيير. يعود مفهوم diff إلى أداة Unix الشهيرة التي صدرت عام 1974 ضمن الإصدار الخامس من Unix. واليوم، تُشكّل مقارنة النصوص العمود الفقري لأنظمة التحكم في الإصدارات كـGit، إذ يخزّن كل commit فروقًا (diff) بدلًا من نسخة كاملة من كل ملف.
تعمل خوارزمية diff بإيجاد أطول تسلسل مشترك (LCS) بين سلسلتَي أسطر. الأسطر الموجودة في LCS تُصنَّف على أنها غير متغيرة. الأسطر الموجودة في النص الأصلي فقط خارج LCS تُصنَّف على أنها محذوفة. والأسطر الموجودة في النص المعدَّل فقط تُصنَّف على أنها مضافة. والنتيجة هي الحد الأدنى من التغييرات اللازمة لتحويل نص إلى الآخر.
يظهر الإخراج في عدة صيغ. تُضيف صيغة unified diff (الافتراضية لـgit diff) علامة ناقص (-) للأسطر المحذوفة وعلامة زائد (+) للأسطر المضافة. وتعرض صيغة المقارنة الجانبية كلا النصين في عمودين متوازيين. تستخدم هذه الأداة المقارنة سطرًا بسطر مع إخراج ملوّن: أخضر للإضافات، وأحمر للحذف، ومحايد للأسطر غير المتغيرة. تُعرض الأسطر غير المتغيرة بدون بادئة افتراضيًا، ويمكن إخفاؤها للتركيز على ما تغيّر فقط.
لماذا تستخدم أداة مقارنة نصوص عبر الإنترنت؟
مقارنة النصوص في الطرفية تتطلب تثبيت أدوات diff والتعامل مع خيارات سطر الأوامر. أداة المقارنة المستندة إلى المتصفح تُغنيك عن كل هذا.
حالات استخدام مقارنة النصوص
مقارنة صيغ إخراج diff
تُنتج أدوات diff الإخراج في عدة صيغ. يلخّص الجدول أدناه أكثرها شيوعًا، وما الذي يُولّدها، ومتى تكون كل منها مفيدة.
| الصيغة | الأداة / المصدر | الوصف |
|---|---|---|
| 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 |
كيف تعمل مقارنة الأسطر: خوارزمية LCS
تستخدم معظم أدوات مقارنة الأسطر، بما فيها هذه الأداة، خوارزمية أطول تسلسل مشترك (LCS). تجد LCS أكبر مجموعة من الأسطر التي تظهر في كلا النصين بنفس الترتيب النسبي، دون اشتراط أن تكون متجاورة. الأسطر غير الموجودة في LCS هي الفروق الفعلية.
تستخدم خوارزمية LCS القياسية البرمجة الديناميكية وتعمل بزمن O(m x n)، حيث m وn هما عدد أسطر النصين. بالنسبة للملفات الكبيرة، تُقلّل المتغيرات المحسّنة كخوارزمية Myers' diff (المستخدمة في Git) هذا إلى O(n + d^2) حيث d هو عدد الفروق، مما يجعلها سريعة عندما تكون معظم الأسطر مشتركة.
أمثلة على التعليمات البرمجية
تطبيقات للمقارنة النصية سطرًا بسطر في JavaScript وPython وGo وسطر الأوامر. كل مثال يُنتج إخراجًا بصيغة 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