feat: migre l'extraction de pdftotext (poppler) vers pdfplumber

Remplace l'appel externe `pdftotext -layout` (poppler-utils) par
pdfplumber, une bibliotheque Python pure : plus aucune dependance
systeme, ce qui permettra d'empaqueter l'application en executable
autonome.

- `read_pdf()` retourne un `PdfContent` (texte layout + mots de la
  premiere page avec coordonnees) ; `extract_text_from_pdf()` est
  conserve comme wrapper de compatibilite.
- Metadonnees : la reference est desormais extraite via les
  coordonnees geometriques des mots (fiable sur l'en-tete
  multi-colonnes), regex du destinataire durcie (forme pointee
  `S.C.I.`).
- Operations : reduction des espaces multiples dans les descriptions.
- Dockerfile : suppression de poppler-utils.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 17:14:31 +02:00
parent d42bfecab4
commit 7cebc0bb3d
9 changed files with 530 additions and 35 deletions

View File

@@ -1,28 +1,74 @@
"""Extraction de texte depuis les PDFs."""
"""Extraction de texte et de mots depuis les PDFs via pdfplumber.
import subprocess
pdfplumber est une bibliothèque Python pure : aucune dépendance système
(contrairement à l'ancien `pdftotext`/poppler), ce qui permet d'empaqueter
l'application en exécutable autonome (Windows notamment).
"""
from dataclasses import dataclass, field
import pdfplumber
@dataclass
class PdfContent:
"""Contenu extrait d'un PDF.
Attributes:
text: Texte de toutes les pages avec mise en page préservée
(équivalent de `pdftotext -layout`). Consommé par les parseurs
opérations et locataires.
words: Mots de la première page avec leurs coordonnées
(``text``, ``x0``, ``x1``, ``top``, ``bottom``). Utilisé par le
parseur de métadonnées pour les champs d'en-tête multi-colonnes
où l'alignement par espaces n'est pas fiable.
"""
text: str
words: list[dict] = field(default_factory=list)
def read_pdf(pdf_path: str) -> PdfContent:
"""Lit un PDF et retourne son texte (layout) et les mots de l'en-tête.
Args:
pdf_path: Chemin vers le fichier PDF
Returns:
Un objet :class:`PdfContent`.
"""
text_parts: list[str] = []
header_words: list[dict] = []
with pdfplumber.open(pdf_path) as pdf:
for page_index, page in enumerate(pdf.pages):
text_parts.append(page.extract_text(layout=True) or "")
# Les métadonnées se trouvent uniquement sur la première page.
if page_index == 0:
header_words = [
{
"text": w["text"],
"x0": w["x0"],
"x1": w["x1"],
"top": w["top"],
"bottom": w["bottom"],
}
for w in page.extract_words()
]
return PdfContent(text="\n".join(text_parts), words=header_words)
def extract_text_from_pdf(pdf_path: str) -> str:
"""Extrait le texte du PDF via pdftotext.
"""Extrait le texte du PDF avec mise en page préservée.
Nécessite pdftotext (poppler-utils) installé sur le système:
- Ubuntu/Debian: sudo apt-get install poppler-utils
- macOS: brew install poppler
Conserve la signature historique (retourne une chaîne) pour la CLI et
les parseurs qui ne consomment que le texte.
Args:
pdf_path: Chemin vers le fichier PDF
Returns:
Texte extrait du PDF avec mise en page préservée
Raises:
subprocess.CalledProcessError: Si pdftotext échoue
"""
result = subprocess.run(
["pdftotext", "-layout", pdf_path, "-"],
capture_output=True,
text=True,
check=True,
)
return result.stdout
return read_pdf(pdf_path).text