---
title: "Lecture de documents : OCR, vision, PDF et tableaux"
description: "Faire lire un scan, un PDF ou un tableau à un programme, et vérifier ce qu'il en tire."
url: https://meridal.fr/apprendre/lecture-de-documents-ocr-vision-pdf/
language: fr
written: 2026-10-09
verified: 2026-10-09
license: CC BY 4.0
---

[Apprendre](https://meridal.fr/apprendre/) › [Construction et programmation](https://meridal.fr/apprendre/dossier/construire/)

S'exercer · Niveau 3, Construire · 30 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

- [← Avant Extraire une facture dans un format imposé](https://meridal.fr/apprendre/extraire-une-facture-dans-un-format-impose/)
- [↓ Plus concret Premier appel à un modèle](https://meridal.fr/apprendre/premier-appel/)
- [→ Ensuite Bibliothèques d'agents](https://meridal.fr/apprendre/bibliotheques-d-agents/)

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.

## 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.

La suite de [extraire une facture dans un format imposé](https://meridal.fr/apprendre/extraire-une-facture-dans-un-format-impose/). Ce qu'une chaîne de lecture donne dans un cabinet : [les chiffres repris des fichiers du client](https://meridal.fr/realisations/document-remis-aux-acquereurs/).

## 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)**

   | 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€)**

   | 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).

   | 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.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 sortie`python3 lire_pdf.py`

```
--- pypdf
Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)
Les ventes progressent au
deuxième trimestre, reculent
pendant l'été, puis repartent
en fin d'année avec les
commandes de cuisines.
Trimestre     Ventes (k€)
T1
412
T2
468
T3
395
T4
503
Total
1778
 
--- pypdf, extraction_mode="layout"
Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)
 
 
 
Les ventes progressent au                                    Trimestre     Ventes (k€)
deuxième trimestre, reculent                                 T1                            412
pendant l'été, puis repartent                                T2                            468
en fin d'année avec les                                      T3                            395
commandes de cuisines.                                       T4                            503
                                                             Total                        1778
--- pdfminer.six
Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)
Les ventes progressent au
deuxième trimestre, reculent
pendant l'été, puis repartent
en fin d'année avec les
commandes de cuisines.
Trimestre     Ventes (k€)
T1
T2
T3
T4
Total
412
468
395
503
1778
--- pdftotext -layout
Rapport d'activité 2026, Menuiserie Valcourt (entreprise fictive)
 
 
Les ventes progressent au                Trimestre   Ventes (k€)
deuxième trimestre, reculent             T1                         412
pendant l'été, puis repartent            T2                         468
en fin d'année avec les                  T3                         395
commandes 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 sortie`python3 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 sortie`python3 lire_classeur.py`

```
cellules fusionnées : ['A1:B1']
lignes masquées     : [5]
B7, la formule      : =SUM(B3:B6)
B7, la valeur       : None
--- pandas
Trimestre  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 sortie`python3 controler.py`

```
PDF       juste : 1778
scan      à revoir : 412 + 463 + 395 + 503 = 1773, le total lu est 1778
classeur  à 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

- Pour le PDF, chaque montant sort avec son trimestre pdfminer.six seul ne suffit pas ici ; le mode layout, oui
- Le scan est reconnu comme une image, et part à un OCR ou à un modèle qui voit
- La ligne masquée du classeur est repérée, pas ignorée
- Le total du classeur est recalculé, ou signalé comme absent
- `controler.py` arrête le scan sur sa somme : 1 773 au lieu de 1 778

## Indications

**Le texte tiré d'un PDF est vide**

**Cause**La page est une image : un scan, ou un PDF imprimé en image.

**À faire**Passer 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**

**Cause**L'outil lit les colonnes l'une après l'autre.

**À faire**Le 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**

**Cause**Chaque page se lit à part.

**À faire**Microsoft 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**

**Cause**Une 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.

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

## Correction commentée

**Ouvrir la correction**

**Le PDF**pdftotext 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 scan**Rien 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 classeur**Lu 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çon**Les 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](https://meridal.fr/apprendre/lecture-de-documents-et-extraction-des-donnees/).
- **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.

| Outil libre | Ce qu'il fait | Licence et version |
|---|---|---|
| Docling | la mise en page, l'ordre de lecture, les tableaux ; sur vos machines, même sans internet | MIT, 2.135.0 du 7 octobre 2026 |
| Marker | un PDF converti en texte structuré ; les mises en page très complexes peuvent échouer, selon ses auteurs | code Apache 2.0, poids sous licence modifiée, 2.0.0 |
| MinerU | la conversion de documents ; licence commerciale au-delà d'un seuil d'usage | Apache 2.0 avec conditions, 4.0.10 |
| PaddleOCR | l'OCR et les tableaux, d'une page à l'autre | Apache 2.0, 3.7.0 |
| docTR | l'OCR, créé par la société française Mindee | Apache 2.0, 1.1.0 |
| Tesseract | l'OCR classique, en ligne de commande | Apache 2.0, 5.5.3 |

## Lectures suivantes

↑ Plus général

Rien avant : c'est un point de départ.

← Avant [Extraire une facture dans un format imposé S'exercer · Niveau 3 · 30 minutes](https://meridal.fr/apprendre/extraire-une-facture-dans-un-format-impose/)

Vous êtes ici

Lecture de documents : OCR, vision, PDF et tableaux

→ Ensuite [Bibliothèques d'agents Choisir · Niveau 3 · 14 minutes](https://meridal.fr/apprendre/bibliotheques-d-agents/)

↓ Plus concret [Premier appel à un modèle S'exercer · Niveau 3 · 15 minutes](https://meridal.fr/apprendre/premier-appel/) [Un modèle commun pour le dossier de vente, et des chiffres repris sans ressaisie Réalisation](https://meridal.fr/realisations/document-remis-aux-acquereurs/)

[Tout le dossier « Construction et programmation »](https://meridal.fr/apprendre/dossier/construire/)

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](https://pypdf.readthedocs.io/en/stable/user/extract-text.html) ; [openpyxl, data_only](https://openpyxl.readthedocs.io/en/stable/api/openpyxl.reader.excel.html) ; [Tesseract](https://github.com/tesseract-ocr/tesseract) ; [Tesseract, installation](https://tesseract-ocr.github.io/tessdoc/Installation.html).
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](https://creativecommons.org/licenses/by/4.0/deed.fr) : 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](https://meridal.fr/offre/#automatisation)

## 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.

[Réserver 30 minutes](https://meridal.fr/contact/#reserver) [Écrire un message](https://meridal.fr/contact/)
