chore: expurgo de blobs excedentes e realinhamento de estado
release.yml (push) Invalid workflow file
release.yml (push) Invalid workflow file
This commit is contained in:
1 parent
593efd42e0
commit
a7cbd1a3b4
114 files changed
+20716
-4258
No files matched your search
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Micro-serviço Python para scraping com Nodriver (anti-detecção)
|
||||
Pipeline:
|
||||
1. Scraping: Nodriver (sem CDP, indetectável)
|
||||
2. Fallback: curl_cffi se Nodriver falhar
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import random
|
||||
import time
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Dict, Optional, Union, List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Bibliotecas de Scraping e Parsing
|
||||
from bs4 import BeautifulSoup
|
||||
import html2text
|
||||
|
||||
# Nodriver (anti-detecção)
|
||||
try:
|
||||
import nodriver as uc
|
||||
HAS_NODRIVER = True
|
||||
except ImportError:
|
||||
HAS_NODRIVER = False
|
||||
logging.warning("nodriver not installed, falling back to curl_cffi")
|
||||
|
||||
# curl_cffi como fallback
|
||||
try:
|
||||
from curl_cffi import requests
|
||||
HAS_CURL_CFFI = True
|
||||
except ImportError:
|
||||
HAS_CURL_CFFI = False
|
||||
|
||||
# Fallback libs
|
||||
try:
|
||||
import requests as pyrequests
|
||||
HAS_PYREQUESTS = True
|
||||
except ImportError:
|
||||
HAS_PYREQUESTS = False
|
||||
|
||||
# --- CONFIGURAÇÃO DO AMBIENTE ---
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
except (AttributeError, Exception):
|
||||
pass
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[PYTHON SCRAPER NODRIVER] %(levelname)s: %(message)s',
|
||||
stream=sys.stderr
|
||||
)
|
||||
|
||||
# --- CONSTANTES E POOLS ---
|
||||
|
||||
UA_POOL_FALLBACK = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0'
|
||||
]
|
||||
|
||||
REQUEST_TIMEOUT = 12 # Reduzido de 20 para 12s para maior velocidade
|
||||
MAX_RETRIES = 2
|
||||
NODRIVER_TIMEOUT = 10 # Timeout específico para nodriver (10s)
|
||||
|
||||
# --- FUNÇÕES AUXILIARES ---
|
||||
|
||||
def get_random_ua() -> str:
|
||||
return random.choice(UA_POOL_FALLBACK)
|
||||
|
||||
def random_jitter():
|
||||
"""Adiciona jitter aleatório para evitar padrões"""
|
||||
time.sleep(random.uniform(0.1, 0.5))
|
||||
|
||||
def clean_html(soup: BeautifulSoup) -> BeautifulSoup:
|
||||
"""Remove scripts, styles e outros elementos desnecessários"""
|
||||
for script in soup(["script", "style", "nav", "header", "footer", "aside", "noscript"]):
|
||||
script.decompose()
|
||||
return soup
|
||||
|
||||
def extract_main_content(soup: BeautifulSoup) -> BeautifulSoup:
|
||||
"""Extrai conteúdo principal usando heurísticas"""
|
||||
# Tentar encontrar main, article, ou div com classe comum
|
||||
selectors = [
|
||||
'main',
|
||||
'article',
|
||||
'[role="main"]',
|
||||
'.content',
|
||||
'.main-content',
|
||||
'#content',
|
||||
'#main',
|
||||
]
|
||||
|
||||
for selector in selectors:
|
||||
main = soup.select_one(selector)
|
||||
if main:
|
||||
return main
|
||||
|
||||
# Fallback: retornar body
|
||||
return soup.find('body') or soup
|
||||
|
||||
def html_to_markdown(html: str) -> str:
|
||||
"""Converte HTML para Markdown"""
|
||||
h = html2text.HTML2Text()
|
||||
h.ignore_links = False
|
||||
h.ignore_images = False
|
||||
h.body_width = 0
|
||||
return h.handle(html)
|
||||
|
||||
# --- SCRAPING COM NODRIVER ---
|
||||
|
||||
async def scrape_with_nodriver(url: str) -> Dict:
|
||||
"""Scraping usando Nodriver (anti-detecção) - OTIMIZADO"""
|
||||
if not HAS_NODRIVER:
|
||||
return {"error": "nodriver not installed"}
|
||||
|
||||
try:
|
||||
logging.info(f"[NODRIVER] Starting scrape for {url}")
|
||||
start_time = time.time()
|
||||
|
||||
# Iniciar browser com Nodriver (sem CDP, indetectável)
|
||||
browser = await uc.start(headless=True)
|
||||
|
||||
try:
|
||||
# Navegar para URL com timeout
|
||||
page = await asyncio.wait_for(
|
||||
browser.get(url),
|
||||
timeout=NODRIVER_TIMEOUT
|
||||
)
|
||||
|
||||
# Aguardar conteúdo carregar com wait condicional otimizado
|
||||
# Tentar múltiplas estratégias de wait para reduzir tempo total
|
||||
try:
|
||||
# Estratégia 1: Aguardar DOMContentLoaded (mais rápido)
|
||||
await asyncio.wait_for(
|
||||
page.wait_for_load_state('domcontentloaded'),
|
||||
timeout=2.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# Se DOMContentLoaded demorar, usar sleep reduzido
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Aguardar um pouco mais para JS executar (reduzido de 2s para 0.3s)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# Extrair HTML
|
||||
html = await page.get_content()
|
||||
title = await page.evaluate('document.title')
|
||||
|
||||
duration = time.time() - start_time
|
||||
logging.info(f"[NODRIVER] Successfully scraped {url} ({len(html)} chars, {duration:.2f}s)")
|
||||
|
||||
return {
|
||||
"content": html,
|
||||
"title": title or "",
|
||||
"method": "nodriver"
|
||||
}
|
||||
finally:
|
||||
await browser.stop()
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logging.warning(f"[NODRIVER] Timeout after {NODRIVER_TIMEOUT}s for {url}")
|
||||
return {"error": f"Timeout after {NODRIVER_TIMEOUT}s"}
|
||||
except Exception as e:
|
||||
logging.error(f"[NODRIVER] Error scraping {url}: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
# --- SCRAPING COM CURL_CFFI (FALLBACK) ---
|
||||
|
||||
def scrape_with_curl_cffi(url: str) -> Dict:
|
||||
"""Scraping usando curl_cffi como fallback - OTIMIZADO"""
|
||||
if not HAS_CURL_CFFI:
|
||||
return {"error": "curl_cffi not installed"}
|
||||
|
||||
ua = get_random_ua()
|
||||
headers = {
|
||||
'User-Agent': ua,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
}
|
||||
|
||||
try:
|
||||
# Timeout reduzido para fallback rápido
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=8, # Reduzido de 15 para 8s
|
||||
impersonate="chrome120",
|
||||
allow_redirects=True
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code in [403, 429, 503]:
|
||||
raise requests.RequestsError(f"Blocked with status {response.status_code}")
|
||||
|
||||
return {
|
||||
"content": response.text,
|
||||
"title": "",
|
||||
"method": "curl_cffi",
|
||||
"headers": dict(response.headers)
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# --- SCRAPING INTELIGENTE ---
|
||||
|
||||
async def scrape_url_smart(url: str) -> Dict:
|
||||
"""Scraping inteligente: Nodriver (PRIMÁRIO) → curl_cffi (fallback) - OTIMIZADO"""
|
||||
# Jitter reduzido para maior velocidade
|
||||
await asyncio.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# Tentativa 1: Nodriver (PRIMÁRIO, anti-detecção)
|
||||
if HAS_NODRIVER:
|
||||
logging.info(f"[PRIMARY] Nodriver -> {url}")
|
||||
result = await scrape_with_nodriver(url)
|
||||
|
||||
if "error" not in result:
|
||||
html_content = result["content"]
|
||||
# Verificar se não é página de bloqueio (mais permissivo para velocidade)
|
||||
if len(html_content) > 1000: # Reduzido de 5000 para 1000 para aceitar mais resultados
|
||||
if "challenge-platform" not in html_content.lower() and "cloudflare" not in html_content.lower():
|
||||
return result
|
||||
else:
|
||||
logging.warning("[NODRIVER] Detectado desafio Cloudflare/WAF. Tentando fallback.")
|
||||
elif len(html_content) > 200: # Aceitar conteúdo mínimo
|
||||
logging.info(f"[NODRIVER] Conteúdo mínimo mas aceitável ({len(html_content)} chars)")
|
||||
return result
|
||||
|
||||
# Tentativa 2: curl_cffi (fallback rápido)
|
||||
if HAS_CURL_CFFI:
|
||||
logging.info(f"[FALLBACK] curl_cffi -> {url}")
|
||||
result = scrape_with_curl_cffi(url)
|
||||
|
||||
if "error" not in result:
|
||||
html_content = result["content"]
|
||||
if len(html_content) > 1000:
|
||||
if "challenge-platform" not in html_content.lower() and "cloudflare" not in html_content.lower():
|
||||
return result
|
||||
|
||||
# Se ambos falharam, retornar erro
|
||||
return {"error": "All scraping methods failed"}
|
||||
|
||||
# --- PROCESSAMENTO HTML ---
|
||||
|
||||
async def process_scraped_content(url: str, result: Dict) -> Dict:
|
||||
"""Processa conteúdo scraped e converte para markdown"""
|
||||
if "error" in result:
|
||||
return {"response_type": "error", "message": result["error"]}
|
||||
|
||||
html_content = result.get("content", "")
|
||||
title = result.get("title", "")
|
||||
|
||||
if not html_content:
|
||||
return {"response_type": "error", "message": "No content extracted"}
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
if not title:
|
||||
title = soup.title.string.strip() if soup.title else url
|
||||
|
||||
soup = clean_html(soup)
|
||||
main_content = extract_main_content(soup)
|
||||
cleaned_html = str(main_content)
|
||||
markdown_text = html_to_markdown(cleaned_html)
|
||||
final_markdown = '\n'.join([line.strip() for line in markdown_text.split('\n') if line.strip()])
|
||||
|
||||
if len(final_markdown) < 50:
|
||||
return {"response_type": "error", "message": "Conteúdo insuficiente após processamento."}
|
||||
|
||||
return {
|
||||
"response_type": "success",
|
||||
"url": url,
|
||||
"title": title,
|
||||
"markdown": final_markdown,
|
||||
"content": final_markdown
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"Erro no processamento HTML: {traceback.format_exc()}")
|
||||
return {"response_type": "error", "message": f"Processing error: {str(e)}"}
|
||||
|
||||
# --- MAIN LOOP ---
|
||||
|
||||
def write_json_response(data: Dict):
|
||||
try:
|
||||
json_line = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
print(json_line)
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
logging.error(f"Erro enviando resposta JSON: {e}")
|
||||
|
||||
async def main():
|
||||
logging.info("Scraper/Search Service v5.0 (Nodriver Edition) Initialized")
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
write_json_response({"response_type": "error", "message": "Invalid JSON"})
|
||||
continue
|
||||
|
||||
command = request.get("command")
|
||||
|
||||
if command == "scrape":
|
||||
url = request.get("url")
|
||||
if url:
|
||||
result = await scrape_url_smart(url)
|
||||
processed = await process_scraped_content(url, result)
|
||||
write_json_response(processed)
|
||||
else:
|
||||
write_json_response({"response_type": "error", "message": "No URL provided"})
|
||||
else:
|
||||
write_json_response({"response_type": "error", "message": f"Unknown command: {command}"})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in main loop: {traceback.format_exc()}")
|
||||
write_json_response({"response_type": "error", "message": str(e)})
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in new issue
Block a user