bcc1fe1738
El monitor de captación scrapeaba Workana sobre el navegador personal del usuario (chromium-personal, CDP 9222), interfiriendo con su navegación. El scraping CDP debe correr siempre en un perfil headless dedicado. - Nuevo pipeline monitor_freelance_projects_headless: levanta un Chromium headless aislado con perfil dedicado (~/.config/fn_scrape_chrome, CDP 9334) vía systemd-run, ejecuta monitor_freelance_projects contra ese puerto y cierra la instancia al terminar (finally). Reutiliza el patrón de lifecycle de ingest_market_trends_headless. Reutiliza un CDP vivo si el puerto ya responde (no cierra lo ajeno). - scrape_workana_projects y monitor_freelance_projects: default de `port` cambiado de 9222 (chromium-personal) a 9334 (perfil dedicado). Default seguro: correr a pelo sin Chrome en 9334 falla limpio, no contamina el 9222 personal. Verificado: el wrapper arranca headless en 9334, scrapea 8 proyectos reales de Workana, cierra la instancia (9334 muerto, sin proceso colgado) y deja el 9222 personal intacto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.0 KiB
6.0 KiB
name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
| name | kind | lang | domain | version | purity | signature | description | tags | uses_functions | uses_types | returns | returns_optional | error_type | imports | params | output | tested | tests | test_file_path | file_path | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| scrape_workana_projects | function | py | browser | 1.0.0 | impure | def scrape_workana_projects(category: str = 'it-programming', language: str = 'es', extra_query: str = '', pages: int = 1, port: int = 9334, timeout_s: float = 20.0) -> dict | Scraper de proyectos freelance de Workana (https://www.workana.com/jobs) via Chrome DevTools Protocol (CDP). Workana es una SPA Vue: el GET HTTP NO trae los proyectos (0 cards en el HTML inicial), hay que renderizar con JS. Navega con un Chrome remoto, espera a que los cards monten async y extrae cada proyecto con un evaluador JS validado. Pieza 1 de un monitor de captacion de clientes: detecta proyectos freelance nuevos sin abrir el navegador a mano. Shape unificado con el scraper hermano de Upwork. Devuelve un dict con count + lista de proyectos; nunca lanza ni inventa datos. |
|
|
false | error_py_core |
|
dict siempre (nunca lanza). En exito: {status:'ok', source:'workana', count:N, projects:[{...}]}. Cada project_dict con claves EXACTAS: source ('workana'), job_id (slug), url (absoluta), title, budget (str|None), posted (str ej 'Hace 4 horas'), bids (str|None nº propuestas), skills (list[str]), snippet (str), country (str|None), scraped_at (ISO8601 UTC). En error (sin cards tras timeout, Chrome muerto, DOM cambiado): {status:'error', error:<mensaje claro>, source:'workana', projects:[]}. NUNCA devuelve filas falsas. | false | python/functions/browser/scrape_workana_projects.py |
Ejemplo
# fn run mapea args POSICIONALMENTE a la firma (category language extra_query pages port timeout_s).
# NO uses flags --category/--language con fn run: el runner los toma como valores posicionales.
# Perfil headless dedicado (port 9334, lo levanta el wrapper monitor_freelance_projects_headless):
fn run scrape_workana_projects it-programming es "" 1 9334 25
# Smoke contra el Chrome aislado interactivo del browser_mcp (port 9333, sin login):
fn run scrape_workana_projects it-programming es "" 1 9333 25
# Ejecucion directa del modulo SI acepta flags --... (argparse del __main__):
python/.venv/bin/python3 python/functions/browser/scrape_workana_projects.py \
--category it-programming --language es --port 9334
import sys, os, json
sys.path.insert(0, os.path.join("python", "functions"))
from browser.scrape_workana_projects import scrape_workana_projects
# Detecta proyectos nuevos en it-programming (es), 1 pagina, via Chrome diario.
res = scrape_workana_projects(category="it-programming", language="es", port=9222)
if res["status"] == "ok":
print(f"{res['count']} proyectos")
for p in res["projects"][:3]:
print("-", p["title"], "|", p["budget"], "|", p["posted"])
else:
print("error:", res["error"])
Cuando usarla
Monitor de captacion: detectar proyectos freelance nuevos en Workana sin abrir el navegador a mano. Lanzala periodicamente (ej. desde el dag_engine) para vigilar una categoria/idioma y alimentar el pipeline de market-intel. Usala cuando necesites el listado renderizado de Workana de forma programatica — el GET HTTP puro NO sirve porque la pagina es una SPA Vue que monta los cards en runtime.
Gotchas
- Requiere un Chrome con remote debugging vivo en
port: por defecto 9334 (el perfil headless dedicado del scraping, que levanta/cierra el wrappermonitor_freelance_projects_headless). NO usa 9222 (chromium-personal del usuario) por defecto: el scraping no abre pestanas en el navegador diario. 9333 (browser_mcp) sirve para smoke interactivo. Sin Chrome escuchando devuelve{status:'error', error:'no hay Chrome en el puerto N...'}— no lanza. - Workana es una SPA Vue: los cards montan ASYNC tras la hidratacion. El load
event NO garantiza que esten en el DOM, por eso la funcion hace polling de
document.querySelectorAll('div.project-item.js-project').lengthhasta >0 o timeout. Si la conexion es lenta, subetimeout_s. - HTTP puro NO sirve: un GET a la URL de listado trae 0 cards (HTML inicial vacio). CDP es obligatorio para renderizar el JavaScript.
- NUNCA inventa datos: si no aparecen cards tras el timeout (chromium en port no
logueado, DOM cambiado), devuelve
status='error'conprojects:[]. No hay filas falsas. - Respeta el rate-limit de Workana: no abuses (no la lances en bucle agresivo ni con muchas paginas seguidas). Workana puede aplicar anti-bot si detecta scraping intensivo.
- El selector del DOM (
div.project-item.js-project) y el extractor JS dependen del HTML actual de Workana. Si Workana cambia su markup, el extractor deja de encontrar cards y la funcion devuelvestatus='error'(no datos corruptos). En ese caso hay que actualizar_CARD_SELECTORy_EXTRACTOR_JS.