SPB Git
10.8 KB · 161 lines python
1#!/usr/bin/env python32r"""Traçabilité du glossaire (annexe A).341. Extrait de tous les chapitres rédigés (`manuscript/chapters/chNN.tex`, sans `\stubchapter`) les termes5   marqués `\motcle{…}`, `\terme{fr}{en}`, les titres d'environnements `definition` et les sigles définis6   entre parenthèses ; les normalise (macros retirées, initiale en minuscule sauf sigle ou nom propre) et7   écrit `verification/glossaire-termes.csv` (terme, type, chapitres d'occurrence, nombre d'occurrences8   dans la prose de tous les chapitres).92. Lit `manuscript/appendices/glossaire.tex` et écrit le bilan `verification/chapters/appA.md`10   (nombre d'entrées, termes sans équivalent anglais établi, chapitres couverts par les renvois,11   termes extraits absents du glossaire).1213Usage : python3 tools/glossaire_extraction.py   (depuis la racine du dépôt ; aucune dépendance externe)14"""15from __future__ import annotations16import csv, datetime, json, pathlib, re, unicodedata17from collections import defaultdict1819ROOT = pathlib.Path(__file__).resolve().parents[1]20CHAP = ROOT / "manuscript/chapters"21GLOS = ROOT / "manuscript/appendices/glossaire.tex"22OUT_CSV = ROOT / "verification/glossaire-termes.csv"23OUT_MD = ROOT / "verification/chapters/appA.md"24TITRES = json.load(open(CHAP / "TITRES.json", encoding="utf-8"))2526RE_MOTCLE = re.compile(r"\\motcle\{((?:[^{}]|\{[^{}]*\})*)\}")27RE_TERME = re.compile(r"\\terme\{((?:[^{}]|\{[^{}]*\})*)\}\{((?:[^{}]|\{[^{}]*\})*)\}")28RE_DEF = re.compile(r"\\begin\{definition\}\{((?:[^{}]|\{[^{}]*\})*)\}")29# sigle défini : « revenu net d'exploitation (RNE, \emph{net operating income}) » ou « (RNE) » ; 2 à 6 capitales30RE_SIGLE = re.compile(r"\(([A-ZÉ][A-ZÉ&/]{1,6})(?:[,;)]|\s*\\emph)")31SIGLES_EXCLUS = {"II", "III", "IV", "VI", "VII", "VIII", "IX", "XI", "XII", "XX", "HC", "OLS", "ISO", "PDF", "CSV", "URL",32                 "USD", "CAD", "API", "SQL", "ID", "QC", "ON", "BC", "AB", "MB", "SK", "NB", "NS", "PE", "NL", "YT", "NT", "NU",33                 "US", "UK", "EU", "SE", "NE", "SO", "NO", "ES", "EN", "FR", "MA", "AR", "ARMA", "ADF", "KPSS", "AIC", "BIC",34                 "MSE", "RMSE", "MAE", "MAPE", "COD", "PRD", "PRB", "IQR", "TM", "RLRQ", "LRC", "LQ"}35PROPRES = ("Québec", "Montréal", "Gatineau", "Ottawa", "Canada", "Laval", "Monte", "Bayes", "Gauss", "Markov", "Moran", "Geary",36           "Getis", "Pareto", "Nash", "Ricardo", "Alonso", "Muth", "Mills", "Rosen", "Lancaster", "Hotelling", "Duan", "Halvorsen",37           "Kennedy", "Case", "Shiller", "Bailey", "Tobit", "Heckman", "Hausman", "Durbin", "Watson", "Shapley", "LASSO", "SHAP",38           "Ellwood", "Akerson", "Inwood", "Hoskold", "Gordon", "Fisher", "Laspeyres", "Paasche", "Törnqvist", "Marshall", "Swift",39           "RSMeans", "Excel", "Python", "R", "Code", "Loi", "Règlement", "Tribunal", "Ordre", "Régie", "Société", "Statistique",40           "Banque", "Teranet", "Centris", "JLR", "OEAQ", "SCHL", "TAL", "IVS", "CUSPAP", "USPAP", "IAAO", "RICS", "AIC", "BSIF",41           "SIG", "ArcGIS", "QGIS", "PostGIS", "OpenStreetMap", "Geocoder", "Lidar", "LiDAR")4243def strip_tex(s: str) -> str:44    s = re.sub(r"\\(emph|textit|textbf|texttt|text|mbox)\{([^{}]*)\}", r"\2", s)45    s = re.sub(r"\\(?:cref|Cref|ref|label|index|parencite|textcite|cite)\{[^{}]*\}", "", s)46    s = re.sub(r"\\[a-zA-Z]+\*?(\[[^\]]*\])?", "", s)47    s = s.replace("~", " ").replace("{", " ").replace("}", " ").replace("$", "")48    s = re.sub(r"\s+", " ", s).strip(" .;:")49    return s5051def normalise(s: str) -> str:52    s = strip_tex(s)53    if not s: return s54    first = s.split()[0]55    if first.isupper() and len(first) <= 6:            # sigle56        return s57    w = re.split(r"[^\wÀ-ÿ]", first)[0]58    if w in PROPRES or w.rstrip("s") in PROPRES:   # nom propre59        return s60    return s[0].lower() + s[1:]6162def sort_key(s: str) -> str:63    s = unicodedata.normalize("NFD", s)64    s = "".join(c for c in s if unicodedata.category(c) != "Mn")65    return s.lower().replace("’", "'").replace("œ", "oe")6667def chapters() -> dict[str, str]:68    res = {}69    for f in sorted(CHAP.glob("ch[0-9][0-9].tex")):70        txt = f.read_text(encoding="utf-8")71        if "\\stubchapter" in txt: continue72        res[f.stem[2:]] = txt73    return res7475def main() -> None:76    chs = chapters()77    plain = {n: strip_tex(re.sub(r"%.*", "", t)) for n, t in chs.items()}78    occ = defaultdict(lambda: {"type": set(), "chapitres": set()})79    for n, txt in chs.items():80        txt = re.sub(r"(?m)%.*$", "", txt)81        for m in RE_MOTCLE.finditer(txt):82            t = normalise(m.group(1))83            if t: occ[t]["type"].add("motcle"); occ[t]["chapitres"].add(n)84        for m in RE_TERME.finditer(txt):85            t = normalise(m.group(1))86            if t: occ[t]["type"].add("terme"); occ[t]["chapitres"].add(n); occ[t].setdefault("en", strip_tex(m.group(2)))87        for m in RE_DEF.finditer(txt):88            t = normalise(m.group(1))89            if t: occ[t]["type"].add("definition"); occ[t]["chapitres"].add(n)90        for m in RE_SIGLE.finditer(txt):91            s = m.group(1)92            if s in SIGLES_EXCLUS or len(s) < 2: continue93            occ[s]["type"].add("sigle"); occ[s]["chapitres"].add(n)94    rows = []95    for t, info in occ.items():96        pat = re.compile(r"(?<![\wÀ-ÿ])" + re.escape(t) + r"(?![\wÀ-ÿ])", re.I)97        counts = {n: len(pat.findall(p)) for n, p in plain.items()}98        total = sum(counts.values())99        where = sorted(set(info["chapitres"]) | {n for n, c in counts.items() if c})100        rows.append({"terme": t, "type": "+".join(sorted(info["type"])), "equivalent_en": info.get("en", ""),101                     "chapitres_definition": " ".join(sorted(info["chapitres"])), "chapitres_occurrence": " ".join(where),102                     "occurrences": total})103    rows.sort(key=lambda r: sort_key(r["terme"]))104    OUT_CSV.parent.mkdir(parents=True, exist_ok=True)105    with open(OUT_CSV, "w", newline="", encoding="utf-8") as f:106        w = csv.DictWriter(f, fieldnames=list(rows[0].keys())); w.writeheader(); w.writerows(rows)107    print(f"{len(rows)} termes extraits de {len(chs)} chapitres -> {OUT_CSV.relative_to(ROOT)}")108109    # ---- bilan du glossaire rédigé ------------------------------------------------------------110    if not GLOS.exists() or "\\stubchapter" in GLOS.read_text(encoding="utf-8"):111        print("glossaire.tex : stub, pas de bilan"); return112    g = GLOS.read_text(encoding="utf-8")113    ARG = r"\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}"   # argument avec deux niveaux d'accolades imbriquées114    entries = re.findall(r"\\gloentree" + ARG * 4, g)115    sans_en = [strip_tex(e[0]) for e in entries if strip_tex(e[1]) in ("", "—", "---", "--")]116    refs = defaultdict(int)117    for e in entries:118        for m in re.finditer(r"ch:(\d\d)", e[3]): refs[m.group(1)] += 1119    non_couverts = sorted(set(f"{int(k):02d}" for k in TITRES) - set(refs))120    labels = [sort_key(strip_tex(e[0])) for e in entries]121    desordre = [entries[i][0] for i in range(1, len(labels)) if labels[i] < labels[i - 1]]122    gl_plain = sort_key(strip_tex(g))123    STOP = {"dans", "pour", "avec", "sans", "sous", "entre", "comme", "cette", "leur", "leurs", "selon", "autre", "autres", "plus", "très", "tres", "moins", "d'un", "d'une", "l'", "art"}124    def couvert(terme: str) -> bool:125        k = sort_key(terme)126        if k in gl_plain: return True127        mots = [w for w in re.findall(r"[a-z0-9]+", k) if len(w) >= 4 and w not in STOP]128        return bool(mots) and all((w in gl_plain) or (w.rstrip("sx") in gl_plain) for w in mots)129    absents = [r["terme"] for r in rows if r["type"] != "sigle" and not couvert(r["terme"])]130    absents_sigles = [r["terme"] for r in rows if r["type"] == "sigle" and not re.search(r"(?<![A-Z])" + re.escape(r["terme"]) + r"(?![A-Z])", strip_tex(g))]131    mots = len(re.findall(r"[\wÀ-ÿ'’-]+", strip_tex(g)))132    today = datetime.date.today().isoformat()133    lines = [f"# Annexe A — Glossaire français–anglais — bilan de rédaction ({today})", "",134             "## Livrable",135             f"- `manuscript/appendices/glossaire.tex` : **{len(entries)} entrées** (`\\gloentree`) en `longtable` à trois colonnes "136             f"(terme français, équivalent anglais, définition et renvoi), triées par ordre alphabétique français sans égard aux accents ; "137             f"≈ {mots} mots. Note liminaire sur l'usage des sigles français/anglais (RNE/NOI, RCD/DSCR, RPV/LTV, TRI/IRR, VAN/NPV, "138             "RBE/EGI, RBP/PGI, PHMU/HBU, MRB/GIM, TGA/overall cap rate). Aucune citation bibliographique : les définitions renvoient aux chapitres.",139             "- `tools/glossaire_build.py` : liste source des entrées (rédigées à la main, dont une réserve d'entrées écartées), tri sans accents, en-têtes de lettre et écriture de la `longtable` ; à relancer après tout ajout.",140             f"- `tools/glossaire_extraction.py` → `verification/glossaire-termes.csv` : {len(rows)} termes extraits "141             f"({sum(1 for r in rows if 'motcle' in r['type'])} `\\motcle`, {sum(1 for r in rows if 'terme' in r['type'])} `\\terme`, "142             f"{sum(1 for r in rows if 'definition' in r['type'])} titres de `definition`, {sum(1 for r in rows if r['type']=='sigle')} sigles) "143             f"dans {len(chs)} chapitres rédigés (stubs exclus : {', '.join(sorted(set(f'{int(k):02d}' for k in TITRES) - set(chs))) or 'aucun'}).", "",144             "## Équivalents anglais non établis (marqués « — »)",145             *( [f"- {t}" for t in sans_en] or ["- aucun"] ), "",146             "## Chapitres couverts par les renvois",147             f"- {len(refs)} chapitres sur 48 reçoivent au moins un renvoi `\\cref{{ch:NN}}` ; renvois par chapitre : " +148             ", ".join(f"ch. {k} ({v})" for k, v in sorted(refs.items())) + ".",149             f"- Chapitres sans renvoi : {', '.join('ch. ' + c for c in non_couverts) or 'aucun'}.", "",150             "## Contrôles automatiques",151             f"- Ordre alphabétique (clé sans accents) : {'respecté' if not desordre else 'ANOMALIES : ' + '; '.join(desordre)}.",152             f"- Termes extraits (`\\motcle`/`\\terme`/`definition`) dont le libellé exact n'apparaît pas dans le glossaire : {len(absents)} "153             "(un terme est réputé couvert si son libellé ou tous ses mots significatifs figurent dans le glossaire ; les termes restants sont des notions ponctuelles ou très techniques laissées aux chapitres ; détail dans `verification/glossaire-termes.csv`) :",154             "  " + "; ".join(absents) if absents else "  aucun",155             f"- Sigles extraits non retenus au glossaire (sigles techniques, de formats de fichiers ou d'organismes cités ponctuellement) : {', '.join(absents_sigles) or 'aucun'}.", ""]156    OUT_MD.write_text("\n".join(lines), encoding="utf-8")157    print(f"bilan -> {OUT_MD.relative_to(ROOT)} ({len(entries)} entrées, {len(sans_en)} sans équivalent EN, {len(refs)} chapitres renvoyés)")158159if __name__ == "__main__":160    main()161
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme