ticket caisse python extraire données

Ticket de caisse et Python: extraire les données

Comment extraire les données d’un ticket de caisse à l’aide Python ? C’est la question que je me suis posée alors que je souhaitais analyser l’évolution des prix des articles que j’ai l’habitude d’acheter dans le supermarché à côté de chez moi.

Ticket de caisse et Python: préliminaires

Au début, comme tout le monde je crois, j’ai fait quelques recherches sur internet sur la façon dont on pouvait extraire les données d’un ticket de caisse.

Je suis tombé sur des pages et des articles parlant pour la plupart du module tesseract. J’ai donc voulu le tester.

Malheureusement, les multiples essais ne furent pas concluant du tout… Ils étaient même à chier! Et pour que je dise cela, c’est que c’était réellement le cas…

Et puis, à force de chercher, je suis tombé sur un site plutôt intéressant: https://asprise.com/royalty-free-library/python-ocr-api-overview.html

Je ne vais pas vous mentir, je n’ai pas encore testé le module présenté sur cette dernière page (asprise_ocr), mais j’ai testé avec succès une API de asprise.

C’est la solution que je vais vous présenter ici.

Ticket de caisse et Python: le projet

Scanner le ticket de caisse

Bien évidemment, il faut avant tout scanner (ou prendre en photo, je pense que ça fonctionnerait bien aussi) le ticket de caisse.

Le mien est le suivant:

ticket caisse Python extraire données
Mon ticket de caisse cobaye

Extraire les données

Je vais donc passer par l’API de asprise (promis, je n’ai pas d’actions chez eux !… Non, je dis ça parce que c’est tout de même la quatrième fois que j’écris ce nom).

Cette API extrait au format json les données.

import requests

try:
    with open('ticket.json','r') as f:
        data = f.read()
    #pass
except IOError:
    receiptOcrEndpoint = 'https://ocr.asprise.com/api/v1/receipt'
    imageFile = "ticket.png" # nom du scan du ticket
    r = requests.post(receiptOcrEndpoint, data = { \
        'client_id': 'TEST', \
        'recognizer': 'auto', \
        'ref_no': 'ocr_python_123', \
    }, \
    files = {"file": open(imageFile, "rb")})
    
    with open('ticket.json' , 'w' , encoding='utf-8') as f:
        f.write( r.text )
    
    data = r.text

Il a fallu ici que j’use d’astuces car l’API ne permet pas de dépasser un certain quota de scans par heure. Comme à mes débuts, je tâtonnais, j’ai bien entendu dépassé ce quota! Il a donc fallu que je sauvegarde les résultats du scan, qui ne changeaient pas d’une fois à l’autre.

Bon, voilà une bonne chose de faite! Je fichier ainsi obtenu est le suivant:

