library(rvest)
library(tidyverse)
library(dplyr)
library(stringr)
library(xml2)
library(tokenizers)
library(udpipe)
library(wordcloud)
library(stopwords)
library(RColorBrewer)
library(DT)
# devtools::install_github("hadley/emo")
library(emo)
# udpipe_download_model(language = "russian-syntagrus")
<- udpipe_load_model(file = "russian-syntagrus-ud-2.5-191206.udpipe") syntagrus
Статистический анализ текстов песен
ДДТ vs Король и Шут
Создание датасета
Я буду обкачивать сайт AmDm («AmDm» 2024) – это сайт с аккордами к песням. Это неоднозначный выбор для подобного исследования.
Потенциальные недостатки:
вполне возможно, не для всех песен сделали аккорды, то есть не все песни представлены (но нам нужна по сути только некоторая выборка песен исполнителя, так что сильно это повлиять не должно);
нужно будет очищать текст от аккордов (эта проблема решилась относительно просто, но возникло несколько схожих, о них в разделе “Чистим данные”).
Но этот сайт не блокирует автоматические запросы, и на нём присутствует достаточно песен выбранных мной исполнителей, так что попробуем.
Установка библиотек
Парсим страницу исполнителя
Здесь нам понадобится две основные функции. Функция get_songdata
позволит нам извлечь из строки таблицы на сайте информацию о песне: название и ссылку.
<- function(song){
get_songdata
<- html_elements(song, "td")
song_data
# в каждой строке есть три столбца: название песни, есть ли к ней видео-разбор и количество просмотров
names(song_data) = c('song', 'video', 'views')
# название песни
<- html_text2(song_data['song'])
name
# ссылка на текст и аккорды к песне
<- song_data['song'] |>
link html_element("a") |>
html_attr("href")
return(tibble(title = name,
url = link))
}
Функция get_songlist
парсит страницу исполнителя и с помощью функции выше извлекает необходимые данные и складывает в датафрейм.
<- function(link){
get_songlist
<- read_html(link) |>
my_html html_element(".artist-profile-song-list") |> #выделили таблицу с информацией о песнях
html_elements("tr") #разделили на строчки
<- my_html[-1] #убрали первую строку таблицы (названия столбцов)
my_html
<- map_df(my_html, get_songdata)
songs_df
return(songs_df)
}
Парсим страницу с текстом
Теперь необходимо пройти по собранным ссылкам и собрать данные. Для этого создадим ещё пару функций.
Функция get_songtext
выкачивает текст со страницы, дополнительно проходя семь кругов ада множество этапов очищения. Amdm достаточно беспроблемно даёт себя обкачивать, но иногда может разорваться соединение, поэтому если песня вдруг не скачалась, просто идём дальше (выборка достаточно большая, можем пожертвовать и не скачивать всё заново).
<- function(link){
get_songtext
tryCatch(
expr = {
<- read_html(link) |>
song_html html_element(".b-podbor__text")
<- song_html |>
toRemove_style html_nodes(css = "style")
<- song_html |>
toRemove_chords html_nodes("div[class='podbor__chord']")
<- song_html |>
toRemove_format html_nodes("div[class='podbor-format']")
<- song_html |>
toRemove_script html_nodes(css = "script")
# удаляем то, что может прилипнуть к тексту: аккорды и код html-страницы
map(list(toRemove_style, toRemove_chords, toRemove_format, toRemove_script), xml_remove)
<- html_text2(song_html) |>
song_text str_replace_all("(\\n)|(\\t)|( +)", " ") |> # очищаем текст от лишних символов
str_replace_all("([а-я])([А-Я])", "\\1, \\2") |> # расклеиваем склеившиеся строчки
str_remove_all("(\\[.+?\\])|Вступление|Куплет|Припев|Интро|Проигрыш|Бридж|([a-zA-Z]+)") # дополнительно убираем слова, не входящие в текст песни
return(song_text)
},
error = function(e){
print("Не смог скачать песню :(")
return(" ")
}
) }
И наконец наша главная функция: get_data
принимает список песен, удаляет все дубликаты (так как это разборы аккордов, то для одной и той же песни может быть масса “дублей” с разными вариантами аккордов, нам нужно от них избавиться), извлекает все тексты и аннотирует их при помощи udpipe.
<- function(songs_df){
get_data
<- songs_df |>
songs_df mutate(title = tolower(title)) |>
mutate(title = str_replace_all(title, "ё", "е")) |>
mutate(title = str_replace_all(title, " ?\\(.+\\)", "")) |>
distinct(title, .keep_all = TRUE)
<- songs_df |>
songs_df_texts mutate(text = map(url, get_songtext)) |>
mutate(text = as.character(text))
# аннотируем
<- udpipe_annotate(syntagrus, songs_df_texts$text)
songs_annotate <- as_tibble(songs_annotate)
pos_data
return(pos_data)
}
Применим же все эти функции на практике! Скачаем песни ДДТ и Короля и Шута.
```{r}
ddt_df <- get_songlist("https://amdm.ru/akkordi/ddt/") |>
get_data()
kish_df <- get_songlist("https://amdm.ru/akkordi/korol_i_shut/") |>
get_data()
```
Или же можем загрузить скачанные загодя данные, чтобы не ждать ещё раз.
load(file="kish_df.RData")
load(file="ddt_df.RData")
И вот датасет готов? Но не совсем.
Чистим данные
Немного почистим наши таблицы, уберём ненужные столбцы и отфильтруем данные о пунктуационных знаках и т.п.
<- kish_df |>
kish_df select(token_id, token, lemma, upos, feats) |>
filter(!upos %in% c("PUNCT", "SYM", "NUM"))
<- ddt_df |>
ddt_df select(token_id, token, lemma, upos, feats) |>
filter(!upos %in% c("PUNCT", "SYM", "NUM"))
(как показали дальнейшие наблюдения, в текстах всё ещё много мусора, относящегося к аккордам и прочих заметок, но, вероятно, это всё-таки издержка работы с подобным сайтом, и чистить тут можно ещё очень долго, а домашку нужно сдать быстро…)
Визуализируем
Частотности различных частей речи
Для начала создадим вспомогательную таблицу, в которой подсчитаем долю каждой части речи в текстах обеих групп. Сначала сделаем это для каждой группы отдельно.
<- ddt_df |>
ddt_pos_distr group_by(upos) |>
summarise(n = n()) |>
mutate(freq = n / sum(n))
<- kish_df |>
kish_pos_distr group_by(upos) |>
summarise(n = n()) |>
mutate(freq = n / sum(n))
Сейчас данные выглядят так:
А теперь объединим таблицы и немного переформатируем данные, чтобы нам было удобнее строить график.
<- merge(ddt_pos_distr, kish_pos_distr, by = "upos") |>
all_pos_distr rename("ddt" = "freq.x",
"kish" = "freq.y") |>
select(upos, ddt, kish) |>
filter(upos %in% c("NOUN", "ADJ", "ADV", "PRON", "VERB")) |>
pivot_longer(!upos, names_to = "band", values_to = "freq")
Наши данные стали выглядеть так:
Построим график, сравнивающий эти доли между собой:
|>
all_pos_distr ggplot(aes(upos, freq, fill = band)) +
geom_bar(stat = "identity",
position = "dodge") +
scale_fill_manual(values = c("darkblue", "darkgray")) +
coord_flip() +
theme_bw() +
labs(title = "Распределение частей речи в песнях групп 'ДДТ' и 'Король и Шут'")
В общем-то, разница видна! Она не такая значительная, как ожидалось, но тому есть несколько причин:
- Распределение частей речи - не такая уж изменчивая штука. Если исполнитель не ставит перед собой задачи не использовать, к примеру, глаголы совсем, их всё равно будет много. Так что данное различие на самом деле может оказаться достаточно весомым.
- ДДТ - это всё же не Кино. От них изначально было меньше ожиданий, поскольку в их текстах в целом присутствует больше “активного сюжета”, чем у Кино. Но интуитивно казалось, что акцент всё-таки не так сильно на сюжете, а больше на ярких образах, и это ожидание подтвердилось.
- Возможно, сравнение внутри одного жанра в целом не такое продуктивное, как если бы мы сравнили, например, рок- и рэп-исполнителей
Вывод: очень хочется теперь применить это к другим исполнителям, чем я и займусь, но уже после дедлайна 💅
Облака слов
А теперь посмотрим, какие образы чаще встречаются в текстах этих групп: визуализируем самые частотные леммы.
Создаём список стоп-слов - прибавляем к уже готовому свои слова - и устанавливаем палитры для графиков
<- c(stopwords('ru'), c("весь", "свой", "твой", "наш", "это", "-|", "--|", "*"))
my_stoplist
<- RColorBrewer::brewer.pal(8, "Blues")
pal1 <- RColorBrewer::brewer.pal(8, "Oranges") pal2
ДДТ:
<- ddt_df |>
ddt_lemmas filter(!lemma %in% my_stoplist)|>
count(lemma) |>
arrange(-n)
wordcloud(ddt_lemmas$lemma,
$n,
ddt_lemmascolors = pal1,
max.words = 40,
scale=c(4, 0.7))
Король и Шут:
<- kish_df |>
kish_lemmas filter(!lemma %in% my_stoplist)|>
count(lemma) |>
arrange(-n)
wordcloud(kish_lemmas$lemma,
$n,
kish_lemmascolors = pal2,
max.words = 40,
scale=c(4, 0.7))
Песни ДДТ посвящены некоторым фундаментальным образам: небо, любовь, ночь, свет, душа, дорога и т.п. В песнях же КиШа более мистическая атмосфера: ночь, кровь, лес, беда…
При этом общие образы также присутствуют: ночь, смерть, друг, душа, мир и т.д.
В песнях ДДТ собираем стихии (вода, земля, ветер, огонь)
А в песнях КиШа много частей тела (рука, нога, голова, глаз)
Также иронично отметим, что самый часто встречающийся родственник из песен ДДТ - это мама, а в песнях КиШа это дед ))