Введение в программирование на Python

Автоматизация типичных задач на вашем компьютере

Разбить на страницы
Показывать лекцию целиком

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

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

Чтобы пройти все каталоги и файлы в дереве директорий, мы используем метод os.walk и цикл for. Аналогичным образом обычная команда open дает возможность прочитать в цикле содержимое файла, сокет позволяет в цикле читать данные через сетевое соединение, а библиотека urllib дает возможность открыть веб-документ и в цикле просматривать его содержимое.

28.1. Имена файлов и пути

Каждая работающая программа имеет "текущий каталог" (current directory), который является каталогом по умолчанию для большинства операций. Например, когда мы открываем файл для чтения, Питон ищет его в текущем каталоге. Модуль os (сокращение от "operating system" – операционная система) обеспечивает нас функциями для работы с файлами и каталогами; метод os.getcwd возвращает название текущего каталога:

>>> import os
>>> cwd = os.getcwd()
>>> print cwd
/Users/csev
  

Аббревиатура cwd является сокращением от "current working directory". В приведенном примере результатом является /Users/csev, это домашняя директория пользователя с именем csev.

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

Пути, с которыми мы имели дело до сих пор, были попросту именами файлов, т.е. они были относительными (рассматривались файлы в текущей директории). Для нахождения абсолютного пути к файлу можно воспользоваться методом os.path.abspath:

>>> os.path.abspath('memo.txt')
'/Users/csev/memo.txt'
  

Метод os.path.exists проверяет, существует ли файл или каталог:

>>> os.path.exists('memo.txt')
True
  

Если путь существует, то метод os.path.isdir определяет, задает ли он каталог (директорию):

>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('music')
True
  

Аналогично, метод os.path.isfile проверяет, задает ли он файл.

Метод os.listdir возвращает список всех файлов и каталогов в заданном каталоге:

>>> os.listdir(cwd)
['music', 'photos', 'memo.txt']
  

28.2. Пример: очистка каталога с именем "photo"

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

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

./2006/03/24-03-06_2018002.jpg
./2006/03/24-03-06_2018002.txt
  

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

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

import os
count = 0
for (dirname, dirs, files) in os.walk('.'):
for filename in files:
if filename.endswith('.txt') :
count = count + 1
print 'Files:', count
python txtcount.py
Files: 1917
  

Ключевым элементом в этой программе является вызов метода os.walk библиотеки Питона. Когда мы вызываем метод os.walk и указываем ему стартовый каталог, он "обходит" все каталоги и подкаталоги внутри начального рекурсивно. Строка "." обозначает текущий каталог, который нужно пройти "в глубину". В процессе обхода в цикле for для каждого каталога мы получаем три значения в форме кортежа: его первым элементом является имя очередного каталога, вторым элементом – список его подкаталогов, третьим – список его файлов.

Нам не нужно явно исследовать содержимое каждого подкаталога, поскольку можно положиться на метод os.walk, который рано или поздно посетит каждый подкаталог. Но нам необходимо проверить все файлы, поэтому мы написали простой цикл for для проверки каждого файла в очередном каталоге. Мы проверяем, заканчивается ли имя файла на ".txt" и, если да, увеличиваем счетчик числа файлов, имена которых оканчиваются суффиксом ".txt".

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

import os
from os.path import join
for (dirname, dirs, files) in os.walk('.'):
for filename in files:
if filename.endswith('.txt') :
thefile = os.path.join(dirname,filename)
print os.path.getsize(thefile), thefile
  

