Более того, эта ошибка не возникает, если стандартный поток вывода сценария перенаправить в файл, на уровне командной оболочки (bigext-tree.py c:\ > out) или в вызове самой функции print (print(dir, file=F)). В последнем случае чтение выходного файла должно выполняться в двоичном режиме, так как попытка вывести в окно консоли содержимое файла, открытого в текстовом режиме, приведет к той же ошибке (и снова, ошибка не возникает, пока не будет предпринята попытка вывода). Фактически программный код, который терпит неудачу при запуске в окне программы Командная строка (Command Prompt) в Windows, работает без ошибок в графическом интерфейсе IDLE, на той же самой платформе, - графический интерфейс IDLE, реализованный на основе библиотеки tkinter, выполняет обработку отображаемых символов, что не делается, когда символы выводятся в поток стандартного вывода, подключенный к окну терминала:

>>> import os # запускайте в IDLE (в графическом интерфейсе на базе tkinter),

# а не в системной командной оболочке >>> root = r'C:\py3000'

>>> for (dir, subs, files) in os.walk(root): print(dir)

C:\py3000

C:\py3000\FutureProofPython - PythonInfo Wiki_files C:\py3000\0akwinter_com Code » Porting setuptools to py3k_files C:\py3000\What’s New in Python 3_0 - Python Documentation_files

Другими словами, исключение возникает только при выводе в окно командной оболочки, уже после того, как будет создана строка с именем файла. Это свидетельствует о выполнении дополнительных преобразований в подсистеме вывода Python, что никак не связано с наличием символов Юникода в именах файлов. Однако, за неимением места для дальнейших рассуждений, нам придется удовлетвориться констатацией факта, что наш обработчик исключений позволяет обойти проблему вывода. Однако вам следует помнить о проблемах декодирования символов Юникода в именах файлов - на некоторых платформах может потребоваться в этом сценарии передавать функции os.walk строки байтов, чтобы предотвратить ошибки декодирования имен файлов.25

Механизм поддержки Юникода в версии 3.1 все еще остается относительно новым, поэтому обязательно проверяйте наличие подобных ошибок в вашей системе и в вашей версии Python. Кроме того, дополнительную информацию по работе с именами файлов, содержащими символы Юникода, можно найти в руководствах по языку Python, а о поддержке Юникода вообще - в книге «Изучаем Python». Как отмечалось выше, сценарии должны открывать текстовые файлы в двоичном режиме, если они могут содержать недекодируемое содержимое. Может показаться странным, что проблемы, связанные с поддержкой Юникода, могут так отражаться на простейших операциях вывода, как в данном примере, но такова жизнь в прекрасном мире Юникода. При этом никаких проблем, связанных с Юникодом, не возникает в большой доле реальных сценариев, включая те, что мы будем рассматривать в следующем разделе.


Разрезание и объединение файлов

Мои дети, как и многие другие, проводили массу времени в Интернете, пока не повзрослели. Насколько мне известно, в наши дни это считалось стоящим занятием. Среди представителей этого нового поколения компьютерные фанаты и гуру пользуются, вероятно, таким же уважением, как когда-то рок-звезды для моего поколения. Когда дети скрываются в своих комнатах, они, скорее всего, возятся с компьютерами, а не осваивают гитарные аккорды (по крайней мере, некоторые из них). Пожалуй, это более здоровое занятие, чем некоторые утехи моей собственной растраченной юности, но это уже тема для книги иного сорта.

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

Проблема в том, что файлы с играми далеко не маленькие. Обычно они не умещались ни на гибких дисках, ни на флешках того времени, а запись на CD или DVD отнимала драгоценное время, которое можно было бы потратить на игру. Если бы все компьютеры у меня дома работали под управлением Linux, это не было бы проблемой. Для Unix существуют программы командной строки, позволяющие разрезать файлы на части, достаточно маленькие, чтобы уместиться на переносном устройстве (команда split), и программы для обратного объединения фрагментов (команда cat). Однако поскольку у нас дома были самые разные компьютеры, нам необходимо было более переносимое решение.26


Разрезание файлов переносимым способом

Так как на всех компьютерах в моем доме стоит Python, на помощь приходит простой переносимый сценарий на этом языке. В примере 6.5 приводится программа на языке Python, которая распределяет содержимое одного файла по группе файлов меньшего размера, которые сохраняются в каталоге.

Пример 6.5. PP4E\System\Filetools\split.py

#!/usr/bin/python

##############################################################################

разрезает файл на несколько частей; сценарий join.py объединяет эти части в один файл; данный сценарий является настраиваемой версией стандартной команды split в Unix; поскольку сценарий написан на языке Python, он с тем же успехом может использоваться в Windows и легко может быть модифицирован; благодаря тому, что он экспортирует функцию, его логику можно импортировать и повторно использовать в других приложениях;

##############################################################################

import sys, os kilobytes = 1024 megabytes = kilobytes * 1000

chunksize = int(1.4 * megabytes) # по умолчанию: примерный размер дискеты

def split(fromfile, todir, chunksize=chunksize):

if not os.path.exists(todir): # ошибки обрабатывает вызвавший

os.mkdir(todir) # создать каталог для фрагментов

else:

for fname in os.listdir(todir): # удалить все существующие файлы os.remove(os.path.join(todir, fname)) partnum = 0

input = open(fromfile, ‘rb’) # двоичный режим: без декодирования и

# без преобразования символов конца

# строки

while True: # eof = прочтена пустая строка

chunk = input.read(chunksize) # прочитать кусок <= chunksize if not chunk: break partnum += 1

filename = os.path.join(todir, (‘part%04d’ % partnum)) fileobj = open(filename, ‘wb’) fileobj.write(chunk)

fileobj.close() # или просто open().write()

input.close()

assert partnum <= 9999 # сортировка в join невозможна,

return partnum # если будет 5 цифр

if __name__ == ‘__main__’:

if len(sys.argv) == 2 and sys.argv[1] == ‘-help’:

print(‘Use: split.py [file-to-split target-dir [chunksize]]’) else:

if len(sys.argv) < 3: interactive = True

fromfile = input(‘File to be split? ‘) # ввод данных, если

# запущен щелчком мыши

todir = input(‘Directory to store part files? ‘) else:

interactive = False

fromfile, todir = sys.argv[1:3] # аргументы командной строки if len(sys.argv) == 4: chunksize = int(sys.argv[3]) absfrom, absto = map(os.path.abspath, [fromfile, todir]) print(‘Splitting’, absfrom, ‘to’, absto, ‘by’, chunksize)

try:

parts = split(fromfile, todir, chunksize) except:

print(‘Error during split:’) print(sys.exc_info()[0], sys.exc_info()[1]) else:

print(‘Split finished:’, parts, ‘parts are in’, absto) if interactive: input(‘Press Enter key’) # пауза, если сценарий

# запущен щелчком мыши

По умолчанию этот сценарий разрезает исходный файл на фрагменты, примерно равные размеру дискеты, - идеальные для перемещения больших файлов между не связанными между собой компьютерами. Самое важное здесь - это полностью переносимый программный код; данный сценарий будет работать на любом компьютере, где нет своей встроенной программы для разрезания файлов. Главное, чтобы на компьютере был установлен интерпретатор Python. Ниже приводится пример использования этого сценария для разрезания самоустанавливающегося выполняемого файла Python 3.1 в Windows в текущем каталоге (для экономии места я опустил некоторые строки, которые выводит команда dir; в Unix воспользуйтесь командой ls -l):

C:\temp> cd C:\temp

C:\temp> dir python-3.1.msi

...часть строк опущена...

06/27/2009 04:53 PM 13,814,272 python-3.1.msi

1 File(s) 13,814,272 bytes 0 Dir(s) 1 88,826,1 89,824 bytes free

C:\temp> python C:\...\PP4E\System\Filetools\split.py -help

Use: split.py [file-to-split target-dir [chunksize]]

C:\temp> python C:\...\P4E\System\Filetools\split.py python-3.1.msi pysplit

Splitting C:\temp\python-3.1.msi to C:\temp\pysplit by 1433600

Split finished: 10 parts are in C:\temp\pysplit


C:\temp> dir pysplit

...часть строк опущена...


02/21/2010

11

13

AM


02/21/2010

11

13

AM


02/21/2010

11

13

AM

1

433,600

part0001

02/21/2010

11

13

AM

1

433,600

part0002

02/21/2010

11

13

AM

1

433,600

part0003

02/21/2010

11

13

AM

1

433,600

part0004

02/21/2010

11

13

AM

1

433,600

part0005

02/21/2010

11

13

AM

1

433,600

part0006

02/21/2010

11

13

AM

1

433,600

part0007

02/21/2010

11

13

AM

1

433,600

part0008

02/21/2010

11

13

AM

1

433,600

part0009

02/21/2010

11

13

AM


911,872

part0010


10

