Parse PDF Files While Retaining Structure with Tabula-py

If you’ve ever tried to do anything with data provided to you in PDFs, you know how painful it is — it's hard to copy-and-paste rows of data out of PDF files. It's especially hard if you want to retain the formats of the data in PDF file while extracting text. Most of the open source PDF parsers available are good at extracting text. But when it comes to retaining the the file's structure, eh, not really. Try tabula-py to extract data into a CSV or Excel spreadsheet using a simple, easy-to-use interface. One look is worth a thousand words. Take a look at the demo screenshot.

Installations

This installation tutorial assumes that you are using Windows. However, according to the offical tabula-py documentation, it was confirmed that tabula-py works on macOS and Ubuntu.

1. Download Java

Tabula-py is a wrapper for tabula-java, which translates Python commands to Java commands. As the name "tabula-java" suggests, it requires Java. You can download Java here.

2. Set environment PATH variable (Windows)

One thing that I don't like about Windows is that it's difficult to use a new program I downloaded in a console environment like Python or CMD window. But oh well, if you are a Windows user, you have to go through this extra step to allow Python to use Java. If you are a macOS or Ubuntu user, you probably don't need this step.

Find where Java is installed, and go to Control Panel > System and Security > System > Advanced system settings > Advanced > Environment Variables... to set environment PATH variable for Java.

Make sure you have Java\jdk1.8.0_201\bin and Java\jre1.8.0_201\bin in the environment path variable. Then, type java -version on CMD window. If you successfully installed Java and configured the environment variable, you should see something like this:

java -version

java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

If you don't see something like this, it means that you didn't properly configure environment PATH variable for Java.

3. Re-start Your Command Prompt

Any program invoked from the command prompt will be given the environment variables that was at the time the command prompt was invoked. If you launched your Python console or Jupyter Notebook before you updated your environment PATH variable, you need to re-start again. Otherwise the change in the environment variable will not be reflected.

If you are experiencing FileNotFoundError or 'java' is not recognized as an internal or external command, operable program or batch file inside Jupyter or Python console, it's the issue of environment variable. Either you set it wrong, or your command prompt is not reflecting the change you made in the environment variable.

To check if the change in the environment variable was reflected, run the following code in Jupyter or Python console:

    
        import os

        s = os.environ["PATH"].split(';')
        for item in s: 
            print(item)
    

Something like these must be in the output if everything is working fine:

    
        C:\Program Files\Java\jdk1.8.0_201\bin
        C:\Program Files\Java\jre1.8.0_201\bin
    

4. Install Tabula-py

This is the last step:

pip install tabula-py

Make sure that you install tabula-py, not tabula. Failing to do so will result in AttributeError: module 'tabula' has no attribute 'read_pdf', as discussed in this thread. More detailed instructions are provided in the github repo of tabula-py

Tabula Web Application

Tabula supports web application to parse PDF files. You do not need this to use tabula-py, but from my personal experience I strongly recommend you to use this tool because it really helps you debugging issues when using tabula-py. For example, I was tring to parse 100s of PDF files at once, and for some reason tabula-py would return an NoneType object instead of pd.DataFrame object (by default, tabula-py extracts tables in dataframe) for one PDF file. There was nothing wrong with my codes, and yet it would just not parse the file. So I tried opening it on the tabula web-app, and realized that it was actually a scanned PDF file and that tabula is unable to parse scanned PDFs.

Long story short, if it can be parsed with tabula web-app, you can replicate it with tabula-py. If tabula web-app can't, you should probably look for a different tool.

Installations

If you already configured the environment PATH variable for Java, all you need to do is downloading the .zip file here and running tabula.exe. That's it. Tabula has really nice web UI that allows you to parse tables from PDFs by just clicking buttons.

Note

The web-app will automatically open in your browser with 127.0.0.1:8080 local host. If port 8080 is already being used by another process, you will need to shut it down. But normally you don't have to worry about this.

Screenshots

This is what you will see when you launch tabula.exe. Browse... the PDF file you want to parse, and import.

