Сбор данных с веб-сайтов с помощью нового R-пакета rvest

Копирование таблиц или списков с веб-сайтов – занятие нудное, унылое, скучное и монотонное. К тому же этот процесс подвержен ошибкам и трудно воспроизводим. К счастью, существуют пакеты для Python и R, позволяющие автоматизировать выполнение подобных задач. В этой статье мы воспользуемся преимуществами нового R-пакета rvest для извлечения адресов, размещенных на веб-сайте. Затем выполним геокодирование этих адресов, используя пакет ggmap, и создадим интерактивную карту с помощью пакета leaflet.

В качестве примера мы решили использовать информацию о винодельческих и пивоваренных компаниях, расположенных в регионе Фингер-Лейкс (Finger Lakes), штат Нью-Йорк.

0) Загружаем библиотеки

В этом примере мы будем использовать несколько пакетов, большинство из которых доступно на основном веб-сайте R (CRAN). Единственным исключением является пакет leaflet, который необходимо установить с GitHub. Руководство находится здесь. Обратите внимание, речь идет о пакете leaflet, а не о пакете leafletR.

#devtools::install_github("rstudio/leaflet")
library(dplyr)
library(rvest)
library(ggmap)
library(leaflet)
library(RColorBrewer)

1) Выполняем парсинг целевой веб-страницы

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

Используя пакет rvest, первым делом необходимо выполнить парсинг целевой веб-страницы, и это легко можно сделать с помощью функции html.

# URL for the Visit Ithaca website, wineries page
url<-html("http://www.visitithaca.com/attractions/wineries.html")

2) Находим и извлекаем необходимые нам элементы веб-страницы

При сборе данных с веб-сайтов труднее всего определить, какие фрагменты HTML-кода необходимо извлечь. Обычно веб-страница представляет собой сложную структуру, состоящую из вложенных объектов, называемую объектной моделью документа (document object model, DOM). Соответственно, вы должны выяснить, какие фрагменты DOM вам нужны.

Чтобы сделать это, необходимо исследовать код веб-страницы с помощью инструментов разработчика, предоставляемых вашим браузером. Если вы используете Chrome или Firefox, открыть инструменты разработчика можно нажатием F12 (или Cmd + Opt + I для Mac), если вы используете SafariCmd + Opt + I.

Мы будем использовать Chrome. Обратите внимание, автор пакета Хэдли Уикхэм (Hadley Wickham) рекомендует использовать инструмент SelectorGadget, распространяемый в виде расширения Chrome, для поиска необходимых вам элементов веб-страниц. Также он рекомендует этот ресурс для изучения селекторов.

Чтобы следить за ходом изложения, откройте страницу со списком винодельческих компаний, которая используется в нашем примере.

Нажав F12 в Chrome, вы увидите нечто подобное тому, что изображено на рисунке ниже. Обратите особое внимание на инструмент для выбора элементов, обведенный красным цветом, и убедитесь в том, что у вас открыта вкладка Elements (элементы).

developer_tools1

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

Извлекаем названия

Чтобы выделить названия, активируйте инструмент для выбора элементов и щелкните мышью на одном из названий винодельческих компаний на веб-странице. Изучив код этого элемента, вы увидите, что название представляет собой гиперссылку (<a href…) и имеет класс pageListingHeader. Поскольку названия – это единственные элементы на странице с данным классом, мы можем извлечь их, используя в качестве селектора только класс.

name

Функция html_nodes извлекает весь узел из DOM, а затем функция html_text позволяет нам извлечь текст из узла. Обратите внимание на использование конвейера %>%, который передает результат выполнения функции html_nodes в функцию html_text

# Pull out the names of the wineries and breweries
selector_name<-".pageListingHeader"

fnames<-html_nodes(url, selector_name) %>%
  html_text()

head(fnames)
## [1] "Varick Winery & Vineyard"                       
## [2] "Ithaca Beer Company"                            
## [3] "Bellwether Hard Cider / Bellwether Wine Cellars"
## [4] "Lakeshore Winery"                               
## [5] "Knapp Winery & Vineyard Restaurant"             
## [6] "Goose Watch Winery"

Извлекаем адреса

Извлечь адреса немного сложнее. Применив инструмент для выбора элементов, вы увидите, что вся остальная информация, кроме названия, находится в контейнере (<div>) с классом results_summary_item_details. Но если мы будем использовать этот селектор сам по себе, мы извлечем все данные, включая описание, номер телефона и т.д. Больше, чем нам нужно.

Если внимательно изучить код, становится ясно, что информация, относящаяся к каждой винодельческой компании, размещена в таблице, состоящей из трех столбцов (первый столбец содержит изображение, второй – пустое пространство, третий – адрес и т.д.). Таким образом, мы можем добавить вторую часть селектора, чтобы извлечь только третий столбец. Более того, мы не хотим получить все содержимое третьего столбца, нам нужен только контент, заключенный между первой парой тегов <strong>. В итоге:

  • На словах: извлекаем контент, размещенный между первой парой тегов <strong> в третьем столбце таблицы, находящейся в контейнере с классом .results_summary_item_details.

  • В виде CSS-селектора: .results_summary_item_details td:nth-child(3) strong:first-child.


