difflib — Помощники для вычисления дельт

Источник: Lib/difflib.py


Этот модуль предоставляет классы и функции для сравнения последовательностей. Он может быть использован, например, для сравнения файлов и может выдавать информацию о различиях файлов в различных форматах, включая HTML, контекстные и унифицированные дифы. Для сравнения каталогов и файлов см. также модуль filecmp.

class difflib.SequenceMatcher

Это гибкий класс для сравнения пар последовательностей любого типа, лишь бы элементы последовательности были hashable. Основной алгоритм предшествует алгоритму, опубликованному в конце 1980-х годов Рэтклиффом и Обершелпом под гиперболическим названием «гештальт-сопоставление шаблонов», и является немного более сложным. Идея состоит в том, чтобы найти самую длинную непрерывную совпадающую подпоследовательность, которая не содержит «мусорных» элементов; эти «мусорные» элементы - те, которые неинтересны в каком-то смысле, например пустые строки или пробельные символы. (Работа с мусором - это расширение алгоритма Рэтклиффа и Обершелпа.) Затем та же идея рекурсивно применяется к частям последовательностей слева и справа от совпадающей подпоследовательности. Это не дает минимальных последовательностей редактирования, но, как правило, дает совпадения, которые «выглядят правильно» для людей.

Тайминг: Базовый алгоритм Рэтклиффа-Обершелпа имеет кубическое время в худшем случае и квадратичное время в ожидаемом случае. SequenceMatcher имеет квадратичное время в худшем случае и сложное поведение в ожидаемом случае, зависящее от количества общих элементов последовательностей; в лучшем случае время линейно.

Автоматическая эвристика хлама: SequenceMatcher поддерживает эвристику, которая автоматически рассматривает определенные элементы последовательности как хлам. Эвристика подсчитывает, сколько раз каждый отдельный элемент встречается в последовательности. Если дубликаты элемента (после первого) составляют более 1 % последовательности, а длина последовательности не менее 200 элементов, этот элемент помечается как «популярный» и рассматривается как хлам для целей подбора последовательности. Эту эвристику можно отключить, установив для аргумента autojunk значение False при создании SequenceMatcher.

Изменено в версии 3.2: Добавлен параметр autojunk.

class difflib.Differ

Это класс для сравнения последовательностей строк текста и получения человекочитаемых разностей или дельт. Differ использует SequenceMatcher как для сравнения последовательностей строк, так и для сравнения последовательностей символов в похожих (близких по значению) строках.

Каждая строка дельты Differ начинается с двухбуквенного кода:

Код

Значение

'- '

линия, уникальная для последовательности 1

'+ '

линия, уникальная для последовательности 2

'  '

линия, общая для обеих последовательностей

'? '

строка, не присутствующая ни в одной из входных последовательностей

Строки, начинающиеся с „?, направляют взгляд на внутрилинейные различия и не присутствуют ни в одной из входных последовательностей. Эти строки могут сбить с толку, если последовательности содержат пробельные символы, такие как пробелы, табуляции или разрывы строк.

class difflib.HtmlDiff

Этот класс можно использовать для создания HTML-таблицы (или полного HTML-файла, содержащего таблицу), показывающей боковое, построчное сравнение текста с выделением межстрочных и внутристрочных изменений. Таблица может быть сгенерирована в режиме полного или контекстного различия.

Конструктором для этого класса является:

__init__(tabsize=8, wrapcolumn=None, linejunk=None, charjunk=IS_CHARACTER_JUNK)

Инициализирует экземпляр HtmlDiff.

tabsize - необязательный ключевой аргумент для задания расстояния между табуляторами, по умолчанию принимается равным 8.

wrapcolumn - необязательное ключевое слово, указывающее номер столбца, в котором строки разбиваются и заворачиваются, по умолчанию None, в котором строки не заворачиваются.

linejunk и charjunk - необязательные ключевые аргументы, передаваемые в ndiff() (используются HtmlDiff для генерации боковых различий HTML). Значения аргументов по умолчанию и их описание см. в документации ndiff().

Следующие методы являются общедоступными:

make_file(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5, *, charset='utf-8')

Сравнивает fromlines и tolines (списки строк) и возвращает строку, которая представляет собой полный HTML-файл, содержащий таблицу, показывающую построчные различия с выделенными межстрочными и внутристрочными изменениями.