{
  "ocr_type" : "receipts",
  "request_id" : "******************",
  "ref_no" : "ocr_python_123",
  "file_name" : "ticket.png",
  "request_received_on" : 1647708136937,
  "success" : true,
  "image_width" : 1766,
  "image_height" : 3624,
  "image_rotation" : 0,
  "recognition_completed_on" : 1647708137948,
  "receipts" : [ {
    "merchant_name" : "*MAGGI SOUPE DESHY:.",
    "merchant_address" : null,
    "merchant_phone" : null,
    "merchant_website" : null,
    "merchant_tax_reg_no" : null,
    "merchant_company_reg_no" : null,
    "region" : null,
    "mall" : null,
    "country" : "FR",
    "receipt_no" : null,
    "date" : "2035-03-02",
    "time" : null,
    "items" : [ {
      "amount" : 3.35,
      "description" : "MMM SSON SAVOIE NO..2",
      "flags" : "",
      "qty" : null,
      "remarks" : null,
      "unitPrice" : null
    }, {
      "amount" : 1.67,
      "description" : "AUC ESS JBON SUP D..2",
      "flags" : "",
      "qty" : null,
      "remarks" : null,
      "unitPrice" : null
    }, {
      "amount" : 4.45,
      "description" : "MILKA PATE A TARTIN..2",
      "flags" : "",
      "qty" : null,
      "remarks" : null,
      "unitPrice" : null
    }, {
      "amount" : 2.09,
      "description" : "PETIT NAVIRE MAQUE..2",
      "flags" : "",
      "qty" : null,
      "remarks" : null,
      "unitPrice" : null
    }, {
      "amount" : 30,
      "description" : "AUCHAN CREME FL",
      "flags" : "",
      "qty" : null,
      "remarks" : null,
      "unitPrice" : null
    } ],
    "currency" : "EUR",
    "total" : null,
    "subtotal" : null,
    "tax" : null,
    "service_charge" : null,
    "tip" : null,
    "payment_method" : null,
    "payment_details" : null,
    "credit_card_type" : null,
    "credit_card_number" : null,
    "ocr_text" : " *MAGGI SOUPE DESHY:.\n *LAITIERE MOUSSE CH..\n TASSIMO L OR CAFE L.\n *MMM SSON SAVOIE NO..    2*3,35\n AUC OEUFS DJP SANS\n *AUC ESS JBON SUP D..    2*1,67\n *MADRANGE PALET DIA..\n *CHARAL CARPACCIO B..\n *BONDUELLE CAROT RA..\n MILKA PATE A TARTIN..    2*4,45\n *JOCKEY STRACCIATEL..\n * AUCHAN POELEE CAMP..\n *LOTUS GAUFRES DE L..\n *AUCHAN POELEE THAI..\n *AUCHAN TAGLIATELLE..\n *MOUSLINE PUREE NAT..\n BONNE MAMAN FRAMBOI..\n AUCHAN CORNICHONS F..\n *JACQ P MAXI TR NAT..\n *AUC BRIOCHE TRANCH..\n *LOTUS GAUFRES DE L..\n * AUC BRIOCHE TRANCH.\n *MAGGI SOUPE DESHY..\n VAHINE VANILL ARTIF..\n *PETIT NAVIRE MAQUE..     2*2,09\n *AUC YAOURT LES MIX..\n *MONT BLANC CREME D..\n *AUCHAN CREME FL 30.\n *AUCHAN POIS CHICHE..\n *LE VIENNOIS CAFE 4..\n *VIENNOIS VANILLE C..\n *POULET ROTI ISSU D..\n *LAITIERE POT CREME..\n *COEUR DE LAITUE X3..\n *MAGGI SOUPE TOMATE..\n AUCHAN LARDONS NATU..",
    "ocr_confidence" : 96.76,
    "width" : 1248,
    "height" : 3551,
    "avg_char_width" : null,
    "avg_line_height" : null,
    "source_locations" : {
      "date" : [ [ {
        "x" : 1020,
        "y" : 324
      }, {
        "x" : 1274,
        "y" : 324
      }, {
        "x" : 1274,
        "y" : 410
      }, {
        "x" : 1020,
        "y" : 410
      } ] ],
      "doc" : [ [ {
        "x" : -35,
        "y" : -149
      }, {
        "x" : 1337,
        "y" : -149
      }, {
        "x" : 1337,
        "y" : 3756
      }, {
        "x" : -35,
        "y" : 3756
      } ] ],
      "merchant_name" : [ [ {
        "x" : -13,
        "y" : 24
      }, {
        "x" : 896,
        "y" : 24
      }, {
        "x" : 896,
        "y" : 110
      }, {
        "x" : -13,
        "y" : 110
      } ] ]
    }
  }, {
    "merchant_name" : "1,56",
    "merchant_address" : null,
    "merchant_phone" : null,
    "merchant_website" : null,
    "merchant_tax_reg_no" : null,
    "merchant_company_reg_no" : null,
    "region" : null,
    "mall" : null,
    "country" : "FR",
    "receipt_no" : null,
    "date" : null,
    "time" : null,
    "items" : null,
    "currency" : "EUR",
    "total" : null,
    "subtotal" : null,
    "tax" : null,
    "service_charge" : null,
    "tip" : null,
    "payment_method" : null,
    "payment_details" : null,
    "credit_card_type" : null,
    "credit_card_number" : null,
    "ocr_text" : "  1,56\n  1,53\n 6,05\n 6,70\n  1,97\n 3,34\n 3,99\n  5,10\n 2,25\n 8,90\n  1,93\n  3,39\n  3,17\n  4,06\n  4,06\n  1,10\n  2,37\n  1,82\n  1,99\n  2,10\n  3,17\n  2,10\n  1,56\n  2,85\n  4,18\n  2,76\n  2,17\n  2,33\n  0,61\n  1,26\n  1,52\n  8,19\n  1,85\n  1,00\n  1,30\n  2,55",
    "ocr_confidence" : 97.00,
    "width" : 284,
    "height" : 3558,
    "avg_char_width" : null,
    "avg_line_height" : null,
    "source_locations" : {
      "doc" : [ [ {
        "x" : 1479,
        "y" : -164
      }, {
        "x" : 1792,
        "y" : -164
      }, {
        "x" : 1792,
        "y" : 3748
      }, {
        "x" : 1479,
        "y" : 3748
      } ] ],
      "merchant_name" : [ [ {
        "x" : 1544,
        "y" : 16
      }, {
        "x" : 1702,
        "y" : 10
      }, {
        "x" : 1706,
        "y" : 111
      }, {
        "x" : 1548,
        "y" : 117
      } ] ]
    }
  } ]
}

