Files
feadulta/mirror-antiguo/scripts/50b-parity-full.py
T
rafa 275aff1430 Mirror del Joomla antiguo: versionar los scripts y reponer los assets que faltaban
Los scripts del mirror (00-90) vivian solo en el disco. Van al repo; los datos que
generan no (16 GB entre crawl, snapshot del origen y Joomla restaurado) -> .gitignore.

Nuevo 91-repone-assets404.sh: repone los ficheros que el crawl no capturo porque se
referencian SOLO desde CSS y el crawler seguia enlaces HTML (system.css, los fondos de
fe_adulta_1, ratingstars.gif de K2). Salian como 404 en los logs de nginx del Hetzner.

Descarga por HTTP desde el Joomla local aislado, nunca del filesystem -- mismo principio
que el crawl, para no arrastrar los .php comprometidos del #183 -- y escanea PHP embebido
antes de copiar a site/.

Resultado sobre las 286 rutas unicas con 404 del log: 196 repuestas y verificadas en
produccion (196/196 en 200 tras el rsync), 82 que dan 301->404 tambien en el origen (ya
estaban rotas en la web original) y 8 rutas basura /%22/... de HTML mal formado.

Refs #180

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:04:31 -04:00

155 lines
5.9 KiB
Python

#!/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"<title[^>]*>(.*?)</title>", re.I | re.S)
SCRIPTS = re.compile(r"<(script|style)[^>]*>.*?</\1>", 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)