Extraiga y procese facturas en PDF en Python con borb

En esta guía, veremos cómo automatizar el procesamiento y la extracción de texto de facturas en PDF en Python con la biblioteca borb.

Introducción

El Formato de documento portátil (PDF) no es un formato WYSIWYG (Lo que ves es lo que obtienes). Fue desarrollado para ser independiente de la plataforma, independiente del sistema operativo subyacente y los motores de renderizado.

Para lograr esto, el PDF se construyó para interactuar a través de algo más parecido a un lenguaje de programación y se basa en una serie de instrucciones y operaciones para lograr un resultado. De hecho, PDF está basado en un lenguaje de secuencias de comandos: Posdata, que fue el primer lenguaje de descripción de página independiente del dispositivo.

En esta guía, usaremos borracho, una biblioteca de Python dedicada a leer, manipular y generar documentos PDF. Ofrece un modelo de bajo nivel (que le permite acceder a las coordenadas y el diseño exactos si elige usarlos) y un modelo de alto nivel (donde puede delegar los cálculos precisos de márgenes, posiciones, etc. a un administrador de diseño) .

En esta guía, veremos cómo procesar una factura en PDF en Python usando borb, extrayendo texto, ya que PDF es un formato extraíble, lo que lo hace propenso al procesamiento automatizado.

La automatización del procesamiento es uno de los objetivos fundamentales de las máquinas, y si alguien no proporciona un documento analizable, como json junto a una factura orientada a personas, tendrá que analizar el contenido del PDF usted mismo.

Instalación de borb {#instalación de borb}

borb puede descargarse desde la fuente en GitHub, o instalarse a través de pip:

1
$ pip install borb

Crear una factura en PDF en Python con borb

En la guía anterior, generamos una factura en PDF usando borb, que ahora procesaremos.

If you'd like to read more about Cómo crear facturas en Python con borb, we've got you covered!

El documento PDF generado específicamente se ve así:

borb factura 5

Procesamiento de una factura en PDF con Borb {#procesamiento de una factura en PDF con Borb}

Comencemos abriendo el archivo PDF y cargándolo en un Documento - la representación del objeto del archivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF

def main():
    d: typing.Optional[Document] = None
    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle)

    assert d is not None


if __name__ == "__main__":
    main()

El código sigue el mismo patrón que puede ver en la biblioteca json; un método estático, loads(), que acepta un identificador de archivo y genera una estructura de datos.

A continuación, nos gustaría poder extraer todo el contenido de texto del archivo. borb permite esto al permitirle registrar clases de EventListener para el análisis del Documento.

Por ejemplo, siempre que borb encuentre algún tipo de instrucción de representación de texto, notificará a todos los objetos EventListener registrados, que luego pueden procesar el Evento emitido.

borb viene con bastantes implementaciones de EventListener:

  • SimpleTextExtraction: Extrae texto de un PDF
  • SimpleImageExtraction: Extrae todas las imágenes de un PDF
  • RegularExpressionTextExtraction: coincide con una expresión regular y devuelve las coincidencias por página
  • etc.

Comenzaremos extrayendo todo el texto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF

# New import
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction

def main():

    d: typing.Optional[Document] = None
    l: SimpleTextExtraction = SimpleTextExtraction()
    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle, [l])

    assert d is not None
    print(l.get_text_for_page(0))


if __name__ == "__main__":
    main()

Este fragmento de código debe imprimir todo el texto de la factura, en orden de lectura (de arriba a abajo, de izquierda a derecha):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[Street Address] Date 6/5/2021
[City, State, ZIP Code] Invoice # 1741
[Phone] Due Date 6/5/2021
[Email Address]
[Company Website]
BILL TO SHIP TO
[Recipient Name] [Recipient Name]
[Company Name] [Company Name]
[Street Address] [Street Address]
[City, State, ZIP Code] [City, State, ZIP Code]
[Phone] [Phone]
DESCRIPTION QTY UNIT PRICE AMOUNT
Product 1 2 $ 50 $ 100
Product 2 4 $ 60 $ 240
Labor 14 $ 60 $ 840
Subtotal $ 1,180.00
Discounts $ 177.00
Taxes $ 100.30
Total $ 1163.30

Por supuesto, esto no es muy útil para nosotros, ya que requeriría más procesamiento antes de que podamos hacer mucho con él, aunque es un gran comienzo, ¡especialmente en comparación con los documentos PDF escaneados con OCR!

Vamos a refinar este código y decirle a borb qué Rectángulo nos interesa.

Por ejemplo, extraigamos la información de envío (pero puede modificar el código para recuperar cualquier área de interés).

Para permitir que borb filtre un Rectángulo, usaremos la clase LocationFilter. Esta clase implementa EventListener. Recibe notificaciones de todos los Eventos cuando representa la Página y pasa aquellos (a sus hijos) que ocurren dentro de límites predefinidos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import typing
from decimal import Decimal

from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction

# New import
from borb.toolkit.location.location_filter import LocationFilter
from borb.pdf.canvas.geometry.rectangle import Rectangle


