- PVSM.RU - https://www.pvsm.ru -

Простой WebScraping на R через API hh.ru

Доброго времени суток, уважаемые читатели

Не так давно преподаватель дал задание: cкачать данные с некоторого сайта на выбор. Не знаю почему, но первое, что пришло мне в голову — это hh.ru.

Далее встал вопрос: "А что же собственно будем выкачивать?", ведь на сайте порядка 5 млн. резюме и 100.000 вакансий.

Решив посмотреть, какими навыками мне придется овладеть в будущем, я набрал в поисковой строке "data science" и призадумался. Может быть по синонимичному запросу найдется больше вакансий и резюме? Нужно узнать, какие формулировки популярны в данный момент. Для этого удобно использовать сервис GoogleTrends.

image
Отсюда видно, что выгоднее всего искать по запросу "machine learning". Кстати, это действительно так.

Соберем вначале вакансии. Их довольно мало, всего 411. API hh.ru поддерживает поиск по вакансиям, поэтому задача становится тривиальной. Единственное, нам необходимо работать с JSON. Для этой цели я использовал пакет jsonlite и его метод fromJSON() принимающий на вход URL и возвращающий разобранную структуру данных.

 data <- fromJSON(paste0("https://api.hh.ru/vacancies?text="machine+learning"&page=", pageNum) # Здесь pageNum - номер страницы. На странице отображается 20 вакансий.

Полный код для выгрузки вакансий

# Scrap vacancies
vacanciesdf <- data.frame(
    Name = character(),  # Название компании
    Currency = character(), # Валюта
    From = character(), # Минимальная оплата
    Area = character(), # Город
    Requerement = character(), stringsAsFactors = T) # Требуемые навыки
for (pageNum in 0:20) { # Всего страниц
    data <- fromJSON(paste0("https://api.hh.ru/vacancies?text="machine+learning"&page=", pageNum))
    vacanciesdf <- rbind(vacanciesdf, data.frame(
                                    data$items$area$name, # Город
                                    data$items$salary$currency, # Валюта
                                    data$items$salary$from, # Минимальная оплата
                                    data$items$employer$name, # Название компании
                                    data$items$snippet$requirement)) # Требуемые навыки
    print(paste0("Upload pages:", pageNum + 1))
    Sys.sleep(3)
}

Записав все данные в DataFrame, давайте немного его почистим. Переведем все зарплаты в рубли и избавимся от столбца Currency, так же заменим NA значения в Salary на нулевые.

Фильтрация DataFrame

# Сделаем приличные названия столбцов
names(vacanciesdf) <- c("Area", "Currency", "Salary", "Name", "Skills") 
# Вместо зарплаты NA будет нулевая
vacanciesdf[is.na(vacanciesdf$Salary),]$Salary <- 0 
# Переведем зарплаты в рубли
vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'USD',]$Salary <- vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'USD',]$Salary * 57
vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'UAH',]$Salary <- vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'UAH',]$Salary * 2.2
vacanciesdf <- vacanciesdf[, -2] # Currency нам больше не нужна
vacanciesdf$Area <- as.character(vacanciesdf$Area)

После этого имеем DataFrame вида:

image

Пользуясь случаем, посмотрим сколько вакансий в городах и какая у них средняя зарплата.

vacanciesdf %>% group_by(Area) %>% filter(Salary != 0) %>%
           summarise(Count = n(), Median = median(Salary), Mean = mean(Salary)) %>% 
                    arrange(desc(Count))

image

