Le format PDF (Portable Document Format) est un format de fichier largement utilisé dans la vie quotidienne. Toutefois, il n’est pas pratique dans le cadre de l’analyse de données car il est nécessaire de formater le fichier dans des formats plus compatibles avec notre langage de programmation (ex. json).

L’ETL (Extract Transform Load) d’un fichier PDF passe souvent par un processus d’analyse syntaxique. La méthode classique pour récupérer des informations depuis un fichier PDF est d’utiliser des expressions régulières et elle est utilisé par Deepki actuellement. Grâce à cette méthode, le PDF est transformé en un fichier texte dont le contenu est considéré comme une longue chaîne de caractères. Il est ensuite possible de manipuler cette donnée et de récupérer les informations à l’aide du module Python re (pour regular expression ou regex).

Utiliser des expressions régulières fonctionne bien sur du texte simple et homogène tel que des articles de presse mais les PDFs traités par Deepki sont souvent plus complexes. Prenons le cas d’une facture d’électricité par exemple ; le fichier contiendra des tableaux, des images (logos) et des parties de texte non regroupées (titres, en-têtes, dates, numéros de téléphone…).

Pour ce genre de PDF, la récupération d’informations avec des expressions régulières pourrait être très lourde et compliquée : il faut maintenir à la main un grand nombre d’expressions complexes et ces expressions complexes existantes ne seront pas utiles pour les nouveaux modèles de PDF.

Je propose donc une autre méthode pour économiser du travail : on peut regarder le PDF comme un plan contenant les informations coordonnées et essayer de parser le fichier selon sa composition.

Le PDF peut être considéré comme la combinaison de plusieurs composants quadrangulaires. Un composant quadrangulaire est par exemple un paragraphe de texte, un tableau… on peut donc parser le contenu en analysant plusieurs composants séparément.

Il existe aujourd’hui beaucoup de packages python pour extraire des tableaux depuis un fichier PDF (pdfplumber, tabula-py, PyPDF2 ou bien pdftotree). Je vais utiliser pdfplumber dans cet article.

Je me suis inspirée par le concept de machine learning: unsupervised learning et supervised learning. Il y a deux façons différentes pour les extractions de données selon notre connaissance du fichier PDF : récupération des tableaux et récupération selon les coordonnées relatives.

Récupération des tableaux (parser sans objectif spécifique)

Si on n’a pas de connaissance sur le PDF, ou on ne cible pas les variables, on peut essayer de récupérer toutes les informations sous forme de tableaux.

import pandas as pd
import pdfplumber
path = 'exemple.pdf'

pdf = pdfplumber.open(path)

for page in pdf.pages:
    for table in enumerate(page.extract_tables()):
        print(pd.DataFrame(table))

une partie du résultat affiché :

0  \ 1       Abonnementélectricité(HT) Période PrixunitaireHT TauxdeTVA 38,12¤
2                                                              Abonnement 3  Consommation(HT) Période Conso4193kWh
PrixunitaireHT TauxdeTVA 366,87¤ 4                                                ElectricitéHeurespleines
5                                                ElectricitéHeurescreuses

1        2     3            4       5        6 1                      None     None  None         None    None     None
2  du01/12/2016au31/03/2017           None   9,53¤/mois   5,50%   38,12¤
3                      None     None  None         None    None     None
4  du29/11/2016au29/01/2017  2827kWh  None  9,460c¤/kWh  20,00%  267,43¤
5  du29/11/2016au29/01/2017  1366kWh  None  7,280c¤/kWh  20,00%   99,44¤

En voyant deux tableaux dans un data frame, on peut utiliser l’option de extract_tables pour différentes extractions. Il est même possible de modifier directement ce package selon notre besoin.

Sinon, Pandas peut aussi nous aider à améliorer notre résultat :

df = df.rename(columns={i: 'col' + str(i) for i in df.columns})
df = df[[c for c in df.columns if not df[c].apply(lambda x: pd.isnull(x) or (x == ''), 1).all()]]
df = df.assign(**{'col0': df['col0'].apply(lambda x: x.split(' ') if x is not None else '')})
df = df.apply(lambda x: x.split(' ') if ' ' in x else x)

