Это много, чтобы охватить один вопрос, но это забавная проблема, поэтому я решил, что все равно решу проблему. Вот к чему это привело.
Версия Tidyverse / rvest
Сначала я собираюсь построить этот скребок в Tidyverse, потому что я знаком с его использованием для веб- выскабливание. Итак, начнем с загрузки необходимых пакетов.
library(tidyverse)
library(rvest)
Один сложный аспект этой проблемы заключается в том, что не существует ни одной страницы, содержащей ссылки на все страницы с речами. Однако, если мы соскребем ссылки с главной страницы, мы обнаружим, что существует набор ссылок на страницы со всеми выступлениями за любой год. Чтобы было ясно, я не видел этих ссылок на главной странице. Вместо этого я обнаружил их, просматривая главную страницу; просматривая узлы типа «а» с html_nodes("a")
, потому что проверка в Chrome сказала мне, что там были найдены соответствующие ссылки; извлеките URL-адреса из этих результатов с помощью html_attr("href")
, а затем просмотрите результаты в консоли, чтобы увидеть, что выглядело полезным. В этих результатах я увидел ссылки с формами "newsevents/speech2020-speeches.htm"
и "newsevents/speech2007speeches.htm"
, и когда я запустил один и тот же процесс для этих ссылок, я увидел, что получаю ссылки на отдельные речи. Итак:
# scrape the main page
base_page <- read_html("https://www.federalreserve.gov/newsevents/speeches.htm")
# extract links to those annual archives from the resulting html
year_links <- base_page %>%
html_nodes("a") %>%
html_attr("href") %>%
# the pattern for those annual pages changes, so we can use this approach to get both types
map(c("/newsevents/speech/[0-9]{4}-speeches.htm", "/newsevents/speech/[0-9]{4}speech.htm"), str_subset, string = .) %>%
reduce(union)
# here's what that produces
> year_links
[1] "/newsevents/speech/2020-speeches.htm" "/newsevents/speech/2019-speeches.htm" "/newsevents/speech/2018-speeches.htm" "/newsevents/speech/2017-speeches.htm"
[5] "/newsevents/speech/2016-speeches.htm" "/newsevents/speech/2015-speeches.htm" "/newsevents/speech/2014-speeches.htm" "/newsevents/speech/2013-speeches.htm"
[9] "/newsevents/speech/2012-speeches.htm" "/newsevents/speech/2011-speeches.htm" "/newsevents/speech/2010speech.htm" "/newsevents/speech/2009speech.htm"
[13] "/newsevents/speech/2008speech.htm" "/newsevents/speech/2007speech.htm" "/newsevents/speech/2006speech.htm"
Хорошо, теперь мы собираемся очистить эти ежегодные страницы для ссылок на страницы для отдельных выступлений, используя map
для повторения процесса по отдельным ссылкам.
speech_links <- map(year_links, function(x) {
# the scraped links are incomplete, so we'll start by adding the missing bit
full_url <- paste0("https://www.federalreserve.gov", x)
# now we'll essentially rerun the process we ran on the main page, only now we can
# focus on a single string pattern, which again I found by trial and error (i.e.,
# scrape the page, look at the hrefs on it, see which ones look relevant, check
# one out in my browser to confirm, then use str_subset() to get ones matching that pattern
speech_urls <- read_html(full_url) %>%
html_nodes("a") %>%
html_attr("href") %>%
str_subset(., "/newsevents/speech/")
# add the header now
return(paste0("https://www.federalreserve.gov", speech_urls))
})
# unlist the results so we have one long vector of links to speeches instead of a list
# of vectors of links
speech_links <- unlist(speech_links)
# here's what the results of that process look like
> head(speech_links)
[1] "https://www.federalreserve.gov/newsevents/speech/quarles20200117a.htm" "https://www.federalreserve.gov/newsevents/speech/bowman20200116a.htm"
[3] "https://www.federalreserve.gov/newsevents/speech/clarida20200109a.htm" "https://www.federalreserve.gov/newsevents/speech/brainard20200108a.htm"
[5] "https://www.federalreserve.gov/newsevents/speech/brainard20191218a.htm" "https://www.federalreserve.gov/newsevents/speech/brainard20191126a.htm"
Теперь, наконец, мы собираемся повторить процесс очистки по страницам для отдельных речей, чтобы сделать пометку с ключевыми элементами для каждой речи: датой, заголовком, оратором, местоположением и полный текст. Я нашел типы узлов для каждого из желаемых элементов, открыв страницу для одного из выступлений в моем браузере Chrome, щелкнув правой кнопкой мыши (я на компьютере Windows) и используя "Inspect", чтобы увидеть html связано с различными битами.
speech_list <- map(speech_links, function(x) {
Z <- read_html(x)
# scrape the date and convert it to 'date' class while we're at it
date <- Z %>% html_nodes("p.article__time") %>% html_text() %>% as.Date(., format = "%B %d, %Y")
title <- Z %>% html_nodes("h3.title") %>% html_text()
speaker <- Z %>% html_nodes("p.speaker") %>% html_text()
location <- Z %>% html_nodes("p.location") %>% html_text()
# this one's a little more involved because the text at that node had two elements,
# of which we only wanted the second, and I went ahead and cleaned up the speech
# text a bit here to make the resulting column easy to work with later
text <- Z %>%
html_nodes("div.col-xs-12.col-sm-8.col-md-8") %>%
html_text() %>%
.[2] %>%
str_replace_all(., "\n", "") %>%
str_trim(., side = "both")
return(tibble(date, title, speaker, location, text))
})
# finally, bind the one-row elements of that list into a single tibble
speech_table <- bind_rows(speech_list)
Вот краткий обзор того, что это производит, охватывающее 804 речи ФРС с 2006 года по настоящее время:
> str(speech_table)
Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 804 obs. of 5 variables:
$ date : Date, format: "2020-01-17" "2020-01-16" "2020-01-09" "2020-01-08" ...
$ title : chr "Spontaneity and Order: Transparency, Accountability, and Fairness in Bank Supervision" "The Outlook for Housing" "U.S. Economic Outlook and Monetary Policy" "Strengthening the Community Reinvestment Act by Staying True to Its Core Purpose" ...
$ speaker : chr "Vice Chair for Supervision Randal K. Quarles" "Governor Michelle W. Bowman" "Vice Chair Richard H. Clarida" "Governor Lael Brainard" ...
$ location: chr "At the American Bar Association Banking Law Committee Meeting 2020, Washington, D.C." "At the 2020 Economic Forecast Breakfast, Home Builders Association of Greater Kansas City, Kansas City, Missouri" "At the C. Peter McColough Series on International Economics, Council on Foreign Relations, New York, New York" "At the Urban Institute, Washington, D.C." ...
$ text : chr "It's a great pleasure to be with you today at the ABA Banking Law Committee's annual meeting. I left the practi"| __truncated__ "Few sectors are as central to the success of our economy and the lives of American families as housing. If we i"| __truncated__ "Thank you for the opportunity to join you bright and early on this January 2020 Thursday morning. As some of yo"| __truncated__ "Good morning. I am pleased to be here at the Urban Institute to discuss how to strengthen the Community Reinves"| __truncated__ ...
Версия Rcrawler
Теперь вы специально спрашивали об этом с пакетом Rcrawler
, а не rvest
, поэтому вот решение с использованием первого.
Мы начнем с использования * Функция 1035 * * LinkExtractor
с регулярным выражением для очистки URL-адресов страниц со ссылками на выступления по годам. Обратите внимание, что я знал только, что искать в регулярном выражении, потому что я уже пробежал через html, чтобы получить решение rvest
.
library(Rcrawler)
year_links = LinkExtractor("https://www.federalreserve.gov/newsevents/speeches.htm",
urlregexfilter = "https://www.federalreserve.gov/newsevents/speech/")
Теперь мы можем использовать lapply
для итерации LinkExtractor
по результатам этого процесса, чтобы очистить ежегодные партии ссылок на отдельные речи. Опять же, мы будем использовать регулярное выражение, чтобы сфокусировать наш анализ, и мы знаем только, какой шаблон использовать в регулярном выражении, потому что мы оценили результаты предыдущего шага и просмотрели некоторые из этих страниц в браузере.
speech_links <- lapply(year_links$InternalLinks, function(i) {
linkset <- LinkExtractor(i, urlregexfilter = "speech/[a-z]{1,}[0-9]{8}a.htm")
# might as well limit the results to the vector of interest while we're here
return(linkset$InternalLinks)
})
# that process returns a list of vectors, so let's collapse that list into one
# long vector of urls for pages with individual speeches
speech_links <- unlist(speech_links)
Наконец, мы можем применить функцию ContentScraper
к результирующему вектору ссылок на отдельные речи для извлечения данных. Изучение html для одной из этих страниц выявило шаблоны CSS, связанные с интересующими битами, поэтому мы будем использовать CssPatterns
, чтобы получить эти биты, и PatternsName
, чтобы дать им хорошие имена. Этот вызов возвращает список списков данных, поэтому мы закончим sh, преобразовав этот список списков в один фрейм данных, используя do.call(rbind.data.frame, ...)
с stringsAsFactors = FALSE
, чтобы избежать преобразования всего в факторы.
DATA <- ContentScraper(Url = speech_links,
CssPatterns = c(".article__time", ".location", ".speaker", ".title", ".col-xs-12.col-sm-8.col-md-8"),
PatternsName = c("date", "location", "speaker", "title", "text"),
# we need this next line to get both elements for the .col-xs-12.col-sm-8.col-md-8
# bit, which is the text of the speech itself. the first element
# is just a repeat of the header info
ManyPerPattern = TRUE)
# because the text element is a vector of two strings, we'll want to flatten the
# results into a one-row data frame to make the final concatenation easier. this
# gives us a row with two cols for text, text1 and text2, where text2 is the part
# you really want
DATA2 <- lapply(DATA, function(i) { data.frame(as.list(unlist(i)), stringsAsFactors = FALSE) })
# finally, collapse those one-row data frames into one big data frame, one row per speech
output <- do.call(rbind.data.frame, c(DATA2, stringsAsFactors = FALSE))
Здесь нужно отметить три вещи: 1) в этой таблице всего 779 строк, а в той, что мы получили с rvest
, - 806, и я не знаю, почему существует расхождение; 2) данные в этой таблице все еще необработанные и могут использовать некоторую очистку (например, преобразовать строки даты в класс date
, привести в порядок строки в текстовом столбце), что вы можете сделать с sapply
; и 3) вы, вероятно, захотите удалить лишний столбец text1
, который вы могли бы сделать в базе R с помощью output$text1 <- NULL
.