如何将PDF文件中的行提取到csv文件中?

时间:2015-09-21 06:21:51

标签: pdf

我希望从此PDF file获取美国所有大学的列表,并将其放入CSV文件中。然后我将CSV文件导入SQL服务器(这样我就可以轻松运行查询)。

我尝试了几个在线pdf到csv转换器和基于Java的pdf到CSV教程。没有任何效果。今天我花了6-8个小时才完成这项工作并且失败了。当我导入csv时,我的csv文件搞砸了,我的数据库中有很多空值。 我甚至尝试过搜索DHS api,它可以给我这个信息,但没有找到。

有人可以帮助我提取大学,就像它们在pdf文件中显示一样吗?

PS:您也可以使用this url查看所有大学。但是,您必须手动滚动以提取所有结果。这将花费太长时间,数据不会采用pdf文件中给出的格式。

4 个答案:

答案 0 :(得分:9)

正如在对该问题的评论中所声称的那样,

  

考虑到相当直接的页面内容流样式,数据应该使用不太复杂的自定义文本提取器来提取。

详细说明:

页面内容流样式

常规表条目内容按条目逐项绘制,每个条目按字段按阅读顺序排列。因此,在浏览内容流时,我们不必尝试重新安排内容以建立该订单。这使得这项任务相当容易。

因此,主要工作是忽略非条目,即第一页上的标题,指示新首字母开始位置的条和页码。

我们这样做

  • 忽略处理标题和第一个字母栏的图形和非黑色文本;
  • 不接受不以“学校名称”列中的数据开头的条目,该栏目负责处理仅位于“校园名称”列中的页码。

(其他方法也可以,例如忽略底页区域中的所有内容来处理页码。)

现在我们只需将条目拆分为其字段。

文档结构再次有用,因为它是一个非常统一的文档,表格列在每个页面上具有相同的位置和尺寸。所以我们只需要以固定的x值进行剖析。

只有一个绊脚石:在某些条目中,原子文本块包含不同列的内容。例如。有时, F M 列的内容会被绘制为单个字符串,如" YN"并通过字符间距引入光学距离。

所以我们必须逐个字符地处理文本块,而不是整体。

示例实现

我在这里使用Java和PDF库iText(当前版本5.5.7开发快照)。这并不意味着使用不同的设置无法完成,这仅仅是我最习惯的设置。

作为分隔符,我使用制表符,因为其他可能的候选词也作为文本的一部分出现,我不想应对逃避它们。

这是为了处理内容而引入的自定义RenderListener类,如上所述:

public class CertifiedSchoolListExtractionStrategy implements RenderListener
{
    public CertifiedSchoolListExtractionStrategy(Appendable data, Appendable nonData)
    {
        this.data = data;
        this.nonData = nonData;
    }

    //
    // RenderListener implementation
    //
    @Override
    public void beginTextBlock() { }

    @Override
    public void endTextBlock() { }

    @Override
    public void renderImage(ImageRenderInfo renderInfo) { }

