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

Создание карты на основе географических данных, извлеченных из Интернет, состоит из трех этапов

  1. извлечение адресов объектов;
  2. геокодирование: сопоставление адресу каждого объекта его географических координат;
  3. отображение на карте.

Извлечение адресов и названий магазинов

По идее, начать работу следовало бы с поиска адресов магазинов. К счастью, большая часть из них приведена на этой странице. Но есть и плохая новость: данные слабо структурированы. Они располагаются в тексте так, что не связаны ни с одним определенным элементом страницы и подобрать к ним единый путь по XPath или CSS-селекторам не удается. Так что для извлечения данных придется воспользоваться текстовым поиском.

Извлечем содержимое страницы.

> library(rvest)    # для извлечения данных
> url <-  "http://metrowarszawa.gazeta.pl/metrowarszawa/1,141635,17512272.html"
> web_page <- html(url)

Выделим из веб-страницы web_page текст, а затем в нем — подстроки, соответствующие шаблону адреса. Но сперва подключим библиотеку операций со строками

> library(stringr)  # для текстового поиска в строке

Адрес начинается c ul., например:

ul. Czarnomorska 13

Шаблон адреса выглядит так:

ul. + ПРОБЕЛ + Прописная буква + один или более каких-то символов + ПРОБЕЛ + одна или более цифр

и записывается следующим образом

ul.s[A-Z].+?s[0-9]+

Восклицательный знак ? ограничивает "жадность" операторов .+.

Поясним, что означает "жадность". Рассмотрим строку

some text ul. Czarnomorska 13 some text 15 some text

Шаблону поиска соответствуют следующие подстроки

ul. Czarnomorska 13
ul. Czarnomorska 13 some text 15

Какая из них будет выдана в результате поиска?

По умолчанию, поведение .+ — "жадное". Он "захватывает" самую длинную подстроку, соответствующую шаблону, которую только можно захватить, т.е.

ul. Czarnomorska 13 some text 15

Но нас это не устраивает. Нам наоборот нужна самая короткая подстрока. Чтобы указать, что поведение .+ должно быть "нежадным" к ним и добавляется ?.

Итак, выделим текст, а из него нужные подстроки

> web_page %>% html_text() %>% str_extract_all(pattern = "ul.s[A-Z].+?s[0-9]+")
[leaflet](1]]
[1] "ul. Czarnomorska 13" "ul. Lektykarska 26"  "ul. Dobra 12"        "ul. Sielecka 2"      "ul. Smolna 11"       "ul. Zamoyskiego 26" 
[7] "ul. Stawki 19"       "ul. Wiktorska 89" 

Результатом будет список

> str(web_page %>% html_text() %>% str_extract_all(pattern = "ul.s[A-Z].+?s[0-9]+"))
List of 1
 $ : chr [1:8] "ul. Czarnomorska 13" "ul. Lektykarska 26" "ul. Dobra 12" "ul. Sielecka 2" ...

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

> address <- web_page %>% html_text() %>% str_extract_all(pattern = "ul.s[A-Z].+?s[0-9]+") %>% unlist()
> address
[1] "ul. Czarnomorska 13" "ul. Lektykarska 26"  "ul. Dobra 12"        "ul. Sielecka 2"      "ul. Smolna 11"       "ul. Zamoyskiego 26" 
[7] "ul. Stawki 19"       "ul. Wiktorska 89"   
> str(address)
 chr [1:8] "ul. Czarnomorska 13" "ul. Lektykarska 26" "ul. Dobra 12" "ul. Sielecka 2" "ul. Smolna 11" "ul. Zamoyskiego 26" ...

В адреса нужно добавить, что все они относятся к Варшаве

> address <- paste0(address, ", Warszawa")
> address
[1] "ul. Czarnomorska 13, Warszawa" "ul. Lektykarska 26, Warszawa"  "ul. Dobra 12, Warszawa"        "ul. Sielecka 2, Warszawa"     
[5] "ul. Smolna 11, Warszawa"       "ul. Zamoyskiego 26, Warszawa"  "ul. Stawki 19, Warszawa"       "ul. Wiktorska 89, Warszawa"   

Названия магазинов выделены жирным шрифтом. Проблема в том, что так выделяются не только они:

> web_page  %>%  html_nodes("b") %>% html_text()
 [1] "SpodobaЕ‚o ci siД™? Polub nas"                 "rower miejski"                                 "rower szosowy"                                
 [4] "rower trekkingowy"                             "rower gГіrski"                                 "Propaganda Rowerowa - NaprawdД™ Е‚adne rowery"
 [7] "Bikerstudio - Kolorowe ostre koЕ‚o"            "Rowery Bajery - Szosowi potentaci"             "JR Concept - Taniej niЕј w centrum"           
[10] "Ostre KoЕ‚o - StwГіrz swГіj rower"             "Dwa Osiem - Sklep, serwis, kawa i rowery "     "Komis rowerowy - rowerowy second hand "       
[13] "Milou - rowerowa pereЕ‚ka z Mokotowa "         " "                                            

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

> tmp <- web_page  %>%  html_nodes("b") %>% html_text()
> Encoding(tmp) <- "UTF-8"
> tmp
 [1] "Spodobało ci się? Polub nas"                 "rower miejski"                               "rower szosowy"                              
 [4] "rower trekkingowy"                           "rower górski"                                "Propaganda Rowerowa - Naprawdę ładne rowery"
 [7] "Bikerstudio - Kolorowe ostre koło"           "Rowery Bajery - Szosowi potentaci"           "JR Concept - Taniej niż w centrum"          
[10] "Ostre Koło - Stwórz swój rower"              "Dwa Osiem - Sklep, serwis, kawa i rowery "   "Komis rowerowy - rowerowy second hand "     
[13] "Milou - rowerowa perełka z Mokotowa "        " "                                          

Вернемся к названиям магазинов

> .Last.value %>% str_match("(.+)s-")
      [,1]                    [,2]                 
 [1,] NA                      NA                   
 [2,] NA                      NA                   
 [3,] NA                      NA                   
 [4,] NA                      NA                   
 [5,] NA                      NA                   
 [6,] "Propaganda Rowerowa -" "Propaganda Rowerowa"
 [7,] "Bikerstudio -"         "Bikerstudio"        
 [8,] "Rowery Bajery -"       "Rowery Bajery"      
 [9,] "JR Concept -"          "JR Concept"         
[10,] "Ostre Koło -"          "Ostre Koło"         
[11,] "Dwa Osiem -"           "Dwa Osiem"          
[12,] "Komis rowerowy -"      "Komis rowerowy"     
[13,] "Milou -"               "Milou"              
[14,] NA                      NA                        

str_match() возвращает не только всю подстроку, удовлетворяющую шаблону, но и ее часть, помещенную в скобки, а это как раз то, что нам нужно. Таким образом, название магазина размещается во 2-й колонке. .Last.value означает результат работы последней команды, в нашем случае — список строк, выделенных жирным шрифтом.

> tmp <- .Last.value %>% .[,2]
> tmp
 [1] NA                    NA                    NA                    NA                    NA                    "Propaganda Rowerowa"
 [7] "Bikerstudio"         "Rowery Bajery"       "JR Concept"          "Ostre Koło"          "Dwa Osiem"           "Komis rowerowy"     
[13] "Milou"               NA                   

Выделим все элементы tmp, кроме тех, что "Not Available" и сохраним результат в name

> tmp[!is.na(tmp)]
[1] "Propaganda Rowerowa" "Bikerstudio"         "Rowery Bajery"       "JR Concept"          "Ostre Koło"          "Dwa Osiem"          
[7] "Komis rowerowy"      "Milou"              
> name <- .Last.value

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

Объединим адреса и названия магазинов в таблицу (data.frame). Кроме того, поместим в таблицу координаты магазина — долготу и широту. Их находят при помощи функции geocode() из пакета ggmap. Сам пакет предназначен для отображения объектов на Google Maps и OpenStreetMaps. Загрузим его:

> library(ggmap)

и создадим таблицу

locations <- data.frame(address = address,      # адрес магазина
                        geocode(address),       # долгота и широта магазина
                        stores = name,          # название
                        stringsAsFactors = F)   # сохранить символьный формат                       
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Czarnomorska+13,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Lektykarska+26,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Dobra+12,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Sielecka+2,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Smolna+11,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Zamoyskiego+26,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Stawki+19,+Warszawa&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=ul.+Wiktorska+89,+Warszawa&sensor=false
> locations
                        address      lon      lat              stores
1 ul. Czarnomorska 13, Warszawa 21.04576 52.17992 Propaganda Rowerowa
2  ul. Lektykarska 26, Warszawa 20.96751 52.28094         Bikerstudio
3        ul. Dobra 12, Warszawa 21.03030 52.23706       Rowery Bajery
4      ul. Sielecka 2, Warszawa 21.03958 52.20277          JR Concept
5       ul. Smolna 11, Warszawa 21.02406 52.23293          Ostre Kolo
6  ul. Zamoyskiego 26, Warszawa 21.04969 52.24737           Dwa Osiem
7       ul. Stawki 19, Warszawa 20.98465 52.25060      Komis rowerowy
8    ul. Wiktorska 89, Warszawa 21.00736 52.19797               Milou

Итак, координаты магазинов получены, данные сведены в таблицу — пора приступать к завершающему этапу работы.

Отображение на карте

Этим занимается пакет leaflet:

> library(leaflet)

Сформируем текст пояснений, которые будут всплывать на карте при нажатии на маркер объекта

store_popup <- paste0("<b >", locations$stores, "</b >", "< br >", 
                             locations$address)

В первой строке жирным шрифтом пишется название магазина, во второй — его адрес.

Функция leaflet() создает объект-карту, addProviderTiles() — добавляет на нее листы (tiles) карты Esri.WorldTopoMap, наконец, addMarkers() — добавляет маркеры объектов с заданными долготой и широтой

m <- leaflet(data = locations) %>% 
     addProviderTiles("Esri.WorldTopoMap") %>% 
     addMarkers(locations$lon, locations$lat, popup = store_popup)
m

byke_map.png



Комментарии

comments powered by Disqus