Это последняя статья из цикла про лексический анализ текста. Вот краткое содержание того, что мы уже сделали в первой части:
- загрузили все тома «Войны и мира» в Python;
 - очистили текст от запятых, предлогов и переносов строк;
 - установили и настроили библиотеку NLTK для работы с текстом;
 - убрали из текста русские и французские стоп-слова (например, артикли и предлоги);
 - нашли самые частые слова в каждом томе;
 - нарисовали облако слов по популярности — чем чаще встречается слово, тем крупнее оно написано.
 
И во второй:
- привели каждое слово в тексте к нормальной форме;
 - построили облака слов для каждого тома;
 - нашли общие самые частые слова, которые есть в каждом томе;
 - убрали те, слова, что есть в каждом облаке, чтобы они не мешали найти суть происходящего в книгах;
 - нарисовали новые облака слов, которые отражают уникальный сюжет каждой книги.
 
В конце второй части мы дошли до сути первого тома «Войны и мира» — нашли самые частые слова, которые описывают сюжет:
Что делаем сегодня
Сегодня мы поиграем с данными и извлечём ещё кое-какие знания из текстов Толстого:
- разделим существительные, прилагательные и глаголы;
 - каждую такую группу слов сохраним в отдельном списке и построим для каждой своё облако тегов;
 - оптимизируем код так, чтобы всё это можно было сделать за один проход.
 
Смысл в том, что существительные нам покажут ключевые элементы, вокруг которых вращается сюжет, прилагательные покажут эмоциональную окраску и характеристики героев, а глаголы — основное действие, которое происходит в книге.
Оптимизируем код
Так как нам нужно будет за один прогон получить три картинки с облаками слов, поменяем код так, чтобы это было сделать проще всего. Для этого нам нужно сделать две вещи:
- Собрать в одном месте импорты нужных библиотек.
 - Превратить в функцию тот фрагмент кода, где происходит подготовка и формирование облака слов.
 
Логика работы будет такая: формируем три списка слов и по очереди вызываем функцию создания облака. Благодаря функции нам не придётся делать спагетти-код и дублировать три раза подряд одно и то же, только с разными переменными.
Соберём все команды импорта в начало скрипта — просто найдём их в коде и перенесём в начало:
# подключаем встроенный модуль работы со строками
import string
# подключаем регулярные выражения
import re
# из библиотеки обработки текста подключаем модуль для токенизации слов
from nltk import word_tokenize
# подключаем библиотеку для нормализации слов
import pymorphy2
# подключаем библиотеку для работы с текстом
import nltk
# подключаем библиотеку для создания облака слов
from wordcloud import WordCloud
# и графический модуль, с помощью которого нарисуем это облако
import matplotlib.pyplot as plt
# подключаем статистику 
from nltk.probability import FreqDist
Теперь найдём в коде такую строчку:
# переводим токены в текстовый формат
text = nltk.Text(filtered_tokens)
Начиная с этой команды скрипт обрабатывает наш текст, чтобы потом на его основе сформировать готовую картинку с облаком. Так как нужные команды идут до конца скрипта, то мы сделаем так:
- Перед этой командой объявим новую функцию 
def text_cloud(). - В качестве параметра укажем переменную 
tokens— её у нас ещё нет, поэтому пусть будет как аргумент функции. - В этой функции меняем везде 
filtered_tokensнаtokens, чтобы функция могла универсально обработать любой список слов, который мы ей дадим. 
В итоге должен получиться такой код:
def text_cloud(tokens):
    # переводим токены в текстовый формат
    text = nltk.Text(tokens)
    # считаем слова в тексте по популярности
    fdist = FreqDist(text)
    # выводим первые 5 популярных слов
    print(fdist.most_common(5))
    # подключаем модуль со стоп-словами
    from nltk.corpus import stopwords
    # добавляем русские и французские стоп-слова
    russian_stopwords = stopwords.words("russian")
    russian_stopwords += stopwords.words("french")
    # перестраиваем токены, не учитывая стоп-слова
    text_tokens = [token.strip() for token in tokens if token not in russian_stopwords]
    # снова приводим токены к текстовому виду
    text = nltk.Text(text_tokens)
    # считаем заново частоту слов
    fdist_sw = FreqDist(text)
    # показываем самые популярные
    print(fdist_sw.most_common(10))
    # добавляем свои слова в этот список
    russian_stopwords.extend(['это', 'что','всё','который', 'свой','говорить','сказать','думать','человек','ещё','весь','лицо','время','мочь','знать','видеть'])
    # перестраиваем токены, не учитывая стоп-слова
    text_tokens = [token.strip() for token in text_tokens if token not in russian_stopwords]
    # снова приводим токены к текстовому виду
    text = nltk.Text(text_tokens)
    # считаем заново частоту слов
    fdist_sw = FreqDist(text)
    # переводим всё в текстовый формат
    text_raw = " ".join(text)
    # готовим размер картинки
    wordcloud = WordCloud(width=1600, height=800).generate(text_raw)
    plt.figure( figsize=(20,10), facecolor='k')
    # добавляем туда облако слов
    plt.imshow(wordcloud)
    # выключаем оси и подписи
    plt.axis("off")
    # убираем рамку вокруг
    plt.tight_layout(pad=0)
    # выводим картинку на экран
    plt.show()
