- 本书赞誉
- 前言
- 目标读者
- 不适合阅读本书的读者
- 本书结构
- 什么是数据处理
- 遇到困难怎么办
- 排版约定
- 使用代码示例
- 致谢
- 第 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.3 利用 pdfminer 解析 PDF
众所周知,处理 PDF 文件十分困难,我们将学习如何解决在代码中遇到的问题,并掌握一些解决问题的基本方法。
我们希望首先采集国家名称,因为国家名称是最终数据集的键。打开文本文件,你会发现国家出现之前有 8 行。第 8 行的内容是 and areas:
5 TABLE 9 CHILD PROTECTION 6 7 Countries 8 and areas 9 Afghanistan 10 Albania 11 Algeria 12 Andorra
浏览一下整个文本文档,你会发现相同的规律。因此,我们要创建一个开关变量,在遇到 and areas 这一行时控制采集过程的开始和结束。
为了完成这个任务,我们需要修改 for 循环,添加一个布尔变量,即 True/False 变量。在遇到 and areas 那一行时将布尔变量设置为 True:
country_line = False ➊ for line in openfile: if line.startswith('and areas'): ➋ country_line = True ➌
❶ 将 country_line 设置为 False,因为默认行里不包含国家。
❷ 搜索以 and area 开头的行。
❸ 将 country_line 设置为 True。
我们要解决的下一个问题是何时将布尔变量再次设置为 False。花点时间查看文本文件,试着找到其中的规律。你怎么能知道国家列表的结尾在哪里呢?
观察下面的文本片段,你会注意到其中有一个空行:
45 China 46 Colombia 47 Comoros 48 Congo 49 50 total 51 10 52 12
但 Python 怎么识别空行呢?在脚本中添加一行代码,打印出每一行的 Python 表示(查阅 7.2.2 节,可以了解关于字符串格式化的更多内容):
country_line = False for line in openfile: if country_line: ➊ print '%r' % line ➋ if line.startswith('and areas'): country_line = True
❶ 在经历了 for 循环的前一次迭代后,如果 country_line 的值为 True,那么……
❷ ……打印出该行的 Python 表示。
观察输出,你会注意到现在所有行的结尾都多出一些字符:
45 'China \n' 46 'Colombia \n' 47 'Comoros \n' 48 'Congo \n' 49 '\n' 50 'total\n' 51 '10 \n' 52 '12 \n'
\n 是一行结束的标志,或者叫换行符。我们现在用它作为修改 country_line 变量的标志(marker)。如果 country_line 的值为 True,而 line 的值为 \n,我们就应该将 country_line 设置为 False,因为这一行标志着国名的结束:
country_line = False for line in openfile: if country_line: ➊ print line if line.startswith('and areas'): country_line = True elif country_line: if line == '\n': ➋ country_line = False
❶ 如果 country_line 的值为 True,打印出该行,我们可以查看国家名称。这段代码在前,是因为我们不希望它出现在 and areas 测试的后面。我们只想打印出实际的国名,而不想打印出 and areas 这一行。
❷ 如果 country_line 的值为 True,且 line 的值为换行符,则将 country_line 设置为 False,因为国家列表已经结束了。
现在运行代码,返回的似乎是包含国家的所有行。我们最后会将其转换成国家列表。现在,对于我们想要采集的数据,我们要寻找相应的标志,然后重复上面的做法。我们想要的是童工数据和童婚数据。首先来看童工数据,我们需要总数、男童数和女童数。我们先来看总数。
我们将利用相同的方法找出童工总数。
(1) 创建一个 True/False 的开关变量。
(2) 寻找开始标志,将开关变量设置为真。
(3) 寻找结束标志,将开关变量设置为假。
查看一下文本,你会发现数据的开始标志是 total。看一下你所创建文本文件的第 50 行,这里出现了第一个标志 2:
2你的文本编辑器很可能可以选择显示行编号,甚至可能可以直接“跳转”到某一行。可以用谷歌搜索一下这些功能的使用方法。
45 China 46 Colombia 47 Comoros 48 Congo 49 50 total 51 10 52 12
结束标志还是换行符或 \n,你可以在 71 行看到:
68 6 69 46 70 3 71 72 26 y 73 5
我们将这个逻辑添加到代码中,然后用 print 查看结果:
country_line = total_line = False ➊ for line in openfile: if country_line or total_line: ➋ print line if line.startswith('and areas'): country_line = True elif country_line: if line == '\n': country_line = False if line.startswith('total'): ➌ total_line = True elif total_line: if line == '\n': total_line = False
❶ 将 total_line 设置为 False。
❷ 如果 country_line 或 total_line 的值为 True,输出该行的内容,方便我们查看数据。
❸ 找到 total_line 的起始点,将 total_line 设置为 True。本行下面的代码与前面 country_line 的代码逻辑相同。
现在我们的代码有一些冗余。我们在重复一些相同的代码,只是开关变量有所不同。这就引出了如何创建非冗余代码的话题。在 Python 中,我们可以用函数来执行重复操作。也就是说,我们可以将这些操作放到一个函数里,然后调用函数执行操作,而不必每次输入全部代码手动执行这些操作。如果我们想测试 PDF 的每一行,可以使用函数。
第一次编写函数时,往往不清楚应该将函数放在代码的什么位置。你需要先写好函数的代码,然后再调用函数。这样 Python 就知道函数的功能是什么。
我们将函数命名为 turn_on_off,设置它最多接收 4 个参数。
· line 是我们目前所在的行。
· statue 是一个布尔变量(True 或 False),代表函数的开或关。
· start 是我们要寻找的开始标志——它会触发开或 True 状态。
· end 是我们要寻找的结束标志——它会触发关或 False 状态。
修改代码,将函数框架添加到 for 循环之前。不要忘记添加函数功能的说明——当日后查看这个函数时,你不会一头雾水。这些说明文字叫作文档字符串(docstring):
def turn_on_off(line, status, start, end='\n'): ➊ """ 这个函数用于检查该行是否以特定值开始/结束。 ➋ 如果该行确实以特定值开始/结束,则状态设为开/关(真/假)。 """ return status ➌ country_line = total_line = False for line in openfile: .....
❶ 本行代码是函数的开始,函数最多接收 4 个参数。前三个参数 line、status 和 start 是必需(required)参数,也就是说,由于它们没有默认值,一定要对它们赋值。最后一个参数 end,默认值是换行符,因为这是我们文件的规律。在调用函数时,我们可以传入其他值来替换默认值。
❷ 一定要写函数说明(或文档字符串),这样你才能清楚它的功能。函数说明不必追求完美,只要有就行。以后你可以随时更新。
❸ return 语句是退出函数的正确方法。这个函数返回的是 status,值为 True 或 False。
有默认值的参数要放在最后
在编写函数时,没有默认值的参数一定要放在有默认值的参数前面。这也是上面的例子中 end='\n' 是最后一个参数的原因。我们可以看到,有默认值的参数像是一个键值对(即 value_name=value),= 后面即为默认值(在上面的例子中默认值为\n)。
在函数被调用时,Python 会计算参数的值。如果我们想调用前面关于国家的函数,调用方法是这样的:
turn_on_off(line, country_line, 'and areas')
这里利用了 end 的默认值。如果你想将默认值替换为两个换行符,可以这样调用:
turn_on_off(line, country_line, 'and areas', end='\n\n')
假设我们将 status 的默认值设置为 False。我们要如何修改代码?
这是修改前函数的第一行:
def turn_on_off(line, status, start, end='\n'):
下面给出两种修改方法:
def turn_on_off(line, start, end='\n', status=False): def turn_on_off(line, start, status=False, end='\n'):
status 参数要放在必需参数之后。在调用新函数时,我们可以使用 end 和 status 的默认值,也可以用其他值替换:
turn_on_off(line, 'and areas') turn_on_off(line, 'and areas', end='\n\n', status=country_line)
如果你不小心把有默认值的参数放在必需参数之前,Python 会报错:SyntaxError: non-default argument follows default argument。你不必记住这句话,但要注意的是,如果你遇到这个错误,要知道它指的是什么意思。
现在把代码从 for 循环中移到函数中。我们想在新的 turn_on_off 函数中复制前面 country_line 的逻辑:
def turn_on_off(line, status, start, end='\n'): """ 这个函数用于检查该行是否以特定值开始/结束。 如果该行确实以特定值开始/结束,则状态设为开/关(真/假)。 """ if line.startswith(start): ➊ status = True elif status: if line == end: ➋ status = False return status ➌
❶ 将寻找开始行的标志替换为 start 变量。
❷ 将我们用的结束文字替换为 end 变量。
❸ 基于相同的逻辑,返回 status 变量(end 表示 False,start 表示 True)。
现在我们在 for 循环中调用这个函数,将前面所有代码放在一起之后,脚本是这个样子的:
pdf_txt = 'en-final-table9.txt' openfile = open(pdf_txt, "r") def turn_on_off(line, status, start, end='\n'): """ 这个函数用于检查该行是否以特定值开始/结束。 如果该行确实以特定值开始/结束,则状态设为开/关(真/假)。 """ if line.startswith(start): status = True elif status: if line == end: status = False return status country_line = total_line = False ➊ for line in openfile: if country_line or total_line: ➋ print '%r' % line country_line = turn_on_off(line, country_line, 'and areas') ➌ total_line = turn_on_off(line, total_line, 'total') ➍
❶ 根据 Python 的语法,一连串 = 符号的意思是,我们将最后一个值赋值给前面每一个变量。本行代码将 False 同时赋值给 country_line 和 total_line。
❷ 我们仍然想要记录在开状态下每一行包含的数据。因此我们使用了 or。Python 中 or 的意思是,如果二者之一为真,执行下面的命令。本行代码的意思是,如果 country_line 和 total_line 有一个值为 True,打印出该行的内容。
❸ 对国家调用函数。将函数返回的状态保存到 country_line 变量中,用于下一次 for 循环。
❹ 对总数调用函数。这一行代码与上一行对国名的用法是相同的。
下面将国家和总数保存成列表。然后将这些列表转换成一个字典,字典的键是国名,字典的值是童工总数。这样我们就可以判断是否需要清洗数据。
创建两个列表的代码如下:
countries = [] ➊ totals = [] ➋ country_line = total_line = False for line in openfile: if country_line: ➌ countries.append(line) ➍ elif total_line: totals.append(line) ➎ country_line = turn_on_off(line, country_line, 'and areas') total_line = turn_on_off(line, total_line, 'total')
❶ 创建空的国家列表。
❷ 创建空的总数列表。
❸ 注意我们删除了 if country_line or total_line 语句。下面我们将这条语句分开来写。
❹ 如果该行包含国家,将国家添加到国家列表中。
❺ 这一行采集的是总数,与上一行的采集国家的用法相同。
我们将采用“拉链方法”(zipping)将国家和总数两个数据集合并。zip 函数从每一个列表中取出一个元素,然后将其配对,直到所有的元素全部配对完成。我们可以将合并后的列表传递给 dict 函数,从而将其转换成字典。
在脚本最后添加以下代码:
import pprint ➊ test_data = dict(zip(countries, totals)) ➋ pprint.pprint(test_data) ➌
❶ 导入 pprint 库。对于复杂的数据结构,这个库的打印格式可读性更好。
❷ 将国家和总数合并到一起,然后转换成一个字典,将字典保存到一个叫作 test_data 的变量中。
❸ 将 test_data 传递给 pprint.pprint() 函数,以美观的格式打印出数据。
现在运行脚本,你会得到类似这样的一个字典:
{'\n': '49 \n', ' \n': '\xe2\x80\x93 \n', ' Republic of Korea \n': '70 \n', ' Republic of) \n': '\xe2\x80\x93 \n', ' State of) \n': '37 \n', ' of the Congo \n': '\xe2\x80\x93 \n', ' the Grenadines \n': '60 \n', 'Afghanistan \n': '10 \n', 'Albania \n': '12 \n', 'Algeria \n': '5 y \n', 'Andorra \n': '\xe2\x80\x93 \n', 'Angola \n': '24 x \n', 'Antigua and Barbuda \n': '\xe2\x80\x93 \n', 'Argentina \n': '7 y \n', 'Armenia \n': '4 \n', 'Australia \n': '\xe2\x80\x93 \n', ......
现在我们需要做一些数据清洗工作。更详细的内容将会在第 7 章中介绍。现在我们需要做的是清洗字符串,因为它们的可读性很差。我们将创建一个函数来清洗每一行的内容。将这个函数放在 for 循环前面,与另一个函数放在一起:
def clean(line): """ 清洗代码中的换行符、空格以及其他特殊符号。 """ line = line.strip('\n').strip() ➊ line = line.replace('\xe2\x80\x93', '-') ➋ line = line.replace('\xe2\x80\x99', '\'') return line ➌
❶ 删除该行中的 \n,然后重新赋值给 line,现在 line 中保存的是清洗后的数据。
❷ 替换特殊字符编码。
❸ 返回清洗后的新字符串。
在上面的数据清洗中,我们可以把方法调用合并在一起,像这样:
line = line.strip('\n').strip().replace( '\xe2\x80\x93', '-').replace('\xe2\x80\x99s', '\'')
然而,想要格式美观,每一行 Python 代码的长度不应超过 80 个字符。这只是一个建议,并不是规定,但控制每行代码的长度可以提高代码的可读性。
下面我们将 clean_line 函数应用到 for 循环中:
for line in openfile: if country_line: countries.append(clean(line)) elif total_line: totals.append(clean(line))
现在运行脚本,我们得到的输出更加接近我们的目标:
{'Afghanistan': '10', 'Albania': '12', 'Algeria': '5 y', 'Andorra': '-', 'Angola': '24 x', 'Antigua and Barbuda': '-', 'Argentina': '7 y', 'Armenia': '4', 'Australia': '-', 'Austria': '-', 'Azerbaijan': '7 y', ...
浏览一下输出,你会发现我们的方法没能充分解析所有的数据。我们需要找出问题的原因。
名字超过一行的国家似乎被分成了两条数据记录。从玻利维亚(Bolivia)的数据可以发现这一点:我们有两条记录,一条是 'Bolivia (Plurinational': '',,另一条是 'State of)': '26 y',。
在 PDF 文件里可以查看数据的组织结构。你可以在 PDF 中看到图 5-2 中的这几行。
图 5-2:PDF 文件中的玻利维亚数据
PDF 文件仿佛兔子洞一般。处理每一个 PDF 文件都要用到特殊的技巧。由于我们对这个 PDF 只需要解析一次,所以我们做了许多人工检查的工作。如果需要定期解析这个 PDF,我们需要仔细查看数据随时间变化的规律,然后用程序来处理这些规律,还要对代码进行检查和测试,确保导入的数据正确。
解决这个问题有两种方法。我们可以创建一个占位符,找到总数里面的空白行,然后将空白行与后面的数据行合并。另一种方法是找出那些国名长度不止一行的国家。由于我们的数据集不是很大,所以我们将尝试第二种方法。
我们将创建一个列表,里面包含每一个跨行国家的第一行内容,然后在脚本中用这个列表来检查每一行。你需要将这个列表放在 for 循环之前。参考元素通常会放在脚本的开头,在必要时方便找到并修改。
将 Bolivia (Plurinational 添加到双行国家组成的列表中:
double_lined_countries = [ 'Bolivia (Plurinational', ]
现在我们需要修改 for 循环,检查上一行内容是否包含在 double_lined_countries 列表中,如果是的话,将上一行与这一行合并。为此我们需要创建一个 previous_line 变量。然后在 for 循环的结尾对 previous_line 变量赋值。只有这样,在代码进行到循环的下一次迭代时,我们才能将两行合并:
countries = [] totals = [] country_line = total_line = False previous_line = '' ➊ for line in openfile: if country_line: countries.append(clean(line)) elif total_line: totals.append(clean(line)) country_line = turn_on_off(line, country_line, 'and areas') total_line = turn_on_off(line, total_line, 'total') previous_line = line ➋
❶ 创建 previous_line 变量,值为空字符串。
❷ 在 for 循环的结尾,将 previous_line 的值修改为当前行的内容。
现在有了 previous_line 变量,我们可以检查 previous_line 是否在 double_lined_countries 列表中,这样我们就知道何时将当前行与上一行合并。另外,我们还要将新合并的一行添加到国家列表中。如果国名的前半部分在 double_lined_countries 列表中的话,一定不要将第一行添加到国名列表中。
根据上面的描述,对代码做如下修改:
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))
❶ 我们需要 if country_line 的逻辑,因为它只与国名相关。
❷ 如果 previous_line 在 double_lined_countries 列表中,那么将 previous_line 与当前行合并,并将合并后的内容赋值给 line 变量。你可以看到,join 的作用是利用最前面的字符串将一个字符串列表合并在一起。本行代码使用空格作为连接字符。
❸ 如果该行不在 double_lined_countries 列表中,那么将该行内容添加到国家列表中。这里我们用的是 elif,在 Python 中的意思是 else if。如果你想使用一种不同于 if - else 的逻辑流,这是一个很好用的工具。
重新运行脚本,我们发现 'Bolivia (Plurinational State of)' 已经合在一起了。现在我们需要检查结果中是否包含了所有国家。由于数据集比较小,我们可以手动检查,但如果数据集较大的话,你需要将检查过程自动化。
数据检查自动化
何时手动检查数据,何时用 Python 自动化完成,怎么选择?下面给出几点建议。
· 如果要定期反复解析数据,选择自动化。
· 如果你的数据集比较大,你很可能应该选择自动化。
· 如果你的数据集可控,而且你只需要解析一次,那么你选哪种方式都可以。在我们的例子中,数据集很小,所以我们没有选择自动化。
用 PDF 阅读器查看 PDF 文件,找出所有占两行的国家名称:
Bolivia (Plurinational State of) Democratic People's Republic of Korea Democratic Republic of the Congo Lao People's Democratic Republic Micronesia (Federated States of) Saint Vincent and the Grenadines The former Yugoslav Republic of Macedonia United Republic of Tanzania Venezuela (Bolivarian Republic of)
我们知道,这些国名的 Python 表示可能不是这样的,所以我们需要将国家打印出来,看看它们的 Python 表示是什么样子,然后将其添加到列表中:
if country_line: print '%r' % line ➊ if previous_line in double_lined_countries:
❶ 添加 print '%r' 语句,输出国名的 Python 表示。
运行脚本,将双行国名的 Python 表示添加到 double_lined_countries 列表中:
double_lined_countries = [ 'Bolivia (Plurinational \n', 'Democratic People\xe2\x80\x99s \n', 'Democratic Republic \n', 'Micronesia (Federated \n', #... 糟糕! ]
我们的输出中漏掉了 Lao People's Democratic Republic(老挝人民民主共和国),但它在 PDF 中占了两行。我们打开 PDF 的文本文件,看看问题出在哪里。
看完文本文件,你能发现问题所在吗?再看一下 turn_on_off 函数。这个函数的原理与文本的书写方式有什么关系?
问题在于,and areas 之后紧跟着一个空行(\n),这正是我们要寻找的标志。查看我们创建的文本文件,你会发现在 1345 行出现了意料之外的空行:
... 1343 Countries 1344 and areas 1345 1346 Iceland 1347 India 1348 Indonesia 1349 Iran (Islamic Republic of) ...
这说明我们的函数没有正常运行。解决这个问题有好几种方法。对于这个例子来说,我们可以加入更多的代码逻辑,保证开 / 关代码的运行符合预期。开始采集国名时,在结束采集之前应该采集到了至少一个国家。如果一个国家都没有采集到的话,那么我们不应该结束采集过程。我们还可以使用上一行来解决这个问题。我们可以在开 / 关函数中检查上一行代码,确保它不属于某些特殊行。
我们采用增加特殊行的方法,以防遇到其他异常:
def turn_on_off(line, status, start, prev_line, end='\n'): """ 该函数用于检查该行会是否开始/结束于特定值。 如果是,且上一行不是特殊行, 则状态设为开/关(真/假)。 """ if line.startswith(start): status = True elif status: if line == end and prev_line != 'and areas': ➊ status = False return status
❶ 如果当前行的值等于 end,而且上一行的值不等于 and areas,那么我们可以结束数据采集。这里我们用的是 !=,这是 Python 用来测试“不相等”的方法。与 == 类似,!= 返回的也是布尔值。
你还需要修改调用函数的代码,将 previous_line 传入函数:
country_line = turn_on_off(line, country_line, 'and areas', previous_line) total_line = turn_on_off(line, total_line, 'total', previous_line)
回到我们最开始的任务——创建双行国家的列表,确保采集到所有双行国家。我们前面进行到了这一步:
double_lined_countries = [ 'Bolivia (Plurinational \n', 'Democratic People\xe2\x80\x99s \n', 'Democratic Republic \n', ]
查看 PDF 文件,我们看到下一个双行国家是 Lao People's Democratic Republic(老挝人民民主共和国)。我们继续将脚本输出的其他双行国家添加到这个列表中:
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', ]
如果你的列表看起来和上面的列表相似的话,运行脚本,你的输出应该找出了所有占两行的国名。一定要在脚本结尾添加 print 语句来查看国家列表:
import pprint pprint.pprint(countries)
前面我们在国家列表上花了不少时间,你能想出解决这个问题的其他方法吗?看一下几个双行国家的第二行:
' Republic of Korea \n' ' Republic \n' ' of the Congo \n'
它们有什么共同点?都以空格开头。用代码检查每一行开头是否有三个空格,这是一种更有效的做法。但是采用前面第一种方法,可以在采集数据的过程中发现数据集的部分缺失。随着你编程水平的不断提高,你将学会解决同一问题的各种方法,然后找出最佳方法。
下面我们看一下童工总数和国家的对应情况。修改 pprint 语句,如下所示:
import pprint data = dict(zip(countries, totals)) ➊ pprint.pprint(data) ➋
❶ 调用 zip(countries, totals),将国家列表和总数列表合并。这样把两个列表变成了元组。然后我们把元组传递给 dict 函数,将其转换成字典。
❷ 打印出我们刚创建的 data 变量。
返回的是一个字典,国家名称是字典的键,童工总数是字典的值。这并不是我们最终的数据格式。我们这样做是为了查看当前的数据。结果应该是像这样的:
{'': '-', 'Afghanistan': '10', 'Albania': '12', 'Algeria': '5 y', 'Andorra': '-', 'Angola': '24 x', ... }
对比 PDF 再次检查这些数据,你会发现,就在双行国家第一次出现的地方,数据出现了错误。对应的数字来自出生登记(Birth registration)一列:
{ ... 'Bolivia (Plurinational State of)': '', 'Bosnia and Herzegovina': '37', 'Botswana': '99', 'Brazil': '99', ... }
如果查看 PDF 的文本文件,你会注意到在双行国家对应的数字那里有一个空行:
6 46 3 26 y 5 9 y
和采集国名遇到的问题一样,我们要用相同的方法来处理这个数据采集问题。如果我们的数据中有空白行,一定不要把空行采集到数据中,这样我们只采集与国名匹配的数据。修改后的代码如下:
for line in openfile: if country_line: print '%r' % 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
❶ 从经验中我们知道,PDF 文件使用换行符作为空行。本行代码用空字符串替代换行符,并删除空格来清洗数据。然后测试字符串的长度是否仍然大于 0。如果是的话,我们认为里面包含数据,并将其添加到童工总数列表中。
运行修改后的代码,在第一个双行国家那里数据又出现了问题。这次第一个双行国家对应的还是出生登记数据。之后的数值也都是错误的。回来看一下文本文件,找到问题出在哪里。如果你查看 PDF 文件里对应的那列数据,会发现 PDF 文本文件中的规律是从 1251 行开始的:
1250 1251 total 1252 - 1253 5 x 1254 26 1255 - 1266 -
进一步观察发现,出生登记列标题的结尾是 total:
266 Birth 267 registration 268 (%)+ 269 2005–2012* 270 total 271 37 272 99
目前搜集童工总数的函数找寻的是 total 这个词,所以在找到下一行国家之前,我们先找到了这一列数据。我们还发现暴力惩戒比例[Violent discipline(%)]列也有一个 total 标签,上面有一个空行。这和我们要采集的 total 具有相同的规律。
接二连三地遇到 bug,说明你的代码逻辑可能存在问题。我们的脚本最开始用的是开 / 关函数,所以想要从根本解决问题,就要重构那里的逻辑。我们想要知道如何找到正确的数据列,或许可以采集列名并排序。我们可能还需要找到一种方法,检查“页码”是否发生了变化。如果我们一直这样头痛医头脚痛医脚,很可能会遇到更多的错误。
只在脚本上投入你认为必要的时间。如果你想构建一个可持续过程,在很长一段时间内都可以在大型数据集上多次运行,你需要花时间仔细考虑所有步骤。
这就是编程的过程:写代码,调试,写代码,调试。无论是经验多么丰富的程序员,有时都会在代码中遇到错误。在学习编程的过程中,遇到错误会非常沮丧。你可能会想:“为什么无法运行?一定是我不擅长编程。”但事实并非如此。和其他事情一样,编程也需要练习。
现在看来,我们目前的方法显然是行不通的。根据我们目前对文本文件的了解,可以这么说,在利用文本寻找每一部分数据的开始和结束时,我们选用的标志是错误的。我们还可以用这个文件重新开始,换一个角度来思考;但我们想探索解决问题的其他方法,修正错误并获取想要的数据。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论