返回介绍

4.3 开始解析

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

我们解析 Excel 文件用的是 xlrd 库。这个库是用 Python 处理 Excel 文件的一系列库(http://www.python-excel.org/)之一。处理 Excel 文件主要有三个库。

· xlrd

读取 Excel 文件。

· xlwt

向 Excel 文件写入,并设置格式。

· xlutils

一组 Excel 高级操作工具(需要先安装 xlrd 和 xlwt)。

在用到这三个库的时候你需要分别安装。但本章只会用到 xlrd。我们要将 Excel 文件读取到 Python 中,所以你先要检查是否安装了 xlrd:

pip install xlrd

如果你得到以下错误信息,说明你没有安装 pip:

bash: pip: command not found

有关 pip 的安装说明,可以查阅 1.2.4 节,也可以访问 https://pip.pypa.io/en/latest/installing/

按步骤完成以下内容,即可安装好 Excel 文件的工作环境(也可能是类似的步骤,这和你的文件组织系统有关)。

(1) 为 Excel 任务创建一个文件夹。

(2) 创建一个新的 Python 文件,文件名叫 parse_excel.py,把它放到上面创建的文件夹中。

(3) 从本书仓库(https://github.com/jackiekazil/data-wrangling)下载名为 SOWC 2014 Stat Tables_Table 9.xlsx 的 Excel 文件,放到同一个文件夹中。

进入这个文件夹,在终端中输入下面的命令,从命令行中运行该脚本:

python parse_excel.py

学完本章后,我们写的脚本就可以解析这个 Excel 文件中保存的童工数据和童婚数据。

在脚本文件的开头,我们需要导入 xlrd 库,然后用 Python 打开 Excel 工作簿。我们将打开的文件保存在变量 book 中:

import xlrd

book = xlrd.open_workbook('SOWC 2014 Stat Tables_Table 9.xlsx')

与 CSV 不同,Excel 工作簿可以有多个标签(tab)或工作表(sheet)。想要获取数据,我们要找到包含目标数据的工作表。

如果有几个工作表,你可以猜一下索引号,但如果工作表很多的话就没法猜了。所以你应该知道 book.sheet_by_name(somename) 命令,其中 somename 是你要访问工作表的名字。

我们来看一下工作表都有哪些名字:

import xlrd

book = xlrd.open_workbook('SOWC 2014 Stat Tables_Table 9.xlsx')

for sheet in book.sheets():
  print sheet.name

我们要找的工作表是 Table 9。所以我们把这个名字添加到脚本中:

import xlrd

book = xlrd.open_workbook('SOWC 2014 Stat Tables_Table 9.xlsx')
sheet = book.sheet_by_name('Table 9')

print sheet

运行上面的代码,程序会退出,并给出以下错误信息:

xlrd.biffh.XLRDError: No sheet named <'Table 9'>

这时你可能有些困惑不解。问题在于我们看到的内容与实际内容并不相同。

打开 Excel 工作簿,双击工作表名选中它,你会发现在结尾有一个多出来的空格。用户在浏览时是看不到这个空格的。第 7 章中我们将学习如何用 Python 处理这样的错误。现在我们暂且在代码中加一个空格。

将这行代码:

sheet = book.sheet_by_name('Table 9')

修改成:

sheet = book.sheet_by_name('Table 9 ')

现在运行脚本应该一切正常了。你会看到类似这样的输出:

<xlrd.sheet.Sheet object at 0x102a575d0>

我们来探索一下能对工作表做哪些事情。在给 sheet 变量赋值后,添加这行代码,然后重新运行脚本:

print dir(sheet)

在返回的列表中,你会发现一个叫作 nrows 的方法。我们将用这个方法来遍历所有行。如果加上 print sheet.nrows,返回值是总行数。

现在试一下:

print sheet.nrows

你得到的返回值应该是 303。我们要遍历每一行,也就是说我们需要一个 for 循环。在 3.1.1 节我们学过,for 循环可以遍历列表中的元素,所以我们需要将 303 转换成一个列表,这样就可以遍历 303 次。我们将使用 range 函数来实现。

什么是 range()

还记得我们说过,Python 有许多有用的内置函数吗? range 就是其中之一。range 函数(https://docs.python.org/2/library/functions.html#range)接收一个数字作为参数,输出一个那么多元素组成的列表。

打开 Python 解释器,输入下面的代码,看看 range 函数的输出是什么样的:

range(3)

输出应该是:

[0, 1, 2]

返回了三个元素。现在我们可以创建一个 for 循环对列表遍历三次。

关于 range 函数需要注意以下两点。

· 返回的列表是从 0 开始的。这是因为 Python 列表索引是从 0 开始的。如果你想让列表从 1 开始,可以设置范围的始末。比如,range(1, 4) 返回的是 [1, 2, 3]。注意,列表中不包含最后一个数字,如果想得到的是 [1, 2, 3],需要将第二个数字设为 4。

· Python 2.7 还有一个叫作 xrange 的函数。二者略有不同,但只有在处理大型数据集时才会发现——xrange 的速度要更快一些。

有了 range 函数,就可以将 303 转换成一个列表,用于 for 循环的遍历,我们的脚本应该是像这样的:

import xlrd

book = xlrd.open_workbook('SOWC 2014 Stat Tables_Table 9.xlsx')
sheet = book.sheet_by_name('Table 9 ')

for i in range(sheet.nrows):    ➊
  print i             ➋

❶ 对 range(303) 的索引号 i 做循环,range(303) 是一个包含 303 个连续整数的列表。

❷ 输出 i,是从 0 到 302 的连续整数。

下面需要查找每一行,提取出每行的内容,而不是仅仅打印编号。要实现查找功能,需要用 i 作为索引编号来定位第 n 行。

我们用 row_values 来获取每一行的值,这个方法是之前 dir(sheet) 给出的。根据 row_values 的文档(http://www.lexicon.net/sjmachin/xlrd.html#xlrd.Sheet.row_values-method),我们知道这个方法接收一个索引数字,返回对应行的值。将这个方法添加到 for 循环中,重新运行脚本:

for i in range(sheet.nrows):
  print sheet.row_values(i)     ➊

❶ 利用 i 作为索引来查找对应行的值。由于这个方法是在 for 循环中,for 循环的次数等于工作表的长度,所以我们对数据表的每一行都调用了这个方法。

运行代码,每行都输出一个列表。下面给出的是一部分输出:

['', u'TABLE 9. CHILD PROTECTION', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '']
['', '', u'TABLEAU 9. PROTECTION DE L\u2019ENFANT', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '']
['', '', '', u'TABLA 9. PROTECCI\xd3N INFANTIL', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '']
['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'', '', '']
['', u'Countries and areas', '', '', u'Child labour (%)+\n2005\u20132012*', '',
'', '', '', '', u'Child marriage (%)\n2005\u20132012*', '', '', '', u'Birth
registration (%)+\n2005\u20132012*', '', u'Female genital mutilation/ cutting (%)+\
n2002\u20132012*', '', '', '', '', '', u'Justification of wife beating (%)\n 2005\
u20132012*', '', '', '', u'Violent discipline (%)+\n2005\u20132012*', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '', '', '']

现在我们能够查看每一行的内容,我们需要从中提取出需要的信息。为了确定需要哪些信息,以及如何获取这些信息,更简单的做法是用 Excel 程序打开该文件,比如 Windows 上的 Microsoft Excel 或 Mac 上的 Numbers。打开工作簿的第二个标签,你会看到好多标题行。

 我们代码的目的是抓取英文文本。但如果你想挑战一下自己,可以尝试提取法语或西班牙语的标题和国家。

在第二个标签里,看一下你能提取哪些信息,并思考一下如何优化组织这些信息。我们这里提供一种可行的方法,但还有很多其他方法,用到了不同的数据结构。

对于本次练习来说,我们要提取的是童工和童婚的统计数据。下面是组织数据的一种方法——后面将以此例作为目标:

{
  u'Afghanistan': {
    'child_labor': {
      'female': [9.6, ''], ➊
      'male': [11.0, ''],
      'total': [10.3, '']},
    'child_marriage': {
      'married_by_15': [15.0, ''],
      'married_by_18': [40.4, '']
    }
  },

  u'Albania': {
    'child_labor': {
      'female': [9.4, u'  '],
      'male': [14.4, u'  '],
      'total': [12.0, u'  ']},
    'child_marriage': {
      'married_by_15': [0.2, ''],
      'married_by_18': [9.6, '']
    }
  },
  ...
}

❶ 如果你查看 Excel 中的数据,有些数字可能不太一样。这是因为 Excel 常常会将数字四舍五入。我们这里给出的数字就是你用 Python 解析时会得到的数字。

 在开始写代码时,提前想好输出格式的样子,并写出一个这样的数据实例,可以节省很多时间。一旦确定了你想要的数据格式,你可以问问自己:“下一步我要怎么做才能实现目标?”当你不知道下一步怎么做时,这种方法特别有用。

我们将使用两种 Python 结构来提取数据。用到的第一个方法是嵌套 for 循环,就是在把一个 for 循环放到另一个 for 循环里。如果你有 x 行数据,每行包含 y 个元素,经常会用到这种方法。想要访问每一个元素,你需要一个 for 循环遍历每一行,然后另一个 for 循环遍历每一个元素。我们在第 3 章的例子中也用过嵌套 for 循环。

我们将使用嵌套 for 循环来输出每一行的每一个单元格。这将会输出我们之前见过的元素,之前每一行是以列表的形式出现。

for i in xrange(sheet.nrows):
  row = sheet.row_values(i)    ➊

  for cell in row:         ➋
    print cell           ➌

❶ 将每一行内容组成的列表保存到 row 变量中。这提高了代码的可读性。

❷ 遍历列表中的每一个元素,也就是当前行的每一个单元格。

❸ 输出单元格的值。

运行包含嵌套 for 循环的完整代码,你会发现输出不再那么有用了。这时我们要用第二种方法来探索 Excel 文件——计数器

什么是计数器

计数器是一种控制程序流的方法。有了计数器,你可以在 for 循环中添加一条 if 语句来控制 for 循环,每次迭代后计数增加。如果计数超过了你的设定值,for 循环将不再运行 if 语句中的代码。在 Python 解释器中试试下面的例子:

count = 0           ➊
    
for i in range(1000):     ➋
  if count < 10:      ➌
    print i
  count += 1        ➍
    
print 'Count: ', count    ➎

➊ 将变量 count 初始值设为 0。

➋ 创建一个循环,循环范围是从 0 到 999。

➌ 测试计数是否小于 10,如果是的话,输出 1。

➍ 增加 count 的值,这样计数会随着循环次数增加而增加。

➎ 输出最终计数。

在我们的代码中添加一个计数器,这样就可以逐项查看每一行和每一个单元格,找出我们需要提取的内容。一定要注意计数器在代码中的位置——放在单元格的循环里或者放在行的循环里,结果可能会大不相同。

修改 for 循环的代码如下:

count = 0
for i in xrange(sheet.nrows):
  if count < 10:
    row = sheet.row_values(i)
    print i, row         ➊

  count += 1

❶ 输出行编号 i 和对应行的内容,这样我们可以看到每一行包含的信息。

现在回头看一下我们期望的最终输出格式,我们真正要搞清楚的是国家名字是从哪行开始的。要记住,国家名字是最终输出字典的第一个键:

{
  u'Afghanistan': {...},
  u'Albania': {...},
  ...
}

上面脚本中含有计数器,并且控制语句是 count < 10,运行后你会发现,输出中并没有包含国家名字出现的那一行。

要跳过几行才能找到感兴趣的数据,所以我们要想办法确定从哪一行开始采集数据。在上面的例子中,我们知道国家名字开始的行编号大于 10。但怎么能知道具体是从哪一行开始的呢?

答案就在下一个代码示例中,但在你看答案之前,试着自己修改计数器,找到国家名字开始的那一行。(有许多种方法都可以找到,所以即使你的答案与下面的代码示例稍有不同,也是可以的。)

找到了正确的行编号,你需要在那一行后面添加一条 if 语句,开始提取该行的内容。我们只处理那一行之后的数据。

如果你找到了正确的行编号,代码应该是像这样的:

count = 0

for i in xrange(sheet.nrows):
  if count < 20:             ➊
    if i >= 14:            ➋
      row = sheet.row_values(i)
      print i, row
    count += 1

❶ 这一行代码遍历前 20 行内容,找到国家名字开始出现的那一行。

❷ if 语句的作用是,从国家名字出现的那一行开始输出每行的内容。

现在你的输出应该是像这样的:

14 ['', u'Afghanistan', u'Afghanistan', u'Afganist\xe1n', 10.3, '', 11.0, '', 9.6,
'', 15.0, '', 40.4, '', 37.4, '', u'\u2013', '', u'\u2013', '', u'\u2013', '', u'\
u2013', '', 90.2, '', 74.4, '', 74.8, '', 74.1, '', '', '', '', '', '', '', '', '',
'', '', '', '']
15 ['', u'Albania', u'Albanie', u'Albania', 12.0, u' ', 14.4, u' ', 9.4,
u' ', 0.2, '', 9.6, '', 98.6, '', u'\u2013', '', u'\u2013', '', u'\u2013', '',
36.4, '', 29.8, '', 75.1, '', 78.3, '', 71.4, '', '', '', '', '', '', '', '', '',
'', '', '', '']
16 ['', u'Algeria', u'Alg\xe9rie', u'Argelia', 4.7, u'y', 5.5, u'y', 3.9, u'y',
0.1, '', 1.8, '', 99.3, '', u'\u2013', '', u'\u2013', '', u'\u2013', '', u'\u2013',
'', 67.9, '', 87.7, '', 88.8, '', 86.5, '', '', '', '', '', '', '', '', '', '', '',
'', '']
...more

下面要把每一行转换成字典格式。这样一来,在后续章节我们想要对数据进行其他操作时,数据将更有意义。

回头看一下前面我们期望的输出格式示例。我们想要的是一个字典,用国家作为键。我们用索引将国家名字提取出来。

什么是索引

回想第 3 章学过的内容,索引是从一系列对象(比如列表)中提取元素的方法。对于要解析的 Excel 文件来说,我们把 i 传入 sheet.row_values(),row_values 方法以 i 作为索引。我们在 Python 解释器里练习一下索引。

创建一个列表实例:

x = ['cat', 'dog', 'fish', 'monkey', 'snake']

想要提取出第二个元素,你可以添加一个索引编号来指代这个元素,像这样:

>>>x[2]
'fish'

如果这不是你预期的结果,要记住 Python 的索引编号是从 0 开始的。所以,如果想提取人们一般所说的第二个元素,我们要用数字 1:

>>>x[1]
'dog'

你还可以用负数索引:

>>>x[-2]
'monkey'

正数索引和负数索引的区别是什么?你可以发现,正数索引是从前向后数,而负数索引是从后向前数。

切片(slicing)是与索引相关的另一个有用工具。切片可以从一个列表或可迭代对象中“切”出一部分来。比如:

>>>x[1:4]
['dog', 'fish', 'monkey']

注意,与 range 一样,切片操作从第一个数字开始,但第二个数字的意思是:“直到这个数,但并不包括这个数。”

如果你没有输入第一个数或最后一个数,切片操作会一直到头或一直到尾。下面给了几个例子:

x[2:]
['fish', 'monkey', 'snake']
    
x[-2:]
['monkey', 'snake']
    
x[:2]
['cat', 'dog']
    
x[:-2]
['cat', 'dog', 'fish']

其他可迭代对象的切片操作与列表相同。

我们在代码中添加一个字典,然后提取出每一行的国家名字,作为字典的键。

将 for 循环的代码修改如下:

count = 0
data = {}                  ➊

for i in xrange(sheet.nrows):
  if count < 10:
    if i >= 14:
      row = sheet.row_values(i)
      country = row[1]         ➋
      data[country] = {}         ➌
      count += 1

print data                   ➍

❶ 创建一个空字典来保存数据。

❷ row[1] 提取出遍历每一行的国家名字。

❸ data[country] 将国家设为 data 字典的键,对应的值设为另一个字典,因为我们接下来要将数据保存在这个字典里。

❹ 输出 data 字典,我们可以看到里面的内容。

现在你的输出应该是像这样的:

{u'Afghanistan': {}, u'Albania': {}, u'Angola': {}, u'Algeria': {},
u'Andorra': {}, u'Austria': {}, u'Australia': {}, u'Antigua and Barbuda': {},
u'Armenia': {}, u'Argentina': {}}

现在我们需要将每一行其他的值与工作簿中的值对应起来,然后保存到字典中。

 在你把所有的值都提取出来,并与 Excel 工作表中的值对比时,你会犯许多错误。这很正常,也是预料之中的事情。你应该接受这个过程,这说明你正在一步步解决问题。

我们首先创建一个空的数据结构,可以用来保存数据。因为我们已经知道数据行是从第 14 行开始的,所以可以删掉计数器。我们知道 xrange 可以输入起点和终点,所以可以从 14 开始计数,直到文件的结尾。修改后的代码如下:

data = {}

for i in xrange(14, sheet.nrows):  ➊
  row = sheet.row_values(i)
  country = row[1]

  data[country] = {        ➋
    'child_labor': {       ➌
      'total': [],       ➍
      'male': [],
      'female': [],
    },
    'child_marriage': {
      'married_by_15': [],
      'married_by_18': [],
    }
  }

print data['Afghanistan']      ➎

❶ 我们可以删掉计数器相关的代码,让 for 循环从工作表第 14 行开始。这一行代码从 i 的值为 14 开始循环,所以我们自动跳过数据集中不需要的那些行。

❷ 这一行代码将字典扩展成很多行,可以填入其他数据点。

❸ 这一行代码创建了 child_labor 键,并把它的值设为另一个字典。

❹ 这个字典又包含了几个字典,字典的键是字符串,说明了保存的数据内容。每一个键对应的值都是列表。

❺ 输出与 Afghanistan 键对应的值。

Afghanistan 的输出数据是像这样的:

{
  'child_labor': {'total': [], 'male': [], 'female': []},
  'child_marriage': {'married_by_18': [], 'married_by_15': []}
}

现在我们来填入数据。因为我们用索引访问每一行的每一列,所以可以将工作表的值填入这些列表中。观察工作表,弄清楚哪一列对应的是哪一部分数据,我们可以把 data 字典修改成这样:

data[country] = {
  'child_labor': {
    'total': [row[4], row[5]],  ➊
    'male': [row[6], row[7]],
    'female': [row[8], row[9]],
  },
  'child_marriage': {
    'married_by_15': [row[10], row[11]],
    'married_by_18': [row[12], row[13]],
  }
}

❶ 每一列包含两个单元格,所以我们的代码把两个值都保存下来。这一行代码中童工总数是第 5 列和第 6 列,而我们知道 Python 从 0 开始索引,所以索引编号是 4 和 5。

重新运行代码,我们会得到类似这样的输出:

{
  'child_labor': {'female': [9.6, ''], 'male': [11.0, ''], 'total': [10.3, '']},
  'child_marriage': {'married_by_15': [15.0, ''], 'married_by_18': [40.4, '']}}
}

 在继续学习后面的内容之前,输出几条数据记录,检查字典中的数据是否正确。有时一个索引错了,剩下的数据就全都错了。

最后,我们可以用 pprint 语句而不是 print 语句来预览数据。对于复杂的数据结构(如字典),用这种方法检查输出要容易很多。将下面两行代码添加到文件结尾,可以查看格式化的数据:

import pprint         ➊
pprint.pprint(data)       ➋

❶ 导入 pprint 库。import 语句一般出现在文件开头,但为了简单起见,我们把它放在这里。后面你可以把这几行删掉,因为它们不会影响脚本的运行。

❷ 将 data 传入 pprint.pprint() 函数。

滚动查看输出的内容,你会发现大部分内容看起来都很好,但有几条记录看起来出了问题。

查看工作表的内容,你应该会注意到,国家的最后一行是津巴布韦(Zimbabwe)。所以我们要找到国家名字是 'Zimbabwe' 的那一行,然后退出循环。我们在代码中添加 break 来退出循环,这样就完全退出了 for 循环,继续执行下面的脚本。我们来添加 break 停止循环。在 for 循环的结尾,添加下列代码,然后重新运行:

    if country == 'Zimbabwe': ➊
      break ➋

❶ 如果国家名字是津巴布韦的话……

❷ 退出 for 循环。

添加了 break 之后,你有没有得到 NameError: name 'country' is not defined 的错误信息?如果有的话,检查一下代码缩进。for 循环中的 if 语句要缩进四个空格。

逐步运行代码可以帮你发现问题所在。如果你想知道 for 循环中某个变量的值,比如 country,可以在 for 循环中添加 print 语句,在脚本报错退出之前查看变量的值。这样你可能会发现出错的地方在哪里。

现在脚本的输出与我们的最终目标是一致的。我们要做的最后一件事情,是在脚本中添加一些注释。

注释

在代码中添加注释,可以让你(或其他人)在以后能够理解代码的含义。添加注释的方法是在注释前加一个 # 号:

# 这是Python注释。Python会忽略该行。

添加多行注释的格式如下:

"""
    
  这是多行注释的格式。
  如果你的注释很长或者
  你想插入一段较长的描述,
  你应该使用这种类型的注释。
    
"""

你的脚本现在应该是这样的:

"""
  这是用来分析童工和童婚数据的脚本。      ➊
  本脚本中用到的Excel文件可以在以下链接中获取:
    http://www.unicef.org/sowc2014/numbers/
"""

import xlrd
book = xlrd.open_workbook('SOWC 2014 Stat Tables_Table 9.xlsx')

sheet = book.sheet_by_name('Table 9 ')
data = {}
for i in xrange(14, sheet.nrows):
  # 从第14行开始,因为这是国家数据的起点。    ➋

  row = sheet.row_values(i)

  country = row[1]

  data[country] = {
    'child_labor': {
      'total': [row[4], row[5]],
      'male': [row[6], row[7]],
      'female': [row[8], row[9]],
    },
    'child_marriage': {
      'married_by_15': [row[10], row[11]],
      'married_by_18': [row[12], row[13]],
    }
  }

  if country == 'Zimbabwe':
    break

import pprint
pprint.pprint(data)                 ➌

❶ 多行注释,大致说明脚本的用途。

❷ 单行注释,说明我们为什么从第 14 行开始,而不是从前面开始。

❸ 从简单的数据解析过渡到数据分析工作时,我们可以也应该删除这两行。

现在我们的输出应该和上一章的数据差不多。在下一章里,我们进一步将相同的数据从 PDF 文件中解析出来。

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

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

发布评论

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