LXF97:Python

Материал из Linuxformat.

Перейти к: навигация, поиск

Содержание

Python: Управляемся

Надоело мириться с дезориентирующими тэгами и нелепыми именами файлов? Укодируйте их до полного исчезновения с помощью Ника Вейча и магии Python!

Правда-правда, я ужасно люблю порядок: все вещи на своем месте, все под рукой, и вы можете мигом определить, что есть, чего нет; и всякое такое. Впрочем, люди, видевшие фото моего рабочего стола или совавшие нос в мой бельевой ящик, пожалуй, оторопеют от явной лживости этого высказывания. Но, пожалуйста, внимательно прочтите его еще раз: я сказал, что люблю порядок, а вовсе не что я люблю его наводить. Моя мечта – чтобы во всех комнатах моего дома стояло по хитроумной мусорной корзине, связанной с этаким интеллектуальным сканером объектов: каждый раз, покончив с использованием/чтением/изучением чего-либо, я бы просто скидывал эту штуку в мусоросборник, а она волшебным образом перемещалась куда-нибудь и дожидалась там своего часа. Увы, похоже, подобные технологии не скоро до меня доберутся. Сейчас самая моя больная тема – музыкальная коллекция. Я, конечно, целиком за свободу творчества, но то, что все мои сборники CD имеют разные шрифты, цвета и формат хранения данных, действует на нервы. Впрочем, виртуальный мир вашей Linux-системы способен удовлетворить любые ваши прихоти, включая страсть к педантизму и скрупулезной правильности!

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

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

  • Некоторые файлы имеют слишком длинные имена, затрудняющие работу с ними. Такое бывает с покупной музыкой: в название

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

  • Некоторые файлы содержат тэги формата ID3v1, а не ID3v2. Беда

небольшая, но по возможности я бы предпочел наслаждаться и преимуществами ID3v2.

  • Некоторые файлы снабжены тэгами формата ID3v2, но не ID3v1.

Из-за этого они не читаются на стареньком MP3-плейере в моем автомобиле.

  • Некоторые из файлов неработоспособны. Неплохо было бы

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

  • Некоторые из файлов сохранены в нетипичных форматах, которые распознаются не всеми доступными устройствами. Список этих

файлов тоже пригодился бы.

Но как же достичь хотя бы части этих целей? Хоть я и не вполне уверен, что все перечисленные мной желания легко выполнимы, но попытаться стоит; по крайней мере, я знаю, с чего начать. В Python'е имеется модуль под названием os. Он реализует все стандартные функции ОС, в частности, связанные с файловой системой. Python – кросс-платформенная разработка, поэтому, несмотря на различия реализации в разных операционных системах, задача у данного модуля одна: предоставление функциональности уровня ОС с единым интерфейсом. Одной из самых полезных из известных мне функций, представленных в данном модуле, является walk(). Если задать ей имя родительского каталога, эта функция вернет список кортежей (см. врезку слева), включающих имя родительского каталога, список подкаталогов (если они есть) и список имен файлов. Таким образом, чтобы получить список всех файлов, включая полный путь к ним, нужно выполнить функцию walk() с заданным нами каталогом и по шагам обработать результат.

Что такое «кортеж»? (врезка)

Кортеж – это объект, состоящий из нескольких значений. Он часто используется в Python. Хороший пример использования кортежа – цвет. Вместо того, чтобы хранить три значения в трех разных переменных для красного, зеленого и синего цветов, вы назначаете одну переменную, содержащую три значения! Кортеж в Python записывается примерно так: (123,255,17). Вы можете получить доступ к любому элементу кортежа с помощью индексов – значений в квадратных скобках, указываемых после имени переменной. Например, команда print Colour[1] вернет значение «255» в случае с предыдущим примером – только не забывайте, что значения индексов начинаются с 0!

Python и музон

import os
 topdir="/usr/share/music/"
 walklist = os.walk(topdir)
 for dirs in walklist:
   if(dirs[2]):
     for file in dirs[2]:
     // Выполнить какое-нибудь действие
     print os.path.join(dirs[0],file)
   else:
     for entry in dirs[1]:
       print dirs[1]