On est d’accord: c’est carrément indigeste! Il faut donc demander à Python de trier tout ça…

Extraction des données utiles

Il y a pas mal de choses inutiles dans le fichier json, et en plus, les données ne sont pas toutes correctes.

Mais fort heureusement, les données utiles sont justes, et se trouvent dans les champs “ocr_text”.

Comme on doit analyser un fichier json, je vais importer le module Python éponyme.

import json

obj_python = json.loads(data)

liste_articles = list()
liste_prix = list()
tmp = True

for key,value in obj_python.items():
    if key == 'receipts':
        for i in value:
            for cle,valeur in i.items():
                if cle == 'ocr_text':
                    if tmp:
                        liste_articles = valeur.split('\n')
                        tmp = False
                    else: liste_prix = valeur.split('\n')

liste = {}

for i in range( len(liste_articles) ):
    if liste_articles[i][1] == '*':
        cle = liste_articles[i][2:]
    else:
        cle = liste_articles[i][1:]
        
    L = cle.split('  ')
    if len(L) < 2:
        liste[ cle ] = liste_prix[i].replace(' ','')
    else:
        LL = L[-1].split('*')
        prix = LL[-1]
        cle = L[0]
        liste[ cle ] = prix
        
for key,value in liste.items():
    print('{:<25} : {:<5}'.format(key,value))

Là, j’ai vachement bidouillé quand-même! Tout ça pour arriver à ceci:

MAGGI SOUPE DESHY:.       : 1,56 
LAITIERE MOUSSE CH..      : 1,53 
TASSIMO L OR CAFE L.      : 6,05 
MMM SSON SAVOIE NO..      : 3,35 
AUC OEUFS DJP SANS        : 1,97 
AUC ESS JBON SUP D..      : 1,67 
MADRANGE PALET DIA..      : 3,99 
CHARAL CARPACCIO B..      : 5,10 
BONDUELLE CAROT RA..      : 2,25 
MILKA PATE A TARTIN..     : 4,45 
JOCKEY STRACCIATEL..      : 1,93 
 AUCHAN POELEE CAMP..     : 3,39 
LOTUS GAUFRES DE L..      : 3,17 
AUCHAN POELEE THAI..      : 4,06 
AUCHAN TAGLIATELLE..      : 4,06 
MOUSLINE PUREE NAT..      : 1,10 
BONNE MAMAN FRAMBOI..     : 2,37 
AUCHAN CORNICHONS F..     : 1,82 
JACQ P MAXI TR NAT..      : 1,99 
AUC BRIOCHE TRANCH..      : 2,10 
 AUC BRIOCHE TRANCH.      : 2,10 
