Aller au contenu principal

Headers, footers et pagination

Mis à jour le 29 juillet 2026

Séparer le décor du contenu

Depuis la version OCR 2512 (décembre 2025), le modèle OCR de Mistral extrait séparément les en-têtes et les pieds de page de chaque page. La fonctionnalité paraît anecdotique tant qu’on n’a pas indexé un contrat de cent vingt pages : le nom du cabinet, la mention « Confidentiel » et la pagination y apparaissent cent vingt fois, et polluent chaque résultat de recherche. Sur les documents formels — contrats, rapports annuels, textes réglementaires —, cette séparation change la qualité de tout ce qui vient ensuite.

Le dilemme est réel. Ces éléments se répètent mécaniquement à chaque page et créent du bruit dans l’indexation comme dans les réponses d’un système RAG. Mais ils contiennent parfois les seules métadonnées fiables du document : le numéro de version, la date d’émission, le niveau de classification. Les extraire à part, plutôt que de les supprimer ou de les laisser dans le corps du texte, vous laisse décider au cas par cas.

Accéder aux champs

Les champs header et footer sont disponibles directement sur chaque page de la réponse :

import os
from mistralai import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

ocr_response = client.ocr.process(
    model="mistral-ocr-latest",
    document={
        "type": "document_url",
        "document_url": "https://exemple.com/contrat.pdf"
    }
)

for page in ocr_response.pages:
    print(f"--- Page {page.index + 1} ---")

    if page.header:
        print(f"En-tête : {page.header}")

    print(f"Contenu : {page.markdown[:100]}...")

    if page.footer:
        print(f"Pied de page : {page.footer}")

    print()

Les tests if page.header ne sont pas superflus : toutes les pages n’ont pas d’en-tête, à commencer par les pages de garde, et le champ vaut alors None.

Reconstruire la pagination

Chaque page est indexée à partir de zéro. Pour un document multi-pages que vous comptez stocker ou renvoyer à une application, il vaut mieux normaliser tout de suite la structure : numéro humain, contenu, décor, dimensions et nombre d’images.

def extraire_avec_pagination(ocr_response) -> dict:
    """Extrait le contenu en préservant la pagination."""
    document = {
        "nombre_pages": len(ocr_response.pages),
        "pages": []
    }

    for page in ocr_response.pages:
        page_data = {
            "numero": page.index + 1,
            "contenu": page.markdown,
            "header": page.header or "",
            "footer": page.footer or "",
            "dimensions": {
                "largeur": page.dimensions.width if page.dimensions else None,
                "hauteur": page.dimensions.height if page.dimensions else None,
            },
            "nombre_images": len(page.images) if page.images else 0,
        }
        document["pages"].append(page_data)

    return document

Les or "" et les gardes sur page.dimensions transforment des valeurs éventuellement nulles en valeurs neutres. C’est ce qui vous évite un AttributeError en pleine boucle sur le quatre-vingtième document d’un lot.

Les dimensions sont exprimées en points PDF, un point valant un soixante-douzième de pouce. Les formats que vous croiserez le plus souvent :

  • A4 : 595 x 842 points
  • Letter : 612 x 792 points
  • A3 : 842 x 1191 points

Cette information sert notamment à repérer les pages hors norme d’un lot — un plan en A3 glissé au milieu de A4, un scan à l’échelle inhabituelle. La conversion en centimètres rend le diagnostic immédiat :

for page in ocr_response.pages:
    if page.dimensions:
        largeur_cm = page.dimensions.width * 2.54 / 72
        hauteur_cm = page.dimensions.height * 2.54 / 72
        print(f"Page {page.index + 1}: {largeur_cm:.1f} x {hauteur_cm:.1f} cm")

Nettoyer le contenu pour le RAG

Selon les documents, l’en-tête et le pied de page peuvent aussi figurer dans le markdown principal. Avant d’indexer, retirez-les lorsqu’ils encadrent effectivement le contenu :

