Automatiser les fiches produits PrestaShop avec l'IA
PrestaShop

Automatiser les fiches produits PrestaShop avec l'IA

Webservice PrestaShop 9 en lecture et en PATCH, script Python, API Gemini : brouillons contrôlés, relecture, sauvegarde et publication par lots réversible.

Mis à jour le 1er novembre 2026. Cet article regroupe désormais notre article « Gemini pour le SEO produit PrestaShop : méthode et contrôle ». Le script utilise l’API Gemini et la méthode PATCH du webservice, vérifiée dans le code de PrestaShop 9.2.0.

Automatiser des fiches produits avec l’IA ne consiste pas à laisser un modèle écrire dans la boutique. Le circuit décrit ici lit chaque produit par le webservice de PrestaShop, demande un brouillon à l’API Gemini, contrôle ce brouillon, attend votre relecture, sauvegarde la fiche d’origine, puis n’écrit que les champs relus, par lots. Chaque écriture peut être annulée.

Cet article traite de la mécanique : clés, requêtes, script, publication. La méthode éditoriale (quels champs réécrire, quelles consignes donner, ce que Google attend) est dans notre méthode SEO produit en 5 étapes. Les scripts d’audit des balises existantes, du sitemap et de la Search Console sont dans nos scripts Python SEO pour PrestaShop.

Le circuit en sept étapes

ÉtapeCommandeCe qui se passeÉcrit dans la boutique ?
1. LiregenererGET /api/products/{id} avec la clé de lectureNon
2. GénérergenererAppel à l’API Gemini, réponse JSON imposéeNon
3. ContrôlergenererLongueurs, balises, chiffres absents de la sourceNon
4. Relireà la mainVous corrigez le fichier et passez relu à trueNon
5. SauvegarderpublierXML complet de la fiche enregistré avec horodatageNon
6. ÉcrirepublierPATCH limité à quatre champs, une seule langueOui
7. Restaurer si besoinrestaurerPATCH des valeurs sauvegardéesOui

Les quatre champs traités sont meta_title, meta_description, description_short et description. Le nom du produit et link_rewrite ne sont jamais modifiés : changer l’URL d’une fiche sans redirection casse les liens existants.

Pourquoi le webservice plutôt que l’export CSV

L’export CSV de la liste des produits, dans PrestaShop 9.2.0, contient huit colonnes : identifiant, image, nom, référence, catégorie, prix hors taxes et toutes taxes, quantité. Il ne contient ni les descriptions ni les balises meta. Le webservice, lui, expose ces champs pour chaque langue, en lecture comme en écriture. C’est donc la voie naturelle pour un traitement automatisé.

Les limites à respecter viennent de la classe Product : 255 caractères pour meta_title, 512 pour meta_description, 128 pour le nom, et pour description_short la valeur du paramètre PS_PRODUCT_SHORT_DESC_LIMIT, 800 caractères par défaut, comptés sans les balises HTML. Un dépassement provoque un refus du webservice avec une erreur de validation.

Deux clés à droits restreints

Le webservice s’active dans Paramètres avancés > Webservice. Créez deux clés plutôt qu’une :

PATCH figure bien parmi les six méthodes que PrestaShop 9.2 enregistre comme permission (GET, PUT, POST, PATCH, DELETE, HEAD). La clé s’envoie en authentification HTTP Basic, comme nom d’utilisateur avec un mot de passe vide ; le répartiteur du webservice accepte aussi le paramètre ws_key dans l’URL, à éviter car il apparaît dans les journaux du serveur.

PUT ou PATCH : ce que montre le code de PrestaShop 9.2

Beaucoup d’exemples relisent la fiche complète, la modifient et la renvoient en PUT. La lecture de WebserviceRequest.php dans la version 9.2.0 explique pourquoi c’est fragile :

Dans les deux cas, l’élément <id> est obligatoire. Le script envoie donc un document minimal : l’identifiant et les quatre champs, pour la seule langue traitée. Le risque d’effacer un prix, une catégorie ou une autre langue disparaît.

Configurer l’environnement

Le script n’a qu’une dépendance externe, requests (pip install requests). Le XML est traité avec xml.etree.ElementTree, inclus dans Python. Les paramètres sont lus dans des variables d’environnement, jamais dans le code :

PRESTASHOP_API_URL=https://boutique.example/api
PRESTASHOP_KEY_READ=cle-lecture-exemple
PRESTASHOP_KEY_WRITE=cle-ecriture-exemple
PRESTASHOP_LANG_ID=1
PS_SHORT_DESC_LIMIT=800
GEMINI_API_KEY=cle-gemini-exemple
GEMINI_MODEL=nom-du-modele

