Тема 9 Веб-скрапинг
Выше мы говорили о таком импорте html, когда все теги разом удаляются. Это не всегда удобно, поскольку файл хранит данные в структурированном виде, например, под разными тегами дату, автора и текст. И может быть желательно эту структуру сохранить.
В R это позволяет делать пакет rvest
. С его помощью мы подготовим для дальнейшего построения тематической модели архив телеграм-канала Antibarbari HSE. Канал публичный, и Telegram дает возможность скачать архив в формате html при помощи кнопки export (однако эта функция может быть недоступна на MacOS).
Эта глава опирается в основом на второе издание книги R for Data Science Хадли Викхема.
9.1 Структура html
Документы html (HyperText Markup Language) имеют ирархическую структуру, состоящую из элементов. В каждом элементе есть открывающий тег (<tag>), опциональные атрибуты (id='first') и закрывающий тег (</tag>). Все, что находится между открывающим и закрывающим тегом, называется содержанием элемента.
Важнейшие теги, о которых стоит знать:
- <html> (есть всегда), с двумя детьми (дочерними элементами): <head> и <body>
- элементы, отвечающие за структуру: <h1> (заголовок), <section>, <p> (параграф), <ol> (упорядоченный список)
- элементы, отвечающие за оформление: <b> (bold), <i> (italics), <a> (ссылка)
Чтобы увидеть структуру веб-страницы, надо нажать правую кнопку мыши и выбрать View Source (это работает и для тех html, которые хранятся у вас на компьютере).
9.2 Каскадные таблицы стилей
У тегов могут быть именованные атрибуты; важнейшие из них – это id и class, которые в сочетании с CSS контролируют внешний вид страницы.
CSS (англ. Cascading Style Sheets «каскадные таблицы стилей») — формальный язык декорирования и описания внешнего вида документа (веб-страницы), написанного с использованием языка разметки (чаще всего HTML или XHTML).
У этого курса тоже есть свой файл .css, в котором блок infobox (вы его видите как серый квадратик с определением) описан так:
.infobox {
padding: 1em 1em 1em 4em;
background: aliceblue 5px center/3em no-repeat;
color: black;
}
Проще говоря, это инструкция, что делать с тем или иным элементом. Каждое правило CSS имеет две основные части — селектор и блок объявлений. Селектор, расположенный в левой части правила до знака «{», определяет, на какие части документа (возможно, специально обозначенные) распространяется правило. Блок объявлений располагается в правой части правила. Он помещается в фигурные скобки, и, в свою очередь, состоит из одного или более объявлений, разделённых знаком «;».
Селекторы CSS полезны для скрапинга, потому что они помогают вычленить необходимые элементы. Это работает так:
- p выберет все элементы <p>
- .title выберет элементы с классом “title”
- #title выберет все элементы с атрибутом id=‘title’
Важно: если изменится структура страницы, откуда вы скрапили информацию, то и код, возможно, придется переписывать.
9.3 Извлечение данных
Чтобы прочесть файл html, используем одноименную функцию.
library(rvest)
messages <- read_html("./files/antibarbari_archive/messages.html")
messages2 <- read_html("./files/antibarbari_archive/messages2.html")
messages
## {html_document}
## <html>
## [1] <head>\n<meta http-equiv="Content-Type" content="text ...
## [2] <body onload="CheckLocation();">\n\n <div class="pag ...
На следующем этапе важно понять, какие именно элементы нужны. Рассмотрим на примере одного сообщения. Для примера я сохраню этот элемент как небольшой отдельный html; rvest
позволяет это сделать (но внутри двойных кавычек должны быть только одинарные):
html <- minimal_html("
<div class='message default clearfix' id='message83'>
<div class='pull_left userpic_wrap'>
<div class='userpic userpic2' style='width: 42px; height: 42px'>
<div class='initials' style='line-height: 42px'>
A
</div>
</div>
</div>
<div class='body'>
<div class='pull_right date details' title='19.05.2022 11:18:07 UTC+03:00'>
11:18
</div>
<div class='from_name'>
Antibarbari HSE
</div>
<div class='text'>
Этот пост открывает серию переложений из «Дайджеста платоновских идиом» Джеймса Ридделла (1823–1866), английского филолога-классика, чей научный путь был связан с Оксфордским университетом. По приглашению Бенджамина Джоветта он должен был подготовить к изданию «Апологию», «Критон», «Федон» и «Пир». Однако из этих четырех текстов вышла лишь «Апология» с предисловием и приложением в виде «Дайджеста» (ссылка) — уже после смерти автора. <br><br>«Дайджест» содержит 326 параграфов, посвященных грамматическим, синтаксическим и риторическим особенностям языка Платона. Знакомство с этим теоретическим материалом позволяет лучше почувствовать уникальный стиль философа и добиться большей точности при переводе. Ссылки на «Дайджест» могут быть уместны и в учебных комментариях к диалогам Платона. Предлагаемая здесь первая часть «Дайджеста» содержит «идиомы имен» и «идиомы артикля» (§§ 1–39).<br><a href='http://antibarbari.ru/2022/05/19/digest_1/'>http://antibarbari.ru/2022/05/19/digest_1/</a>
</div>
<div class='signature details'>
Olga Alieva
</div>
</div>
</div>
")
Из всего этого мне может быть интересно id сообщения (<div class=‘message default clearfix’ id=‘message83’>), текст сообщения (<div class=‘text’>), а также, если указан, автор сообщения (<div class=‘signature details’>). Извлекаем текст (для этого рекомендуется использовать функцию html_text2()
):
## [1] "Этот пост открывает серию переложений из «Дайджеста платоновских идиом» Джеймса Ридделла (1823–1866), английского филолога-классика, чей научный путь был связан с Оксфордским университетом. По приглашению Бенджамина Джоветта он должен был подготовить к изданию «Апологию», «Критон», «Федон» и «Пир». Однако из этих четырех текстов вышла лишь «Апология» с предисловием и приложением в виде «Дайджеста» (ссылка) — уже после смерти автора.\n\n«Дайджест» содержит 326 параграфов, посвященных грамматическим, синтаксическим и риторическим особенностям языка Платона. Знакомство с этим теоретическим материалом позволяет лучше почувствовать уникальный стиль философа и добиться большей точности при переводе. Ссылки на «Дайджест» могут быть уместны и в учебных комментариях к диалогам Платона. Предлагаемая здесь первая часть «Дайджеста» содержит «идиомы имен» и «идиомы артикля» (§§ 1–39).\nhttp://antibarbari.ru/2022/05/19/digest_1/"
В классе signature details есть пробел, достаточно на его месте поставить точку:
## [1] "Olga Alieva"
Важно помнить, что html_element
всегда возвращает один элемент. Если их больше, надо использовать html_elements
.
Осталось добыть message id:
## [1] "message83"
9.4 Извлечение в тиббл
library(tidyverse)
tibble(id = html %>%
html_element("div") %>%
html_attr("id"),
signature = html %>%
html_element(".signature.details") %>%
html_text2(),
text = html %>%
html_element(".text") %>%
html_text2()
)
## # A tibble: 1 × 3
## id signature text
## <chr> <chr> <chr>
## 1 message83 Olga Alieva "Этот пост открывает серию переложе…
9.5 Скрапим телеграм-канал
До сих пор наша задача упрощалась тем, что мы имели дело с игрушечным html для единственного сообщения. В настоящем html тег div повторяется на разных уровнях, нам надо извлечь только такие div, которым соответствует определенный класс:
## {xml_nodeset (6)}
## [1] <div class="message default clearfix" id="message3">\ ...
## [2] <div class="message default clearfix" id="message5">\ ...
## [3] <div class="message default clearfix" id="message6">\ ...
## [4] <div class="message default clearfix" id="message7">\ ...
## [5] <div class="message default clearfix" id="message8">\ ...
## [6] <div class="message default clearfix" id="message9">\ ...
Уже из этого списка можем доставать все остальное.
messages_tbl1 <- tibble(id = messages %>%
html_elements("div.message.default") %>%
html_attr("id"),
signature = messages %>%
html_elements("div.message.default") %>%
html_element(".signature.details") %>%
html_text2(),
text = messages %>%
html_elements("div.message.default") %>%
html_element(".text") %>%
html_text2()
)
messages_tbl2 <- tibble(id = messages2 %>%
html_elements("div.message.default") %>%
html_attr("id"),
signature = messages2 %>%
html_elements("div.message.default") %>%
html_element(".signature.details") %>%
html_text2(),
text = messages2 %>%
html_elements("div.message.default") %>%
html_element(".text") %>%
html_text2()
)
Обратите внимание, что мы сначала извлекаем нужные элементы при помощи html_elements()
, а потом применяем к каждому из них html_element()
. Это гарантирует, что в каждом столбце нашей таблицы равное число наблюдений, т.к. функция html_element()
, если она не может найти, например, подпись, возвращает NA.
Сшиваем воедино два тиббла.
## # A tibble: 1,096 × 3
## id signature text
## <chr> <chr> <chr>
## 1 message3 <NA> "Latin never sleeps. Новое видео на …
## 2 message5 <NA> "Подборка видео семинара по медленно…
## 3 message6 <NA> "Новое видео в плейлисте \"Латинский…
## 4 message7 <NA> "🤖 ОТКРЫТА ЗАПИСЬ НА ПРОЕКТ\n\nВ ра…
## 5 message8 <NA> "Желающие присоединиться к группе, ч…
## 6 message9 <NA> "https://youtu.be/I-U_lG0mB3M"
## 7 message10 <NA> "В клубе Antibarbari продолжается се…
## 8 message11 <NA> "Филеб. Семинар 3 марта 2022. Сократ…
## 9 message12 <NA> "Вышка вернулась в аудитории, поэтом…
## 10 message13 <NA> "Воспользовались дополнительным выхо…
## # ℹ 1,086 more rows
Создатели канала не сразу разрешили подписывать посты, поэтому для первых нескольких десятков подписи не будет. В некоторых постах только фото, для них в столбце text – NA, их можно сразу отсеять.
Извлеките из архива антиварваров дату публикации для каждого поста.
9.6 Эмотиконы
В постах довольно много эмотиконов. Я их удалю, но сначала скажу о полезном пакете, который позволяет их все извлечь и, например, посчитать.
library(emoji)
messages_tbl %>%
mutate(emoji = emoji_extract_all(text)) %>%
pull(emoji) %>%
unlist() %>%
as_tibble() %>%
count(value) %>%
arrange(-n)
## # A tibble: 159 × 2
## value n
## <chr> <int>
## 1 👾 68
## 2 ⭐ 23
## 3 👀 21
## 4 👇 20
## 5 🍿 18
## 6 📚 16
## 7 🔼 12
## 8 1️⃣ 10
## 9 📌 10
## 10 🔗 10
## # ℹ 149 more rows
Заменяем их все на пробелы.
9.7 Рутинная уборка
Подготовка текста для анализа включает в себя удаление сносок, иногда хэштегов, чисел, имейлов, возможно имен и т.п. В нашем случае ситуация осложняется тем, что тексты включают цитаты на латыни и древнегреческом, некоторые технические сокращения, номера страниц и др. Вот так, например, выглядит типичный пост:
## [1] "🎞 Теэтет #10 149b7-150b8\nСократ продолжает засыпать семнадцатилетнего математика подробностями из области акушерства и гинекологии, и мы вместе с ним терпеливо изучаем, чем сводничество отличается от сватовства. Верните квадратные корни. #платон #теэтет\nhttps://vk.com/video-211800158_456239238"
Вот так вылавливается гиперссылка.
## [[1]]
## [1] "https://vk.com/video-211800158_456239238"
Вот так вылавливается пагинация и номер семинара (и некоторые другие числа).
## [[1]]
## [1] "#10" "149b7-" "150b8" "21180" "0158" "45623"
## [7] "9238"
Похожим образом можно выловить даты, имейлы и т.п. Все это удаляем из текста.
messages_clean <- messages_tbl %>%
mutate(text = str_replace_all(text, "(http|https)(\\S+)", " ")) %>%
mutate(text = str_replace_all(text, "\\d{2}\\.\\d{2}\\.\\d{4}", " ")) %>%
mutate(text = str_replace_all(text, "\\W[-A-Za-z0-9_.%]+\\@[-A-Za-z0-9_.%]+\\.[A-Za-z]", " ")) %>%
mutate(text = str_replace_all(text, "#?\\d{2,3}\\w?\\d?-?", " ")) %>%
mutate(text = str_replace_all(text, "\\n+", " "))
Остались еще сокращения вроде “г.”, но токены из одной буквы можно будет удалить после разделения на слова. Знаки пунктуации можно оставить или убрать – иногда они бывают интересным стилистическим маркером. В любом случае лучше это делать после лемматизации, т.к. на тексте без знаков препинания анализатор покажет себя хуже.
## # A tibble: 1 × 3
## id signature text
## <chr> <chr> <chr>
## 1 message465 Olga Alieva "🎞 Теэтет Сократ продолжает з…
Число id и число текстов не совпадает, поскольку для некоторых постов текста нет (NA), а у других он совпадает (“Пост выходного дня”). Это надо сразу исправить, чтобы результат лемматизации можно было потом соединить с данными о подписи. Я просто уберу очень короткие посты, поскольку для анализа они неинтересны.
## [1] 734 3
Переименуем первый столбец и переназначим id, чтобы можно было потом соединить с результатами лемматизации.
9.8 Лемматизация
На лемматизацию мы отдаем вектор с сообщениями.
library(udpipe)
russian_syntagrus <- udpipe_load_model(file = "russian-syntagrus-ud-2.5-191206.udpipe")
messages_ann <- udpipe_annotate(russian_syntagrus, messages_clean$text)
messages_ann <- as_tibble(messages_ann)
messages_ann
Убедимся, что после лемматизации число id не изменилось, и соединим этот тиббл с данными о подписи.
## [1] 734
messages_signed <- messages_ann %>%
left_join(messages_clean, by = "doc_id") %>%
select(-text, -sentence_id, -paragraph_id, -xpos, -feats,
-head_token_id, -dep_rel, -deps, -misc)
length(unique(messages_signed$doc_id))
## [1] 734
В постах упоминаются многие коллеги и студенты, чьи имена я бы хотела удалить, чтобы они не появлялись на графиках и т.п., но есть и много древних и новых имен, которые хотелось бы оставить.
# valid_names <- messages_ann %>%
# filter(upos == "PROPN") %>%
# count(lemma) %>%
# arrange(-n) %>%
# filter(!str_detect(lemma, "[\\.«]")) %>%
# filter(str_detect(lemma, "[[\u0400-\u04FF]]")) %>%
# filter(n > 1)
#
# valid_names_vec <- as.character(valid_names$lemma)
# список имен отредактирован вручную
# write.table(valid_names_vec, file = "files/names.txt",
# row.names = F, col.names = F)
valid_names <- read_table(file = "files/names.txt", col_names = F)
valid_names <- valid_names %>%
rename(names = X1) %>%
mutate(names = str_remove_all(names, "\\W")) %>%
pull(names)
messages_signed <- messages_signed %>%
filter(upos != "PROPN" | upos == "PROPN" & lemma %in% valid_names) %>%
filter(upos != "PUNCT") %>%
filter(!upos %in% c("X", "NUM")) %>%
mutate(lemma = str_replace_all(lemma, "-", "")) %>%
mutate(lemma = str_remove_all(lemma, "[[:punct:]]"))
После некоторых сомнений, я удалю все, кроме кириллицы – на канале очень много латинского и греческого текста, на этапе создания термдокументной матрицы из-за этого будет почти 100% разреженность.
messages_signed <- messages_signed %>%
mutate(lemma = str_replace_all(lemma, "[[^\u0400-\u04FF]]", " ")) %>%
filter(nchar(lemma) > 0)
Дальше были исправлены некоторые ошибки лемматизации.
text_tidy <- messages_signed %>%
mutate(lemma = tolower(lemma)) %>%
mutate_at(vars(lemma), ~
case_when(lemma == "мят" ~ "мята",
str_detect(lemma, "сенек") ~ "сенека",
str_detect(lemma, "аттик") ~ "аттик",
str_detect(lemma, "кальвиз") ~ "кальвизий",
str_detect(lemma, "горац") ~ "гораций",
str_detect(lemma, "гален") ~ "гален",
str_detect(lemma, "плотин") ~ "плотин",
str_detect(lemma, "августин") ~ "августин",
str_detect(lemma, "абари") ~ "абарид",
str_detect(lemma, "катулл") ~ "катулл",
str_detect(lemma, "лукул") ~ "лукулл",
str_detect(lemma, "посидо") ~ "посидоний",
str_detect(lemma, "деркилл") ~ "деркиллид",
str_detect(lemma, "порфир") ~ "порфирий",
str_detect(lemma, "афин") ~ "афины",
str_detect(lemma, "локк") ~ "локк",
str_detect(lemma, "макроб") ~ "макробий",
str_detect(lemma, "лаэр") ~ "лаэрций",
str_detect(lemma, "макрин") ~ "макрина",
str_detect(lemma, "маркиш") ~ "маркиш",
str_detect(lemma, "маячк") ~ "маячок",
str_detect(lemma, "очерк") ~ "очерк",
str_detect(lemma, "птолем") ~ "птолемей",
str_detect(lemma, "росса") ~ "росс",
str_detect(lemma, "самосат") ~ "самосата",
str_detect(lemma, "стрепсиад") ~ "стрепсиад",
str_detect(lemma, "эпихар") ~ "эпихарм",
str_detect(lemma, "ямвл") ~ "ямвлих",
str_detect(lemma, "брумал") ~ "брумалии",
str_detect(lemma, "иоанн") ~ "иоанн",
str_detect(lemma, "кинопоиск") ~ "кинопоиск",
str_detect(lemma, "корнар") ~ "корнарий",
str_detect(lemma, "луция") ~ "луций",
str_detect(lemma, "кассия") ~ "кассий",
str_detect(lemma, "минф") ~ "минфа",
str_detect(lemma, "персефон") ~ "персефона",
str_detect(lemma, "пестум") ~ "пестум",
str_detect(lemma, "платон") ~ "платон",
str_detect(lemma, "платно") ~ "платон",
str_detect(lemma, "филеб") ~ "филеб",
str_detect(lemma, "фульг") ~ "фульгенций",
str_detect(lemma, "аврел") ~ "аврелий",
str_detect(lemma, "антигон") ~ "антигона",
str_detect(lemma, "анция") ~ "анций",
str_detect(lemma, "борея") ~ "борей",
str_detect(lemma, "вольтер") ~ "вольтер",
str_detect(lemma, "гераклид") ~ "гераклид",
str_detect(lemma, "теэтет") ~ "теэтет",
str_detect(lemma, "евангел") ~ "евангелие",
str_detect(lemma, "федон") ~ "федон",
TRUE ~ .))
Получившийся тиббл сохраняю – он понадобится в главе 13.
9.9 Html таблицы
Если вам повезет, то ваши данные уже будут храниться в HTML-таблице, и их можно будет просто считать из этой таблицы39. Распознать таблицу в браузере обычно несложно: она имеет прямоугольную структуру из строк и столбцов, и ее можно скопировать и вставить в такой инструмент, как Excel.
Таблицы HTML строятся из четырех основных элементов: <table>, <tr> (строка таблицы), <th> (заголовок таблицы) и <td> (данные таблицы). Мы соберем информацию о проектных группах ФГН в 2022-2024 г.
html <- read_html("https://hum.hse.ru/proj/project2022_2024")
my_table <- html %>%
html_element(".bordered") %>%
html_table()
DT::datatable(my_table)
С сайта Новой философской энциклопедии извлеките список слов на букву П. Используйте map_df()
для объединения таблиц.
Сколько всего слов на букву П в НФЭ?
9.10 Wikisource
Многие тексты доступны на сайте Wikisource.org. Попробуем извлечь все сказки Салтыкова-Щедрина.
url <- "https://ru.wikisource.org/wiki/%D0%9C%D0%B8%D1%85%D0%B0%D0%B8%D0%BB_%D0%95%D0%B2%D0%B3%D1%80%D0%B0%D1%84%D0%BE%D0%B2%D0%B8%D1%87_%D0%A1%D0%B0%D0%BB%D1%82%D1%8B%D0%BA%D0%BE%D0%B2-%D0%A9%D0%B5%D0%B4%D1%80%D0%B8%D0%BD"
html = read_html(url)
Для того, чтобы справиться с такой страницей, пригодится Selector Gadget (расширение для Chrome). Вот тут можно посмотреть короткое видео, как его установить. При помощи селектора выбираем нужные уровни.
## {xml_nodeset (6)}
## [1] <a href="/wiki/%D0%9F%D0%BE%D0%B2%D0%B5%D1%81%D1%82%D ...
## [2] <a href="/wiki/%D0%93%D0%BE%D0%B4%D0%BE%D0%B2%D1%89%D ...
## [3] <a href="/wiki/%D0%9F%D1%80%D0%BE%D0%BF%D0%B0%D0%BB%D ...
## [4] <a href="/wiki/%D0%94%D0%B8%D0%BA%D0%B8%D0%B9_%D0%BF% ...
## [5] <a href="/wiki/%D0%9F%D1%80%D0%B5%D0%BC%D1%83%D0%B4%D ...
## [6] <a href="/wiki/%D0%A1%D0%B0%D0%BC%D0%BE%D0%BE%D1%82%D ...
Теперь у нас есть список ссылок.
Данных о годе публикации под тегом нет; надо подняться на уровень выше:
## {xml_nodeset (6)}
## [1] <li>\n<a href="/wiki/%D0%9F%D0%BE%D0%B2%D0%B5%D1%81%D ...
## [2] <li>\n<a href="/wiki/%D0%93%D0%BE%D0%B4%D0%BE%D0%B2%D ...
## [3] <li>\n<a href="/wiki/%D0%9F%D1%80%D0%BE%D0%BF%D0%B0%D ...
## [4] <li>\n<a href="/wiki/%D0%94%D0%B8%D0%BA%D0%B8%D0%B9_% ...
## [5] <li>\n<a href="/wiki/%D0%9F%D1%80%D0%B5%D0%BC%D1%83%D ...
## [6] <li>\n<a href="/wiki/%D0%A1%D0%B0%D0%BC%D0%BE%D0%BE%D ...
## [1] "Повесть о том, как один мужик двух генералов прокормил, 1869"
## [2] "Годовщина, 1869"
## [3] "Пропала совесть, 1869"
## [4] "Дикий помещик, 1869"
## [5] "Премудрый пискарь, 1883"
## [6] "Самоотверженный заяц, 1883"
## [7] "Бедный волк, 1883"
## [8] "Добродетели и Пороки, 1884"
## [9] "Медведь на воеводстве, 1884"
## [10] "Обманщик-газетчик и легковерный читатель, 1884"
## [11] "Вяленая вобла, 1884"
## [12] "Орёл-меценат, 1884"
## [13] "Карась-идеалист, 1884"
## [14] "Игрушечного дела людишки, 1879, 1886"
## [15] "Чижиково горе, 1884"
## [16] "Верный Трезор, 1885"
## [17] "Недреманное око, конец 1885 или начало 1886"
## [18] "Дурак, 1885"
## [19] "Соседи, 1885"
## [20] "Здравомысленный заяц, 1885"
## [21] "Либерал, 1885"
## [22] "Баран-непомнящий, 1885"
## [23] "Коняга, 1855"
## [24] "Кисель, 1855"
## [25] "Праздный разговор, 1886"
## [26] "Деревенский пожар, 1885"
## [27] "Путём-дорогою, 1886"
## [28] "Богатырь, 1886"
## [29] "Гиена, 1886"
## [30] "Приключение с Крамольниковым, 1886"
## [31] "Христова ночь, 1886"
## [32] "Ворон-челобитчик, 1886"
## [33] "Рождественская сказка, 1886"
Соединяем:
## # A tibble: 33 × 2
## title_year href
## <chr> <chr>
## 1 Повесть о том, как один мужик двух генералов проко… /wik…
## 2 Годовщина, 1869 /wik…
## 3 Пропала совесть, 1869 /wik…
## 4 Дикий помещик, 1869 /wik…
## 5 Премудрый пискарь, 1883 /wik…
## 6 Самоотверженный заяц, 1883 /wik…
## 7 Бедный волк, 1883 /wik…
## 8 Добродетели и Пороки, 1884 /wik…
## 9 Медведь на воеводстве, 1884 /wik…
## 10 Обманщик-газетчик и легковерный читатель, 1884 /wik…
## # ℹ 23 more rows
Дальше можно достать текст для каждой сказки. Потренируемся на одной. Снова привлекаем Selector Gadget для составления правила.
url_test <- tales %>%
filter(row_number() == 1) %>%
pull(href) %>%
paste0("https://ru.wikisource.org", .)
text <- read_html(url_test) %>%
html_elements(".indent p") %>%
html_text2()
text[1]
## [1] "Жили да были два генерала, и так как оба были легкомысленны, то в скором времени, по щучьему велению, по моему хотению, очутились на необитаемом острове."
## [1] "Однако, и об мужике не забыли; выслали ему рюмку водки да пятак серебра: веселись, мужичина!"
Первый и последний параграф достали верно; можно обобщать.
Функция для извлечения текстов.
get_text <- function(url) {
read_html(url) %>%
html_elements(".indent p") %>%
html_text2() %>%
paste(collapse= " ")
}
Несколько сказок не выловились: там другая структура html, но в целом все получилось.
## # A tibble: 33 × 3
## title_year href value
## <chr> <chr> <chr>
## 1 Повесть о том, как один мужик двух генералов прокор… http… "Жил…
## 2 Годовщина, 1869 http… "Сег…
## 3 Пропала совесть, 1869 http… "Про…
## 4 Дикий помещик, 1869 http… "В н…
## 5 Премудрый пискарь, 1883 http… "Жил…
## 6 Самоотверженный заяц, 1883 http… "Одн…
## 7 Бедный волк, 1883 http… "Дру…
## 8 Добродетели и Пороки, 1884 http… "Доб…
## 9 Медведь на воеводстве, 1884 http… ""
## 10 Обманщик-газетчик и легковерный читатель, 1884 http… "Жил…
## # ℹ 23 more rows
Дальше можно разделить столбец с названием и годом и, например, удалить ссылку, она больше не нужна. Разделить по запятой не получится, т.к. она есть в некоторых названиях.
tales <- tales %>%
select(-href) %>%
separate(title_year, into = c("title", "year"), sep = -5) %>%
mutate(title = str_remove(title, ",$"))
## # A tibble: 33 × 3
## title year value
## <chr> <chr> <chr>
## 1 Повесть о том, как один мужик двух генералов прокор… " 18… "Жил…
## 2 Годовщина " 18… "Сег…
## 3 Пропала совесть " 18… "Про…
## 4 Дикий помещик " 18… "В н…
## 5 Премудрый пискарь " 18… "Жил…
## 6 Самоотверженный заяц " 18… "Одн…
## 7 Бедный волк " 18… "Дру…
## 8 Добродетели и Пороки " 18… "Доб…
## 9 Медведь на воеводстве " 18… ""
## 10 Обманщик-газетчик и легковерный читатель " 18… "Жил…
## # ℹ 23 more rows
Недостающие две сказки я не буду пытаться извлечь, но логику вы поняли.