Geneanet récupère régulièrement des fichiers PDF à distance qui n’ont pas vocation à être conservés ; on souhaite seulement obtenir leur texte. En effet, il peut y avoir plusieurs composantes dans le fichier ; dans notre cas le plus courant, il y a une partie image et une partie texte, cette première étant la numérisation d’un ouvrage papier, la seconde étant la version texte de cette image, extraite par un OCR. Seule la partie texte nous intéresse.

Ce texte va ensuite être utilisé pour la bibliothèque de Geneanet, qui permet de trouver des individus et ensuite d’obtenir la source originelle.

Cependant, nous souhaitons pouvoir télécharger ces fichiers le plus rapidement possible, et économiser les ressources des sites distants. Nous avons déjà constaté que dans certains cas, les temps d’extraction pouvaient s’étendre sur de très longues périodes.

Théorie

On se dit que si le texte n’est présent que dans certaines zones du fichier, on peut ne télécharger que celles-ci grâce à des requêtes HTTP de type range.

Illustration de la théorie

Test

On va d’abord chercher à confirmer notre intuition. Pour cela, nous avons utilisé deux librairies en Python capables d’extraire le texte d’un fichier PDF : PDFMiner et PyPDF2.

Ces librairies s’attendent à un objet file classique, mais on peut simplement créer une classe proxy qui regardera si le fichier est lu complètement ou en partie.

Diagramme FileProxy

import numpy

class FileProxy(object):
    def __init__(self, realfp, bitmap):
        self.realfp = realfp
        self.bitmap = bitmap

    def close(self):
        pass

    def seek(self, *args, **kwargs):
        self.realfp.seek(*args, **kwargs)

    def tell(self):
        return self.realfp.tell()

    def read(self, size):
        pos = self.tell()
        for i in xrange(pos, min(pos + size, len(self.bitmap))):
            self.bitmap[i] = True
        return self.realfp.read(size)

realfile = 'myfile.pdf'
size = os.path.getsize(realfile)
with open(realfile, 'rb') as realfp:
    bitmap = numpy.zeros(size, dtype=bool)
    fp = FileProxy(realfp, bitmap)

Ici, on utilise un bitmap fourni par numpy afin d’avoir une structure beaucoup plus rapide à analyser et plus légère en mémoire. Chaque octet lu sera marqué à True, le reste à False.

Pour compter le nombre d’octets lus, il suffit d’appeler sum(bitmap).

Voici les statistiques sur le nombre d’octets lus sur quatre fichiers d’origines différentes :

Librairie Fichier 1 Fichier 2 Fichier 3 Fichier 4
PDFMiner 100% 100 % 100% 100%
PyPDF2 1% - mauvaise extraction 1% 46% - crash 6%

Aucun résultat n’est satisfaisant. La librairie qui lit partiellement les fichiers ne donne pas toujours de bons textes, et celle qui en donne lit toujours le fichier complet. Cependant, cela nous indique que l’on peut chercher plus loin.

En production, nous utilisons la librairie Poppler. Seulement, cela nécessite actuellement de lancer un exécutable sur lequel on n’a pas la main. Plutôt que de se plonger dans ses centaines de milliers de lignes de code, on va utiliser une possibilité qu’offre le noyau Linux : la création de systèmes de fichiers en userspace (FUSE).

Diagramme FUSE

Librairie Fichier 1 Fichier 2 Fichier 3 Fichier 4
PyPDF2 1% - mauvaise extraction 1% 46% - crash 6%
Poppler 33% 54% 100% 20%

Les résultats obtenus sont plutôt concluants.

La réalité

Seulement, ce n’est pas suffisant, il nous faut aussi estimer le nombre de requêtes nécessaires.

Pour cela, on va faire une cartographie des octets lus. Voici deux exemples pertinents :

Carte 1

Carte 2

On peut voir que le premier cas va demander beaucoup trop de requêtes.

Pour pallier à cela, on va lire plus qu’il n’est demandé par le programme, ce qui est une technique déjà utilisée pour les optimisations liées aux accès disque. Cela permet de réduire le nombre d’accès au total, sans forcément lire beaucoup plus.

Voici le pourcentage du fichier lu et le nombre de zones contiguës :

Blocs Fichier 1 Fichier 2 Fichier 3 Fichier 4
4K 33% / 408 54% / 262 46% / 1 20% / 3
16K 79% / 50 54% / 34 100% / 1 23% / 2

Les résultats à 16K sont les plus intéressants, car même pour le premier fichier le nombre de requêtes aurait été bien trop haut.

La réalité encore plus réelle

Tout d’abord, nous préférons Poppler en mode raw en production. Voici les résultats une fois ce mode demandé :

Blocs Fichier 1 Fichier 2 Fichier 3 Fichier 4
raw 4K 100% / 1 100% / 1 100% / 1 20% / 3
raw 16K 100% / 1 100% / 1 100% / 1 23% / 2

Pour nos premiers fichiers, il faut donc soit abandonner le mode raw, soit le téléchargement partiel.

Enfin, nous avons supposé que Poppler ne fait que lire du début vers la fin ; tout retour en arrière demanderait des optimisations plus complexes que de simplement lire en avance le fichier. Les zones contiguës représentent le nombre minimum de requêtes, mais pas le maximum !

Il serait possible que le programme souhaite d’abord lire une certaine zone du fichier, puis une située plus en avant, comme illustré ici : Diagramme ordre de lecture possible

Nous avons donc compté les retour en arrière effectués par Poppler :

Mode Fichier 1 Fichier 2 Fichier 3 Fichier 4
  70 8 0 2
raw 70 8 0 2

C’est plutôt raisonnable.

Conclusion

Nous avons montré qu’il est possible de faire du téléchargement partiel de PDF afin d’en extraire seulement son texte. Il est possible de rendre le fonctionnement plus adapté à HTTP en réduisant le nombre de requêtes et téléchargeant un peu plus.

Cependant, cette possibilité dépend fortement du style de fichier PDF rencontré, il faut donc bien analyser en avance pour éviter d’implémenter une solution contre-productive.

Commentaires