Чтобы убедиться, что всё работает, добавим в самый конец скрипта команду:
text_cloud(filtered_tokens)
Сортируем слова
Чтобы разделить существительные, прилагательные и глаголы, нам понадобится дополнительный элемент в библиотеке pymorphy2 — свойство tag. В нём хранится дополнительная информация о слове — тип, число, род и другая информация. 
Например, если проанализированное слово хранится в переменной p, то проверить, существительное оно или нет, можно так:
if “NOUN” in p.tag:
    # это — существительное
Проще говоря, нам нужно проверить, есть ли нужное свойство среди всех свойств внутри тега. Чтобы разобраться, как в библиотеке кодируются разные части речи, идём на сайт с документацией библиотеки и смотрим таблицу «Часть речи»:
Чтобы мы сразу могли отправлять найденные части речи по своим спискам, сразу после добавления анализатора слов заведём новые переменные:
# новые переменные для существительных, прилагательных и глаголов 
noun_tokens = []
adjf_tokens = []
verb_tokens = []
Теперь переберём все слова в исходном тексте и сразу посмотрим, в какой список их отправлять. Для этого посмотрим, какие признаки слов есть в тегах, и в зависимости от этого примем решение:
# перебираем все слова в исходном тексте
for token in text_tokens:
    # получаем нормальную форму текущего слова
    p = morph.parse(str(token))[0]
    if "NOUN" in p.tag:
        # добавляем его в массив c существительными
        noun_tokens.append(p.normal_form)
    elif "ADJF" in p.tag or "ADJS" in p.tag:
        # добавляем его в массив c прилагательными
        adjf_tokens.append(p.normal_form)
    elif "VERB" in p.tag or "INFN" in p.tag:
        # добавляем его в массив c глаголами
        verb_tokens.append(p.normal_form)
