Kodomo

Пользователь

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

Разбор домашних заданий

Создайте модуль utils, в который положите функции tokenize, token_types (и token_type, который для неё нужен). Напишите программу, которая пользуется модулем utils, читает и токенизирует файл words.txt, и выводит на экран построчно: токен и его тип.

В файл utils.py копируем соответствующие функции из конспектов:

   1 def tokenize(text):
   2     chunks = text.split()
   3     tokens = []
   4     for chunk in chunks:
   5         token = chunk.strip(u".,?!\"'-+")
   6         if token != u"":
   7             tokens.append(token)
   8     return tokens
   9 
  10 def token_type(token):
  11   if token.isalpha():
  12     if token.isupper():
  13       return "upper"
  14     if token.islower():
  15       return "lower"
  16     if token.istitle():
  17       return "title"
  18     return "mixed"
  19   else:
  20     if token.isalnum():
  21       return "alnum"
  22     else:
  23       return "other"
  24 
  25 def token_types(tokens):
  26     types = []
  27     for token in tokens:
  28         types.append(token_type(token))
  29     return types

После этого создаём нашу программу, например, tokenize.py, и пишем:

   1 import codecs
   2 import utils
   3 
   4 with codecs.open("words.txt", "r", "utf-8") as infile:
   5     text = infile.read()
   6 
   7 tokens = utils.tokenize(text)
   8 types = utils.token_types(tokens)
   9 
  10 for token, type in zip(tokens, types):
  11     print token, type

Напишите скрипт, который открывает файл words.txt, токенизирует, считает частоты слов, и выводит ответ в виде CSV-таблицы с двумя колонками: токен, частота.

Дописываем в конец модуля utils.py функцию counts_v4 из конспекта:

   1 def counts(words):
   2     counts = {}
   3     for word in words:
   4         if word in counts:
   5             counts[word] = counts[word] + 1
   6         else:
   7             counts[word] = 1
   8     return counts

И создаём новый скрипт, который пользуется этим модулем:

   1 import codecs
   2 import utils
   3 
   4 with codecs.open("words.txt", "r", "utf-8") as infile:
   5     tokens = utils.tokenize(infile.read())
   6 
   7 counts = utils.counts(tokens)
   8 
   9 with codecs.open("word-counts.csv", "w", "utf-8") as outfile:
  10     for token in counts:
  11         frequency = counts[token]
  12         line = "{0};{1}".format(token, frequency)
  13         outfile.write(line)

Напишите функцию parse_exceptions, которая получает на вход CSV-файл с ровно двумя колонками (не нужно проверять, что это так), в первой колонке токен, во второй – тип токена, и возвращает словарь, который по токену возвращает его тип.

Пишется по обазу и подобию функций для разбора CSV-файла, и функций для преобразования списка.

   1 def parse_exceptions(file):
   2     result = {}
   3     for line in file:
   4         line = line.rstrip()
   5         cells = line.split(";")
   6         key = cells[0]
   7         value = cells[1]
   8         # всё тело цикла выше можно сократить до одной строки:
   9         # key, value = line.rstrip().split(";")
  10         # хотя на самом деле её поведение будет чуть-чуть отличаться -- вопрос на засыпку: в чём?
  11         result[key] = value
  12     return result

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

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

   1 print parse_exceptions(["a;b\n", "c;d"])

4

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

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

Решается почти дословно так же, как и третья:

   1 def parse_dictionary(file):
   2     result = {}
   3     for line line file:
   4         cells = line.rstrip().split(";")
   5         result[cells[0]] = cells[1:]
   6     return result

Лучше всего дописать этот код туда же в utils.py.

Напишите функцию definition_pairs, которая принимает на вход словарь (ключ – определяемое слово, значение – список слов-определений) и возвращает список пар определяемое слово – определение.

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

   1 def definition_pairs(dictionary):
   2     result = []
   3     for term in dictionary:
   4         for definition in dictionary[term]:
   5             pair = (term, definition)
   6             result.append(pair)
   7     return result

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

И снова лучше бы дописать этот код туда же в utils.py.

Напишите функцию filter_counts, которая получает на вход словарь (ключ – текст находки, значение – количество находок) и число – минимальное количество находок, и возвращает список тех находок, которые встретились не меньше заданного количества раз.

Когда-то мы такое уже писали, с той маленькой разницей, что фильтровали не словарь, а список:

   1 def filter_counts(counts, limit):
   2     result = []
   3     for pair in counts:
   4         if counts[pair] >= limit:
   5             result.append(pair)
   6     return result

Как и прошлые задачи, пишем это в utils.py.

Соберите из решений задач 2, 5, 6, 7, решение задания, которое я давал в начале семестра на автомат.

