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

  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

Комментарии (3)


  1. zhonya016
    24.07.2024 13:45

    Добрый день, правильно я понимаю:
    1. получив один раз "Ваш код указан после ?code" полученный код уже можно не запрашивать каждый раз?
    2. Получив code, в дальнейшем надо работать только с токенами (access_token и refresh_token)?


    1. Abby_Baby Автор
      24.07.2024 13:45

      Код нужно будет запросить заново, когда перестанет действовать токен. Именно это и мешает автоматизировать запрос токена, так как для этого нужно каждый раз получать ?code


  1. Abby_Baby Автор
    24.07.2024 13:45

    У токена есть срок жизни, указанный в поле expires_in, после истечения которого токен надо обновить.