Парсим API HeadHunter с помощью R

от автора

Что нам понадобится?

  1. Ознакомиться со статьями https://teletype.in/@h0h1_hr_analytics

    Статьи очень помогли разобраться в деталях hh. Но есть устаревшая информация, в связи с чем, решил актуализировать детали.

  2. Получить OAuth токен hh — https://dev.hh.ru

    Получение токена очень важно, так как его наличие позволит вам отправлять большое кол-во запросов к API без блокировки со стороны hh.

  3. Изучить документацию на GitHub

  4. Использовать библиотеки R

    library(tidyverse) library(httr2) library(furrr)

    Начнем с HH: получение токена

    Вам необходимо зарегистрироваться на сайте hh.ru. После чего Вы сможете подать заявку на регистрацию своего приложения.

    Чтобы это сделать перейдите по ссылке https://dev.hh.ru

    Untitled

    Там будет раздел “Регистрация приложения”. Кликаете на кнопку Добавить приложение

    Untitled

    Заполняете все поля. Сильно можно не “заморачиваться”. В Redirect URI указываете любую ссылку, которая имеет к вам отношение. В моем случае, я указал корпоративный сайт.

    Untitled

    После заполнения формы, нажимаете на кнопку Добавить. Примерно через неделю, если со стороны hh не будет к вам вопросов, вам одобрят заявку.

    Untitled

    Что нам понадобится?

    • Redirect URI

    • Client ID

    • Client Secret

    • Code

      Как мы видим, у нас нет одно из параметров — Code

    Для его получения нам необходимо:

    1. Скопировать ссылку

    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.

    1. Переходим по получившейся ссылке и нажимаем Продолжить

    Untitled

    У вас сгенерируется новая ссылка типа:

    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


ссылка на оригинал статьи https://habr.com/ru/articles/830424/