fromdesc и todesc - необязательные ключевые аргументы для указания строк заголовков столбцов из/в файл (по умолчанию обе строки пустые).

context и numlines - необязательные ключевые аргументы. Установите context в значение True, если необходимо показать контекстные различия, иначе по умолчанию используется значение False для показа полных файлов. Значение numlines по умолчанию равно 5. Когда context имеет значение True. numlines управляет количеством контекстных строк, окружающих выделение различий. Когда context имеет значение False numlines управляет количеством строк, которые показываются перед выделением различий при использовании гиперссылок «следующий» (при установке нуля гиперссылки «следующий» будут размещать следующее выделение различий в верхней части браузера без какого-либо ведущего контекста).

Примечание

fromdesc и todesc интерпретируются как неэкранированный HTML и должны быть правильно экранированы при получении входных данных из ненадежных источников.

Изменено в версии 3.5: Был добавлен аргумент charset, который является только ключевым словом. Кодовая сетка HTML-документа по умолчанию изменена с 'ISO-8859-1' на 'utf-8'.

make_table(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5)

Сравнивает fromlines и tolines (списки строк) и возвращает строку, которая представляет собой полную HTML-таблицу, показывающую построчные различия с выделенными межстрочными и внутристрочными изменениями.

Аргументы для этого метода те же, что и для метода make_file().

difflib.context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

Сравнивает a и b (списки строк); возвращает дельту (a generator, генерирующую строки дельты) в формате context diff.

Контекстные диффы - это компактный способ показать только те строки, которые изменились, плюс несколько строк контекста. Изменения отображаются в стиле «до/после». Количество контекстных строк задается параметром n, который по умолчанию равен трем.

По умолчанию управляющие строки diff (те, что имеют *** или ---) создаются с концевой новой строкой. Это полезно для того, чтобы входы, созданные с помощью io.IOBase.readlines(), приводили к диффам, пригодным для использования с io.IOBase.writelines(), поскольку и на входах, и на выходах есть завершающие новые строки.

Для входных данных, не содержащих концевых строк, установите аргумент lineterm в "", чтобы вывод был равномерно свободен от строк.

Формат контекстной разницы обычно содержит заголовок для имен файлов и времени модификации. Любой из них или все они могут быть указаны с помощью строк fromfile, tofile, fromfiledate и tofiledate. Время модификации обычно выражается в формате ISO 8601. Если строки не указаны, то по умолчанию они будут пустыми.

>>> import sys
>>> from difflib import *
>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(context_diff(s1, s2, fromfile='before.py',
...                        tofile='after.py'))
*** before.py
--- after.py
***************
*** 1,4 ****
! bacon
! eggs
! ham
  guido
--- 1,4 ----
! python
! eggy
! hamster
  guido

Более подробный пример см. в разделе Интерфейс командной строки для difflib.

difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)

Возвращает список лучших «достаточно хороших» совпадений. слово - это последовательность, для которой требуется близкое соответствие (обычно это строка), а возможности - это список последовательностей, с которыми можно сопоставить слово (обычно это список строк).

Необязательный аргумент n (по умолчанию 3) - это максимальное количество близких совпадений, которое нужно вернуть; n должно быть больше, чем 0.

Необязательный аргумент cutoff (по умолчанию 0.6) - плавающая величина в диапазоне [0, 1]. Возможности, которые не имеют хотя бы такого сходства со словом, игнорируются.

Лучшие (не более n) совпадения из возможных возвращаются в виде списка, отсортированного по степени сходства, сначала наиболее похожие.

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('pineapple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
difflib.ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)

Сравните a и b (списки строк); верните дельту в стиле Differgenerator генерирует строки дельты).

Необязательные ключевые параметры linejunk и charjunk являются функциями фильтрации (или None):

linejunk: Функция, принимающая единственный строковый аргумент и возвращающая true, если строка не содержит мусора, или false, если не содержит. По умолчанию используется None. Существует также функция IS_LINE_JUNK() на уровне модуля, которая отфильтровывает строки без видимых символов, за исключением не более одного символа фунта ('#') - однако базовый класс SequenceMatcher проводит динамический анализ того, какие строки встречаются настолько часто, что представляют собой шум, и это обычно работает лучше, чем использование этой функции.