    @Override
    public void renderText(TextRenderInfo renderInfo)
    {
        try
        {
            Vector startPoint = renderInfo.getBaseline().getStartPoint();
            BaseColor fillColor = renderInfo.getFillColor();
            if (fillColor instanceof GrayColor && ((GrayColor)fillColor).getGray() == 0)
            {
                if (debug)
                    data.append(String.format("%4d\t%3.3f %3.3f\t%s\n", chunk, startPoint.get(I1), startPoint.get(I2), renderInfo.getText()));
                for (TextRenderInfo info : renderInfo.getCharacterRenderInfos())
                {
                    renderCharacter(info);
                }
            }
            else
            {
                if (debug)
                    nonData.append(String.format("%4d\t%3.3f %3.3f\t%s\n", chunk, startPoint.get(I1), startPoint.get(I2), renderInfo.getText()));
                if (currentField > -1)
                    finishEntry();
                entryBuilder.append(renderInfo.getText());
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            chunk++;
        }
    }

    public void renderCharacter(TextRenderInfo renderInfo) throws IOException
    {
        Vector startPoint = renderInfo.getBaseline().getStartPoint();

        float x = startPoint.get(I1);

        if (currentField > -1)
        {
            if (isInCurrentField(x))
            {
                entryBuilder.append(renderInfo.getText());
                return;
            }
            if (isInNextField(x))
            {
                currentField++;
                entryBuilder.append('\t').append(renderInfo.getText());
                return;
            }
            finishEntry();
        }
        if (isInNextField(x))
        {
            finishEntry();
            currentField = 0;
        }
        entryBuilder.append(renderInfo.getText());
    }

    public void close() throws IOException
    {
        finishEntry();
    }

    boolean isInCurrentField(float x)
    {
        if (currentField == -1)
            return false;

        if (x < fieldstarts[currentField])
            return false;

        if (currentField == fieldstarts.length - 1)
            return true;

        return x <= fieldstarts[currentField + 1];
    }

    boolean isInNextField(float x)
    {
        if (currentField == fieldstarts.length - 1)
            return false;

        if (x < fieldstarts[currentField + 1])
            return false;

        if (currentField == fieldstarts.length - 2)
            return true;

        return x <= fieldstarts[currentField + 2];
    }

    void finishEntry() throws IOException
    {
        if (entryBuilder.length() > 0)
        {
            if (currentField == fieldstarts.length - 1)
            {
                data.append(entryBuilder).append('\n');
            }
            else
            {
                nonData.append(entryBuilder).append('\n');
            }

            entryBuilder.setLength(0);
        }
        currentField = -1;
    }

    //
    // hidden members
    //
    final Appendable data, nonData;
    boolean debug = false;

    int chunk = 0;
    int currentField = -1;
    StringBuilder entryBuilder = new StringBuilder();

    final int[] fieldstarts = {20, 254, 404, 415, 431, 508, 534};
}

CertifiedSchoolListExtractionStrategy.java

我们可以像这样使用它:

@Test
public void testCertifiedSchoolList_9_16_2015() throws IOException
{
    try (   Writer data = new OutputStreamWriter(new FileOutputStream(new File(RESULT_FOLDER, "data.txt")), "UTF-8");
            Writer nonData = new OutputStreamWriter(new FileOutputStream(new File(RESULT_FOLDER, "non-data.txt")), "UTF-8")    )
    {
        CertifiedSchoolListExtractionStrategy strategy = new CertifiedSchoolListExtractionStrategy(data, nonData);
        PdfReader reader = new PdfReader("certified-school-list-9-16-2015.pdf");

        PdfReaderContentParser parser = new PdfReaderContentParser(reader);
        for (int page = 1; page <= reader.getNumberOfPages(); page++)
            parser.processContent(page, strategy);
        strategy.close();
    }
}

ExtractCertifiedSchoolList.java

现在data.txt包含所有条目,以制表符分隔的行和non-data.txt所有内容都被忽略。

幕后花絮

要了解这里发生的事情,首先要知道PDF中的页面内容是如何组织的以及如何(对于给出的示例代码)iText对其进行操作。

PDF内部

PDF文档是由许多基础对象类型构建的结构,一些基本类型(数字,字符串,...)和一些更复杂的类型(其他对象或流的数组或字典)。

PDF文档中的页面由这样的字典对象表示,该对象包含定义某些页面属性(如页面维度)的条目以及引用定义页面上绘制内容的对象的其他条目:内容流。

内容流基本上包含一系列操作,可能

