Mining Data from PDF Files with Python
Join the DZone community and get the full member experience.
Join For FreePDF files aren't pleasant.
The good news is that they're documented (http://www.adobe.com/devnet/pdf/pdf_reference.html).
The bad news is that they're rather complex.
I found four Python packages for reading PDF files.
- http://pybrary.net/pyPdf/ - weak
- http://www.swftools.org/gfx_tutorial.html - depends on binary XPDF
- http://blog.didierstevens.com/programs/pdf-tools/ - limited
- http://www.unixuser.org/~euske/python/pdfminer/ - acceptable
I elected to work with PDFMiner for two reasons. (1) Pure Python, (2) Reasonably Complete.
This is not, however, much of an endorsement. The implementation (while seemingly correct for my purposes) needs a fair amount of cleanup.
Here's one example of remarkably poor programming.
Only one of these two is needed; the other is trivially handled as part of the setter method.
Also, the package seems to rely on a huge volume of isinstance type checking. It's not clear if proper polymorphism is even possible. But some kind of filter that picked elements by type might be nicer than a lot of isinstance checks.
In a couple of hours, I had this example of how to read a PDF document and collect the data filled into the form.
This gives us a dictionary of field names and values. Essentially transforming the PDF form into the same kind of data that comes from an HTML POST request.
An important part is that we don't want much of the background text. Just enough to confirm the version of the form file itself.
The cryptic text.sort( key=lambda row: (-row.y, row.x) ) will sort the text blocks into order from top-to-bottom and left-to-right. For the most part, a page footer will show up last. This is not guaranteed, however. In a multi-column layout, the footer can be so close to the bottom of a column that PDFMiner may put the two text blocks together.
The other unfortunate part is the extremely long (and opaque) setup required to get the data from the page.
Source: http://slott-softwarearchitect.blogspot.com/2012/02/pdf-reading.html
# Connect the parser and document objects. parser.set_document(doc) doc.set_parser(parser)
Only one of these two is needed; the other is trivially handled as part of the setter method.
Also, the package seems to rely on a huge volume of isinstance type checking. It's not clear if proper polymorphism is even possible. But some kind of filter that picked elements by type might be nicer than a lot of isinstance checks.
Annotation Extraction
While shabby, the good news is that PDFMiner seems to reliably extract the annotations on a PDF form.In a couple of hours, I had this example of how to read a PDF document and collect the data filled into the form.
from pdfminer.pdfparser import PDFParser, PDFDocument from pdfminer.psparser import PSLiteral from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter, PDFTextExtractionNotAllowed from pdfminer.pdfdevice import PDFDevice from pdfminer.pdftypes import PDFObjRef from pdfminer.layout import LAParams, LTTextBoxHorizontal from pdfminer.converter import PDFPageAggregator from collections import defaultdict, namedtuple TextBlock= namedtuple("TextBlock", ["x", "y", "text"]) class Parser( object ): """Parse the PDF. 1. Get the annotations into the self.fields dictionary. 2. Get the text into a dictionary of text blocks. The key to the dictionary is page number (1-based). The value in the dictionary is a sequence of items in (-y, x) order. That is approximately top-to-bottom, left-to-right. """ def __init__( self ): self.fields = {} self.text= {} def load( self, open_file ): self.fields = {} self.text= {} # Create a PDF parser object associated with the file object. parser = PDFParser(open_file) # Create a PDF document object that stores the document structure. doc = PDFDocument() # Connect the parser and document objects. parser.set_document(doc) doc.set_parser(parser) # Supply the password for initialization. # (If no password is set, give an empty string.) doc.initialize('') # Check if the document allows text extraction. If not, abort. if not doc.is_extractable: raise PDFTextExtractionNotAllowed # Create a PDF resource manager object that stores shared resources. rsrcmgr = PDFResourceManager() # 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) # Process each page contained in the document. for pgnum, page in enumerate( doc.get_pages() ): interpreter.process_page(page) if page.annots: self._build_annotations( page ) txt= self._get_text( device ) self.text[pgnum+1]= txt def _build_annotations( self, page ): for annot in page.annots.resolve(): if isinstance( annot, PDFObjRef ): annot= annot.resolve() assert annot['Type'].name == "Annot", repr(annot) if annot['Subtype'].name == "Widget": if annot['FT'].name == "Btn": assert annot['T'] not in self.fields self.fields[ annot['T'] ] = annot['V'].name elif annot['FT'].name == "Tx": assert annot['T'] not in self.fields self.fields[ annot['T'] ] = annot['V'] elif annot['FT'].name == "Ch": assert annot['T'] not in self.fields self.fields[ annot['T'] ] = annot['V'] # Alternative choices in annot['Opt'] ) else: raise Exception( "Unknown Widget" ) else: raise Exception( "Unknown Annotation" ) def _get_text( self, device ): text= [] layout = device.get_result() for obj in layout: if isinstance( obj, LTTextBoxHorizontal ): if obj.get_text().strip(): text.append( TextBlock(obj.x0, obj.y1, obj.get_text().strip()) ) text.sort( key=lambda row: (-row.y, row.x) ) return text def is_recognized( self ): """Check for Copyright as well as Revision information on each page.""" bottom_page_1 = self.text[1][-3:] bottom_page_2 = self.text[2][-3:] pg1_rev= "Rev 2011.01.17" == bottom_page_1[2].text pg2_rev= "Rev 2011.01.17" == bottom_page_2[0].text return pg1_rev and pg2_rev
This gives us a dictionary of field names and values. Essentially transforming the PDF form into the same kind of data that comes from an HTML POST request.
An important part is that we don't want much of the background text. Just enough to confirm the version of the form file itself.
The cryptic text.sort( key=lambda row: (-row.y, row.x) ) will sort the text blocks into order from top-to-bottom and left-to-right. For the most part, a page footer will show up last. This is not guaranteed, however. In a multi-column layout, the footer can be so close to the bottom of a column that PDFMiner may put the two text blocks together.
The other unfortunate part is the extremely long (and opaque) setup required to get the data from the page.
Source: http://slott-softwarearchitect.blogspot.com/2012/02/pdf-reading.html
PDF
Data (computing)
Python (language)
Mining (military)
Opinions expressed by DZone contributors are their own.
Comments