charjunk: Функция, принимающая символ (строку длины 1) и возвращающая значение, если символ не содержит мусора, или false, если не содержит. По умолчанию используется функция уровня модуля IS_CHARACTER_JUNK(), которая отфильтровывает пробельные символы (пробел или табуляция; не стоит включать сюда новую строку!).

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
difflib.restore(sequence, which)

Возвращает одну из двух последовательностей, породивших дельту.

Из последовательности, полученной с помощью Differ.compare() или ndiff(), извлеките строки, происходящие из файла 1 или 2 (параметр which), удаляя префиксы строк.

Пример:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print(''.join(restore(diff, 1)), end="")
one
two
three
>>> print(''.join(restore(diff, 2)), end="")
ore
tree
emu
difflib.unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

Сравните a и b (списки строк); верните дельту (generator, создающий строки дельты) в унифицированном формате diff.

Унифицированные дифы - это компактный способ показать только те строки, в которых произошли изменения, плюс несколько строк контекста. Изменения отображаются в стиле инлайн (вместо отдельных блоков «до/после»). Количество контекстных строк задается параметром n, который по умолчанию равен трем.

По умолчанию управляющие строки diff (те, что имеют ---, +++ или @@) создаются с концевой новой строкой. Это полезно для того, чтобы входы, созданные с помощью io.IOBase.readlines(), приводили к дифам, пригодным для использования с io.IOBase.writelines(), поскольку и на входах, и на выходах есть завершающие новые строки.

Для входных данных, не содержащих концевых строк, установите аргумент lineterm в "", чтобы вывод был равномерно свободен от строк.

Унифицированный формат diff обычно содержит заголовок для имен файлов и времени модификации. Любой или все они могут быть указаны с помощью строк fromfile, tofile, fromfiledate и tofiledate. Время модификации обычно выражается в формате ISO 8601. Если строки не указаны, то по умолчанию они будут пустыми.

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(unified_diff(s1, s2, fromfile='before.py', tofile='after.py'))
--- before.py
+++ after.py
@@ -1,4 +1,4 @@
-bacon
-eggs
-ham
+python
+eggy
+hamster
 guido

Более подробный пример см. в разделе Интерфейс командной строки для difflib.

difflib.diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')

Сравните a и b (списки байтовых объектов) с помощью dfunc; выдайте последовательность дельта-строк (также байтов) в формате, возвращаемом dfunc. dfunc должен быть вызываемым объектом, обычно либо unified_diff(), либо context_diff().

Позволяет сравнивать данные с неизвестной или непоследовательной кодировкой. Все входные данные, кроме n, должны быть объектами типа bytes, а не str. Работает путем преобразования без потерь всех входных данных (кроме n) в str и вызова dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm). Выход dfunc затем преобразуется обратно в байты, поэтому получаемые строки дельты имеют те же неизвестные/несовместимые кодировки, что и a и b.

Added in version 3.5.

difflib.IS_LINE_JUNK(line)

Возвращает True для игнорируемых строк. Строка line является игнорируемой, если line пуста или содержит один '#', иначе она не является игнорируемой. В старых версиях используется по умолчанию для параметра linejunk в ndiff().

difflib.IS_CHARACTER_JUNK(ch)

Возвращает True для игнорируемых символов. Символ ch является игнорируемым, если ch - это пробел или табуляция, в противном случае он не является игнорируемым. Используется по умолчанию для параметра charjunk в ndiff().

См.также

Pattern Matching: The Gestalt Approach

Обсуждение аналогичного алгоритма Джоном В. Рэтклиффом и Д. Е. Метценером. Эта статья была опубликована в Dr. Dobb’s Journal в июле 1988 года.

Объекты SequenceMatcher

Класс SequenceMatcher имеет такой конструктор:

class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)

Необязательный аргумент isjunk должен быть None (по умолчанию) или одноаргументной функцией, которая принимает элемент последовательности и возвращает true тогда и только тогда, когда этот элемент является «мусором» и должен быть проигнорирован. Передача None для isjunk эквивалентна передаче lambda x: False; другими словами, ни один элемент не будет проигнорирован. Например, передайте:

lambda x: x in " \t"

если вы сравниваете строки как последовательности символов и не хотите синхронизировать их с пробелами или жесткими вкладками.

Необязательные аргументы a и b - это сравниваемые последовательности; по умолчанию оба аргумента представляют собой пустые строки. Элементы обеих последовательностей должны быть hashable.