table_header = df['col0'].apply(pd.Series)
df['col0'] = df['col0'].apply(lambda x: x[0] if isinstance(x, list) else x)
df = df[1:]
df.columns = table_header.loc[0, :]
col0                      col1          col2  \ 0          Consommation(HT)                   Période  Conso4193kWh
1  ElectricitéHeurespleines  du29/11/2016au29/01/2017       2827kWh
2  ElectricitéHeurescreuses  du29/11/2016au29/01/2017       1366kWh

col4       col5     col6 0  PrixunitaireHT  TauxdeTVA  366,87¤ 1     9,460c¤/kWh     20,00%  267,43¤
2     7,280c¤/kWh     20,00%   99,44¤

Cas particulier

Il arrive que le package regroupe deux colonnes dans une seule, il est alors nécessaire de retraiter la colonne :

Dans ce cas, on obtient après l’extraction un tableau avec seulement 1 colonne :

1 0                                                           None 1  MontantHorsTVA
542,48¤\nMontantTVA(payéesurlesdébits) 102,53¤ 2                                             FactureTTC 645,01¤

A l’aide de Pandas :

df = df.assign(**{'col': df['col'].apply(lambda x: x.split("\n") if x is not None else x)})
df = df.merge(df['col'].apply(pd.Series), left_index=True, right_index=True).drop('col', 1).reset_index(drop=True)
print({x.split(' ')[0]: x.split(' ')[1] for x in df[0].append(df[1]) if pd.notnull(x)}) # index for the key, index 2 for the value
{ "MontantHorsTVA": "542,48¤", "FactureTTC": "645,01¤", "MontantTVA(payéesurlesdébits)": "102,53¤" }

Système de coordonnées cartésiennes (plan avec label)

Dans le cas où l’on connaît les mots que nous cherchons dans un PDF et où l’on veut trouver leurs valeurs, il est possible de récupérer les valeurs selon leur position relative avec un mot clé.

Les PDFs sont utilisés par les humains pour comprendre des informations. Par conséquent nous accédons souvent à l’information recherchée en se repérant avec des mots contenus dans le fichier. Par exemple ci-dessous nous recherchons un total :

Il est donc possible d’extraire les mots en utilisant leur position dans le fichier:

for i, page in enumerate(pdf.pages):
    for p in page.extract_words():
        print(p)
{
    'x0': Decimal('39.539'),
    'x1': Decimal('88.255'),
    'top': Decimal('381.566'),
    'bottom': Decimal('387.763'),
    'text': 'TotalHorsTVApourcesite'
}

Légende :

  • x0 : Distance entre le côté gauche du caractère et le côté gauche de la page.
  • x1 : Distance du côté droit du caractère du côté gauche de la page.
  • top : Distance entre le haut du caractère et le haut de la page.
  • bottom : Distance entre le bas du caractère et le haut de la page

Cet exemple :

y = df.loc[df.text == 'TotalHorsTVApourcesite', 'top'].tolist()[0]
x = df.loc[df.text == 'TotalHorsTVApourcesite', 'x0'].tolist()[0]
v = df.loc[df.top == y]
v = v.assign(x0=v['x0'].astype(float) - float(x))
v = v.loc[v.x0 != 0]
print(v.loc[v['x0'] == v['x0'].min()].text)

nous retourne :

542,48¤

De plus, il est aussi possible de trouver une valeur (une information, un nombre…) en utilisant comme repère (indice) le point d’intersection entre deux mots-clés (horizontal et vertical) lorsque la valeur se trouve dans un tableau :

Pour obtenir le prix de de l’Abonnement dans cet exemple:

y = df.loc[df.text =='Abonnement','top'].tolist()[0]
x = df.loc[df.text == 'PrixunitaireHT', 'x1'].tolist()[0]
v = df.loc[(df.top ==y)]
h = v.loc[abs(df.x1.astype(float) - float(x)) < 10]
print(h.text)

on obtient la valeur :

9,53¤/mois

Conclusion

Cette méthode permet d’économiser beaucoup de travail, particulièrement avec la tendance de données massives. Pour améliorer la performance de parsing de cette méthode, on pourrait améliorer le package d’extraction des tableaux afin qu’il puisse positionner et structurer les tableaux plus précisément, on pourrait aussi entraîner notre algorithme afin qu’il puisse analyser des données sources plus compliquées ou produire des résultats plus adaptés. J’ai utilisé ‘pdfplumber’ dans cet article et il sera très intéressant d’explorer les autres packages python pour extraire des tableaux ou réaliser les fonctions similaires.