Un service REST qui traite des factures scannées en Python

Partager cet article

Objectif

Afin de mettre en pratique ce que nous avons vu dans les articles précédents, je vous propose de créer un web service (RESTFul) en Python qui récupérera une image (scan de facture) pour la traiter et renvoyer les éléments constitutifs. Histoire de ne pas commencer trop complexe (et peut être aussi parceque je n’ai pas assez de données en entrées) nous traiterons le contenu des factures sans Machine Learning (je ne parle pas de l’OCR bien sur).

Voici les différentes étapes de traitement :

  1. Récupération de l’image (JPEG) sous forme binaire par le Web Service REST. Nous utiliserons Flask.
  2. Analyse de l’image avec un OCR. Nous utiliserons Tesseract que nous avons vu lors d’un article précédent.
  3. Récupération des éléments de facture et création d’un fichier JSON.
  4. Renvoi de la réponse (contenu JSON).

Prérequis technique :

  • Python (j’utilise ici la version 3.7). il vous faudra aussi les librairies (pytesseract, opencv, flask, json)
  • Tesseract (avec la librairie pytesseract)

Analyse de l’image facture

L’analyse de la facture (qui est fournit sous la forme d’un fichier JPEG) est opérée via Tesseract.

Voici le modèle de facture que nous allons analyser (vous pouvez aussi la télécharger sur mon git ici) :

Pour celà nous devons importer les librairies python tout d’abord puis initialiser Tesseract comme suit:

try:
    from PIL import Image
except ImportError:
    import Image
import pytesseract
import json
import cv2
import numpy as np

# Si votre installation de Tesseract n'est pas dans le path ou si l'exécutable ne se nomme pas exactement tesseract, joutez les lignes suivantes :
#pytesseract.pytesseract.tesseract_cmd = r'tesseract-4.0.0.exe'
#fichier = r'/home/monuser/git/python_tutos/tesseract/tessFactures/Facture_1.jpg'
fichier = r'Facture_2.jpg'
image = Image.open(fichier)
print(pytesseract.image_to_string(image))

Normalement vous devriez avoir comme résultat (c’est la retranscription litteral de l’image … en gros voici comment tesseract a compris le fichier JPEG) :

BLUEPRISM FACTURE

9 rue du colisée
75008 PARIS

+33 1 54 47 89 89
www.blueprism.com/fr

Compagnie d’eléectricité
3 avenue Foch

75001 PARIS
Référence: ZR8978989
Date: 06/12/2020
Client: 897695146
Intitulé: Rénovation de la tour eiffel
Quantité Désignation Prix unitaire HT — Prix total HT
45 Briquette 36 541
25 Planches de chantier 120 410
Total HT 951,00 €
TVA (20%) 190,20 €
Total TTC (en euros) 1041,20€

En votre aimable réglement,
Cordialement,

Conditions de paiement : paiement a réception de facture, a 30 jours...

Aucun escompte consenti pour reglement anticipé

Tout incident de paiement est passible d'intérét de retard. Le montant des pénalités résulte de l'application
aux sommes restant dues d'un taux d'intérét legal en vigueur au moment de I'incident.

Indemnité forfaitaire pour frais de recouvrement due au créancier en cas de retard de paiement : 40€

N° Siret 210.896.764 00015 RCS Montpellier
Code APE 947A - N° TVA Intracom. FR 77825896764000

Récupération des éléments de la facture

Nous allons à partir de marqueurs de la factures récupérer les différents éléments qui nous interressent : adresse, nom, montants, éléments de facture, total, etc.

Comme je le disais en préambule on pourrait (si on avait assez de données/factures) utiliser des algorithmes de Machine learning pour cette étape, mais pour commencer nous ferons des choses simples, partant du postulat que nous traitons ici toujours les mêmes types de factures (du même fournisseur).

Avant tout je vais créer quelques fonctions qui vont permettre de récupérer de manière générique tous les éléments :

def RemoveEmptyLines(entree):
    tab = entree.strip()
    tableausansvide = [ x for x in tab.splitlines() if x!='' ]
    res = ''
    for i in range(0, len(tableausansvide)):
        res = res + tableausansvide[i] + '\n'
    return res