Необязательный аргумент autojunk может быть использован для отключения автоматической эвристики хлама.

Изменено в версии 3.2: Добавлен параметр autojunk.

Объекты SequenceMatcher получают три атрибута данных: bjunk - множество элементов b, для которых значение isjunk равно True; bpopular - множество элементов без мусора, которые эвристика считает популярными (если она не отключена); b2j - dict, отображающий оставшиеся элементы b на список позиций, в которых они встречаются. Все три дикта сбрасываются всякий раз, когда b сбрасывается с помощью set_seqs() или set_seq2().

Added in version 3.2: Атрибуты bjunk и bpopular.

Объекты SequenceMatcher имеют следующие методы:

set_seqs(a, b)

Задайте две сравниваемые последовательности.

SequenceMatcher вычисляет и кэширует подробную информацию о второй последовательности, поэтому, если вы хотите сравнить одну последовательность с множеством последовательностей, используйте set_seq2(), чтобы один раз задать часто используемую последовательность, и вызывайте set_seq1() несколько раз, по одному разу для каждой из остальных последовательностей.

set_seq1(a)

Установите первую сравниваемую последовательность. Вторая сравниваемая последовательность не изменяется.

set_seq2(b)

Установите вторую сравниваемую последовательность. Первая сравниваемая последовательность не изменяется.

find_longest_match(alo=0, ahi=None, blo=0, bhi=None)

Найдите самый длинный совпадающий блок в a[alo:ahi] и b[blo:bhi].

