chore(verify): recuperar enum_urls.php y commitear tooling de verificación #121
El enumerador de URLs no se había commiteado y se perdió. Recuperado reproduciendo la composición de la corrida previa (menús×5 idiomas + categorías con contenido + contenido no-ES). Se añade también classify_gaps.py (clasifica huecos: traducir vs descargar) y run_all.sh. Outputs y .venv quedan en .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# Capa 0 (enlaces): comprueba status de los enlaces internos salientes hallados en el crawl.
|
||||
# Marca rotos (>=400/error) y qué páginas los referencian. stdlib, coste 0.
|
||||
import json, os, ssl, urllib.request, urllib.parse
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
DIR=os.path.dirname(os.path.abspath(__file__))
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
SITE='farmer.taild3aaf6.ts.net'
|
||||
CAP=int(os.environ.get('CAP','9000'))
|
||||
|
||||
def norm(u):
|
||||
return u.split('#')[0].rstrip('/')
|
||||
|
||||
crawled={} # url normalizada -> status (de report)
|
||||
referers={} # link -> set(paginas)
|
||||
links=set()
|
||||
with open(os.path.join(DIR,'report.jsonl'),encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line=line.strip()
|
||||
if not line: continue
|
||||
r=json.loads(line)
|
||||
if r.get('final'): crawled[norm(r['final'])]=r.get('status')
|
||||
crawled.setdefault(norm(r['url']), r.get('status'))
|
||||
for l in (r.get('links') or []):
|
||||
if SITE in l:
|
||||
n=norm(l); links.add(n); referers.setdefault(n,set()).add(r['url'])
|
||||
|
||||
# solo comprobar las que NO conocemos ya por el crawl
|
||||
todo=[l for l in links if l not in crawled]
|
||||
capped = len(todo)>CAP
|
||||
todo=todo[:CAP]
|
||||
print(f"enlaces internos únicos: {len(links)} | ya conocidos: {len(links)-len(todo)-(len(todo) if False else 0)} | a comprobar: {len(todo)}"+(" (CAP)" if capped else ""))
|
||||
|
||||
def check(u):
|
||||
try:
|
||||
req=urllib.request.Request(u, method='HEAD', headers={'User-Agent':'fea-link-audit'})
|
||||
with urllib.request.urlopen(req,timeout=15,context=ctx) as r: return (u,r.status)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (405,501): # HEAD no permitido → GET
|
||||
try:
|
||||
req=urllib.request.Request(u, headers={'User-Agent':'fea-link-audit'})
|
||||
with urllib.request.urlopen(req,timeout=20,context=ctx) as r: return (u,r.status)
|
||||
except Exception as e2: return (u, getattr(e2,'code',-1))
|
||||
return (u,e.code)
|
||||
except Exception:
|
||||
# reintento único (evita falsos -1 por saturación)
|
||||
try:
|
||||
req=urllib.request.Request(u, headers={'User-Agent':'fea-link-audit'})
|
||||
with urllib.request.urlopen(req,timeout=25,context=ctx) as r: return (u,r.status)
|
||||
except Exception: return (u,-1)
|
||||
|
||||
res={}
|
||||
with ThreadPoolExecutor(max_workers=8) as ex:
|
||||
futs=[ex.submit(check,u) for u in todo]
|
||||
for i,fu in enumerate(as_completed(futs)):
|
||||
u,s=fu.result(); res[u]=s
|
||||
if (i+1)%500==0: print(f" {i+1}/{len(todo)}")
|
||||
|
||||
# combinar
|
||||
allstatus=dict(crawled); allstatus.update(res)
|
||||
broken=[]
|
||||
for l in links:
|
||||
s=allstatus.get(l)
|
||||
if s is not None and (s==-1 or s>=400):
|
||||
broken.append({'link':l,'status':s,'referenced_by':sorted(referers.get(l,[]))[:8],'n_referers':len(referers.get(l,[]))})
|
||||
broken.sort(key=lambda b:(-b['n_referers'], b['link']))
|
||||
json.dump({'capped':capped,'checked':len(todo),'unique_links':len(links),'broken':broken},
|
||||
open(os.path.join(DIR,'broken_links.json'),'w',encoding='utf-8'),ensure_ascii=False,indent=1)
|
||||
print(f"ENLACES ROTOS: {len(broken)} (de {len(links)} únicos)")
|
||||
for b in broken[:15]: print(f" [{b['status']}] {b['link']} <- {b['n_referers']} págs")
|
||||
Reference in New Issue
Block a user