Теперь вместо простого подсчета файлов мы создаем строку, представляющую путь к файлу, путем соединения имени директории с именем файла внутри нее при помощи метода os.path.join. Важно использовать именно os.path.join вместо простой конкатенации строк, поскольку в Windows в обозначении пути к файлу используется символ "обратная косая черта" (backslash '\'), а в операционных системах Linux и Apple – прямая косая черта '/'. Метод os.path.join знает об этих различиях и учитывает, в какой системе работает программа, выбирая правильный разделитель; поэтому программа Питона работает правильно и под Windows, и в системах типа Unix.

После того, как мы получили полное имя файла, включающее путь к нему, мы используем метод os.path.getsize, чтобы получить и напечатать размер файла. Вот что выдает наша программа:

python txtsize.py
...
18 ./2006/03/24-03-06_2303002.txt
22 ./2006/03/25-03-06_1340001.txt
22 ./2006/03/25-03-06_2034001.txt
...
2565 ./2005/09/28-09-05_1043004.txt
2565 ./2005/09/28-09-05_1141002.txt
...
2578 ./2006/03/27-03-06_1618001.txt
2578 ./2006/03/28-03-06_2109001.txt
2578 ./2006/03/29-03-06_1355001.txt
...
  

Рассматривая вывод, обратим внимание на то, что некоторые файлы совсем короткие, а некоторые, наоборот, очень большие, причем они имеют один и тот же размер (либо 2578, либо 2565). Посмотрев содержимое одного из подобных файлов, можно увидеть, что в них не содержится ничего, кроме одинакового HTML-текста, присланного моей системе с моего мобильного телефона:

<html>
<head>
<title>T-Mobile</title>
...
  

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

Итак, напишем следующую программу:

import os
from os.path import join
for (dirname, dirs, files) in os.walk('.'):
for filename in files:
if filename.endswith('.txt') :
thefile = os.path.join(dirname,filename)
size = os.path.getsize(thefile)
if size == 2578 or size == 2565:
continue
fhand = open(thefile,'r')
 lines = list()
for line in fhand:
lines.append(line)
fhand.close()
if len(lines) > 1:
print len(lines), thefile
print lines[:4]
  

Мы используем оператор continue для пропуска файлов с одним из двух "плохих" размеров, остальные файлы открываем и считываем содержащиеся в них строки в список Питона; если строк больше одной, мы печатаем количество строк в файле и первые 3 строки.

Всё это выглядит как отфильтровывание файлов с двумя плохими размерами, а также допущение, что файлы, содержащие только одну строку, корректны; после этого выдача нашей программы становится достаточно содержательной:

python txtcheck.py
3 ./2004/03/22-03-04_2015.txt
['Little horse rider\r\n', '\r\n', '\r']
2 ./2004/11/30-11-04_1834001.txt
['Testing 123.\n', '\n']
3 ./2007/09/15-09-07_074202_03.txt
['\r\n', '\r\n', 'Sent from my iPhone\r\n']
3 ./2007/09/19-09-07_124857_01.txt
['\r\n', '\r\n', 'Sent from my iPhone\r\n']
3 ./2007/09/20-09-07_115617_01.txt
...
  

Остался еще один тип файлов, доставляющих беспокойство: это файлы, содержащие по 3 строки, из которых первые две пустые, а третья строка представляет собой сообщение "Sent from my iPhone" ("Отправлено с моего телефона"), неизвестно как просочившееся внутрь моих данных. Поэтому мы сделаем еще одно изменение в нашей программе, чтобы учесть и такие файлы.

lines = list()
for line in fhand:
lines.append(line)
if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
continue
if len(lines) > 1:
print len(lines), thefile
print lines[:4]
  

Мы просто проверяем файлы из трех строк, и, если третья строка начинается с указанного текста, пропускаем файл.

Теперь, запустив программу, мы видим всего 4 оставшихся многострочных файла, и все 4 выглядят вполне разумно:

python txtcheck2.py

3 ./2004/03/22-03-04_2015.txt
['Little horse rider\r\n', '\r\n', '\r']
2 ./2004/11/30-11-04_1834001.txt
['Testing 123.\n', '\n']
2 ./2006/03/17-03-06_1806001.txt
['On the road again...\r\n', '\r\n']
2 ./2006/03/24-03-06_1740001.txt
['On the road again...\r\n', '\r\n']
  

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

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

import os
from os.path import join
for (dirname, dirs, files) in os.walk('.'):
for filename in files:
if filename.endswith('.txt') :
thefile = os.path.join(dirname,filename)
size = os.path.getsize(thefile)
if size == 2578 or size == 2565:
print 'T-Mobile:',thefile
continue
fhand = open(thefile,'r')
lines = list()
for line in fhand:
lines.append(line)
fhand.close()
if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
print 'iPhone:', thefile
continue
  

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

python txtcheck3.py
...
T-Mobile: ./2006/05/31-05-06_1540001.txt
T-Mobile: ./2006/05/31-05-06_1648001.txt
iPhone: ./2007/09/15-09-07_074202_03.txt
iPhone: ./2007/09/15-09-07_144641_01.txt
iPhone: ./2007/09/19-09-07_124857_01.txt
...
  

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

if size == 2578 or size == 2565:
print 'T-Mobile:',thefile
os.remove(thefile)
continue
...
if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
print 'iPhone:', thefile
os.remove(thefile)
continue
  

В этом варианте программы мы печатаем названия плохих файлов и затем удаляем их, используя метод os.remove.

python txtdelete.py
T-Mobile: ./2005/01/02-01-05_1356001.txt
T-Mobile: ./2005/01/02-01-05_1858001.txt
...
  

Ради интереса запустите программу во второй раз – она не выдаст ничего, поскольку все плохие файлы уже уничтожены. Если запустить рассмотренную ранее программу txtcount.py, подсчитывающую текстовые файлы, мы увидим, что было удалено 899 плохих файлов:

python txtcount.py
Files: 1018
  

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

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

28.3. Аргументы командной строки

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

name = raw_input('Enter file:')
handle = open(name, 'r')
text = handle.read()
...
  

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

python words.py
Enter file: mbox-short.txt
...
  

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

import sys
print 'Count:', len(sys.argv)
print 'Type:', type(sys.argv)
for arg in sys.argv:
print 'Argument:', arg
  

Содержимое переменной sys.argv является списком строк, в котором первая строка – это имя Питон-программы, а следующие строки представляют собой аргументы командной строки, указанные в команде после имени файла с программой. Ниже приведен вывод нашей программы для конкретной командной строки:

python argtest.py hello there
Count: 3
Type: <type 'list'>
Argument: argtest.py
Argument: hello
Argument: there
  

Здесь 3 аргумента командной строки передаются нашей программе в виде трехэлементного списка. Первым элементом является имя программы (argtest.py), двумя другими (hello и there) – слова, указанные в команде после имени файла.

Можно переписать нашу программу, чтобы она читала файл, получая его имя из командной строки:

import sys
name = sys.argv[1]
handle = open(name, 'r')
text = handle.read()
print name, 'is', len(text), 'bytes'
  

В качестве имени файла берется второй аргумент командной строки (пропускается имя программы, соответствующее индексу [0]). Мы открываем файл, читаем его содержимое и печатаем его длину в байтах:

python argfile.py mbox-short.txt
mbox-short.txt is 94626 bytes
  

Использование аргументов командной строки облегчает повторное использование Питон-программ, особенно когда нужно вводить только одну или две строки.

28.4. Программные каналы (pipes)

Большинство операционных систем предоставляет интерфейс командной строки, известный под названием оболочка (shell). Оболочка обычно предоставляет команды для перемещения по файловой системе и запуска приложений. Например, в Unix'е можно перемещаться по директориям с помощью команды "cd", просматривать содержимое директории с помощью "ls" и запускать веб-браузер, например, с помощью команды "firefox".

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

Программный канал (pipe) – это объект, представляющий работающий процесс.

Например, команда Unix'а При использовании каналов для вызова команд операционной системы, таких, как "ls", важно знать, какую именно операционную систему вы используете, и вызывать только команды, поддерживаемые операционной системой. "ls -l" показывает содержимое текущего каталога (в подробном формате). Можно запустить эту команду, используя метод os.popen:

>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)
  

