En este post tutorial vamos a ver cómo extraer el contenido de un documento PDF para poder manejarlo a tu antojo. Nos será útil cuando nos manden trabajos pesados que consistan en copiar el contenido de un PDF a otro formato diferente. Para ello, usaremos la librería PDFBox en Java.
- Introducción
- Entorno
- Cómo añadir PDFBox a nuestro proyecto
- Cómo extraer el contenido de un PDF línea por línea
- Conclusiones
- Recursos
Introducción
PDFBox es una librería open source para Java que permite trabajar con documentos PDF. Aunque en este tutorial se va a ver cómo utilizarlo para extraer el contenido, también se puede utilizar para generar PDFs, separar y juntar diferentes documentos, rellenar formularios y más.
Existen en Adictos al trabajo otros dos tutoriales que explican diferentes usos de la herramienta: Librería PDFBOX de Java y Agregar fuentes a la librería PdfBox.
La extracción de la información de un documento PDF puede ser muy útil para automatizar tareas repetitivas relativas a la revisión de documentos PDF, especialmente cuando el PDF tiene un formato constante del cual se puede sacar información.
Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 15,6 (2,2 GHz Intel Core i7, 16GB 1600 MHz DDR3, 500GB Flash Storage)
- Sistema operativo: macOS Catalina v. 10.15.5
- Java 8
- Maven 3.6.3
- PDFBox 2.0.19
Cómo añadir PDFBox a nuestro proyecto
Si estamos utilizando Maven en nuestro proyecto Java, podemos añadir la dependencia de PDFBox directamente en el fichero pom.xml:
<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.19</version> </dependency>
Sin embargo, si no se quiere usar Maven, también se puede descargar la librería desde la web oficial: https://pdfbox.apache.org/
Una vez hemos incluido la librería en nuestro proyecto, podemos importar los paquetes necesarios.
Cómo extraer el contenido de un PDF línea por línea
PDFBox ofrece por defecto métodos para sacar todo el texto de un PDF en String, pero esto no nos permite leer el contenido línea por línea, ya que nos llega toda la información en un formato que no separa las líneas. Aunque no es el objetivo de este tutorial, es interesante ver cómo se haría esto para luego entender cómo leerlo línea por línea.
PDDocument pdDocument = null; try { pdDocument = PDDocument.load(new File("C:/pdf-to-read.pdf")); PDFTextStripper pdfStripper = new PDFTextStripper(); pdfStripper.setStartPage(1); pdfStripper.setEndPage(5); String parsedText = pdfStripper.getText(pdDocument); System.out.println(parsedText); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (pdDocument != null) { try { pdDocument.close(); } catch (IOException e) { e.printStackTrace(); } } }
Lo primero que hacemos es crear un objeto PDDocument, que es una representación en memoria de un documento PDF. Con el método estático load de la clase se pasa un objeto de tipo File con la ruta del fichero PDF que queramos leer.
A continuación, se crea un objeto de tipo PDFTextStripper que será el que se encargue de obtener el texto completo del PDF. Se debe configurar con algunos parámetros, como son el número de la página de inicio y final, lo que nos permite evitar leer el documento entero si no nos interesa. Finalmente, todo el contenido textual del PDF lo tendríamos en parsedText.
Pero lo que a nosotros nos interesa es poder leer el documento línea por línea, lo que nos va a permitir llevar un seguimiento de lo que estamos leyendo hasta ese momento, que puede ser importante. Además, también obtendremos información del estilo de cada línea, que también nos puede ayudar a diferenciar una línea en concreto del resto del texto.
Para ello, tenemos que extender la clase PDFTextStripper para reescribir uno de sus métodos y poder obtener el texto que hay en cada línea. Lo primero es crear la clase que vaya a extender PDFTextStripper, que será la misma en la que vamos a extraer la información del documento.
public class PDFInfoExtractor extends PDFTextStripper { public PDFInfoExtractor() throws IOException {} }
A continuación, creamos un método que será al que se llame para hacer el parseo de la información. Este método tendrá la configuración del PDFTextStripper como en el ejemplo del principio, pero en vez de llamar al método getText, lo hará a writeText. La diferencia se encuentra en que éste último escribe la información en un objeto de tipo Writer.
public class PDFInfoExtractor extends PDFTextStripper { List<String> parsedText; public PDFInfoExtractor() throws IOException {} public List<String> parsePDFDocument(String filePath) { parsedText = new ArrayList<>(); PDDocument pdDocument = null; try { pdDocument = PDDocument.load(new File(filePath)); this.setSortByPosition(true); this.setStartPage(1); this.setEndPage(5); Writer dummyWriter = new OutputStreamWriter(new ByteArrayOutputStream()); this.writeText(pdDocument, dummyWriter); } catch (IOException e) { e.printStackTrace(); } finally { if (pdDocument != null) { try { pdDocument.close(); } catch (IOException e) { e.printStackTrace(); } } } return parsedText; } }
En este método de ejemplo vemos que se pide como parámetro la ruta del fichero a leer y devuelve una lista de Strings, en este caso será una lista con cada línea del documento. También se podría devolver otro tipo de objeto dependiendo de lo que se quiera conseguir con esta extracción.
Esta vez, en vez de crear un objeto de tipo PDFTextStripper, debemos usar el objeto propio para la configuración y para llamar al método writeText. A diferencia del primer ejemplo, en este se establece una ordenación por posición, para que la forma de lectura del documento sea de arriba a abajo, ya que si no podría leerlo ordenándolo por fuente u otros criterios.
Después de las configuraciones de lectura, se crea un objeto de tipo Writer, necesario para poder llamar al método writeText. Esto se debe a que originalmente, este método escribe todo el contenido del documento en dicho writer, para luego hacer lo que queramos con él. Sin embargo, nosotros vamos a editar ese comportamiento, evitando que se escriba en el writer y escribiéndolo en la lista que tenemos, por eso el nombre de la variable es dummyWriter.
El método que debemos reescribir es writeString que, como vemos a continuación, tiene como parámetros el String con la línea que se está leyendo y una lista con objetos TextPosition que veremos más adelante para qué sirve.
public class PDFInfoExtractor extends PDFTextStripper { List<String> parsedText; public PDFInfoExtractor() throws IOException {} public List<String> parsePDFDocument(String filePath) { // ... } @Override protected void writeString(String text, List<TextPosition> textPositions) throws IOException { parsedText.add(text); } }
Para entender por qué sobreescribimos el método writeString, tenemos que ver cómo funciona el método writeText al que llamamos y las llamadas que hace, mirando el código de la librería.
- El método writeText configura el lector y comienza la lectura del documento, llamando a proccessPages, que hace un procesado de todas las páginas.
- Después, el método processPages llama a processPage por cada página, para procesar cada una de las páginas.
- Este a su vez llama a writePage, que es el método encargado de escribir la información de una página en el writer, pero lo hace llamando a writeLine línea por línea.
- Finalmente, como writeLine escribe no solo el texto de cada línea, sino que procesa otra información que no nos interesa, vemos que writeLine utiliza writeString para escribir en el writer la línea que sí contiene el texto.
Por otro lado, como hemos visto, el método tiene dos parámetros, el String text y una lista con TextPosition. El primer parámetro contiene el texto que hay en la línea que está siendo leída.
El segundo es una lista que tiene la misma longitud que el texto del primer parámetro. Cada objeto TextPosition de la lista contiene información sobre un 9 del texto, el que corresponda en posición. Permite obtener información sobre la posición y el tamaño de dicho caracter, además de su fuente usando el método getFont que devuelve un objeto de tipo PDFont. Esto nos permite conocer el nombre de la fuente que se ha utilizado, además de saber si se ha aplicado algún formato como negrita o cursiva.
Con estos dos parámetros podemos analizar la composición y contenido de cada una de las filas del documento PDF que queremos analizar.
Conclusiones
Esta librería nos podrá ayudar a automatizar tareas pesadas y repetitivas de extracción de información de documentos PDF.
Además de esta función de extracción, también ofrece muchas otras funcionalidades para trabajar con este tipo de documentos, lo que hace que sea una librería muy potente y útil si tenemos un proyecto en el que se va a trabajar con PDFs de forma recurrente.
Recursos
- Documentación oficial de PDFBox: https://pdfbox.apache.org/docs/2.0.0/javadocs/index.html?overview-summary.html