Sur cette page
- Un PDF garde des morceaux de texte posés sur la page, pas l'ordre de lecture : selon l'outil, un tableau en colonnes sort ligne par ligne ou colonne par colonne.
- Un scan n'a pas de texte : il faut un OCR, ou un modèle qui lit l'image ; pypdf le dit lui-même.
- Un classeur cache ses pièges : une ligne masquée que le programme lit quand même, une formule dont la valeur n'existe que si Excel l'a calculée.
- Dans les trois cas, une règle simple, ici le total, dit si la lecture tient.
- Lire n'est pas comprendre : on contrôle ce qui sort avant de s'en servir.
Objectif
Lire le même tableau de ventes dans trois fichiers, un rapport PDF, un scan, un classeur Excel, en sortir les quatre trimestres et le total, et dire pour chaque fichier si la lecture est juste.
Les trois fichiers sont fabriqués : entreprise fictive, chiffres inventés. Les lectures du PDF et du classeur ont tourné, leurs sorties sont réelles. La lecture du scan (étape 2) n'a pas été exécutée par Meridal : la lecture utilisée à l'étape 4 est fabriquée.
Données fournies
Le même tableau, livré trois fois.
rapport.pdf
Fichier fabriqué pour l'exercice de Meridal : entreprise fictive, chiffres inventés. Un PDF avec sa couche de texte, en deux colonnes.
Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)
Données fournies Le commentaire, à gauche Le tableau, à droite Les ventes progressent au deuxième trimestre, reculent pendant l'été, puis repartent en fin d'année avec les commandes de cuisines. Trimestre et ventes en k€, T1 à T4, puis le total scan.png
Fichier fabriqué pour l'exercice de Meridal : le même tableau, photographié un peu de travers et flou. Une image : aucun texte à copier.
Ventes 2026 (k€)
Données fournies Trimestre Ventes T1 412 T2 468 T3 395 T4 503 Total 1778 classeur.xlsx
Fichier fabriqué pour l'exercice de Meridal. Le titre est fusionné sur deux colonnes, la ligne de T3 est masquée, le total est une formule : =SUM(B3:B6).
Données fournies Trimestre Ventes T1 412 T2 468 T4 503 Total (formule) À l'écran, la ligne de T3 n'apparaît pas.
Voir le programme qui fabrique les trois fichiers
fabriquer.py"""Fabrique les trois fichiers de l'exercice dans fichiers/ : un rapport PDF en deux colonnes, le même tableau
scanné (une image), et un classeur Excel avec une cellule fusionnée, une ligne masquée et une formule."""
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from openpyxl import Workbook
from donnees import SOCIETE, VENTES, TOTAL
# 1. Le rapport : le commentaire à gauche, le tableau à droite
c = canvas.Canvas("fichiers/rapport.pdf", pagesize=A4)
c.setFont("Helvetica-Bold", 14); c.drawString(50, 790, f"Rapport d'activité 2026, {SOCIETE}")
c.setFont("Helvetica", 11)
gauche = ["Les ventes progressent au", "deuxième trimestre, reculent", "pendant l'été, puis repartent", "en fin d'année avec les", "commandes de cuisines."]
for i, l in enumerate(gauche):
c.drawString(50, 740 - 18 * i, l)
c.drawString(320, 740, "Trimestre Ventes (k€)")
for i, (t, v) in enumerate(VENTES):
c.drawString(320, 722 - 18 * i, f"{t}"); c.drawRightString(470, 722 - 18 * i, f"{v}")
c.drawString(320, 722 - 18 * 4, "Total"); c.drawRightString(470, 722 - 18 * 4, f"{TOTAL}")
c.save()
# 2. Le même tableau, scanné : une image un peu de travers
im = Image.new("L", (900, 520), 247); d = ImageDraw.Draw(im)
police = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 34)
d.text((60, 40), "Ventes 2026 (k€)", fill=35, font=police)
for i, (t, v) in enumerate(VENTES + [("Total", TOTAL)]):
d.text((60, 120 + 70 * i), t, fill=35, font=police); d.text((520, 120 + 70 * i), str(v), fill=35, font=police)
im.rotate(-1.5, fillcolor=247).filter(ImageFilter.GaussianBlur(0.9)).save("fichiers/scan.png")
# 3. Le classeur : un titre fusionné, la ligne de T3 masquée, le total en formule
wb = Workbook(); ws = wb.active; ws.title = "Ventes"
ws["A1"] = "Ventes 2026 (k€)"; ws.merge_cells("A1:B1")
ws.append(["Trimestre", "Ventes"])
for t, v in VENTES:
ws.append([t, v])
ws.row_dimensions[5].hidden = True
ws["A7"] = "Total"; ws["B7"] = "=SUM(B3:B6)"
wb.save("fichiers/classeur.xlsx")
print("fabriqués : rapport.pdf, scan.png, classeur.xlsx")Consignes
Étape 1. Le PDF : quatre lectures du même texte
Le rapport a sa couche de texte : on peut copier ses mots. Quatre façons de la tirer, avec trois outils libres. pypdf propose un mode « layout » qui reproduit la mise en page.
lire_pdf.pyimport subprocess
from pypdf import PdfReader
from pdfminer.high_level import extract_text
print("--- pypdf")
print(PdfReader("fichiers/rapport.pdf").pages[0].extract_text())
print("--- pypdf, extraction_mode=\"layout\"")
print(PdfReader("fichiers/rapport.pdf").pages[0].extract_text(extraction_mode="layout").rstrip())
print("--- pdfminer.six")
texte = extract_text("fichiers/rapport.pdf")
print("\n".join(l for l in texte.splitlines() if l.strip())) # sans les lignes vides
print("--- pdftotext -layout")
print(subprocess.run(["pdftotext", "-layout", "fichiers/rapport.pdf", "-"], capture_output=True, text=True).stdout.rstrip())python3 lire_pdf.py--- pypdfRapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)Les ventes progressent audeuxième trimestre, reculentpendant l'été, puis repartenten fin d'année avec lescommandes de cuisines.Trimestre Ventes (k€)T1412T2468T3395T4503Total1778 --- pypdf, extraction_mode="layout"Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive) Les ventes progressent au Trimestre Ventes (k€)deuxième trimestre, reculent T1 412pendant l'été, puis repartent T2 468en fin d'année avec les T3 395commandes de cuisines. T4 503 Total 1778--- pdfminer.sixRapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)Les ventes progressent audeuxième trimestre, reculentpendant l'été, puis repartenten fin d'année avec lescommandes de cuisines.Trimestre Ventes (k€)T1T2T3T4Total4124683955031778--- pdftotext -layoutRapport d'activité 2026, Menuiserie Valcourt (entreprise fictive) Les ventes progressent au Trimestre Ventes (k€)deuxième trimestre, reculent T1 412pendant l'été, puis repartent T2 468en fin d'année avec les T3 395commandes de cuisines. T4 503 Total 1778
Essayé le 9 octobre 2026 avec Python 3.12.7, pypdf 6.17.0, pdfminer.six 20260107, pdftotext 26.09.0, openpyxl 3.1.5 et pandas 2.2.0, sur les fichiers fabriqués de la page.
pdfminer.six rend d'abord tous les trimestres, puis tous les montants : l'ordre des colonnes est perdu, et rien ne dit plus que 395 va avec T3. pypdf garde chaque montant sous son trimestre ; les deux modes « layout », de pypdf et de pdftotext, gardent la ligne entière, commentaire compris.
Étape 2. Le scan : un OCR ou un modèle qui voit
Le scan n'a aucun texte : les trois outils de l'étape 1 n'y trouvent rien. Deux chemins. Un OCR libre, comme Tesseract, sous licence Apache 2.0, s'installe sur la machine, avec la langue française en plus. Ou un modèle qui lit l'image : il comprend le tableau, mais il peut se tromper sur une image floue, tournée ou petite.
Mac (Homebrew)brew install tesseract tesseract-lang tesseract fichiers/scan.png - -l fra
Windows (installeur de l'université de Mannheim)tesseract fichiers\scan.png - -l fra
lire_scan.pyimport base64
import anthropic
image = base64.standard_b64encode(open("fichiers/scan.png", "rb").read()).decode()
reponse = anthropic.Anthropic().messages.create(
model="claude-sonnet-5-5",
max_tokens=512,
messages=[{"role": "user", "content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image}},
{"type": "text", "text": "Recopie ce tableau en JSON, une clé par ligne, "
"les nombres tels qu'écrits. Ne calcule rien ; "
"ce qui est illisible reste null."},
]}],
)
print(reponse.content[0].text)python3 lire_scan.pyPas encore exécuté par Meridal. Ni Tesseract ni le modèle n'ont été lancés par Meridal : aucun logiciel n'est installé, aucun appel payant n'est fait pour cette page. La lecture fabriquée, dans lecture_scan.json, lit 463 au lieu de 468 pour T2 : la confusion d'un 8 flou avec un 3.
Étape 3. Le classeur : ce que voit l'écran, ce que lit le programme
lire_classeur.pyfrom openpyxl import load_workbook
import pandas as pd
ws = load_workbook("fichiers/classeur.xlsx").active
print("cellules fusionnées :", [str(z) for z in ws.merged_cells.ranges])
print("lignes masquées :", [n for n, d in ws.row_dimensions.items() if d.hidden])
print("B7, la formule :", ws["B7"].value)
print("B7, la valeur :", load_workbook("fichiers/classeur.xlsx", data_only=True).active["B7"].value)
df = pd.read_excel("fichiers/classeur.xlsx", header=1)
print("--- pandas")
print(df.to_string(index=False))python3 lire_classeur.pycellules fusionnées : ['A1:B1']lignes masquées : [5]B7, la formule : =SUM(B3:B6)B7, la valeur : None--- pandasTrimestre Ventes T1 412.0 T2 468.0 T3 395.0 T4 503.0 Total NaN
Essayé le 9 octobre 2026 avec Python 3.12.7, pypdf 6.17.0, pdfminer.six 20260107, pdftotext 26.09.0, openpyxl 3.1.5 et pandas 2.2.0, sur les fichiers fabriqués de la page.
pandas lit la ligne de T3, que l'écran cache, sans le signaler. Et le total, une formule, n'a pas de valeur : openpyxl rend, avec data_only, la valeur enregistrée la dernière fois qu'Excel a lu la feuille ; ce fichier n'a jamais été ouvert dans Excel, donc il n'y en a pas.
Étape 4. Contrôler les trois lectures
Une seule règle : quatre trimestres, et leur somme égale au total. Le classeur est lu comme l'écran le montre, sans les lignes masquées, pour que le contrôle le voie.
controler.pyimport json, re, subprocess
from openpyxl import load_workbook
def depuis_pdf():
"""Les lignes « T1 … 412 » du texte mis en page par pdftotext."""
texte = subprocess.run(["pdftotext", "-layout", "fichiers/rapport.pdf", "-"], capture_output=True, text=True).stdout
return {m[0]: int(m[1]) for m in re.findall(r"\b(T\d|Total)\s+(\d+)\s*$", texte, re.M)}
def depuis_classeur():
"""Les lignes visibles seulement, et le total recalculé si la formule n'a pas de valeur enregistrée."""
ws = load_workbook("fichiers/classeur.xlsx", data_only=True).active
lignes = {}
for n in range(3, ws.max_row + 1):
if ws.row_dimensions[n].hidden or ws.cell(n, 1).value is None:
continue
lignes[ws.cell(n, 1).value] = ws.cell(n, 2).value
return lignes
def controle(nom, l):
trimestres = {k: v for k, v in l.items() if re.fullmatch(r"T\d", k)}
echecs = []
if len(trimestres) != 4:
echecs.append(f"{len(trimestres)} trimestres lus sur 4")
if l.get("Total") is None:
echecs.append("total absent (formule jamais calculée)")
elif sum(trimestres.values()) != l["Total"]:
echecs.append(f"{' + '.join(map(str, trimestres.values()))} = {sum(trimestres.values())}, le total lu est {l['Total']}")
return f"{nom:<9} " + ("à revoir : " + " ; ".join(echecs) if echecs else f"juste : {l['Total']}")
print(controle("PDF", depuis_pdf()))
print(controle("scan", json.load(open("lecture_scan.json"))))
print(controle("classeur", depuis_classeur()))python3 controler.pyPDF juste : 1778scan à revoir : 412 + 463 + 395 + 503 = 1773, le total lu est 1778classeur à revoir : 3 trimestres lus sur 4 ; total absent (formule jamais calculée)
Essayé le 9 octobre 2026 avec Python 3.12.7, pypdf 6.17.0, pdfminer.six 20260107, pdftotext 26.09.0, openpyxl 3.1.5 et pandas 2.2.0, sur les fichiers fabriqués de la page.
Critères de réussite
Indications
Le texte tiré d'un PDF est vide
CauseLa page est une image : un scan, ou un PDF imprimé en image.
À fairePasser par un OCR, ou envoyer la page en image à un modèle ; un scan doit faire 150 DPI au moins selon AWS.
Les montants ne sont plus en face de leur ligne
CauseL'outil lit les colonnes l'une après l'autre.
À faireLe mode layout de pypdf ou de pdftotext ; pour un vrai tableau, un outil qui reconstruit les tableaux, comme Docling.
Correction commentée
Ouvrir la correction
Le PDFpdftotext en mode layout garde chaque ligne du tableau entière ; une expression régulière en tire les paires trimestre, montant. La somme tombe juste : 1 778.
Le scanRien ne dit à l'œil que 463 est faux : c'est un nombre plausible. Seule la somme, 1 773 contre un total lu de 1 778, l'arrête. Une personne revoit l'image, ou la ligne en cause.
Le classeurLu comme l'écran, il perd T3 ; lu comme pandas, il le garde sans dire qu'il était masqué. Le bon geste : lire toutes les lignes, mais lister les lignes masquées, et recalculer le total plutôt que de faire confiance à une formule sans valeur.
La leçonLes trois fichiers disaient la même chose ; deux lectures sur trois étaient fausses, et aucune n'a levé d'erreur. Le contrôle, lui, les a trouvées.
Extensions
- Des documents réels : refaire l'exercice sur vingt documents de l'entreprise, et compter les lectures justes.
- Les services en ligne, leurs prix pour 1 000 pages et ce qu'ils lisent en français : lecture de documents et extraction des données.
- Les classements des lecteurs de documents : OmniDocBench compte 1 651 pages de dix types, rapports financiers compris ; ses mainteneurs placent TeleOCR en tête, à 96,91. Chaque éditeur publie ses propres scores : Mistral les dit indicatifs et conseille d'essayer sur ses documents.
Lectures suivantes
Rien avant : c'est un point de départ.
Lecture de documents : OCR, vision, PDF et tableaux
Les pages qui renvoient ici
Limites et sources
Les trois fichiers et la lecture du scan sont fabriqués. Les lectures du PDF et du classeur ont tourné le 9 octobre 2026 ; ni Tesseract ni un modèle n'ont été lancés.
Les versions et licences des outils libres sont celles relevées les 7 et 9 octobre 2026 ; elles changent souvent.
- pypdf, extraire le texte ; openpyxl, data_only ; Tesseract ; Tesseract, installation.
- Le relevé :
apprendre/recherches/2026-10-07-extraction-documents.json.
Écrit par Meridal, avec l'aide de l'IA ; chaque fait vérifié à sa source et relu avant publication. Texte et schémas sous licence CC BY 4.0 : reprenez-les en citant Meridal. Le code sous licence MIT.
Ne plus recopier d'un logiciel à l'autre
1 100 € à 3 400 €, puis sur devis