В данном примере мы сначала импортируем модуль os, затем назначаем каталог, который хотим использовать. В результате выполнения функции walk() генерируется объект в переменной walklist. С помощью конструктора for мы можем затем использовать имеющийся в Python способ пошагового перемещения по списку для работы с каждым элементом. Чтобы вам стало понятнее, данные переменной walklist должны выглядеть примерно так:

('/usr/share/music/Blonde_Redhead', ['1980 Forward', '23'], [])
 ('/usr/share/music/Blonde_RedHead/1980 Forward', [], ['18-Magic Mountain.mp3'])
 ('/usr/share/music/Blonde_Redhead/23', [], ['10-My Impure Hair.mp3','1-23.mp3', '7-Publisher.mp3', '3-The Dress.mp3', '6-Silently.mp3'])

Теперь пошагово проверим каждую строку. Если элемент строки является файлом (одним или несколькими, в виде списка), мы хотим выполнить с ним определенные действия. Оператор if проверяет, имеется ли что-нибудь в каталоге, а если имеется, мы продолжим перемещаться по списку файлов тем же способом. Что делать с файлами, пока не совсем ясно, поэтому просто распечатаем список в стандартный вывод. Здесь мы воспользуемся другой полезной функцией модуля os – os.path.join(). Она соединяет вместе все компоненты пути к файлу и записывает их в виде, соответствующем текущему модулю os, что, несомненно, гораздо лучше, чем простое соединение вхождений с помощью слэша /, как это обычно делается в Linux!

Я также добавил здесь еще одну ветку else – на случай, если мы захотим что-то проделать и с каталогами (может, переименовать?). Но для начала просто выполните данный скрипт, чтобы проверить, работает ли он в вашей системе. Главное – не забудьте указать именно тот каталог, где действительно имеются MP3-файлы.

Проверка типов файлов

Ну вот, теперь самое время добавить новые функции в данный скрипт. Но прежде чем приняться за файл, я предлагаю убедиться, что он действительно музыкальный! Думаю, мы можем смело предположить, что все MP3-файлы имеют расширение mp3. Таким образом, в нашем цикле мы могли бы проверить это и пометить файлы, не соответствующие данному критерию. Вероятно, мы могли бы просто включить такую проверку внутри цикла, уже осуществляемого оператором if, но так как проверка может потребоваться не одна, гораздо разумнее присвоить расширение переменной и затем проверять ее. Для извлечения расширения воспользуемся простым и мощным способом обработки строк и переменных других типов в Python (подробнее – см. врезку справа вверху). Добыв расширение файла, можно проверить, не совпадает ли оно с другими форматами музыкальных файлов, вроде '.wma' и '.ogg' – на случай, если мы захотим поработать и с такими файлами. В Python нет оператора case:; придется нагромоздить последовательность операторов if, elif (else if) и else. Символ # в Python указывает, что оставшаяся часть строки – комментарий. В итоге наш основной цикл примет такой вид:

for file in dirs[2]:
   fullpath = os.path.join(dirs[0],file)
   extension = file[-4:]
   if (extension!='.mp3'):
     # Это не MP3-файл; проверить на другие расширения?
     if (extension =='.wma'):
       wmalist.append(fullpath)
     elif (extension =='.ogg'):
       ogglist.append(fullpath)
     else:
       unknownlist.append(fullpath)
   else:
     # Это MP3-файл.
     # Что-нибудь с ним сделаем.
     print fullpath

Мы также должны объявить три переменных типа «список» в основном коде. Возможно, вы захотите добавить код, обрабатывающий файлы, или указать другие варианты расширений. Для полноты картины, создадим также список неизвестных типов файлов, найденных в нашем каталоге – чисто информативный; заодно он поможет обнаружить то, что мы упустили. Теперь займемся MP3-файлами. Спецификация ID3v1 весьма незамысловата: в конец файла просто добавляется 128 байт информации о файле, и легко создать синтаксический анализатор для сбора этих данных и дальнейшей обработки. Однако ID3v2 придерживается других стандартов: информация заносится в начало файла, в виде сложной структуры с переменной длиной данных, и, если честно, нам пришлось бы изрядно разрастить наш маленький скрипт, чтобы с ней справиться. Поэтому мы поступим, как все нормальные программисты: схитрим! Существует множество готовых модулей Python, способных читать данные MP3-тэгов, вот и возьмем один из них. Рекомендую EyeD3 – это признанный фаворит, и в нем есть все, что нам нужно. Вы найдете его на DVD, прилагаемом к данному журналу, а на сайте http:// eyed3.nicfit.net может оказаться более свежая версия. Данный модуль творит всяческие чудеса; некоторые из них нам даже и трогать незачем. Вы можете почитать о нем подробнее на указанном сайте, но для наших целей достаточно импортировать модуль и ознакомиться с функциями, которые нам пригодятся. К счастью, модуль eyeD3 не относится к числу сложных. Опасения внушает только один объект – eyeD3.Tag, содержащий структуру тэгов ID3v1 и ID3v2. Чтобы заполнить его данными из вашего файла, вы должны использовать метод объекта link с соответствующим именем файла, и выглядит это примерно так:

