Сравнение файлов в Python: где diff врёт под нагрузкой
Наивное сравнение двух логов построчно загрузило память до 4 ГБ на файле в 800 МБ. Разбираем, почему difflib и чтение целиком ломаются на больших данных и чем их заменить.

Скрипт сверял два дампа выгрузки по 800 МБ каждый. На ноутбуке с 8 ГБ он падал с MemoryError, а на сервере съедал около 4 ГБ и работал минуты. Причина не в железе: код читал оба файла целиком через readlines() и скармливал их в difflib.ndiff, у которого сложность близка к квадратичной по числу отличий. Ниже — почему стандартные инструменты сравнения ведут себя так и какие подходы держат большие файлы. Проверялось на CPython 3.12, стандартная библиотека без сторонних зависимостей.
Три задачи, которые называют одним словом «сравнить»
Под «сравнить файлы» скрываются разные операции с разной ценой. Прежде чем выбирать инструмент, определите, что именно вам нужно.
- Проверить идентичность — одинаковы файлы или нет, без деталей. Самое дешёвое.
- Получить построчный diff — что добавилось, что удалилось, как в
git diff. Дороже всего. - Объединить содержимое — слить строки, убрать дубли, найти пересечение или разность множеств. Средняя цена, зависит от того, важен ли порядок.
Смешивать их в одном проходе — типичная ошибка: люди строят полный diff там, где хватило бы сравнения хешей.
Проверка идентичности: не читайте файл в память
Для ответа «одинаковы или нет» в стандартной библиотеке есть filecmp.cmp. С аргументом shallow=False он сравнивает содержимое поблочно и останавливается на первом различии, не держа весь файл в памяти.
import filecmp
# shallow=False заставляет сравнивать содержимое, а не только os.stat
# (размер и время модификации). По умолчанию shallow=True.
identical = filecmp.cmp("dump_a.csv", "dump_b.csv", shallow=False)
print("Файлы совпадают" if identical else "Есть различия")
Важный нюанс: при shallow=True (значение по умолчанию) два файла с одинаковыми размером и временем модификации будут признаны совпадающими без чтения байтов. Для сгенерированных выгрузок это ложное «равны». Если вам нужна честная проверка — всегда ставьте shallow=False.
Когда важно не «равны ли», а «одинаковый ли это файл» между запусками, дешевле сравнивать хеш. Файл читается один раз потоком:
import hashlib
def file_digest(path, chunk_size=1024 * 1024):
# hashlib.file_digest доступен с Python 3.11.
# Для более старых версий читайте файл кусками в цикле вручную.
with open(path, "rb") as handle:
return hashlib.file_digest(handle, "sha256").hexdigest()
same_content = file_digest("dump_a.csv") == file_digest("dump_b.csv")
Хеш отвечает только на вопрос «равны или нет». Если совпадения нет, он не скажет, где именно отличие — за деталями придётся идти к diff.
Построчный diff: почему difflib тяжёлый
difflib.ndiff и difflib.SequenceMatcher строят минимальное сравнение — тот самый аккуратный diff с контекстом. Расплата — память и время: обе последовательности целиком лежат в памяти, а алгоритм на файлах с большим числом отличий деградирует. На двух списках по несколько сотен тысяч строк это уже ощутимо.
Если вам нужен именно unified diff (формат git), берите unified_diff — он легче ndiff, потому что не размечает посимвольные изменения внутри строк:
import difflib
def read_lines(path):
with open(path, encoding="utf-8") as handle:
return handle.readlines() # переносы строк сохраняются
old = read_lines("config_old.txt")
new = read_lines("config_new.txt")
diff = difflib.unified_diff(
old, new,
fromfile="config_old.txt",
tofile="config_new.txt",
lineterm="",
)
for line in diff:
print(line)
Ключевое ограничение: readlines() держит оба файла в памяти. Для конфигов и небольших логов это нормально. Для многогигабайтных дампов — нет, и здесь difflib вам не помощник в принципе: минимальный diff огромных файлов не помещается ни в память, ни в голову.
Объединение и разность: работайте множествами
Когда порядок строк не важен, а нужен ответ «что есть только в A, что только в B, что в обоих» — не стройте diff. Стройте множества. Это линейно по памяти относительно числа уникальных строк и быстро.
# Читаем построчно генератором, не загружая файл целиком в список.
def line_set(path):
with open(path, encoding="utf-8") as handle:
return {line.rstrip("\n") for line in handle}
lines_a = line_set("emails_a.txt")
lines_b = line_set("emails_b.txt")
only_in_a = lines_a - lines_b # разность
only_in_b = lines_b - lines_a
in_both = lines_a & lines_b # пересечение
union_all = lines_a | lines_b # объединение без дублей
Плата за скорость: set теряет порядок и хранит только уникальные значения. Если строки повторяются и вам важно, сколько раз, берите collections.Counter — он считает вхождения и умеет вычитаться:
from collections import Counter
def line_counter(path):
with open(path, encoding="utf-8") as handle:
return Counter(line.rstrip("\n") for line in handle)
count_a = line_counter("log_a.txt")
count_b = line_counter("log_b.txt")
# Counter.subtract учитывает кратность: строка, встречавшаяся
# в A трижды, а в B дважды, останется с count 1.
extra_in_a = count_a - count_b # только положительные остатки
Что выбрать под задачу
| Задача | Инструмент | Память | Порядок строк |
|---|---|---|---|
| Равны или нет | filecmp.cmp(shallow=False) | константа | учитывается |
| Одинаковый файл между запусками | hashlib.file_digest | константа | учитывается |
| Diff с контекстом (небольшие файлы) | difflib.unified_diff | оба файла в RAM | учитывается |
| Что есть только в одном из двух | set / разность | уникальные строки в RAM | теряется |
| Разность с учётом повторов | collections.Counter | уникальные строки в RAM | теряется |
Когда что выбирать
- Нужно только «да/нет» — не трогайте difflib.
filecmp.cmp(shallow=False)прочитает ровно до первого расхождения. - Нужен человекочитаемый diff и файлы влезают в память —
unified_diff. Если не влезают, режьте на куски по логическим границам (по дате в логе, по ID) и сравнивайте куски. - Порядок не важен, нужны разности и объединения — множества. Это отдельная задача от diff, не путайте: diff учитывает позицию строки, множество — нет.
- Файлы отсортированы по одному ключу и огромны — сравнивайте потоково двумя курсорами (алгоритм слияния), не загружая целиком. Реализация выходит за рамки этого разбора.
Где ломается
Три ловушки, на которые натыкаются чаще всего.
- Кодировка и BOM. Открытие без явного
encoding="utf-8"использует системную локаль — на Windows это часто cp1251, и «одинаковые» файлы дадут ложные отличия. Файл с BOM даст невидимый символ\ufeffв первой строке; открывайте сencoding="utf-8-sig", если BOM возможен. - Переносы строк.
\r\nпротив\n— частая причина «весь файл изменился». Открывайте текст в текстовом режиме (по умолчанию), тогда Python нормализует переносы. Для точного сравнения байт в байт открывайте в"rb"и сравнивайте байты. - Память на разностях множеств.
setиз файла с миллиардом уникальных строк не влезет в RAM. Тогда путь один — внешняя сортировка и потоковое слияние либо база данных, а не питоновские множества.
Что делать дальше
Если упираетесь в память на больших файлах, посмотрите в сторону потоковой обработки: сортировка каждого файла (например, системной утилитой sort) и слияние двумя указателями за один проход. Для табличных данных с миллионами строк дешевле сравнивать не текстом, а через pandas или загрузку в SQLite и запрос EXCEPT/INTERSECT. Замерьте своё узкое место профилировщиком памяти (tracemalloc из стандартной библиотеки), прежде чем переписывать: часто хватает замены readlines() на генератор.
Помощник по курсовой и диплому: Большая работа поэтапно: от плана до защиты
Спросить за 25 ₽Источники: Python docs: difflib — Helpers for computing deltas, Python docs: filecmp — File and Directory Comparisons
Частые вопросы
У меня Python 3.9, hashlib.file_digest не работает
difflib выдаёт разницу, хотя файлы на вид одинаковые
Как сравнить два больших файла и не упасть по памяти?
Чем отличается объединение через set от diff?
Как в проде не словить ложные различия из-за кодировки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.