Aller au contenu

S'exercerNiveau 3, Construire30 minutes

Lecture de documents : OCR, vision, PDF et tableaux

Un PDF, un scan, un classeur : trois façons de livrer le même tableau, et trois façons pour un programme de mal le lire. L'ordre de lecture d'un PDF en colonnes, l'image qui n'a pas de texte, la ligne masquée et la formule sans valeur. Les lire, puis vérifier ce qu'on en tire.

Écrit le 9 octobre 2026 · Vérifié le 9 octobre 2026

Vous saurez

  • Tirer le texte d'un PDF, et savoir quel outil garde l'ordre d'un tableau
  • Reconnaître un scan, et choisir entre un OCR et un modèle qui voit
  • Lire un classeur sans perdre une ligne masquée ni un total
  • Vérifier une lecture par une règle, quelle que soit sa source
Sur cette page
  1. L'essentiel
  2. Objectif
  3. Données fournies
  4. Consignes
  5. Critères de réussite
  6. Indications
  7. Correction commentée
  8. Extensions
  9. Lectures suivantes
L'essentiel
  1. 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.
  2. 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.
  3. 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.
  4. Dans les trois cas, une règle simple, ici le total, dit si la lecture tient.
  5. Lire n'est pas comprendre : on contrôle ce qui sort avant de s'en servir.
1

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.

2

Données fournies

Le même tableau, livré trois fois.

  1. 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
  2. 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
  3. 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")
3

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.py
import 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())
La sortiepython3 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.py
import 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)
La sortiepython3 lire_scan.py

Pas 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.py
from 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))
La sortiepython3 lire_classeur.py
cellules 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.py
import 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()))
La sortiepython3 controler.py
PDF       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.

4

Critères de réussite

5

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.

Un tableau sur deux pages sort en deux morceaux

CauseChaque page se lit à part.

À faireMicrosoft conseille de traiter les pages à part puis de recoller le tableau ; PaddleOCR-VL recolle les tableaux d'une page à l'autre.

Les cellules fusionnées décalent les colonnes

CauseUne cellule fusionnée n'a de valeur que dans sa première case ; AWS signale aussi des résultats inégaux sur les tableaux aux cellules fusionnées.

À faireLire l'en-tête à part (header= dans pandas), et vérifier le nombre de colonnes.

6

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.

7

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.
Extensions
Outil libreCe qu'il faitLicence et version
Doclingla mise en page, l'ordre de lecture, les tableaux ; sur vos machines, même sans internetMIT, 2.135.0 du 7 octobre 2026
Markerun PDF converti en texte structuré ; les mises en page très complexes peuvent échouer, selon ses auteurscode Apache 2.0, poids sous licence modifiée, 2.0.0
MinerUla conversion de documents ; licence commerciale au-delà d'un seuil d'usageApache 2.0 avec conditions, 4.0.10
PaddleOCRl'OCR et les tableaux, d'une page à l'autreApache 2.0, 3.7.0
docTRl'OCR, créé par la société française MindeeApache 2.0, 1.1.0
Tesseractl'OCR classique, en ligne de commandeApache 2.0, 5.5.3
8

Lectures suivantes

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.

    1. pypdf, extraire le texte ; openpyxl, data_only ; Tesseract ; Tesseract, installation.
    2. 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.

    Service correspondant

    Ne plus recopier d'un logiciel à l'autre

    1 100 € à 3 400 €, puis sur devis

    Voir le service

    Un premier échange de 30 minutes

    En visio et sans frais : vous décrivez votre situation, nous vous indiquons ce qui est réalisable, avec quels outils et pour quel ordre de prix. Vous pouvez aussi nous écrire ; chaque message reçoit une réponse écrite.