Disclaimer
O resultado que eu desenvolvi trata-se de um cli que automatiza por meio do selenium o acesso ao Linkedin, testei em minha conta pessoal e não tive problemas, mas não sei dizer qual a legalidade dessa brincadeira e se usuários detectados usufruindo desta poderão sofrer algum tipo de penalidade ou perder a sua conta. USE POR CONTA E RISCO
Este projeto assume que você saiba ler definições de Dockerfile, esteja habituado ao uso de ferramentas como poetry e tenha em seu sistema python instalado ao menos na versão 3.11 e a versão mais recente do Docker com o plugin docker-compose. Lembrando que esses experiemtnos foram feitos no Debian 12 (bookworm), não sei até que ponto usar Mac ou Windows será reproduzivél
Linkedisney: Um lugar para falar mal sobre seu trabalho
O LinkedIn é uma rede social que tem o intuito de conectar profissionais a outros profissionais e empresas, ou seja, um mar de confusão, posts motivacionais e pessoas ditando regras sobre if’s e else’s, mas em meio a essa bagunça nada organizada, é possível sim achar vagas de empregos.
Claro a plataforma tem toda uma estrutura de anúncios de empregos e formas de aplicações, ainda que, sem saber o motivo, eu tenho encontrado posts de anuncio de vagas, onde constam a descrição da posição, os requisitos e por fim alguma forma de contato, bem como aprendi recentemente por meio do post do Gab Bo, dessa forma decidi criar uma ferramenta de CLI (por hora) que auxiliasse as pessoas a fazerem network em cima dessas postagens do LinkedIn, afinal o que conta pra uma vaga de emprego é o famoso QI (Quem indica)
Vamos ao planejamento postumo
Como bom desenvolvedor, adepto do Go Horse, eu simplesmente fui desenvolvendo tudo da forma mais estúpida criativa que conheço, mas como nem toda boa ação sai impune, tive que organizar essa bagunça em passos com o inutito de tentar descrever o que eu pensei como solução pra essa brincadeira, afinal amo gastar um mês automatizando algo que faço em meia hora.
Piadas de gosto duvidoso a parte, esses são os objetivos e fluxos necessários para este projeto
- Implementar uma forma de autenticar no LinkedIn sem ser detectado como bot
- Buscar postagens com base na query do usuário, como por exemplo “vaga” + “estágio” + “java” + “remoto”
- Comentar (por hora de forma genérica) em postagens encontradas em que ainda não comentou
- Dar um like nessas postagens
- Se conectar aos autores da postagem
E assim fariamos nosso network.
Por hoje vamos criar a estrutura de gerenciamento dos navegadores e implementar o passo 1
Dependencias
Para entender melhor as depedências que serão usadas no projeto recomendo dar uma conferida (ou copiar e colar) as definições do nosso Dockerfile no repositório do bot
Vamos criar nosso container
Para facilitar o uso e por cuasa da minha preguiça, usaremos o selenium e o selenium-grid como base pra esse projeto, como a documentação descreve na captura que tirei no dia 16/03/2024 é uma ferramenta para automatizar o browser e é isso… Já o selenium-grid é uma solução que permite manipular webdrivers do selenium em maquinas geridas remotamente. Resumindo para nosso caso de uso, vamos criar uma configuração de containeres com o docker-composer que nos permita criar containeres de navegadores pré configurados para serem operados pelo selenium, asism estarei reduzindo todo o atrito que normalmente temos ao usar o selenium que é selecionar o navegador da maquina host pra ser usado.
Para fazer essa magia toda funcionar, iremos usar docker e docker-composer, que é por onde vamos começar:
# To execute this docker-compose yml file use `docker-compose -f docker-compose-v3-full-grid.yml up`
# Add the `-d` flag at the end for detached execution
# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3-full-grid.yml down`
version: "3"
services:
worker:
build: .
container_name: vagabot-worker
environment:
- LINKEDIN_EMAIL=${LINKEDIN_EMAIL}
- LINKEDIN_PASS=${LINKEDIN_PASS}
# this define host for selenium grid router, if you runing outside container is should be http://localhost:4444 by default
- SE_ROUTER_HOST=selenium-router
- SE_ROUTER_PORT=4444
- DB_FILENAME=./db/vagabot.db
depends_on:
- selenium-router
volumes:
- ./db:/usr/app/db
selenium-event-bus:
image: selenium/event-bus:4.1.1-20211217
container_name: vagabot-selenium-event-bus
ports:
- "4442:4442"
- "4443:4443"
- "5557:5557"
selenium-sessions:
image: selenium/sessions:4.1.1-20211217
container_name: vagabot-selenium-sessions
ports:
- "5556:5556"
depends_on:
- selenium-event-bus
environment:
- SE_EVENT_BUS_HOST=selenium-event-bus
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
selenium-session-queue:
image: selenium/session-queue:4.1.1-20211217
container_name: vagabot-selenium-session-queue
ports:
- "5559:5559"
depends_on:
- selenium-event-bus
environment:
- SE_EVENT_BUS_HOST=selenium-event-bus
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
selenium-distributor:
image: selenium/distributor:4.1.1-20211217
container_name: vagabot-selenium-distributor
ports:
- "5553:5553"
depends_on:
- selenium-event-bus
- selenium-sessions
- selenium-session-queue
environment:
- SE_EVENT_BUS_HOST=selenium-event-bus
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_SESSIONS_MAP_HOST=selenium-sessions
- SE_SESSIONS_MAP_PORT=5556
- SE_SESSION_QUEUE_HOST=selenium-session-queue
- SE_SESSION_QUEUE_PORT=5559
selenium-router:
image: selenium/router:4.1.1-20211217
container_name: vagabot-selenium-router
ports:
- "4444:4444"
depends_on:
- selenium-distributor
- selenium-sessions
- selenium-session-queue
environment:
- SE_DISTRIBUTOR_HOST=selenium-distributor
- SE_DISTRIBUTOR_PORT=5553
- SE_SESSIONS_MAP_HOST=selenium-sessions
- SE_SESSIONS_MAP_PORT=5556
- SE_SESSION_QUEUE_HOST=selenium-session-queue
- SE_SESSION_QUEUE_PORT=5559
chrome:
container_name: vagabot-selenium-chrome
image: selenium/node-chrome:4.1.1-20211217
shm_size: 2gb
depends_on:
- selenium-event-bus
environment:
- SE_EVENT_BUS_HOST=selenium-event-bus
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_INSTANCES=4
- SE_NODE_MAX_SESSIONS=4
Um pouco extenso não? Mas explicando o que acontece aqui nesse docker-compose, definimos os serviços do selenium-grid onde cada um tem uma responsabilidade
selenium-event-bus
: Este serviço executa o barramento de eventos Selenium, que é usado para comunicação entre os diferentes componentes da grade Selenium.selenium-sessions
: Este serviço gerencia as sessões Selenium. Ele depende doselenium-event-bus
.selenium-session-queue
: Este serviço é responsável por gerenciar a fila de sessões Selenium. Ele também depende doselenium-event-bus
.selenium-distributor
: Este serviço distribui as sessões Selenium para os nós disponíveis. Ele depende doselenium-event-bus
,selenium-sessions
eselenium-session-queue
.selenium-router
: Este serviço roteia as solicitações para o componente apropriado na grid Selenium. Ele depende doselenium-distributor
,selenium-sessions
eselenium-session-queue
.chrome
: Este serviço executa um nó Selenium que permite a execução de uma instância do chrome, podemos usar ele como base para definir outros navegadores como firefox e o Edge com suporte ao Internet Exxplorer habilitado.
Agora ao utilizar o comando abaixo teremos toda a nossa infra de selenium disponível com uma interface web que permite a visualização das sessões ativas por meio do protocolo VNC
docker compose up -d --build
Podemos verificar se está funcionado acessando localhost:4444, mas apenas verá um painel vazio como mostra o print abaixo
Implementando o browser e o login
Agora que temos nossa infraestrutura, vamos por nossa mão na massa para conseguir logar no LinkedIn sem ser detectado como bot, essa parte do projeto ainda está meio obscura, pois ainda não consegui desvendar de fato a metodologia aplicada pelo LinkedIn para esta detecção.
Para isso eu criei um diretório vagabot/workflows
para podeer modularizar melhor a aplicação, tudo que for executado no browser será nesse tal de módulo workflows
Então, usando python decidi definir uma instância de browser anonimizada por meio de configurações de agente. Essa brincadeira ficou mais ou menos assim:
# vagabot/workflows/linkedin_workflow.py 56,2
import time
from abc import abstractmethod
import fake_useragent
from decouple import config
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from undetected_chromedriver import ChromeOptions
class LinkedinWorkflow:
# TODO: moving foward for cli
_se_router_host = config("SE_ROUTER_HOST", "localhost")
_se_router_port = config("SE_ROUTER_PORT", "4444")
def __init__(self) -> None:
self.drivers: dict[str, WebDriver] = {}
if self._se_router_port:
self._se_router_url = (
f"http://{self._se_router_host}:{self._se_router_port}"
)
else:
self._se_router_url = f"http://{self._se_router_host}"
@staticmethod
def human_input_simulate(element: WebElement, content: str, delay=1):
for key in content:
time.sleep(delay)
element.send_keys(key)
def open_browser(self) -> str:
print(f"Open browser in {self._se_router_url}")
opts = ChromeOptions()
opts.add_argument(
f"user-agent={fake_useragent.UserAgent(os='windows', browsers=['chrommiun'])}"
)
driver = webdriver.Remote(options=opts, command_executor=self._se_router_url)
if not driver.session_id:
raise Exception("Not able to regisrer a driver")
driver.maximize_window()
self.drivers[driver.session_id] = driver
return driver.session_id
def close(self, driver_key: str):
self.drivers[driver_key].close()
del self.drivers[driver_key]
def __del__(self):
for driver_key in list(self.drivers.keys()):
self.close(driver_key)
@abstractmethod
def execute(self, *args, **kwargs): ...
usando a blibioteca fake_useragent
no método open_browser
, pude implementar um browser cujo as configurações são pouco detectáveis, de forma que simule um pc com Windows usando o chrome, algo não muito incomun certo?
Usando o webbdricver.Remote
provido pela blibioteca do selenium eu pude configurar uma instancia de browser que usa as variaveis de anbiente SE_ROUTER_HOST
e SE_ROUTER_PORT
No construtor da classe (método __init__
) eu implemento um dicionário de webdrivers, para que eu possa ter múltiplos browsers instanciados por execução, algo que usarei no futuro para paralelizar a execução
O método human_input_simulate
é uma gambiarra para que o input de textos e conteúdos ao serem digitados possuam um delay de um segundo ao menos para que o bot não seja detectado por digitar rápido demais, algo que acontecia com frequencia ao tentar logar
defini um método abstrato para poder usar essa classe como base para as demais, mas basicamente criamos uma classe abstrata que definie como vai ser a estrutura interna da execução que de fato irá controlar o navegador
Em seguida e por algum motivo que eu não me lembro, decidi separar a instância do navegador e a função de logar no LinkedIn em classes separadas, dessa forma ficamos com a seguinte implementação
# vagabot/workflows/linkedin_auth.py
import logging
import time
from selenium.common import exceptions
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from vagabot.workflows.linkedin_workflow import LinkedinWorkflow
class LinkedinAuth(LinkedinWorkflow):
USERNAME_INPUT_XPATH = "//input[@id='session_key']"
PASSWORD_INPUT_XPATH = "//input[@id='session_password']"
BUTTON_SUBMIT_XPATH = "//*[@id='main-content']/section/div/div/form/div/button"
def login(self, driver: WebDriver, username: str, password: str):
logging.info("go to site")
driver.maximize_window()
driver.get("https://www.linkedin.com")
input_wait = WebDriverWait(driver, timeout=30)
logging.info("set input")
input_list = {
self.USERNAME_INPUT_XPATH: username,
self.PASSWORD_INPUT_XPATH: password,
}
for xpath, value in input_list.items():
try:
txt_input = input_wait.until(
EC.presence_of_element_located((By.XPATH, xpath))
)
self.human_input_simulate(txt_input, value)
except exceptions.TimeoutException:
raise Exception(f"Not found {xpath} input")
try:
time.sleep(5)
btn_submit = input_wait.until(
EC.presence_of_element_located((By.XPATH, self.BUTTON_SUBMIT_XPATH))
)
btn_submit.click()
logging.info("button clicked")
except exceptions.TimeoutException:
raise Exception("Not found button input")
Aqui está uma explicação detalhada do que o código faz:
Importações: O código começa importando os módulos necessários. Isso inclui
logging
etime
do Python padrão, várias classes e funções do pacoteselenium
, e a classeLinkedinWorkflow
de um módulo local.Definição da Classe: A classe
LinkedinAuth
é definida, herdando deLinkedinWorkflow
. Ela contém três constantes de classe que definem os caminhos XPATH para os campos de entrada do nome de usuário e senha e o botão de envio na página de login do LinkedIn.Método de Login: A classe
LinkedinAuth
tem um método chamadologin
que aceita três argumentos: um objetoWebDriver
, um nome de usuário e uma senha. Este método automatiza o processo de login no LinkedIn da seguinte maneira:- Primeiro, ele registra uma mensagem de log, maximiza a janela do navegador e navega até a página inicial do LinkedIn.
- Em seguida, ele cria um objeto
WebDriverWait
que será usado para pausar a execução do script até que certas condições sejam atendidas. - Ele define um dicionário
input_list
que mapeia os caminhos XPATH dos campos de entrada para os valores correspondentes do nome de usuário e senha. - Para cada par de caminho XPATH e valor no dicionário
input_list
, ele tenta localizar o elemento de entrada correspondente na página e simula a digitação do valor no campo de entrada. Se o elemento de entrada não for encontrado dentro do tempo limite, ele lança uma exceção. - Depois de preencher os campos de entrada, ele pausa a execução do script por 5 segundos. Isso pode ser para dar tempo para a página processar as entradas ou para simular o comportamento humano.
- Finalmente, ele tenta localizar o botão de envio na página e clica nele. Se o botão de envio não for encontrado dentro do tempo limite, ele lança uma exceção.
Nós já conseguimos testar essa brincadeira no terminal interativo do python
Assumindo que você visitou o repositório do vagabot, tenha copiado nosso requirements.txt
e nosso arquivo pyproject.toml
, iremos iniciar nosso projeto da seguinte forma:
- Criando um anbiente virtual python
python -m venv .venv
- Ativando esse anbiente virtual (exemplo linux)
source .veenv/bin/activate
- Instalando o poetry no anbiente virtual
pip install -r requirements.txt
- Instalando as dependeicas do projeto via poetry
poetry install --no-interaction
- Defina um arquio .env com os valores
LINKEDIN_EMAIL
eLINKEDIN_PASS
com os valores de seu usuário e senha - Abra o interpretador do python localmente com o comando python apenas
- Digite os seguintes comandos no interpretador dinamico, lembrando que linha a linha para dar certo
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from decouple import config
>>> print(config("LINKEDIN_EMAIL"))
vfbraton@gmail.com
>>> from vagabot.workflows.linkedin_auth import LinkedinAuth
>>> service = LinkedinAuth()
>>> driver_key = service.open_browser()
Open browser in http://localhost:4444
>>> service.login(username=config("LINKEDIN_EMAIL"),password=config("LINKEDIN_PASS"),driver=service.drivers[driver_key])
>>> service.close(driver_key)
Agora podemos iniciar o anbiente interativo do python e importar o que queremos, com o cli ativo basta chamar nosso módulo e o método passando o login e a senha. Lembrando que antes desse passo recomenda-se logar no LinkedIn no seu navegador.
Como exemplo você pode ver o teste no vídeo abaixo do que fiz acima
E assim conseguimos cumprir nosso primeiro objetivo, uma implementação geral que consegue logar no LinkedIn.
Ufa! Por hoje é só, nos vemos por ai…