Что нам понадобится?
Ознакомиться со статьями https://teletype.in/@h0h1_hr_analytics
Статьи очень помогли разобраться в деталях hh. Но есть устаревшая информация, в связи с чем, решил актуализировать детали.
Получить OAuth токен hh — https://dev.hh.ru
Получение токена очень важно, так как его наличие позволит вам отправлять большое кол-во запросов к API без блокировки со стороны hh.
Изучить документацию на GitHub
Использовать библиотеки R
library(tidyverse) library(httr2) library(furrr)
Начнем с HH: получение токена
Вам необходимо зарегистрироваться на сайте hh.ru. После чего Вы сможете подать заявку на регистрацию своего приложения.
Чтобы это сделать перейдите по ссылке https://dev.hh.ru
Там будет раздел “Регистрация приложения”. Кликаете на кнопку
Добавить приложение
Заполняете все поля. Сильно можно не “заморачиваться”. В
Redirect URI
указываете любую ссылку, которая имеет к вам отношение. В моем случае, я указал корпоративный сайт.После заполнения формы, нажимаете на кнопку
Добавить.
Примерно через неделю, если со стороны hh не будет к вам вопросов, вам одобрят заявку.Что нам понадобится?
Redirect URI
Client ID
Client Secret
Code
Как мы видим, у нас нет одно из параметров —
Code
Для его получения нам необходимо:
Скопировать ссылку
https://hh.ru/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI
где: YOUR_CLIENT_ID необходимо заменить на Client ID, а YOUR_REDIRECT_URI — на Redirect URI.
Переходим по получившейся ссылке и нажимаем
Продолжить
У вас сгенерируется новая ссылка типа:
https://eco-hotel.ru/?code=N90K2RSM216W9UNBNK121MWC2JEW8RHV7HHN7RW714TE7QWW9P1WJJ0WPP8TWG
Ваш код указан после
?code
. Его нужно скопировать и сохранить. Он вам понадобится. Стоит обратить внимание, что он меняется каждый раз, когда вы инициируете шаги выше.На этом работа с hh заканчивается. Дальше понадобится R, чтобы сгенерировать токен.
Переходим к R: получение токена
Библиотеки
library(tidyverse) library(httr2)
Сохраняем переменные
client_id <- 'YOUR_CLIENT_ID' client_secret <- 'YOUR_CLIENT_SECRET' code <- 'YOUR_CODE'
Также понадобится POST запрос к hh.ru для получения токена по ссылке https://api.hh.ru/token
#POST запрос к hh.ru для получения токена oauth_endpoint <- "https://api.hh.ru/token"
#Запрос токена, используем библиотеку httr2 TOKEN <- request(oauth_endpoint) %>% req_body_form( grant_type = "authorization_code", client_id = client_id, client_secret = client_secret, code = code, redirect_uri = "https://eco-hotel.ru" ) %>% req_perform() %>% resp_body_json()
Здесь стоит отметить, что переменная
grant_type
должна стоять первой, хотя в документации это явно не указано. Название переменнойgrant_type
это и естьauthorization_code
. Это НЕ код, который вам нужно получить, а текстовая переменная.После выполнении запроса вы получите переменную
TOKEN
. Извлечь сам токен можно при помощи командыTOKEN_ID <- TOKEN$access_token
Справочник параметров
В документации hh есть раздел “Справочники”. Он вам точно понадобится, если вы хотите более тонко настроить парсинг.
Регионы
Чтобы получить список всех регионов, который есть на hh, достаточно выполнить следующий запрос:
areas_url <- "https://api.hh.ru/areas" areas <- request(areas_url) %>% req_perform() %>% resp_body_json()
Вы получите список. Со списком работать не очень удобно, поэтому можно сформировать
dataframe
для индетификацииid
тех регионов, которые вас интересуют. Для России это можно сдлеать так:areas_df <- map_dfr(areas[[1]]$areas, ~tibble(id = .x$id, name = .x$name))
Меня интересовало три региона, которые я и выбрал
areas_id <- map(areas[[1]]$areas, ~.x$id) %>% keep(~ . %in% c("1", "2019", "2"))
Все тоже самое мы можем сдлеать и для профессиональных ролей
Профессиональные роли
professional_roles_url <- "https://api.hh.ru/professional_roles" professional_roles <- request(professional_roles_url) %>% req_perform() %>% resp_body_json()
Здесь преобразование списка в
dataframe
меняется, так как нужно было понять к какой категории относится та или иная профессиональная роль:roles_df <- professional_roles$categories %>% map_dfr( ~{ tibble( id_categories = .x$id, name_categories = .x$name, id_roles = map_chr(.x$roles, "id"), name_roles = map_chr(.x$roles, "name") ) }) %>% distinct(id_roles, .keep_all = TRUE)
Мой список выглядит так:
roles_id <- list("8", "90", "89", "130", "72", "74", "94", "40", "113", "87", "76", "51", "26", "3")
Опыт работы
hh.ru имеет ограничения по количеству запросов в каждой категории:
Максимум 2000 записей на категорию
Ограничение в 100 элементов на страницу
Это означает, что в одной категории можно просмотреть не более:
20 страниц
100 вакансий на каждой странице
В связи с этим, нам понадобится критерий, который позволит “раздробить” запросы на более мелкие части. В моем случае, отлично подходит
опыт работы
:## Опыт работы ---- exp_id <- list('noExperience', #нет опыта 'between1And3', #от 1 до 3 лет 'between3And6', #от 3 до 6 лет 'moreThan6') #более 6 летрсонала
Функции, которая преобразует JSON ответ в dataframe
# Функция, которая разворачивает ответ JSON и преобразовывает его в dataframe ---- get_vacancies_inter <- function(vacancies) { # Извлекаем элемент 'items' из входящего JSON-объекта и применяем функцию 'map_dfr' # для каждой вакансии, объединяя результаты в один dataframe vacancies$items %>% map_dfr(~ { tibble( # Извлекаем и сохраняем идентификатор вакансии id = .x$id, # Извлекаем и сохраняем название вакансии name = .x$name, # Извлекаем и сохраняем идентификатор области area_id = .x$area$id, # Извлекаем и сохраняем название области area_name = .x$area$name, # Извлекаем и сохраняем минимальную зарплату, если она указана, иначе сохраняем NA salary_from = .x$salary$from %||% NA_real_, # Извлекаем и сохраняем максимальную зарплату, если она указана, иначе сохраняем NA salary_to = .x$salary$to %||% NA_real_, # Извлекаем и сохраняем информацию о том, указана ли зарплата до вычета налогов salary_gross = .x$salary$gross %||% NA, # Извлекаем и сохраняем тип графика работы schedule = .x$schedule$name, # Преобразуем и сохраняем дату публикации вакансии в формат "YYYY-MM-DD" published_at = format(as.Date(.x$published_at, "%Y-%m-%d")), # Преобразуем и сохраняем дату создания вакансии в формат "YYYY-MM-DD" created_at = format(as.Date(.x$created_at, "%Y-%m-%d")), # Извлекаем и сохраняем альтернативный URL вакансии alternate_url = .x$alternate_url, # Извлекаем и сохраняем название работодателя employer = .x$employer$name, # Извлекаем и сохраняем идентификаторы профессиональных ролей professional_roles_id = map_chr(.x$professional_roles, "id"), # Извлекаем и сохраняем названия профессиональных ролей professional_roles_name = map_chr(.x$professional_roles, "name"), # Извлекаем и сохраняем требуемый опыт работы experience = .x$experience$name, # Извлекаем и сохраняем тип занятости employment = .x$employment$name ) }) }
Это не полный список того, что можно “вытащить” из JSON ответа. В моем случае, данных параметров достаточно.
Функция парсинга API hh.ru
# Функция для получения списка вакансий с фильтрацией по опыту, области и профессиональной роли ---- get_vacancies_result <- function(page, experience = NULL, area = NULL, professional_role = NULL) { # Выполняем HTTP-запрос к API для получения списка вакансий vacancies <- request(vacancies_url) %>% # Устанавливаем заголовок авторизации с токеном req_headers( Authorization = paste("Bearer", TOKEN_ID) ) %>% # Устанавливаем параметры URL-запроса req_url_query( per_page = 100, # Количество вакансий на странице only_with_salary = TRUE, # Только вакансии с указанной зарплатой page = page, # Номер страницы experience = experience, # Требуемый опыт работы (если указан) professional_role = professional_role, # Профессиональная роль (если указана) area = area # Область (если указана) ) %>% # Выполняем запрос req_perform() %>% # Преобразуем тело ответа в формат JSON resp_body_json() # Если в полученном списке вакансий нет элементов, возвращаем NULL if (length(vacancies$items) == 0) { return(NULL) } # Преобразуем полученные вакансии в dataframe, используя вспомогательную функцию vacancies_df_inter <- get_vacancies_inter(vacancies) # Возвращаем полученный dataframe return(vacancies_df_inter) }
Создаем параметры для нашей функции get_vacancies_result
# Создание сетки параметров для запросов вакансий ---- params <- expand_grid( # Задаем вектор значений для параметра 'page' от 0 до 19 (включительно), # что соответствует страницам результатов поиска page = 0:19, # Используем вектор 'roles_id', содержащий идентификаторы профессиональных ролей, # для параметра 'professional_role' professional_role = roles_id, # Используем вектор 'exp_id', содержащий идентификаторы опыта работы, # для параметра 'experience' experience = exp_id, # Используем вектор 'areas_id', содержащий идентификаторы областей, # для параметра 'area' area = areas_id )
Функция
expand_grid
из пакетаtidyr
создает все возможные комбинации из заданных значений параметров. Она принимает несколько векторов и возвращаетdataframe
, где каждая строка представляет одну из возможных комбинаций этих векторов.В результате, вы должны получить следующее:
# A tibble: 3,360 × 4 page professional_role experience area <int> <list> <list> <list> 1 0 <chr [1]> <chr [1]> <chr [1]> 2 0 <chr [1]> <chr [1]> <chr [1]> 3 0 <chr [1]> <chr [1]> <chr [1]> 4 0 <chr [1]> <chr [1]> <chr [1]> 5 0 <chr [1]> <chr [1]> <chr [1]> 6 0 <chr [1]> <chr [1]> <chr [1]> 7 0 <chr [1]> <chr [1]> <chr [1]> 8 0 <chr [1]> <chr [1]> <chr [1]> 9 0 <chr [1]> <chr [1]> <chr [1]> 10 0 <chr [1]> <chr [1]> <chr [1]> # ℹ 3,350 more rows # ℹ Use `print(n = ...)` to see more rows
Выполняем запрос к API: используем пакет furrr
Код ниже выполняет параллельный запрос к API для получения списка вакансий по заданным параметрам, обрабатывает результаты и измеряет время выполнения.
# Устанавливаем URL API для получения вакансий vacancies_url <- "https://api.hh.ru/vacancies" # Устанавливаем токен для авторизации TOKEN_ID <- "YOUR_TOKEN" # Настраиваем план параллельного выполнения задач с использованием нескольких сессий future::plan(multisession) # Записываем текущее время для измерения времени выполнения start.time <- Sys.time() # Используем 'params' для выполнения параллельных запросов к API и обработки результатов vacancies_df <- params %>% # Выполняем функцию 'get_vacancies_result' для каждого набора параметров параллельно future_pmap_dfr(get_vacancies_result) %>% # Удаляем дубликаты по идентификатору вакансии, сохраняя все остальные столбцы distinct(id, .keep_all = TRUE) %>% # Добавляем столбец 'region' с использованием функции 'case_when' mutate( region = case_when( area_id == 1 ~ "МСК", # Если 'area_id' равно 1, устанавливаем регион "МСК" area_id == 2 ~ "СПБ", # Если 'area_id' равно 2, устанавливаем регион "СПБ" .default = "МО" # Для всех остальных значений устанавливаем регион "МО" ) ) # Записываем текущее время для измерения времени выполнения end.time <- Sys.time() # Вычисляем время, затраченное на выполнение запросов и обработку данных time.taken <- end.time - start.time # Выводим время выполнения print(time.taken)
Тут стоит отметить, что добавлять столбец
region
, с использованием функцииcase_when
— необязательно. В данном случае, каждый город московской области имеет свойid
, поэтому было принято решение добавить данную переменную.Данный запрос выполняется ~
Time difference of 4.136553 mins
.Результат, выполнения функции:
# A tibble: 54,370 × 17 id name area_id area_name salary_from salary_to salary_gross schedule <chr> <chr> <chr> <chr> <dbl> <dbl> <lgl> <chr> 1 104336620 Администрато… 1 Москва 68000 NA TRUE Сменный… 2 80290406 Администрато… 1 Москва 70000 70000 TRUE Полный … 3 104335755 Вечерний адм… 1 Москва 60000 60000 TRUE Полный … 4 104335439 Администрато… 1 Москва 3000 NA TRUE Сменный… 5 96379378 Администрато… 1 Москва 45000 NA FALSE Гибкий … 6 103817094 Дизайнер инт… 1 Москва 100000 NA FALSE Полный … 7 103433054 Специалист п… 1 Москва 75000 165000 FALSE Полный … 8 103629566 Администрато… 1 Москва 75000 100000 FALSE Полный … 9 104052792 Администратор 1 Москва 60000 NA FALSE Полный … 10 103037149 Администрато… 1 Москва 55000 NA FALSE Сменный… # ℹ 54,360 more rows # ℹ 9 more variables: published_at <chr>, created_at <chr>, alternate_url <chr>, # employer <chr>, professional_roles_id <chr>, professional_roles_name <chr>, # experience <chr>, employment <chr>, region <chr> # ℹ Use `print(n = ...)` to see more rows