Для scraping`a в R обычно используется пакет rvest, имеющий два ключевых метода read_html() и html_nodes(). Первый позволяет скачивать страницы из интернета, а второй обращаться к элементам страницы с помощью xPath и CSS-селектора. API не поддерживает возможность поиска по резюме, но дает возможность получить информацию о нем по id. Будем выгружать все id, а затем уже через API получать данные из резюме. Всего резюме на сайте по данному запросу — 1049.

hhResumeSearchURL <- 'https://hh.ru/search/resume?exp_period=all_time&order_by=relevance&text=machine+learning&pos=full_text&logic=phrase&clusters=true&page=';
# Загрузим очередную страницу с номером pageNum
hDoc <- read_html(paste0(hhResumeSearchURL, as.character(pageNum)))
# Выделим все аттрибуты ссылок на странице
    ids <- html_nodes(hDoc, css = 'a') %>% as.character() 
# Выделим из ссылок необходимые id ( последовательности букв и цифр длины 38 )
    ids <- as.vector(ids) %>% `[`(str_detect(ids, fixed('/resume/'))) %>%
            str_extract(pattern = '/resume/.{38}') %>% str_sub(str_count('/resume/') + 1)
    ids <- ids[4:length(ids)] # В первых 3х мусор

После этого уже известным нам методом fromJSON получим информацию, содержащуюся в резюме.

 resumes <- fromJSON(paste0("https://api.hh.ru/resumes/", id))

Полный код для выгрузки резюме

hhResumeSearchURL <- 'https://hh.ru/search/resume?exp_period=all_time&order_by=relevance&text=machine+learning&pos=full_text&logic=phrase&clusters=true&page=';
for (pageNum in 0:51) { # Всего 51 страница
   #Вытащим id резюме 
    hDoc <- read_html(paste0(hhResumeSearchURL, as.character(pageNum)))
    ids <- html_nodes(hDoc, css = 'a') %>% as.character() 
   # Выделим все аттрибуты ссылок на странице
    ids <- as.vector(ids) %>% `[`(str_detect(ids, fixed('/resume/'))) %>%
    str_extract(pattern = '/resume/.{38}') %>% str_sub(str_count('/resume/') + 1)
    ids <- ids[4:length(ids)] # В первых 3х мусор
    Sys.sleep(1) # Подождем на всякий случай 
    for (id in ids) {
        resumes <- fromJSON(paste0("https://api.hh.ru/resumes/", id))
        skills <- if (is.null(resumes$skill_set)) "" else resumes$skill_set 
        buffer <- data.frame(
          Age = if(is.null(resumes$age)) 0 else resumes$age, # Возраст
          if (is.null(resumes$area$name)) "NoCity" else resumes$area$name,# Город
          if (is.null(resumes$gender$id)) "NoGender" else resumes$gender$id, # Пол
          if (is.null(resumes$salary$amount)) 0 else resumes$salary$amount, # Зарплата
          if (is.null(resumes$salary$currency)) "NA" else resumes$salary$currency, # Валюта  
         # Список навыков одной строкой через ,                 
          str_c(if (!length(skills)) "" else skills, collapse = ",")) 
        write.table(buffer, 'resumes.csv', append = T, fileEncoding = "UTF-8",col.names = F)
        Sys.sleep(1) # Подождем на всякий случай 
    }   
    print(paste("Скачал страниц:", pageNum))
}

Также почистим получившийся DataFrame, конвертируя валюты в рубли и удалив NA из столбцов.

image

Найдем топ — 15 навыков, чаще остальных встречающихся в резюме

SkillNameDF <- data.frame(SkillName = str_split(str_c(
                       resumes$Skills, collapse = ','), ','), stringsAsFactors = F)
names(SkillNameDF) <- 'SkillName'
mostSkills <- head(SkillNameDF %>% group_by(SkillName) %>%
                              summarise(Count = n()) %>% arrange(desc(Count)), 15 )

image

Посмотрим, сколько женщин и мужчин знают machine learning, а так же на какую зарплату претендуют

resumes %>% group_by(Gender) %>% filter(Salary != 0)  %>% 
          summarise(Count = n(), Median = median(Salary), Mean = mean(Salary)

image

И напоследок, топ — 10 самых популярных возрастов специалистов по машинному обучению

resumes %>% filter(Age!=0) %>% group_by(Age) %>% 
                summarise(Count = n()) %>% arrange(desc(Count))

image

Автор: pdepdepde

Источник [1]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/263661

Ссылки в тексте:

[1] Источник: https://habrahabr.ru/post/337684/