You can either use Autodetect Tables or drag your mouse to choose the area of your interest. If the PDF file has a complicated structure, it is usually better to manually choose the area of your interest. Also, note the option Repeat to All Pages. Selecting this option will apply the area you chose for all pages.

Here's the output. More explanation about Lattice and Stream options will be discussed in detail later.

Template JSON Files

Tabula web-app accepts the user's drag & click as input and translates it into Java arguments that are actually used behind the scenes to parse PDF files. The translated Java arguments are accessible to users in a JSON format.

Select the area you want to parse, and click Save Selections as Template. Then, Download the translated Java arguments in a text JSON file. These arguments are useful when coding arguments for tabula.read_pdf() later.

template.json

    
        {
            "page": 2,
            "extraction_method": "guess",
            "x1": 24.785995330810547,
            "x2": 589.3559953308105,
            "y1": 390.5325,
            "y2": 695.0025,
            "width": 564.57,
            "height": 304.47
         }
    

Running Tabula-py

Tabula-py enables you to extract tables from PDFs into DataFrame and JSON. It can also extract tables from PDFs and save files as CSV, TSV or JSON. Some basic code examples are as follows:

    
        import tabula

        # Read pdf into DataFrame
        df = tabula.read_pdf("test.pdf", options)

        # Read remote pdf into DataFrame
        df2 = tabula.read_pdf("https://github.com/tabulapdf/tabula-java/raw/master/src/test/resources/technology/tabula/arabic.pdf")

        # convert PDF into CSV
        tabula.convert_into("test.pdf", "output.csv", output_format="csv")

        # convert all PDFs in a directory
        tabula.convert_into_by_batch("input_directory", output_format='csv')
    


Area Selection

You can select portions of PDFs you want to analyze by setting area (top,left,bottom,right) option in tabula.read_pdf(). This is equivalent to dragging your mouse and setting the area of your interest in tabula web-app as it was mentioned above. Default is the entire page. Also note that you can choose the page, or pages you want to parse with pages option.

The sample PDF file can be downloaded from here.

In [1]:
import tabula
import pandas as pd
In [37]:
file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, lattice=True, pages=2, area=(406, 24, 695, 589))
df
Out[37]:
Start Date End Date (hr) Activity Activity Detail Operation Com
0 12/13/2014\r06:00 12/13/2014\r09:00 3.0 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
1 12/13/2014\r09:00 12/13/2014\r11:00 2.0 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
2 12/13/2014\r11:00 12/13/2014\r14:00 3.0 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
3 12/13/2014\r14:00 12/13/2014\r16:00 2.0 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...
4 12/13/2014\r16:00 12/13/2014\r17:30 1.5 PLAN DRLG CSG PTJSA / R/U Pipe Pros.Csg. tools / PTJSA on ru...
5 12/13/2014\r17:30 12/13/2014\r18:00 0.5 PLAN DRLG CSG Make up 13 3/8 Gemco PDC drillable float shoe;...
6 12/13/2014\r18:00 12/13/2014\r18:30 0.5 PLAN PERS SFTY HJSM with Morning tour crew, Pipe Pro casing c...
7 12/13/2014\r18:30 12/13/2014\r23:30 5.0 PLAN DRLG CSG Make up 13 /8" PDC drillable float collar onto...
8 12/13/2014\r23:30 12/14/2014\r01:30 2.0 SURF-CIRC CIRCULATE CIRC HJSM on Hoisting personal; Make up Swedge in ...
9 12/14/2014\r01:30 12/14/2014\r03:30 2.0 PLAN DRLG CSG Run 13 3/8"J-55 54.5 BTC f/ 1,639' to 1,819';...
10 12/14/2014\r03:30 12/14/2014\r04:30 1.0 SURF-CIRC CIRCULATE CIRC Circulate Bttms up while Rigging down csg crew...
11 12/14/2014\r04:30 12/14/2014\r06:00 1.5 SURF-CMT CEMENT SURFACE\rCASING CMT HJSM w/ Basic Cementer, H&P rig crew & PNR; D...

