feat: add JobScraper_MultiSite Python project
- main_scraper.py: Main entry point, consolidates data from all sources - scrapers/programathor_scraper.py: Scraper for ProgramaThor - scrapers/geekhunter_scraper.py: Scraper for GeekHunter - requirements.txt: Python dependencies (requests, beautifulsoup4, pandas) - README.md: Documentation with usage instructions - Modular architecture for easy addition of new sites
This commit is contained in:
parent
15fe5db50e
commit
8856357acd
7 changed files with 401 additions and 0 deletions
86
JobScraper_MultiSite/README.md
Normal file
86
JobScraper_MultiSite/README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# 🐴 JobScraper MultiSite
|
||||||
|
|
||||||
|
Raspador de vagas de emprego multi-plataforma para sites de tecnologia brasileiros.
|
||||||
|
|
||||||
|
## 📁 Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
JobScraper_MultiSite/
|
||||||
|
├── main_scraper.py # Arquivo principal
|
||||||
|
├── scrapers/ # Módulos de raspagem por site
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── programathor_scraper.py
|
||||||
|
│ └── geekhunter_scraper.py
|
||||||
|
├── output/ # Arquivos CSV gerados
|
||||||
|
│ └── vagas_consolidadas.csv
|
||||||
|
├── requirements.txt
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Criar ambiente virtual (recomendado)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Linux/Mac
|
||||||
|
# ou: venv\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# Instalar dependências
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Ou instalar manualmente:
|
||||||
|
pip install requests beautifulsoup4 pandas
|
||||||
|
```
|
||||||
|
|
||||||
|
## ▶️ Execução
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Executar raspagem de todos os sites
|
||||||
|
python main_scraper.py
|
||||||
|
|
||||||
|
# Testar um scraper individual
|
||||||
|
python -m scrapers.programathor_scraper
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Output
|
||||||
|
|
||||||
|
Os resultados são salvos na pasta `output/`:
|
||||||
|
- `vagas_consolidadas.csv` - Versão mais recente
|
||||||
|
- `vagas_consolidadas_YYYYMMDD_HHMMSS.csv` - Versões com timestamp
|
||||||
|
|
||||||
|
### Campos extraídos:
|
||||||
|
| Campo | Descrição |
|
||||||
|
|-------------|------------------------------|
|
||||||
|
| titulo | Título da vaga |
|
||||||
|
| empresa | Nome da empresa |
|
||||||
|
| localizacao | Localização/Modalidade |
|
||||||
|
| link | URL da vaga |
|
||||||
|
| fonte | Site de origem |
|
||||||
|
|
||||||
|
## ➕ Adicionando Novos Sites
|
||||||
|
|
||||||
|
1. Crie um novo arquivo em `scrapers/` (ex: `novosite_scraper.py`)
|
||||||
|
2. Implemente a função `scrape_novosite()` seguindo o padrão existente
|
||||||
|
3. Adicione ao dicionário `SITES` em `main_scraper.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from scrapers.novosite_scraper import scrape_novosite
|
||||||
|
|
||||||
|
SITES = {
|
||||||
|
'programathor': scrape_programathor,
|
||||||
|
'geekhunter': scrape_geekhunter,
|
||||||
|
'novosite': scrape_novosite, # Novo!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Boas Práticas Anti-Bloqueio
|
||||||
|
|
||||||
|
- ✅ Sempre use `time.sleep()` entre requisições (mínimo 2s)
|
||||||
|
- ✅ Use headers que simulem um navegador real
|
||||||
|
- ✅ Não faça muitas requisições em sequência rápida
|
||||||
|
- ✅ Respeite o `robots.txt` de cada site
|
||||||
|
- ✅ Considere usar proxies para grandes volumes
|
||||||
|
|
||||||
|
## 📝 Licença
|
||||||
|
|
||||||
|
Uso educacional. Respeite os Termos de Serviço de cada site.
|
||||||
130
JobScraper_MultiSite/main_scraper.py
Normal file
130
JobScraper_MultiSite/main_scraper.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
🐴 JobScraper MultiSite - Raspador de Vagas Multi-Plataforma
|
||||||
|
=============================================================
|
||||||
|
Consolida vagas de emprego de múltiplos sites de tecnologia.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Imports dos scrapers
|
||||||
|
from scrapers.programathor_scraper import scrape_programathor
|
||||||
|
from scrapers.geekhunter_scraper import scrape_geekhunter
|
||||||
|
|
||||||
|
# Configuração: Sites a serem raspados
|
||||||
|
SITES = {
|
||||||
|
'programathor': scrape_programathor,
|
||||||
|
'geekhunter': scrape_geekhunter,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pasta de output
|
||||||
|
OUTPUT_DIR = 'output'
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_output_dir():
|
||||||
|
"""Cria a pasta de output se não existir."""
|
||||||
|
if not os.path.exists(OUTPUT_DIR):
|
||||||
|
os.makedirs(OUTPUT_DIR)
|
||||||
|
print(f"📁 Pasta '{OUTPUT_DIR}' criada.")
|
||||||
|
|
||||||
|
|
||||||
|
def run_scrapers(sites_to_run: list = None) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Executa os scrapers configurados e consolida os resultados.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sites_to_run: Lista de sites para raspar. Se None, raspa todos.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame consolidado com todas as vagas.
|
||||||
|
"""
|
||||||
|
if sites_to_run is None:
|
||||||
|
sites_to_run = list(SITES.keys())
|
||||||
|
|
||||||
|
all_vagas = []
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("🐴 JobScraper MultiSite - Iniciando raspagem...")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for site_name in sites_to_run:
|
||||||
|
if site_name not in SITES:
|
||||||
|
print(f"⚠️ Site '{site_name}' não configurado. Pulando...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n📡 Processando: {site_name.upper()}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
scraper_func = SITES[site_name]
|
||||||
|
df = scraper_func()
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
all_vagas.append(df)
|
||||||
|
|
||||||
|
if all_vagas:
|
||||||
|
consolidated_df = pd.concat(all_vagas, ignore_index=True)
|
||||||
|
# Remover duplicatas baseado no link
|
||||||
|
consolidated_df.drop_duplicates(subset=['link'], inplace=True)
|
||||||
|
return consolidated_df
|
||||||
|
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def save_results(df: pd.DataFrame, filename: str = None) -> str:
|
||||||
|
"""
|
||||||
|
Salva o DataFrame consolidado em CSV.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame com as vagas.
|
||||||
|
filename: Nome do arquivo (opcional).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Caminho do arquivo salvo.
|
||||||
|
"""
|
||||||
|
ensure_output_dir()
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
filename = f'vagas_consolidadas_{timestamp}.csv'
|
||||||
|
|
||||||
|
filepath = os.path.join(OUTPUT_DIR, filename)
|
||||||
|
df.to_csv(filepath, index=False, encoding='utf-8-sig')
|
||||||
|
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Função principal."""
|
||||||
|
# Executar scrapers
|
||||||
|
df = run_scrapers()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print("❌ Nenhuma vaga encontrada.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Estatísticas
|
||||||
|
print(f"📊 Total de vagas coletadas: {len(df)}")
|
||||||
|
print(f"📊 Fontes: {df['fonte'].value_counts().to_dict()}")
|
||||||
|
|
||||||
|
# Salvar resultados
|
||||||
|
filepath = save_results(df)
|
||||||
|
print(f"\n💾 Vagas salvas em: {filepath}")
|
||||||
|
|
||||||
|
# Também salvar versão "latest"
|
||||||
|
latest_path = save_results(df, 'vagas_consolidadas.csv')
|
||||||
|
print(f"💾 Versão atual: {latest_path}")
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("✅ Raspagem concluída com sucesso!")
|
||||||
|
|
||||||
|
# Preview
|
||||||
|
print("\n📋 Preview das vagas:")
|
||||||
|
print(df[['titulo', 'empresa', 'fonte']].head(10).to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
JobScraper_MultiSite/output/.gitkeep
Normal file
1
JobScraper_MultiSite/output/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Keep this folder in git
|
||||||
4
JobScraper_MultiSite/requirements.txt
Normal file
4
JobScraper_MultiSite/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
requests>=2.28.0
|
||||||
|
beautifulsoup4>=4.11.0
|
||||||
|
pandas>=1.5.0
|
||||||
|
lxml>=4.9.0
|
||||||
2
JobScraper_MultiSite/scrapers/__init__.py
Normal file
2
JobScraper_MultiSite/scrapers/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Scrapers package
|
||||||
|
# Each site has its own scraper module
|
||||||
89
JobScraper_MultiSite/scrapers/geekhunter_scraper.py
Normal file
89
JobScraper_MultiSite/scrapers/geekhunter_scraper.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
Scraper para GeekHunter - https://www.geekhunter.com.br/vagas
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import pandas as pd
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Headers para simular navegador e evitar bloqueios
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||||
|
}
|
||||||
|
|
||||||
|
def scrape_geekhunter(delay: float = 2.0) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Raspa vagas do site GeekHunter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delay: Tempo de espera antes da requisição (anti-bloqueio)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame com colunas: titulo, empresa, localizacao, link
|
||||||
|
"""
|
||||||
|
url = "https://www.geekhunter.com.br/vagas"
|
||||||
|
vagas = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Delay anti-bloqueio
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
print(f"🔍 Raspando vagas de: {url}")
|
||||||
|
response = requests.get(url, headers=HEADERS, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Encontrar cards de vagas (ajustar seletores conforme estrutura do site)
|
||||||
|
job_cards = soup.select('.job-card') or soup.select('[class*="job"]') or soup.select('article')
|
||||||
|
|
||||||
|
for card in job_cards:
|
||||||
|
try:
|
||||||
|
# Extrair título
|
||||||
|
titulo_elem = card.select_one('h2') or card.select_one('h3') or card.select_one('.title')
|
||||||
|
titulo = titulo_elem.get_text(strip=True) if titulo_elem else "N/A"
|
||||||
|
|
||||||
|
# Extrair empresa
|
||||||
|
empresa_elem = card.select_one('.company') or card.select_one('[class*="company"]')
|
||||||
|
empresa = empresa_elem.get_text(strip=True) if empresa_elem else "N/A"
|
||||||
|
|
||||||
|
# Extrair localização
|
||||||
|
loc_elem = card.select_one('.location') or card.select_one('[class*="location"]')
|
||||||
|
localizacao = loc_elem.get_text(strip=True) if loc_elem else "Remoto"
|
||||||
|
|
||||||
|
# Extrair link
|
||||||
|
link_elem = card.select_one('a[href*="/vagas/"]') or card.select_one('a')
|
||||||
|
if link_elem:
|
||||||
|
href = link_elem.get('href', '')
|
||||||
|
link = f"https://www.geekhunter.com.br{href}" if href.startswith('/') else href
|
||||||
|
else:
|
||||||
|
link = url
|
||||||
|
|
||||||
|
vagas.append({
|
||||||
|
'titulo': titulo,
|
||||||
|
'empresa': empresa,
|
||||||
|
'localizacao': localizacao,
|
||||||
|
'link': link,
|
||||||
|
'fonte': 'GeekHunter'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erro ao processar card: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"✅ {len(vagas)} vagas encontradas no GeekHunter")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Erro na requisição ao GeekHunter: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erro inesperado no GeekHunter: {e}")
|
||||||
|
|
||||||
|
return pd.DataFrame(vagas)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Teste individual do scraper
|
||||||
|
df = scrape_geekhunter()
|
||||||
|
print(df.head())
|
||||||
89
JobScraper_MultiSite/scrapers/programathor_scraper.py
Normal file
89
JobScraper_MultiSite/scrapers/programathor_scraper.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
Scraper para ProgramaThor - https://programathor.com.br/jobs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import pandas as pd
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Headers para simular navegador e evitar bloqueios
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||||
|
}
|
||||||
|
|
||||||
|
def scrape_programathor(delay: float = 2.0) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Raspa vagas do site ProgramaThor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delay: Tempo de espera antes da requisição (anti-bloqueio)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame com colunas: titulo, empresa, localizacao, link
|
||||||
|
"""
|
||||||
|
url = "https://programathor.com.br/jobs"
|
||||||
|
vagas = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Delay anti-bloqueio
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
print(f"🔍 Raspando vagas de: {url}")
|
||||||
|
response = requests.get(url, headers=HEADERS, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Encontrar cards de vagas (ajustar seletores conforme estrutura do site)
|
||||||
|
job_cards = soup.select('.cell-list')
|
||||||
|
|
||||||
|
for card in job_cards:
|
||||||
|
try:
|
||||||
|
# Extrair título
|
||||||
|
titulo_elem = card.select_one('h3') or card.select_one('.title')
|
||||||
|
titulo = titulo_elem.get_text(strip=True) if titulo_elem else "N/A"
|
||||||
|
|
||||||
|
# Extrair empresa
|
||||||
|
empresa_elem = card.select_one('.company-name') or card.select_one('h4')
|
||||||
|
empresa = empresa_elem.get_text(strip=True) if empresa_elem else "N/A"
|
||||||
|
|
||||||
|
# Extrair localização
|
||||||
|
loc_elem = card.select_one('.location') or card.select_one('.info')
|
||||||
|
localizacao = loc_elem.get_text(strip=True) if loc_elem else "Remoto"
|
||||||
|
|
||||||
|
# Extrair link
|
||||||
|
link_elem = card.select_one('a[href*="/jobs/"]')
|
||||||
|
if link_elem:
|
||||||
|
href = link_elem.get('href', '')
|
||||||
|
link = f"https://programathor.com.br{href}" if href.startswith('/') else href
|
||||||
|
else:
|
||||||
|
link = url
|
||||||
|
|
||||||
|
vagas.append({
|
||||||
|
'titulo': titulo,
|
||||||
|
'empresa': empresa,
|
||||||
|
'localizacao': localizacao,
|
||||||
|
'link': link,
|
||||||
|
'fonte': 'ProgramaThor'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erro ao processar card: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"✅ {len(vagas)} vagas encontradas no ProgramaThor")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Erro na requisição ao ProgramaThor: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erro inesperado no ProgramaThor: {e}")
|
||||||
|
|
||||||
|
return pd.DataFrame(vagas)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Teste individual do scraper
|
||||||
|
df = scrape_programathor()
|
||||||
|
print(df.head())
|
||||||
Loading…
Reference in a new issue