#!/usr/bin/env python3 """Paridad COMPLETA sobre el inventario entero, a fuego lento. Igual que 50-parity.py pero (a) recorre las 25.437 URLs en vez de una muestra, (b) mete una pausa entre peticiones para no ahogar al Joomla local ni a la WSL (ver leccion del crawl que tumbo la VM), y (c) escribe JSONL incremental para poder mirar el progreso y reanudar sin repetir trabajo. Uso: python3 50b-parity-full.py [pausa_segundos] [workers] (por defecto 0.35 y 1) Se registra ademas el codigo HTTP del Joomla local: sin eso, un 500 del contenedor se contaria como "el mirror difiere" y ensuciaria el informe con diferencias que no lo son. """ import os, re, sys, time, json, hashlib, subprocess, threading, collections import concurrent.futures from urllib.parse import urlsplit BASE = "/home/rafa/joomla-migration/mirror-antiguo" RUN = open(os.path.join(BASE, "CURRENT_RUN")).read().strip() DIR = os.path.join(BASE, "runs", RUN) RAW = os.path.join(DIR, "raw", "antiguo.feadulta.com") PAUSA = float(sys.argv[1]) if len(sys.argv) > 1 else 0.35 WORKERS = int(sys.argv[2]) if len(sys.argv) > 2 else 1 JSONL = os.path.join(DIR, "parity-full.jsonl") PROG = os.path.join(DIR, "parity-full.progress") OUT = os.path.join(DIR, "parity-report-full.json") TITLE = re.compile(r"]*>(.*?)", re.I | re.S) SCRIPTS = re.compile(r"<(script|style)[^>]*>.*?", re.I | re.S) TAGS = re.compile(r"<[^>]+>") WS = re.compile(r"\s+") VOLATILE = re.compile(r"[0-9a-f]{32}|csrf|token", re.I) HITS = re.compile(r"(Read|Visto|Le[iĆ­]do)\s+\d+\s+(times|veces)", re.I) def texthash(s): s = SCRIPTS.sub(" ", s) s = TAGS.sub(" ", s) s = WS.sub(" ", s).strip() s = VOLATILE.sub("", s) s = HITS.sub("HITS", s) return hashlib.sha256(s.encode("utf-8", "replace")).hexdigest(), len(s) def title(s): m = TITLE.search(s) return WS.sub(" ", m.group(1)).strip() if m else "" urls = [l.strip() for l in open(os.path.join(BASE, "inventory", "urls-input.txt")) if l.strip()] # Reanudable: lo ya comprobado no se repite. hechas = set() if os.path.exists(JSONL): for line in open(JSONL, encoding="utf-8"): try: hechas.add(json.loads(line)["url"]) except Exception: pass pendientes = [u for u in urls if u not in hechas] print(f"inventario={len(urls)} ya_hechas={len(hechas)} pendientes={len(pendientes)} " f"pausa={PAUSA}s workers={WORKERS}", flush=True) t0 = time.time() lock = threading.Lock() contador = {"n": 0} codigos = collections.Counter() def comprueba(u): path = urlsplit(u).path fp = os.path.join(RAW, path.lstrip("/")) if path.endswith("/"): fp = os.path.join(fp, "index.html") if not os.path.exists(fp): return {"url": u, "estado": "falta_fichero"} disk = open(fp, encoding="utf-8", errors="replace").read() salida = subprocess.run( ["curl", "-s", "-w", "\n%{http_code}", "--max-time", "60", "-H", "Host: antiguo.feadulta.com", "http://127.0.0.1:8086" + path], capture_output=True).stdout.decode("utf-8", "replace") live, _, code = salida.rpartition("\n") code = code.strip() or "000" td, tl = title(disk), title(live) hd, ld = texthash(disk) hl, ll = texthash(live) if PAUSA: time.sleep(PAUSA) return {"url": u, "estado": "ok", "http_vivo": code, "titulo_igual": td == tl, "texto_igual": hd == hl, "titulo_mirror": td[:120], "titulo_vivo": tl[:120], "len_mirror": ld, "len_vivo": ll} with open(JSONL, "a", encoding="utf-8") as fh: with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as ex: for d in ex.map(comprueba, pendientes): with lock: fh.write(json.dumps(d, ensure_ascii=False) + "\n") contador["n"] += 1 i = contador["n"] codigos[d.get("http_vivo", "-")] += 1 if i % 100 == 0: fh.flush() hechas_tot = len(hechas) + i ritmo = i / max(time.time() - t0, 1) queda = (len(pendientes) - i) / max(ritmo, 0.001) / 60 open(PROG, "w").write( f"{hechas_tot}/{len(urls)} ({100*hechas_tot/len(urls):.1f}%) " f"ritmo={ritmo:.2f}/s ETA={queda:.0f}min " f"http_vivo={dict(codigos)}\n") # Resumen final a partir del JSONL completo. res = {"inventario": len(urls), "comprobadas": 0, "falta_fichero": 0, "vivo_no_200": 0, "http_vivo": {}, "titulo_igual": 0, "titulo_distinto": 0, "texto_igual": 0, "texto_distinto": 0, "diffs": []} for line in open(JSONL, encoding="utf-8"): d = json.loads(line) if d["estado"] == "falta_fichero": res["falta_fichero"] += 1 res["diffs"].append({"url": d["url"], "motivo": "fichero ausente"}) continue code = d.get("http_vivo", "?") res["http_vivo"][code] = res["http_vivo"].get(code, 0) + 1 if code not in ("200", "?"): # El Joomla local fallo en esta peticion: no es una diferencia del mirror. res["vivo_no_200"] += 1 res["diffs"].append({"url": d["url"], "motivo": "joomla local " + code}) continue res["comprobadas"] += 1 if d["titulo_igual"]: res["titulo_igual"] += 1 else: res["titulo_distinto"] += 1 res["diffs"].append({"url": d["url"], "motivo": "titulo", "mirror": d["titulo_mirror"], "vivo": d["titulo_vivo"]}) if d["texto_igual"]: res["texto_igual"] += 1 else: res["texto_distinto"] += 1 res["diffs"].append({"url": d["url"], "motivo": "texto", "len_mirror": d["len_mirror"], "len_vivo": d["len_vivo"]}) json.dump(res, open(OUT, "w"), indent=2, ensure_ascii=False) for k in ("inventario", "comprobadas", "falta_fichero", "vivo_no_200", "http_vivo", "titulo_igual", "titulo_distinto", "texto_igual", "texto_distinto"): print(k, "=", res[k]) print("informe:", OUT)