- 本书赞誉
- 前言
- 目标读者
- 不适合阅读本书的读者
- 本书结构
- 什么是数据处理
- 遇到困难怎么办
- 排版约定
- 使用代码示例
- 致谢
- 第 1 章 Python 简介
- 第 2 章 Python 基础
- 第 3 章 供机器读取的数据
- 第 4 章 处理 Excel 文件
- 第 5 章 处理 PDF 文件 以及用 Python 解决问题
- 第 6 章 数据获取与存储
- 第 7 章 数据清洗:研究、匹配与格式化
- 第 8 章 数据清洗:标准化和脚本化
- 第 9 章 数据探索和分析
- 第 10 章 展示数据
- 第 11 章 网页抓取:获取并存储网络数据
- 第 12 章 高级网页抓取:屏幕抓取器与爬虫
- 第 13 章 应用编程接口
- 第 14 章 自动化和规模化
- 第 15 章 结论
- 附录 A 编程语言对比
- 附录 B 初学者的 Python 学习资源
- 附录 C 学习命令行
- 附录 D 高级 Python 设置
- 附录 E Python 陷阱
- 附录 F IPython 指南
- 附录 G 使用亚马逊网络服务
- 关于作者
- 关于封面
5.4 学习解决问题的方法
本节包含好几个练习,你可以试着解析 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论