MAGGI SOUPE DESHY..       : 1,56 
VAHINE VANILL ARTIF..     : 2,85 
PETIT NAVIRE MAQUE..      : 2,09 
AUC YAOURT LES MIX..      : 2,76 
MONT BLANC CREME D..      : 2,17 
AUCHAN CREME FL 30.       : 2,33 
AUCHAN POIS CHICHE..      : 0,61 
LE VIENNOIS CAFE 4..      : 1,26 
VIENNOIS VANILLE C..      : 1,52 
POULET ROTI ISSU D..      : 8,19 
LAITIERE POT CREME..      : 1,85 
COEUR DE LAITUE X3..      : 1,00 
MAGGI SOUPE TOMATE..      : 1,30 
AUCHAN LARDONS NATU..     : 2,55 

Avouez que c’est pas dégueu quand-même hein ?

Comme on intention est d’exploiter les prix à des fins de comparaison, je vais transformer les chaînes de caractères donnant les prix en float:

for key,value in liste.items():
    liste[ key ] = float( value.replace(',','.') )

Sauvegarde dans une base de données des données du ticket avec Python

Création de la base de données et de la table des produits

Voilà! J’ai un dictionnaire avec mes prix. Il ne reste plus qu’à les enregistrer dans une base de données sqlite ou MySql. Pour moi, ce sera MySql car j’utilise MySql dans le cadre de mon travail. J’ai donc plus l’habitude… Mais sinon, sqlite3 est très bien!

conn = mysql.connector.connect(host='localhost', user='root', password='')
cursor = conn.cursor()

# on se connecte à la base de données

def create_database(cursor):
    try:
        cursor.execute(
            "CREATE DATABASE auchan DEFAULT CHARACTER SET 'utf8'")
    except mysql.connector.Error as err:
        print("Failed creating database: {}".format(err))
        exit(1)

try:
    cursor.execute("USE auchan")
except mysql.connector.Error:
    create_database(cursor)
    conn.database = 'auchan'

# On crée la table 'produits' si elle n'existe pas

table = (
    "CREATE TABLE IF NOT EXISTS `produits` ("
    "  `idProduit` int(3) NOT NULL AUTO_INCREMENT,"
    "  `description` varchar(25) UNIQUE,"
    "  PRIMARY KEY (`idProduit`) "
    ") ENGINE=InnoDB")

cursor.execute(table)

# On insère les produits s'ils n'existent pas

for key,value in liste.items():
    request = ("INSERT IGNORE INTO produits (description) VALUES ('{}')".format(key))
    cursor.execute(request)
    conn.commit()
    
cursor.close()
conn.close()
ticket python base de données
Table des produits obtenue

Créations de la table des prix

Il faut maintenant insérer tous les prix dans une autre table, que l’on va nommer “prix”.

# création de la table des prix

prix = (
    "CREATE TABLE IF NOT EXISTS `prix` ("
    "  `idPrix` int(3) NOT NULL AUTO_INCREMENT,"
    "  `date` date NOT NULL,"
    "  `idProduit` int(3) NOT NULL,"
    "  `valeur` decimal(3,2) NOT NULL,"
    "  UNIQUE (`date`,`idProduit`) , PRIMARY KEY (`idPrix`)"
    ") ENGINE=InnoDB")

cursor.execute(prix)

# insertion des prix

for key, value in liste.items():
    request = ("INSERT IGNORE INTO prix (date,idProduit,valeur) VALUES (DATE(NOW()), (SELECT (idProduit) FROM produits WHERE description = '{}'),{})".format(key,value))
    cursor.execute(request)
    conn.commit()

Et voilà ! Notre table est prête!

base de données prix python ticket
Table des prix obtenue

Remarque sur les doublons

Vous avez sans doute remarqué qu’à la création des tables, j’ai utilisé le mot-clé UNIQUE; et vous avez aussi remarqué que lors de l’insertion des données, j’ai utilisé le mot-clé IGNORE.

Ceci est pour éviter d’insérer des doublons. Il serait en effet dommage d’avoir plusieurs entrées identiques.

Laisser un commentaire