Данные и информация всегда были важным активом. А в век цифровизации и IT-технологий их значение существенно возросло, особенно, если данные хорошо структурированы и могут быть применены для определённых целей.
LinkedIn – это идеальная площадка для сбора информации о соискателях, о потенциальных работодателях, о тенденциях на рынке труда и т.п. Но у LinkedIn нет официального API интерфейса, поэтому для сбора данных придётся писать свой парсер.
Эта статья – пошаговое руководство для новичков. На примерах кода покажем, как осуществляется парсинг страниц LinkedIn, что для этого потребуется и какие могут быть подводные камни.
LinkedIn – это крупная соцсеть, которая профилируется на деловых связях и публикации контактов. Очень популярна в США и Европе. Сейчас её аудитория превышает 1 млрд. зарегистрированных пользователей. Среди клиентов соискатели и крупнейшие работодатели из разных стран (200+).
Владелец LinkedIn – компания Microsoft. Поэтому неудивительно, что внутри платформы активно развиваются сервисы на базе искусственного интеллекта. К слову, LinkedIn с 2016 года блокируется на территории России, так как площадка отказалась переносить данные российских пользователей в Россию, как того требует местное законодательство.
При такой аудитории LinkedIn становится сервисом №1 для публикации резюме или вакансий, для поиска подходящих кандидатов на определённые должности, а также для личных рекомендаций и обмена контактами (по аналогии с сарафанным радио).
Но есть одна загвоздка – у LinkedIn нет инструментов автоматизации или API-интерфейсов для получения данных без использования браузера. Чтобы собрать нужную информацию в большом объёме, придётся автоматизировать процесс за счёт скриптов парсинга.
Web-скрейпинг LinkedIn позволяет достичь следующих результатов:
Разрешает ли LinkedIn скрапинг? Сайт работает с персональными данными пользователей. Несмотря на то, что данные хранятся в открытом доступе (их можно посмотреть после авторизации), попытка сбора информации о клиентах может иметь неприятные последствия из-за требований различных региональных законов. Технически парсинг не нарушает законов. Нарушение происходит в момент использования данных, например, в момент рассылки спама.
Тем не менее, парсинг официально запрещён правилами LinkedIn – это отдельно оговаривается в пользовательском соглашении.
Отсюда возникает вопрос: насколько вообще легален скрапинг LinkedIn? Вся проблема в целях сбора данных. Если вы автоматизируете рутину, например, пишете скрипт для быстрого просмотра подходящих профилей соискателей (вы бы то же самое могли делать вручную, просто хотите сэкономить своё время и силы), то никаких юридических последствий не будет.
Но если данные собираются для целей, которые явно запрещены политиками LinkedIn и требованиями законов, то вполне можно получить не только бан, но и реальный судебный иск. Такая практика имеется. В частности, речь о решении по делу против компании hiQ Labs. Оно строилось на основе Закона о компьютерном мошенничестве и злоупотреблениях (CFAA).
Так как LinkedIn не знает ваших целей, то весь автоматический трафик и попытки парсинга блокируются. У сервиса одна из самых сложных антифрод-систем на рынке.
В связи с этим мы настоятельно рекомендуем защитить себя по максимуму: использовать несколько виртуальных профилей (для распараллеливания запросов и для диверсификации рисков), их не стоит регистрировать на свои реальные номера телефонов и персональные данные, обращаться к сайту следует только через прокси (желательно резидентные, а лучше через мобильные).
Лучшие практики для веб-скрапинга без блокировок
Обратите внимание: LinkedIn использует динамическую бесконечную подгрузку страниц, много где работает с AJAX и JavaScript. А это значит, что без headless-браузера не обойтись. Способы подключения к API безголовых браузеров существуют разные: через специальные библиотеки, напрямую или через драйверы/локальные серверы (как Selenium).
Так как профилей будет много, у них должны быть свои уникальные отпечатки. Чтобы обслуживать работу большого числа экземпляров браузеров с уникальными профилями внутри, лучше всего задействовать антидетект-браузер, такой как Dolphin {anty}. Он же может работать в headless-режиме (с доступом по API).
В результате ужесточения внутренних политик LinkedIn теперь для аккаунтов действуют следующие ограничения:
Обойти такие ограничения можно только за счёт распараллеливания аккаунтов.
Ниже мы будем разбирать ситуацию парсинга LinkedIn на Python. Питон – кроссплатформенный язык программирования. Для его поддержки нужно скачать среду исполнения под свою операционную систему и заблаговременно установить. Скачать Python можно с официального сайта проекта. Есть установочные пакеты для Windows, Linux и MacOS. Специально для веб-разработчиков предоставляются даже готовые Docker-контейнеры.
На всякий случай изучите популярные библиотеки, помогающие с парсингом на Python.
А теперь к коду нашего парсера.
Начинаем с подготовки рабочей среды. Для этого как минимум нужно установить ряд python-библиотек, облегчающих написание скриптов для парсинга (скрапинга).
Откройте консоль и запустите среду Python. Введите команды:
pip install playwright
pip3 install requests
pip3 install beautifulsoup4
Если вы используете Windows, то после установки среды Python можно открыть терминал (командную строку) и ввести команду так:
py -m pip install playwright requests beautifulsoup4
Инструменты для выгрузки в CSV-формате встроены в Python.
При большом желании можно законнектить с браузером библиотеку Selenium. Для этого потребуется не только сама библиотека, но и веб-драйвер (это автономный веб-сервер, созданный специально для тестирования браузера). Начиная с версии браузера Хром 115 и выше следует использовать панель доступности Chrome для тестирования.
Мы пошли простым путём и используем playwright.
Но и тут есть нюансы. Просто установить playwright недостаточно. Нужно ещё установить для него браузер. Если браузер уже есть в системе, то скрипт начнёт ругаться и станет предлагать форсированную установку.
Так как Хром есть на 99% компьютеров, установим Firefox (это будет наш отдельный headless-браузер).
Команда для терминала Windows:
py -m playwright install firefox
Дождитесь загрузки Firefox и его распаковки.
Без входа в аккаунт платформа не допустит к просмотру профилей, к результатам поиска и ко многим другим функциям.
Для авторизации нужно перейти на страницу входа, найти соответствующие поля, ввести в них логин и пароль.
Это можно сделать заранее, в том браузере, с которым вы будете работать по API.
Например, в нашем Файрфоксе, который установил Playwright, это делается так:
На уровне кода это делается так (это кусок скрипта на Python, полный код будет ниже):
#импортируем модули playwright
from playwright.sync_api import sync_playwright
with sync_playwright() as pw:
#нам нужно запустить именно Файрфокс и без активации скрытого headless-режима, то есть все действия будут вам видны (не трогайте окно браузера, когда он запустится)
browser = pw.firefox.launch(headless=False)
#тут задаются параметры окна
context = browser.new_context(viewport={"width": 1366, "height": 720})
#запускаем новую вкладку
page = context.new_page()
page.goto('https://www.linkedin.com/login')
page.wait_for_selector("h2") # тут можно найти и указать тот селектор, который прогружается после формы ввода
# Заполнение форм ввода
page.get_by_role ("textbox", name="username").fill("ваш_email@адрес.com")
get_by_role ("textbox", name="password").fill("ВАШ-ПАРОЛЬ")
page.get_by_role ("button", name="login").click()
Выше мы использовали код для playwright.
Если используется web-драйвер, то код будет таким:
#это страница авторизации
driver.get('https://www.linkedin.com/login')
#здесь мы находим нужные поля ввода и заполняем их методом send_keys ()
driver.find_element_by_id('username').send_keys('your_email@example.com')
driver.find_element_by_id('password').send_keys('your_password')
driver.find_element_by_css_selector('.login__form_action_container button').click()
Предположим, что нам нужно извлечь данные со страницы поиска – тут у нас бесконечная страница со списками.
Много информации записывается непосредственно в структуре URL-адреса. Простой пример для поиска вакансий:
https://www.linkedin.com/jobs/search?keywords=DevOps%20Engineer&location=United%20States&position=1&pageNum=0&start=0
Мы искали «DevOps Engineer» в США (United States). Символ пробела заменяется на комбинацию «%20».
pageNum ни на что не влияет, так как страница подгружается динамически и всегда остаётся одной и той же.
А вот параметр start определяет с какого элемента списка нужно выводить результаты поиска. Опытным путём было выявлено, что лучшим шагом для перехода по списку будет 25 элементов. То есть параметр start может равняться 0, 25, 50, 75… и так далее до 975 (дальше вместо результатов будет показываться ошибка 404).
Но в каких конкретно кусках HTML-кода кроются названия должностей, названия компаний и т.п.?
При анализе кода страниц в консоли разработчиков можно вычленить такие закономерности:
Обратите внимание, со временем теги и классы могут поменяться, так как ничто не вечно. При желании вы всегда можете открыть консоль разработчика в веб-браузере и самостоятельно найти теги и классы, которые соответствуют тем или иным элементам вёрстки.
Когда авторизация пройдена, нам остаётся перейти на страницу поиска, подождать загрузки определённого селектора (чтобы наверняка всё прогрузилось), затем найти карточки вакансий и пройтись по ним, собирая нужное содержимое.
Мы будет просто выводить массив данных в консоль.
При написании полноценного парсера можно подключить модуль выгрузки в CSV с разбивкой на столбцы и поля.
Вот так будет выглядеть наш код для обхода карточек с вакансиями:
page_two.goto('https://www.linkedin.com/jobs/search?keywords=DevOps%20Engineer&location=United%20States&position=1&pageNum=0&start=0')
page_two.wait_for_selector("h3") # ждём, когда контент на странице загрузится
# Сначала находим карточки с результатами поиска, объявляем массив
parsed = []
all_jobs = page_two.query_selector_all('base-card relative w-full hover:no-underline focus:no-underline base-card--link base-search-card base-search-card--link job-search-card')
# Внутри каждой карточки обходим нужные элементы и сохраняем данные
for job in all_jobs.element_handles():
parsed.append({
#Сначала название вакансии
"Название вакансии": job.query_selector("h3 .base-search-card__title").inner_text(),
#Затем название компании
"Название компании": job.query_selector("h4 .base-search-card__subtitle").inner_text(),
#Потом адрес
"Адрес": job.query_selector("span .job-search-card__location").inner_text(),
#И наконец ссылка на профиль
"Ссылка": job.query_selector("a .base-card__full-link").get_attribute("href"),
})
# Печатаем всё в консоль
for item in parsed:
print(item)
Итоговый скрипт у нас выглядит так:
#импортируем модули playwright
from playwright.sync_api import sync_playwright
with sync_playwright() as pw:
#нам нужно запустить именно Файрфокс и без активации скрытого headless-режима, то есть все действия будут вам видны (не трогайте окно браузера, когда он запустится)
browser = pw.firefox.launch(headless=False)
#тут задаются параметры окна
context = browser.new_context(viewport={"width": 1366, "height": 720})
#запускаем новую вкладку
page = context.new_page()
page.goto('https://www.linkedin.com/login')
page.wait_for_selector("h2") # тут можно найти и указать тот селектор, который прогружается после формы ввода
# Заполнение форм ввода
page.get_by_role ("textbox", name="username").fill("ваш_email@адрес.com")
get_by_role ("textbox", name="password").fill("ВАШ-ПАРОЛЬ")
page.get_by_role ("button", name="login").click()
page_two.goto('https://www.linkedin.com/jobs/search?keywords=DevOps%20Engineer&location=United%20States&position=1&pageNum=0&start=0')
page_two.wait_for_selector("h3") # ждём, когда контент на странице загрузится
# Сначала находим карточки с результатами поиска
parsed = []
all_jobs = page_two.query_selector_all('base-card relative w-full hover:no-underline focus:no-underline base-card--link base-search-card base-search-card--link job-search-card')
# Внутри каждой карточки обходим нужные элементы и сохраняем данные
for job in all_jobs.element_handles():
parsed.append({
#Сначала название вакансии
"Название вакансии": job.query_selector("h3 .base-search-card__title").inner_text(),
#Затем название компании
"Название компании": job.query_selector("h4 .base-search-card__subtitle").inner_text(),
#Потом адрес
"Адрес": job.query_selector("span .job-search-card__location").inner_text(),
#И наконец ссылка на профиль
"Ссылка": job.query_selector("a .base-card__full-link").get_attribute("href"),
})
# Печатаем всё в консоль
for item in parsed:
print(item)
Чисто гипотетически, если правильно перебирать номера стартовых карточек в структуре URL-адреса с поиском, то можно обойтись без headless-браузера. Нужно только запустить цикл повышения номеров с шагом в 25 карточек. Тогда на странице с результатами будут выводиться результаты без предыдущих элементов – получается эффект, аналогичный пагинации (разбивке на страницы).
Но подход работает только с поиском LinkedIn.
На остальных страницах часто встречается динамическая подгрузка, поэтому без headless-браузера уже никак не обойтись.
У LinkedIn одна из самых серьёзных защит. Сервис в лицо знает все расширения для браузеров, которые используются для обхода блокировок или для ротации прокси. Более того, многие VPN и популярные общие прокси уже в чёрных списках.
Единственный рабочий инструмент – чистые резидентные и мобильные прокси с ротацией.
Но даже прокси решают проблему лишь частично. Основным ограничением будет обязательная авторизация в профиле. Без авторизации доступ к контенту никак не получить.
Все возможные попытки автоматизации жёстко отслеживаются и наказываются банами. Банятся не IP-адреса, а конкретные учётные записи. Даже наличие платной подписки никак не спасает от блокировки за парсинг.
Вывод – нужно иметь большое количество записей и диверсифицировать риски. Для управления и автоматизации работы с большим числом аккаунтов потребуется уже не headless-, а антидетект-браузер.
Итого: полный джекпот с headless и антидетект-браузерами, большим количеством профилей, с самыми сложными системами защиты и с обязательно белыми прокси.
Чтобы пропарсить страницы LinkedIn придётся изрядно потрудиться. Парсер не получится написать в пару строк кода. Для его работы потребуется полноценное окружение: headless/антидетект-браузер, библиотеки, ротируемые прокси.
Вероятность блокировки напрямую будет зависеть от качества прокси. Найти белые прокси с ротацией можно у нас. Froxy – это более 10 млн. IP. В базе резидентные, мобильные и серверные прокси. Оплата не за количество адресов, а за потребляемый трафик. Неизрасходованный трафик можно переносить на следующий месяц. Таргетинг – до уровня города и провайдера связи. Ротация по времени или по API/запросу.