Аргументом является строка, содержащую команду оболочки. Возвращаемое значение является указателем на файл, который можно использовать точно так же, как и при открытии обычного файла с помощью функции open. Можно читать вывод процесса "ls" последовательно по одной строке с помощью метода readline или получить сразу весь вывод с помощью метода read:

>>> res = fp.read()

По окончании работы следует закрыть канал так же, как и файл:

>>> stat = fp.close()
>>> print stat
None
  

Возвращаемое методом close значение содержит статус завершения процесса ls; "None" означает нормальное завершение (т.е. отсутствие ошибок).

28.5. Глоссарий

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

Контрольная сумма (checksum): см. также "хеширование". Термин "контрольная сумма" был порожден необходимостью проверки данных, посланных по сети или записанных на внешний носитель и затем прочитанных обратно. Когда данные записываются или пересылаются, передающая система вычисляет контрольную сумму и пересылает ее вместе с данными. Когда данные считываются или принимаются по сети, принимающая система перевычисляет контрольную сумму полученных данных и сравнивает ее с принятой контрольной суммой. Если контрольные суммы не совпадают, то это означает, что данные были искажены при передаче.

Аргументы командной строки (command line arguments): параметры, указанные в командной строке Питона после имени файла с программой.

Текущий каталог/директория (current working directory): текущий каталог, в котором "вы находитесь". Можно изменить текущий каталог, используя команду "cd", которая есть в большинстве операционных систем в командном интерфейсе. Когда вы открываете файл в Питоне, используя только его имя и не указывая путь, файл должен быть в текущем каталоге, в котором вы запускаете программу.