Alternatively, you can set area with percentage scale by setting relative_area=True. For this specific PDF file, the below area=(50, 5, 92, 100), relative_area=True option is equivalent to area=(406, 24, 695, 589) above.

In [38]:
file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, lattice=True, pages=2, area=(50, 5, 92, 100), relative_area=True)
df
Out[38]:
Start Date End Date Dur (hr) Activity Activity Detail Operation Com
0 2/13/2014\r6:00 12/13/2014\r09:00 3.0 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
1 2/13/2014\r9:00 12/13/2014\r11:00 2.0 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
2 2/13/2014\r1:00 12/13/2014\r14:00 3.0 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
3 2/13/2014\r4:00 12/13/2014\r16:00 2.0 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...
4 2/13/2014\r6:00 12/13/2014\r17:30 1.5 PLAN DRLG CSG PTJSA / R/U Pipe Pros.Csg. tools / PTJSA on ru...
5 2/13/2014\r7:30 12/13/2014\r18:00 0.5 PLAN DRLG CSG Make up 13 3/8 Gemco PDC drillable float shoe;...
6 2/13/2014\r8:00 12/13/2014\r18:30 0.5 PLAN PERS SFTY HJSM with Morning tour crew, Pipe Pro casing c...
7 2/13/2014\r8:30 12/13/2014\r23:30 5.0 PLAN DRLG CSG Make up 13 /8" PDC drillable float collar onto...
8 2/13/2014\r3:30 12/14/2014\r01:30 2.0 SURF-CIRC CIRCULATE CIRC HJSM on Hoisting personal; Make up Swedge in ...
9 2/14/2014\r1:30 12/14/2014\r03:30 2.0 PLAN DRLG CSG Run 13 3/8"J-55 54.5 BTC f/ 1,639' to 1,819';...
10 2/14/2014\r3:30 12/14/2014\r04:30 1.0 SURF-CIRC CIRCULATE CIRC Circulate Bttms up while Rigging down csg crew...
11 2/14/2014\r4:30 12/14/2014\r06:00 1.5 SURF-CMT CEMENT SURFACE\rCASING CMT HJSM w/ Basic Cementer, H&P rig crew & PNR; D...

Notes on Escape Characters

When used as lattice mode, tabula replaces abnormally large spacing between texts and newline within a cell with \r. This can be fixed with a simple regex manipulation.

In [41]:
clean_df = df.replace('\r',' ', regex=True)
clean_df
Out[41]:
Start Date End Date Dur (hr) Activity Activity Detail Operation Com
0 2/13/2014 6:00 12/13/2014 09:00 3.0 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
1 2/13/2014 9:00 12/13/2014 11:00 2.0 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
2 2/13/2014 1:00 12/13/2014 14:00 3.0 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
3 2/13/2014 4:00 12/13/2014 16:00 2.0 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...
4 2/13/2014 6:00 12/13/2014 17:30 1.5 PLAN DRLG CSG PTJSA / R/U Pipe Pros.Csg. tools / PTJSA on ru...
5 2/13/2014 7:30 12/13/2014 18:00 0.5 PLAN DRLG CSG Make up 13 3/8 Gemco PDC drillable float shoe;...
6 2/13/2014 8:00 12/13/2014 18:30 0.5 PLAN PERS SFTY HJSM with Morning tour crew, Pipe Pro casing c...
7 2/13/2014 8:30 12/13/2014 23:30 5.0 PLAN DRLG CSG Make up 13 /8" PDC drillable float collar onto...
8 2/13/2014 3:30 12/14/2014 01:30 2.0 SURF-CIRC CIRCULATE CIRC HJSM on Hoisting personal; Make up Swedge in ...
9 2/14/2014 1:30 12/14/2014 03:30 2.0 PLAN DRLG CSG Run 13 3/8"J-55 54.5 BTC f/ 1,639' to 1,819';...
10 2/14/2014 3:30 12/14/2014 04:30 1.0 SURF-CIRC CIRCULATE CIRC Circulate Bttms up while Rigging down csg crew...
11 2/14/2014 4:30 12/14/2014 06:00 1.5 SURF-CMT CEMENT SURFACE CASING CMT HJSM w/ Basic Cementer, H&P rig crew & PNR; D...