Если значение isjunk было опущено или None, то find_longest_match() возвращает (i, j, k) такой, что a[i:i+k] равен b[j:j+k], где alo <= i <= i+k <= ahi и blo <= j <= j+k <= bhi. Для всех (i', j', k'), удовлетворяющих этим условиям, также выполняются дополнительные условия k >= k', i <= i', а если i == i', то и j <= j'. Другими словами, из всех максимальных совпадающих блоков верните тот, который начинается раньше всех в a, а из всех тех максимальных совпадающих блоков, которые начинаются раньше всех в a, верните тот, который начинается раньше всех в b.

>>> s = SequenceMatcher(None, " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=0, b=4, size=5)

Если было указано isjunk, сначала определяется самый длинный совпадающий блок, как описано выше, но с дополнительным ограничением, что в блоке нет ни одного нежелательного элемента. Затем этот блок расширяется настолько, насколько это возможно, за счет совпадения (только) нежелательных элементов с обеих сторон. Таким образом, результирующий блок никогда не совпадает по хламу, за исключением случаев, когда идентичный хлам оказывается рядом с интересным совпадением.

Приведем тот же пример, что и раньше, но считая пробелы ненужными. Это не позволяет ' abcd' напрямую совпасть с ' abcd' в хвостовой части второй последовательности. Вместо этого может совпасть только 'abcd', который совпадает с самым левым 'abcd' во второй последовательности:

>>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=1, b=0, size=4)

Если ни один блок не совпадает, возвращается (alo, blo, 0).

Этот метод возвращает named tuple Match(a, b, size).

Изменено в версии 3.9: Добавлены аргументы по умолчанию.

get_matching_blocks()

Возвращает список троек, описывающих непересекающиеся совпадающие подпоследовательности. Каждая тройка имеет вид (i, j, n) и означает, что a[i:i+n] == b[j:j+n]. Тройки монотонно возрастают по i и j.

Последняя тройка является фиктивной и имеет значение (len(a), len(b), 0). Это единственная тройка со значением n == 0. Если (i, j, n) и (i', j', n') - соседние тройки в списке, и вторая не является последней тройкой в списке, то i+n < i' или j+n < j'; другими словами, соседние тройки всегда описывают несмежные равные блоки.

>>> s = SequenceMatcher(None, "abxcd", "abcd")
>>> s.get_matching_blocks()
[Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
get_opcodes()

Возвращает список из 5 кортежей, описывающих, как превратить a в b. Каждый кортеж имеет вид (tag, i1, i2, j1, j2). Первый кортеж имеет i1 == j1 == 0, а остальные кортежи имеют i1, равный i2 из предыдущего кортежа, и, аналогично, j1, равный предыдущему j2.

Значения tag являются строками с такими значениями:

Значение

Значение

'replace'

a[i1:i2] следует заменить на b[j1:j2].

'delete'

a[i1:i2] следует удалить. Обратите внимание, что в этом случае j1 == j2.

'insert'

b[j1:j2] должен быть вставлен в a[i1:i1]. Обратите внимание, что в данном случае i1 == i2.

'equal'

a[i1:i2] == b[j1:j2] (подпоследовательности равны).

Например:

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...     print('{:7}   a[{}:{}] --> b[{}:{}] {!r:>8} --> {!r}'.format(
...         tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]))
delete    a[0:1] --> b[0:0]      'q' --> ''
equal     a[1:3] --> b[0:2]     'ab' --> 'ab'
replace   a[3:4] --> b[2:3]      'x' --> 'y'
equal     a[4:6] --> b[3:5]     'cd' --> 'cd'
insert    a[6:6] --> b[5:6]       '' --> 'f'
get_grouped_opcodes(n=3)

Возвращает generator групп, содержащих до n строк контекста.

Начиная с групп, возвращаемых get_opcodes(), этот метод разделяет кластеры с меньшими изменениями и удаляет промежуточные диапазоны, в которых не было изменений.

Группы возвращаются в том же формате, что и get_opcodes().

ratio()

Возвращает меру сходства последовательностей в виде float в диапазоне [0, 1].

Где T - общее количество элементов в обеих последовательностях, а M - количество совпадений, это 2.0*M / T. Обратите внимание, что это 1.0, если последовательности идентичны, и 0.0, если у них нет ничего общего.

Это дорогостоящее вычисление, если get_matching_blocks() или get_opcodes() еще не были вызваны, и в этом случае вы можете попробовать сначала quick_ratio() или real_quick_ratio(), чтобы получить верхнюю границу.

Примечание

Внимание: Результат вызова ratio() может зависеть от порядка аргументов. Например:

>>> SequenceMatcher(None, 'tide', 'diet').ratio()
0.25
>>> SequenceMatcher(None, 'diet', 'tide').ratio()
0.5
quick_ratio()

Сравнительно быстро возвращает верхнюю границу ratio().

real_quick_ratio()

Быстро верните верхнюю границу для ratio().

Три метода, возвращающие отношение числа совпадений к общему числу символов, могут давать разные результаты из-за разного уровня аппроксимации, хотя quick_ratio() и real_quick_ratio() всегда не меньше, чем ratio():

>>> s = SequenceMatcher(None, "abcd", "bcde")
>>> s.ratio()
0.75
>>> s.quick_ratio()
0.75
>>> s.real_quick_ratio()
1.0

Примеры SequenceMatcher

Этот пример сравнивает две строки, считая пробелы «мусором»:

>>> s = SequenceMatcher(lambda x: x == " ",
...                     "private Thread currentThread;",
...                     "private volatile Thread currentThread;")

ratio() возвращает плавающее значение в диапазоне [0, 1], измеряющее сходство последовательностей. Как правило, значение ratio() больше 0,6 означает, что последовательности близко совпадают:

>>> print(round(s.ratio(), 3))
0.866

Если вас интересует только то, где последовательности совпадают, удобно использовать get_matching_blocks():

>>> for block in s.get_matching_blocks():
...     print("a[%d] and b[%d] match for %d elements" % block)
a[0] and b[0] match for 8 elements
a[8] and b[17] match for 21 elements
a[29] and b[38] match for 0 elements

Обратите внимание, что последний кортеж, возвращаемый get_matching_blocks(), всегда является фиктивным, (len(a), len(b), 0), и это единственный случай, когда последним элементом кортежа (количество совпавших элементов) является 0.

Если вы хотите узнать, как заменить первую последовательность на вторую, используйте get_opcodes():

>>> for opcode in s.get_opcodes():
...     print("%6s a[%d:%d] b[%d:%d]" % opcode)
 equal a[0:8] b[0:8]
insert a[8:8] b[8:17]
 equal a[8:29] b[17:38]

См.также

  • Функция get_close_matches() в этом модуле показывает, как простой код, построенный на SequenceMatcher, может быть использован для выполнения полезной работы.

  • Simple version control recipe для небольшого приложения, созданного с помощью SequenceMatcher.

Различать объекты

Обратите внимание, что Differ-генерируемые дельты не претендуют на то, чтобы быть минимальными диффами. Напротив, минимальные диффы часто неинтуитивны, потому что они синхронизируются везде, где только можно, иногда случайно совпадая на 100 страниц друг с другом. Ограничение точек синхронизации смежными совпадениями сохраняет некоторое представление о локальности, но иногда ценой создания более длинного дифа.

Класс Differ имеет такой конструктор:

class difflib.Differ(linejunk=None, charjunk=None)

Необязательные ключевые параметры linejunk и charjunk предназначены для функций фильтрации (или None):

linejunk: Функция, принимающая единственный строковый аргумент и возвращающая true, если строка является нежелательной. По умолчанию используется значение None, означающее, что ни одна строка не считается нежелательной.

charjunk: Функция, принимающая аргумент в виде одного символа (строка длины 1) и возвращающая true, если символ является нежелательным. По умолчанию используется значение None, означающее, что ни один символ не считается нежелательным.

Эти функции фильтрации мусора ускоряют поиск различий и не приводят к игнорированию отличающихся строк или символов. Прочитайте описание параметра isjunk метода find_longest_match(), чтобы получить объяснение.

Объекты Differ используются (генерируются дельты) через один метод:

compare(a, b)

Сравните две последовательности строк и выведите дельту (последовательность строк).

Каждая последовательность должна содержать отдельные однострочные строки, заканчивающиеся новыми строками. Такие последовательности можно получить из метода readlines() файлоподобных объектов. Сгенерированная дельта также состоит из строк, заканчивающихся новой строкой, и готова к печати как есть с помощью метода writelines() файлоподобного объекта.

Разница Пример

В этом примере сравниваются два текста. Сначала мы зададим тексты - последовательности отдельных однострочных строк, заканчивающихся новыми строками (такие последовательности можно также получить из метода readlines() файлоподобных объектов):

>>> text1 = '''  1. Beautiful is better than ugly.
...   2. Explicit is better than implicit.
...   3. Simple is better than complex.
...   4. Complex is better than complicated.
... '''.splitlines(keepends=True)
>>> len(text1)
4
>>> text1[0][-1]
'\n'
>>> text2 = '''  1. Beautiful is better than ugly.
...   3.   Simple is better than complex.
...   4. Complicated is better than complex.
...   5. Flat is better than nested.
... '''.splitlines(keepends=True)

Далее мы создадим объект Differ:

>>> d = Differ()

Обратите внимание, что при инстанцировании объекта Differ мы можем передавать функции для фильтрации строк и символов «мусора». Подробности см. в конструкторе Differ().

Наконец, мы сравним их между собой:

>>> result = list(d.compare(text1, text2))

result - это список строк, поэтому давайте выведем его в красивом виде:

>>> from pprint import pprint
>>> pprint(result)
['    1. Beautiful is better than ugly.\n',
 '-   2. Explicit is better than implicit.\n',
 '-   3. Simple is better than complex.\n',
 '+   3.   Simple is better than complex.\n',
 '?     ++\n',
 '-   4. Complex is better than complicated.\n',
 '?            ^                     ---- ^\n',
 '+   4. Complicated is better than complex.\n',
 '?           ++++ ^                      ^\n',
 '+   5. Flat is better than nested.\n']

В виде одной многострочной строки это выглядит следующим образом:

>>> import sys
>>> sys.stdout.writelines(result)
    1. Beautiful is better than ugly.
-   2. Explicit is better than implicit.
-   3. Simple is better than complex.
+   3.   Simple is better than complex.
?     ++
-   4. Complex is better than complicated.
?            ^                     ---- ^
+   4. Complicated is better than complex.
?           ++++ ^                      ^
+   5. Flat is better than nested.

Интерфейс командной строки для difflib

В этом примере показано, как использовать difflib для создания diff-подобной утилиты.

""" Command line interface to difflib.py providing diffs in four formats:

* ndiff:    lists every line and highlights interline changes.
* context:  highlights clusters of changes in a before/after format.
* unified:  highlights clusters of changes in an inline format.
* html:     generates side by side comparison with change highlights.

"""

import sys, os, difflib, argparse
from datetime import datetime, timezone

def file_mtime(path):
    t = datetime.fromtimestamp(os.stat(path).st_mtime,
                               timezone.utc)
    return t.astimezone().isoformat()

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-c', action='store_true', default=False,
                        help='Produce a context format diff (default)')
    parser.add_argument('-u', action='store_true', default=False,
                        help='Produce a unified format diff')
    parser.add_argument('-m', action='store_true', default=False,
                        help='Produce HTML side by side diff '
                             '(can use -c and -l in conjunction)')
    parser.add_argument('-n', action='store_true', default=False,
                        help='Produce a ndiff format diff')
    parser.add_argument('-l', '--lines', type=int, default=3,
                        help='Set number of context lines (default 3)')
    parser.add_argument('fromfile')
    parser.add_argument('tofile')
    options = parser.parse_args()

    n = options.lines
    fromfile = options.fromfile
    tofile = options.tofile

    fromdate = file_mtime(fromfile)
    todate = file_mtime(tofile)
    with open(fromfile) as ff:
        fromlines = ff.readlines()
    with open(tofile) as tf:
        tolines = tf.readlines()

    if options.u:
        diff = difflib.unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)
    elif options.n:
        diff = difflib.ndiff(fromlines, tolines)
    elif options.m:
        diff = difflib.HtmlDiff().make_file(fromlines,tolines,fromfile,tofile,context=options.c,numlines=n)
    else:
        diff = difflib.context_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)

    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()

