<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/freeing-pdf-data-to-account-for-the-unaccounted/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://rud.is/b/2018/07/02/freeing-pdf-data-to-account-for-the-unaccounted/"> R – rud.is</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>I’ve mentioned <a rel="nofollow" href="http://twitter.com/stiles" target="_blank">@stiles</a> before on the blog but for those new to my blatherings, Matt is a top-notch data journalist with the <a rel="nofollow" href="http://twitter.com/latimes" target="_blank">@latimes</a> and currently stationed in South Korea. I can only imagine how much busier his life has gotten since that fateful, awful November 2016 Tuesday, but I’m truly glad his eyes, pen and R console are covering the important events there.</p>
<p>When I finally jumped on Twitter today, I saw this:</p>
<blockquote class="twitter-tweet" data-lang="en">
<p lang="en" dir="ltr"><a rel="nofollow" href="https://twitter.com/hrbrmstr?ref_src=twsrc%5Etfw" target="_blank"><a rel="nofollow" href="http://twitter.com/hrbrmstr" target="_blank">@hrbrmstr</a></a> Do you have an R rig to convert this large PDF to csv? I tried xpdf and <a rel="nofollow" href="https://twitter.com/TabulaPDF?ref_src=twsrc%5Etfw" target="_blank"><a rel="nofollow" href="http://twitter.com/TabulaPDF" target="_blank">@TabulaPDF</a></a> but I don't trust the results.</p>
<p>— Matt Stiles (<a rel="nofollow" href="http://twitter.com/stiles" target="_blank">@stiles</a>) <a rel="nofollow" href="https://twitter.com/stiles/status/1013587088908808192?ref_src=twsrc%5Etfw" target="_blank">July 2, 2018</a></p>
</blockquote>
<p></p>
<p>and went into action and figured I should blog the results as one can never have too many “convert this PDF to usable data” examples.</p>
<h3>The Problem</h3>
<p>The U.S. Defense POW/MIA Accounting Agency maintains POW/MIA data for all our nation’s service members. Matt is working with data <a rel="nofollow" href="http://www.dpaa.mil/Our-Missing/Korean-War/Korean-War-POW-MIA-List/" target="_blank">from Korea</a> (the “All US Unaccounted-For” PDF direct link is in the code below) and needed to get the PDF into a usable form and (as you can see if you read through the Twitter thread) both Tabulizer and other tools were introducing sufficient errors that the resultant extracted data was either not complete or trustworthy enough to rely on (hand-checking nearly 8,000 records is not fun).</p>
<p>There PDF in question was pretty uniform, save for the first and last pages. Here’s a sample:</p>
<p><a rel="nofollow" href="https://rud.is/b/2018/07/02/freeing-pdf-data-to-account-for-the-unaccounted/#gallery-10966-1-slideshow" target="_blank">Click to view slideshow.</a></p>
<p>We just need a reproducible way to extract the data with sufficient veracity to ensure we can use it faithfully.</p>
<h2>The Solution</h2>
<p>We’ll need some packages and the file itself, so let’s get that bit out of the way first:</p>
<pre><code>library(stringi)
library(pdftools)
library(hrbrthemes)
library(ggpomological)
library(tidyverse)

# grab the PDF text
mia_url &lt;- &quot;http://www.dpaa.mil/portals/85/Documents/KoreaAccounting/pmkor_una_all.pdf&quot;
mia_fil &lt;- &quot;~/Data/pmkor_una_all.pdf&quot;
if (!file.exists(mia_fil)) download.file(mia_url, mia_fil)

# read it in
doc &lt;- pdf_text(mia_fil) 
</code></pre>
<p>Let's look at those three example pages:</p>
<pre><code>cat(doc[[1]])
##                                   Defense POW/MIA Accounting Agency
##                                       Personnel Missing - Korea (PMKOR)
##                                        (Reported for ALL Unaccounted For)
##                                                                                                Total Unaccounted: 7,699
## Name                       Rank/Rate     Branch                           Year State/Territory
## ABBOTT, RICHARD FRANK      M/Sgt         UNITED STATES ARMY               1950 VERMONT
## ABEL, DONALD RAYMOND       Pvt           UNITED STATES ARMY               1950 PENNSYLVANIA
## ...
## AKERS, HERBERT DALE        Cpl           UNITED STATES ARMY               1950 INDIANA
## AKERS, JAMES FRANCIS       Cpl           UNITED STATES MARINE CORPS       1950 VIRGINIA

