Лень — двигатель прогресса. Имея в своем распоряжении переносимый сценарий search_all из примера 6.17, я мог точнее находить файлы, которые было необходимо отредактировать при изменении содержимого или структуры дерева примеров в книге. Первоначально я в одном окне запускал search_all, чтобы отобрать подозрительные файлы, и вручную редактировал каждый из них в другом окне.
Однако довольно скоро и это стало утомительным. Вводить вручную имена файлов в командах запуска редактора — занятие невеселое, особенно если нужно отредактировать много файлов. Поскольку у меня всегда найдется более интересное занятие, чем десятки раз запускать редактор вручную, я стал думать, как автоматически запускать редактор для каждого подозрительного файла.
К сожалению, сценарий search_all просто выводит полученные результаты на экран. Хотя этот текст можно перехватить и проанализировать с помощью другой программы, запускаемой функцией os.popen, проще может оказаться подход, когда редактор запускается прямо во время поиска, но для этого могут потребоваться большие изменения в реализации сценария. Здесь мне пришли в голову три мысли.
Избыточность
Написав несколько утилит обхода каталогов, я понял, что каждый раз я снова и снова пишу однотипный программный код. Обход можно упростить еще больше, если скрыть детали под оболочкой и тем самым упростить повторное использование решения. Инструмент os.walk позволяет избежать необходимости писать рекурсивные функции, но при его использовании выполняются лишние действия (например, присоединение имен каталогов, вывод трассировочной информации).
Расширяемость
Исходя из прошлого опыта, очевидно, что в долгосрочной перспективе легче добавлять новые возможности в универсальный механизм поиска в каталогах в виде внешних компонентов, чем менять программный код исходного сценария. Редактирование файлов могло быть одним из возможных расширений (а что вы думаете об автоматизации операции замены текста?), поэтому предпочтительнее выглядит более обобщенное и настраиваемое решение, допускающее возможность многократного использования. Функция os.walk достаточно проста в использовании, но прием, основанный на циклах, не так хорошо поддается настройке, как использование классов.
Инкапсуляция
Опираясь на прошлый опыт, я также знаю, что всегда желательно стараться максимально скрывать детали реализации инструментов от программ. Функция os.walk скрывает свою рекурсивную природу, тем не менее она предлагает весьма специфический интерфейс, который вполне может измениться в будущем. Подобные изменения имели место в прошлом — ближе к концу этого раздела я расскажу, как из версии Python 3.X был исключен один из инструментов обхода деревьев, что сразу же привело к нарушениям в работе программного кода, использующего его. Было бы лучше скрыть подобные зависимости за более нейтральным интерфейсом, чтобы клиентский программный код не приходил в негодность, как только нам потребуется внести изменения в реализацию нашего инструмента.
Конечно, если вы в достаточной мере изучили язык Python, то вы не можете не понимать, что все эти цели указывают на необходимость использования объектно-ориентированного подхода к реализации обхода и поиска. В примере 6.18 приводится одна из возможных реализаций этих целей. Этот модуль экспортирует универсальный класс FileVisitor, который в основном служит лишь оболочкой для os.walk, облегчающей использование и расширение, а также базовый класс SearchVisitor, обобщающий идею поиска в каталоге.
Сам по себе класс SearchVisitor делает то же самое, что делал сценарий search_all, но кроме этого, он открывает новые возможности по настройке процедуры поиска — какие-то черты его поведения могут модифицироваться путем перегрузки методов в подклассах. Более того, его базовая логика поиска может быть использована везде, где требуется поиск: достаточно просто определить подкласс, в котором будут добавлены специфические для поиска расширения. То же относится и к классу FileVisitor — переопределяя его методы и используя его атрибуты, можно внедряться в процесс обхода деревьев, используя приемы ООП. Это обычное дело в программировании — как только вы начинаете достаточно часто решать одни и те же тактические задачи, они наталкивают вас на подобные стратегические размышления.
Пример 6.18. PP4E\Tools\visitor.py
############################################################################## Тест: “python …\Tools\visitor.py dir testmask [строка]”. Использует классы и подклассы для сокрытия деталей использования функции os.walk при обходе и поиске; testmask — битовая маска, каждый бит в которой определяет тип самопроверки; смотрите также: подклассы visitor_*/.py; вообще подобные фреймворки должны использовать псевдочастные имена вида __X, однако в данной реализации все имена экспортируются для использования в подклассах и клиентами; переопределите метод reset для поддержки множественных, независимых объектов- обходчиков, требующих обновлений в подклассах;
##############################################################################
import os, sys
class FileVisitor:
Выполняет обход всех файлов, не являющихся каталогами, ниже startDir (по умолчанию ‘.’); при создании собственных обработчиков файлов/каталогов переопределяйте методы visit*; аргумент/атрибут context является необязательным и предназначен для хранения информации, специфической для подкласса; переключатель режима трассировки trace: 0 — нет трассировки, 1 — подкаталоги, 2 — добавляются файлы
def __init__(self, context=None, trace=2): self.fcount = 0 self.dcount = 0 self.context = context self.trace = trace
def run(self, startDir=os.curdir, reset=True):
if reset: self.reset()
for (thisDir, dirsHere, filesHere) in os.walk(startDir): self.visitdir(thisDir)
for fname in filesHere: # для некаталогов
fpath = os.path.join(thisDir, fname) # fname не содержит пути self.visitfile(fpath)
def reset(self): # используется обходчиками,
self.fcount = self.dcount = 0 # выполняющими обход независимо
def visitdir(self, dirpath): # вызывается для каждого каталога
self.dcount += 1 # переопределить или расширить
if self.trace > 0: print(dirpath, ‘…’)
def visitfile(self, filepath): # вызывается для каждого файла
self.fcount += 1 # переопределить или расширить
if self.trace > 1: print(self.fcount, ‘=>’, filepath)
class SearchVisitor(FileVisitor):
Выполняет поиск строки в файлах, находящихся в каталоге startDir и ниже; в подклассах: переопределите метод visitmatch, списки расширений, метод candidate, если необходимо; подклассы могут использовать testexts, чтобы определить типы файлов, в которых может выполняться поиск (но могут также переопределить метод candidate, чтобы использовать модуль mimetypes для определения файлов с текстовым содержимым: смотрите далее) skipexts = []
testexts = [‘.txt’, ‘.py’, ‘.pyw’, ‘.html’, ‘.c’, ‘.h’] # допустимые расш.
#skipexts = [‘.gif’, ‘.jpg’, ‘.pyc’, ‘.o’, ‘.a’, ‘.exe’] # или недопустимые # расширения
def __init__(self, searchkey, trace=2):
FileVisitor.__init__(self, searchkey, trace)
self.scount = 0
def reset(self): # в независимых обходчиках
self.scount = 0
def candidate(self, fname): # переопределить, если желательно
ext = os.path.splitext(fname)[1] # использовать модуль mimetypes if self.testexts:
return ext in self.testexts # если допустимое расширение
else: # или, если недопустимое
return ext not in self.skipexts # расширение
def visitfile(self, fname): # поиск строки
FileVisitor.visitfile(self, fname)
if not self.candidate(fname):
if self.trace > 0: print(‘Skipping’, fname) else:
text = open(fname).read() # ‘rb’ для недекодируемого текста
if self.context in text: # или text.find() != -1
self.visitmatch(fname, text) self.scount += 1
def visitmatch(self, fname, text): # обработка совпадения
print(‘%s has %s’ % (fname, self.context)) # переопределить
if __name__ == ‘__main__’:
# логика самотестирования dolist = 1
dosearch = 2 # 3 = список и поиск
donext = 4 # при добавлении следующего теста
def selftest(testmask): if testmask & dolist: visitor = FileVisitor(trace=2) visitor.run(sys.argv[2]) print(‘Visited %d files and %d dirs’ % (visitor.fcount, visitor.dcount))
if testmask & dosearch:
visitor = SearchVisitor(sys.argv[3], trace=0) visitor.run(sys.argv[2]) print(‘Found in %d files, visited %d’ % (visitor.scount, visitor.fcount))
selftest(int(sys.argv[1])) # например, 3 = dolist | dosearch
Этот модуль служит в основном для экспорта классов, используемых другими программами, но и при запуске в виде самостоятельного сценария делает кое-что полезное. Если вызвать его как сценарий с одним аргументом 1, он создаст и запустит объект FileVisitor и выведет полный список всех файлов и каталогов, начиная с того каталога, откуда он вызван, и ниже:
C:\…\PP4E\Tools> visitor.py 1 C:\temp\PP3E\Examples
C:\temp\PP3E\Examples …
1 => C:\temp\PP3E\Examples\README-root.txt
C:\temp\PP3E\Examples\PP3E …
2 => C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw
3 => C:\temp\PP3E\Examples\PP3E\LaunchBrowser.pyw
4 => C:\temp\PP3E\Examples\PP3E\Launcher.py
5 => C:\temp\PP3E\Examples\PP3E\Launcher.pyc
…множество строк опущено (передайте по конвейеру команде more или перенаправьте в файл)…
1424 => C:\temp\PP3E\Examples\PP3E\System\Threads\thread-count.py
1425 => C:\temp\PP3E\Examples\PP3E\System\Threads\thread1.py
C:\temp\PP3E\Examples\PP3E\TempParts …
1426 => C:\temp\PP3E\Examples\PP3E\TempParts\109_0237.JPG
1427 => C:\temp\PP3E\Examples\PP3E\TempParts\lawnlake1-jan-03.jpg
1428 => C:\temp\PP3E\Examples\PP3E\TempParts\part-001.txt
1429 => C:\temp\PP3E\Examples\PP3E\TempParts\part-002.html
Visited 1429 files and 186 dirs
Если же вызвать этот сценарий с 2 в первом аргументе, он создаст и запустит объект SearchVisitor, используя третий аргумент в качестве ключа поиска. Эта форма напоминает запуск знакомого нам сценария search_all.py, но в данном случае при обнаружении совпадений сценарий не останавливается:
C:\…\PP4E\Tools> visitor.py 2 C:\temp\PP3E\Examples mimetypes
C:\temp\PP3E\Examples\PP3E\extras\LosAlamosAdvancedClass\day1-system\data.txt has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailParser.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailSender.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat_modular.py has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\ftptools.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\uploadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\System\Media\playfile.py has mimetypes Found in 8 files, visited 1429
Технически при передаче сценарию числа 3 в первом аргументе он выполнит оба объекта, FileVisitor и SearchVisitor (осуществив два отдельных обхода). Первый аргумент в действительности используется в качестве битовой маски для выбора одной или более поддерживаемых самопроверок — если бит для какого-либо теста установлен в двоичном значении аргумента, этот тест будет выполнен. Поскольку 3 представляется в двоичном виде, как 011, выбираются одновременно поиск (010) и вывод списка (001). В более дружественной системе можно было бы определить символические параметры (например, искать аргументы —search и —list), но для целей данного сценария достаточно битовых масок.
Как обычно, этот модуль можно также использовать в интерактивном сеансе. Ниже приводится один из способов определения количества файлов и каталогов внутри определенного каталога. Последняя команда выполняет обход всего жесткого диска (и выводит результаты после заметной задержки!). Смотрите также пример «Найди самый большой файл Python» в начале этой главы, где описываются такие проблемы, как повторное посещение подкаталогов, не обрабатываемые данной реализацией:
C:\…\PP4E\Tools> python
> >> from visitor import FileVisitor
> >> V = FileVisitor(trace=0)
> >> V.run(r’C:\temp\PP3E\Examples’)
> >> V.dcount, V.fcount
(186, 1429)
> >> V.run(‘..’) # независимый обход (сброс счетчиков)
> >> V.dcount, V.fcount
(19, 181)
> >> V.run(‘..’, reset=False) # накопительный обход (счетчики сохраняются)
> >> V.dcount, V.fcount
(38, 362)
>>> V = FileVisitor(trace=0) # новый независимый обходчик (свои счетчики)
>>> V.run(r’C:\\’) # весь диск: в Unix попробуйте ‘/’
>>> V.dcount, V.fcount
(24992, 198585)
Модуль visitor удобно использовать как самостоятельный сценарий, чтобы получить список файлов и выполнить поиск в дереве каталогов, но в действительности он создавался, чтобы служить основой для расширения. В оставшейся части этого раздела мы коротко познакомимся с некоторыми клиентами этого модуля, которые добавляют свои операции с деревьями каталогов, используя приемы ООП.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011