Files
fn_registry/python/functions/browser/scrape_upwork_projects.md
T
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00

5.8 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_upwork_projects function py browser 0.1.0 impure def scrape_upwork_projects(query: str = '', sort: str = 'recency', pages: int = 1, port: int = 9222, timeout_s: float = 25.0) -> dict Scraper de ofertas de trabajo (jobs) de Upwork via Chrome DevTools Protocol sobre una pestana YA LOGUEADA del navegador diario (chromium-personal, port 9222). Upwork tiene anti-bot fuerte (Cloudflare + PerimeterX): HTTP puro recibe 403 y la busqueda real exige sesion. Por eso navega via CDP a /nx/search/jobs, hace polling hasta que montan las job tiles (SPA) y extrae con un solo eval. Pieza 2 (hermana de scrape_workana_projects) de un monitor de captacion de clientes. Devuelve el shape unificado: status, source='upwork', count, projects con job_id, url, title, budget, posted, bids, skills, snippet, country, scraped_at. NUNCA inventa datos: sin tiles devuelve status error.
market-intel
recon
flow-replay
browser
cdp
upwork
scraper
jobs
freelance
captacion-clientes
cdp_open_url_and_wait_py_pipelines
cdp_eval_py_browser
false error_py_core
name desc
query Busqueda libre, ej. 'custom software'. Se url-encodea. '' (vacio) = listado de jobs por defecto.
name desc
sort Orden de resultados: 'recency' (mas recientes, default) o 'relevance'. Cualquier otro valor cae a 'recency'.
name desc
pages Numero de paginas de resultados a recorrer (>=1). Default 1. Cada pagina = navegacion + extraccion.
name desc
port Puerto de remote debugging del Chrome LOGUEADO en Upwork. Default 9222 (chromium-personal, navegador diario con sesion activa). NO usar 9333 (Chrome aislado del browser_mcp, sin login).
name desc
timeout_s Timeout por pagina en segundos para navegacion + aparicion de las job tiles (polling cada 1s). Default 25.0.
dict siempre (nunca lanza). En exito: {status:'ok', source:'upwork', count:N, projects:[{source:'upwork', job_id, url, title, budget, posted, bids, skills:list[str], snippet, country, scraped_at:ISO8601-UTC}, ...]}. En error (sin job tiles): {status:'error', error:<mensaje claro>, source:'upwork', projects:[]}. Shape IDENTICO al scraper de Workana para que un agregador downstream consuma ambas fuentes sin ramas. Campos no encontrados en el DOM quedan a null. false
python/functions/browser/scrape_upwork_projects.py

Ejemplo

# Requiere chromium-personal LOGUEADO en Upwork escuchando en port 9222.
fn run scrape_upwork_projects --query "custom software" --sort recency
import sys, os, json
sys.path.insert(0, os.path.join("python", "functions"))
from browser.scrape_upwork_projects import scrape_upwork_projects

# Navega a la busqueda logueada y extrae las job tiles de la primera pagina.
res = scrape_upwork_projects(query="custom software", sort="recency", pages=1, port=9222)
if res["status"] == "ok":
    print(f"{res['count']} jobs de Upwork")
    for job in res["projects"][:3]:
        print(job["title"], "|", job["budget"], "|", job["country"])
else:
    # Sin sesion logueada o selectores desfasados → error explicito, projects vacio.
    print("error:", res["error"])

Cuando usarla

Cuando necesites el feed de jobs/ofertas de Upwork para un monitor de captacion de clientes y tengas el navegador diario (chromium-personal) LOGUEADO en Upwork. Es la pieza 2 del par con scrape_workana_projects: ambas devuelven el mismo shape unificado, asi que un agregador downstream las consume sin ramas especiales. Usala cuando el HTTP puro no sirve (Upwork = 403 por Cloudflare + PerimeterX) y la busqueda exige sesion. Para una sola consulta puntual: fn run scrape_upwork_projects --query "...". Para recorrer varias paginas: sube pages.

Gotchas

  • Selectores NO validados en vivo (sin sesion al crearla). El extractor JS usa selectores best-effort de Upwork con cascada por campo. Estan declarados en la constante SELECTORS del .py para que corregirlos sea trivial. Valida los selectores con una busqueda real ANTES de confiar en produccion: Upwork cambia el DOM con frecuencia. Si un campo sale null de forma sistematica, el selector de ese campo esta desfasado.
  • Requiere chromium-personal LOGUEADO en Upwork en port (9222). Sin sesion la pagina de busqueda no muestra resultados reales (redirige a login / challenge). El servicio systemd chromium-personal debe estar vivo con remote debugging activo. Sin Chrome en el puerto: error claro, no lanza.
  • NO usar port=9333 (Chrome aislado del browser_mcp): no tiene tu login de Upwork, asi que no veria los resultados logueados.
  • Sin job tiles → status:"error" con projects vacio. La funcion NUNCA inventa datos. El mensaje distingue las dos causas probables: sesion no logueada o selectores desactualizados ("Validar con sesion real").
  • Anti-bot puede mostrar un challenge (Cloudflare/PerimeterX) en vez de los resultados aunque haya sesion. En ese caso no aparecen tiles y devuelve error: hay que resolver el challenge a mano en el navegador antes de reintentar.
  • Mezcla tu sesion personal. Con port=9222 abre tabs en TU navegador diario (los cierra best-effort con window.close() al terminar). Respeta los terminos de servicio de Upwork y el scope legal del scraping.
  • scraped_at y source los pone Python, no el JS, para garantizar el sello UTC consistente en todas las filas de la misma corrida.

Capability growth log

  • v0.1.0 (2026-06-17) — version inicial. Selectores best-effort PENDIENTES de validacion en vivo (no habia sesion Upwork logueada al crear la funcion). El extractor JS lee la constante SELECTORS; corregir alli tras validar con una busqueda real. Sin smoke test ejecutado contra Upwork.