пример ndiff

В этом примере показано, как использовать difflib.ndiff().

"""ndiff [-q] file1 file2
    or
ndiff (-r1 | -r2) < ndiff_output > file1_or_file2

Print a human-friendly file difference report to stdout.  Both inter-
and intra-line differences are noted.  In the second form, recreate file1
(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin.

In the first form, if -q ("quiet") is not specified, the first two lines
of output are

-: file1
+: file2

Each remaining line begins with a two-letter code:

    "- "    line unique to file1
    "+ "    line unique to file2
    "  "    line common to both files
    "? "    line not present in either input file

Lines beginning with "? " attempt to guide the eye to intraline
differences, and were not present in either input file.  These lines can be
confusing if the source files contain tab characters.

The first file can be recovered by retaining only lines that begin with
"  " or "- ", and deleting those 2-character prefixes; use ndiff with -r1.

The second file can be recovered similarly, but by retaining only "  " and
"+ " lines; use ndiff with -r2; or, on Unix, the second file can be
recovered by piping the output through

    sed -n '/^[+ ] /s/^..//p'
"""

__version__ = 1, 7, 0

import difflib, sys

def fail(msg):
    out = sys.stderr.write
    out(msg + "\n\n")
    out(__doc__)
    return 0

# open a file & return the file object; gripe and return 0 if it
# couldn't be opened
def fopen(fname):
    try:
        return open(fname)
    except IOError as detail:
        return fail("couldn't open " + fname + ": " + str(detail))