Lattice Mode vs Stream Mode

Tabula supports two primary modes of table extraction — Lattice mode and Stream mode.

Lattice Mode

lattice=True forces PDFs to be extracted using lattice-mode extraction. It recognizes each cells based on ruling lines, or borders of each cell.

Stream Mode

stream=True forces PDFs to be extracted using stream-mode extraction. This mode is used when there are no ruling lines to differentiate one cell from the other. Instead, it uses spacings among each cells to recognize each cell.

PDF File 1: Lattice mode recommended

PDF file 2: Stream mode recommended

How would it look like if PDF File 1 and PDF file 2 are each extracted in both stream mode and lattice mode?

In [51]:
# PDF File 1: lattice mode

file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, lattice=True, pages=2, area=(406, 24, 695, 589))
df.head()
Out[51]:
Start Date End Date (hr) Activity Activity Detail Operation Com
0 12/13/2014\r06:00 12/13/2014\r09:00 3.0 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
1 12/13/2014\r09:00 12/13/2014\r11:00 2.0 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
2 12/13/2014\r11:00 12/13/2014\r14:00 3.0 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
3 12/13/2014\r14:00 12/13/2014\r16:00 2.0 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...
4 12/13/2014\r16:00 12/13/2014\r17:30 1.5 PLAN DRLG CSG PTJSA / R/U Pipe Pros.Csg. tools / PTJSA on ru...
In [57]:
# PDF File 1: stream mode

file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, stream=True, guess=False, pages=2, area=(406, 24, 695, 589))
df.head(11)
Out[57]:
Start Date End Date (hr) Activity Activity Detail Operation Com
0 12/13/2014 12/13/2014 3.0 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
1 06:00 09:00 NaN NaN NaN NaN SPP 2300, motor diff 650, 800 GPM, torque 18k.
2 NaN NaN NaN NaN NaN NaN (T.D. Surface @ 09:00 12-13-14)
3 12/13/2014 12/13/2014 2.0 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
4 09:00 11:00 NaN NaN NaN NaN NaN
5 12/13/2014 12/13/2014 3.0 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
6 11:00 14:00 NaN NaN NaN NaN Hole taking correct fill
7 12/13/2014 12/13/2014 2.0 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...
8 14:00 16:00 NaN NaN NaN NaN ext.
9 12/13/2014 12/13/2014 1.5 PLAN DRLG CSG PTJSA / R/U Pipe Pros.Csg. tools / PTJSA on ru...
10 16:00 17:30 NaN NaN NaN NaN csg.
In [62]:
# PDF File 2: lattice mode

file = 'pdf_parsing/stream-railroad-pages-1-4.pdf'
df = tabula.read_pdf(file, lattice=True, pages=1, area=(209, 12.5, 387.3, 386))
df
Out[62]:
WELL INFORMATION
In [64]:
# PDF File 2: stream mode

file = 'pdf_parsing/stream-railroad-pages-1-4.pdf'
df = tabula.read_pdf(file, stream=True, guess=False, pages=1, area=(209, 12.5, 387.3, 386))
df
Out[64]:
Unnamed: 0 WELL INFORMATION
0 API No.: 42-003-46352 County: A
1 Well No.:22H RRC Distri
2 Lease Name: UNIVERSITY "7-43" Field Name
3 RRC Lease No.: 40532 Field No.:
4 Location: Section: 35, Block: 7, Survey: UN... NaN
5 Latitude: Longitude:
6 This well is located 17.2 miles in a SE
7 direction from ANDREWS, NaN
8 which is the nearest town in the county. NaN

Observe how lattice mode extraction for PDF file 2 was able to extract only "WELL INFORMATION" string. This is not an error. Recall that lattice mode identifies cells by ruling lines.

