返回介绍

5.4 学习解决问题的方法

发布于 2024-01-27 21:43:11 字数 10741 浏览 0 评论 0 收藏 0

本节包含好几个练习,你可以试着解析 PDF 脚本,同时挑战自己写 Python 代码的能力。首先,我们先来回顾一下已经写好的代码:

pdf_txt = 'en-final-table9.txt'
openfile = open(pdf_txt, "r")

double_lined_countries = [
  'Bolivia (Plurinational \n',
  'Democratic People\xe2\x80\x99s \n',
  'Democratic Republic \n',
  'Lao People\xe2\x80\x99s Democratic \n',
  'Micronesia (Federated \n',
  'Saint Vincent and \n',
  'The former Yugoslav \n',
  'United Republic \n',
  'Venezuela (Bolivarian \n',
]


def turn_on_off(line, status, prev_line, start, end='\n', count=0):
  """
    该函数用于检查该行会是否开始/结束于特定值。
    如果是,且上一行不是特殊行,
    则状态设为开/关(真/假)。
  """
  if line.startswith(start):
    status = True
  elif status:
    if line == end and prev_line != 'and areas':
      status = False
  return status


def clean(line):
  """
    清洗代码行中的换行符、空格以及特殊符号。

  """
  line = line.strip('\n').strip()
  line = line.replace('\xe2\x80\x93', '-')
  line = line.replace('\xe2\x80\x99', '\'')

  return line

countries = []
totals = []
country_line = total_line = False
previous_line = ''

for line in openfile:
  if country_line:
    if previous_line in double_lined_countries:
      line = ' '.join([clean(previous_line), clean(line)])
      countries.append(line)
    elif line not in double_lined_countries:
      countries.append(clean(line))

  elif total_line:
    if len(line.replace('\n', '').strip()) > 0:
      totals.append(clean(line))

  country_line = turn_on_off(line, country_line,
                 'and areas', previous_line)
  total_line = turn_on_off(line, total_line,
               'total', previous_line)
  previous_line = line


import pprint
data = dict(zip(countries, totals))
pprint.pprint(data)

有好几种方法可以解决我们面临的问题。在接下来的几节中,我们会讲到其中几种解决方法。

5.4.1 练习:使用表格提取,换用另一个库

