Text Diff
Сравнивайте два текста рядом с построчной подсветкой различий
Текст A
Текст B
Что такое text diff?
Text diff (сокращение от «difference» — «различие») — это результат сравнения двух блоков текста с определением того, какие строки были добавлены, удалены или остались без изменений. Концепция восходит к утилите diff операционной системы Unix, впервые выпущенной в 1974 году в составе Version 5 Unix. Сегодня text diff является основой систем контроля версий, таких как Git, где каждый коммит хранит diff, а не полную копию каждого файла.
Алгоритм diff находит Наибольшую Общую Подпоследовательность (LCS) между двумя последовательностями строк. Строки, входящие в LCS, помечаются как неизменённые. Строки исходного текста, не вошедшие в LCS, помечаются как удалённые. Строки изменённого текста, не вошедшие в LCS, помечаются как добавленные. В результате получается минимальный набор изменений, необходимых для преобразования одного текста в другой.
Вывод diff бывает нескольких форматов. Unified diff (формат по умолчанию для git diff) предваряет удалённые строки знаком минус, а добавленные — знаком плюс. Side-by-side 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 |
Как работает построчный diff: алгоритм LCS
Большинство инструментов построчного сравнения, включая данный, используют алгоритм Наибольшей Общей Подпоследовательности (LCS). LCS находит наибольший набор строк, присутствующих в обоих текстах в одном относительном порядке, без требования их непрерывности. Строки, не входящие в LCS, являются фактическими различиями.
Стандартный алгоритм LCS использует динамическое программирование и выполняется за O(m × n), где m и n — количество строк в двух текстах. Для больших файлов оптимизированные варианты, такие как алгоритм Myers' diff (используемый Git), сокращают это до O(n + d²), где 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