Парсинг контактов LinkedIn на Python + ruCaptcha

Введение

Недавно наша компания запустила новый проект — школу программирования. В рамках этого проекта мы обучаем студентов, детей и взрослых программированию (в частности, на Python).

Хотя в редакции Pythonist.ru работают опытные питонисты, нам все же были нужны отдельные сотрудники — преподаватели курсов. Изучив самые популярные (из известных нам) сайты по поиску кандидатов, мы неожиданно поняли, почему в больших компаниях бывает целый штат HR-специалистов. Также стало очевидным, что на всех мало-мальски солидных сайтах размещение объявления о вакансии стоит больших денег (при том, что совершенно не гарантирует успеха).

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

Мы поняли, что самое простое решение нашей проблемы — парсинг контактов на LinkedIn (в поиске Junior Python разработчиков из России) и последующая отправка им сообщений с предложением работы.

Задача:

  1. Парсинг выдачи LinkedIn по запросу «Python разработчик» или «Python developer» в регионе «Россия».
  2. Отправка сообщения с предложением работы в нашей компании.
  3. Обход различных блокировок со стороны LinkedIn (блокировка за слишком частые сообщения, за роботоподобное поведение на сайте и пр.).
  4. Решение вопроса ввода капчи (после отправки большого количества одинаковых сообщений).

Инструменты

Для написания самого скрипта мы будем использовать Python третей версии. Но нам еще нужно имитировать поведение пользователя на сайте — для этого используем Selenium WebDriver.

Приступим

Для начала нам нужно залогиниться на LinkedIn, поскольку каждый новый запуск Selenium-скрипта создает окно браузера в режиме инкогнито. Также нам нужно скопировать URL, который LinkedIn генерирует при поиске «Python разработчик». Затем необходимо спарсить страницу и написать поведение перекликивания с первой страницы выдачи на вторую, третью и т.д.

from selenium.common.exceptions import NoSuchElementException
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

TEXTMESSAGE = """
               Привет. Нашел тебя в своих контактах. Может тебе будет интересно поработать преподавателем на онлайн-курсах по питону. Ссылка на вакансию: <ТУТ>
            """
SEARCH_LINK = 'https://www.linkedin.com/uas/login?session_redirect=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fsearch%2Fresults%2Fpeople%2F%3FfacetGeoUrn%3D%255B%2522101728296%2522%252C%2522102264497%2522%255D%26keywords%3Djunior%2520python%26origin%3DFACETED_SEARCH%26page%3D2&fromSignIn=true&trk=cold_join_sign_in'

def open_links_by_browser(driver, link):
    driver.get(link)
    driver.quit()

driver = webdriver.Chrome("/usr/lib/chromium-browser/chromedriver")
driver.get(LINK)

email_field = driver.find_element_by_css_selector('#username')
password_field = driver.find_element_by_css_selector('#password')
submit_button = driver.find_element_by_css_selector('.btn__primary--large')
email_field.send_keys('YOUEMAIL@gmail.com')
password_field.send_keys('YOUR_PASSWORD')
submit_button.click()

На следующем этапе мы будем проходить по страницам поиска и отправлять всем людям по списку сообщения с предложением о работе. При этом мы не будем сильно заморачиваться насчет количества страниц в выдаче: будем просто идти, пока не упремся в конец списка.

Первая попытка:

script = "window.scrollTo(0, {});".format(2000)
driver.execute_script(script)

search_lists = driver.find_element_by_css_selector('.search-results__list')
connect_buttons = search_lists.find_elements_by_css_selector('.message-anywhere-button')
    for button in connect_buttons:
        try:
            button.click()
            message = driver.find_element_by_css_selector('.msg-form__contenteditable')
            message.send_keys(TEXTMESSAGE)
            message.send_keys(Keys.RETURN)
            driver.find_element_by_css_selector('.msg-overlay-bubble-header__controls .ember-view').click()
        except:
            pass
        try:
            donebutton = driver.find_element_by_css_selector('.artdeco-modal .ml1')
            donebutton.click()
        except NoSuchElementException:
            pass