  • 选择一种颜色(用于抚摸或填充),
  • 定义一条路径(移动到某个点,行到另一个点,曲线到另一个点,......),
  • 描边或填充这样的路径,
  • 在某处绘制一些位图图片,
  • 在某处绘制一些文字,或
  • 做很多其他事情。

对于手头的问题,我们主要对绘制文本所涉及的操作感兴趣。与文字处理程序相比,操作不是采用这一长串文本并将其排列为段落,而是更原始地在此处移动文本位置绘制此短文字符串再次移动文本位置在那里绘制另一个字符串。

E.g。在示例PDF中,绘制表头和第一个输入行的操作是:

/TT2 1 Tf

选择大小为1的字体 TT2

9.72 0 0 9.72 20.16 687.36 Tm

设置文本矩阵,将文本插入坐标移动到20.16,687.36,然后按比例缩放9.72。

0 g

选择灰度填充颜色黑色

0 Tc
0 Tw

选择其他字符和单词间距为0。

(SCHOOL)Tj

画&#34;学校&#34;这里。

/TT1 1 Tf

选择字体 TT1

3.4082 0 TD

在x方向上将文本插入点移动3.4082。

<0003>Tj

绘制一个空格字符(当前字体使用不同的编码,每个字符使用16位,而不是8,这里以十六进制表示。)

/TT2 1 Tf
.2261 0 TD
[(NAME)-17887.4(CAMPUS)]TJ

选择字体,移动文本插入点,然后绘制字符串&#34; NAME&#34;,然后绘制17887.4文本单位的间隙,然后绘制&#34; CAMPUS&#34;。

/TT1 1 Tf
24.1809 0 TD
<0003>Tj
/TT2 1 Tf
.2261 0 TD
[(NAME)-8986.6(F)-923.7(M)-459.3(CITY)-6349.9(ST)-1390.2(CAMPUS)]TJ
/TT1 1 Tf
28.5147 0 TD
<0003>Tj
/TT2 1 Tf
.2261 0 TD
(ID)Tj

绘制标题行的其余部分。

/TT4 1 Tf
-56.782 -1.3086 TD

向左移动56.782并向下移动1.3086,即向第一个输入行的开头移动。

("I)Tj
/TT3 1 Tf
.6528 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(Am")Tj
/TT3 1 Tf
1.7783 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(School)Tj
/TT3 1 Tf
2.6919 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
[(Inc.)-16894.2("I)]TJ
/TT3 1 Tf
18.9997 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(Am")Tj
/TT3 1 Tf
1.7783 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(School)Tj
/TT3 1 Tf
2.6919 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
[(Inc.)-8239.9(Y)-1018.9(N)-576.7(Mount)]TJ
/TT3 1 Tf
15.189 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
[(Shasta)-2423.3(CA)-2443.7(41789)]TJ

绘制第一个输入行。

正如您所见,并且如上所述,表格内容按阅读顺序绘制。甚至多行列条目也按所需顺序排列,例如校园名称&#34; A F of Westlake Village&#34;:

[(Inc.)-7228.7(A)]TJ
/TT3 1 Tf
9.26 0 TD 
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(F)Tj
/TT3 1 Tf
.4595 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(International)Tj
/TT3 1 Tf
5.2886 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(of)Tj
/TT3 1 Tf
.8325 0 TD
<0003>Tj
/TT4 1 Tf
.2261 0 TD
(Westlake)Tj
/TT3 1 Tf
3.7739 0 TD
<0003>Tj
/TT4 1 Tf
-11.8374 -1.3086 TD

向下移动到列的第二行。

(Village)Tj
15.4938 1.3086 TD

再次上移到条目的主线。

[(Y)-1018.9(N)-576.7(Westlake)]TJ 

因此我们可以消化文本,不需要排序(内容可以以完全不同的方式排序)。

但我们也看到没有明显的列起点和终点。因此,要将文本与列相关联,我们必须计算每个字符的位置,并将它们与外部给定的列起始位置进行比较。

库支持解析

PDF库通常提供一些帮助解析此类内容流的机制。

有两种基本体系结构,库可以解析内容流

  • 作为一个整体,并将其作为大量定位的文本块或
  • 提供
  • 或使用侦听器模式分段和转发单个定位的文本块。

最初的变体似乎更容易处理,但可能有很大的资源需求(我遇到过多MB内容流),而第二个似乎有点难以处理,但内存需求较小。

我使用的库(iText)遵循后一种方法,但您的问题也可以使用前一个库来解决。

RenderListener是在此实现的侦听器接口,renderText方法检索具有位置的个人文本块。

在上面的实现(CertifiedSchoolListExtractionStrategy)中,renderText方法首先检查与块相关联的填充颜色,并仅转发黑色文本以便在renderCharacter中进一步处理。该方法(和一些助手)依次检查文本所在的字段(通过硬编码的位置边界),并相应地导出制表符分隔值。这个逻辑同样也可以使用其他库来实现。

答案 1 :(得分:7)

另一个解决方案没有花太多精力来编写代码来获取pdf:有一个带有-layout标记的linux工具,如askubuntu中已经提到的那样。它被称为pdftotext

$ pdftotext -layout <input.pdf> <output.txt>

对于您提供的pdf file,它非常有前景。当然,它不是解决您问题的完整解决方案。但是你要做的就是清理文本输出。这可能比其他解决方案的时间更短。

以下是一个示例:

$ head -30 test.txt
                                                                                                          Updated
                                     SEVP Certified Schools                                      September 16, 2015
SCHOOL NAME                                     CAMPUS NAME                            F M CITY                     ST   CAMPUS ID
"I Am" School Inc.                              "I Am" School Inc.                     Y N Mount Shasta             CA     41789
424 Aviation                                    424 Aviation                           N Y Miami                    FL     103705
                                                            ‐ A ‐
A F International School of Languages Inc.      A F International College              Y   N Los Angeles            CA      9538
A F International School of Languages Inc.      A F International of Westlake          Y   N Westlake Village       CA     57589
                                                Village
A. T. Still University of Health Sciences       Kirksville Coll of Osteopathic         Y   N Kirksville         MO         3606
                                                Medicine
Aaron School                                    Aaron School ‐ 30th Street             Y   N   New York             NY    159091
Aaron School                                    Aaron School                           Y   N   New York             NY    114558
ABC Beauty Academy, INC.                        ABC Beauty Academy, INC.               N   Y   Flushing             NY    95879
ABC Beauty Academy, LLC                         ABC Beauty Academy                     N   Y   Garland              TX    50677
Abcott Institute                                Abcott Institute                       N   Y   Southfield           MI    197890
Aberdeen Catholic School System                 Roncalli Primary                       Y   N   Aberdeen             SD    180510
Aberdeen Catholic School System                 Roncalli                               Y   N   Aberdeen             SD    21405
Aberdeen Catholic School System                 Roncalli Elementary                    Y   N   Aberdeen             SD    180511
Aberdeen School District 6‐1                    Aberdeen Central High School           Y   N   Aberdeen             SD    36568
Abiding Savior Lutheran School                  Abiding Savior Lutheran School         Y   N   Lake Forest          CA     9920
Abilene Christian Schools                       Abilene Christian Schools              Y   N   Abilene              TX     8973
Abilene Christian University                    Abilene Christian University           Y   N   Abilene              TX     7498
Abington Friends School                         Abington Friends School                Y   N   Jenkintown           PA    20191
Above It All, Inc                               Benchmark Flight /Hawaii Flight        N   Y   Kailua‐Kona          HI    24353
                                                Academy
Abraham Baldwin Agricultural College            Tifton Campus                          Y   N Tifton             GA         6931
Abraham Joshua Heschel School                   Abraham Joshua Heschel School          Y   N New York           NY        106824

ABT Jacqueline Kennedy Onassis School           ABT Jacqueline Kennedy Onassis         Y   Y New York               NY     52401

因此,这会将您的文本输出转换为数据库可读的csv文件。也许你或其他人可能更喜欢这种做法。

答案 2 :(得分:2)

我曾经有一个做过这种工作的Ruby项目。 我使用了宝石pdf /阅读器,甚至它dit工作,但我建议不要使用这个方法,你的PDF的内容没有字段开始和停止的标记,而是你必须测量每个文本的位置(和那里每个字段有很多部分),这里是第一个字段的例子

"I
NUL ETX
Am"
NUL ETX
School
NUL ETX
Inc.

并将其与您必须通过试验找到的边界进行比较,例如“如果位置距离左边距2.54cm并且距左边距<5.78cm”等。 这很乏味且容易出错。

最简单的解决方案是以某种方式读取second url的整个文本内容,方法是手动滚动,选择内容并将其复制到编辑器中并删除头部和尾部的额外内容或使用一些网络抓取宝石,如机械化,然后转换此文本为CSV。最后一部分很容易,因为结构是固定的

"I Am" School
118 Siskiyou Avenue
Mount Shasta , CA , 96067
5309266263  <--end of first record
424 Aviation
13230 SW 132 Ave.
Miami , FL , 33186
7862424848  <--end of second record

如果你需要最后一部分的帮助,没问题

如果这是一次操作,你也可以使用像able2extract这样的工具(如果你在windows上)它读取pdf并保存在Excel中,我使用它的时间结果是不错的,布局完好无损。

答案 3 :(得分:0)

对于这种PDF提取,您可以使用IntelliGet(http://akribiatech.com/intelliget)。像下面这样的简单脚本可以满足您的目的

userVariables = school, campus;
{ start = IsNumeric(Substring(Line(0),112,115)); 
  school = ""; campus = "";
  { start = 1;
    maxCount = 2;
    school = Concat(school, " ", Trim(Substring(Line(0),1,52)));
    campus = Concat(campus, " ", Trim(Substring(Line(0),53,82)));
  }
  output = Concat(Trim(school), "|", Trim(campus));
}
相关问题