def contenu_principal(ocr_response) -> str:
    """Extrait uniquement le contenu principal, sans headers/footers."""
    parties = []

    for page in ocr_response.pages:
        contenu = page.markdown

        # Optionnel : retirer les headers/footers du markdown
        # s'ils sont inclus dans le contenu principal
        if page.header and contenu.startswith(page.header):
            contenu = contenu[len(page.header):].strip()

        if page.footer and contenu.endswith(page.footer):
            contenu = contenu[:-len(page.footer)].strip()

        parties.append(contenu)

    return "\n\n".join(parties)

Les conditions startswith et endswith sont volontairement prudentes : on ne supprime la chaîne que si elle se trouve bien en tête ou en fin de page. Une suppression aveugle par replace risquerait d’effacer, au milieu du texte, une occurrence légitime du nom de l’entreprise.

Exploiter les métadonnées des pieds de page

Une fois les footers isolés, quelques expressions régulières suffisent à en tirer des métadonnées structurées : la pagination sous forme « 3 / 40 », la date d’émission, le numéro de version, et le niveau de classification.

import re

def extraire_metadonnees_footer(ocr_response) -> dict:
    """Extrait les métadonnées depuis les pieds de page."""
    metadonnees = {
        "numeros_page": [],
        "dates": [],
        "versions": [],
        "classifications": [],
    }

    for page in ocr_response.pages:
        footer = page.footer or ""

        # Recherche de numéro de page
        match_page = re.search(r"(\d+)\s*/\s*(\d+)", footer)
        if match_page:
            metadonnees["numeros_page"].append(
                f"{match_page.group(1)}/{match_page.group(2)}"
            )

        # Recherche de date
        match_date = re.search(r"\d{2}/\d{2}/\d{4}", footer)
        if match_date:
            metadonnees["dates"].append(match_date.group())

        # Recherche de version
        match_version = re.search(r"v(?:ersion)?\s*(\d+\.?\d*)", footer, re.I)
        if match_version:
            metadonnees["versions"].append(match_version.group(1))

        # Classification (Confidentiel, Public, etc.)
        for classification in ["confidentiel", "public", "interne", "secret"]:
            if classification in footer.lower():
                metadonnees["classifications"].append(classification)

    return metadonnees

L’intérêt dépasse la curiosité documentaire. La classification détectée ici peut piloter la suite du traitement : router les pièces marquées « confidentiel » vers un index à accès restreint, ou refuser leur envoi vers un service externe. Vous adapterez évidemment le vocabulaire à celui de votre organisation.

Ne traiter qu’une partie du document

Sur les documents volumineux, il est courant de ne vouloir qu’une section — les annexes financières d’un rapport, le chapitre signé d’un contrat. Le filtrage se fait après l’extraction, sur la réponse déjà obtenue :

def extraire_plage(ocr_response, debut: int, fin: int) -> str:
    """Extrait le contenu d'une plage de pages (1-indexé)."""
    parties = []

    for page in ocr_response.pages:
        numero = page.index + 1
        if debut <= numero <= fin:
            parties.append(page.markdown)

    return "\n\n---\n\n".join(parties)


# Extraire les pages 5 à 10
contenu_partiel = extraire_plage(ocr_response, 5, 10)

La fonction raisonne en numérotation humaine, à partir de 1 : demander les pages 5 à 10 renvoie bien celles-là. Rappelez-vous toutefois que le document entier a été traité, et facturé, avant ce filtrage. Si vous savez à l’avance que seules quelques pages vous intéressent, découpez le PDF en amont plutôt que d’envoyer les mille pages à l’API.

Points clés à retenir

  • Les headers et footers sont extraits séparément depuis OCR 2512
  • Filtrez-les pour éviter le bruit dans vos pipelines RAG
  • Les dimensions de page sont en points PDF (72 points = 1 pouce)
  • Les footers contiennent souvent des métadonnées utiles (version, date, classification)
  • Traitez par plages de pages pour les documents volumineux