Журнал LinuxFormat - перейти на главную

LXF79:Python

Материал из Linuxformat
Перейти к: навигация, поиск
Python
Python для профессионалов
Python + PyGame
Python + Web
Python + Clutter

ЧАСТЬ 5 Мы долго изучали различные компоненты Pyhton, и теперь пришло время сделать что-то понастоящему полезное. Сегодня мы вместе с Сергеем Супруновым перейдем к одному очень важному с практической точки зрения применению языка Python – обработке текста.

Содержание


Пожалуй, для языков программирования сценариев типа Perl и Python обработка строк является одним из основных применений. Выбрать статистику из log-файла, разобрать «по косточкам» пользовательский ввод, проанализировать содержащиеся в таблице dbf данные… В общем, задачи, как полезные, так и не очень, можно придумывать бесконечно.

Так уж исторически сложилось, что «мы говорим: обработка текста – подразумеваем Perl, мы говорим Perl – подразумеваем: обработка текста». По крайней мере, это любят повторять бородатые администраторы. Сегодня я постараюсь показать, что и Python справляется с этим ничуть не хуже, сохраняя при этом все свои преимущества.

Строка как объект

Начнём с более подробного рассмотрения строки как объекта, которому присущи определённые методы. С некоторыми операциями мы уже сталкивались, рассматривая типы данных. Раньше работа со строками велась преимущественно силами модуля string. Например, разбиение строки на слова (используя в качестве разделителя пробел) выполнялось примерно так:

 >>>  import string
 >>>  str = "Строка символов как набор слов"
 >>>  for s in string.split(str, " "): print s
...
 Строка
 символов
 как
 набор
 слов

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

 >>> str = "Строка символов как набор слов"
 >>> for s in str.split(" "): print s
...
 Строка
 символов
 как
 набор
 слов

Как видите, намного удобнее. Кстати, вы уже поняли, как работает метод split()? Всё правильно – принимая в качестве параметра строку разделитель (в нашем случае это пробел), он возвращает список подстрок, разделённых разделителем (извините за каламбур). Сам разделитель в итоговые подстроки не входит. Мы могли вообще не указывать параметр, поскольку пробел используется по умолчанию: str.split() вернёт тот же результат.

Рассмотрим ещё несколько примеров:

 >>> a = 'just a test line'
 >>> a.count('t')
 3
 >>> a.center(25)
' just a test line '
 >>> a.rjust(30)
' just a test line'
 >>> '-'.join(a.split())
'just-a-test-line'
 >>> a.title()
'Just A Test Line'
 >>> a.title().swapcase()
'jUST a tEST lINE'

Ещё не забыли, как получить полный список доступных методов? Всё правильно – наша любимая функция: dir('qwe').

Входит и выходит

Говоря о строках, коротко остановимся на вводе данных и форматировании вывода. И с тем, и с другим мы уже сталкивались, но не вдавались в подробности.

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

>>> a = input("Введите число: ")
Введите число: 5
>>> print a
5
>>> a = input("Введите число: ")
Введите число: число
Traceback (most recent call last):
 File "<stdin>", line 1, in ?
 File "<string>", line 1
 число
 ^
SyntaxError: invalid syntax

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

Теперь – пара слов о форматированном выводе. Использование знакомест %s – это его зачатки. На самом деле, возможности данного механизма гораздо богаче:

>>> a = 0
>>> b = "один"
>>> c = 2.34
>>> print "Значения: %d, %s, %f" % (a, b, c)
Значения: 0, один, 2.340000
>>> print "Три знака после запятой: %0.3f" % c
Три знака после запятой: 2.340
>>> print "Выравнивание вправо: %20s" % b
Выравнивание вправо: один

Можно также использовать так называемый «словарный» синтаксис. В качестве примера рассмотрим небольшой сценарий, анализирующий учётные записи пользователей:

#!/usr/bin/Python
fd = open('/etc/passwd', 'r')
line = 'none'
user = {}
while line != '':
 line = fd.readline()
 if line != '' and line[0] != '#':
  tmp = line.split(':')
  user['login'] = tmp[0]
  user['uid'] = tmp[2]
  user['homedir'] = tmp[5]
  print "| %(login)-5s | %(uid)5s | %(homedir)-14s |" % user
fd.close()