mytag=eyeD3.Tag()
 mytag.link('/usr/share/music/Blonde_Redhead/23/1-23.mp3')
 print mytag.getArtist()
 print mytag.getAlbum()
 print mytag.getTrackNum()

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

  • Получить расширение файла
  • Он заканчивается на .mp3?
  • Проверить, действительно ли это MP3-файл
  • Прочитать тэги версии 2
  • Прочитать тэги версии 1
  • Если имеется только один тип, копировать в другой тип
  • Если тэгов вообще нет, установить пометку
  • Создать нормальное имя файла из тэгов
  • Предложить/записать новое имя файла
  • Проверить, соответствует ли название альбома имени каталога
  • Если это не MP3-файл, то что? Добавить в соответствующий список

Как сказано выше, впишем это в скрипт в виде комментариев. Обычно приступать к делу лучше с первого пункта, чтобы от вас не ускользнули логика действий и структура скрипта. Особо хитрые участки – или участки, используемые многократно – можно вынести в отдельную функцию. Существуют мнения и за и против этого, и программисты нередко обсуждают, что допустимо выделять в отдельную функцию. Теория выглядит примерно так: чем больше вы выносите из основного цикла программы, тем более очевидной становится общая задача. Но есть и контрдовод: чем больше вы включаете подобных обобщений, делая программу максимально простой и понятной, тем сложнее потом добраться до работы отдельных функций. Резюме: поскольку вы пишете программу исключительно для личного пользования, делайте так, как вам удобнее!

Проверка типов файлов – 2

Теперь пора и делом заняться. Прежде всего, получим расширения файлов через слайсинг Python'а, а затем проверим, MP3 это или нет. После чего употребим модуль eyeD3, чтобы распознать, действительно ли файл соответствует заявленному формату.

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

В представленном ниже коде можно встретить так называемые функции «санации» (sanitize). Присутствие их тела в основном коде сделало бы его неудобочитаемым, кроме того, иногда их бывает нужно вызывать из разных мест. Наконец, так с их реализацией можно разо- браться и позже, не затрагивая основной код.

