web-dev-qa-db-fra.com

Comment extraire du texte et des coordonnées de texte à partir d'un fichier PDF?

Je veux extraire toutes les zones de texte et les coordonnées des zones de texte d'un fichier PDF avec PDFMiner.

De nombreux autres messages Stack Overflow expliquent comment extraire tout le texte de manière ordonnée, mais comment puis-je faire l'étape intermédiaire pour obtenir le texte et les emplacements de texte?

Étant donné un fichier PDF, la sortie devrait ressembler à:

   489, 41,  "Signature"
   500, 52,  "b"
   630, 202, "a_g_i_r"
21
pnj

Les retours à la ligne sont convertis en traits de soulignement dans la sortie finale. C'est la solution de travail minimale que j'ai trouvée.

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfpage import PDFTextExtractionNotAllowed
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfdevice import PDFDevice
from pdfminer.layout import LAParams
from pdfminer.converter import PDFPageAggregator
import pdfminer

# Open a PDF file.
fp = open('/Users/me/Downloads/test.pdf', 'rb')

# Create a PDF parser object associated with the file object.
parser = PDFParser(fp)

# Create a PDF document object that stores the document structure.
# Password for initialization as 2nd parameter
document = PDFDocument(parser)

# Check if the document allows text extraction. If not, abort.
if not document.is_extractable:
    raise PDFTextExtractionNotAllowed

# Create a PDF resource manager object that stores shared resources.
rsrcmgr = PDFResourceManager()

# Create a PDF device object.
device = PDFDevice(rsrcmgr)

# BEGIN LAYOUT ANALYSIS
# Set parameters for analysis.
laparams = LAParams()

# Create a PDF page aggregator object.
device = PDFPageAggregator(rsrcmgr, laparams=laparams)

# Create a PDF interpreter object.
interpreter = PDFPageInterpreter(rsrcmgr, device)

def parse_obj(lt_objs):

    # loop over the object list
    for obj in lt_objs:

        # if it's a textbox, print text and location
        if isinstance(obj, pdfminer.layout.LTTextBoxHorizontal):
            print "%6d, %6d, %s" % (obj.bbox[0], obj.bbox[1], obj.get_text().replace('\n', '_'))

        # if it's a container, recurse
        Elif isinstance(obj, pdfminer.layout.LTFigure):
            parse_obj(obj._objs)

# loop over all pages in the document
for page in PDFPage.create_pages(document):

    # read the page into a layout object
    interpreter.process_page(page)
    layout = device.get_result()

    # extract text from this object
    parse_obj(layout._objs)
35
pnj

Voici un exemple de copier-coller prêt qui répertorie les coins supérieurs gauche de chaque bloc de texte dans un PDF, et qui je pense devrait fonctionner pour tout PDF qui ne comprend pas " Form XObjects "qui contiennent du texte:

from pdfminer.layout import LAParams, LTTextBox
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator

fp = open('yourpdf.pdf', 'rb')
rsrcmgr = PDFResourceManager()
laparams = LAParams()
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)
pages = PDFPage.get_pages(fp)

for page in pages:
    print('Processing next page...')
    interpreter.process_page(page)
    layout = device.get_result()
    for lobj in layout:
        if isinstance(lobj, LTTextBox):
            x, y, text = lobj.bbox[0], lobj.bbox[3], lobj.get_text()
            print('At %r is text: %s' % ((x, y), text))

Le code ci-dessus est basé sur l'exemple Performing Layout Analysis dans les documents PDFMiner, plus les exemples de pnj ( https://stackoverflow.com/a/22898159/1709587 ) et Matt Swain ( https://stackoverflow.com/a/25262470/1709587 ). J'ai apporté quelques modifications à ces exemples précédents:

  • J'utilise PDFPage.get_pages(), qui est un raccourci pour créer un document, le vérifier is_extractable Et le passer à PDFPage.create_pages()
  • Je ne prends pas la peine de gérer LTFigures, car PDFMiner est actuellement incapable de gérer proprement le texte à l'intérieur.