Итого, у нас в utils.py есть полезного: parse_dictionary, definition_pairs, counts, filter_counts.

   1 import codecs
   2 import utils
   3 
   4 # Нам нужно работать с многими файлами, положим их имена в список:
   5 filenames = [...]
   6 min_dictionaries = 3 # количество словарей, которые должны поддержать одно определение
   7 max_definitions = 3 # количество определений из одной словарной статьи, которые мы используем
   8 
   9 # Не разделённого на маленькие кусочки осталось ещё два шага.
  10 
  11 # Шаг - где-то в середине: в прочитанном словаре нужно оставить только первые N слов определения:
  12 def truncate_definitions(dictionary, max_definitions):
  13     result = {}
  14     for term in dictionary:
  15         definition = dictionary[term]
  16         result[term] = definition[:max_definitions]
  17     return result
  18 
  19 # Шаг 1: нужно прочитать все файлы и получить из них все пары определений:
  20 def read_pairs(filenames):
  21     pairs = []
  22     for filename in filenames:
  23         with codecs.open(filename, "r", "utf-8") as infile:
  24             dictionary = utils.parse_dictionary(infile)
  25             pairs = pairs + utils.definition_pairs(dictionary)
  26     return pairs
  27 
  28 # Шаг 2: найти только поддержанные пары:
  29 def supported_pairs(pairs):
  30     counts = utils.counts(pairs) # магия: наша функция counts умеет считать не только слова!
  31     return filter_counts(counts, min_dictionaries)
  32 
  33 # Осталось только вызвать нужные функции. Делаем это прилично:
  34 if __name__ == "__main__":
  35     pairs = supported_pairs(read_pairs(filenames))
  36     for term, definition in pairs:
  37         print term, definition

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

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

Регулярные выражения – это способ обозначать множество строк. Он отличается от простого перечня строк:

Выражение

Синонимы

Комментарий

Примеры

Vanya

Выражение обозначает одну строку: Vanya

+ Vanya
- Vania

Van[yj]a

В квадратных скобочках указываются символы, которые могут стоять в данной позиции. При этом можно использовать тире для того, чтобы указывать последовательность символов, например: [0-9] (все цифры), [a-z] все маленькие буквы, [a-zA-Z] (все буквы)

+ Vanya
+ Vanja
- Vania

Van[^ji]

Если первым символом после открывающей квадратной скобочки является ^, то в квадратных скобочках перечисляются символы, которые НЕ могут стоять в данной позиции, т.е. остальные все символы, кроме этих, могут.

+ Vanya
+ Vanna
- Vanja
- Vania

Van{3}ya

Vannnya

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

+ Vannnya
- Vanya

Van{1,3}ya

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

+ Vannnya
+ Vanya
- Vaya
- Vannnnya

Van{2,}ya

Если максимальное количество раз пропущено, то это значит что количество повторений сверху не ограничено и может быть бесконечным

+ Vannya
+ Vannnnnya
- Vanya

Van?ya

Van{0,1}ya

Знак вопроса означает, что непосредственно предшествующий символ необязательный

+ Vaya
+ Vanya
- Vannya

Van*ya

Van{0,}ya

Звездочка означает 0 или большее количество повторений непосредственно предшествующего символа

+ Vaya
+ Vanya
+ Vannya

Van+ya

Van{1,}ya

Плюсик означает 1 или большее количество повторений непосредственно предшествующего символа

- Vaya
+ Vanya
+ Vannya

Va.ya

Точка означает любой символ, кроме символа конец строки (\n)

+ Va.ya
+ Vanya
- Vaya

Va\.ya

Backslash «защищает» символ, который следует за ним, т.е. делает символ обычным.

+ Va.ya
- Vanya
- Vaya

(Va)+nya

Скобочки используются для группировки

+ Vanya
+ VaVanya
- Vavanya

Vanya|Kolya

Выбор одного из нескольких вариантов

+Vanya
+ Kolya
- Roma

Van(ya|e)

Выбор одного из нескольких вариантов и круглые скобки

+Vanya
+ Vane
- Vani

Пояснение про конкатенацию

Когда мы пишем два регулярных выражения вплотную, мы совершаем между ними операцию конкатенации (та же самая конкатенация, которую делает операция плюс со строками в питоне – "a" + "b"). Конкатенация двух регулярных выражений обозначает следующее: мы берём каждый возможный вариант, подходящий под левое выражение и присоединяем к нему каждый возможный вариант, подходящий под правое выражение. Например: выражение 1|2 описывает строки 1 и 2; выражение a|b описывает строки a и b; выражение (a|b)(1|2) описывает строки a1, a2, b1, b2.

Несколько примеров

Чтобы обозначить множество всех дат, записанных через чёрточку, мы можем написать: [0-9]{2}-[0-9]{2}-{0-9]{4} – эта запись довольно простая (и примерно такими простыми записями я рекомендую вам пользоваться в жизни), но при этом в нашем множестве окажутся наряду с вполне допустимыми (например 24-10-2013), ещё и такие немыслимые даты, как 99-42-0000.

На примере номеров месяцев: мы можем заменить [0-9]{2} на множество чисел от 1 до 12. 0?[1-9]|1[0-2]. В этом множестве есть строки 01, 12, 9, но нет строк 001, 012, 13, 0, 99. То есть некоторым усложнением выражения мы можем добиться того, чтобы более точно соответствовать нашим пожеланиям. Во многих случаях не стоит увлекаться тем, чтобы слишком детализировать критерии искомого множества.

Ещё один пример. В самом грубом приближении задачу описания множества всех мнимых английских имён собственных мы можем решить так: [A-Z][a-z]+ – то есть просто как любое слово, начинающееся с большой буквы и содержащее не меньше одной маленькой буквы дальше.

Литература