Text Diff
Порівнюйте два тексти поруч і виділяйте відмінності рядок за рядком
Текст A
Текст B
Що таке Text Diff?
Text diff (скорочення від «difference» — різниця) — це результат порівняння двох текстових блоків, що визначає, які рядки були додані, видалені або залишились незмінними. Концепція походить з утиліти Unix diff, вперше випущеної у 1974 році як частина Version 5 Unix. Сьогодні text diff є основою систем контролю версій, зокрема Git, де кожен коміт зберігає diff, а не повну копію кожного файлу.
Алгоритм diff знаходить Найдовшу Спільну Підпослідовність (LCS) між двома послідовностями рядків. Рядки, присутні в LCS, позначаються як незмінені. Рядки в оригінальному тексті, що не входять до LCS, позначаються як видалені. Рядки в зміненому тексті, що не входять до LCS, позначаються як додані. Результатом є мінімальний набір змін, необхідних для перетворення одного тексту на інший.
Вивід diff існує в кількох форматах. Unified diff (стандарт для git diff) позначає видалені рядки знаком мінус, а додані — знаком плюс. Side-by-side diff розташовує обидва тексти в паралельних стовпцях. Цей інструмент використовує порівняння рядок за рядком із кольоровим виведенням: зелений для доданих, червоний для видалених, нейтральний для незмінених рядків. Незмінені рядки за замовчуванням відображаються без префікса, але їх можна приховати, щоб зосередитись лише на змінах.
Навіщо використовувати онлайн-інструмент Text Diff?
Порівняння текстів у терміналі потребує встановлення утиліт diff та роботи з прапорами командного рядка. Браузерний інструмент diff повністю усуває це ускладнення.
Сценарії використання Text 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