Опубликован: 14.06.2015 | Доступ: свободный | Студентов: 7371 / 1134 | Длительность: 09:49:00
Авторские права: Creative Commons Attribution 3.0
Лекция 15:

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

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

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

Чтобы пройти все каталоги и файлы в дереве директорий, мы используем метод 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
  
Алексей Виноградов
Алексей Виноградов

Видеокурс выложен на сайте Altube.ru вместо Youtube и плеер Altube не поддерживает субтитры. Прошу решить вопрос о предоставлении русских субтитров в этом англоязычном видеокурсе.

Петр Олейников
Петр Олейников

Данные файлы неоходимы не только для самостоятельных работ, но и для тестов. А по ссылкам в лекциях они не доступны, выдает ошибку 404.