Привет, сообщество.

Проработав в одной компании три года, пришло время сменить работодателя. И тут я понял, что я не знаю рыночную цену своим компетенциям. Во время работы на одном месте перестаешь ходить на собеседования, общаться по-душам с рекрутерами, а рост внутри одной компании как правило небольшой, как по скилам, так и по зарплате.

В то же время, как хобби, я юзал аналитический инструмент Power BI - красивые графики, диаграммы и тд. А главный сервис с вакансиями в РФ, ну вы знаете. Поразмыслив, я почувствовал, что добавив одно к другому, может получится интересная история.

Итак, мне нужны данные по рынку. Если воспользоваться поиском на сайте HHru, в выдаче можно увидеть кучу вакансий, но когда их сотни, для человека анализ не представляется возможным. Нашел в документации по API HHru, что данные по вакансиям бесплатны и открыты. То есть можно получить те же результаты, что и поиском, только в формате json, что в конечном счете съедобно для Power BI. Поехали!

Далее по шагам строим модель
  1. Запускаем Power BI Desktop и заходим в редактор запросов.

  2. Создаем новый Пустой запрос с именем function 1 и прописываем в него код ниже:

    = (Adress as text) => let data = Json.Document(Web.Contents(Adress)) in data
  3. Создаем новый Пустой запрос с именем function 2 и прописываем в него код ниже:

= (Adress as text) => let
        data = Json.Document(Web.Contents("https://api.hh.ru/vacancies/"&Adress))
    in
        data
  1. Создаем новый Пустой запрос с именем Вакансии и прописываем в него код ниже:

    let
        Источник = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("BcFRCoAgDADQq4QHUOmjj0A6iEkMXW1QQ3RGx++9GA2p1r46B5UtkW3DvZBBMmPfFD8NBRQmlIsFse3D+3npCC3TcTLeJQg8aFL6AQ==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [API = _t]),
        #"Измененный тип" = Table.TransformColumnTypes(Источник,{{"API", type text}}),
        #"Вызвана настраиваемая функция" = Table.AddColumn(#"Измененный тип", "Rez", each #"function 1"([API])),
        #"Развернутый элемент Rez" = Table.ExpandRecordColumn(#"Вызвана настраиваемая функция", "Rez", {"pages", "per_page", "page"}, {"Rez.pages", "Rez.per_page", "Rez.page"}),
        #"Вызвана настраиваемая функция1" = Table.AddColumn(#"Развернутый элемент Rez", "RezAll", 
       each 
    let rt = [API], pages = [Rez.pages]
    in
    List.Generate( ()=> [a = 0], each [a] < pages, each [ a = [a] + 1 ], each #"function 1"(rt&"&page="&Number.ToText([a])))
    ),
        #"Удаленные столбцы" = Table.RemoveColumns(#"Вызвана настраиваемая функция1",{"Rez.pages", "Rez.per_page", "Rez.page"}),
        #"Развернутый элемент RezAll" = Table.ExpandListColumn(#"Удаленные столбцы", "RezAll"),
        #"Развернутый элемент RezAll1" = Table.ExpandRecordColumn(#"Развернутый элемент RezAll", "RezAll", {"items", "found", "pages", "per_page", "page", "clusters", "arguments", "alternate_url"}, {"RezAll.items", "RezAll.found", "RezAll.pages", "RezAll.per_page", "RezAll.page", "RezAll.clusters", "RezAll.arguments", "RezAll.alternate_url"}),
        #"Другие удаленные столбцы" = Table.SelectColumns(#"Развернутый элемент RezAll1",{"API", "RezAll.items", "RezAll.page"}),
        #"Развернутый элемент RezAll.items" = Table.ExpandListColumn(#"Другие удаленные столбцы", "RezAll.items"),
        #"Развернутый элемент RezAll.items1" = Table.ExpandRecordColumn(#"Развернутый элемент RezAll.items", "RezAll.items", {"id", "premium", "name", "department", "has_test", "response_letter_required", "area", "salary", "type", "address", "response_url", "sort_point_distance", "published_at", "created_at", "archived", "apply_alternate_url", "insider_interview", "url", "alternate_url", "relations", "employer", "snippet", "contacts", "schedule", "working_days", "working_time_intervals", "working_time_modes", "accept_temporary"}, {"RezAll.items.id", "RezAll.items.premium", "RezAll.items.name", "RezAll.items.department", "RezAll.items.has_test", "RezAll.items.response_letter_required", "RezAll.items.area", "RezAll.items.salary", "RezAll.items.type", "RezAll.items.address", "RezAll.items.response_url", "RezAll.items.sort_point_distance", "RezAll.items.published_at", "RezAll.items.created_at", "RezAll.items.archived", "RezAll.items.apply_alternate_url", "RezAll.items.insider_interview", "RezAll.items.url", "RezAll.items.alternate_url", "RezAll.items.relations", "RezAll.items.employer", "RezAll.items.snippet", "RezAll.items.contacts", "RezAll.items.schedule", "RezAll.items.working_days", "RezAll.items.working_time_intervals", "RezAll.items.working_time_modes", "RezAll.items.accept_temporary"}),
        #"Другие удаленные столбцы1" = Table.SelectColumns(#"Развернутый элемент RezAll.items1",{"RezAll.items.id"}),
        #"Измененный тип1" = Table.TransformColumnTypes(#"Другие удаленные столбцы1",{{"RezAll.items.id", type text}}),
        #"Вызвана настраиваемая функция2" = Table.AddColumn(#"Измененный тип1", "API", each #"function 2"([RezAll.items.id])),
        #"Развернутый элемент API" = Table.ExpandRecordColumn(#"Вызвана настраиваемая функция2", "API", {"id", "premium", "billing_type", "relations", "name", "insider_interview", "response_letter_required", "area", "salary", "type", "address", "allow_messages", "site", "experience", "schedule", "employment", "department", "contacts", "description", "branded_description", "vacancy_constructor_template", "key_skills", "accept_handicapped", "accept_kids", "archived", "response_url", "specializations", "professional_roles", "code", "hidden", "quick_responses_allowed", "driver_license_types", "accept_incomplete_resumes", "employer", "published_at", "created_at", "negotiations_url", "suitable_resumes_url", "apply_alternate_url", "has_test", "test", "alternate_url", "working_days", "working_time_intervals", "working_time_modes", "accept_temporary"}, {"API.id", "API.premium", "API.billing_type", "API.relations", "API.name", "API.insider_interview", "API.response_letter_required", "API.area", "API.salary", "API.type", "API.address", "API.allow_messages", "API.site", "API.experience", "API.schedule", "API.employment", "API.department", "API.contacts", "API.description", "API.branded_description", "API.vacancy_constructor_template", "API.key_skills", "API.accept_handicapped", "API.accept_kids", "API.archived", "API.response_url", "API.specializations", "API.professional_roles", "API.code", "API.hidden", "API.quick_responses_allowed", "API.driver_license_types", "API.accept_incomplete_resumes", "API.employer", "API.published_at", "API.created_at", "API.negotiations_url", "API.suitable_resumes_url", "API.apply_alternate_url", "API.has_test", "API.test", "API.alternate_url", "API.working_days", "API.working_time_intervals", "API.working_time_modes", "API.accept_temporary"}),
        #"Удаленные столбцы1" = Table.RemoveColumns(#"Развернутый элемент API",{"API.premium", "API.billing_type", "API.relations", "RezAll.items.id", "API.insider_interview", "API.response_letter_required"}),
        #"Удаленные столбцы2" = Table.RemoveColumns(#"Удаленные столбцы1",{"API.type", "API.address", "API.allow_messages", "API.site", "API.professional_roles"}),
        #"Удаленные столбцы3" = Table.RemoveColumns(#"Удаленные столбцы2",{"API.department", "API.contacts", "API.branded_description", "API.vacancy_constructor_template", "API.accept_handicapped", "API.accept_kids", "API.archived", "API.response_url", "API.code", "API.hidden", "API.quick_responses_allowed", "API.driver_license_types", "API.accept_incomplete_resumes", "API.negotiations_url", "API.suitable_resumes_url", "API.apply_alternate_url", "API.accept_temporary", "API.working_days", "API.working_time_intervals", "API.working_time_modes", "API.has_test", "API.test", "API.specializations"}),
        #"Переименованные столбцы" = Table.RenameColumns(#"Удаленные столбцы3",{{"API.id", "id"}, {"API.name", "Вакансия"}, {"API.area", "Локация"}, {"API.schedule", "График"}, {"API.employment", "Занятость"}, {"API.experience", "Опыт"}, {"API.alternate_url", "Ссылка"}, {"API.key_skills", "Навыки"}, {"API.employer", "Компания"}}),
        #"Развернутый элемент API.employment" = Table.ExpandRecordColumn(#"Переименованные столбцы", "Занятость", {"name"}, {"API.employment.name"}),
        #"Развернутый элемент API.schedule" = Table.ExpandRecordColumn(#"Развернутый элемент API.employment", "График", {"name"}, {"API.schedule.name"}),
        #"Развернутый элемент API.experience" = Table.ExpandRecordColumn(#"Развернутый элемент API.schedule", "Опыт", {"name"}, {"API.experience.name"}),
        #"Развернутый элемент API.salary" = Table.ExpandRecordColumn(#"Развернутый элемент API.experience", "API.salary", {"from", "to", "currency"}, {"API.salary.from", "API.salary.to", "API.salary.currency"}),
        #"Развернутый элемент API.area" = Table.ExpandRecordColumn(#"Развернутый элемент API.salary", "Локация", {"name"}, {"API.area.name"}),
        #"Развернутый элемент API.employer" = Table.ExpandRecordColumn(#"Развернутый элемент API.area", "Компания", {"name"}, {"API.employer.name"}),
        #"Переименованные столбцы1" = Table.RenameColumns(#"Развернутый элемент API.employer",{{"API.employer.name", "Компания"}, {"API.employment.name", "Занятость"}, {"API.schedule.name", "График"}, {"API.experience.name", "Опыт"}, {"API.salary.currency", "Валюта"}, {"API.salary.to", "ЗП до"}, {"API.salary.from", "ЗП от"}, {"API.area.name", "Локация"}}),
        #"Измененный тип2" = Table.TransformColumnTypes(#"Переименованные столбцы1",{{"id", Int64.Type}, {"Вакансия", type text}, {"Локация", type text}, {"ЗП от", type number}, {"ЗП до", type number}, {"Валюта", type text}, {"Опыт", type text}, {"График", type text}, {"Занятость", type text}, {"API.description", type text}, {"Компания", type text}, {"Ссылка", type text}})
    in
        #"Измененный тип2"
  2. Создаем новый Пустой запрос с именем именем Навыки и пропысываем в него код:

    let
        Источник = Вакансии,
        #"Развернутый элемент API.key_skills" = Table.ExpandListColumn(Источник, "Навыки"),
        #"Другие удаленные столбцы" = Table.SelectColumns(#"Развернутый элемент API.key_skills",{"id", "Навыки"}),
        #"Развернутый элемент Навыки" = Table.ExpandRecordColumn(#"Другие удаленные столбцы", "Навыки", {"name"}, {"Навыки.name"}),
        #"Переименованные столбцы" = Table.RenameColumns(#"Развернутый элемент Навыки",{{"Навыки.name", "Навыки"}}),
        #"Измененный тип" = Table.TransformColumnTypes(#"Переименованные столбцы",{{"Навыки", type text}, {"id", type text}})
    in
        #"Измененный тип"
  3. В редакторе запросов должна получится такая картина

    Hidden text
    Запросы в Power Qwery
    Запросы в Power Qwery
  4. С кодом всё! Жмакаем по кнопке Закрыть и применить. Ждём пару минут.

  5. Далее необходимо связать таблицы Вакансии и Навыки по полям id на вкладке Моделирование.

Моделирование
Моделирование

После того как мы подготовили модель начинается творческая работа, можно строить дашборды и проводить анализ. Давайте построим таблицу по вакансиям и отсортируем по полю ЗП, для этого используем таблицу Вакансии. Потом добавим график по требуемуму опыту. Выведем карточку с общим количеством вакансий, табличку в разрезе зарплат с указанием валюты.

Используя таблицу Навыки, добавим диаграмму по требуемым навыкам с сортировкой по убыванию. Должно получиться примерно так:

Итоговый дашборд
Итоговый дашборд

Причем, если в таблице поле Ссылка обозначить как URL-адрес, поле будет кликабельным. Можно отфильтровать вакансии и кликать по ссылке для просмотра подробной информации на сайте-источнике.

Отчет доступен онлайн по ссылке

Заключение

  1. Таким образом, меняя название вакансии в запросах, можно получить анализ по любой вакансии. Программировать не нужно!

  2. Есть ограничения HHru. В данном отчете можно получить не более 1500 вакансий. Нужно проверить колличество на сайте HHru. При проверке не забудьте проставить галочку "Искать в названиях вакансий" - так точнее поиск. Если превысите этот лимит, потом блокируют Ваш IP примерно на час.

На этом все :-)

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


  1. KirillSimonov
    04.04.2022 22:17

    Отличное применение знание постройки дэшборда! Как раз хотел посмотреть данные по вакансиям на Казахстане!


  1. BetAlbo
    05.04.2022 08:43

    Дашборд бомба! как изменить условие поиска на другие профессии по той ссылке?