def main():

    d: typing.Optional[Document] = None

    # Define rectangle of interest
    # x, y, width, height
    r: Rectangle = Rectangle(Decimal(280),
                             Decimal(510),
                             Decimal(200),
                             Decimal(130))

    # Set up EventListener(s)
    l0: LocationFilter = LocationFilter(r)
    l1: SimpleTextExtraction = SimpleTextExtraction()
    l0.add_listener(l1)

    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle, [l0])

    assert d is not None
    print(l1.get_text_for_page(0))


if __name__ == "__main__":
    main()

Al ejecutar este código, asumiendo que se elige el rectángulo derecho, se imprime:

1
2
3
4
5
6
SHIP TO
[Recipient Name]
[Company Name]
[Street Address]
[City, State, ZIP Code]
[Phone]

Este código no es exactamente el más flexible o preparado para el futuro. Se necesita algo de esfuerzo para encontrar el “Rectángulo” correcto, y no hay garantía de que funcione si el diseño de la factura cambia aunque sea un poco.

Vamos a tener que construir algo más robusto, para tener una aplicación práctica real.

Podemos comenzar eliminando el ‘Rectángulo’ codificado de forma rígida. RegularExpressionTextExtraction puede coincidir con una expresión regular y devolver (entre otras cosas) sus coordenadas en la Página. Usando la coincidencia de patrones, podemos buscar elementos en un documento automáticamente y recuperarlos, en lugar de adivinar dónde dibujar un rectángulo.

Usemos esta clase para encontrar las palabras "ENVIAR A", y construyamos un Rectángulo basado en esas coordenadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.pdf.canvas.geometry.rectangle import Rectangle

# New imports
from borb.toolkit.text.regular_expression_text_extraction import RegularExpressionTextExtraction, PDFMatch

def main():

    d: typing.Optional[Document] = None
        
    # Set up EventListener
    l: RegularExpressionTextExtraction = RegularExpressionTextExtraction("SHIP TO")
    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle, [l])

    assert d is not None

    matches: typing.List[PDFMatch] = l.get_matches_for_page(0)
    assert len(matches) == 1

    r: Rectangle = matches[0].get_bounding_boxes()[0]
    print("%f %f %f %f" % (r.get_x(), r.get_y(), r.get_width(), r.get_height()))

if __name__ == "__main__":
    main()

Aquí, construimos un Rectángulo alrededor de la sección e imprimimos sus coordenadas:

1
299.500000 621.000000 48.012000 8.616000

Habrás notado que get_bounding_boxes() devuelve typing.List[Rectangle]. Este es el caso cuando una expresión regular coincide con varias líneas de texto en el PDF.

Además, tenga en cuenta que el origen de un PDF (el punto [0, 0]) se encuentra en la esquina inferior izquierda. Entonces, la parte superior de la Página tiene la coordenada Y más alta.

Ahora que sabemos dónde encontrar "ENVIAR A", podemos actualizar nuestro código anterior para colocar el Rectángulo de interés justo debajo de esas palabras:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import typing
from decimal import Decimal

from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.pdf.canvas.geometry.rectangle import Rectangle
from borb.toolkit.location.location_filter import LocationFilter
from borb.toolkit.text.regular_expression_text_extraction import RegularExpressionTextExtraction, PDFMatch
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction

def find_ship_to() -> Rectangle:

    d: typing.Optional[Document] = None

    # Set up EventListener
    l: RegularExpressionTextExtraction = RegularExpressionTextExtraction("SHIP TO")
    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle, [l])

    assert d is not None

    matches: typing.List[PDFMatch] = l.get_matches_for_page(0)
    assert len(matches) == 1

    return matches[0].get_bounding_boxes()[0]
def main():

    d: typing.Optional[Document] = None

    # Define rectangle of interest
    ship_to_rectangle: Rectangle = find_ship_to()
    r: Rectangle = Rectangle(ship_to_rectangle.get_x() - Decimal(50),
                             ship_to_rectangle.get_y() - Decimal(100),
                             Decimal(200),
                             Decimal(130))

    # Set up EventListener(s)
    l0: LocationFilter = LocationFilter(r)
    l1: SimpleTextExtraction = SimpleTextExtraction()
    l0.add_listener(l1)

    with open("output.pdf", "rb") as pdf_in_handle:
        d = PDF.loads(pdf_in_handle, [l0])

    assert d is not None
    print(l1.get_text_for_page(0))

if __name__ == "__main__":
    main()

Y este código imprime:

1
2
3
4
5
6
SHIP TO
[Recipient Name]
[Company Name]
[Street Address]
[City, State, ZIP Code]
[Phone]

Esto aún requiere algún conocimiento del documento, pero no es tan rígido como el enfoque anterior, y siempre que sepa qué texto desea extraer, puede obtener coordenadas y arrebatar el contenido dentro de un rectángulo en esa página.

Conclusión

En esta guía, hemos analizado cómo procesar una factura en Python usando borb. Comenzamos extrayendo todo el texto y refinamos nuestro proceso para extraer solo una región de interés. Finalmente, comparamos una expresión regular con un PDF para que el proceso sea aún más sólido y esté preparado para el futuro.