feat(lecturas): lote masivo de lecturas bíblicas vía índice del leccionario
build_lectionary_index.py construye un índice por referencia bíblica descargando un ciclo litúrgico de evangelizo.ws (combina idiomas por reading_code, robusto a fiestas trasladadas por país). lecturas_apply.py casa las lecturas ES por referencia y apply_lecturas_wp.php crea+asocia las traducciones en Polylang. 440/501 lecturas cubiertas y publicadas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* apply_lecturas_wp.php — Crea las traducciones de lecturas bíblicas casadas por
|
||||
* referencia (lecturas_apply.py) y las asocia en Polylang. Idempotente.
|
||||
*
|
||||
* Ejecutar dentro del contenedor:
|
||||
* docker cp /tmp/lecturas_creadas.json wordpress-web:/tmp/
|
||||
* docker exec wordpress-web wp eval-file /tmp/apply_lecturas_wp.php [publish]
|
||||
*/
|
||||
$status = (isset($argv[1]) && $argv[1] === 'publish') ? 'publish' : 'draft';
|
||||
$data = json_decode(file_get_contents('/tmp/lecturas_creadas.json'), true);
|
||||
$created = 0; $posts_done = 0; $skipped = 0;
|
||||
foreach ($data as $row) {
|
||||
$es_id = (int) $row['es_id'];
|
||||
$es = get_post($es_id);
|
||||
if (!$es) { $skipped++; continue; }
|
||||
$existing = function_exists('pll_get_post_translations') ? pll_get_post_translations($es_id) : ['es' => $es_id];
|
||||
$group = $existing;
|
||||
$es_cats = wp_get_post_categories($es_id);
|
||||
foreach (['en', 'fr', 'it', 'pt'] as $L) {
|
||||
if (!empty($existing[$L])) { $group[$L] = $existing[$L]; continue; }
|
||||
if (empty($row['langs'][$L])) continue;
|
||||
$id = wp_insert_post([
|
||||
'post_title' => $es->post_title, // referencia bíblica (igual en todos)
|
||||
'post_content' => $row['langs'][$L],
|
||||
'post_status' => $status,
|
||||
'post_type' => 'post',
|
||||
'comment_status' => 'closed',
|
||||
], true);
|
||||
if (is_wp_error($id)) continue;
|
||||
pll_set_post_language($id, $L);
|
||||
$cats = [];
|
||||
foreach ($es_cats as $c) { $t = pll_get_term($c, $L); if ($t) $cats[] = $t; }
|
||||
if ($cats) wp_set_post_categories($id, $cats);
|
||||
$group[$L] = $id;
|
||||
$created++;
|
||||
}
|
||||
if (function_exists('pll_save_post_translations')) pll_save_post_translations($group);
|
||||
$posts_done++;
|
||||
}
|
||||
echo "posts ES procesados: $posts_done | traducciones creadas: $created | status=$status | skip=$skipped\n";
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
build_lectionary_index.py — Descarga el leccionario de evangelizo.ws para un rango de
|
||||
fechas (un ciclo litúrgico completo cubre todas las lecturas) en es/en/fr/it/pt y
|
||||
construye un índice POR REFERENCIA bíblica, para casar lecturas sin depender de fechas.
|
||||
|
||||
Salida: /tmp/lectionary_index.json { "LIBRO|cap|vers": {es,en,fr,it,pt: html} }
|
||||
Cache por día/idioma en /tmp/evangelizo_cache (resumible).
|
||||
|
||||
Uso: python3 build_lectionary_index.py 2023-01-01 2025-12-31
|
||||
"""
|
||||
import sys, os, re, json, time, html, unicodedata, urllib.request
|
||||
from datetime import date, timedelta
|
||||
|
||||
LANGS = {"SP": "es", "AM": "en", "FR": "fr", "IT": "it", "PT": "pt"}
|
||||
CACHE = "/tmp/evangelizo_cache"
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
INDEX = "/tmp/lectionary_index.json"
|
||||
|
||||
|
||||
def norm_book(full_title):
|
||||
# "Libro de Jeremías" / "Carta de san Pablo a los Romanos" -> "JEREMIAS"/"ROMANOS"
|
||||
s = unicodedata.normalize("NFKD", full_title).encode("ascii", "ignore").decode().upper()
|
||||
s = re.sub(r"[^A-Z0-9 ]", " ", s)
|
||||
toks = [t for t in s.split() if t]
|
||||
return toks[-1] if toks else ""
|
||||
|
||||
|
||||
def clean(raw):
|
||||
raw = html.unescape(raw or "")
|
||||
raw = re.sub(r"\[\[[^\]]+\]\]", "", raw)
|
||||
paras = [p.strip() for p in raw.split("\n") if p.strip()]
|
||||
return "".join(f"<p>{p}</p>\n" for p in paras)
|
||||
|
||||
|
||||
def fetch(date_s, lang_code):
|
||||
fp = os.path.join(CACHE, f"{date_s}_{lang_code}.json")
|
||||
if os.path.exists(fp):
|
||||
try:
|
||||
return json.load(open(fp))
|
||||
except Exception:
|
||||
pass
|
||||
url = f"https://publication.evangelizo.ws/{lang_code}/days/{date_s}"
|
||||
for a in range(3):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "fea-lect/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
data = json.load(r)
|
||||
out = []
|
||||
for rd in data.get("data", {}).get("readings", []) or []:
|
||||
out.append({
|
||||
"code": rd.get("reading_code", ""),
|
||||
"ref": (rd.get("reference_displayed") or "").strip().rstrip("."),
|
||||
"book": (rd.get("book") or {}).get("full_title", ""),
|
||||
"text": clean(rd.get("text", "")),
|
||||
})
|
||||
json.dump(out, open(fp, "w"), ensure_ascii=False)
|
||||
time.sleep(0.15)
|
||||
return out
|
||||
except Exception:
|
||||
if a == 2:
|
||||
json.dump([], open(fp, "w"))
|
||||
return []
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def key_from(book_full, ref):
|
||||
m = re.match(r"(\d{1,3})\s*,\s*(\d{1,3})", ref)
|
||||
if not m:
|
||||
return None
|
||||
return f"{norm_book(book_full)}|{int(m.group(1))}|{int(m.group(2))}"
|
||||
|
||||
|
||||
def main():
|
||||
d0 = date.fromisoformat(sys.argv[1])
|
||||
d1 = date.fromisoformat(sys.argv[2])
|
||||
days = (d1 - d0).days + 1
|
||||
|
||||
# Las fiestas trasladadas caen en fechas distintas por país/idioma → NO se puede
|
||||
# casar dentro del mismo día. Indexamos por reading_code (estable entre idiomas)
|
||||
# acumulando el texto de cada idioma desde CUALQUIER día donde aparezca.
|
||||
code_text = {wl: {} for wl in LANGS.values()} # lang -> {code: text}
|
||||
code_book = {} # code -> norm_book (del SP)
|
||||
cur, n = d0, 0
|
||||
while cur <= d1:
|
||||
ds = cur.isoformat()
|
||||
for lc, wl in LANGS.items():
|
||||
for rd in fetch(ds, lc):
|
||||
code = rd["code"]
|
||||
if not code:
|
||||
continue
|
||||
if rd["text"] and code not in code_text[wl]:
|
||||
code_text[wl][code] = rd["text"]
|
||||
if wl == "es" and code not in code_book:
|
||||
nb = norm_book(rd["book"])
|
||||
m = re.search(r"(\d{1,3})\s*,\s*(\d{1,3})", code)
|
||||
if nb and m:
|
||||
code_book[code] = f"{nb}|{int(m.group(1))}|{int(m.group(2))}"
|
||||
n += 1
|
||||
if n % 60 == 0:
|
||||
print(f" {n}/{days} días codes_es={len(code_text['es'])}", flush=True)
|
||||
cur += timedelta(days=1)
|
||||
|
||||
# combinar: para cada code con clave-ES y texto en los 4 idiomas
|
||||
index = {}
|
||||
for code, key in code_book.items():
|
||||
if key in index:
|
||||
continue
|
||||
entry = {}
|
||||
ok = True
|
||||
for wl in ("es", "en", "fr", "it", "pt"):
|
||||
t = code_text[wl].get(code)
|
||||
if not t:
|
||||
ok = (wl == "es") and ok # es siempre presente; faltar otro descarta
|
||||
if wl != "es":
|
||||
ok = False
|
||||
break
|
||||
entry[wl] = t
|
||||
if ok and all(l in entry for l in ("en", "fr", "it", "pt")):
|
||||
index[key] = entry
|
||||
json.dump(index, open(INDEX, "w"), ensure_ascii=False)
|
||||
print(f"FIN. {n} días. codes_es={len(code_book)} → índice {len(index)} referencias en {INDEX}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lecturas_apply.py — Casa las lecturas ES sin traducir contra el índice del leccionario
|
||||
(build_lectionary_index.py) POR REFERENCIA bíblica y vuelca las traducciones a crear.
|
||||
|
||||
Entrada: /tmp/lectionary_index.json , /tmp/lecturas_todo.json
|
||||
Salida: /tmp/lecturas_creadas.json (para que un wp eval cree+asocie+publique)
|
||||
/tmp/lecturas_skip.json
|
||||
|
||||
Uso: python3 lecturas_apply.py [--limit N]
|
||||
"""
|
||||
import sys, re, json, unicodedata
|
||||
from collections import Counter
|
||||
|
||||
# Alias de nombre de libro: feadulta -> token usado por evangelizo (último token full_title ES)
|
||||
ALIAS = {
|
||||
"HECHOS": "APOSTOLES", "HCH": "APOSTOLES",
|
||||
"CANTAR": "CANTARES",
|
||||
"APOC": "APOCALIPSIS", "AP": "APOCALIPSIS",
|
||||
"QOHELET": "ECLESIASTES",
|
||||
# abreviaturas litúrgicas
|
||||
"MT": "MATEO", "MC": "MARCOS", "LC": "LUCAS", "JN": "JUAN",
|
||||
"RM": "ROMANOS", "GA": "GALATAS", "EF": "EFESIOS", "FLP": "FILIPENSES",
|
||||
"COL": "COLOSENSES", "HB": "HEBREOS", "ST": "SANTIAGO",
|
||||
"IS": "ISAIAS", "JR": "JEREMIAS", "EZ": "EZEQUIEL", "GN": "GENESIS",
|
||||
"EX": "EXODO", "DT": "DEUTERONOMIO", "SAL": "SALMOS", "PR": "PROVERBIOS",
|
||||
}
|
||||
|
||||
|
||||
def norm(s):
|
||||
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().upper()
|
||||
return re.sub(r"[^A-Z]", "", s) # solo letras → descarta el número del libro
|
||||
|
||||
|
||||
def title_keys(title):
|
||||
keys = []
|
||||
for part in re.split(r"\s*/\s*", title):
|
||||
m = re.search(r"([0-9]?\s*[A-Za-zÀ-ÿ][A-Za-zÀ-ÿ.\s]+?)\s+(\d{1,3})\s*,\s*(\d{1,3})", part)
|
||||
if not m:
|
||||
return None # parte no parseable → no casar el post entero
|
||||
book = norm(m.group(1))
|
||||
book = ALIAS.get(book, book)
|
||||
keys.append(f"{book}|{int(m.group(2))}|{int(m.group(3))}")
|
||||
return keys or None
|
||||
|
||||
|
||||
def main():
|
||||
limit = 0
|
||||
if "--limit" in sys.argv:
|
||||
limit = int(sys.argv[sys.argv.index("--limit") + 1])
|
||||
idx = json.load(open("/tmp/lectionary_index.json"))
|
||||
todo = json.load(open("/tmp/lecturas_todo.json"))
|
||||
if limit:
|
||||
todo = todo[:limit]
|
||||
|
||||
creadas, skip = [], []
|
||||
for t in todo:
|
||||
keys = title_keys(t["title"])
|
||||
if not keys:
|
||||
skip.append({**t, "why": "título no parseable"})
|
||||
continue
|
||||
if not all(k in idx for k in keys):
|
||||
missing = [k for k in keys if k not in idx]
|
||||
skip.append({**t, "why": "ref no en índice", "missing": missing})
|
||||
continue
|
||||
langs = {}
|
||||
for wl in ("en", "fr", "it", "pt"):
|
||||
langs[wl] = "".join(idx[k][wl] for k in keys)
|
||||
creadas.append({"es_id": t["id"], "title": t["title"], "langs": langs})
|
||||
|
||||
json.dump(creadas, open("/tmp/lecturas_creadas.json", "w"), ensure_ascii=False)
|
||||
json.dump(skip, open("/tmp/lecturas_skip.json", "w"), ensure_ascii=False)
|
||||
print(f"CASADAS: {len(creadas)} / {len(todo)} SKIP: {len(skip)}")
|
||||
print("motivos skip:", dict(Counter(s["why"] for s in skip)))
|
||||
# muestra de refs que faltan (para ampliar alias/rango)
|
||||
missing = Counter()
|
||||
for s in skip:
|
||||
for k in s.get("missing", []):
|
||||
missing[k.split("|")[0]] += 1
|
||||
print("libros con más misses:", dict(missing.most_common(12)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user