File(s

13,814,272

bytes


2

Dir(s)

188,812,328,960 bytes free


Каждый из созданных здесь файлов фрагментов представляет один двоичный кусок файла python-3.1.msi, достаточно маленький, чтобы поместиться на одной дискете. Действительно, если сложить вместе размеры созданных фрагментов, показанные командой dir, то получится точно то же число байтов, что и в оригинальном файле. Прежде чем пытаться снова сложить вместе эти файлы, рассмотрим несколько примечаний, касающихся сценария.

Режимы работы

Данный сценарий может получать свои параметры в интерактивном режиме или в режиме командной строки - он проверяет количество аргументов командной строки, чтобы определить, в каком режиме используется. В режиме командной строки сценарию передаются имя файла, который нужно разрезать, и каталог для сохранения фрагментов. При этом с помощью необязательного третьего аргумента можно переопределить установленный по умолчанию размер одного фрагмента.

В интерактивном режиме сценарий запрашивает имя файла и каталог для сохранения фрагментов в окне консоли с помощью функции input и перед завершением делает остановку, ожидая нажатия клавиши. Этот режим используется, когда программа запускается щелчком на значке файла, - в Windows параметры вводятся во всплывающем окне DOS, которое в этом случае не исчезает автоматически. Сценарий также показывает абсолютные пути для своих параметров (пропуская их через os.path.abspath), потому что в интерактивном режиме они могут быть не очевидны.

Двоичный режим доступа к файлам

Этот сценарий открывает входные и выходные файлы в двоичном режиме (rb, wb), потому что такие файлы, как выполняемые или аудиофайлы, должны обрабатываться переносимым способом, не как текст. В главе 4 мы узнали, что в Windows при работе с текстовыми файлами символы конца строки \r\n автоматически отображаются в \n при вводе, и \n отображается в \r\n при выводе. При работе с двоичными данными было бы нежелательно, чтобы \r исчезали при чтении и ненужные символы \r попадали бы при записи в выходной файл. Для файлов, открываемых в двоичном режиме в Windows, такая трансформация символа \r не производится, и искажения данных не происходит.

Кроме того, в Python 3.X при работе с файлами в двоичном режиме данные в сценарии представляются в виде объектов bytes, а не в виде кодированного текста str. При этом нам даже не приходится предусматривать выполнение каких-то особых действий - операции по обработке файлов, реализованные в сценарии, действуют одинаково и в Python 3.X, и в Python 2.X. Фактически двоичный режим является обязательным для версии 3.X, потому что данные, записываемые в выходной файл, вообще могут не быть текстом - текстовый режим необходим в версии 3.X, только когда приходится выполнять декодирование содержимого файла, которое может приводить к ошибкам при работе с настоящими двоичными файлами и с текстовыми файлами, полученными из других систем. Операция записи в двоичном режиме принимает объект типа bytes и подавляет кодирование символов Юникода, а также преобразование символов конца строки.

Закрытие файлов вручную

Этот сценарий также заботится о том, чтобы вручную закрыть используемые им файлы. Как мы видели в главе 4, часто это можно сделать одной строкой: open(partname, ‘wb’).write(chunk). Эта более короткая форма использует тот факт, что в текущей реализации Python файлы автоматически закрываются при уничтожении объектов файлов (то есть при утилизации их на этапе сборки мусора, когда не остается ссылок на объект файла). В этой строке объект файла будет уничтожен немедленно, потому что результат open является в выражении временным и ссылка на него не сохраняется в какой-либо долгоживущей переменной. Аналогично при выходе из функции split уничтожается объект файла input.

Однако есть вероятность, что такой режим автоматического закрытия в будущем может измениться. Более того, в Jython - реализации Python на языке Java - объекты, на которые нет ссылок, не уничтожаются с такой же поспешностью, как в стандартной реализации Python. Если сейчас или в будущем вам может потребоваться перейти на Java, а ваш сценарий в состоянии создать много файлов за короткий промежуток времени и, возможно, будет выполняться в системе, которая ограничивает количество открытых файлов в каждой программе, закрывайте файлы вручную. Поскольку функция split в этом модуле задумана как универсальный инструмент, в ней учтены варианты такого наихудшего развития событий. В главе 4 также упоминался менеджер контекста файла и инструкция with - они обеспечивают альтернативный способ гарантированного закрытия файлов.


Соединение файлов переносимым образом

Вернемся к перемещению больших файлов по дому: после загрузки больших файлов игровых программ вы можете воспользоваться предыдущим сценарием split.py, щелкнув на нем в окне Проводника Windows (Windows Explorer) и введя имена файлов. После разрезания просто скопируйте каждый файл фрагмента на отдельную дискету (или на более современный носитель), перейдите с дискетами к требуемому компьютеру и скопируйте файлы фрагментов с дискет. Затем щелкните на файле сценария из примера 6.6 (или запустите его другим способом), чтобы вновь объединить фрагменты.

Пример 6.6. PP4E\System\Filetools\join.py

#!/usr/bin/python

##############################################################################

объединяет все файлы фрагментов, имеющиеся в каталоге и созданные с помощью сценария split.py,воссоздавая первоначальный файл.

По своему действию этот сценарий напоминает команду ‘cat fromdir/* > tofile’ в Unix, но данная реализация более переносимая и настраиваемая; сценарий

экспортирует операцию объединения в виде функции, доступной для многократного использования. Зависит от порядка сортировки имен файлов, поэтому все они должны быть одинаковой длины. Сценарии разрезания/объединения можно дополнить возможностью вывода диалога с графическим интерфейсом tkinter, позволяющего выбирать файлы.

##############################################################################

import os, sys readsize = 1024

def join(fromdir, tofile):

output = open(tofile, ‘wb’) parts = os.listdir(fromdir) parts.sort() for filename in parts:

filepath = os.path.join(fromdir, filename) fileobj = open(filepath, ‘rb’) while True:

filebytes = fileobj.read(readsize) if not filebytes: break output.write(filebytes) fileobj.close() output.close()

if __name__ == ‘__main__’:

if len(sys.argv) == 2 and sys.argv[1] == ‘-help’:

print(‘Use: join.py [from-dir-name to-file-name]’) else:

if len(sys.argv) != 3: interactive = True

fromdir = input(‘Directory containing part files? ‘) tofile = input(‘Name of file to be recreated? ‘) else:

interactive = False fromdir, tofile = sys.argv[1:] absfrom, absto = map(os.path.abspath, [fromdir, tofile]) print(‘Joining’, absfrom, ‘to make’, absto)

try:

join(fromdir, tofile) except:

print(‘Error joining files:’) print(sys.exc_info()[0], sys.exc_info()[1]) else:

print(‘Join complete: see’, absto) if interactive: input(‘Press Enter key’) # пауза, если сценарий

# запущен щелчком мыши

Ниже приводится пример объединения файлов фрагментов в Windows, созданных нами минуту назад. После выполнения сценария join вам может потребоваться воспользоваться какими-нибудь другими утилитами, такими как zip, gzip или tar, чтобы распаковать архивный файл, если он поставлялся не исполняемым, но в любом случае загруженный файл будет готов к дальнейшему использованию:27

C:\temp> python C:\...\PP4E\System\Filetools\join.py -help

Use: join.py [from-dir-name to-file-name]

C:\temp> python C:\...\PP4E\System\Filetools\join.py pysplit mypy31.msi

Joining C:\temp\pysplit to make C:\temp\mypy31.msi

Join complete: see C:\temp\mypy31.msi

C:\temp> dir *.msi

...часть строк опущена...

02/21/2010 11:21 AM 13,814,272 mypy31.msi

06/27/2009 04:53 PM 13,814,272 python-3.1.msi

2 File(s) 27,628,544 bytes 0 Dir(s) 188,798,611,456 bytes free

C:\temp> fc /b mypy31.msi python-3.1.msi

Comparing files mypy31.msi and PYTHON-3.1.MSI

FC: no differences encountered

Чтобы выбрать все части файла, присутствующие в каталоге, сценарий join использует функцию os.listdir и сортирует список имен файлов, чтобы расположить части в правильном порядке. Получается точная байт-в-байт копия исходного файла (что проверяется выше командой DOS fc; в Unix используйте команду cmp).

Конечно, часть этого процесса выполняется вручную (я еще не придумал, как запрограммировать этап «перехода с дискетами к другому компьютеру»), но с помощью сценариев split и join перемещение больших файлов становится быстрым и простым. Так как этот сценарий является также переносимым программным кодом Python, он выполняется на любой платформе, на которую может понадобиться перенести разрезанные файлы. Например, у меня дома есть компьютеры, работающие под управлением не только Windows, но и Linux; а так как сценарий выполняется на любой из платформ, у моих игроков не возникает проблем. Однако, прежде чем двинуться дальше, рассмотрим несколько особенностей реализации сценария join:

Чтение файлов блоками целиком

Прежде всего, обратите внимание, что сценарий работает с файлами в двоичном режиме, а также читает файлы фрагментов блоками размером в 1 Кбайт. Значение readsize (размер блоков, читаемых из входного файла) не имеет никакого отношения к chunksize в сценарии split.py (общий размер каждого выходного файла). Как было показано в главе 4, каждый файл фрагмента можно было бы прочесть сразу целиком: output.write(open(filepath, ‘rb’).read()). Недостаток такого приема в том, что при этом весь файл целиком загружается в оперативную память. Например, при чтении файла фрагмента размером 1.4 Мбайт в память целиком в ней будет создана строка размером 1.4 Мбайт, содержащая все байты файла. Поскольку сценарий split разрешает пользователям указывать еще более крупные размеры фрагментов, сценарий join ожидает худшего и читает содержимое файлов блоками ограниченного размера. Полная надежность была бы обеспечена, если бы сценарий split также читал исходный файл меньшими порциями, но на практике в этом нет необходимости (напомню, что в процессе выполнения программы интерпретатор автоматически утилизирует строки, на которые нет ни одной ссылки, поэтому данная реализация не так расточительна, как могло бы показаться).

Сортировка имен файлов

Если внимательно изучить реализацию сценария, можно заметить, что порядок объединения полностью зависит от порядка сортировки имен файлов в каталоге с файлами фрагментов. Так как сценарий объединения просто вызывает метод sort списка имен файлов, возвращаемого функцией os.listdir, он подразумевает, что при разрезании создаются файлы с именами одинаковой длины и имеющими один и тот же формат. Чтобы удовлетворить это требование, сценарий split использует выражение форматирования (‘ part%04d ’), которое добавляет незначащие нули и тем самым обеспечивает присутствие одинакового количества цифр (четырех) в именах файлов. Наличие ведущих нулей в маленьких числах гарантирует, что имена файлов фрагментов будут отсортированы правильно.

При желании можно было бы извлекать цифры из имен файлов, преобразовывать их в значения int и выполнять числовую сортровку, воспользовавшись аргументом keys метода sort списков, но в этом случае все равно необходимо, чтобы все имена файлов начинались с подстроки некоторого вида, и это не устранило бы зависимость между сценариями split и join. Однако поскольку эти сценарии задуманы как два этапа одного и того же процесса, какие-то зависимости между ними выглядят вполне оправданными.


Варианты использования

Проделаем еще несколько экспериментов с этими системными утилитами Python, чтобы продемонстрировать другие режимы работы. Если при запуске в командной строке заданы не все аргументы, сценарии split и join достаточно сообразительны, чтобы попросить пользователя ввести параметры интерактивно. Рассмотрим снова процесс разрезания и склеивания самоустанавливающегося файла Python в Windows, когда параметры вводятся в окне консоли DOS:

C:\temp> python C:\...\PP4E\System\Filetools\split.py File to be split? python-3.1.msi

Directory to store part files? splitout

Splitting C:\temp\python-3.1.msi to C:\temp\splitout by 1433600 Split finished: 10 parts are in C:\temp\splitout Press Enter key

C:\temp> python C:\...\PP4E\System\Filetools\join.py

Directory containing part files? splitout Name of file to be recreated? newpy31.msi Joining C:\temp\splitout to make C:\temp\newpy31.msi Join complete: see C:\temp\newpy31.msi Press Enter key

C:\temp> fc /B python-3.1.msi newpy31.msi

Comparing files python-3.1.msi and NEWPY31.MSI FC: no differences encountered

Когда эти программы запускаются в проводнике файлов Windows двойным щелчком мыши, они действуют точно так же (при таком способе запуска они обычно не получают аргументов командной строки). В таком режиме вывод абсолютного пути помогает прояснить, где в действительности находятся файлы. Помните, что при запуске щелчком мыши на файле текущим рабочим каталогом является исходный каталог сценария, поэтому при вводе просто имени файла оно будет отнесено в каталог со сценарием. Чтобы поместить файлы фрагментов в другое место, введите полный путь:

[во всплывающем окне консоли DOS, когда split.py запущен щелчком мыши] File to be split? c:\temp\python-3.1.msi

Directory to store part files? c:\temp\parts Splitting c:\temp\python-3.1.msi to c:\temp\parts by 1433600 Split finished: 10 parts are in c:\temp\parts Press Enter key

[во всплывающем окне консоли DOS, когда join.py запущен щелчком мыши]

Directory containing part files? c:\temp\parts Name of file to be recreated? c:\temp\morepy31.msi Joining c:\temp\parts to make c:\temp\morepy31.msi Join complete: see c:\temp\morepy31.msi Press Enter key

Поскольку основная логика этих сценариев оформлена в виде функций, их легко можно использовать, импортировав и вызвав из другого компонента Python (убедитесь, что путь поиска модулей включает каталог, содержащий корень PP4E, - первая сокращенная строка в следующем примере демонстрирует один из способов добиться этого):

C:\temp> set PYTHONPATH=C:\...\dev\Examples C:\temp> python

>>> from PP4E.System.Filetools.split import split >>> from PP4E.System.Filetools.join import join

>>>

>>> numparts = split('python-3.1.msi', 'calldir')

>>> numparts

10

>>> join('calldir', 'callpy31.msi')

>>>

>>> import os

>>> os.system('fc /B python-3.1.msi callpy31.msi')

Comparing files python-3.1.msi and CALLPY31.msi FC: no differences encountered 0

Замечание, касающееся производительности: все приведенные здесь примеры запуска сценариев split и join обрабатывают файл размером 13 Мбайт и выполняются не более 1 секунды реального времени на моем ноутбуке, работающем под управлением Windows 7 и снабженном процессором Atom с тактовой частотой 2 Ггц, - достаточно быстро для любого мыслимого применения. Оба сценария столь же быстро справляются и с другими значениями размеров фрагментов. Ниже показано, как выполняется разрезание файла на фрагменты по 4 Мбайта и 500 Кбайт:

C:\temp> C:\...\PP4E\System\Filetools\split.py python-3.1.msi tempsplit 4000000

Splitting C:\temp\python-3.1.msi to C:\temp\tempsplit by 4000000 Split finished: 4 parts are in C:\temp\tempsplit

C:\temp> dir tempsplit ...часть строк опущена...

Directory of C:\temp\tempsplit

02/21/2010

01

27

PM

02/21/2010

01

27

PM

02/21/2010

01

27

PM

02/21/2010

01

27

PM

02/21/2010

01

27

PM

02/21/2010

01

27

PM

4

File(s

2

Dir(s)

.

..

4.000. 000 part0001

4.000. 000 part0002

4.000. 000 part0003

1.814.272 part0004

13.814.272 bytes 188,671,983,616 bytes free

C:\temp> C:\...\PP4E\System\Filetools\split.py python-3.1.msi tempsplit 500000

Splitting C:\temp\python-3.1.msi to C:\temp\tempsplit by 500000

Split finished: 28 parts are in C:\temp\tempsplit

C:\temp> dir tempsplit

...часть строк опущена...

Directory of C:\temp\tempsplit


02/21/2010

01

27

PM


02/21/2010

01

27

PM


02/21/2010

01

27

PM

500,

000

part0001

02/21/2010

01

27

PM

500,

000

part0002

02/21/2010

01

27

PM

500,

000

part0003

02/21/2010

01

27

PM

500,

000

part0004

02/21/2010

01

27

PM

500,

000

part0005

...часть строк опущена...


02/21/2010

01

27

PM

500,

000

part0024

02/21/2010

01

27

PM

500,

000

part0025

02/21/2010

01

27

PM

500,

000

part0026

02/21/2010

01

27

PM

500,

000

part0027

02/21/2010

01

27

PM

314,

272

part0028


28

Tle(s) 13,814

272

bytes


2 Dir(s) 188,671,946,752 bytes free

Разрезание может выполняться заметно дольше, если указать размеры файлов фрагментов настолько маленькими, что это приведет к созданию тысяч фрагментов, - разрезание на 1382 фрагмента выполняется медленнее (впрочем, некоторые современные компьютеры настолько быстрые, что разницу во времени можно не заметить):

C:\temp> C:\...\PP4E\System\Filetools\split.py python-3.1.msi tempsplit 10000

Splitting C:\temp\python-3.1.msi to C:\temp\tempsplit by 10000 Split finished: 1382 parts are in C:\temp\tempsplit

C:\temp> C:\...\PP4E\System\Filetools\join.py tempsplit manypy31.msi

Joining C:\temp\tempsplit to make C:\temp\manypy31.msi Join complete: see C:\temp\manypy31.msi

C:\temp> fc /B python-3.1.msi manypy31.msi

Comparing files python-3.1.msi and MANYPY31.MSI FC: no differences encountered

C:\temp> dir tempsplit

...часть строк опущена...

Directory of C:\temp\tempsplit


02/21/2010

01

40

PM


02/21/2010

01

40

PM


02/21/2010

01

39

PM

10,000

part0001

02/21/2010

01

39

PM

10,000

part0002

02/21/2010

01

39

PM

10,000

part0003


02/21/2010

01

39

PM

10,000

part0004

02/21/2010

01

39

PM

10,000

part0005

...более 1000 строк опущено...


02/21/2010

01

40

PM

10,000

part1378

02/21/2010

01

40

PM

10,000

part1379

02/21/2010

01

40

PM

10,000

part1380

02/21/2010

01

40

PM

10,000

part1381

02/21/2010

01

40

PM

4,272

part1382


1382

File(s)

13,814,272

bytes


2

Dir(s)

188,651,008,000 bytes free

Наконец, сценарий split достаточно умен, чтобы создать выходной каталог, если его не существует, или очистить его от старых файлов, если он существует, - в следующем примере видно, что в выходном каталоге остались только новые файлы. Так как сценарий join объединяет все файлы, присутствующие в выходном каталоге, это полезное эргономическое дополнение. Если бы выходной каталог не очищался перед каждым запуском сценария split, можно было бы легко забыть, что в каталоге находятся файлы от предыдущего прогона. Поскольку эти сценарии предназначены для выполнения простыми пользователями, они должны быть как можно более снисходительными. Ваши пользователи могут оказаться более подготовленными (хотя вам не следует надеяться на это).

C:\temp> C:\...\PP4E\System\Filetools\split.py python-3.1.msi tempsplit 5000000

Splitting C:\temp\python-3.1.msi to C:\temp\tempsplit by 5000000

Split finished: 3 parts are in C:\temp\tempsplit


C:\temp> dir tempsplit

...часть строк опущена...

Directory of C:\temp\tempsplit


02/21/2010

01

47

PM

.

02/21/2010

01

47

PM

..

02/21/2010

01

47

PM

5,000,000 part0001

02/21/2010

01

47

PM

5,000,000 part0002

02/21/2010

01

47

PM

3,814,272 part0003


3

File(s)

13,814,272 bytes


2

Dir(s)

188,654,452,736 bytes free


Конечно, проблему, которую решают эти сценарии, в настоящее время легко можно решить, просто купив более емкую флешку или предоставив детям выход в Интернет. Тем не менее, как только вы решите какую-либо проблему с помощью сценариев, вы поймете, насколько простым и гибким средством является Python, снабженный мощными встроенными инструментами, особенно удобными для разработки сценариев автоматизации, таких как эти. При грамотном использовании Python может стать для вас аналогом швейцарского армейского ножа.


Создание веб-страниц для переадресации

Перемещаться всегда трудно, даже в киберпространстве. Изменение адреса веб-сайта в Интернете может привести ко всякого рода неудобствам. Вам придется просить знакомых использовать новый адрес, а в случае всех остальных - надеяться, что по ходу дела они наткнутся на него сами. Но если ваша работа зависит от Интернета, то перемещение вызывает не меньше проблем, чем смена адреса в реальном мире.

К сожалению, таких перемещений сайта часто невозможно избежать. Как провайдеры интернет-услуг (Internet Service Providers, ISP), так и серверы с течением времени приходят и уходят. Кроме того, некоторые провайдеры допускают падение уровня обслуживания до неприемлемого уровня; если вам не повезло и случилось подписаться на услуги такого провайдера, не остается ничего иного, как сменить его, а это часто требует изменения веб-адреса.28

Представьте себе, однако, что вы пишете книги для издательства O’Reilly и опубликовали адрес своего веб-сайта во многих книгах, продаваемых по всему свету. Что делать, если качество обслуживания вашего провайдера такое, что требуется переместить сайт? Оповещение об этом сотен тысяч читателей не представляется возможным.

Вероятно, лучшее, что можно сделать, это на достаточно продолжительный промежуток времени поместить на прежнем сайте инструкции по переадресации - виртуальный эквивалент вывески «Мы переехали по новому адресу» в витрине магазина. В Сети такое объявление может автоматически отправлять посетителей на новый сайт: нужно просто оставить на прежнем сайте страничку, содержащую гиперссылку на адрес страницы на новом сайте и обеспечивающую возможность автоматического перемещения через несколько секунд. При наличии таких файлов со ссылками переадресации посетители прежнего адреса окажутся на расстоянии одного щелчка от нового адреса.

Звучит довольно просто. Но поскольку посетители могут попытаться непосредственно обратиться по адресу любого файла на вашем прежнем сайте, в общей сложности вам потребуется оставить по одному файлу со ссылкой переадресации для каждого прежнего файла - страниц

HTML, графических файлов и так далее. Если ваш прежний сервер не поддерживает возможность автоматической переадресации (мой не поддерживал), это может превратиться в проблему. Если вам нравится бездумно вводить с клавиатуры большие объемы данных, можете вручную создать все файлы со ссылками переадресации. Но с учетом того, что мой домашний сайт содержал более 100 файлов HTML к тому времени, когда я писал этот абзац, перспектива запускать редактор для каждого файла оказалась более чем достаточной мотивацией для выработки автоматизированного решения.


Файл шаблона страницы

Вот что я придумал. Прежде всего, я создал текстовый файл универсального шаблона страницы, представленный в примере 6.7 и описывающий вид всех файлов со ссылками переадресации, части которых будут заполнены позднее.

Пример 6.7. PP4E\System\Filetools\template.html

Site Redirection Page: $file$

This page has moved

This page now lives at this address:

http://$server$/$home$/$file$

Please click on the new address to jump to this page, and update any links accordingly. You will be redirectly shortly.


Чтобы полностью разобраться в этом шаблоне, требуется некоторое знание HTML - языка описания веб-страниц, который мы рассмотрим в четвертой части книги. Но для целей данного примера можно проигнорировать большую часть содержимого этого файла и сосредоточиться только на тех частях, которые окружены знаками доллара: строки $server$, $home$ и $file$ являются элементами, которые должны быть заменены действительными значениями с помощью операции глобального поиска с заменой. Значения этих элементов зависят от места, куда был перемещен сайт, и от имени файла.


Сценарий генератора страниц

Теперь, имея файл шаблона страницы, сценарий Python из примера 6.8 автоматически сгенерирует все необходимые файлы со ссылками переадресации.

Пример 6.8. PP4E\System\Filetools\site-forward.py

##############################################################################

Создает страницы со ссылками переадресации на перемещенный веб-сайт.

Генерирует по одной странице для каждого существующего на сайте файла html; сгенерированные файлы нужно выгрузить на ваш старый веб-сайт. Смотрите описание ftplib далее в книге, где представлены приемы реализации выгрузки файлов в сценариях после или в процессе создания файлов страниц. ##############################################################################

import os

servername = ‘learning-python.com’ # новый адрес сайта homedir = ‘books’ # корневой каталог сайта

sitefilesdir = r’C:\temp\public_html’ # локальный каталог с файлами сайта uploaddir = r’C:\temp\isp-forward’ # где сохранять сгенерированные файлы templatename = ‘template.html’ # шаблон для генерируемых страниц

try:

os.mkdir(uploaddir) # при необходимости создать каталог для

except OSError: pass # выгружаемых страниц

template = open(templatename).read() # загрузить или импортировать шаблон sitefiles = os.listdir(sitefilesdir) # имена файлов без пути к ним

count = 0

for filename in sitefiles:

if filename.endswith(’.html’) or filename.endswith(’.htm’): fwdname = os.path.join(uploaddir, filename) print(‘creating’, filename, ‘as’, fwdname)

filetext = template.replace(‘$server$’, servername) # вставить текст filetext = filetext.replace(‘$home$’, homedir) # и записать

filetext = filetext.replace(‘$file$’, filename) # измененный файл

open(fwdname, ‘w’).write(filetext) count += 1

print(‘Last file =>\n’, filetext, sep=’’) print(‘Done:’, count, ‘forward files created.’)

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

Но главное, что нужно отметить, - этому сценарию совершенно безразлично, как выглядит файл шаблона. Он просто слепо выполняет глобальную подстановку с различными именами файлов для каждого генерируемого файла. Фактически файл шаблона можно изменить как угодно, и это никак не отразится на сценарии. Такое разделение труда может быть использовано в самых разных ситуациях - при создании «make-файлов», бланков писем, ответов CGI-сценариев на веб-сервере и так далее. Что касается библиотечных инструментов, сценарий генератора:

• Использует функцию os.listdir для обхода всех имен файлов в каталоге сайта (точно так же можно было бы использовать функцию glob.glob, но при этом потребовалось бы удалять пути к файлам из их имен)

• Использует строковый метод replace для поиска и замены элементов в тексте файла шаблона, ограниченных символами $, и метод end-swith, чтобы пропустить файлы, не являющиеся страницами HTML (например, изображения - большинство броузеров не знают, что делать с разметкой HTML в файлах «.jpg»)

• Использует функцию os.path.join и встроенные объекты файлов для записи полученного текста в файл со ссылками переадресации с тем же именем в выходном каталоге

Окончательным результатом является зеркальное отражение первоначального каталога веб-сайта, содержащее только файлы со ссылками переадресации, созданные по шаблону страницы. Дополнительным преимуществом сценария генератора является возможность его выполнения практически на любой платформе Python. Я запускал его на ноутбуке, работающем под управлением Windows (на котором я пишу эту книгу), а также на Linux-сервере (где находится мой сайт http:// learning-python.com). Ниже показан пример запуска этого сценария в Windows:

C:\...\PP4E\System\Filetools> python site-forward.py

creating about-lp.html as C:\temp\isp-forward\about-lp.html creating about-lp1e.html as C:\temp\isp-forward\about-lp1e.html creating about-lp2e.html as C:\temp\isp-forward\about-lp2e.html creating about-lp3e.html as C:\temp\isp-forward\about-lp3e.html creating about-lp4e.html as C:\temp\isp-forward\about-lp4e.html ...множество строк удалено...

creating training.html as C:\temp\isp-forward\training.html creating whatsnew.html as C:\temp\isp-forward\whatsnew.html creating whatsold.html as C:\temp\isp-forward\whatsold.html creating xlate-lp.html as C:\temp\isp-forward\xlate-lp.html creating zopeoutline.htm as C:\temp\isp-forward\zopeoutline.htm Last file =>

Site Redirection Page: zopeoutline.htm

This page has moved

This page now lives at this address:

http://learning-python.com/books/zopeoutline.htm

Please click on the new address to jump to this page, and update any links accordingly. You will be redirectly shortly.


Done: 124 forward files created.

Чтобы проверить результаты работы сценария, щелкните дважды на любом сгенерированном файле и посмотрите, как он выглядит в вебброузере (или выполните команду start в окне консоли DOS, например

start isp-forward\about-lp4e.html). На рис. 6.1 показано, как выглядит одна из сгенерированных страниц на моем компьютере.

Рис. 6.1. Сгенерированная страница переадресации сайта

Для завершения процесса еще необходимо установить ссылки переадресации: выгрузите все сгенерированные файлы из выходного каталога в веб-каталог вашего старого сайта. Если и это слишком большой объем для ручной работы, посмотрите, как это можно сделать автоматически с помощью Python посредством сценария загрузки на сервер по FTP в главе 13 (эту работу выполняет сценарий PP4E\Internet\Ftp\ uploadflat.py). Всерьез занявшись написанием сценариев, вы поразитесь тому, какой объем ручного труда можно автоматизировать с помощью Python. В следующем разделе представлен еще один большой пример.


Сценарий регрессивного тестирования

Рано или поздно ошибки случаются. Как мы уже видели, Python предоставляет интерфейсы доступа к различным системным службам, а также инструменты для добавления новых интерфейсов. В примере 6.9 показаны некоторые часто используемые системные инструменты в действии. В нем реализована простая система регрессивного тестирования сценариев на языке Python. Она запускает каждый сценарий Python в указанном каталоге с заданным набором входных файлов и аргументов командной строки и сравнивает вывод каждого прогона с предыдущими результатами. Этот сценарий можно было бы использовать в качестве автоматизированной системы тестирования, чтобы отлавливать ошибки, появляющиеся в результате изменений в исходных файлах программы, - в большой системе нельзя быть уверенным в том, что исправление не является в действительности скрытой ошибкой.

Пример 6.9. PP4E\System\Tester\tester.py

##############################################################################

Тестирует сценарии Python в каталоге, передает им аргументы командной строки, выполняет перенаправление stdin, перехватывает stdout, stderr и код завершения, чтобы определить наличие ошибок и отклонений от предыдущих результатов выполнения. Запуск сценариев и управление потоками ввода-вывода производится с помощью переносимого модуля subprocess (как это делает функция os.popen3 в Python 2.X). Потоки ввода-вывода всегда интерпретируются модулем subprocess как двоичные. Стандартный ввод, аргументы, стандартный вывод и стандартный вывод ошибок отображаются в файлы, находящиеся в подкаталогах.

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

Дополнительные возможные расширения: можно было бы реализовать по несколько наборов аргументов командной строки и/или входных файлов для каждого тестируемого сценария и запускать их по несколько раз (использовать функцию glob для выборки нескольких файлов “.in*” в каталоге Inputs).

Возможно, было бы проще хранить все файлы, необходимые для проведения тестов, в одном и том же каталоге, но с различными расширениями, однако с течением времени их объем мог бы оказаться слишком большим.

В случае ошибок можно было бы сохранять содержимое потоков вывода stderr и stdout в подкаталоге Errors, но я предпочитаю иметь ожидаемый/фактический вывод в подкаталоге Outputs.

##############################################################################

import os, sys, glob, time

from subprocess import Popen, PIPE

# конфигурационные аргументы

testdir = sys.argv[1] if len(sys.argv) > 1 else os.curdir forcegen = len(sys.argv) > 2 print(‘Start tester:’, time.asctime()) print(‘in’, os.path.abspath(testdir))

def verbose(*args): print(‘-’*80)

for arg in args: print(arg) def quiet(*args): pass trace = quiet

# отбор сценариев для тестирования

testpatt = os.path.join(testdir, ‘Scripts’, ‘*.py’) testfiles = glob.glob(testpatt) testfiles.sort() trace(os.getcwd(), *testfiles)

numfail = 0

for testpath in testfiles: # протестировать все сценарии

testname = os.path.basename(testpath) # отбросить путь к файлу

# получить входной файл и аргументы для тестируемого сценария infile = testname.replace(‘.py’, ‘.in’)

inpath = os.path.join(testdir, ‘Inputs’, infile)

indata = open(inpath, ‘rb’).read() if os.path.exists(inpath) else b’’

argfile = testname.replace(‘.py’, ‘.args’)

argpath = os.path.join(testdir, ‘Args’, argfile)

argdata = open(argpath).read() if os.path.exists(argpath) else ''

# местоположение файлов для сохранения stdout и stderr,

# очистить предыдущие результаты outfile = testname.replace(‘.py’, ‘.out’) outpath = os.path.join(testdir, ‘Outputs’, outfile) outpathbad = outpath + ‘.bad’

if os.path.exists(outpathbad): os.remove(outpathbad)

errfile = testname.replace(‘.py’, ‘.err’) errpath = os.path.join(testdir, ‘Errors’, errfile) if os.path.exists(errpath): os.remove(errpath)

# запустить тестируемый сценарий, перенаправив потоки ввода-вывода pypath = sys.executable

command = ‘%s %s %s’ % (pypath, testpath, argdata) trace(command, indata)

process = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)

process.stdin.write(indata)

process.stdin.close()

outdata = process.stdout.read()

errdata = process.stderr.read() # при работе с двоичными файлами

exitstatus = process.wait() # данные имеют тип bytes

trace(outdata, errdata, exitstatus)

# проанализировать результаты if exitstatus != 0:

print(‘ERROR status:’, testname, exitstatus) # код заверш.

if errdata: # и/или stderr

print(‘ERROR stream:’, testname, errpath) # сохр. текст ошибки

open(errpath, ‘wb’).write(errdata)

if exitstatus or errdata: # оба признака ошибки

numfail += 1 # можно получить код завершения + код ошибки

open(outpathbad, ‘wb’).write(outdata) # сохранить вывод

elif not os.path.exists(outpath) or forcegen:

print(‘generating:’, outpath) # создать файл, если

open(outpath, ‘wb’).write(outdata) # необходимо

else:

priorout = open(outpath, ‘rb’).read() # или сравнить с прежними

# результатами

if priorout == outdata:

print(‘passed:’, testname) else:

numfail += 1

print(‘FAILED output:’, testname, outpathbad) open(outpathbad, ‘wb’).write(outdata)

print(‘Finished:’, time.asctime())

print(‘%s tests were run, %s tests failed.’ % (len(testfiles), numfail))

Мы уже познакомились с инструментами, используемыми этим сценарием, выше в этой части книги - с модулем subprocess, с функциями os.path, glob, с файлами и другими. Этот пример в значительной степени просто объединяет эти инструменты для решения поставленной задачи. Основной операцией в сценарии является сравнение нового и старого вывода с целью обнаружить изменения («регрессии»). Попутно он также манипулирует аргументами командной строки, сообщениями об ошибках, кодами завершения и файлами.

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


Запускаем тестирование

Основная магия сценария, выполняющего тестирование, представленного в примере 6.9, заключена в используемой им структуре каталогов. При первом запуске в каталоге тестирования (или если вы заставляете его начать сначала, передавая ему второй аргумент командной строки) сценарий:

• Составит список тестируемых сценариев в подкаталоге Scripts

• Извлечет ассоциированные с тестируемым сценарием входной файл и аргументы командной строки из подкаталогов Inputs и Args

• Сгенерирует начальные файлы для стандартного потока вывода stdout, которые обычно помещаются в подкаталог Outputs

• Сообщит о сценариях, в процессе тестирования которых либо появились сообщения об ошибках в потоке stderr, либо код завершения отличается от нуля

В случае любых ошибок, обнаруженных при тестировании сценария, сохраняется содержимое потока stderr с текстом сообщений об ошибках, а также полный вывод, сгенерированный до момента ошибки. Текст из стандартного потока ошибок сохраняется в файл в подкаталоге Errors. Содержимое стандартного вывода в случае обнаружения ошибок сохраняется в файл, имя которого имеет специальное расширение «.bad» в подкаталоге Outputs (сохранение в файле с нормальным именем в подкаталоге Outputs привело бы к ошибке тестирования после устранения ошибки в тестируемом сценарии!). Ниже приводится пример первого запуска:

C:\...\PP4E\System\Tester> python tester.py . 1 Start tester: Mon Feb 22 22:13:38 2010

in C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester

generating: .\Outputs\test-basic-args.out

generating: .\Outputs\test-basic-stdout.out

generating: .\Outputs\test-basic-streams.out

generating: .\Outputs\test-basic-this.out

ERROR status: test-errors-runtime.py 1

ERROR stream: test-errors-runtime.py .\Errors\test-errors-runtime.err ERROR status: test-errors-syntax.py 1

ERROR stream: test-errors-syntax.py .\Errors\test-errors-syntax.err ERROR status: test-status-bad.py 42 generating: .\Outputs\test-status-good.out Finished: Mon Feb 22 22:13:41 2010 8 tests were run, 3 tests failed.

При запуске каждого сценария тестирующий сценарий устанавливает все заданные аргументы командной строки, передает все входные данные (если таковые имеются) в канал ввода и перехватывает стандартные потоки вывода и ошибок, сохраняет код завершения. Когда я запускал этот пример, в каталоге находилось 8 тестируемых сценариев, а также множество входных и выходных файлов. Поскольку схема именования каталогов и файлов имеет ключевое значение для данного примера, ниже приводится список с содержимым тестового каталога для этого сеанса - основным является каталог Scripts, потому что в нем собраны тестируемые сценарии:

C:\...\PP4E\System\Tester> dir /B

Args

Errors

Inputs

Outputs

Scripts

tester.py

xxold

C:\...\PP4E\System\Tester> dir /B Scripts

test-basic-args.py

test-basic-stdout.py

test-basic-streams.py

test-basic-this.py

test-errors-runtime.py

test-errors-syntax.py

test-status-bad.py

test-status-good.py

Другие подкаталоги содержат все необходимые входные данные и выходные файлы с результатами работы тестируемых сценариев:

C:\...\PP4E\System\Tester> dir /B Args

test-basic-args.args

test-status-good.args

C:\...\PP4E\System\Tester> dir /B Inputs

test-basic-args.in

test-basic-streams.in

C:\...\PP4E\System\Tester> dir /B Outputs

test-basic-args.out

test-basic-stdout.out

test-basic-streams.out

test-basic-this.out

test-errors-runtime.out.bad

test-errors-syntax.out.bad

test-status-bad.out.bad

test-status-good.out

C:\...\PP4E\System\Tester> dir /B Errors

test-errors-runtime.err

test-errors-syntax.err

Я не буду приводить здесь содержимое всех файлов (как видите, их достаточно много и все они входят в состав пакета с примерами для данной книги), но, чтобы вы могли получить некоторое представление, ниже приводится содержимое файлов, ассоциированных с тестируемым сценарием test-basic-args.py:

C:\...\PP4E\System\Tester> type Scripts\test-basic-args.py

# аргументы, потоки import sys, os

print(os.getcwd()) # в Outputs

print(sys.path[0])

print(‘[argv]’)

for arg in sys.argv: # из Args

print(arg) # в Outputs

print(‘[interaction]’) # в Outputs

text = input(‘Enter text:’) # из Inputs

rept = sys.stdin.readline() # из Inputs

sys.stdout.write(text * int(rept)) # в Outputs

C:\...\PP4E\System\Tester> type Args\test-basic-args.args

-command -line --stuff

C:\...\PP4E\System\Tester> type Inputs\test-basic-args.in

Eggs

10

C:\...\PP4E\System\Tester> type Outputs\test-basic-args.out

C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester

C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester\Scripts

[argv]

.\Scripts\test-basic-args.py

-command

-line

--stuff

[interaction]

Enter text:EggsEggsEggsEggsEggsEggsEggsEggsEggsEggs

А еще ниже - два файла, связанные с одной из обнаруженных ошибок. Первый из них хранит содержимое потока stderr, а второй - содержимое потока stdout, сгенерированное до момента появления ошибки. Они предназначены для анализа человеком (или с помощью других инструментов) и автоматически будут удалены в следующем сеансе тестирования:

C:\...\PP4E\System\Tester> type Errors\test-errors-runtime.err

Traceback (most recent call last):

File “.\Scripts\test-errors-runtime.py”, line 3, in print(1 / 0)

ZeroDivisionError: int division or modulo by zero

C:\...\PP4E\System\Tester> type Outputs\test-errors-runtime.out.bad

starting

Если теперь запустить тестирование еще раз, не внося никаких изменений в тестируемые сценарии, тестирующий сценарий сравнит сохраненные ранее результаты с новыми и определит отсутствие регрессий. Об ошибках, которые определяются по коду завершения, и о наличии сообщений в потоке stderr будет сообщаться, как и прежде, но в других тестах не будет обнаружено никаких отклонений от сохраненных ранее результатов:

C:\...\PP4E\System\Tester> python tester.py Start tester: Mon Feb 22 22:26:41 2010

in C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester

passed: test-basic-args.py

passed: test-basic-stdout.py

passed: test-basic-streams.py

passed: test-basic-this.py

ERROR status: test-errors-runtime.py 1

ERROR stream: test-errors-runtime.py .\Errors\test-errors-runtime.err ERROR status: test-errors-syntax.py 1

ERROR stream: test-errors-syntax.py .\Errors\test-errors-syntax.err ERROR status: test-status-bad.py 42 passed: test-status-good.py Finished: Mon Feb 22 22:26:43 2010 8 tests were run, 3 tests failed.

Но когда я внес в один из тестируемых сценариев изменение, повлиявшее на его вывод (я изменил счетчик цикла, чтобы сценарий выводил меньше строк), тестирующий сценарий обнаружил регрессию и сообщил о ней - отличия между новым и старым выводом были восприняты как ошибка прохождения теста, и новый вывод был сохранен в подкаталоге Outputs в файле с расширением «.bad»:

C:\...\PP4E\System\Tester> python tester.py Start tester: Mon Feb 22 22:28:35 2010

in C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester passed: test-basic-args.py

FAILED output: test-basic-stdout.py .\Outputs\test-basic-stdout.out.bad

passed: test-basic-streams.py

passed: test-basic-this.py

ERROR status: test-errors-runtime.py 1

ERROR stream: test-errors-runtime.py .\Errors\test-errors-runtime.err ERROR status: test-errors-syntax.py 1

ERROR stream: test-errors-syntax.py .\Errors\test-errors-syntax.err ERROR status: test-status-bad.py 42 passed: test-status-good.py Finished: Mon Feb 22 22:28:38 2010 8 tests were run, 4 tests failed.

C:\...\PP4E\System\Tester> type Outputs\test-basic-stdout.out.bad

begin

Spam!

Spam!Spam!

Spam!Spam!Spam!

Spam!Spam!Spam!Spam!

end

И еще одно замечание по использованию: если переменную trace в этом сценарии установить в значение verbose, он будет выводить более подробные сообщения, которые помогут вам проследить за порядком выполнения программы (но, вероятно, чересчур подробные для практического применения):

C:\...\PP4E\System\Tester> tester.py Start tester: Mon Feb 22 22:34:51 2010

in C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester

C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Tester

.\Scripts\test-basic-args.py

.\Scripts\test-basic-stdout.py

.\Scripts\test-basic-streams.py

.\Scripts\test-basic-this.py

.\Scripts\test-errors-runtime.py

.\Scripts\test-errors-syntax.py

.\Scripts\test-status-bad.py

.\Scripts\test-status-good.py

C:\Python31\python.exe .\Scripts\test-basic-args.py -command -line --stuff b’Eggs\r\n10\r\n’

b’C:\\Users\\mark\\Stuff\\Books\\4E\\PP4E\\dev\\Examples\\PP4E\\System\\Tester\r \nC:\\Users\\mark\\Stuff\\Books\\4E\\PP4E\\dev\\Examples\\PP4E\\System\\Tester\\ Scripts\r\n[argv]\r\n.\\Scripts\\test-basic-args.py\r\n-command\r\n-line\r\n--st uff\r\n[interaction]\r\nEnter text:EggsEggsEggsEggsEggsEggsEggsEggsEggsEggs’ b’’

0

passed: test-basic-args.py ...множество строк удалено...

Изучите внимательнее реализацию тестирующего сценария, чтобы получить о нем более полное представление. Естественно, о тестировании как таковом можно было бы рассказать намного больше, чем позволяет пространство книги. Например, для тестирования сценариев необязательно запускать их в дочерних процессах и вполне можно обойтись импортированием модулей и тестированием с помощью обработчиков исключений в инструкциях try. Кроме того, наш тестирующий сценарий можно было бы расширять и совершенствовать в самых разных направлениях (некоторые предложения приводятся в строке документирования). Более того, в состав Python входят два фреймворка тестирования, doctest и unittest (он же PyUnit), которые предоставляют инструменты и структуры для создания регрессивных и модульных тестов: unittest

Объектно-ориентированный фреймворк, который позволяет определять совокупности тестовых данных, ожидаемые результаты и комплекты тестов. Подклассы предоставляют методы тестирования и используют унаследованные методы проверки для определения ожидаемых результатов.

doctest

Анализирует и выполняет тесты, представленные в виде листингов интерактивного сеанса в строках документирования внутри тестируемого модуля. В листингах определяются тестовые вызовы и ожидаемые результаты - фреймворк doctest по сути повторно выполняет интерактивный сеанс.

За дополнительной информацией об инструментах и способах тестирования, сторонних или входящих в состав Python, обращайтесь к руководству по библиотеке Python, на веб-сайт PyPI и к своим любимым поисковым системам.

Тем не менее наш сценарий прекрасно справляется с задачей автоматизации тестирования сценариев командной строки на языке Python, которые выполняются как независимые программы в стандартном контексте выполнения. Поскольку наш сценарий полностью независим от тестируемых сценариев, мы можем определять новые совокупности тестовых данных без необходимости изменять реализацию тестирующего сценария. А так как он написан на языке Python, его легко и быстро можно приспосабливать под новые потребности. Как мы увидим еще раз в следующем разделе, такая простота, обеспечиваемая языком Python, может оказаться решающим преимуществом для реальных задач.

Тест прошел неудачно?

Когда в главе 13 мы узнаем, как из сценариев на языке Python отправлять электронную почту, вы, возможно, захотите улучшить этот сценарий так, чтобы он автоматически отправлял письмо в случае неудачи регулярно выполняемого теста (например, с помощью планировщика заданий cron в Unix). Благодаря этому не нужно будет даже помнить о необходимости проверить результаты. Конечно, можно развивать его еще дальше.

В одной компании я работал над добавлением звуковых эффектов в компилятор тестовых сценариев: вы слышали аплодисменты, если в процессе тестирования не было обнаружено регрессий, и совсем другие звуки в противном случае. (Советы по воспроизведению звуков смотрите в конце данной главы, в файле playfile.py.)


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



Копирование деревьев каталогов

Мой пишущий привод компакт-дисков иногда делает странные вещи. Копии файлов с необычными именами могут оказаться совершенно испорченными на CD, несмотря на то, что остальные файлы оказываются целыми и невредимыми. При этом не обязательно идет насмарку вся работа - если в большой резервной копии на CD испорчено лишь несколько файлов, я всегда могу поштучно скопировать проблемные файлы на дискеты. К несчастью, копирование в Windows путем перетаскивания мышью нехорошо поступает с такими CD: копирование прерывается, как только будет обнаружен первый такой испорченный файл. Получить удается столько файлов, сколько было скопировано к моменту возникновения ошибки, и не больше того.

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

Возможно, в Windows существует какая-нибудь волшебная настройка, позволяющая справиться с этой особенностью, но я перестал ее разыскивать, когда понял, что проще написать сценарий копирования на языке Python. Сценарий cpall.py, представленный в примере 6.10, реализует один из возможных способов копирования. С его помощью я могу управлять действиями, которые выполняются при обнаружении проблемных файлов, например, пропустить файл с помощью обработчика исключения. Кроме того, этот инструмент работает и на других платформах с тем же интерфейсом и таким же результатом. По крайней мере, мне кажется, что потратить несколько минут и написать на языке Python переносимый и многократно используемый сценарий для решения некоторой задачи более выгодно, чем искать решения, работающие только на одной платформе (если они вообще есть).

Пример 6.10. PP4E\System\Filetools\epall.py

##############################################################################

Порядок использования: “python cpall.py dirFrom dirTo”.

Рекурсивно копирует дерево каталогов. Действует подобно команде Unix “cp -r dirFrom/* dirTo”, предполагая, что оба аргумента dirFrom и dirTo являются именами каталогов.

Был написан с целью обойти фатальные ошибки при копировании файлов перетаскиванием мышью в Windows (когда встреча первого же проблемного файла вызывает прекращение операции копирования) и обеспечить возможность реализации более специализированных операций копирования на языке Python. ##############################################################################

import os, sys maxfileload = 1000000 blksize = 1024 * 500

def copyfile(pathFrom, pathTo, maxfileload=maxfileload):

Копирует один файл из pathFrom в pathTo, байт в байт; использует двоичный режим для подавления операций кодирования/декодирования и преобразований символов конца строки

if os.path.getsize(pathFrom) <= maxfileload:

bytesFrom = open(pathFrom,’rb’).read() # маленький файл читать целиком open(pathTo, ‘wb’).write(bytesFrom) else:

fileFrom = open(pathFrom, ‘rb’) # большие файлы - по частям

fileTo = open(pathTo, ‘wb’) # режим b для обоих файлов

while True:

bytesFrom = fileFrom.read(blksize) # прочитать очередной блок if not bytesFrom: break # пустой после последнего блока

fileTo.write(bytesFrom)

def copytree(dirFrom, dirTo, verbose=0):

Копирует содержимое dirFrom и вложенных подкаталогов в dirTo, возвращает счетчики (files, dirs);

для представления имен каталогов, недекодируемых на других платформах, может потребоваться использовать переменные типа bytes; в Unix может потребоваться выполнять дополнительные проверки типов файлов, чтобы пропускать ссылки, файлы fifo и так далее.

fcount = dcount = 0

for filename in os.listdir(dirFrom): # для файлов/каталогов

pathFrom = os.path.join(dirFrom, filename) pathTo = os.path.join(dirTo, filename) # расширить оба пути if not os.path.isdir(pathFrom): # скопировать простые файлы

try:

if verbose > 1: print(‘copying’, pathFrom, ‘to’, pathTo) copyfile(pathFrom, pathTo)

fcount += 1 except:

print(‘Error copying’, pathFrom, ‘to’, pathTo, ‘--skipped’) print(sys.exc_info()[0], sys.exc_info()[1]) else:

if verbose: print(‘copying dir’, pathFrom, ‘to’, pathTo) try:

os.mkdir(pathTo) # создать новый подкаталог

below = copytree(pathFrom, pathTo) # спуск в подкаталоги fcount += below[0] # увеличить счетчики

dcount += below[1] # подкаталогов

dcount += 1 except:

print(‘Error creating’, pathTo, ‘--skipped’) print(sys.exc_info()[0], sys.exc_info()[1]) return (fcount, dcount)

def getargs():

Извлекает и проверяет аргументы с именами каталогов, по умолчанию возвращает None в случае ошибки

try:

dirFrom, dirTo = sys.argv[1:] except:

print(‘Usage error: cpall.py dirFrom dirTo’) else:

if not os.path.isdir(dirFrom):

print(‘Error: dirFrom is not a directory’) elif not os.path.exists(dirTo): os.mkdir(dirTo)

print(‘Note: dirTo was created’) return (dirFrom, dirTo) else:

print(‘Warning: dirTo already exists’) if hasattr(os.path, ‘samefile’):

same = os.path.samefile(dirFrom, dirTo) else:

same = os.path.abspath(dirFrom) == os.path.abspath(dirTo) if same:

print(‘Error: dirFrom same as dirTo’) else:

return (dirFrom, dirTo)

if __name__ == ‘__main__’: import time dirstuple = getargs() if dirstuple:

print(‘Copying...’)

start = time.clock()

fcount, dcount = copytree(*dirstuple)

print(‘Copied’, fcount, ‘files,’, dcount, ‘directories’, end=’ ‘) print(‘in’, time.clock() - start, ‘seconds’)

В этом сценарии реализована собственная логика рекурсивного обхода дерева каталогов, в ходе которого запоминаются пути каталогов источника и приемника. На каждом уровне она копирует простые файлы, создает каталоги в целевом пути и производит рекурсивный спуск в подкаталоги с расширением путей «из» и «в» на один уровень. Эту задачу можно запрограммировать и другими способами (например, в процессе обхода можно изменять текущий рабочий каталог с помощью функции os.chdir или использовать решение на основе функции os.walk, замещая пути «из» и «в» по мере их обхода), но на практике вполне достаточно использовать прием расширения имен каталогов при спуске.

Обратите внимание на повторно используемую в этом сценарии функцию copyfile - на тот случай, если потребуется копировать файлы размером в несколько гигабайтов, она, исходя из размера файла, решает, читать ли файл целиком или по частям (напомню, что при вызове без аргументов метода read файла он загружает весь файл в строку, находящуюся в памяти). Мы выбрали достаточно большие размеры для читаемых целиком файлов и для блоков, потому что чем больший объем мы будем читать за один присест, тем быстрее будет работать сценарий. Это решение гораздо эффективнее, чем могло бы показаться на первый взгляд, - строки, остающиеся в памяти после последней операции чтения, будут утилизироваться сборщиком мусора, и освободившаяся память будет повторно использована последующими операциями. Здесь мы снова используем двоичный режим доступа к файлам, чтобы подавить кодирование/декодирование содержимого файлов и преобразование символов конца строки - в дереве каталогов могут находиться файлы самых разных типов.

Заметьте также, что сценарий при необходимости создает целевой каталог, и перед началом копирования предполагает, что он пуст, - удалите целевой каталог перед копированием нового дерева с тем же именем, иначе к дереву результата могут присоединиться старые файлы (мы могли бы автоматически очищать целевой каталог перед копированием, но это не всегда бывает желательно). Кроме того, данный сценарий пытается определить - не являются ли исходный и конечный каталоги одним и тем же каталогом. В Unix-подобных системах, где есть такие диковины, как ссылки, функция os.path.samefile проделывает более сложную работу, чем простое сравнение абсолютных имен файлов (разные имена файлов могут означать один и тот же файл).

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

txt), и при необходимости выполнить команду оболочки rm -r или rmdir /S (или аналогичную для соответствующей платформы), чтобы сначала удалить целевой каталог:

C:\...\PP4E\System\Filetools> rmdir /S copytemp

copytemp, Are you sure (Y/N)? y

C:\...\PP4E\System\Filetools> cpall.py C:\temp\PP3E\Examples copytemp

Note: dirTo was created

Copying...

Copied 1430 files, 185 directories in 10.4470980971 seconds

C:\...\PP4E\System\Filetools> fc /B copytemp\PP3E\Launcher.py

C:\temp\PP3E\Examples\PP3E\Launcher.py

Comparing files COPYTEMP\PP3E\Launcher.py and C:\TEMP\PP3E\EXAMPLES\PP3E\

LAUNCHER.PY

FC: no differences encountered

Вы можете воспользоваться аргументом verbose функции копирования, чтобы проследить, как протекает процесс копирования. Когда я работал над этим изданием в 2010 году, в этом примере за 10 секунд было скопировано дерево каталогов, содержащее 1430 файлов и 185 подкаталогов, - на моем удручающе медлительном нетбуке (для получения системного времени была использована встроенная функция time.clock). У вас аналогичная операция может выполняться быстрее или медленнее. Во всяком случае, это не хуже, чем самые лучшие результаты хронометража, полученные мной при перетаскивании каталогов мышью на этом же компьютере.

Каким же образом этот сценарий справляется с проблемными файлами на CD с резервными копиями? Секрет заключается в том, что он перехватывает и игнорирует исключения и продолжает обход. Чтобы скопировать с CD все хорошие файлы, я просто выполняю команду такого вида:

C:\...\PP4E\System\Filetools> python cpall.py G:\Examples C:\PP3E\Examples

Поскольку на моем компьютере, работающем под управлением Windows, привод CD доступен как диск «G:», эта команда эквивалентна копированию путем перетаскивания элемента, находящегося в папке верхнего уровня на компакт-диске, за исключением того, что сценарий Python восстанавливается после возникающих ошибок и копирует остальные файлы. В случае ошибки копирования он выводит сообщение в стандартный поток вывода и продолжает работу. При копировании большого количества файлов, вероятно, будет удобнее перенаправить стандартный вывод сценария в файл, чтобы позднее его можно было детально исследовать.

Вообще говоря, сценарию cpall можно передать любой абсолютный путь к каталогу на вашем компьютере, даже такой, который обозначает устройство, например привод CD. Для выполнения сценария в Linux можно обратиться к приводу CD, указав такой каталог, как /dev/edrom. После копирования дерева каталогов таким способом у вас может появиться желание проверить получившийся результат. Чтобы увидеть, как это делается, перейдем к следующему примеру.


Сравнение деревьев каталогов

Инженеры могут иметь параноидальные наклонности (но я вам этого не говорил). По крайней мере, ко мне это относится. Я полагаю, что все это от многолетнего наблюдения за встречающимися ошибками. Например, когда я создаю CD с резервной копией жесткого диска, то ощущаю, что за этим процессом стоит нечто слишком таинственное, чтобы я мог положиться на то, что делает программа записи CD. Может быть, я неправ, но я не могу безоговорочно доверять инструментам, которые временами калечат файлы и, кажется, выбрали третий четверг каждого месяца, чтобы вызывать крах моего компьютера с Windows. В общем, в решающий момент неплохо было бы иметь возможность как можно скорее убедиться, что скопированные на CD данные совпадают с исходными, или хотя бы найти отклонения. Если резервная копия вообще нужна, то такая возможность когда-нибудь действительно понадобится.

Поскольку доступ к компакт-дискам с данными осуществляется как к обычным деревьям каталогов, мы снова попадаем в сферу действия инструментов обхода деревьев - для проверки CD с резервной копией достаточно просто выполнить обход его каталога верхнего уровня. Если написать достаточно универсальный сценарий, его также можно будет использовать для проверки других операций копирования, например загруженных tar-файлов, резервных копий жестких дисков и так далее. Фактически, объединив сценарий cpall из предыдущего раздела с универсальным инструментом сравнения деревьев, мы получили бы переносимый и легко настраиваемый способ копирования и проверки наборов данных.

Мы уже познакомились с некоторыми механизмами обхода деревьев каталогов, но мы не сможем здесь воспользоваться ими непосредственно: нам необходимо параллельно обойти два каталога и попутно исследовать встречающиеся файлы. Кроме того, обход только одного из двух каталогов не позволит выявить файлы и каталоги, существующие только в другом каталоге. Здесь требуется некоторое более специализированное и рекурсивное решение.


Поиск расхождений между каталогами

Прежде чем приступать к программированию, необходимо выяснить, что значит сравнить два дерева каталогов. Если оба дерева имеют одинаковую структуру ветвей и глубину, проблема сводится к сравнению соответствующих файлов в каждом дереве. Однако в общем случае деревья могут иметь произвольную различную форму, глубину и так далее.

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

Иными словами, алгоритм сравнения деревьев должен также попутно выполнять сравнение каталогов. Начнем с реализации сравнения имен файлов для одного каталога, представленной в примере 6.11, так как это вложенная и более простая операция.

Пример 6.11. PP4E\System\Filetools\dirdiff.py

##############################################################################

Порядок использования: python dirdiff.py dir1-path dir2-path Сравнивает два каталога, пытаясь отыскать файлы, присутствующие в одном и отсутствующие в другом.

Эта версия использует функцию os.listdir и выполняет поиск различий между двумя списками. Обратите внимание, что сценарий проверяет только имена файлов, но не их содержимое, - версию, которая сравнивает результаты вызова методов .readQ^bi найдете в сценарии diffall.py.

##############################################################################

import os, sys

def reportdiffs(unique1, unique2, dir1, dir2):

Генерирует отчет о различиях для одного каталога: часть вывода функции comparedirs

if not (unique1 or unique2):

print(‘Directory lists are identical’) else:

if unique1:

print(‘Files unique to’, dir1) for file in unique1: print(’...’, file) if unique2:

print(‘Files unique to’, dir2) for file in unique2: print(‘...’, file)

def difference(seq1, seq2):

Возвращает элементы, присутствующие только в seq1;

Операция set(seq1) - set(seq2) даст аналогичный результат, но множества являются неупорядоченными коллекциями, поэтому порядок следования элементов в каталоге будет утерян

return [item for item in seq1 if item not in seq2]

def comparedirs(dir1, dir2, files1=None, files2=None):

Сравнивает содержимое каталогов, но не сравнивает содержимое файлов; функции listdir может потребоваться передавать аргумент типа bytes, если могут встречаться имена файлов, недекодируемые на других платформах

print(‘Comparing’, dir1, ‘to’, dir2)

files1 = os.listdir(dir1) if files1 is None else files1

files2 = os.listdir(dir2) if files2 is None else files2

unique1 = difference(files1, files2)

unique2 = difference(files2, files1)

reportdiffs(unique1, unique2, dir1, dir2)

return not (unique1 or unique2) # true, если нет различий

def getargs():

“Аргументы при работе в режиме командной строки” try:

dir1, dir2 = sys.argv[1:] # 2 аргумента командной строки

except:

print(‘Usage: dirdiff.py dir1 dir2’) sys.exit(1) else:

return (dir1, dir2)

if __name__ == ‘__main__’: dir1, dir2 = getargs() comparedirs(dir1, dir2)

Получив списки имен для каждого каталога, этот сценарий просто выбирает уникальные имена в первом каталоге, уникальные имена во втором каталоге и сообщает о найденных уникальных именах как о расхождениях (то есть о файлах, имеющихся в одном каталоге, но отсутствующих в другом). Функция comparedirs возвращает значение True, если расхождения не были обнаружены, что полезно для обнаружения различий при вызове из других программ.

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

C:\...\PP4E\System\Filetools> dirdiff.py C:\temp\PP3E\Examples copytemp

Comparing C:\temp\PP3E\Examples to copytemp Directory lists are identical

C:\...\PP4E\System\Filetools> dirdiff.py C:\temp\PP3E\Examples\PP3E\System ..

Comparing C:\temp\PP3E\Examples\PP3E\System to ..

Files unique to C:\temp\PP3E\Examples\PP3E\System

... App

... Exits

... Media

... moreplus.py

Files unique to ..

... more.pyc ... spam.txt ... Tester ... __init__.pyc

В основе сценария лежит функция difference: она реализует простую операцию сравнения списков. Применительно к каталогам, уникальные элементы представляют различия между деревьями, а общие элементы представляют имена файлов или подкаталогов, которые заслуживают дальнейшего сравнения или обхода. В Python 2.4 и более поздних версиях можно было бы использовать встроенные объекты типа set, если порядок следования имен в результатах не имеет значения - множества не являются последовательностями, поэтому они не сохраняют оригинальный порядок следования элементов в списках, полученных с помощью функции os.listdir. По этой причине (и чтобы не вынуждать пользователей модернизировать сценарий) вместо множеств мы будем использовать функцию, опирающуюся на использование выражения-генератора.


Поиск различий между деревьями

Мы только что реализовали инструмент, отбирающий уникальные имена файлов и каталогов. Теперь нам осталось реализовать инструмент обхода дерева, который будет применять функции из модуля dir-diff на каждом уровне, чтобы отобрать уникальные файлы и каталоги; явно сравнит содержимое общих файлов и обойдет общие каталоги. Эти операции осуществляет сценарий из примера 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, - не самая большая победа, но на моем медлительном нетбуке, который я использовал во время работы над этим изданием, каждый лишний цикл на счету.


Запускаем сценарий

Так как мы уже изучали инструменты обхода деревьев, задействованные в этом сценарии, перейдем прямо к некоторым примерам его использования. При обработке идентичных деревьев во время обхода выводятся сообщения о состоянии, а в конце появляется сообщение: «No diffs found» (Расхождений не обнаружено):

C:\...\PP4E\System\Filetools> diffall.py C:\temp\PP3E\Examples

copytemp > diffs.txt

C:\...\PP4E\System\Filetools> type diffs.txt | more

Comparing C:\temp\PP3E\Examples to copytemp Directory lists are identical Comparing contents README-root.txt matches

Comparing C:\temp\PP3E\Examples\PP3E to copytemp\PP3E

Directory lists are identical

Comparing contents

echoEnvironment.pyw matches

LaunchBrowser.pyw matches

Launcher.py matches

Launcher.pyc matches

...более 2000 строк опущено...

Comparing C:\temp\PP3E\Examples\PP3E\TempParts to copytemp\PP3E\TempParts

Directory lists are identical

Comparing contents

109_0237.JPG matches

lawnlake1-jan-03.jpg matches

part-001.txt matches

part-002.html matches

No diffs found.

При использовании этого сценария я обычно устанавливаю флаг verbose в значение True и перенаправляю вывод в файл (для больших деревьев выводится слишком много информации, которую трудно воспринимать в процессе выполнения сценария). Чтобы ограничить количество сообщений, устанавливайте флаг verbose в значение False. Чтобы посмотреть, как выглядит отчет о расхождениях, нужно их создать. Для простоты я вручную изменил несколько файлов в одном из деревьев, но вы можете воспользоваться сценарием глобального поиска и замены, представленным выше в этой главе. Заодно удалим несколько файлов, чтобы в процессе поиска можно было обнаружить уникальные элементы. Последние две команды удаления из приведенных ниже воздействуют на один и тот же каталог в разных деревьях:

C:\...\PP4E\System\Filetools> notepad copytemp\PP3E\README-PP3E.txt C:\...\PP4E\System\Filetools> notepad copytemp\PP3E\System\Filetools\commands.py C:\...\PP4E\System\Filetools> notepad C:\temp\PP3E\Examples\PP3E\__init__.py

C:\...\PP4E\System\Filetools> del copytemp\PP3E\System\Filetools\cpall_visitor.py C:\...\PP4E\System\Filetools> del copytemp\PP3E\Launcher.py C:\...\PP4E\System\Filetools> del C:\temp\PP3E\Examples\PP3E\PyGadgets.py

Теперь перезапустим сценарий сравнения, чтобы обнаружить различия, и перенаправим вывод в файл, чтобы облегчить просмотр результатов. Ниже приведена лишь часть выходного отчета, в которой сообщается о различиях. При обычном использовании я сначала смотрю на сводку в конце отчета, а затем ищу в тексте отчета строки «DIFF» и «unique», если мне нужна дополнительная информация об отличиях, указанных в сводке, - конечно, этот интерфейс можно было бы сделать более дружественным, но мне вполне хватает и этого:

C:\...\PP4E\System\Filetools> diffall.py C:\temp\PP3E\Examples

copytemp > diff2.txt

C:\...\PP4E\System\Filetools> notepad diff2.txt

Comparing C:\temp\PP3E\Examples to copytemp Directory lists are identical Comparing contents README-root.txt matches

Comparing C:\temp\PP3E\Examples\PP3E to copytemp\PP3E

Files unique to C:\temp\PP3E\Examples\PP3E

... Launcher.py

Files unique to copytemp\PP3E

... PyGadgets.py

Comparing contents

echoEnvironment.pyw matches

LaunchBrowser.pyw matches

Launcher.pyc matches

...множество строк опущено...

PyGadgets_bar.pyw matches README-PP3E.txt DIFFERS todos.py matches tounix.py matches

__init__.py DIFFERS

__init__.pyc matches

Comparing C:\temp\PP3E\Examples\PP3E\System\Filetools to copytemp\PP3E\System\ Fil...

Files unique to C:\temp\PP3E\Examples\PP3E\System\Filetools

... cpall_visitor.py

Comparing contents

commands.py DIFFERS

cpall.py matches

...множество строк опущено...

Comparing C:\temp\PP3E\Examples\PP3E\TempParts to copytemp\PP3E\TempParts

Directory lists are identical

Comparing contents

109_0237.JPG matches

lawnlake1-jan-03.jpg matches

part-001.txt matches

part-002.html matches

Diffs found: 5

- unique files at C:\temp\PP3E\Examples\PP3E - copytemp\PP3E

- files differ at C:\temp\PP3E\Examples\PP3E\README-PP3E.txt -

copytemp\PP3E\README-PP3E.txt

- files differ at C:\temp\PP3E\Examples\PP3E\__init__.py -

copytemp\PP3E\__init__.py

- unique files at C:\temp\PP3E\Examples\PP3E\System\Filetools -

copytemp\PP3E\System\Filetools

- files differ at C:\temp\PP3E\Examples\PP3E\System\Filetools\commands.py -

copytemp\PP3E\System\Filetools\commands.py

Я добавил разрывы строк и отступы кое-где, чтобы уместить листинг по ширине страницы, но отчет легко понять. В дереве, насчитывающем 1430 файлов и 185 каталогов, было найдено пять различий - три файла были изменены мною вручную, а два каталога мы рассогласовали тремя командами удаления.


Проверка резервных копий

Каким же образом этот сценарий способен унять паранойю при создании резервных копий на CD? Для дублирующей проверки работы моего пишущего привода CD я выполняю команду, как показано ниже. С помощью такой команды я могу также найти изменения, произведенные после предыдущего резервного копирования. И снова, поскольку на моем компьютере привод CD представляется как «G:», я указываю путь с таким корнем. В Linux используйте корень вида /dev/cdrom или /mnt/ cdrom:

C:\...\PP4E\System\Filetools> python diffall.py Examples

g:\PP3E\Examples > diff0226

C:\...\PP4E\System\Filetools> more diff0226

...вывод опущен...

Компакт-диск крутится, сценарий сравнивает, и в конце отчета появляется информация о различиях. Пример полного отчета о различиях находится в файле diff*.txt в пакете с примерами для этой книги. А чтобы быть действительно уверенным, я выполняю следующую команду глобального сравнения - чтобы проверить все дерево с резервной копией книги на флешке (которая, с точки зрения файловой системы, ничем не отличается от CD):

C:\...\PP4E\System\Filetools> diffall.py F:\writing-backups\feb-26-10\dev

C:\Users\mark\Stuff\Books\4E\PP4E\dev > diff3.txt

C:\...\PP4E\System\Filetools> more diff3.txt

Comparing F:\writing-backups\feb-26-10\dev to C:\Users\mark\Stuff\Books\4E\PP4E\ dev

Directory lists are identical

Comparing contents

ch00.doc DIFFERS

ch01.doc matches

ch02.doc DIFFERS

ch03.doc matches

ch04.doc DIFFERS

ch05.doc matches ch06.doc DIFFERS

...множество строк опущено...

Comparing F:\writing-backups\feb-26-10\dev\Examples\PP4E\System\Filetools to C:\...

Files unique to C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\

Filetools

... copytemp

... cpall.py

... diff2.txt

... diff3.txt

... diffall.py

... diffs.txt

... dirdiff.py

... dirdiff.pyc

Comparing contents

bigext-tree.py matches

bigpy-dir.py matches

...множество строк опущено...

Diffs found: 7

- files differ at F:\writing-backups\feb-26-10\dev\ch00.doc -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\ch00.doc

- files differ at F:\writing-backups\feb-26-10\dev\ch02.doc -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\ch02.doc

- files differ at F:\writing-backups\feb-26-10\dev\ch04.doc -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\ch04.doc

- files differ at F:\writing-backups\feb-26-10\dev\ch06.doc -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\ch06.doc

- files differ at F:\writing-backups\feb-26-10\dev\TOC.txt -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\TOC.txt

- unique files at F:\writing-backups\feb-26-10\dev\Examples\PP4E\System\Filetools -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\System\Filetools

- files differ at F:\writing-backups\feb-26-10\dev\Examples\PP4E\Tools\visitor.py -

C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Tools\visitor.py

Данный пример показывает, что после создания предыдущей резервной копии я добавил несколько примеров и изменил несколько файлов с текстом глав. Если выполнить эту команду сразу после создания резервной копии, то радаром diffall не будет обнаружено никаких изменений, за исключением файлов, которые вообще не могут быть скопированы. Такое глобальное сравнение может занять несколько минут. Эта операция выполняет побайтовое сравнение всех файлов глав и снимков с экрана, дерева примеров и других, но это точное и полное сравнение. Дерево каталогов этой книги содержит большое количество файлов, и какая-либо менее автоматизированная процедура проверки без помощи Python была бы совершенно немыслима.

После того как этот сценарий был написан, я начал использовать его для проверки резервных копий моих ноутбуков на внешнем жестком диске, создаваемых автоматически. Для этого я запускаю сценарий cpall, написанный нами в предыдущем разделе этой главы, а затем, чтобы проверить результаты и получить список файлов, вызвавших проблемы при копировании, - сценарий сравнения, разработанный здесь. Когда я выполнял эту процедуру в последний раз, было скопировано и проверено 225 000 файлов и 15 000 каталогов, занимающих 20 Гбайт дискового пространства, - это явно не та задача, которую можно выполнить вручную!

Ниже приводятся магические заклинания, которые я вводил на моем ноутбуке с системой Windows. Здесь f:\ - это раздел на внешнем жестком диске, и вас не должно удивлять, что каждая из этих команд выполняется около получаса или даже больше на распространенном аппаратном обеспечении. Копирование, инициированное операцией перетаскивания мышью, выполняется ничуть не быстрее (если вообще выполняется!):

C:\...\System\Filetools> cpall.py c:\ f:\ > f:\copy-log.txt

C:\...\System\Filetools> diffall.py f:\ c:\ > f:\diff-log.txt


Отчет о различиях и другие идеи

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

Когда мне нужны дополнительные сведения о фактических различиях в двух несовпавших файлах, я либо открываю их в редакторе, либо выполняю команду сравнения файлов на соответствующей платформе (например, fc в Windows/DOS, diff или cmp в Unix и Linux). Этот последний шаг не является переносимым решением, но для стоявших передо мною задач просто нахождение различий в дереве из 1400 файлов было значительно более важным, чем сообщение в отчете о том, в каких строках различаются эти файлы.

Конечно, поскольку в Python всегда можно вызвать команды оболочки, этот последний шаг можно автоматизировать, порождая при обнаружении различий команду diff или fc с помощью os.popen (или делать это после обхода, сканируя содержащуюся в отчете сводку). Вывод этих системных вызовов можно было бы поместить в отчет в первоначальном виде или оставить только наиболее важные его части.

Мы могли бы также открывать текстовые файлы в текстовом режиме, чтобы игнорировать различия, вызванные разными комбинациями символов завершения строк при передаче файлов между платформами, но не всегда ясно - действительно ли такие отличия должны игнорироваться (что если пользователь пожелает узнать, не изменились ли символы конца строки?). Например, после загрузки файла с веб-сайта с помощью сценария FTP, с которым мы встретимся в главе 13, сценарий diffall обнаруживает несоответствие между локальной копией файла и оригиналом на удаленном сервере. Продолжая исследования, я просто выполнил несколько инструкций в интерактивном сеансе Python:

>>> a = open('lp2e-updates.html', 'rb').read()

>>> b = open(r'C:\Mark\WEBSITE\public_html\lp2e-updates.html', 'rb').read()

>>> a == b

False

Эта проверка показывает, что двоичное содержимое локальной версии файла отличается от содержимого удаленной версии. Чтобы выяснить, обусловлено ли это различием способов завершения строк в Unix и DOS, я попробовал выполнить то же самое, но в текстовом режиме, чтобы перед сравнением символы окончания строк были приведены к стандартному символу \n:

>>> a = open('lp2e-updates.html', 'r').read()

>>> b = open(r'C:\Mark\WEBSITE\public_html\lp2e-updates.html', 'r').read()

>>> a == b

True

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

>>> a = open('lp2e-updates.html', 'rb').read()

>>> b = open(r'C:\Mark\WEBSITE\public_html\lp2e-updates.html', 'rb').read()

>>> for (i, (ac, bc)) in enumerate(zip(a, b)):

... if ac != bc:

... print(i, repr(ac), repr(bc))

... break

37966 ‘ \r’ ‘ \n’

Этот результат означает, что в загруженном файле в байте со смещением 37 966 находится символ \r, а в локальной копии - символ \n. Эта строка в одном файле оканчивается комбинацией символов завершения строки в DOS, а в другом - символом завершения строки в Unix. Чтобы увидеть больше, можно вывести текст, окружающий несовпадение:

>>> for (i, (ac, bc)) in enumerate(zip(a, b)):

... if ac != bc:

... print(i, repr(ac), repr(bc))

... print(repr(a[i-20:i+20]))

... print(repr(b[i-20:i+20]))

... break

37966 ‘ \r’ ‘ \n’

‘ re>\r\ndef min(*args):\r\n tmp = list(arg'

‘ re>\r\ndef min(*args):\n tmp = list(args’

По всей видимости, я вставил символ завершения строки Unix в одном месте в локальной копии, там, где в загруженной версии находится комбинация символов завершения строки в DOS, - результат использования текстового режима в сценарии загрузки (который преобразует символы \n в комбинации \r\n) и многих лет использования ноутбуков и PDA, работающих под управлением Linux и Windows (вероятно, я внес это изменение, когда после редактирования этого файла в Linux я скопировал его в Windows в двоичном режиме). Такой программный код, как показано выше, можно было бы добавить в сценарий diffall, чтобы обеспечить более интеллектуальное сравнение текстовых файлов и вывод более подробной информации об отличиях в них.

Поскольку Python отлично подходит для обработки строк и файлов, можно пойти еще дальше и реализовать на языке Python сценарий, эквивалентный командам fc и diff. Фактически большая часть работы в этом направлении уже выполнена - эту задачу можно было бы существенно упростить, задействовав модуль difflib из стандартной библиотеки. Более подробные сведения о нем и примеры использования вы найдете в руководстве по библиотеке Python.

Можно было бы поступить еще умнее и не выполнять загрузку и сравнение файлов, отличающихся размерами; выполнять чтение файлов более мелкими порциями, чтобы уменьшить потребление памяти. Для большинства деревьев каталогов такие оптимизации излишни -чтение многомегабайтных файлов в строки в Python осуществляется очень быстро, а память постоянно освобождается сборщиком мусора в процессе работы.

Поскольку эти улучшения выходят за рамки задач данного сценария и размеров этой главы, его предоставляется выполнить любознательному читателю. (В этой книге официально отсутствуют упражнения для самостоятельного решения, но данное предложение выглядит именно так, не правда ли?) А теперь перейдем к исследованию способов реализации еще одной операции, часто применяемой к деревьям каталогов: поиск.


Поиск в деревьях каталогов

Инженеры любят что-нибудь изменять. В процессе написания этой книги я испытывал почти непреодолимое желание перемещать и переименовывать каталоги, переменные и совместно используемые модули в дереве примеров книги, как только мне начинало казаться, что я набрел на более подходящую структуру. На достаточно раннем этапе все было приемлемо, но по мере того как дерево становилось все более запутанным, сопровождение его превратилось в кошмар. Пути к каталогам программ и имена модулей были разбросаны по всему программному коду - в операциях импорта пакетов, вызовах программ, комментариях, файлах конфигурации и так далее.

Можно, конечно, исправлять все эти ссылки, вручную редактируя файлы в каталоге и отыскивая в них ту информацию, которая изменилась. Однако это настолько объемная работа, что для дерева примеров данной книги она практически неосуществима. Дерево примеров к предыдущему изданию содержит 186 каталогов и 1429 файлов! Очевидно, что мне был необходим инструмент, автоматизирующий обновление после производимых изменений. Существуют различные способы решения этой задачи - от команд оболочки до операций поиска, функций обхода деревьев и универсальных фреймворков. В этом и в следующем разделах мы исследуем каждый способ в отдельности, как это сделал я в поисках наиболее оптимального решения этой проблемы.


grep, glob и find

Если вы работаете в Unix-подобной системе, то наверняка знаете о существовании стандартного способа поиска строк в файлах в таких системах. Программа командной строки grep и родственные ей позволяют получить перечень всех строк в одном или нескольких файлах, содержащих строку или шаблон строки.29 Учитывая, что командные оболочки Unix автоматически расширяют (то есть «глобализуют») шаблоны имен файлов, такая команда, как приведена ниже, будет искать строку, указанную в командной строке, в файлах Python, расположенных в одном каталоге (в этом примере используется команда grep, входящая в состав облочки Cygwin для Windows, о которой я рассказывал в предыдущей главе):

C:\...\PP4E\System\Filetools> c:\cygwin\bin\grep.exe walk *.py

bigext-tree.py:for (thisDir, subsHere, filesHere) in os.walk(dirname): bigpy-path.py: for (thisDir, subsHere, filesHere) in os.walk(srcdir):

bigpy-tree.py:for (thisDir, subsHere, filesHere) in os.walk(dirname):

Как мы уже знаем, те же действия можно запрограммировать в сценарии на языке Python, организовав запуск такой команды с помощью os.system или os.popen. А в случае реализации операции поиска вручную, мы могли бы добиться похожих результатов с помощью модуля glob, с которым познакомились в главе 4, - он, подобно командной оболочке, расширяет шаблоны имен файлов в списки строк соответствующих имен файлов:

C:\...\PP4E\System\Filetools> python >>> import os

>>> for line in os.popen(r'c:\cygwin\bin\grep.exe walk *.py'):

... print(line, end='')

bigext-tree.py:for (thisDir, subsHere, filesHere) in os.walk(dirname): bigpy-path.py: for (thisDir, subsHere, filesHere) in os.walk(srcdir):

bigpy-tree.py:for (thisDir, subsHere, filesHere) in os.walk(dirname):

>>> from glob import glob

>>> for filename in glob('*.py'):

... if 'walk' in open(filename).read():

... print(filename)

bigext-tree.py

bigpy-path.py

bigpy-tree.py

К сожалению, область действия этих инструментов обычно ограничивается одним каталогом. Модуль glob способен выполнить обход нескольких каталогов при правильно сформированной строке шаблона, но он не является универсальным средством обхода деревьев каталогов, который требуется мне для обслуживания большого дерева каталогов с примерами. В Unix-подобных системах команда find оболочки предоставляет расширенные возможности для обхода всего дерева каталогов. Например, следующая команда Unix точно определила бы файлы и строки в текущем каталоге и ниже, где встречается строка popen:

find . -name “*.py” -print -exec fgrep popen {} \;

Если у вас имеется Unix-подобная команда find на всех компьютерах, которыми вы пользуетесь, можете считать, что у вас есть инструмент для обработки каталогов.


Создание собственного модуля find

Но если команда find доступна не на всех ваших компьютерах, не волнуйтесь - ее легко можно реализовать на языке Python. Ранее в стандартной библиотеке Python имелся модуль find, который я часто использовал. И хотя этот модуль был удален из библиотеки где-то между вторым и третьим изданиями этой книги, в стандартной библиотеке появилась функция os.walk, которая способна упростить создание собственного модуля find. Вместо того чтобы оплакивать исчезновение модуля, я решил потратить 10 минут и написать свой эквивалент.

В примере 6.13 представлена утилита find, реализованная на языке Python, которая выбирает все имена файлов в каталоге, соответствующие шаблону. В отличие от glob.glob, функция find.find автоматически выполняет поиск во всем дереве каталогов. А в отличие от структуры обхода os.walk, результаты find.find можно трактовать, как простую линейную группу строк.

Пример 6.13. PP4E\Tools\find.py

#!/usr/bin/python

############################################################################## Возвращает все имена файлов, соответствующие шаблону в дереве каталогов;

собственная версия модуля find, ныне исключенного из стандартной библиотеки: импортируется как “PP4E.Tools.find”; похож на оригинал, но использует цикл os.walk, не поддерживает возможность обрезания ветвей подкаталогов и может запускаться как самостоятельный сценарий;

find() - функция-генератор, использующая функцию-генератор os.walk(), возвращающая только имена файлов, соответствующие шаблону: чтобы получить весь список результатов сразу, используйте функцию findlist(); ##############################################################################

import fnmatch, os

def find(pattern, startdir=os.curdir):

for (thisDir, subsHere, filesHere) in os.walk(startdir): for name in subsHere + filesHere:

if fnmatch.fnmatch(name, pattern):

fullpath = os.path.join(thisDir, name) yield fullpath

def findlist(pattern, startdir=os.curdir, dosort=False): matches = list(find(pattern, startdir)) if dosort: matches.sort() return matches

if__name__== ‘__main__’:

import sys

namepattern, startdir = sys.argv[1], sys.argv[2] for name in find(namepattern, startdir): print(name)

Вроде бы немного делает этот программный код - по сути, он лишь несколько расширяет возможности функции os.walk, - но его функция find позволяет получить те же результаты, что давал ранее существовавший в стандартной библиотеке модуль find и одноименная утилита в Unix. Кроме того, этот модуль является более переносимым решением, и пользоваться им намного проще, чем повторять его программный код всякий раз, когда потребуется выполнить поиск. Поскольку этот файл можно использовать и как сценарий, и как библиотечный модуль, его можно применять как инструмент командной строки и вызывать из других программ.

Например, чтобы обработать все файлы с программным кодом на языке Python, находящиеся в дереве каталогов, с корнем на один уровень выше текущего рабочего каталога, достаточно просто запустить приведенную ниже команду в окне консоли. Запустите эту команду у себя, чтобы увидеть ее в действии, - в данном примере стандартный вывод сценария передается по конвейеру команде more, обеспечивающей возможность постраничного просмотра результатов, но его точно так же можно передать любой другой программе обработки, которая читает входные данные из стандартного потока ввода:

C:\...\PP4E\Tools> python find.py *.py .. | more

..\LaunchBrowser.py

..\Launcher.py

..\__init__.py

..\Preview\attachgui.py ..\Preview\customizegui.py ...множество строк опущено...

Чтобы получить еще больший контроль, выполните следующий программный код на языке Python в сценарии или в интерактивной оболочке. При таком подходе к найденным файлам можно применять любые операции, доступные в языке Python:

C:\...\PP4E\System\Filetools> python

>>> from PP4E.Tools import find # или просто import find, если

>>> for filename in find.find('*.py', '..’): # модуль находится в cwd ... if 'walk’ in open(filename).read():

... print(filename)

..\Launcher.py

..\System\Filetools\bigext-tree.py

..\System\Filetools\bigpy-path.py

..\System\Filetools\bigpy-tree.py

..\Tools\cleanpyc.py

..\Tools\find.py

..\Tools\visitor.py

Обратите внимание, что в данном случае отпала необходимость во вложенных циклах, необходимых при использовании функции os.walk, когда требуется получить список имен файлов, соответствующих шаблону, - во многих случаях такой подход концептуально выглядит проще. Заметьте также, что функция поиска является функцией-генератором, благодаря чему сценарию не приходится ждать, пока не будут выбраны все соответствующие имена файлов, - функция os.walk поставляет результаты для каждого каталога в отдельности, а функция find.find поставляет имена файлов, выбирая их из этих результатов.

Ниже приводится более сложный пример использования модуля find: следующая команда выводит все имена файлов с программным кодом на языке Python, находящиеся в дереве каталогов с корнем в C:\temp\ PP3E, начинающиеся с символа q или t. Обратите внимание, что find возвращает полные пути к файлам, начиная от указанного каталога:

C:\...\PP4E\Tools> find.py [qx]*.py C:\temp\PP3E

C:\temp\PP3E\Examples\PP3E\Database\SQLscripts\querydb.py

C:\temp\PP3E\Examples\PP3E\Gui\Tools\queuetest-gui-class.py

C:\temp\PP3E\Examples\PP3E\Gui\Tools\queuetest-gui.py

C:\temp\PP3E\Examples\PP3E\Gui\Tour\quitter.py

C:\temp\PP3E\Examples\PP3E\Internet\Other\Grail\Question.py

C:\temp\PP3E\Examples\PP3E\Internet\Other\XML\xmlrpc.py

C:\temp\PP3E\Examples\PP3E\System\Threads\queuetest.py

А ниже приводится программный код Python, который выполняет тот же поиск, но при этом выводит только имена файлов без путей к ним и размеры файлов:

C:\...\PP4E\Tools> python

>>> import os

>>> from find import find

>>> for name in find('[qx]*.py', r'C:\temp\PP3E'):

... print(os.path.basename(name), os.path.getsize(name))

querydb.py 635 queuetest-gui-class.py 1152 queuetest-gui.py 963 quitter.py 801 Question.py 817 xmlrpc.py 705 queuetest.py 1273

Модуль fnmatch

Чтобы добиться такой экономии программного кода, модуль find вызывает функцию os.walk для обхода дерева каталогов и просто возвращает соответствующие имена файлов в процессе обхода. Однако в нем содержится еще одна новинка - модуль fnmatch, входящий в состав стандартной библиотеки Python, который выполняет сопоставление имен файлов с шаблоном. Этот модуль поддерживает общие операторы в строках шаблонов: * соответствует любому количеству символов, ? соответствует одному любому символу, а [... ] и [!... ] соответствуют любым символам, перечисленным и отсутствующим в квадратных скобках, соответственно; другие символы соответствуют самим себе. В отличие от модуля re, модуль fnmatch поддерживает только самые общие операторы шаблонов командной оболочки Unix и не поддерживает полноценные регулярные выражения. Значение этого отличия мы увидим в главе 19.

Интересно отметить, что функция glob.glob тоже использует модуль fn-match для сопоставления имен: она объединяет os.listdir и fnmatch для сопоставления имен файлов в каталоге практически так же, как наша функция find.find объединяет os.walk и fnmatch для поиска совпадений в деревьях (хотя функция os.walk, в свою очередь, использует функцию os.listdir). Одно из следствий всего этого состоит в том, что имеется возможность передавать функции find.find имя начального каталога и шаблон в виде строк байтов, если необходимо подавить декодирование имен файлов, содержащих символы Юникода, как это возможно при использовании функций os.walk и glob.glob, - в результате вы будете получать имена файлов в виде строк байтов. Подробнее о символах Юникода в именах файлов рассказывается в главе 4.

Для сравнения, вызов find.find со строкой шаблона «*» является примерным эквивалентом команды оболочки, выводящей содержимое дерева каталогов, такой как dir /B /S в DOS и Windows. Поскольку шаблону «*» соответствуют все файлы, такой вызов вернет все имена файлов, присутствующих в дереве, за один проход. Подобные команды мы легко можем выполнять в сценариях на языке Python с помощью функции os.popen, поэтому следующий фрагмент выполняет ту же самую работу, но он изначально является непереносимым и приводит к запуску параллельной программы:

>>> import os

>>> for line in os.popen('dir /B /S’): print(line, end=’’)

>>> from PP4E.Tools.find import find

>>> for name in find(pattern=’*’, startdir=’.’): print(name)

Данная утилита еще будет демонстрироваться далее в этой главе и в книге, включая самые, пожалуй, убедительные демонстрации в следующем разделе и в диалоге Grep, в главе 11 - в реализации текстового редактора PyEdit с графическим интерфейсом, где она будет играть центральную роль в многопоточном внешнем инструменте поиска. Модуль find был исключен из стандартной библиотеки, но это не повод забывать о нем.

Если модулю fnmatch имя файла передается в виде строки байтов, то шаблон также должен иметь тип bytes (либо оба аргумента должны иметь тип str), потому что используемый им модуль re, реализующий сопоставление с регулярными выражениями, не позволяет смешивать типы испытуемой строки и шаблона. Это требование по наследству переходит и нашей функции find.find, принимающей имя каталога и шаблон. Подробнее о модуле re рассказывается в главе 19.

Любопытно отметить, что модуль fnmatch в Python 3.1 также преобразует строку шаблона типа bytes в строку str Юникода и обратно в ходе внутренней обработки текста, используя при этом кодировку Latin-1. Этого достаточно для большинства применений, но это может вступать в противоречие с некоторыми кодировками, которые неточно отображаются в кодировку Latin-1. В таких ситуациях параметр sys. getfilesystemencoding мог бы точнее соответствовать используемой кодировке, так как он отражает ограничения, накладываемые файловой системой (как мы узнали в главе 4, параметр sys.getdefaultencoding отражает кодировку содержимого файлов, а не их имен).

Когда функции os.walk передаются аргументы типа str, она предполагает, что имена файлов следуют соглашениям для данной платформы, и не игнорирует ошибки декодирования, возбуждаемые функцией os.listdir. В утилите «grep» в примере PyEdit в главе 11 эта картина еще больше омрачается тем фактом, что строку str шаблона, полученную из графического интерфейса, необходимо кодировать в строку bytes, используя кодировку, возможно, неподходящую для некоторых файлов. За дополнительными подробностями обращайтесь к описанию функций fnmatch.py и os.py в руководстве по стандартной библиотеке Python и к их исходному программному коду. Работа с Юникодом может оказаться очень тонким делом.


Удаление файлов с байт-кодом

Модуль find из предыдущего раздела - не самый универсальный инструмент поиска строк из тех, что мы увидим далее, но несомненно является первым важным шагом. Он отбирает файлы, по которым затем можно реализовать поиск в сценарии автоматизации. Фактически наличие одной только операции отбора файлов из дерева каталогов достаточно для решения разнообразных задач системного администрирования.

Например, одна из типичных задач, которую мне приходится выполнять регулярно, заключается в удалении всех файлов с байт-кодом в дереве каталогов. Поскольку эти файлы не всегда являются переносимыми между основными версиями Python, их желательно удалять при подготовке программы к передаче пользователям, чтобы дать возможность интерпретатору самому создать новые файлы при первой же попытке импортировать модули. Теперь, когда мы получили богатый опыт использования функции os.walk, мы могли бы отказаться от услуг посредников и использовать ее непосредственно. В примере 6.14 представлен переносимый и универсальный инструмент командной строки, принимающий аргументы, обрабатывающий исключения и поддерживающий режим трассировки и простого поиска без удаления.

Пример 6.14. PP4E\Tools\cleanpyc.py

удаляет все файлы .pyc с байт-кодом в дереве каталогов: аргумент командной строки, если он указан, интерпретируется как корневой каталог, в противном случае корневым считается текущий рабочий каталог

import os, sys findonly = False

rootdir = os.getcwd() if len(sys.argv) == 1 else sys.argv[1] found = removed = 0

for (thisDirLevel, subsHere, filesHere) in os.walk(rootdir): for filename in filesHere:

if filename.endswith(‘.pyc’):

fullname = os.path.join(thisDirLevel, filename) print(‘=>’, fullname) if not findonly: try:

os.remove(fullname) removed += 1 except:

type, inst = sys.exc_info()[:2] print(‘*’*4, ‘Failed:’, filename, type, inst) found += 1

print(‘Found’, found, ‘files, removed’, removed)

Если запустить этот сценарий, он выполнит обход дерева каталогов (CWD - по умолчанию, или дерева с корнем в каталоге, переданном в виде аргумента командной строки) и удалит все встретившиеся файлы с байт-кодом:

C:\...\Examples\PP4E> Tools\cleanpyc.py

=> C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\__init__.pyc => C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\initdata.pyc => C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\make_db_file.pyc => C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\manager.pyc => C:\Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\person.pyc ...множество строк опущено...

Found 24 files, removed 24

C:\...\PP4E\Tools> cleanpyc.py .

=> .\find.pyc => .\visitor.pyc

=> .\__init__.pyc

Found 3 files, removed 3

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

Пример 6.15. PP4E\Tools\cleanpyc-find-shell.py

отыскивает и удаляет все файлы “*.pyc” с байт-кодом в дереве каталогов, имя которого передается в виде аргумента командной строки; предполагает наличие непереносимой Unix-подобной команды find

import os, sys

rundir = sys.argv[1] if sys.platform[:3] == ‘win’:

findcmd = r’c:\cygwin\bin\find %s -name “*.pyc” -print’ % rundir else:

findcmd = ‘find %s -name “*.pyc” -print’ % rundir

print(findcmd) count = 0

for fileline in os.popen(findcmd): # обход всех строк результата,

count += 1 # завершающихся символом \n

print(fileline, end=’’) os.remove(fileline.rstrip())

print(‘Removed %d .pyc files’ % count)

Этот сценарий удалит все файлы, имена которых возвращает команда оболочки:

C:\...\PP4E\Tools> cleanpyc-find-shell.py .

c:\cygwin\bin\find . -name “*.pyc” -print

./find.pyc

./visitor.pyc

./__init__.pyc

Removed 3 .pyc files

В этом сценарии функция os.popen получает вывод программы find из оболочки Cygwin, установленной на одном из моих Windows-компьютеров, или от стандартной команды find, имеющейся в Linux. Он также абсолютно непереносим на компьютеры, работающие под управлением Windows, если на них не установлена Unix-подобная программа find, а ее нет ни на одном из моих личных компьютеров (не говоря уже о большинстве компьютеров в мире в целом). Кроме того, как мы уже знаем, запуск команд оболочки из сценариев наносит ущерб производительности, поскольку при этом приходится запускать новую независимую программу.

Мы можем значительно улучшить переносимость и производительность и при этом сохранить программный код простым, применив инструмент поиска, написанный нами на языке Python в предыдущем разделе. Новый сценарий приводится в примере 6.16.

Пример 6.16. PP4E\Tools\cleanpyc-find-py.py

отыскивает и удаляет все файлы “*.pyc” с байт-кодом в дереве каталогов, имя которого передается в виде аргумента командной строки;

использует утилиту find, написанную на языке Python, за счет чего обеспечивается переносимость;

запустите этот сценарий, чтобы удалить файлы .pyc, скомпилированные старой версией Python;

import os, sys, find # here, gets Tools.find count = 0

for filename in find.find(‘*.pyc’, sys.argv[1]): count += 1

print(filename)

os.remove(filename)

print(‘Removed %d .pyc files’ % count)

Как и прежде, этот сценарий удалит все файлы с байт-кодом в дереве каталогов с корнем, переданным в аргументе командной строки, но на этот раз наш сценарий может выполняться в любой системе, где установлен Python:

C:\...\PP4E\Tools> cleanpyc-find-py.py .

.\find.pyc

.\visitor.pyc

.\__init__.pyc

Removed 3 .pyc files

Этот сценарий переносим и позволяет избежать издержек, связанных с запуском внешних программ. Однако утилита find просто осуществляет поиск в дереве каталогов - она отбирает имена файлов, соответствующие шаблону, но не обращает внимания на их содержимое. Мы можем запрограммировать дополнительный поиск, используя результаты, полученные с помощью find, однако ручное управление позволит нам использовать процесс поиска более непосредственно. Как это можно сделать, демонстрируется в следующем разделе.

Сценарий Python для поиска в дереве

Наконец, после экспериментов с инструментами grep, glob и find для упрощения глобального поиска на всех платформах, которые могут мне когда-либо встретиться, я написал сценарий на языке Python, который выполняет основную работу вместо меня. В примере 6.17 применяются стандартные средства Python, с которыми мы познакомились в предыдущих главах: os.walk - для обхода файлов в каталоге, os.path. splitext - для пропуска файлов с расширениями, характерными для двоичных файлов, и os.path.join - для переносимого объединения путей к каталогам с именами файлов.

Поскольку он написан исключительно на языке Python, этот сценарий в равной степени может использоваться и в Linux, и в Windows. На самом деле он должен работать на любом компьютере, где установлен Python. Более того, благодаря непосредственному использованию системных вызовов он должен работать быстрее, чем при использовании приема запуска команды оболочки.

Пример 6.17. PP4E\Tools\search_all.py

############################################################################## Порядок использования: “python ...\Tools\search_all.py dir string”.

Отыскивает все файлы в указанном дереве каталогов, содержащие заданную строку; для предварительного отбора имен файлов использует интерфейс os.walk вместо

Загрузка...