cat(doc[[2]])
## Name                          Rank/Rate Branch                     Year State/Territory
## AKERS, RICHARD ALLEN          1st Lt    UNITED STATES ARMY         1951 PENNSYLVANIA
## AKI, CLARENCE HALONA          Sgt       UNITED STATES ARMY         1950 HAWAII
...
## AMIDON, DONALD PRENTICE       PFC       UNITED STATES MARINE CORPS 1950 TEXAS
## AMOS, CHARLES GEARL           Cpl       UNITED STATES ARMY         1951 NORTH CAROLINA

cat(doc[[length(doc)]])
## Name                                                Rank/Rate           Branch                                              Year         State/Territory
## ZAVALA, FREDDIE                                     Cpl                 UNITED STATES ARMY                                  1951         CALIFORNIA
## ZAWACKI, FRANK JOHN                                 Sgt                 UNITED STATES ARMY                                  1950         OHIO
## ...
## ZUVER, ROBERT LEONARD                               Pfc                 UNITED STATES ARMY                                  1950         CALIFORNIA
## ZWILLING, LOUIS JOSEPH                              Cpl                 UNITED STATES ARMY                                  1951         ILLINOIS
##                                       This list of Korean War missing personnel was prepared by the Defense POW/MIA Accounting Agency (DPAA).
##                Please visit our web site at http://www.dpaa.mil/Our-Missing/Korean-War-POW-MIA-List/ for updates to this list and other official missing personnel data lists.
## Report Prepared: 06/19/2018 11:25
</code></pre>
<p>The <code>poppler</code> library's &quot;layout&quot; mode (which <code>pdftools</code> uses brilliantly) combined with the author of the PDF not being evil will help us make short work of this since:</p>
<ul>
<li>there's a uniform header on each page</li>
<li>the &quot;layout&quot; mode returned uniform per-page, fixed-width columns</li>
<li>there's no &quot;special column tweaks&quot; that some folks use to make PDFs more readable by humans</li>
</ul>
<p>There are plenty of comments in the code, so I'll refrain from too much blathering about it, but the general plan is to go through each of the 119 pages and:</p>
<ul>
<li>convert the text to lines</li>
<li>find the header line</li>
<li>find the column start/end positions from the header on the page (since they are different for each page)</li>
<li>reading it in with <code>readr::read_fwf()</code></li>
<li>remove headers, preamble and epilogue cruft </li>
<li>turn it all into one data frame</li>
</ul>
<pre><code># we're going to process each page and read_fwf will complain violently
# when it hits header/footer rows vs data rows and we rly don't need to
# see all those warnings
read_fwf_q &lt;- quietly(read_fwf)

# go through each page
map_df(doc, ~{
  
  stri_split_lines(.x) %>% 
    flatten_chr() -> lines # want the lines from each page
  
  # find the header on the page and get the starting locations for each column
  keep(lines, stri_detect_regex, &quot;^Name&quot;) %>% 
    stri_locate_all_fixed(c(&quot;Name&quot;, &quot;Rank&quot;, &quot;Branch&quot;, &quot;Year&quot;, &quot;State&quot;)) %>% 
    map(`[`, 1) %>% 
    flatten_int() -> starts
  
  # now get the ending locations; cheating and using `NA` for the last column  
  ends &lt;- c(starts[-1] - 1, NA)

  # since each page has a lovely header and poppler's &quot;layout&quot; mode creates 
  # a surprisingly usable fixed-width table, the core idiom is to find the start/end
  # of each column using the header as a canary
  cols &lt;- fwf_positions(starts, ends, col_names = c(&quot;name&quot;, &quot;rank&quot;, &quot;branch&quot;, &quot;year&quot;, &quot;state&quot;))

  paste0(lines, collapse=&quot;\n&quot;) %>%        # turn it into something read_fwf() can read 
    read_fwf_q(col_positions = cols) %>%  # read it!
    .$result %>%                          # need to do this b/c of `quietly()`
    filter(!is.na(name)) %>%              # non-data lines
    filter(name != &quot;Name&quot;) %>%            # remove headers from each page
    filter(!stri_detect_regex(name, &quot;^(^This|Please|Report)&quot;)) # non-data lines (the last pg footer, rly)
  
}) -> xdf

xdf
## # A tibble: 7,699 x 5
##    name                       rank   branch                  year  state        
##    </code></pre>