def getTextBetween(mainString, startWord, endWord):
    start = mainString.find(startWord) + len(startWord)
    end = mainString.find(endWord)
    return RemoveEmptyLines(mainString[start:end])

def getPosElement(po):
    element = {}
    element['quantite'] = po[0:po.find (' ')].strip()
    po = po[po.find (' '):len(po)]
    element['prixtotht'] = po[po.rfind (' '):len(po)].strip()
    po = po[0:po.rfind (' ')]
    element['prixunitht'] = po[po.rfind (' '):len(po)].strip()
    po = po[0:po.rfind (' ')]
    element['decription'] = po.strip()
    return element

Ensuite un par un nous allons récupérer chaque élément de la facture et les placer dans un objet JSON. Les objets json dans python se gère avec une facilité déconcertante, il suffit d’affecter les variables directement et la librairie json gère toute la syntaxe en y ajoutant les {} et [] comme il faut.

    output = {}    
    resultat = pytesseract.image_to_string(image)
    output["Adresse"] = getTextBetween(resultat, 'www.blueprism.com/fr', 'Référence').strip()
    output["Reference"] = getTextBetween(resultat, 'Référence: ', 'Date: ').strip()
    output["DateFacture"] = getTextBetween(resultat, 'Date: ', 'Client: ').strip()
    output["CodeClient"] = getTextBetween(resultat, 'Client: ', 'Intitulé: ').strip()
    
    # Récupération des lignes de PO
    pos = getTextBetween(resultat, 'Prix total HT', 'Total HT ')
    tabPOs = pos.splitlines()
    print ('Nombre de PO: ' + str(len(tabPOs)))
    output["NbPo"] = len(tabPOs)
    pos = []
    for i in range(0, len(tabPOs)):
        pos.append(getPosElement(tabPOs[i]))
    output['po'] = pos
    output["totalht"] = getTextBetween(resultat, 'Total HT ', 'TVA (20%) ').strip()
    output["tva"] = getTextBetween(resultat, 'TVA (20%) ', 'Total TTC (en euros) ').strip()
    output["total"] = getTextBetween(resultat, 'Total TTC (en euros) ', 'En votre aimable réglement,').strip()

L’objet json (output) est donc bien créé à partir de la facture, regardons le résultat :

print (output)
{'Adresse': 'Compagnie des eaux\n2 rue de la foret\n45879 BOIS VILLIERS', 'CodeClient': '98908908', 'DateFacture': '12/12/2020', 'NbPo': 1, 'Reference': 'ZR980980', 'po': [{'decription': "tomette — Réf 'Toscane blanc' (20*20)", 'prixtotht': '276', 'prixunitht': '23', 'quantite': '12'}], 'total': '331,20 €', 'totalht': '276,00 €', 'tva': '55,20€'}

Création du service REST

Maintenant nous allons rendre cela accessible via appel de web service. Pour celà nous allons utiliser Flask avec pour particularité que nous enverrons l’image au format binaire au service pour récupérer le contenu json précédent. Créons un fichier python (fichier.py) qui va contenir le code du service.

Ajoutons-y la route principale et précisons lui le mode POST.

@app.route('/facture', methods=['POST'])

Récupérons ensuite le flux binaire de l’image (envoyé en POST) :

r = request
# convert string of image data to uint8
nparr = np.frombuffer(r.data, np.uint8)
# decode image
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

N’oublions pas l’encodage à la fin du traitement pour renvoyer un flux json correct :

# Prepare respsonse, encode JSON to return
response_pickled = jsonpickle.encode(output)
return Response(response=response_pickled, status=200, mimetype="application/json")

Voilà à quoi celà doit ressembler :

from flask import Flask, request, Response
try:
    from PIL import Image
except ImportError:
    import Image
import pytesseract
import json
import jsonpickle
import numpy as np
import cv2

app = Flask(__name__)

# fonctions ici ...

@app.route('/')
def index():
    return "Lecture de fichier de factures"