Как видите, можно создавать именованные знакоместа вида %(key)s, где key – ключ словаря, который передаётся оператору print. Есть один интересный приём – вы всегда можете воспользоваться функцией vars(), которая вернёт словарь инициированных в данный момент переменных. То есть к переменной qwe вы можете обратиться и так: vars()['qwe']. Как следствие, вставить значение переменной qwe в вывод оператора print можно таким образом: print "%(qwe)s" % vars().

Некоторые описатели

\A – начало строки

\Z – конец строки

\b – граница слова

\d – любая цифра

\s – пробельный символ

\w – цифра или буква

Знак «минус» перед числом, указывающим ширину текстового поля, означает выравнивание по левому краю (по умолчанию используется выравнивание вправо).

Результат работы приведённого выше сценария будет выглядеть примерно так:

serg$ ./py3.py
| root | 0 | /root |
| www | 80 | /nonexistent |
| ftp | 21 | /home/ftp |
| oops | 5002 | /nonexistent |
| dspam | 8080 | /var/db/dspam |

Развивая наш сценарий, можно добавить «шапку» таблицы, и т.д. Оставлю это вам в качестве упражнения.

Регулируем выражения

Безусловно, никакие методы строк не сравнятся по своей мощи с регулярными выражениями. В языке Python за работу с ними отвечает модуль re (когда-то это был самостоятельный модуль, сейчас – просто «обёртка» к более мощному модулю sre).

Говоря простым языком, регулярное выражение (РВ) – это специальная строка, описывающая группу строк, имеющих определённый формат. По сути, РВ – это шаблон, которому может соответствовать несколько различных строк. Например, выражение «[a-z]» описывает строки, состоящие из одной строчной буквы латинского алфавита. Специальные символы, использующиеся в регулярных выражениях, можно разделить на три группы: описатели, квантификаторы и символы группировки.

