fix: corrige la perte de lots quand une page coupe le tableau des locataires
pdfplumber fragmente parfois le tableau en plusieurs bbox disjoints quand un filet de séparation manque entre deux lots (ex: page 3 du sample), ce qui faisait disparaître silencieusement les lots tombés dans l'interstice ou situés après le fragment ignoré. Corrige aussi la troncature du nom de locataire quand il s'étale sur plusieurs lignes visuelles ou coïncide avec la ligne de période (lot coupé entre deux pages). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,8 +88,9 @@ def _rows_from_page(page) -> list[dict]:
|
|||||||
ne contient pas ce tableau.
|
ne contient pas ce tableau.
|
||||||
"""
|
"""
|
||||||
# Repérer le tableau « situation locataires » (en-tête commençant par « Locataires »).
|
# Repérer le tableau « situation locataires » (en-tête commençant par « Locataires »).
|
||||||
|
candidates = page.find_tables()
|
||||||
table = None
|
table = None
|
||||||
for candidate in page.find_tables():
|
for candidate in candidates:
|
||||||
header = candidate.rows[0].cells
|
header = candidate.rows[0].cells
|
||||||
if header and header[0] is not None:
|
if header and header[0] is not None:
|
||||||
first = _strip_accents(page.crop(header[0]).extract_text() or "")
|
first = _strip_accents(page.crop(header[0]).extract_text() or "")
|
||||||
@@ -109,7 +110,20 @@ def _rows_from_page(page) -> list[dict]:
|
|||||||
return i
|
return i
|
||||||
return None
|
return None
|
||||||
|
|
||||||
words = page.crop(table.bbox).extract_words()
|
# Quand un filet de séparation manque entre deux lots, pdfplumber détecte
|
||||||
|
# la table logique en plusieurs fragments dont les bbox se touchent sans
|
||||||
|
# se recouvrir : une ligne à cheval sur la coupure tombe alors hors des
|
||||||
|
# deux bbox et serait perdue. On étend donc la zone scannée jusqu'au bas
|
||||||
|
# du fragment le plus bas partageant la même marge gauche que l'ancre,
|
||||||
|
# pour balayer une zone continue plutôt que des bbox disjointes.
|
||||||
|
x0, top, x1, bottom = table.bbox
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate is table:
|
||||||
|
continue
|
||||||
|
if abs(candidate.bbox[0] - x0) <= 2:
|
||||||
|
bottom = max(bottom, candidate.bbox[3])
|
||||||
|
|
||||||
|
words = page.crop((x0, top, x1, bottom)).extract_words()
|
||||||
words.sort(key=lambda w: (round((w["top"] + w["bottom"]) / 2, 1), w["x0"]))
|
words.sort(key=lambda w: (round((w["top"] + w["bottom"]) / 2, 1), w["x0"]))
|
||||||
|
|
||||||
# Regrouper les mots en lignes visuelles (clustering vertical tolérant).
|
# Regrouper les mots en lignes visuelles (clustering vertical tolérant).
|
||||||
@@ -271,6 +285,11 @@ def extract_situation_locataires_from_pdf(pdf_path: str) -> list[dict]:
|
|||||||
_fill_totaux(current, row)
|
_fill_totaux(current, row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Ligne de total général de fin de tableau (« TOTAUX », tout en
|
||||||
|
# majuscules) : ne concerne aucun lot, à ignorer.
|
||||||
|
if loc == "TOTAUX":
|
||||||
|
continue
|
||||||
|
|
||||||
# Solde Antérieur (libellé + montant dans la colonne période).
|
# Solde Antérieur (libellé + montant dans la colonne période).
|
||||||
if periode.startswith("Solde Antérieur"):
|
if periode.startswith("Solde Antérieur"):
|
||||||
montant = _num(periode)
|
montant = _num(periode)
|
||||||
@@ -307,14 +326,22 @@ def extract_situation_locataires_from_pdf(pdf_path: str) -> list[dict]:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ligne de période (loyer ou divers).
|
# Ligne de période (loyer ou divers). Quand l'en-tête du lot est en bas
|
||||||
|
# d'une page et ses données en haut de la suivante, le nom du locataire
|
||||||
|
# peut se retrouver sur cette même ligne visuelle (colonne « Locataires »
|
||||||
|
# non vide) plutôt que sur la ligne d'en-tête du lot.
|
||||||
if _PERIODE_RE.search(periode):
|
if _PERIODE_RE.search(periode):
|
||||||
|
if loc:
|
||||||
|
nom = current["locataire"]["nom"]
|
||||||
|
current["locataire"]["nom"] = f"{nom} {loc}".strip() if nom else loc
|
||||||
_append_periode_line(current, row)
|
_append_periode_line(current, row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Nom du locataire (cellule « Locataires » seule, nom pas encore trouvé).
|
# Nom du locataire (cellule « Locataires » seule). Peut être réparti sur
|
||||||
if loc and not current["locataire"]["nom"]:
|
# plusieurs lignes visuelles quand le nom est trop long pour la colonne.
|
||||||
current["locataire"]["nom"] = loc
|
if loc:
|
||||||
|
nom = current["locataire"]["nom"]
|
||||||
|
current["locataire"]["nom"] = f"{nom} {loc}".strip() if nom else loc
|
||||||
|
|
||||||
if current:
|
if current:
|
||||||
situations.append(current)
|
situations.append(current)
|
||||||
|
|||||||
Reference in New Issue
Block a user