feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: scrape_upwork_projects
|
||||
kind: function
|
||||
lang: py
|
||||
domain: browser
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "def scrape_upwork_projects(query: str = '', sort: str = 'recency', pages: int = 1, port: int = 9222, timeout_s: float = 25.0) -> dict"
|
||||
description: "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."
|
||||
tags: [market-intel, recon, flow-replay, browser, cdp, upwork, scraper, jobs, freelance, captacion-clientes]
|
||||
uses_functions: ["cdp_open_url_and_wait_py_pipelines", "cdp_eval_py_browser"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: query
|
||||
desc: "Busqueda libre, ej. 'custom software'. Se url-encodea. '' (vacio) = listado de jobs por defecto."
|
||||
- name: sort
|
||||
desc: "Orden de resultados: 'recency' (mas recientes, default) o 'relevance'. Cualquier otro valor cae a 'recency'."
|
||||
- name: pages
|
||||
desc: "Numero de paginas de resultados a recorrer (>=1). Default 1. Cada pagina = navegacion + extraccion."
|
||||
- name: port
|
||||
desc: "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: timeout_s
|
||||
desc: "Timeout por pagina en segundos para navegacion + aparicion de las job tiles (polling cada 1s). Default 25.0."
|
||||
output: "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."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/browser/scrape_upwork_projects.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Requiere chromium-personal LOGUEADO en Upwork escuchando en port 9222.
|
||||
fn run scrape_upwork_projects --query "custom software" --sort recency
|
||||
```
|
||||
|
||||
```python
|
||||
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.
|
||||
Reference in New Issue
Block a user