Описатели – это выражения, описывающие определённый набор символов, которые могут им соответствовать. Например, «[0-9]» описывает любую цифру, «\s» – любой пробельный символ, «.» – любой символ. Просто символ, не принадлежащий специальным или экранированный обратным слэшем (например, «m», «\[») описывает сам себя.

Символы группировки – круглые скобки – позволяют объединить несколько описателей в группу, которая будет рассматриваться как единое целое.

Наконец, квантификаторы задают число повторений символов, соответствующих описателю или группе, после которых он задан. Например, «.*» означает любое (в том числе, нулевое) количество любых символов, «\d+» требует наличия как минимум одной цифры, выражению «([a-z][0-9]){2,4}» будет соответствовать строка, в которой группа из буквы и цифры повторяется от двух до четырёх раз – f5e3, o2o2o2o2.

Не все так просто

Стоит отметить, что вполне рядовая на первый взгляд задача поиска адресов e-mail на самом деле очень и очень сложна. Стандарт RFC822, определяющий формат адреса электронной почты, предусматривает произвольную глубину вложенности комментариев (которые заключаются в круглые скобки) – подобные конструкции регулярным выражениям просто не по зубам. Полный код регулярного выражения, ограниченного одним-единственным уровнем вложенности комментариев, содержит 4724 символа! Подробности можно найти в замечательной книге Дж. Фридла «Регулярные выражения. Библиотека программиста», выпущенной издательством O'Reilly и переведенной на русский язык издательством «Питер».

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

Жадность квантификатора можно поубавить, поставив после него символ «?». Например, «.+?.+?» будет вести себя уже по-другому – первый «+» заберёт себе один символ (по принципу, «а мне больше и не надо»), а всё остальное придётся «кушать» второму квантификатору.

Казалось бы, не всё ли равно, кто сколько символов захватит? Главное ведь чтобы результат был одинаковый? Если нам нужно просто определить соответствие, то, конечно, разницы никакой. Но если мы используем группировку для последующей обработки фрагментов (см. пример ниже), то о «жадности» забывать нельзя.

Однако хватит теории (о регулярных выражениях много и хорошо написано в различных книгах, статьях и на man-страницах). Вернёмся к модулю re.

Наиболее типичный способ его использования – создание объекта-шаблона с помощью метода re.compile() с последующим его применением к конкретным строкам с помощью методов re.match(), re.search(), re.sub() и т.д. Методы match() и search() отличаются тем, что первый требует соответствия РВ с начала строки, а второй проверяет наличие подходящего фрагмента в любом месте строки:

 >>> a = re.compile('\d+')
 >>> print a.search('qwe123') or "Нет соответствия"
 <_sre.SRE_Match object at 0x81bbd78>
 >>> print a.match('qwe123') or "Нет соответствия"
 Нет соответствия
 >>> print a.match('123qwe') or "Нет соответствия"
 <_sre.SRE_Match object at 0x81bbd78>

Как видите, в случае соответствия как match(), так и search() возвращают объект класса SRE_Match. Получить фрагмент, удовлетворяющий РВ, можно с помощью метода group() этого объекта:

 >>> print a.search('qwe123qwe').group()
 123

В данном примере мы «вырезали» число из строки (чему при желании можно найти достаточно много практических применений).

Метод sub(текст_замены, строка) выполняет замену соответствующих шаблону фрагментов заданной строкой:

 >>> a = re.compile('\d+')
 >>> print a.sub('несколько', 'Стоит 50 рублей')
 Стоит несколько рублей

Пример использования ещё одного полезного метода – findall() – вы найдёте в рассматриваемом ниже сценарии. Ну а в том, как работает split(), думаю, разберётесь сами.

Некоторые квантификаторы

* – любое количество фрагментов

+ – один и более фрагментов

? – ноль или один фрагмент

{m,n} – не меньше m и не больше n

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

  • обязательное наличие символа «@» внутри фрагмента;
  • до «@» может стоять любое число латинских букв, цифр и символов «точка»;
  • после «@» должно находиться не менее двух групп латинских букв, цифр и дефисов, разделённых точками; причём последняя такая группа может содержать от двух до четырёх букв (другие символы не допускаются).
  • к регистру символов придираться не будем.

Пример «правильного» фрагмента: Vasya.Pupkin@gdeto-tam.D12.info. Записать наши требования можно с помощью такого выражения:

 ([\w.]+@([\w-]+\.)+[a-z]{2,4})\W

Завершающий определитель \W нужен для того, чтобы после последней группы обязательно стоял не цифро-буквенный символ (в противном случае qwe@qwe.qwerty будет соответствовать РВ, т.к. квантификатор возьмёт себе необходимые 4 символа, а остальное просто не будет приниматься во внимание). Нужный же нам адрес будет форми роваться в первой группе (внешние скобки).

Внимательные читатели, думаю, уже заметили, что нашему шаблону будет соответствовать и такой «адрес»: ...@-----.-.ru. Небольшим усложнением нашего «фильтра» эту проблему можно решить – попробуйте на досуге.

Базовая версия нашего сценария выглядит следующим образом:

 #!/usr/bin/Python
 # -*- coding: koi8-r -*-
 import re
 def getmail(text):
     filter = re.compile(r'([\w.]+@([\w-]+\.)+[a-z]{2,4})\W', re.I)
     for i in filter.findall(text):
         print i[0]
 
 example = '''\
Этот текст служит для проверки работы функции getmail.
Он должен нормально распознавать такие адреса как vasya@mail.ru, Vasya.Pupkin@gde-to.tam.D12.info, u_12@_test.me и 12.34@56-78.90.ua.
Адреса вида @mail.ru, mail@mail и qwe@domain.qwerty и qwe@ma.ru123 должны игнорироваться.
К сожалению, здесь не решена проблема адресов вида: ...@---.--ru.
В принципе, исправить это не сложно.
'''
getmail(example)

Результат его работы:

serg$ ./getmail.py
vasya@mail.ru
Vasya.Pupkin@gde-to.tam.D12.info
u_12@_test.me
12.34@56-78.90.ua
...@---.--.ru

Вторым параметром метода compile() задаются флаги, в частности, флаг re.I (re.IGNORECASE) отключает проверку регистра символов. Дополнительных пояснений, думаю, не требуется. Замечу лишь, что findall() возвращает в кортеже все найденные группы в порядке обнаружения первой скобки. Таким образом, первым элементом и будет искомый адрес электронной почты. Ещё обратите внимание на то, что перед строкой регулярного выражения стоит буква «r». Она говорит о том, что последующую строку следует рассматривать как «сырую», то есть не обрабатывать специальные символы, экранированные обратным слэшем.

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

Персональные инструменты
купить
подписаться
Яндекс.Метрика