Сравнение и объединение файлов в Python: что выбрать
difflib, filecmp, hashlib и слияние данных — какой инструмент решает вашу задачу, где каждый ломается и чем платите за выбор в реальном проекте.

Задача одна, инструментов пять
Вам нужно сказать, отличаются ли два файла, показать, чем именно, или собрать один файл из нескольких. Звучит как одна задача, но под ней прячется как минимум четыре разных: побайтовое сравнение, построчный diff, сравнение по хешу и объединение содержимого. Выберете не тот инструмент — получите либо ложное «файлы разные» на одинаковых данных, либо загрузку гигабайтного лога в память ради ответа «да/нет».
Разбираем на CPython 3.11+ (всё из примеров работает и на 3.8, отличия отмечены). Только стандартная библиотека: filecmp, difflib, hashlib. Для объединения — обычный ввод-вывод плюс shutil и heapq. Каждый подход — с ценой: что выигрываете и чем платите.
Сравнение: четыре подхода и их цена
1. Быстрый ответ «одинаковы ли» — filecmp.cmp
Когда нужен только булев ответ и не нужно знать, чем файлы отличаются, filecmp.cmp — самый дешёвый вариант. С параметром shallow=True (значение по умолчанию) он сравнивает метаданные из os.stat: тип, размер, время модификации. Содержимое при этом не читается вообще.
import filecmp
# shallow=True: сравнивает только os.stat (размер, mtime), НЕ читает содержимое
print(filecmp.cmp("a.log", "b.log", shallow=True))
# shallow=False: читает и сравнивает содержимое поблочно
print(filecmp.cmp("a.log", "b.log", shallow=False))Цена дешевизны: при shallow=True два файла с одинаковым содержимым, но разным временем модификации будут признаны разными. А два файла с разным содержимым, но одинаковыми размером и mtime — одинаковыми. Для контроля целостности shallow=True не годится, для «изменился ли файл с прошлого прогона» — вполне.
2. Целостность и большие файлы — hashlib
Когда файлы большие или лежат на разных машинах, сравнивать содержимое напрямую дорого. Считайте хеш и сравнивайте хеши. Читать нужно чанками, иначе f.read() затянет файл целиком в память.
import hashlib
def file_hash(path, algo="sha256", chunk_size=65536):
"""Хеш файла без загрузки целиком в память."""
digest = hashlib.new(algo)
with open(path, "rb") as f:
# читаем блоками по 64 КБ, пока не кончится файл
for block in iter(lambda: f.read(chunk_size), b""):
digest.update(block)
return digest.hexdigest()
print(file_hash("a.log") == file_hash("b.log"))Цена: хеш читает весь файл всегда, даже если файлы различаются в первом байте. Для сравнения двух локальных файлов «в лоб» побайтовое сравнение с ранним выходом быстрее — оно остановится на первом различии. Хеш выигрывает, когда одно значение нужно сравнить с многими (дедупликация) или передать по сети.
Хеш не отвечает на вопрос «чем отличаются файлы». Он отвечает только «одинаковы или нет». Если вам нужно показать различия — это другой инструмент.
3. Показать различия построчно — difflib
Когда нужно увидеть, какие строки добавились, удалились или изменились, работает difflib. Модуль строит diff в терминах строк, поэтому годится для текста: конфигов, кода, CSV, логов. Для бинарных файлов он бесполезен.
import difflib
with open("config_old.ini", encoding="utf-8") as f:
old = f.readlines()
with open("config_new.ini", encoding="utf-8") as f:
new = f.readlines()
# unified_diff даёт формат, привычный по git diff
diff = difflib.unified_diff(
old, new,
fromfile="config_old.ini",
tofile="config_new.ini",
lineterm="",
)
for line in diff:
print(line)Цена: difflib держит оба файла в памяти списком строк и работает по алгоритму, близкому к квадратичному по числу строк в худшем случае. На двух файлах по паре тысяч строк — мгновенно. На двух логах по миллиону строк — рискуете и памятью, и временем. Для diff огромных файлов берут внешний diff через subprocess или построчное потоковое сравнение, если порядок строк совпадает.
4. Сравнение деревьев каталогов — filecmp.dircmp
Когда сравниваете не два файла, а две папки целиком, filecmp.dircmp обходит их и раскладывает результат: файлы только слева, только справа, общие, различающиеся. Важная деталь: атрибуты diff_files и same_files используют то же неглубокое сравнение по os.stat, что и filecmp.cmp с shallow=True.
import filecmp
comparison = filecmp.dircmp("release_v1", "release_v2")
comparison.report() # краткий отчёт по верхнему уровню
print(comparison.left_only) # есть только в release_v1
print(comparison.diff_files) # различающиеся файлы (сравнение неглубокое)Таблица: когда что
| Задача | Инструмент | Читает содержимое | Плата |
|---|---|---|---|
| «Изменился ли файл» по метаданным | filecmp.cmp(shallow=True) | Нет | Ложные срабатывания на mtime |
| Одинаковы ли два локальных файла точно | filecmp.cmp(shallow=False) | Да, поблочно | Читает оба до первого различия |
| Целостность, дедупликация, сеть | hashlib | Да, весь файл | Читает файл целиком всегда |
| Показать, чем отличаются тексты | difflib.unified_diff | Да, в память | Память и время на больших файлах |
| Сравнить два каталога | filecmp.dircmp | Зависит от режима | По умолчанию неглубокое сравнение |
Объединение: три сценария
Просто склеить
Если нужно соединить содержимое файлов подряд — читайте чанками и пишите в приёмник. Не собирайте всё в одну строку через +=: на десятках файлов это раздует память и создаст лишние копии.
import shutil
sources = ["part1.csv", "part2.csv", "part3.csv"]
with open("merged.csv", "wb") as out:
for name in sources:
with open(name, "rb") as src:
# copyfileobj копирует потоком, без загрузки файла целиком
shutil.copyfileobj(src, out)Подводный камень с CSV: при простой склейке заголовок повторится в каждом куске. Если файлы с заголовками — открывайте их как текст и пропускайте первую строку у всех, кроме первого файла. Это отдельная логика, shutil.copyfileobj её не знает.
Слить отсортированные файлы без загрузки в память
Когда есть несколько отсортированных файлов и нужен один отсортированный на выходе — не читайте всё и не сортируйте заново. heapq.merge берёт итераторы и выдаёт слитый поток лениво.
import heapq
def open_lines(paths):
for path in paths:
with open(path, encoding="utf-8") as f:
yield from f
# каждый файл уже отсортирован; heapq.merge объединяет их по строкам
with open("sorted_out.txt", "w", encoding="utf-8") as out:
parts = [open(p, encoding="utf-8") for p in ["s1.txt", "s2.txt"]]
try:
for line in heapq.merge(*parts):
out.write(line)
finally:
for f in parts:
f.close()Условие работы: входные файлы должны быть уже отсортированы по тому же ключу. На неотсортированных heapq.merge даст мусор без ошибки — это тихий баг, который легко пропустить на ревью.
Объединить структурированные данные
Для CSV и JSON склейка байтов почти всегда неправильна. У CSV — заголовки и возможные переносы строк внутри полей, у JSON — валидность документа целиком. Здесь работает csv и json, а не shutil. Слияние по ключу (как SQL JOIN) — отдельная тема, здесь только отмечаю: для неё берут dict по ключу или pandas, если данные помещаются в память.
Что выбирать
- Нужен только «да/нет» и файлы локальные —
filecmp.cmp(shallow=False). - «Изменился ли с прошлого раза» и точность до байта не критична —
shallow=True, это самый быстрый вариант. - Целостность, передача по сети, поиск дублей —
hashlibс чтением чанками. - Нужно показать различия человеку —
difflib, но следите за размером текста. - Склейка байт в байт —
shutil.copyfileobj; отсортированное слияние —heapq.merge; структурированные данные —csv/json.
Что ломается чаще всего
- Кодировка. При работе с текстом всегда указывайте
encoding="utf-8". По умолчанию Python берёт кодировку из локали ОС, и на Windows это часто cp1251 — diff и слияние тихо испортят символы. - Режим открытия. Хеш и побайтовое сравнение — только в
"rb". В текстовом режиме перенос строк нормализуется, и хеши одного файла на Windows и Linux разойдутся. - Память на больших файлах.
difflibи любойread()целиком опасны на гигабайтных данных. Проверяйте размер до чтения. - Незакрытые файлы при heapq.merge. Открытые дескрипторы нельзя закрыть в контекстном менеджере поодиночке — держите список и закрывайте в
finally.
Дальше
Начните с вопроса «мне нужен ответ да/нет или нужно показать различия» — он сразу отсекает половину инструментов. Для мониторинга изменений замерьте на своих данных filecmp.cmp(shallow=False) против hashlib: на небольших файлах первый обычно быстрее за счёт раннего выхода. Если упрётесь в diff больших файлов — смотрите в сторону внешнего diff через subprocess или потокового сравнения строк, когда порядок гарантирован. Актуальный набор параметров каждого модуля сверяйте с документацией вашей версии Python.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: filecmp — File and Directory Comparisons (Python docs), difflib — Helpers for computing deltas (Python docs)
Частые вопросы
У меня Python 3.8, а не 3.11 — примеры сработают?
filecmp.cmp вернул True, но я вижу, что файлы разные — почему?
Что быстрее для двух локальных файлов — хеш или прямое сравнение?
Почему хеши одного файла на Windows и Linux разные?
difflib падает или тормозит на большом файле — что делать?
Можно ли heapq.merge применять к неотсортированным файлам?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.