Запускаем скрипт
Сейчас у нас есть отсортированные слова, но нет картинок — всё потому, что мы не отправили их на обработку в функцию text_cloud(). Исправим это и добавим в самый конец скрипта такие команды:
# вызываем по очереди функцию создания облака тегов для каждого списка
text_cloud(noun_tokens)
text_cloud(adjf_tokens)
text_cloud(verb_tokens)
Скрипт сначала сформирует первую картинку; когда мы её закроем, то покажет вторую, а когда закроем и это окно — появится третья.
Вот что у нас получилось в детальном рассмотрении первого тома «Войны и мира». С существительными у библиотеки всё более-менее хорошо:
А вот с прилагательными модуль дал сбой: «ваш» и «наш» он посчитал тоже прилагательными, как и Болконского. Их можно добавить в стоп-лист, но для чистоты эксперимента мы ничего не трогали:
С глаголами тоже всё хорошо — нет почти ни одного лишнего слова:
# подключаем встроенный модуль работы со строками
import string
# подключаем регулярные выражения
import re
# из библиотеки обработки текста подключаем модуль для токенизации слов
from nltk import word_tokenize
# подключаем библиотеку для нормализации слов
import pymorphy2
# подключаем библиотеку для работы с текстом
import nltk
# подключаем библиотеку для создания облака слов
from wordcloud import WordCloud
# и графический модуль, с помощью которого нарисуем это облако
import matplotlib.pyplot as plt
# подключаем статистику 
from nltk.probability import FreqDist
# открываем текстовый файл
f = open('tom1.txt', "r", encoding="utf-8")
# закидываем его содержимое в переменную
text = f.read()
# выводим начало, чтобы убедиться, что всё считалось правильно
print(text[:300])
# переводим символы в нижний регистр, чтобы всё было одинаково
text = text.lower()
# добавляем к стандартным знакам пунктуации кавычки и многоточие
spec_chars = string.punctuation + '«»\t—…’'
# очищаем текст от знаков препинания
text = "".join([ch for ch in text if ch not in spec_chars])
# меняем переносы строк на пробелы
text = re.sub('\n', ' ', text)
# убираем из текста цифры
text = "".join([ch for ch in text if ch not in string.digits])
# смотрим на результат
print(text[:300])
# токенизируем текст
text_tokens = word_tokenize(text)
# добавляем анализатор слов
morph = pymorphy2.MorphAnalyzer()
# новые переменные для существительных, прилагательных и глаголов 
noun_tokens = []
adjf_tokens = []
verb_tokens = []
# перебираем все слова в исходном тексте
for token in text_tokens:
    # получаем нормальную форму текущего слова
    p = morph.parse(str(token))[0]
    if "NOUN" in p.tag:
        # добавляем его в массив c существительными
        noun_tokens.append(p.normal_form)
    elif "ADJF" in p.tag or "ADJS" in p.tag:
        # добавляем его в массив c прилагательными
        adjf_tokens.append(p.normal_form)
    elif "VERB" in p.tag or "INFN" in p.tag:
        # добавляем его в массив c глаголами
        verb_tokens.append(p.normal_form)
def text_cloud(tokens):
    # переводим токены в текстовый формат
    text = nltk.Text(tokens)
    # считаем слова в тексте по популярности
    fdist = FreqDist(text)
    # выводим первые 5 популярных слов
    print(fdist.most_common(5))
    # подключаем модуль со стоп-словами
    from nltk.corpus import stopwords
    # добавляем русские и французские стоп-слова
    russian_stopwords = stopwords.words("russian")
    russian_stopwords += stopwords.words("french")
    # перестраиваем токены, не учитывая стоп-слова
    text_tokens = [token.strip() for token in tokens if token not in russian_stopwords]
    # снова приводим токены к текстовому виду
    text = nltk.Text(text_tokens)
    # считаем заново частоту слов
    fdist_sw = FreqDist(text)
    # показываем самые популярные
    print(fdist_sw.most_common(10))
    # добавляем свои слова в этот список
    russian_stopwords.extend(['это', 'что','всё','который', 'свой','говорить','сказать','думать','человек','ещё','весь','лицо','время','мочь','знать','видеть'])
    # перестраиваем токены, не учитывая стоп-слова
    text_tokens = [token.strip() for token in text_tokens if token not in russian_stopwords]
    # снова приводим токены к текстовому виду
    text = nltk.Text(text_tokens)
    # считаем заново частоту слов
    fdist_sw = FreqDist(text)
    # переводим всё в текстовый формат
    text_raw = " ".join(text)
    # готовим размер картинки
    wordcloud = WordCloud(width=1600, height=800).generate(text_raw)
    plt.figure( figsize=(20,10), facecolor='k')
    # добавляем туда облако слов
    plt.imshow(wordcloud)
    # выключаем оси и подписи
    plt.axis("off")
    # убираем рамку вокруг
    plt.tight_layout(pad=0)
    # выводим картинку на экран
    plt.show()
# вызываем по очереди функцию создания облака тегов для каждого списка
text_cloud(noun_tokens)
text_cloud(adjf_tokens)
text_cloud(verb_tokens)
Что дальше
Мы поработали с текстом на самом начальном уровне, в жизни обычно всё сложнее и глубже. Но даже этих знаний достаточно, чтобы, например, проанализировать тексты любимого блогера за последние пару лет и посмотреть, изменилось ли что в них или нет. А если изменилось — то что?
Если сделаете что-то подобное — поделитесь в комментариях, что у вас получилось и к каким выводам вы пришли.