# open two files & spray the diff to stdout; return false iff a problem
def fcompare(f1name, f2name):
    f1 = fopen(f1name)
    f2 = fopen(f2name)
    if not f1 or not f2:
        return 0

    a = f1.readlines(); f1.close()
    b = f2.readlines(); f2.close()
    for line in difflib.ndiff(a, b):
        print(line, end=' ')

    return 1

# crack args (sys.argv[1:] is normal) & compare;
# return false iff a problem

def main(args):
    import getopt
    try:
        opts, args = getopt.getopt(args, "qr:")
    except getopt.error as detail:
        return fail(str(detail))
    noisy = 1
    qseen = rseen = 0
    for opt, val in opts:
        if opt == "-q":
            qseen = 1
            noisy = 0
        elif opt == "-r":
            rseen = 1
            whichfile = val
    if qseen and rseen:
        return fail("can't specify both -q and -r")
    if rseen:
        if args:
            return fail("no args allowed with -r option")
        if whichfile in ("1", "2"):
            restore(whichfile)
            return 1
        return fail("-r value must be 1 or 2")
    if len(args) != 2:
        return fail("need 2 filename args")
    f1name, f2name = args
    if noisy:
        print('-:', f1name)
        print('+:', f2name)
    return fcompare(f1name, f2name)

# read ndiff output from stdin, and print file1 (which=='1') or
# file2 (which=='2') to stdout

def restore(which):
    restored = difflib.restore(sys.stdin.readlines(), which)
    sys.stdout.writelines(restored)

if __name__ == '__main__':
    main(sys.argv[1:])