if (extension !='.mp3'):
   # Не файл MP3? Проверить на другие расширения?
   if (extension == '.wma'):
     wmalist.append(fullpath)
     # logger.warn("silly wma file %s",fullpath)
   elif (extension == '.ogg'):
     ogglist.append(fullpath)
   else:
     unknownlist.append(fullpath)
 else:
   # Это вправду MP3-файл?
   if eyeD3.isMp3File(fullpath):
     # Прочитать тэг ID3V2
     tag2 = eyeD3.Tag()
     tag1 = eyeD3.Tag()
     a = tag2.link(fullpath,eyeD3.ID3_V2)
     b = tag1.link(fullpath,eyeD3.ID3_V1)
     if b and not a:
       # Имеется только тэг ID3v1
       print "version1 only"
       # Создать информацию для tag2 по tag1
       print fullpath
       artist = tag1.getArtist()
       album = tag1.getAlbum()
       title = tag1.getTitle()
       print artist,album,title
       tag1.update(eyeD3.ID3_V2)
       # check tags are cool
     elif a and not b:
       # Имеется только тэг ID3v2
       print fullpath
       artist = tag2.getArtist()
       album = tag2.getAlbum()
       title = tag2.getTitle()
       print artist,album,title
       try:
         tag2.update(eyeD3.ID3_V1_1)
       except UnicodeEncodeError:
         logger.error("tag invalid for v1.1 in file %s", fullpath)
     elif a and b:
       # Имеются оба тэга
       logger.info( "both versions fine %s", fullpath)
     else:
       # Тэгов нет вообще
       logger.warn('this file has no tags! %s', fullpath)
       error_flag = True
       # М.б. удастся вытянуть что-нибудь из имени каталога, где сидит файл!
     if not error_flag:
       # Пускай имя файла будет
       # number-name.mp3
       title=tag2.getTitle()
       title = title.replace(' ','_')
       n=tag2.getTrackNum()
       # Номер (number) у нас есть
       ns = str(n[0])
       if len(ns)==1:
           ns= '0'+ns
     ns=ns+'-'+title+'.mp3'
     # Уберем нехорошие символы
     ns=sanitize(ns)
     if (file!=ns):
       logger.info("change filename suggested for %s, to %s!",filename,ns)
       os.rename(fullpath, os.path.join(dirs[0],ns)
   else:
     # Выдадим предупреждение
     logger.warn(" file %s does not seem to be a valid mp3 file",fullpath)

Полная распечатка этого небольшого скрипта представлена на DVD данного номера Linux Format. Но будьте осторожны – скрипт не завершен! Нельзя гарантировать, что он не испортит вашу музыкаль- ную коллекцию! Особенно следует обратить внимание на проверку символов в кодировке Unicode, нередко встречающихся в тэгах ID3v2 (недопустимых в тэгах первой версии). Также вы, возможно, захотите обработать и файлы формата .ogg.

Здесь не доделано также множество проверок на ошибки. В случае с тэгами в кодировке Unicode поможет структура try: ... except:; она же пригодится и для других случаев (например, выяснении, как поступать с музыкальными файлами без прав доступа на запись). Но я надеюсь, что рассмотренный в статье пример показал вам, как просто создавать полезные скрипты!

Будем резать! (врезка)

В Python используются индексы для «нарезания на кусочки» (иногда говорят «слайсинг», от англ. slicing) строк, списков и других типов переменных. Пусть у нас имеется запись plop. mp3; мы можем запросто извлечь из нее любой нужный нам бит. Откройте терминал, запустите Python для входа в интерактивный режим и попробуйте следующие команды:

>>> string = 'plop.mp3'
>>> print string[1]
l
>>> print string[:2]
pl
>>> print string [-0]
p
>>> print string [-1]
3
>>> print string [-4:]
.mp3
>>>

В последнем операторе print из строки выхватываются четыре последних символа. Отрицательные индексы отсчитываются с конца строки, но будьте внимательны: -0 означает

Журналирование (врезка)

Небольшие скрипты часто нашпиговывают множеством выводов промежуточных данных на печать, чтобы знать, что и как происходит. Это действительно удобно; но иногда хочется превратить строку с оператором печати в комментарий, потом снова её подключить, и т.д. Проще всего в таком случае использовать встроенный в Python модуль ведения журнала! Он умеет выводить сообщения и в консоль, но способен различать сообщения по значимости, и вы сможете гибко регулировать количество выводов на консоль путём правки всего одной строки кода скрипта. Добавьте этот модуль к списку импортируемых и впишите следующий код в начало вашего скрипта:

logger = logging.getLogger("pymusic")
 # Создать обработчик консоли
 handler = logging.StreamHandler()
 # Создать форматирование и настроить обработчик на его использование
 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
 handler.setFormatter(formatter)
 # Добавить обработчик к модулю ведения журнала
 logger.addHandler(handler)
 logger.setLevel(logging.WARN)

Часть этих штук смахивает на волшебство, и кое-кто считает модуль ведения журнала непостижимым, но на самом деле все очень просто. Если вы хотите узнать больше, документация находится здесь: http://docs.python.org/lib/module-logging.html. Фактически, всё, что необходимо знать – это набор стандартных команд для управления выводом сообщений в консоль. Например, следующий код:

logger.warn(" file %s does not seem to be a valid mp3 file",fullpath)

выведет в консоль только сообщения, относящиеся к предупреждениям типа WARN или ниже (порядок такой: Critical, Error, Warning, Info, Debug). Можно установить и собственные уровни, но мы займёмся этим в другой раз... то же самое, что и просто 0, а именно – начало строки! (Индексы в Python нумеруются с 0.)

Форматирование (врезка)

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

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию