Приготовим пакетики

packages <- c("readr", "dplyr", "readxl")
# install.packages(packages)
library(readr)
library(readxl)
library(dplyr)

HTTP - HyperText Transfer Protocol

HTTP – это система правил обмена данными между компьютерами. По сути это “язык интернета”. Вы сталквиваетесь с протоколом HTTP каждый день, когда сидите в интернете. Допустим, вы открываете какой-то сайт, ваш компьютер - клиент (client) делает запрос (request) к серверу (server). Затем сервер отправляет обратно ответ (response), в котором содержатся файлы, с помощью которых получается веб-страница.

Вы можете не только попросить какие-то данные, но и, например, отправить их. Для разных задача есть разные методы для обращения к серверу. Ниже представлены некоторые из них.

  1. GET
  2. POST
  3. HEAD
  4. PUT

Нас будет интересовать только получение данных от сервера, поэтому мы сконцентрируем свое внимание на методе GET.

Простые примеры

read_csv

На самом деле вы уже использовали этот метод ранее, когда использовали функцию read_csv передавая ей url, где лежит нужная нам csv.

url <- 'https://github.com/ahmedushka7/R/raw/master/docs/homeworks/test/data/covid.csv'
df <- read_csv(url)
df %>% head(n = 3)
## # A tibble: 3 x 5
##   country     state date       confirmed_cases fatalities
##   <chr>       <chr> <date>               <dbl>      <dbl>
## 1 Afghanistan <NA>  2020-01-23               0          0
## 2 Afghanistan <NA>  2020-01-24               0          0
## 3 Afghanistan <NA>  2020-01-25               0          0

Функция read_csv под капотом вызывает метод GET и получает csv.

download.file

Не всегда можно подгрузить файл из интернета сразу в R. Например, это не работает с excel файлами.

url <- "https://github.com/ahmedushka7/R/blob/master/docs/scripts/hse_data_analysis/sem_7/data_extra/file_excel.xlsx?raw=true"
df <- read_excel(url)
## Error: `path` does not exist: 'https://github.com/ahmedushka7/R/blob/master/docs/scripts/hse_data_analysis/sem_7/data_extra/file_excel.xlsx?raw=true'

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

С помощью функции file.path можно создать путь, куда сохранится файл. Она просто расставляет слэши (/).

url <- "https://github.com/ahmedushka7/R/blob/master/docs/scripts/hse_data_analysis/sem_7/data_extra/file_excel.xlsx?raw=true"
path <- file.path("~", "test.xlsx")
print(path)
## [1] "~/test.xlsx"

А теперь используем функцию download.file.

download.file(url, path)

Теперь файл у нас на компьютере и мы можем его подгрузить.

df <- read_excel(path)
df
## # A tibble: 3 x 4
##   Ахмед     `19` М     `7.75`
##   <chr>    <dbl> <chr>  <dbl>
## 1 Миша        20 М       6.65
## 2 Алина       25 Ж       5.56
## 3 Ангелина    23 Ж       1.15

Если вы не хотите оставлять файл у себя на компьютере, то можно его удалить.

file.remove(path)
## [1] TRUE

Работа с HTTP

Основы

Если вы хотите делать свои HTTP запросы, то можно использовать библиотеку httr. Давайте установим и подгрузим её.

# install.packages("httr")
library(httr)

Давайте попробуем сделать свой первый HTTP запрос. Для этого просто используем функцию GET, в которую передадим url и запишем результат, который выдаст нам сервер в переменную response.

url <- "http://www.example.com/"
response <- GET(url)

Что такое response? Это list, в котором хранится некоторая метаинформация о запросе, а также сами данные, которые мы хотели получить.

print(response)
## Response [http://www.example.com/]
##   Date: 2020-11-08 21:41
##   Status: 200
##   Content-Type: text/html; charset=UTF-8
##   Size: 1.26 kB
## <!doctype html>
## <html>
## <head>
##     <title>Example Domain</title>
## 
##     <meta charset="utf-8" />
##     <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
##     <meta name="viewport" content="width=device-width, initial-scale=1" />
##     <style type="text/css">
##     body {
## ...

Из метаинформации стоит обратить на Status. Это код, который говорит о том, отработал ли запрос или нет.

  • 200 – все отлично!
  • 404 – знаменитая ошибка, означающая что страница не найдена
  • 5xx – семейство ошибок, означающие неполадки со стороны сервера

Все ошибки можно посмотреть по ссылке.

Статус-код можно получить несколькими способами.

print(response$status_code)  # обращение к листу
## [1] 200
print(status_code(response))  # использование функции
## [1] 200

Другая метаинформация обычно не нужна, поэому хочется получить те данные (контент), которые мы просили. Можно получить их с помощью функции content.

data <- content(response)
print(data)
## {html_document}
## <html>
## [1] <head>\n<title>Example Domain</title>\n<meta charset="utf-8">\n<meta http ...
## [2] <body>\n<div>\n    <h1>Example Domain</h1>\n    <p>This domain is for use ...

По дефолту функция content сама парсит полученные данные. Но если вы хотите получить “голый” текст, то можете указать второй параметр as = "text".

data <- content(response, as = "text")
print(data)
## [1] "<!doctype html>\n<html>\n<head>\n    <title>Example Domain</title>\n\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style type=\"text/css\">\n    body {\n        background-color: #f0f0f2;\n        margin: 0;\n        padding: 0;\n        font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n        \n    }\n    div {\n        width: 600px;\n        margin: 5em auto;\n        padding: 2em;\n        background-color: #fdfdff;\n        border-radius: 0.5em;\n        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);\n    }\n    a:link, a:visited {\n        color: #38488f;\n        text-decoration: none;\n    }\n    @media (max-width: 700px) {\n        div {\n            margin: 0 auto;\n            width: auto;\n        }\n    }\n    </style>    \n</head>\n\n<body>\n<div>\n    <h1>Example Domain</h1>\n    <p>This domain is for use in illustrative examples in documents. You may use this\n    domain in literature without prior coordination or asking for permission.</p>\n    <p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>\n</div>\n</body>\n</html>\n"

Тот ужас, который вы видите перед глазами называется HTML страницей. Это код страницы, которую вы видите в браузере. О контенте, который мы получаем и HTML мы поговорим позже.

Что за контент такой?

Ну сделали мы запрос, получили статус код 200, что дальше? Контент, который мы получаем может быть абсолютно разным. Но зачастую, это либо HTML, либо JSON. С HTML мы разберемся на следующем занятии. А вот про JSON поговорим поподробнее.

JSON

JSON это key-value структура. Есть ключ, а ему соотвествует какое-то значение. По такой структуре очень удобно перемещаться. В R нет JSON, но есть структура данных list, которая очень похожа. Если вам все таки потребуется работать с JSON, то попробуйте пакет jsonlite.

list

Структура данных list (список) очень удобна, так как может содержать в себе абсолютно любые другие объекты. В списке может содержаться другой список. Это очень напоминает JSON. Список можно создать с помощью функции list.

my_list <- list(10, "R", c(-12, 13, 14))
print(my_list)
## [[1]]
## [1] 10
## 
## [[2]]
## [1] "R"
## 
## [[3]]
## [1] -12  13  14
print(class(my_list))
## [1] "list"

Очень удобно здесь использовать функцию str, которая может показать структуру списка.

str(my_list)
## List of 3
##  $ : num 10
##  $ : chr "R"
##  $ : num [1:3] -12 13 14

В списках можно давать наименования.

my_list <- list(number = 10, language = "R", ages = c(20, 30, 44))
str(my_list)
## List of 3
##  $ number  : num 10
##  $ language: chr "R"
##  $ ages    : num [1:3] 20 30 44

В списке могу быть другие списки.

my_list = list(flist = list(1, 2), slist = list(3, 4))
str(my_list)
## List of 2
##  $ flist:List of 2
##   ..$ : num 1
##   ..$ : num 2
##  $ slist:List of 2
##   ..$ : num 3
##   ..$ : num 4

Создавать научились, осталось научиться работать с ними. Есть три типа доступа к элементам.

  1. [] – берет срез текущего списка, результат список
  2. [[]] – переходит в элемент, который содержится в текущем списке
  3. $ – аналогично [[]], только более просто
my_list <- list(number = 10, name = "Ahmed", na = NA, other_list = list(array = rep(1, 4)))
my_list[1:3] # вернули list с его первыми 3 элементами
## $number
## [1] 10
## 
## $name
## [1] "Ahmed"
## 
## $na
## [1] NA
my_list[[1]] # вытащили значение второго элемента
## [1] 10
my_list$number # аналогично, только доступ по имени
## [1] 10
my_list$other_list$array # можно делать даже так
## [1] 1 1 1 1

Большой функционал для работы с list есть в пакете rlist.

Пример

url_sw4 <- "http://www.omdbapi.com/?apikey=72bc447a&i=tt0076759&r=json"

response <- GET(url_sw4)
con <- content(response)
str(con)
## List of 25
##  $ Title     : chr "Star Wars: Episode IV - A New Hope"
##  $ Year      : chr "1977"
##  $ Rated     : chr "PG"
##  $ Released  : chr "25 May 1977"
##  $ Runtime   : chr "121 min"
##  $ Genre     : chr "Action, Adventure, Fantasy, Sci-Fi"
##  $ Director  : chr "George Lucas"
##  $ Writer    : chr "George Lucas"
##  $ Actors    : chr "Mark Hamill, Harrison Ford, Carrie Fisher, Peter Cushing"
##  $ Plot      : chr "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee and two droids to save the galaxy from"| __truncated__
##  $ Language  : chr "English"
##  $ Country   : chr "USA"
##  $ Awards    : chr "Won 6 Oscars. Another 52 wins & 29 nominations."
##  $ Poster    : chr "https://m.media-amazon.com/images/M/MV5BNzVlY2MwMjktM2E4OS00Y2Y3LWE3ZjctYzhkZGM3YzA1ZWM2XkEyXkFqcGdeQXVyNzkwMjQ"| __truncated__
##  $ Ratings   :List of 3
##   ..$ :List of 2
##   .. ..$ Source: chr "Internet Movie Database"
##   .. ..$ Value : chr "8.6/10"
##   ..$ :List of 2
##   .. ..$ Source: chr "Rotten Tomatoes"
##   .. ..$ Value : chr "92%"
##   ..$ :List of 2
##   .. ..$ Source: chr "Metacritic"
##   .. ..$ Value : chr "90/100"
##  $ Metascore : chr "90"
##  $ imdbRating: chr "8.6"
##  $ imdbVotes : chr "1,208,256"
##  $ imdbID    : chr "tt0076759"
##  $ Type      : chr "movie"
##  $ DVD       : chr "N/A"
##  $ BoxOffice : chr "N/A"
##  $ Production: chr "Lucasfilm Ltd."
##  $ Website   : chr "N/A"
##  $ Response  : chr "True"

Рейтинг фильма на Metacritic.

con$Ratings[[3]]$Value
## [1] "90/100"

Проблемы

Большое количество запросов

Обычно хочется сделать не один HTTP запрос, а очень много. Давайте попробуем сделать 30 запросов на сайт Авито. Будем следить за статус кодом и с помощью функции Sys.time() посмотрим как часто будут делаться запросы.

for (i in 1:20) {
  response <- GET(url = "https://www.avito.ru/moskva")
  print(paste("Статус код = ", status_code(response), ", время запроса = ", Sys.time()))
}

