diff --git a/JobScraper_MultiSite/README.md b/JobScraper_MultiSite/README.md new file mode 100644 index 0000000..b76dda1 --- /dev/null +++ b/JobScraper_MultiSite/README.md @@ -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. diff --git a/JobScraper_MultiSite/main_scraper.py b/JobScraper_MultiSite/main_scraper.py new file mode 100644 index 0000000..9136ec9 --- /dev/null +++ b/JobScraper_MultiSite/main_scraper.py @@ -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() diff --git a/JobScraper_MultiSite/output/.gitkeep b/JobScraper_MultiSite/output/.gitkeep new file mode 100644 index 0000000..b8cc9fd --- /dev/null +++ b/JobScraper_MultiSite/output/.gitkeep @@ -0,0 +1 @@ +# Keep this folder in git diff --git a/JobScraper_MultiSite/requirements.txt b/JobScraper_MultiSite/requirements.txt new file mode 100644 index 0000000..d36f2b3 --- /dev/null +++ b/JobScraper_MultiSite/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.28.0 +beautifulsoup4>=4.11.0 +pandas>=1.5.0 +lxml>=4.9.0 diff --git a/JobScraper_MultiSite/scrapers/__init__.py b/JobScraper_MultiSite/scrapers/__init__.py new file mode 100644 index 0000000..ed9cf4e --- /dev/null +++ b/JobScraper_MultiSite/scrapers/__init__.py @@ -0,0 +1,2 @@ +# Scrapers package +# Each site has its own scraper module diff --git a/JobScraper_MultiSite/scrapers/geekhunter_scraper.py b/JobScraper_MultiSite/scrapers/geekhunter_scraper.py new file mode 100644 index 0000000..c599f46 --- /dev/null +++ b/JobScraper_MultiSite/scrapers/geekhunter_scraper.py @@ -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()) diff --git a/JobScraper_MultiSite/scrapers/programathor_scraper.py b/JobScraper_MultiSite/scrapers/programathor_scraper.py new file mode 100644 index 0000000..dc1bf88 --- /dev/null +++ b/JobScraper_MultiSite/scrapers/programathor_scraper.py @@ -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())