# Pull out the addresses of the wineries and breweries
selector_address<-".results_summary_item_details td:nth-child(3) strong:first-child"

faddress<-html_nodes(url, selector_address) %>%
    html_text()

head(faddress)
## [1] "on the Cayuga Lake Wine Trail5102 Rt. 89, Romulus, NY 14541"                         
## [2] "122 Ithaca Beer Drive Ithaca, NY 14850"                                              
## [3] "on the Cayuga Lake Wine Trail9070 Rt. 89, Trumansburg, NY 14886"                     
## [4] "5132 Rt. 89, Romulus, NY 14541"                                                      
## [5] "on the Cayuga Lake Wine Trail2770 County Rd. 128, (Ernsberger Rd.) Romulus, NY 14541"
## [6] "on the Cayuga Lake Wine Trail5480 Rt. 89, Romulus, NY 14541"

Информация на данной веб-странице немного неоднородна: иногда адрес винодельческой компании присутствует сам по себе, а иногда ему предшествует строка наподобие «on the Cayuga Lake Wine Trail». Поэтому необходимо выполнить небольшую постобработку.

to_remove<-paste(c("on the Cayuga Lake Wine Trail", "Cayuga Lake Wine Trail",
    "on the Cayuga Wine Trail", "on the Finger Lakes Beer Trail"), collapse="|")

faddress<-gsub(to_remove, "", faddress)

head(faddress)
## [1] "5102 Rt. 89, Romulus, NY 14541"                         
## [2] "122 Ithaca Beer Drive Ithaca, NY 14850"                 
## [3] "9070 Rt. 89, Trumansburg, NY 14886"                     
## [4] "5132 Rt. 89, Romulus, NY 14541"                         
## [5] "2770 County Rd. 128, (Ernsberger Rd.) Romulus, NY 14541"
## [6] "5480 Rt. 89, Romulus, NY 14541"

Теперь у нас есть названия и адреса, и мы готовы к геокодированию.

3) Геокодируем адреса с помощью ggmap

Геокодирование

Пакет ggmap имеет хорошую функцию geocode, которую мы будем использовать для получения координат. 

# Using output="latlona" includes the address that was geocoded
geocodes<-geocode(faddress, output="latlona")

head(geocodes)
##         lon      lat                                      address
## 1 -76.77388 42.78128     5102 new york 89, romulus, ny 14541, usa
## 2 -76.53506 42.41655 122 ithaca beer drive, ithaca, ny 14850, usa
## 3 -76.67176 42.58552  9070 new york 89, interlaken, ny 14847, usa
## 4 -76.77593 42.77588     5132 new york 89, romulus, ny 14541, usa
## 5 -76.79253 42.76668      ernsberger road, romulus, ny 14541, usa
## 6 -76.77504 42.75862     5480 new york 89, romulus, ny 14541, usa

[Дополнение от 26.05.2015] В одном из комментариев читатель отмечает, что данный подход противоречит условиям использования сервиса Google Maps (потому что мы отображаем результаты геокодирования не на карте Google Maps). Я согласен с этим, поэтому мы можем использовать альтернативный вариант. Хорошей альтернативой является сервис Yahoo PlaceFinder, при использовании которого нет подобных ограничений. Для использования данного сервиса существует небольшой R-пакет, разработанный Джеффом Алленом (Jeff Allen). Вы можете установить его с GitHub. Кроме того, нужно будет получить ключ и секретный код, необходимые для работы с сервисом (на странице пакета есть руководство). Код, реализующий геокодирование, представлен ниже.

Я поэкспериментировал с различными открытыми инструментами для геокодирования, с такими как, например, функция, описанная в этой дискуссии на stackoverflow.com, использующая сервис MapQuest. Но результаты оказались не очень хорошими. Для тестирования я использовал адрес винодельческой компании Varick Winery (5102 NY-89, Romulus, NY 14541). Сервис Google Maps смог найти координаты для данного адреса, в то время как многие другие сервисы – нет. Сервис Yahoo PlaceFinder также успешно справился с задачей. Сравнение различных сервисов геокодирования представлено в таблице, о которой идет речь в этом обсуждении на gis.stackexchange.com.

# code using Yahoo instead of Google
devtools::install_github("trestletech/rydn")
addresses<-lapply(faddress, function(x){
  tmp<-find_place(x, commercial=FALSE, 
             key="GET A KEY",
             secret="GET SECRET CODE")
  # this is the lazy way to do this, in a few cases more than one result is
  # provided and I have not looked into the details so I'm picking the first
  tmp<-tmp[1 ,c("quality",  "latitude", "longitude", "radius")]
  
})
geocodes<-do.call("rbind", addresses) %>%
  rename(lat=latitude, lon=longitude) %>%
  mutate(address=faddress)

Постобработка

Полученные результаты представлены в нижнем регистре, поэтому там, где необходимо, мы преобразуем символы в верхний регистр. Также выполним классификацию по производимому продукту: вино или пиво/сидр/другое. Кроме того, произведем небольшую дополнительную обработку. Для обработки результатов мы используем пакет dplyr (mutate, filter, select – функции из этого пакета).