Проблема:

После 10-12 отправленных сообщений выскакивает капча (текстовая). Хоть Selenium и хорош, но, по всей видимости, внутри LinkedIn стоит проверка на частые отправки сообщений. Тут у нас два варианта на выбор: либо отправлять сообщения понемногу и с различными промежутками, либо решить задачу, используя сервис распознавания капчи, и отправить сообщения всем сразу.

Второй способ требует написания небольшого куска кода, но сразу решает нашу задачу.

Мы использовали сервис по разгадыванию капчи rucaptcha.com.

Для начала нужно при появлении капчи скачать ее на компьютер. Делаем это в два шага:

  1. получаем img капчи по xpath
  2. скачиваем на компьютер.
# get the image source
img = driver.find_element_by_xpath('//div[@id="recaptcha_image"]/img')
src = img.get_attribute('src')

# download the image
urllib.urlretrieve(src, "captcha.png")

Затем нужно отправить на API сервиса RuCaptcha эту картинку, дождаться ответа и вписать ответ в окно ввода.

Как работает ruCaptcha

Сервис ruCaptcha предоставляет программный интерфейс (API), позволяющий интегрировать ваше программное обеспечение с этим сервисом и автоматизировать процесс распознавания изображений и решения капчи.

Процесс распознавания изображений и решения капчи состоит из нескольких простых шагов:

  1. Вы отправляете изображение на сервер ruCaptcha.
  2. Сервер возвращает вам уникальный идентификатор вашей задачи (Captcha ID).
  3. Вы запускаете цикл, который проверяет, выполнена ли задача.
  4. Сервер возвращает вам результат распознавания.

Непосредственный алгоритм разгадывания

1. Получите ваш персональный ключ API в настройках вашего аккаунта.

2. Чтобы решить нормальную капчу с помощью сервиса, вам необходимо загрузить изображение с помощью HTTP POST запроса к URL API сервиса ruCaptcha:
https://rucaptcha.com/in.php
Сервер принимает изображения в формате multipart или base64.

Для отправки сообщения на сервер можно использовать библиотеку requests.

import base64
import requests

CAPTCHA = open("captcha.png", "r").read()
encoded_image = base64.b64encode(CAPTCHA)


answer = requests.post(
    f"https://rucaptcha.com/in.php",
    json={"key": YOUR_APIKEY, "method": "base64", "body": encoded_image},
)

CAPTCHA_ID = answer['id']

BASE64_FILE — тело файла капчи в формате base64.

Вы можете указать дополнительные параметры в вашем запросе, чтобы определить, какой тип капчи вы решаете и помочь сервису решить её правильно. Полный список параметров приведен в таблице на сайте сервиса.

Если вы отправили корректный запрос, сервер вернёт ID капчи в виде json,
{"status":1,"request":"2122988149"}

Если что-то пошло не так, сервер вернёт ошибку. Описание ошибок можно посмотреть здесь.

3. Затем нужно подождать 5-10 секунд и отослать GET-запрос с API_KEY и ID капчи.

import requests

answer = requests.get(
    "https://rucaptcha.com/res.php?key=API_KEY&action=get&id=CAPTCHA_ID"
)

Если ваша капча еще не решена, сервер вернет CAPCHA_NOT_READY. В таком случае повторите ваш запрос через 5 секунд.

Если что-то пошло не так, сервер вернёт ошибку, описание которой можно найти на сайте сервиса в главе Обработка ошибок.


Вот таким легким и элегантным способом мы обошли проблему разгадывания капчи. Дальше остается только подставить ее в форму и отправить сообщения огромному количеству разработчиков из России.

Результат:

Потратив 2-4 часа на написание кода, вы можете сэкономить бюджет для набора 5-10 преподавателей. Что мы, собственно, и сделали. Через LinkedIn мы таким образом уже наняли двоих преподавателей по Python и одного по JS (FrontEnd).