前面我们对 PDF 转换成文本遇到的困难头痛不已,下面我们寻找其他方法来实现表格提取,不用 pdfminer。我们找到了 pdftables 库(http://pdftables.readthedocs.org/),这个库已经不再更新了(原作者的最后一次更新时间是两年多以前)。

我们需要安装必要的库(http://pdftables.readthedocs.io/en/latest/#installation),只需运行 pip install pdftables 和 pip install requests 即可完成安装。原作者并没有及时更新所有的文档,所以文档和 README.md 中的某些例子明显是错的。尽管如此,我们还是找到了一个“多合一”(all in one)的函数,可以用来获取我们想要的数据:

from pdftables import get_tables

all_tables = get_tables(open('EN-FINAL Table 9.pdf', 'rb'))

print all_tables

我们创建一个新的代码文件(pdf_table_data.py)并运行。你应该会看到旋风般滚动的数据,看起来就是我们要提取的数据。你会注意到,标题并不是完全正确,但每一行的内容似乎都包含在 all_tables 变量中。我们来仔细观察一下,看看如何提取我们想要的标题、数据列和注释。

你可能也注意到了,all_tables 是一个由列表组成的列表(或者叫矩阵)。它有很多行,每一行里还包含很多行。这种方法可能很适合表格提取,因为表格本质上就是行和列。 get_tables 函数返回的是每一页内容组成的表格,每个表格都包含一个行列表,每个元素又是由许多列组成的列表。

第一步,找到每一列的标题。我们试着查看输出的前几行,看能否找到列标题:

print all_tables[0][:6]

我们看一下第一页的前六行:

... [u'',
  u'',
  u'',
  u'',
  u'',
  u'',
  u'Birth',
  u'Female',
  u'genital mutila',
  u'tion/cutting (%)+',
  u'Jus',
  u'tification of',
  u'',
  u'',
  u'E'],
 [u'',
  u'',
  u'Child labour (%',
  u')+',
  u'Child m',
  u'arriage (%)',
  u'registration',
  u'',
  u'2002\u201320',
  u'12*',
  u'wife',
  u'beating (%)',
  u'',
  u'Violent disciplin',
  u'e (%)+ 9'],
 [u'Countries and areas',
  u'total',
  u'2005\u20132012*male',
  u'female',
  u'2005married by 15',
  u'\u20132012*married by 18',
  u'(%)+ 2005\u20132012*total',
  u'prwomena',
  u'evalencegirlsb',
  u'attitudessupport for thepracticec',
  u'2male',
  u'005\u20132012*female',
  u'total',
  u'2005\u20132012*male',
  u'female'],...

可以看到,标题都包含在前三个列表中,格式混乱。从 print 语句的输出中还可以看出,每行数据还是相当干净的。如果我们对比 PDF 文件手动设置标题(如下所示),可以得到干净的数据集:

headers = ['Country', 'Child Labor 2005-2012 (%) total',
       'Child Labor 2005-2012 (%) male',
       'Child Labor 2005-2012 (%) female',
       'Child Marriage 2005-2012 (%) married by 15',
       'Child Marriage 2005-2012 (%) married by 18',
       'Birth registration 2005-2012 (%)',
       'Female Genital mutilation 2002-2012 (prevalence), women',
       'Female Genital mutilation 2002-2012 (prevalence), girls',
       'Female Genital mutilation 2002-2012 (support)',
       'Justification of wife beating 2005-2012 (%) male',
       'Justification of wife beating 2005-2012 (%) female',
       'Violent discipline 2005-2012 (%) total',
       'Violent discipline 2005-2012 (%) male',
       'Violent discipline 2005-2012 (%) female']  ➊

for table in all_tables:
  for row in table:
    print zip(headers, row)  ➋

❶ 将所有标题添加到一个列表中,其中包括国名。现在我们可以将这个列表与行数据合并,将数据和标题对齐。

❷ 使用 zip 方法将标题与每一行数据合并。

从代码输出中可以看出,有些行我们已经匹配好了,但还有很多行不是国家行(和我们之前的结果类似,之前在表格中发现了多余的空格和空行)。

根据目前所学的内容,我们希望用编程加测试的方法解决这个问题。我们知道有些国家占了不止一行。我们还知道 PDF 文件用破折号(-)表示数据缺失,所以全空的行实际上不是数据行。从上一次 print 输出中我们就知道,每一页的数据从第五行开始。我们还知道,我们关注的最后一行是津巴布韦(Zimbabwe)。将我们已知的内容综合在一起,我们得到:

for table in all_tables:
  for row in table[5:]: ➊
    if row[2] == '':  ➋
      print row

❶ 在每一页中找出我们想要的那些行,即索引数为 5 之后的切片。

❷ 如果数据为空,打印查看该行内容。

运行代码,你会发现列表中包含一些随机分布的空白行,其中也不包含国名。这可能就是我们上一段脚本的问题所在。我们尝试将国名合并在一起,跳过其他空白行。我们还加上对津巴布韦(Zimbabwe)的测试:

first_name = ''

for table in all_tables:
  for row in table[5:]:
    if row[0] == '':  ➊
      continue
    if row[2] == '':
      first_name = row[0]  ➋
      continue
    if row[0].startswith(' '):  ➌
      row[0] = '{} {}'.format(first_name, row[0])
    print zip(headers, row)  ➍
    if row[0] == 'Zimbabwe':
      break  ➎

❶ 如果数据行索引数为 0 的值缺失,说明这一行不包含国名,是一个空行。下一行代码用 continue 跳过这一行,continue 是一个 Python 关键字,作用是转到 for 循环的下一次迭代。

❷ 如果数据行索引数为 2 的值缺失,我们知道这可能是国名的前半部分。本行代码将国名的前半部分保存为变量 first_name。下一行代码跳转到下一行数据。

❸ 如果数据行以空格开头,我们知道这是国名的后半部分。我们希望将国名的两部分重新合并在一起。

❹ 如果我们的假设正确,观察打印出的结果,数据应该是匹配好的。本行代码打印出每次迭代的内容,便于我们观察。

❺ 遇到津巴布韦(Zimbabwe)时,本行代码跳出 for 循环。

大部分数据看起来都是正确的,但我们还会发现一些异常数据。看下面这个例子:

[('Country', u'80    THE STATE OF T'),
('Child Labor 2005-2012 (%) total', u'HE WOR'),
('Child Labor 2005-2012 (%) male', u'LD\u2019S CHILDRE'),
('Child Labor 2005-2012 (%) female', u'N 2014'),
('Child Marriage 2005-2012 (%) married by 15', u'IN NUMBER'),
('Child Marriage 2005-2012 (%) married by 18', u'S'),
('Birth registration 2005-2012 (%)', u''),
.....

可以看到,开头的页码被误以为是国名。你知道哪些国家名称里有数字吗?我们当然不知道!我们添加一个对数字的测试,看是否能剔除坏数据。我们还注意到双行国家的对应并不正确。从输出来看,pdftables 在导入数据时会自动修正行首的空格。太好了!现在我们应该添加一个测试,测试上一行数据有没有 first_name:

from pdftables import get_tables
import pprint

headers = ['Country', 'Child Labor 2005-2012 (%) total',
       'Child Labor 2005-2012 (%) male',
       'Child Labor 2005-2012 (%) female',
       'Child Marriage 2005-2012 (%) married by 15',
       'Child Marriage 2005-2012 (%) married by 18',
       'Birth registration 2005-2012 (%)',
       'Female Genital mutilation 2002-2012 (prevalence), women',
       'Female Genital mutilation 2002-2012 (prevalence), girls',
       'Female Genital mutilation 2002-2012 (support)',
       'Justification of wife beating 2005-2012 (%) male',
       'Justification of wife beating 2005-2012 (%) female',
       'Violent discipline 2005-2012 (%) total',
       'Violent discipline 2005-2012 (%) male',
       'Violent discipline 2005-2012 (%) female']

all_tables = get_tables(open('EN-FINAL Table 9.pdf', 'rb'))

first_name = False
final_data = []

for table in all_tables:
  for row in table[5:]:
    if row[0] == '' or row[0][0].isdigit():
      continue
    elif row[2] == '':
      first_name = row[0]
      continue
    if first_name: ➊
      row[0] = u'{} {}'.format(first_name, row[0])
      first_name = False ➋
    final_data.append(dict(zip(headers, row)))
    if row[0] == 'Zimbabwe':
      break

pprint.pprint(final_data)

❶ 如果这一行有 first_name,那么在该行内将国名合并。

❷ 将 first_name 重新设置为 False,这样下一次迭代可以正常运行。

现在数据导入工作已全部完成。如果你希望数据结构与从 Excel 导入的数据完全相同,需要对数据做进一步处理,但我们已经可以将 PDF 中的数据保存成行数据。

 pdftables 已经不再受到积极的支持,它的开发者现在提供替代的新产品,但却是收费的(https://pdftables.com/)。依赖不受支持的代码是很危险的,我们也不能认为 pdftables 总是可用 3。但是,开源社区的一部分内容就是回馈,所以我们鼓励你找到好项目,为它做贡献,帮它宣传,希望像 pdftables 这样的项目能够保持开源,能够继续成长并发展。

3似乎的确有人在维护并支持一些活跃的 GitHub 分支(https://github.com/drj11/pdftables/network)。我们建议你关注这些仓库的动态,以满足 PDF 表格解析的需求。

下面,我们来看一下解析 PDF 数据的其他方法,其中包括手动清洗数据。

5.4.2 练习:手动清洗数据

我们来聊一聊一个大家闭口不谈却确实存在的事实。阅读本章的过程中,你可能一直想知道,我们为什么不修改 PDF 文本文件,这样处理起来会更方便。你可以这么做,这是解决问题的众多方法之一。但我们希望你能挑战一下,用 Python 工具处理这个文件。你也不是每次都能手动修改 PDF 文件。

如果在处理 PDF 或其他文件类型时遇到了困难,一种按部就班的方法是将数据提取到文本文件,然后手动处理数据。在这种情况下,提前预估一下你愿意在手动处理上花费的时间,然后将实际花费的时间控制在这个范围内。

想了解数据清洗自动化的更多内容,请查阅第 8 章。

5.4.3 练习:试用另一种工具

当最开始寻找用来解析 PDF 的 Python 库时,我们在网络上搜索其他人如何完成这个任务,并找到了 slate,它看起来很好用,但需要一些自定义代码。

想了解还有哪些可用的工具,我们试着搜索“extracting tables from pdf”(从 pdf 中提取表格),而不是搜索“parsing pdf python”(解析 pdf python),这样可以找到针对表格问题的解决方法(其中有一篇博客文章对几种工具做了对比,http://www.interhacktives.com/2014/03/12/extract-data-pdf/)。

对于像我们要解析的这种小型 PDF,我们可以使用 Tabula(http://tabula.technology/)。 Tabula 不一定总能解决问题,但它有一些很好的功能。

Tabula 的使用方法如下。

(1) 下载 Tabula(http://tabula.technology/)。

(2) 双击启动应用,这会在浏览器中打开 Tabula 工具。

(3) 上传童工 PDF 文件。

从这里开始,你需要修改 Tabula 选择抓取的内容。跳过标题行可以让 Tabula 找到每一页的数据并自动高亮,方便后续提取。首先,选择你感兴趣的表格(见图 5-3)。

图 5-3:在 Tabula 中选择表格

接下来,下载数据(见图 5-4)。

图 5-4:Tabula 的下载界面

点击“Download CSV”(下载 CSV 文件),你会得到类似图 5-5 中的数据。

图 5-5:提取的 CSV 数据

得到的数据并不完美,但比我们用 pdfminer 得到的数据更干净。

接下来的挑战是,解析 Tabula 创建的 CSV 文件。这与我们解析过的其他 CSV(见第 3 章)有所不同,要更杂乱一些。如果你遇到困难,可以先放在一边,等读完第 7 章再回来解决它。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文