LAParams vous permet de définir certains paramètres qui contrôlent la façon dont les caractères individuels dans le PDF sont regroupés comme par magie dans les lignes et les zones de texte par PDFMiner. Si vous êtes surpris qu'un tel regroupement soit une chose qui doit se produire du tout, c'est justifié dans les documents pdf2txt :

Dans un fichier PDF PDF réel, les portions de texte peuvent être divisées en plusieurs morceaux au milieu de son exécution, selon le logiciel de création. Par conséquent, l'extraction de texte doit épisser des morceaux de texte.

Les paramètres de LAParams sont, comme la plupart de PDFMiner, non documentés, mais vous pouvez les voir dans le code source ou en appelant help(LAParams) à votre Python Shell. La signification de certains des paramètres est donnée à https://pdfminer-docs.readthedocs.io/ pdfminer_index.html # pdf2txt-py car ils peuvent également être passés comme arguments à pdf2text sur la ligne de commande.

L'objet layout ci-dessus est un LTPage, qui est un itérable des "objets de mise en page". Chacun de ces objets de mise en page peut être l'un des types suivants ...

  • LTTextBox
  • LTFigure
  • LTImage
  • LTLine
  • LTRect

... ou leurs sous-classes. (En particulier, vos zones de texte seront probablement toutes LTTextBoxHorizontals.)

Plus de détails sur la structure d'un LTPage est montré par cette image à partir des documents:

Tree diagram of the structure of an <code>LTPage</code>. Of relevance to this answer: it shows that an <code>LTPage</code> contains the 5 types listed above, and that an <code>LTTextBox</code> contains <code>LTTextLine</code>s plus unspecified other stuff, and that an <code>LTTextLine</code> contains <code>LTChar</code>s, <code>LTAnno</code>s, <code>LTText</code>s, and unspecified other stuff.

Chacun des types ci-dessus a une propriété .bbox Qui contient un ( x0 , y0 , x1 , y1 ) Tuple contenant les coordonnées de la gauche, du bas, de la droite et du haut de l'objet respectivement. Les coordonnées y sont données comme la distance par rapport au bas de la page. S'il est plus pratique pour vous de travailler avec l'axe des y allant de haut en bas, vous pouvez les soustraire de la hauteur du .mediabox De la page:

x0, y0, x1, y1 = some_lobj.bbox
y0 = page.mediabox[3] - y1
y1 = page.mediabox[3] - y0

En plus d'une bbox, LTTextBoxes ont également une méthode .get_text(), illustrée ci-dessus, qui renvoie leur contenu texte sous forme de chaîne. Notez que chaque LTTextBox est une collection de LTChars (caractères explicitement dessinés par le PDF, avec un bbox) et LTAnnos (espaces supplémentaires que PDFMiner ajoute à la représentation sous forme de chaîne du contenu de la zone de texte en fonction des caractères dessinés à une grande distance; ceux-ci n'ont pas de bbox).

L'exemple de code au début de cette réponse a combiné ces deux propriétés pour afficher les coordonnées de chaque bloc de texte.

Enfin, il convient de noter que, contrairement à les autres réponses Stack Overflow citées ci-dessus, je ne me donne pas la peine de revenir dans LTFigures. Bien que LTFigures puisse contenir du texte, PDFMiner ne semble pas capable de regrouper ce texte dans LTTextBoxes (vous pouvez vous essayer sur l'exemple PDF de https://stackoverflow.com/a/27104504/1709587 ) et produit à la place un LTFigure qui contient directement LTChar objets. Vous pourriez, en principe, comprendre comment reconstituer ces ensemble dans une chaîne, mais PDFMiner (à partir de la version 20181108) ne peut pas le faire pour vous.

Heureusement, cependant, les fichiers PDF que vous devez analyser n'utilisent pas de formulaire XObjects contenant du texte, et cette mise en garde ne s'appliquera donc pas à vous.

13
Mark Amery