Мы только что реализовали инструмент, отбирающий уникальные имена файлов и каталогов. Теперь нам осталось реализовать инструмент обхода дерева, который будет применять функции из модуля dirdiff на каждом уровне, чтобы отобрать уникальные файлы и каталоги; явно сравнит содержимое общих файлов и обойдет общие каталоги. Эти операции осуществляет сценарий из примера 6.12.
Пример 6.12. PP4E\System\Filetools\diffall.py
############################################################################## Порядок использования: “python diffall.py dir1 dir2”.
Выполняет рекурсивное сравнение каталогов: сообщает об уникальных файлах, существующих только в одном из двух каталогов, dir1 или dir2; сообщает о файлах с одинаковыми именами и с разным содержимым, присутствующих в каталогах dir1 и dir2; сообщает об разнотипных элементах с одинаковыми именами, присутствующих в каталогах dir1 и dir2; то же самое выполняется для всех подкаталогов с одинаковыми именами, находящихся внутри деревьев каталогов dir1
и dir2. Сводная информация об обнаруженных отличиях помещается в конец вывода, однако в процессе поиска в вывод добавляется дополнительная информация об отличающихся и уникальных файлах с метками “DIFF” и “unique”. Новое: (в 3 издании) для больших файлов введено ограничение на размер читаемых блоков в 1 Мбайт, (3 издание) обнаруживаются одинаковые имена файлов/каталогов, (4 издание) исключены лишние вызовы os.listdir() в dirdiff.comparedirs() за счет передачи результатов.
##############################################################################
import os, dirdiff
blocksize = 1024 * 1024 # не более 1 Мбайта на одну операцию чтения
def intersect(seq1, seq2):
Возвращает все элементы, присутствующие одновременно в seq1 и seq2;
выражение set(seq1) & set(seq2) возвращает тот же результат, но множества являются неупорядоченными коллекциями, поэтому при их использовании может быть утерян порядок следования элементов, если он имеет значение для некоторых платформ
return [item for item in seq1 if item in seq2]
def comparetrees(dir1, dir2, diffs, verbose=False):
Сравнивает все подкаталоги и файлы в двух деревьях каталогов;
для предотвращения кодирования/декодирования содержимого и преобразования символов конца строки использует двоичный режим доступа к файлам, так как деревья могут содержать произвольные двоичные и текстовые файлы; функции listdir может потребоваться передавать аргумент типа bytes, если могут встречаться имена файлов, недекодируемые на других платформах
# сравнить списки с именами файлов
print(‘-’ * 20)
names1 = os.listdir(dir1)
names2 = os.listdir(dir2)
if not dirdiff.comparedirs(dir1, dir2, names1, names2):
diffs.append(‘unique files at %s — %s’ % (dir1, dir2))
print(‘Comparing contents’)
common = intersect(names1, names2)
missed = common[:]
# сравнить содержимое файлов с одинаковыми именами for name in common:
path1 = os.path.join(dir1, name)
path2 = os.path.join(dir2, name)
if os.path.isfile(path1) and os.path.isfile(path2):
missed.remove(name)
file1 = open(path1, ‘rb’)
file2 = open(path2, ‘rb’)
while True:
bytes1 = file1.read(blocksize)
bytes2 = file2.read(blocksize) if (not bytes1) and (not bytes2): if verbose: print(name, ‘matches’) break
if bytes1 != bytes2:
diffs.append(‘files differ at %s — %s’ % (path1, path2)) print(name, ‘DIFFERS’) break
# рекурсивно сравнить каталоги с одинаковыми именами for name in common:
path1 = os.path.join(dir1, name)
path2 = os.path.join(dir2, name)
if os.path.isdir(path1) and os.path.isdir(path2):
missed.remove(name)
comparetrees(path1, path2, diffs, verbose)
# одинаковые имена, но оба не являются одновременно файлами или каталогами? for name in missed:
diffs.append(‘files missed at %s — %s: %s’ % (dir1, dir2, name)) print(name, ‘DIFFERS’)
if __name__ == ‘__main__’:
dir1, dir2 = dirdiff.getargs()
diffs = []
comparetrees(dir1, dir2, diffs, True) # список diffs изменяется в print(‘=’ * 40) # процессе обхода, вывести diffs
if not diffs:
print(‘No diffs found.’)
else:
print(‘Diffs found:’, len(diffs)) for diff in diffs: print(‘-’, diff)
В каждом каталоге этого дерева данный сценарий просто использует модуль dirdiff, чтобы обнаружить уникальные имена, а затем сравнивает общие имена, присутствующие одновременно в обоих списках содержимого каталогов. Рекурсивный спуск в подкаталоги выполняется только после сравнения всех файлов на каждом уровне, чтобы вывод сценария было удобнее воспринимать на глаз (трассировка обхода подкаталогов выводится ниже результатов сравнения файлов — они не смешиваются).
Обратите внимание на список misses, добавленный в сценарий в третьем издании книги. Очень маловероятно, но не невозможно, чтобы одно и то же имя в одном каталоге соответствовало файлу, а в другом — подкаталогу. Кроме того, обратите внимание на переменную blocksize. Как и в сценарии копирования деревьев каталогов, который мы видели выше, вместо того чтобы слепо пытаться читать файлы в память целиком, мы установили ограничение в 1 Мбайт для каждой операции чтения — на тот случай, если какие-нибудь файлы окажутся слишком большими, чтобы их можно было загрузить в память. Предыдущая версия этого сценария, без этого ограничения, которая читала файлы целиком, как показано ниже, на некоторых компьютерах возбуждала исключение MemoryError:
bytes1 = open(path1, ‘rb’).read() bytes2 = open(path2, ‘rb’).read() if bytes1 == bytes2: …
Этот код проще, но менее практичен в ситуациях, когда могут встречаться очень большие файлы, не умещающиеся в память целиком (например, файлы образов CD и DVD). В новой версии файл читается в цикле порциями не более 1 Мбайта, пока не будет возвращена пустая строка, свидетельствующая об окончании файла. Файлы считаются одинаковыми, если совпадают все прочитанные из них блоки и конец файла достигнут одновременно.
Помимо всего прочего, мы обрабатываем содержимое файлов в двоичном режиме, чтобы подавить операцию декодирования их содержимого и предотвратить преобразование символов конца строки, потому что деревья каталогов могут содержать произвольные двоичные и текстовые файлы. Держим также наготове обычное замечание о необходимости передачи аргумента типа bytes функции os.listdir на платформах, где имена файлов могут оказаться недекодируемыми (например, с помощью dir1.encode()). На некоторых платформах может также потребоваться определять и пропускать некоторые файлы специальных типов, чтобы обеспечить полную универсальность, но в моих каталогах такие файлы отсутствовали, поэтому я не включил эту проверку в сценарий.
В четвертом издании было внесено одно незначительное изменение: для каждого подкаталога результаты os.listdir теперь собираются и передаются только один раз, чтобы избежать лишних вызовов функций из модуля dirdiff, — не самая большая победа, но на моем медлительном нетбуке, который я использовал во время работы над этим изданием, каждый лишний цикл на счету.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011