Хеширование (hashing): чтение потенциально очень большого объема данных и вычисление контрольной суммы для этих данных — так называемой хеш-функции. Лучшие хеш-функции создают минимальное число "коллизий", когда два различных потока данных дают при вычислении хеш-функции один и тот же результат. MD5, SHA1 и SHA256 являются названиями наиболее распространенных хеш-функций.

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

Относительный путь (relative path): строка, описывающая, где хранится файл или каталог (директория) относительно текущего каталога.

Командная оболочка (shell): интерфейс командной строки к операционной системе, называемый также "терминалом" в некоторых системах. В нем пользователь вводит команду и ее параметры и затем нажимает клавишу "Enter" для выполнения команды.

Обход (walk): термин, используемый для описания процесса посещений узлов дерева каталогов, подкаталогов, под-подкаталогов, пока мы не посетим все каталоги. Мы называем этот процесс "обходом дерева каталогов/директорий".

28.6. Упражнения

Упражнение 28.1.

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

  • Напишите программу, которая обходит все каталоги и подкаталоги, находит все файлы с указанным суффиксом (например, .mp3) и перечисляет пары файлов с одинаковым размером. Совет: используйте словарь, в котором ключом является размер файла, полученный с помощью метода os.path.getsize, а значением является путь к файлу (включая его имя). При получении очередного файла проверяйте, имеется ли уже в словаре файл с таким же размером. Если да, то надо напечатать размер файла и названия обоих файлов (один из словаря, второй — текущий просматриваемый файл).
  • Измените предыдущую программу так, чтобы она сравнивала не только размеры, но и содержимое файлов, используя алгоритм вычисления контрольной суммы или хеш-функции. Например, алгоритм MD5 (Message-Digest algorithm 5) читает "сообщение" произвольной длины и вычисляет 128-битовую "контрольную сумму". Вероятность того, что у двух разных файлов будет одинаковая контрольная сумма, ничтожно мала. Описание MD5 можно прочитать по адресу . Следующий фрагмент кода открывает файл, читает его содержимое и вычисляет контрольную сумму.
    import hashlib
    ...
    fhand = open(thefile,'r')
    data = fhand.read()
    fhand.close()
    checksum = hashlib.md5(data.encode()).hexdigest()
        

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

    ./2004/11/15-11-04_0923001.jpg ./2004/11/15-11-04_1016001.jpg
    ./2005/06/28-06-05_1500001.jpg ./2005/06/28-06-05_1502001.jpg
    ./2006/08/11-08-06_205948_01.jpg ./2006/08/12-08-06_155318_02.jpg
    ./2006/09/28-09-06_225657_01.jpg ./2006/09-50-years/28-09-06_225657_01.jpg
    ./2006/09/29-09-06_002312_01.jpg ./2006/09-50-years/29-09-06_002312_01.jpg
        

    Очевидно, я иногда отправляю одни и те же фотографии более одного раза или копирую фотографии без удаления файла-оригинала.

  • Страницы:

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

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

    Чтобы пройти все каталоги и файлы в дереве директорий, мы используем метод os.walk и цикл for. Аналогичным образом обычная команда open дает возможность прочитать в цикле содержимое файла, сокет позволяет в цикле читать данные через сетевое соединение, а библиотека urllib дает возможность открыть веб-документ и в цикле просматривать его содержимое.

    28.1. Имена файлов и пути

    Каждая работающая программа имеет "текущий каталог" (current directory), который является каталогом по умолчанию для большинства операций. Например, когда мы открываем файл для чтения, Питон ищет его в текущем каталоге. Модуль os (сокращение от "operating system" – операционная система) обеспечивает нас функциями для работы с файлами и каталогами; метод os.getcwd возвращает название текущего каталога:

    >>> import os
    >>> cwd = os.getcwd()
    >>> print cwd
    /Users/csev
      

    Аббревиатура cwd является сокращением от "current working directory". В приведенном примере результатом является /Users/csev, это домашняя директория пользователя с именем csev.

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

    Пути, с которыми мы имели дело до сих пор, были попросту именами файлов, т.е. они были относительными (рассматривались файлы в текущей директории). Для нахождения абсолютного пути к файлу можно воспользоваться методом os.path.abspath:

    >>> os.path.abspath('memo.txt')
    '/Users/csev/memo.txt'
      

    Метод os.path.exists проверяет, существует ли файл или каталог:

    >>> os.path.exists('memo.txt')
    True
      

    Если путь существует, то метод os.path.isdir определяет, задает ли он каталог (директорию):

    >>> os.path.isdir('memo.txt')
    False
    >>> os.path.isdir('music')
    True
      

    Аналогично, метод os.path.isfile проверяет, задает ли он файл.

    Метод os.listdir возвращает список всех файлов и каталогов в заданном каталоге:

    >>> os.listdir(cwd)
    ['music', 'photos', 'memo.txt']
      

    28.2. Пример: очистка каталога с именем "photo"

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

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

    ./2006/03/24-03-06_2018002.jpg
    ./2006/03/24-03-06_2018002.txt
      

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

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

    import os
    count = 0
    for (dirname, dirs, files) in os.walk('.'):
    for filename in files:
    if filename.endswith('.txt') :
    count = count + 1
    print 'Files:', count
    python txtcount.py
    Files: 1917
      

    Ключевым элементом в этой программе является вызов метода os.walk библиотеки Питона. Когда мы вызываем метод os.walk и указываем ему стартовый каталог, он "обходит" все каталоги и подкаталоги внутри начального рекурсивно. Строка "." обозначает текущий каталог, который нужно пройти "в глубину". В процессе обхода в цикле for для каждого каталога мы получаем три значения в форме кортежа: его первым элементом является имя очередного каталога, вторым элементом – список его подкаталогов, третьим – список его файлов.

    Нам не нужно явно исследовать содержимое каждого подкаталога, поскольку можно положиться на метод os.walk, который рано или поздно посетит каждый подкаталог. Но нам необходимо проверить все файлы, поэтому мы написали простой цикл for для проверки каждого файла в очередном каталоге. Мы проверяем, заканчивается ли имя файла на ".txt" и, если да, увеличиваем счетчик числа файлов, имена которых оканчиваются суффиксом ".txt".

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

    import os
    from os.path import join
    for (dirname, dirs, files) in os.walk('.'):
    for filename in files:
    if filename.endswith('.txt') :
    thefile = os.path.join(dirname,filename)
    print os.path.getsize(thefile), thefile
      

    Теперь вместо простого подсчета файлов мы создаем строку, представляющую путь к файлу, путем соединения имени директории с именем файла внутри нее при помощи метода os.path.join. Важно использовать именно os.path.join вместо простой конкатенации строк, поскольку в Windows в обозначении пути к файлу используется символ "обратная косая черта" (backslash '\'), а в операционных системах Linux и Apple – прямая косая черта '/'. Метод os.path.join знает об этих различиях и учитывает, в какой системе работает программа, выбирая правильный разделитель; поэтому программа Питона работает правильно и под Windows, и в системах типа Unix.

    После того, как мы получили полное имя файла, включающее путь к нему, мы используем метод os.path.getsize, чтобы получить и напечатать размер файла. Вот что выдает наша программа:

    python txtsize.py
    ...
    18 ./2006/03/24-03-06_2303002.txt
    22 ./2006/03/25-03-06_1340001.txt
    22 ./2006/03/25-03-06_2034001.txt
    ...
    2565 ./2005/09/28-09-05_1043004.txt
    2565 ./2005/09/28-09-05_1141002.txt
    ...
    2578 ./2006/03/27-03-06_1618001.txt
    2578 ./2006/03/28-03-06_2109001.txt
    2578 ./2006/03/29-03-06_1355001.txt
    ...
      

    Рассматривая вывод, обратим внимание на то, что некоторые файлы совсем короткие, а некоторые, наоборот, очень большие, причем они имеют один и тот же размер (либо 2578, либо 2565). Посмотрев содержимое одного из подобных файлов, можно увидеть, что в них не содержится ничего, кроме одинакового HTML-текста, присланного моей системе с моего мобильного телефона:

    <html>
    <head>
    <title>T-Mobile</title>
    ...
      

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

    Итак, напишем следующую программу:

    import os
    from os.path import join
    for (dirname, dirs, files) in os.walk('.'):
    for filename in files:
    if filename.endswith('.txt') :
    thefile = os.path.join(dirname,filename)
    size = os.path.getsize(thefile)
    if size == 2578 or size == 2565:
    continue
    fhand = open(thefile,'r')
     lines = list()
    for line in fhand:
    lines.append(line)
    fhand.close()
    if len(lines) > 1:
    print len(lines), thefile
    print lines[:4]
      

    Мы используем оператор continue для пропуска файлов с одним из двух "плохих" размеров, остальные файлы открываем и считываем содержащиеся в них строки в список Питона; если строк больше одной, мы печатаем количество строк в файле и первые 3 строки.

    Всё это выглядит как отфильтровывание файлов с двумя плохими размерами, а также допущение, что файлы, содержащие только одну строку, корректны; после этого выдача нашей программы становится достаточно содержательной:

    python txtcheck.py
    3 ./2004/03/22-03-04_2015.txt
    ['Little horse rider\r\n', '\r\n', '\r']
    2 ./2004/11/30-11-04_1834001.txt
    ['Testing 123.\n', '\n']
    3 ./2007/09/15-09-07_074202_03.txt
    ['\r\n', '\r\n', 'Sent from my iPhone\r\n']
    3 ./2007/09/19-09-07_124857_01.txt
    ['\r\n', '\r\n', 'Sent from my iPhone\r\n']
    3 ./2007/09/20-09-07_115617_01.txt
    ...
      

    Остался еще один тип файлов, доставляющих беспокойство: это файлы, содержащие по 3 строки, из которых первые две пустые, а третья строка представляет собой сообщение "Sent from my iPhone" ("Отправлено с моего телефона"), неизвестно как просочившееся внутрь моих данных. Поэтому мы сделаем еще одно изменение в нашей программе, чтобы учесть и такие файлы.

    lines = list()
    for line in fhand:
    lines.append(line)
    if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
    continue
    if len(lines) > 1:
    print len(lines), thefile
    print lines[:4]
      

    Мы просто проверяем файлы из трех строк, и, если третья строка начинается с указанного текста, пропускаем файл.

    Теперь, запустив программу, мы видим всего 4 оставшихся многострочных файла, и все 4 выглядят вполне разумно:

    python txtcheck2.py
    
    3 ./2004/03/22-03-04_2015.txt
    ['Little horse rider\r\n', '\r\n', '\r']
    2 ./2004/11/30-11-04_1834001.txt
    ['Testing 123.\n', '\n']
    2 ./2006/03/17-03-06_1806001.txt
    ['On the road again...\r\n', '\r\n']
    2 ./2006/03/24-03-06_1740001.txt
    ['On the road again...\r\n', '\r\n']
      

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

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

    import os
    from os.path import join
    for (dirname, dirs, files) in os.walk('.'):
    for filename in files:
    if filename.endswith('.txt') :
    thefile = os.path.join(dirname,filename)
    size = os.path.getsize(thefile)
    if size == 2578 or size == 2565:
    print 'T-Mobile:',thefile
    continue
    fhand = open(thefile,'r')
    lines = list()
    for line in fhand:
    lines.append(line)
    fhand.close()
    if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
    print 'iPhone:', thefile
    continue
      

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

    python txtcheck3.py
    ...
    T-Mobile: ./2006/05/31-05-06_1540001.txt
    T-Mobile: ./2006/05/31-05-06_1648001.txt
    iPhone: ./2007/09/15-09-07_074202_03.txt
    iPhone: ./2007/09/15-09-07_144641_01.txt
    iPhone: ./2007/09/19-09-07_124857_01.txt
    ...
      

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

    if size == 2578 or size == 2565:
    print 'T-Mobile:',thefile
    os.remove(thefile)
    continue
    ...
    if len(lines) == 3 and lines[2].startswith('Sent from my iPhone') :
    print 'iPhone:', thefile
    os.remove(thefile)
    continue
      

    В этом варианте программы мы печатаем названия плохих файлов и затем удаляем их, используя метод os.remove.

    python txtdelete.py
    T-Mobile: ./2005/01/02-01-05_1356001.txt
    T-Mobile: ./2005/01/02-01-05_1858001.txt
    ...
      

    Ради интереса запустите программу во второй раз – она не выдаст ничего, поскольку все плохие файлы уже уничтожены. Если запустить рассмотренную ранее программу txtcount.py, подсчитывающую текстовые файлы, мы увидим, что было удалено 899 плохих файлов:

    python txtcount.py
    Files: 1018
      

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

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

    28.3. Аргументы командной строки

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

    name = raw_input('Enter file:')
    handle = open(name, 'r')
    text = handle.read()
    ...
      

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

    python words.py
    Enter file: mbox-short.txt
    ...
      

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

    import sys
    print 'Count:', len(sys.argv)
    print 'Type:', type(sys.argv)
    for arg in sys.argv:
    print 'Argument:', arg
      

    Содержимое переменной sys.argv является списком строк, в котором первая строка – это имя Питон-программы, а следующие строки представляют собой аргументы командной строки, указанные в команде после имени файла с программой. Ниже приведен вывод нашей программы для конкретной командной строки:

    python argtest.py hello there
    Count: 3
    Type: <type 'list'>
    Argument: argtest.py
    Argument: hello
    Argument: there
      

    Здесь 3 аргумента командной строки передаются нашей программе в виде трехэлементного списка. Первым элементом является имя программы (argtest.py), двумя другими (hello и there) – слова, указанные в команде после имени файла.

    Можно переписать нашу программу, чтобы она читала файл, получая его имя из командной строки:

    import sys
    name = sys.argv[1]
    handle = open(name, 'r')
    text = handle.read()
    print name, 'is', len(text), 'bytes'
      

    В качестве имени файла берется второй аргумент командной строки (пропускается имя программы, соответствующее индексу [0]). Мы открываем файл, читаем его содержимое и печатаем его длину в байтах:

    python argfile.py mbox-short.txt
    mbox-short.txt is 94626 bytes
      

    Использование аргументов командной строки облегчает повторное использование Питон-программ, особенно когда нужно вводить только одну или две строки.

    28.4. Программные каналы (pipes)

    Большинство операционных систем предоставляет интерфейс командной строки, известный под названием оболочка (shell). Оболочка обычно предоставляет команды для перемещения по файловой системе и запуска приложений. Например, в Unix'е можно перемещаться по директориям с помощью команды "cd", просматривать содержимое директории с помощью "ls" и запускать веб-браузер, например, с помощью команды "firefox".

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

    Программный канал (pipe) – это объект, представляющий работающий процесс.

    Например, команда Unix'а При использовании каналов для вызова команд операционной системы, таких, как "ls", важно знать, какую именно операционную систему вы используете, и вызывать только команды, поддерживаемые операционной системой. "ls -l" показывает содержимое текущего каталога (в подробном формате). Можно запустить эту команду, используя метод os.popen:

    >>> cmd = 'ls -l'
    >>> fp = os.popen(cmd)
      

    Аргументом является строка, содержащую команду оболочки. Возвращаемое значение является указателем на файл, который можно использовать точно так же, как и при открытии обычного файла с помощью функции open. Можно читать вывод процесса "ls" последовательно по одной строке с помощью метода readline или получить сразу весь вывод с помощью метода read:

    >>> res = fp.read()

    По окончании работы следует закрыть канал так же, как и файл:

    >>> stat = fp.close()
    >>> print stat
    None
      

    Возвращаемое методом close значение содержит статус завершения процесса ls; "None" означает нормальное завершение (т.е. отсутствие ошибок).

    28.5. Глоссарий

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

    Контрольная сумма (checksum): см. также "хеширование". Термин "контрольная сумма" был порожден необходимостью проверки данных, посланных по сети или записанных на внешний носитель и затем прочитанных обратно. Когда данные записываются или пересылаются, передающая система вычисляет контрольную сумму и пересылает ее вместе с данными. Когда данные считываются или принимаются по сети, принимающая система перевычисляет контрольную сумму полученных данных и сравнивает ее с принятой контрольной суммой. Если контрольные суммы не совпадают, то это означает, что данные были искажены при передаче.

    Аргументы командной строки (command line arguments): параметры, указанные в командной строке Питона после имени файла с программой.

    Текущий каталог/директория (current working directory): текущий каталог, в котором "вы находитесь". Можно изменить текущий каталог, используя команду "cd", которая есть в большинстве операционных систем в командном интерфейсе. Когда вы открываете файл в Питоне, используя только его имя и не указывая путь, файл должен быть в текущем каталоге, в котором вы запускаете программу.

    Хеширование (hashing): чтение потенциально очень большого объема данных и вычисление контрольной суммы для этих данных — так называемой хеш-функции. Лучшие хеш-функции создают минимальное число "коллизий", когда два различных потока данных дают при вычислении хеш-функции один и тот же результат. MD5, SHA1 и SHA256 являются названиями наиболее распространенных хеш-функций.

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

    Относительный путь (relative path): строка, описывающая, где хранится файл или каталог (директория) относительно текущего каталога.

    Командная оболочка (shell): интерфейс командной строки к операционной системе, называемый также "терминалом" в некоторых системах. В нем пользователь вводит команду и ее параметры и затем нажимает клавишу "Enter" для выполнения команды.

    Обход (walk): термин, используемый для описания процесса посещений узлов дерева каталогов, подкаталогов, под-подкаталогов, пока мы не посетим все каталоги. Мы называем этот процесс "обходом дерева каталогов/директорий".

    28.6. Упражнения

    Упражнение 28.1.

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

  • Напишите программу, которая обходит все каталоги и подкаталоги, находит все файлы с указанным суффиксом (например, .mp3) и перечисляет пары файлов с одинаковым размером. Совет: используйте словарь, в котором ключом является размер файла, полученный с помощью метода os.path.getsize, а значением является путь к файлу (включая его имя). При получении очередного файла проверяйте, имеется ли уже в словаре файл с таким же размером. Если да, то надо напечатать размер файла и названия обоих файлов (один из словаря, второй — текущий просматриваемый файл).
  • Измените предыдущую программу так, чтобы она сравнивала не только размеры, но и содержимое файлов, используя алгоритм вычисления контрольной суммы или хеш-функции. Например, алгоритм MD5 (Message-Digest algorithm 5) читает "сообщение" произвольной длины и вычисляет 128-битовую "контрольную сумму". Вероятность того, что у двух разных файлов будет одинаковая контрольная сумма, ничтожно мала. Описание MD5 можно прочитать по адресу . Следующий фрагмент кода открывает файл, читает его содержимое и вычисляет контрольную сумму.
    import hashlib
    ...
    fhand = open(thefile,'r')
    data = fhand.read()
    fhand.close()
    checksum = hashlib.md5(data.encode()).hexdigest()
        

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

    ./2004/11/15-11-04_0923001.jpg ./2004/11/15-11-04_1016001.jpg
    ./2005/06/28-06-05_1500001.jpg ./2005/06/28-06-05_1502001.jpg
    ./2006/08/11-08-06_205948_01.jpg ./2006/08/12-08-06_155318_02.jpg
    ./2006/09/28-09-06_225657_01.jpg ./2006/09-50-years/28-09-06_225657_01.jpg
    ./2006/09/29-09-06_002312_01.jpg ./2006/09-50-years/29-09-06_002312_01.jpg
        

    Очевидно, я иногда отправляю одни и те же фотографии более одного раза или копирую фотографии без удаления файла-оригинала.

  • Вернуться к учебному плану