¿Cómo extraer texto y coordenadas de texto de un archivo PDF?

Quiero extraer todos los cuadros de texto y las coordenadas de los cuadros de texto de un archivo PDF con PDFMiner.

Muchas otras publicaciones de desbordamiento de stack tratan cómo extraer todo el texto de una manera ordenada, pero ¿cómo puedo hacer el paso intermedio para obtener el texto y las ubicaciones del texto?

Dado un archivo PDF, la salida debe verse algo como:

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

Las líneas nuevas se convierten en guiones bajos en la salida final. Esta es la solución de trabajo mínima que encontré.

 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) 

Aquí hay un ejemplo listo para copiar y pegar que enumera las esquinas superiores izquierdas de cada bloque de texto en un PDF, y creo que debería funcionar para cualquier PDF que no incluya “Form XObjects” que tienen texto en ellos:

 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)) 

El código anterior se basa en el ejemplo de Performing Layout Analysis en los documentos de PDFMiner, más los ejemplos de pnj ( https://stackoverflow.com/a/22898159/1709587 ) y Matt Swain ( https://stackoverflow.com/a/ 25262470/1709587 ). Hay un par de cambios que he hecho de estos ejemplos anteriores:

  • Utilizo PDFPage.get_pages() , que es una forma abreviada de crear un documento, verificarlo is_extractable y pasarlo a PDFPage.create_pages()
  • No me molesto en manejar LTFigure s, ya que PDFMiner actualmente no puede manejar de manera limpia el texto dentro de ellos de todos modos.

LAParams permite establecer algunos parámetros que controlan cómo los caracteres individuales en el PDF se agrupan mágicamente en líneas y cuadros de texto mediante PDFMiner. Si te sorprende que tal agrupación sea algo que debe suceder, está justificado en los documentos pdf2txt :

En un archivo PDF real, las porciones de texto se pueden dividir en varios trozos en medio de su ejecución, dependiendo del software de creación. Por lo tanto, la extracción de texto necesita unir fragmentos de texto.

LAParams parámetros de LAParams son, como la mayoría de PDFMiner, no documentados, pero puede verlos en el código fuente o llamando a la help(LAParams) en su shell de Python. El significado de algunos de los parámetros se encuentra en https://pdfminer-docs.readthedocs.io/pdfminer_index.html#pdf2txt-py ya que también se pueden pasar como argumentos a pdf2text en la línea de comandos.

El objeto de layout anterior es un LTPage , que es un iterable de “objetos de diseño”. Cada uno de estos objetos de diseño puede ser uno de los siguientes tipos …

  • LTTextBox
  • LTFigure
  • LTImage
  • LTLine
  • LTRect

… o sus subclases. (En particular, sus cuadros de texto probablemente serán todos LTTextBoxHorizontal s.)

Esta imagen de la documentación muestra más detalles de la estructura de un LTPage :

Diagrama de árbol de la estructura de un <code/> LTPage . De relevancia para esta respuesta: muestra que un <code> LTPage </code> contiene los 5 tipos enumerados anteriormente, y que un <code> LTTextBox </code> contiene <code> LTTextLine </code> más otras cosas no especificadas , y que un <code> LTTextLine </code> contiene <code> LTChar </code> s, <code> LTAnno </code> s, <code> LTText </code> s, y otras cosas no especificadas.”></p>
<p>  Cada uno de los tipos anteriores tiene una propiedad <code>.bbox</code> que contiene una tupla ( <em>x0</em> , <em>y0</em> , <em>x1</em> , <em>y1</em> ) que contiene las coordenadas de la izquierda, la parte inferior, la derecha y la parte superior del objeto, respectivamente.  Las coordenadas y se dan como la distancia desde la <em>parte inferior</em> de la página.  Si es más conveniente para usted trabajar con el eje y que va de arriba a abajo, puede restarlos de la altura del <code>.mediabox</code> de la página: </p>
<pre> <code>x0, y0, x1, y1 = some_lobj.bbox y0 = page.mediabox[3] - y1 y1 = page.mediabox[3] - y0</code> </pre>
<p>  Además de un <code>bbox</code> , <code>LTTextBox</code> es también tiene un método <code>.get_text()</code> , que se muestra arriba, que devuelve su contenido de texto como una cadena.  Tenga en cuenta que cada <code>LTTextBox</code> es una colección de <code>LTChar</code> s (caracteres dibujados explícitamente por el PDF, con un <code>bbox</code> ) y <code>LTAnno</code> s (espacios adicionales que PDFMiner agrega a la representación de cadena del contenido del cuadro de texto en función de los caracteres que se dibujan muy separados ; estos no tienen <code>bbox</code> ). </p>
<p>  El ejemplo de código al comienzo de esta respuesta combinó estas dos propiedades para mostrar las coordenadas de cada bloque de texto. </p>
<p>  Por último, vale la pena señalar que, a <em>diferencia de</em> las otras respuestas de desbordamiento de stack citadas anteriormente, no me molesto en <code>LTFigure</code> a los <code>LTFigure</code> s.  Aunque <code>LTFigure</code> s puede contener texto, PDFMiner no parece capaz de agrupar ese texto en <code>LTTextBox</code> es (puede probarse usted mismo en el PDF de ejemplo de <a href=https://stackoverflow.com/a/27104504/1709587 ) y en su lugar produce un LTFigure que contiene directamente objetos LTChar . En principio, podría averiguar cómo unirlos en una cadena, pero PDFMiner (a partir de la versión 20181108) no puede hacerlo por usted.

Sin embargo, con suerte, los PDF que necesita analizar no usan el Form XObjects con texto en ellos, por lo que esta advertencia no se aplicará a usted.