PRESTASHOP_LANG_ID est l’identifiant de la langue visée, visible dans International > Localisation > Langues. GEMINI_MODEL reçoit le nom d’un modèle listé dans la documentation de Google au moment de la mise en place : le script n’en impose aucun. Ne versionnez ni ce fichier, ni les dossiers brouillons/ et sauvegardes/, ni journal.log.

Générer avec l’API Gemini

Le script appelle la méthode generateContent de l’API Gemini, avec la clé dans l’en-tête x-goog-api-key, comme le montre la documentation de Google. Le paramètre responseMimeType: "application/json" et un schéma de réponse imposent un objet JSON aux quatre clés attendues : pas de texte libre à découper.

La consigne interdit toute invention et demande [A VERIFIER] quand une information manque. Le modèle ne reçoit que la référence, le nom et les textes actuels : ajoutez la fiche fabricant si vos descriptions sont pauvres.

Deux points de conformité, tirés des conditions de l’API Gemini mises à jour le 28 avril 2026 : sur les services gratuits, Google utilise les contenus soumis pour améliorer ses produits ; pour des utilisateurs situés dans l’Espace économique européen, seuls les services payants sont autorisés. Utilisez donc une clé rattachée à un compte de facturation. Le tarif dépend du modèle choisi : calculez-le sur la grille de Google à partir du nombre de fiches, avant de lancer un lot.

Contrôles automatiques

La fonction controler ajoute des alertes au brouillon, sans le bloquer :

Ces contrôles ne remplacent pas la relecture. La grille de contrôle qualité d’un texte produit est dans notre article sur les descriptions générées.

Relire, puis publier par lots

Chaque brouillon est enregistré dans brouillons/{id}.json, avec les données source, le contenu proposé, les alertes et "relu": false. La relecture consiste à comparer chaque affirmation à la fiche fabricant, corriger le texte dans le fichier, puis passer relu à true. Le script ne modifie jamais ce champ.

La commande publier traite une liste de fichiers dans l’ordre. Pour chacun, elle vérifie que le brouillon est relu et rédigé pour la langue configurée, relit la fiche avec la clé d’écriture, enregistre le XML complet dans sauvegardes/, envoie le PATCH, puis relit la fiche pour comparer. À la première erreur, le lot s’arrête : les fiches suivantes ne sont pas touchées. Une pause de deux secondes sépare deux produits.

Commencez par un ou deux produits, puis une dizaine. Un lot plus large ne se lance qu’après un essai de restauration réussi sur une copie.

Restaurer une fiche

La commande restaurer lit un fichier de sauvegarde, en extrait les quatre champs pour la langue configurée et les renvoie en PATCH. Les autres champs, modifiés entre-temps dans le back-office, ne sont pas écrasés. Faites cet essai avant tout lot de production.

Le script complet

Enregistrez ce code sous fiches_ia.py. Sa syntaxe a été vérifiée avec Python 3.12 et ses fonctions de lecture, de contrôle, de publication et de restauration ont été testées sur des données fictives, avec des appels réseau simulés. Il n’a pas été exécuté contre une boutique réelle : testez-le d’abord sur une copie.

"""Brouillons de fiches produit PrestaShop par IA, relecture puis publication par lots.

Commandes :
  python fiches_ia.py generer 12 13 14
  python fiches_ia.py publier brouillons/12.json brouillons/13.json
  python fiches_ia.py restaurer sauvegardes/product-12-AAAAMMJJ-HHMMSS.xml
"""
import html
import json
import logging
import os
import re
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path

import requests

PS_URL = os.environ["PRESTASHOP_API_URL"].rstrip("/")      # ex. https://boutique.example/api
CLE_LECTURE = os.environ["PRESTASHOP_KEY_READ"]            # GET seul sur products
CLE_ECRITURE = os.environ["PRESTASHOP_KEY_WRITE"]          # GET + PATCH sur products
LANG_ID = os.environ["PRESTASHOP_LANG_ID"]                 # identifiant de la langue traitée
LIMITE_COURTE = int(os.environ.get("PS_SHORT_DESC_LIMIT", "800"))
GEMINI_URL = ("https://generativelanguage.googleapis.com/v1beta/models/"
              f"{os.environ['GEMINI_MODEL']}:generateContent")

CHAMPS = ("meta_title", "meta_description", "description_short", "description")
LIMITES = {"meta_title": 255, "meta_description": 512}
BROUILLONS, SAUVEGARDES = Path("brouillons"), Path("sauvegardes")
PAUSE = 2  # secondes entre deux produits