Notes About guess option

According to the offical documentation, guess is known to make a conflict between stream option. If you feel something strange with your result, try setting guess=False.

For example, for PDF File 1, if stream mode is used without setting guess=False, it would look like this:

In [66]:
# PDF File 1: stream mode, guess=True

file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, stream=True, pages=2, area=(406, 24, 695, 589))
df.head(11)
Out[66]:
Report #:3 Daily Operation:12/13/2014 06:00 - 12/14/2014 06:00 Unnamed: 2 Unnamed: 3 Unnamed: 4 Unnamed: 5 Unnamed: 6
0 Job Category NaN Primary Job Type NaN NaN AFE Number NaN
1 ORIG DRILLING NaN ODR NaN NaN 033402 NaN
2 Days From Spud (days) Days on Location (days) End Depth (ftKB) End Depth (TVD) (ftKB) Dens Last Mud (lb/gal) Rig NaN NaN
3 1 3 1,859.0 1,858.5 8.60 H & P, 637 NaN NaN
4 Operations Summary NaN NaN NaN NaN NaN NaN
5 Drld. Surface f/1600' to 1859' T.D. @ 09:00... NaN NaN NaN NaN NaN NaN
6 S/M, R/U csg running equip & Run 45 jts. of... NaN NaN NaN NaN NaN NaN
7 Surface Float shoe @ 1,857.5' NaN NaN NaN NaN NaN Surface
8 Float collar @ 1,817.9' NaN NaN NaN NaN NaN NaN
9 Remarks NaN NaN NaN NaN NaN NaN
10 Rig (H&P 637), Well (University 7-43 # 22H) NaN NaN NaN NaN NaN NaN

Pandas Option

Pandas arguments can be passed into tabula.read_pdf() as a dictionary object.

In [74]:
file = 'pdf_parsing/lattice-timelog-multiple-pages.pdf'
df = tabula.read_pdf(file, lattice=True, pages=2, area=(406, 24, 695, 589), pandas_options={'header': None})
df.head()
Out[74]:
0 1 2 3 4 5 6
0 Start Date End Date (hr) Activity Activity Detail Operation Com
1 12/13/2014\r06:00 12/13/2014\r09:00 3 SURF-DRILL DRILL SURFACE DRL Rotate from 1600' to 1859' (259' @ 8 fph). WOB...
2 12/13/2014\r09:00 12/13/2014\r11:00 2 SURF-CIRC CIRCULATE CIRC Pump 2- 50 bbl hi vis sweep; Circulate to surface
3 12/13/2014\r11:00 12/13/2014\r14:00 3 SURF-TRIP TOOH TRIP TOOH (Slick off bottom) f/1859' to 108' (SLM)...
4 12/13/2014\r14:00 12/13/2014\r16:00 2 PLAN EQUIP BHA PJSA - Break bit & L/D directional BHA, clean ...

More Documentation

Further instructions about tabula-py can be found on its official github repo.



Related Posts

    Natural Language Processing

    Understanding Multi-Dimensionality in Vector Space Modeling

    How does word vectors in Natural Language Processing capture meaningful relationships among words? How can you quantify those relationships? Addressing these questions starts from understanding the multi-dimensional nature of NLP applications.

    2019-04-16
    18 min reading
    Natural Language Processing

    Demystifying Neural Network in Skip-Gram Language Modeling

    The past couple of years, neural networks in Word2Vec have nearly taken over the field of NLP, thanks to their state-of-art performance. But how much do you understand about the algorithm behind it? This post will crack the secrets behind neural net in Word2Vec.

    2019-05-06
    20 min reading
    Natural Language Processing

    Optimize Computational Efficiency of Skip-Gram with Negative Sampling

    When training your NLP model with Skip-Gram, the very large size of vocabs imposes high computational cost on your machine. Since the original Skip-Gram model is unable to handle this high cost, we use an alternative, called Negative Sampling.

    2019-05-26
    22 min reading