Если запустить код выше, то будет видно, что запросы делаются буквально каждую секунду. Кажется, что нет ничего плохого в этом, но есть одно но. Сервер не любит, когда один и тот же клиент отправляет много запросов. Если посмотреть на статус код, то сначала он равен 200, а потом будет равен 429 (Too Many Requests). Сервер заблокировал нас на какое-то время.

Нужно умереть пыл и делать запросы не каждую секунду, а с некоторой задержкой. Самый простой способ, это использовать функцию Sys.sleep. Она принимает на вход количество секунд, на которое нужно “заснуть”. То есть R засыпает на какое-то время и ничего не делает. Давайте делать запрос каждые 4 секунды.

for (i in 1:20) {
  response <- GET(url = "https://www.avito.ru/moskva")
  print(paste("Статус код = ", status_code(response), ", время запроса = ", Sys.time()))
  Sys.sleep(4)
}

Более элегантный способ, это использовать функцию slowly из пакета purrr.

# install.packages("purrr")
library(purrr)

Если у вас есть готовая функция, которую вы хотите “замедлить”, то можно это сделать следующим образом.

request <- function(url){ # наша функция
  response <- GET(url = url)
}

slowed_request <- slowly(~request, rate = rate_delay(4)) # создаем новую функцию с задержкой в 4 секунды

for (i in 1:20) {
  response <- slowed_request("https://www.avito.ru/moskva")
  print(paste("Статус код = ", status_code(response), ", время запроса = ", Sys.time()))
}

Притвориться человеком

Когда мы делаем HTTP запрос, вместе с ним отправляется некоторая мета информация о нас (клиенте). Можно увидеть эту метаинформацию перейдя на сайт https://httpbin.org/headers. Нас интересует User-Agent. Можно увидеть, что там описан браузер, а также устройство с которого мы зашли на сайт. Давайте попробуем сделать аналогичный запрос через R.

response <- GET("https://httpbin.org/headers")
cont <- content(response)
cont$headers$`User-Agent`
## [1] "libcurl/7.64.1 r-curl/4.3 httr/1.4.2"

Видно, что User-Agent совсем другой. Здесь перечислены некоторые пакеты операционной системы, с помощью которых делается HTTP запрос.

Так как эта информация приходит на сервер, сервер может увидеть, что запрос сделал не человек, а “робот”. И просто заблокировать нам доступ (обычно это ошибка 403). К счастью, мы можем менять User-Agent так как мы хотим.

response <- GET(url = "https://httpbin.org/headers", 
                user_agent("Hey, my name is Ahmedushka!"))
cont <- content(response)
cont$headers$`User-Agent`
## [1] "Hey, my name is Ahmedushka!"

Таким образом, мы можем притвориться настоящим человеком.

Установить User-Agent можно глобально, он будет применяться ко всем запросам. Это бывает удобно!

set_config(add_headers(`User-Agent` = "Hey, my name is Ahmedushka!"))
response <- GET('http://example.com')
cont <- content(response)
cont$headers$`User-Agent`
## NULL

Парсим через посредников

Даже если вы меняете User-Agent и делаете запросы не так часто, сервер может все равно разозлиться, дело в том, что ваш ip адрес остается таким же.

response <- GET(url = "https://httpbin.org/ip")
cont <- content(response)
cont$origin
## [1] "109.252.51.59"

Поменять свой ip нельзя, но можно попросить другие серверы с другим ip сделать нужный нам запрос. Для этого есть proxy серверы. Их можно найти на разных сайтах. Вот некоторые из них.

К сожалению, некоторые бесплатные прокси быстро отваливаются, а некоторые и вовсе не работают. Поэтому иногда пишут парсер для поиска хороших прокси, а потом уже с ними парсят то, что нужно.

response <- GET(url = "https://httpbin.org/ip",
                use_proxy(url = "36.89.148.161", port = 8080))
## Error in curl::curl_fetch_memory(url, handle = handle): Timeout was reached: [httpbin.org] Operation timed out after 10001 milliseconds with 0 out of 0 bytes received
cont <- content(response)
cont$origin
## [1] "109.252.51.59"