logging.basicConfig(filename="journal.log", level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")


def lire_produit(id_produit, cle):
    r = requests.get(f"{PS_URL}/products/{id_produit}", auth=(cle, ""), timeout=30)
    r.raise_for_status()
    return r.content


def texte_langue(produit, champ):
    noeud = produit.find(champ)
    if noeud is None:
        raise ValueError(f"Champ {champ} absent de la réponse")
    for langue in noeud.findall("language"):
        if langue.get("id") == LANG_ID:
            return langue.text or ""
    raise ValueError(f"Langue {LANG_ID} absente du champ {champ}")


def donnees_source(xml_brut):
    produit = ET.fromstring(xml_brut).find("product")
    source = {"reference": produit.findtext("reference", default=""),
              "name": texte_langue(produit, "name")}
    for champ in CHAMPS:
        source[champ] = texte_langue(produit, champ)
    return source


def appeler_gemini(consigne, essais=3):
    schema = {"type": "OBJECT",
              "properties": {c: {"type": "STRING"} for c in CHAMPS},
              "required": list(CHAMPS)}
    corps = {"contents": [{"role": "user", "parts": [{"text": consigne}]}],
             "generationConfig": {"responseMimeType": "application/json",
                                  "responseSchema": schema}}
    for essai in range(essais):
        r = requests.post(GEMINI_URL, json=corps, timeout=120,
                          headers={"x-goog-api-key": os.environ["GEMINI_API_KEY"]})
        if r.status_code in (429, 500, 503) and essai < essais - 1:
            time.sleep(10 * (essai + 1))
            continue
        r.raise_for_status()
        texte = r.json()["candidates"][0]["content"]["parts"][0]["text"]
        return json.loads(texte)
    raise RuntimeError("Gemini indisponible")


def generer_brouillon(source):
    consigne = (
        "Vous rédigez une fiche produit en français, au vouvoiement, sans superlatif. "
        "Utilisez uniquement les informations ci-dessous. N'inventez aucune "
        "caractéristique, norme, garantie, délai ni prix. Si une information manque, "
        "écrivez [A VERIFIER].\n"
        "meta_title : 60 caractères au plus. meta_description : 150 caractères au plus. "
        "description_short : 2 phrases, texte brut. description : 150 à 250 mots, "
        "HTML limité à <p>, <ul>, <li>, <strong>.\n\n"
        + json.dumps(source, ensure_ascii=False, indent=2)
    )
    return appeler_gemini(consigne)


def longueur_texte(valeur):
    return len(html.unescape(re.sub(r"<[^>]+>", "", valeur)))


def controler(brouillon, source):
    alertes = []
    for champ in CHAMPS:
        if not str(brouillon.get(champ, "")).strip():
            alertes.append(f"{champ} vide")
    for champ, limite in LIMITES.items():
        if len(brouillon.get(champ, "")) > limite:
            alertes.append(f"{champ} dépasse {limite} caractères (refusé par PrestaShop)")
    if longueur_texte(brouillon.get("description_short", "")) > LIMITE_COURTE:
        alertes.append(f"description_short dépasse {LIMITE_COURTE} caractères")
    texte = " ".join(str(v) for v in brouillon.values())
    if "[" in texte:
        alertes.append("marqueur entre crochets à traiter")
    if re.search(r"<\s*(script|iframe|style)", texte, re.I):
        alertes.append("balise interdite")
    connus = " ".join(source.values())
    for nombre in sorted(set(re.findall(r"\d+(?:[.,]\d+)?", texte))):
        if nombre not in connus:
            alertes.append(f"chiffre absent de la source : {nombre}")
    return alertes


def commande_generer(ids):
    BROUILLONS.mkdir(exist_ok=True)
    for id_produit in ids:
        source = donnees_source(lire_produit(id_produit, CLE_LECTURE))
        brouillon = generer_brouillon(source)
        alertes = controler(brouillon, source)
        fichier = BROUILLONS / f"{id_produit}.json"
        fichier.write_text(json.dumps(
            {"id_product": id_produit, "lang_id": LANG_ID, "source": source,
             "contenu": brouillon, "alertes": alertes, "relu": False},
            ensure_ascii=False, indent=2), encoding="utf-8")
        logging.info("brouillon %s : %d alerte(s)", id_produit, len(alertes))
        time.sleep(PAUSE)


def xml_patch(id_produit, contenu):
    """Document minimal : PATCH ne traite que les champs présents."""
    racine = ET.Element("prestashop")
    produit = ET.SubElement(racine, "product")
    ET.SubElement(produit, "id").text = str(id_produit)
    for champ in CHAMPS:
        langue = ET.SubElement(ET.SubElement(produit, champ), "language", id=LANG_ID)
        langue.text = contenu[champ]
    return ET.tostring(racine, encoding="utf-8", xml_declaration=True)


def envoyer_patch(id_produit, contenu):
    r = requests.request("PATCH", f"{PS_URL}/products/{id_produit}",
                         auth=(CLE_ECRITURE, ""), data=xml_patch(id_produit, contenu),
                         headers={"Content-Type": "application/xml"}, timeout=30)
    r.raise_for_status()


def commande_publier(fichiers):
    SAUVEGARDES.mkdir(exist_ok=True)
    for fichier in fichiers:
        data = json.loads(Path(fichier).read_text(encoding="utf-8"))
        id_produit = data["id_product"]
        if data.get("relu") is not True or data.get("lang_id") != LANG_ID:
            sys.exit(f"{fichier} : non relu ou langue différente, lot arrêté.")
        if any("refusé" in a for a in controler(data["contenu"], data["source"])):
            sys.exit(f"{fichier} : longueur hors limites, lot arrêté.")
        original = lire_produit(id_produit, CLE_ECRITURE)
        horodatage = datetime.now().strftime("%Y%m%d-%H%M%S")
        sauvegarde = SAUVEGARDES / f"product-{id_produit}-{horodatage}.xml"
        sauvegarde.write_bytes(original)  # une erreur ici arrête tout avant l'écriture
        try:
            envoyer_patch(id_produit, data["contenu"])
        except requests.RequestException as erreur:
            logging.error("échec %s : %s (sauvegarde %s)", id_produit, erreur, sauvegarde)
            sys.exit(f"Échec sur le produit {id_produit}, lot arrêté.")
        relu = donnees_source(lire_produit(id_produit, CLE_ECRITURE))
        if relu["meta_title"] != data["contenu"]["meta_title"]:
            logging.warning("produit %s : relecture après écriture différente", id_produit)
        logging.info("produit %s publié (sauvegarde %s)", id_produit, sauvegarde)
        time.sleep(PAUSE)


def commande_restaurer(fichier):
    nom = Path(fichier).name
    id_produit = int(re.match(r"product-(\d+)-", nom).group(1))
    source = donnees_source(Path(fichier).read_bytes())
    envoyer_patch(id_produit, {c: source[c] for c in CHAMPS})
    logging.info("produit %s restauré depuis %s", id_produit, nom)


if __name__ == "__main__":
    if len(sys.argv) < 3:
        sys.exit(__doc__)
    action, cibles = sys.argv[1], sys.argv[2:]
    if action == "generer":
        commande_generer([int(i) for i in cibles])
    elif action == "publier":
        commande_publier(cibles)
    elif action == "restaurer":
        commande_restaurer(cibles[0])
    else:
        sys.exit(__doc__)

Le journal ne contient jamais de clé ni d’en-tête HTTP : seulement la date, le produit, l’action, le nombre d’alertes et le chemin de la sauvegarde.

Après la publication

Videz le cache de PrestaShop et contrôlez quelques fiches sur mobile et sur ordinateur. Si votre catalogue alimente Google Merchant Center, les titres et descriptions créés par IA générative doivent y être transmis dans structured_title et structured_description, avec la valeur trained_algorithmic_media : vérifiez ce que fait votre module de flux. La page de Google sur le contenu généré par IA, mise à jour le 5 octobre 2026, demande de vérifier l’exactitude des titres, descriptions meta et données structurées avant publication.

Pour mesurer l’effet, notez la date de chaque lot et suivez les pages modifiées dans Google Search Console, en les comparant à des produits témoins non modifiés. Si vous préférez une interface dans le back-office avec file de brouillons, l’architecture est décrite dans créer un module PrestaShop d’IA, et vous pouvez nous décrire votre besoin dans le formulaire de demande de développement sur mesure.

Questions fréquentes

Le script peut-il publier une fiche sans relecture ?

Non. La commande publier arrête le lot dès qu’un fichier n’a pas relu à true ou vise une autre langue. Seule une personne modifie ce champ, après comparaison avec la fiche fabricant.

Pourquoi PATCH plutôt que PUT ?

Dans PrestaShop 9.2, PUT remet à vide les champs absents du XML et refuse les champs en lecture seule renvoyés par GET. PATCH ne modifie que les champs envoyés, et seulement pour les langues envoyées.

Peut-on utiliser un autre modèle que Gemini ?

Oui. Seule la fonction appeler_gemini dépend du fournisseur. Remplacez-la par l’appel de votre fournisseur, en gardant une réponse JSON aux quatre clés, et vérifiez ses conditions d’utilisation des données.

Ask AI de PrestaShop 9.2 rend-il ce script inutile ?

Pas pour un traitement par lots relu. Ask AI peut modifier des balises meta après votre accord, conversation par conversation, mais il ne gère ni brouillons, ni sauvegardes horodatées, ni descriptions longues dans un circuit de relecture. Voir notre article sur les modules d’IA pour PrestaShop.

Sources

Agence web PrestaShop & WordPress

Agence web spécialisée PrestaShop et WordPress : création, maintenance, sécurité, performance et référencement. Nos tutoriels sont issus de nos interventions sur de vraies boutiques.

L'agence Genisoft → LinkedIn →