@app.route('/facture', methods=['POST'])
def order():
    r = request
    # convert string of image data to uint8
    nparr = np.frombuffer(r.data, np.uint8)
    # decode image
    image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    
    output = {}    
    # ...
    
    # Preprare respsonse, encode JSON to return
    response_pickled = jsonpickle.encode(output)
    return Response(response=response_pickled, status=200, mimetype="application/json")

if __name__ == '__main__':
    app.run(debug=True, host='127.0.0.1', port=8080)

Vous pouvez télécharger le code complet ici.

Testons notre service

Pour tester le service je vous conseille d’utiliser un Jupyter Notebook (voici le miens ici). Tapez le code suivant :

import requests
import json
import cv2

addr = 'http://localhost:8080'
url = addr + '/facture'

# prepare headers for http request
headers = {'content-type': 'image/jpeg'}
fichier = r'/home/benoit/git/python_tutos/tesseract/tessFactures/Facture_1.jpg'

img = cv2.imread(fichier)

# encode image as jpeg
_, img_encoded = cv2.imencode('.jpg', img)

Ensuite lancez le service flask via un terminal/console. Vous devez avoir quelque chose qui ressemble à ceci si votre serveur flask se lance bien :

(base) [email protected]:~/git/python_tutos/tesseract/tessFactures$ python tessfacturews2.py
 * Serving Flask app "tessfacturews2" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 176-379-994
 * Detected change in '/home/benoit/git/python_tutos/tesseract/tessFactures/tessfacturews2.py', reloading
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 176-379-994

Maintenant testons notre service en lui envoyant l’image :

# send http request with image and receive response
response = requests.post(url, data=img_encoded.tostring(), headers=headers)

# decode response
print(json.loads(response.text))

Si tout s’en encore bien passé vous devez récupérer :

{'Adresse': 'Compagnie des eaux\n2 rue de la foret\n45879 BOIS VILLIERS', 'CodeClient': '98908908', 'DateFacture': '12/12/2020', 'NbPo': 1, 'Reference': 'ZR980980', 'po': [{'decription': "tomette — Réf 'Toscane blanc' (20*20)", 'prixtotht': '276', 'prixunitht': '23', 'quantite': '12'}], 'total': '331,20 €', 'totalht': '276,00 €', 'tva': '55,20€'}

Ça donne quoi avec un fichier pdf ?

C’est chose courante quand on traite des factures. On peut tout aussi bien récupérer des fichiers pdf à la place d’images scannées. Dans ce cas, il faut gérer en plus de la différence de format la notion de pagination qui n’existe pas dans une image.

Pour ce qui est du traitement des pdf, je veux bien sur parler de la conversion pdf -> image, je vous renvoi à l’article sur tesseract avancé. En ce qui concerne nous allons créer une nouvelle route pour notre web service dans Flask afin de traiter notre fichier pdf :

@app.route('/pdf', methods=['POST'])
def pdf():
    r = request.data
    output = {}
    pytesseract.pytesseract.tesseract_cmd = r'tesseract-4.0.0.exe'
    content = convert_from_bytes(r)
    pages = []

    output['Nb Pages'] = len(content)
    print ("Nombre de pages: " + str(len(content)))

    for i in range(len(content)):
        pages.append(pytesseract.image_to_string(content[i]))

    output['Pages'] = pages
   
    # Preprare response, encode JSON to return
    response_pickled = jsonpickle.encode(output)
    return Response(response=response_pickled, status=200, mimetype="application/json")

Et voila le tour est joué, vous pouvez maintenant envoyer des fichiers pdf qui seront traités par tesseract. le résultat étant renvoyé sous la forme d’un fichier JSON (dans lequel vous trouverez un tableau Pages avec le contenu de chaque page).

N’hésitez pas à tester d’autres factures, j’en ai mis 3 sur github. D’ailleurs vous trouverez comme d’habitude tous le code et fichiers nécessaires à télécharger ici (répertoire git/les-tutos-datacorner.fr/vision-par-ordinateur/tessFactures).

Partager cet article

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.

deux × un =

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.