763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.8 KiB
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. |
|
|
false | error_py_core |
|
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
SELECTORSdel.pypara 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 salenullde 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 systemdchromium-personaldebe 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"conprojectsvacio. 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=9222abre tabs en TU navegador diario (los cierra best-effort conwindow.close()al terminar). Respeta los terminos de servicio de Upwork y el scope legal del scraping. scraped_atysourcelos 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.