# FUNCTION from help for chartr 
capwords<-function(s, strict = FALSE) {
  cap<-function(s) paste(toupper(substring(s, 1, 1)),
        {s<-substring(s, 2); if(strict) tolower(s) else s},
        sep = "", collapse = " " )
    sapply(strsplit(s, split = " "),
        cap, USE.NAMES = !is.null(names(s)))
}
# ---------------------------------------


full<-mutate(geocodes, name=fnames) %>%
  mutate(category=ifelse(grepl("Winery", name), 1, 2)) %>%
  mutate(addressUse=gsub("Ny", "NY", capwords(gsub(", usa", "", address)))) %>%
  mutate(street=sapply(strsplit(addressUse, ","), "[[", 1)) %>%
  mutate(city=sapply(strsplit(addressUse, ","), "[[", 2)) %>%
  filter(!grepl('Interlaken|Ithaca|Aurora|Seneca Falls', street)) %>%
  select(name, street, city, category, lat, lon)

head(full)
##                                              name           street
## 1                        Varick Winery & Vineyard 5102 New York 89
## 2 Bellwether Hard Cider / Bellwether Wine Cellars 9070 New York 89
## 3                                Lakeshore Winery 5132 New York 89
## 4              Knapp Winery & Vineyard Restaurant  Ernsberger Road
## 5                              Goose Watch Winery 5480 New York 89
## 6                     Cayuga Ridge Estates Winery 6800 New York 89
##          city category      lat       lon
## 1     Romulus        1 42.78128 -76.77388
## 2  Interlaken        2 42.58552 -76.67176
## 3     Romulus        1 42.77588 -76.77593
## 4     Romulus        1 42.76668 -76.79253
## 5     Romulus        1 42.75862 -76.77504
## 6        Ovid        1 42.69610 -76.74304

4) Отображаем данные на карте!

Для создания карты мы используем пакет leaflet из RStudio.

# Assign colors for our 3 categories
cols<-colorFactor(c("#3F9A82", "#A1CD4D", "#2D4565"), domain = full$category)


# Create the popup information with inline css styling
popInfo<-paste("<h4 style='border-bottom: thin dotted #43464C;
    padding-bottom:4px; margin-bottom:4px;
    font-family: Tahoma, Geneva, sans-serif;
    color:#43464C;'>", full$name, "</h4>
    <span style='color:#9197A6;'>", full$street, "<br>",
    paste(full$city, ", NY", sep=""), "</span>", sep="")


# Create the final map color-coded by type!
leaflet(data=full, height="650px", width="100%") %>%
    addCircles(lat = ~ lat, lng = ~ lon, color = ~cols(category), weight=2, opacity=1,
        fillOpacity=0.6, radius=500, popup = popInfo) %>%
    addTiles("http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png") %>%
    setView(-76.63, 42.685, zoom=10) %>% addLegend(
  position = 'bottomright',
  colors = cols(1:2),
  labels = c("Winery", "Brewery, Cidery, Other"), opacity = 1
)

Сайты с формами и кнопками

Не все сайты, с которыми приходится работать, предоставляют чистый список или таблицу на одной веб-странице. Существует множество инструментов для взаимодействия со страницами, на которых присутствуют кнопки, раскрывающиеся списки и подобные элементы. Одним из особенно полезных пакетов является RSelenium. Мы планируем добавить статью о RSelenium, но на данный момент я представлю только предварительный вариант кода без подробностей. Скажу лишь, что RSelenium можно использовать для имитации использования реального браузера.

Код, представленный ниже, открывает браузер, переходит на веб-страницу, нажимает кнопку «Search» (поиск), извлекает данные из таблицы с результатами поиска, а затем переходит на следующую страницу результатов поиска.

# Sneak preview of code for interacting with a web page with RSelenium
# a proper blog post with explanation will follow.

library(RSelenium)
# make sure you have the server
checkForServer()

# use default server 
startServer()
remDr<-remoteDriver$new()


# send request to server
url<-"https://programs.iowadnr.gov/animalfeedingoperations/FacilitySearch.aspx?Page=0"
remDr$open(silent = TRUE) #opens a browser
remDr$navigate(url)


# identify search button and click
searchID<-'//*[@id="ctl00_foPageContent_SearchButton"]'
webElem<-remDr$findElement(value = searchID)
webElem$clickElement()

# identify the table
tableID<-'//*[@id="ctl00_foPageContent_Panel1"]/div[2]/table'
webElem<-remDr$findElement(value = tableID)

doc<-htmlParse(remDr$getPageSource()[[1]])

tabledat<-readHTMLTable(doc)[[17]]
tabledat[,]<-lapply(tabledat[,],
    function(x) gsub("ÂÂ", "", as.character(x)))
tabledat<-tabledat[-nrow(tabledat),-1]
# go to next page
nextID<-'//*[@id="ctl00_foPageContent_FacilitySearchRepeater_ctl11_PagePlus1"]'
webElem<-remDr$findElement(value = nextID)
webElem$clickElement()

По материалам: Zevross.com

Добавить комментарий

Ваш e-mail не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =