Repository Public Research preview
1#!/usr/bin/env python32"""3================================================================4Auteur : Simon-Pierre Boucher5Contact : simon-pierre.boucher@uqo.ca6Projet : Prévision de volatilité réalisée multi-actifs7 (HAR-RV vs GARCH vs Machine Learning)8Fichier : 90_verify_bib.py9Description : Utilitaire de révision — interroge Crossref pour10 chaque entrée de paper/references.bib (titre +11 premier auteur), rapporte le meilleur DOI candidat,12 la similarité de titre et les métadonnées, et écrit13 docs/bib-verification.csv. Aucune modification du14 .bib n'est faite automatiquement.15Usage : python scripts/90_verify_bib.py [--bib paper/references.bib]16================================================================17"""1819from __future__ import annotations2021import argparse22import csv23import difflib24import re25import sys26import time27from pathlib import Path2829import requests3031ROOT = Path(__file__).resolve().parents[1]32API = "https://api.crossref.org/works"33UA = {"User-Agent": "wp12-bib-verifier/1.0 (mailto:simon-pierre.boucher@uqo.ca)"}343536def parse_bib(text: str) -> list[dict[str, str]]:37 """Minimal BibTeX parser (one field per line, as in references.bib)."""38 entries: list[dict[str, str]] = []39 for block in re.split(r"\n(?=@)", text):40 m = re.match(r"@(\w+)\{([^,]+),", block.strip())41 if not m:42 continue43 e = {"type": m.group(1), "key": m.group(2).strip()}44 for fm in re.finditer(r"^\s*(\w+)\s*=\s*\{(.*)\},?\s*$", block, re.M):45 e[fm.group(1).lower()] = fm.group(2).strip()46 entries.append(e)47 return entries484950def clean(s: str) -> str:51 s = re.sub(r"[{}\\]", "", s)52 s = re.sub(r"\s+", " ", s)53 return s.strip().lower()545556def main() -> None:57 ap = argparse.ArgumentParser()58 ap.add_argument("--bib", default=str(ROOT / "paper" / "references.bib"))59 ap.add_argument("--out", default=str(ROOT / "docs" / "bib-verification.csv"))60 args = ap.parse_args()61 entries = parse_bib(Path(args.bib).read_text(encoding="utf-8"))62 rows = []63 for e in entries:64 title = clean(e.get("title", ""))65 first_author = clean(e.get("author", "").split(" and ")[0])66 q = {"query.bibliographic": f"{e.get('title','')} {first_author}", "rows": 3}67 try:68 r = requests.get(API, params=q, headers=UA, timeout=30)69 items = r.json()["message"]["items"]70 except Exception as exc: # noqa: BLE00171 rows.append({**{k: e.get(k, "") for k in ("key", "year", "doi")},72 "status": f"ERROR {exc}"})73 print(e["key"], "ERROR", exc, file=sys.stderr)74 continue75 best, best_sim = None, 0.076 for it in items:77 t = clean(" ".join(it.get("title", [])))78 sim = difflib.SequenceMatcher(None, title, t).ratio()79 if sim > best_sim:80 best, best_sim = it, sim81 row = {"key": e["key"], "year": e.get("year", ""), "doi_in_bib": e.get("doi", "")}82 if best is None:83 row.update(status="NO MATCH")84 else:85 cy = ""86 for k in ("published-print", "published-online", "issued"):87 parts = best.get(k, {}).get("date-parts", [[None]])88 if parts and parts[0] and parts[0][0]:89 cy = str(parts[0][0]); break90 row.update(91 status="OK" if best_sim >= 0.85 else "CHECK",92 sim=f"{best_sim:.2f}",93 crossref_doi=best.get("DOI", ""),94 crossref_title=" ".join(best.get("title", []))[:120],95 crossref_container=" ".join(best.get("container-title", []))[:60],96 crossref_year=cy, crossref_volume=best.get("volume", ""),97 crossref_issue=best.get("issue", ""), crossref_pages=best.get("page", ""),98 )99 rows.append(row)100 print(f"{row['key']:32s} {row['status']:8s} {row.get('sim','')} {row.get('crossref_doi','')}")101 time.sleep(0.3)102 fields = ["key", "year", "doi_in_bib", "status", "sim", "crossref_doi", "crossref_title",103 "crossref_container", "crossref_year", "crossref_volume", "crossref_issue",104 "crossref_pages"]105 Path(args.out).parent.mkdir(parents=True, exist_ok=True)106 with open(args.out, "w", newline="", encoding="utf-8") as fh:107 w = csv.DictWriter(fh, fieldnames=fields, extrasaction="ignore")108 w.writeheader()109 w.writerows(rows)110 print(f"written {args.out} ({len(rows)} entries)")111112113if __name__ == "__main__":114 main()115