wip: Trabajando CL (Nuxt SPA, necesita Playwright - Ola2)

This commit is contained in:
2026-07-29 06:49:29 +02:00
parent b1c8a22c84
commit 0258b6b9ac
+381
View File
@@ -0,0 +1,381 @@
import os
import re
import time
import logging
import requests
from bs4 import BeautifulSoup
import psycopg2
from psycopg2.extras import execute_values
from datetime import datetime
# -- CONFIG -------------------------------------------------------------------
PROXY = 'socks5h://127.0.0.1:1090'
PROXIES = {'http': PROXY, 'https': PROXY}
DB_HOST = '100.75.240.87'
DB_PORT = 5432
DB_NAME = 'trabajando_cl'
DB_USER = 'pgadmin'
DB_PASS = 'J5BVlq65JvedWxZVrcY96OQX'
BASE_URL = 'https://www.trabajando.cl'
LIST_URL = 'https://www.trabajando.cl/trabajo?page={page}'
UA = (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/124.0.0.0 Safari/537.36'
)
DELAY = 0.8
MAX_PAGES = 20
NEW_STREAK = 3
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
log = logging.getLogger(__name__)
def get_session():
sess = requests.Session()
sess.proxies.update(PROXIES)
sess.headers.update({
'User-Agent': UA,
'Accept-Language': 'es-CL,es;q=0.9',
'Accept': (
'text/html,application/xhtml+xml,'
'application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
),
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Cache-Control': 'max-age=0',
})
return sess
def create_table(conn):
ddl_create = """
CREATE TABLE IF NOT EXISTS jobs (
job_id TEXT PRIMARY KEY,
titulo TEXT,
empresa TEXT,
ubicacion TEXT,
url TEXT UNIQUE,
fecha_pub TEXT,
scraped_at TIMESTAMP DEFAULT NOW()
);
"""
columns = [
('titulo', 'TEXT'),
('empresa', 'TEXT'),
('ubicacion', 'TEXT'),
('url', 'TEXT'),
('fecha_pub', 'TEXT'),
('scraped_at', 'TIMESTAMP DEFAULT NOW()'),
]
with conn.cursor() as cur:
cur.execute(ddl_create)
for col, typedef in columns:
try:
cur.execute(
f"ALTER TABLE jobs ADD COLUMN IF NOT EXISTS {col} {typedef};"
)
except Exception as exc:
log.debug("ALTER TABLE ignorado para columna %s: %s", col, exc)
conn.commit()
log.info("Tabla jobs verificada/creada correctamente.")
def upsert(conn, rows):
if not rows:
return 0, 0
job_ids = [r['job_id'] for r in rows]
with conn.cursor() as cur:
cur.execute(
"SELECT job_id FROM jobs WHERE job_id = ANY(%s);",
(job_ids,)
)
existing = {row[0] for row in cur.fetchall()}
tuples = [
(
r['job_id'],
r.get('titulo'),
r.get('empresa'),
r.get('ubicacion'),
r.get('url'),
r.get('fecha_pub'),
)
for r in rows
]
sql = """
INSERT INTO jobs (job_id, titulo, empresa, ubicacion, url, fecha_pub)
VALUES %s
ON CONFLICT (job_id) DO UPDATE SET
titulo = EXCLUDED.titulo,
empresa = EXCLUDED.empresa,
ubicacion = EXCLUDED.ubicacion,
url = EXCLUDED.url,
fecha_pub = EXCLUDED.fecha_pub,
scraped_at = NOW();
"""
execute_values(cur, sql, tuples)
conn.commit()
inserted = sum(1 for r in rows if r['job_id'] not in existing)
updated = len(rows) - inserted
return inserted, updated
def scrape_page(sess, url):
backoffs = [2, 5, 15]
resp = None
for attempt, wait in enumerate(backoffs, 1):
try:
resp = sess.get(url, timeout=30)
if resp.status_code in (403, 429):
log.warning(
"HTTP %s en %s (intento %s) - sleep 30s y reintento extra",
resp.status_code, url, attempt
)
time.sleep(30)
resp = sess.get(url, timeout=30)
if resp.status_code in (403, 429):
log.warning(
"HTTP %s persiste tras reintento extra - devolviendo []",
resp.status_code
)
return []
resp.raise_for_status()
break
except requests.RequestException as exc:
log.warning(
"Error intento %s/%s en %s: %s",
attempt, len(backoffs), url, exc
)
if attempt < len(backoffs):
time.sleep(wait)
else:
log.error("Todos los intentos fallaron para %s", url)
return []
if resp is None or not resp.text.strip():
log.warning("Respuesta vacia para %s", url)
return []
soup = BeautifulSoup(resp.text, 'lxml')
results = []
cards = (
soup.select('article.opportunity')
or soup.select('div.opportunity')
or soup.select('[class*="opportunity"]')
or soup.select('li[class*="job"]')
or soup.select('div[class*="job-card"]')
or soup.select('div[class*="job-item"]')
or soup.select('article[class*="job"]')
or soup.select('div[class*="oferta"]')
or soup.select('div[class*="aviso"]')
or []
)
if not cards:
log.debug("No se encontraron cards con selectores conocidos; usando fallback de links")
seen_ids = set()
for a_tag in soup.select('a[href*="/trabajo/"]'):
href = a_tag.get('href', '')
m = re.search(r'/trabajo/(\d+)', href)
if not m:
continue
job_id = m.group(1)
if job_id in seen_ids:
continue
seen_ids.add(job_id)
full_url = href if href.startswith('http') else BASE_URL + href
titulo = a_tag.get_text(strip=True) or None
results.append({
'job_id': job_id,
'titulo': titulo,
'empresa': None,
'ubicacion': None,
'url': full_url,
'fecha_pub': None,
})
return results
seen_ids = set()
for card in cards:
try:
a_tag = (
card.select_one('a[href*="/trabajo/"]')
or card.select_one('a[href]')
)
if not a_tag:
continue
href = a_tag.get('href', '')
m = re.search(r'/trabajo/(\d+)', href)
if not m:
continue
job_id = m.group(1)
if job_id in seen_ids:
continue
seen_ids.add(job_id)
full_url = href if href.startswith('http') else BASE_URL + href
titulo_tag = (
card.select_one('h1')
or card.select_one('h2')
or card.select_one('h3')
or card.select_one('[class*="title"]')
or card.select_one('[class*="titulo"]')
or card.select_one('[class*="cargo"]')
or card.select_one('[class*="puesto"]')
)
titulo = titulo_tag.get_text(strip=True) if titulo_tag else (
a_tag.get_text(strip=True) or None
)
empresa_tag = (
card.select_one('[class*="company"]')
or card.select_one('[class*="empresa"]')
or card.select_one('[class*="employer"]')
or card.select_one('[class*="razon"]')
or card.select_one('span.company')
or card.select_one('div.company')
)
empresa = empresa_tag.get_text(strip=True) if empresa_tag else None
ubicacion_tag = (
card.select_one('[class*="location"]')
or card.select_one('[class*="ubicacion"]')
or card.select_one('[class*="ciudad"]')
or card.select_one('[class*="region"]')
or card.select_one('[class*="lugar"]')
or card.select_one('span.location')
or card.select_one('div.location')
)
ubicacion = ubicacion_tag.get_text(strip=True) if ubicacion_tag else None
fecha_pub = None
fecha_tag = (
card.select_one('time')
or card.select_one('[class*="date"]')
or card.select_one('[class*="fecha"]')
or card.select_one('[class*="publicad"]')
or card.select_one('[datetime]')
)
if fecha_tag:
fecha_pub = (
fecha_tag.get('datetime')
or fecha_tag.get_text(strip=True)
or None
)
results.append({
'job_id': job_id,
'titulo': titulo,
'empresa': empresa,
'ubicacion': ubicacion,
'url': full_url,
'fecha_pub': fecha_pub,
})
except Exception as exc:
log.debug("Error parseando card individual: %s", exc)
continue
return results
def main():
log.info("=== Iniciando scraper Trabajando.cl ===")
sess = get_session()
try:
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS,
)
except Exception as exc:
raise RuntimeError(
f"No se pudo conectar a la base de datos '{DB_NAME}': {exc}"
) from exc
log.info("Conexion a DB '%s' establecida.", DB_NAME)
create_table(conn)
total_new = 0
total_upd = 0
no_new_streak = 0
for page in range(1, MAX_PAGES + 1):
url = LIST_URL.format(page=page)
log.info("Procesando pagina %s/%s: %s", page, MAX_PAGES, url)
try:
rows = scrape_page(sess, url)
except Exception as exc:
log.error("Error inesperado en scrape_page pagina %s: %s", page, exc)
rows = []
if not rows:
log.info("Pagina %s: sin resultados - racha sin nuevos: %s/%s",
page, no_new_streak + 1, NEW_STREAK)
no_new_streak += 1
else:
try:
new_p, upd_p = upsert(conn, rows)
except Exception as exc:
log.error("Error en upsert pagina %s: %s", page, exc)
raise RuntimeError(
f"Error critico al insertar datos en DB (pagina {page}): {exc}"
) from exc
log.info(
"Pagina %s: %s encontrados -> %s nuevos, %s actualizados",
page, len(rows), new_p, upd_p
)
total_new += new_p
total_upd += upd_p
if new_p == 0:
no_new_streak += 1
else:
no_new_streak = 0
if no_new_streak >= NEW_STREAK:
log.info(
"Racha de %s paginas consecutivas sin nuevos registros - deteniendo.",
NEW_STREAK
)
break
if page < MAX_PAGES:
time.sleep(DELAY)
try:
conn.close()
except Exception:
pass
print(f"Finalizado: {total_new} nuevos, {total_upd} actualizados")
if __name__ == '__main__':
main()