第 3 章 供机器读取的数据

数据可以存储成许多不同的格式和文件类型。某些格式存储的数据很容易被机器处理,而另一些格式存储的数据则容易被人工读取。微软的 Word 文档属于后者,而 CSV、JSON 和 XML 文件则属于前者。本章我们将学习如何读取那些容易被机器处理的文件,在第 4 章和第 5 章我们将讨论那些供人工读取的文件。

第 3 章 供机器读取的数据 - 图1

以易于机器理解的方式来存储数据的文件格式,通常被称作机器可读的(machine readable)。常见的机器可读格式包括:

  • 逗号分隔值(Comma-Separated Values,CSV)

  • JavaScript 对象符号(JavaScript Object Notation,JSON)

  • 可扩展标记语言(eXtensible Markup Language,XML)

在口语和书面语中,提到这些数据格式时通常使用它们的短名字(如 CSV)。我们将使用这些缩写。

在寻找数据、向组织或机构发出数据请求时,你能找到最好的资源就是本章讲到的这些格式。与易于人工读取的格式相比,这些格式更容易被 Python 脚本处理,在数据网站上通常也很容易找到。

创建代码主文件夹

为了能够顺利完成本章的例子和代码,你需要将文件保存到本地计算机。你应该创建一个文件夹(如果之前还没有创建的话),用来保存 Python 代码和数据文件。文件夹的名字要直观,比如叫 data_wrangling(数据处理)。然后在这个文件夹中创建一个子文件夹,用来保存与本书相关的代码[比如叫 code(代码)]。这有助于保持你的文件夹结构清晰,命名直观。

如果你按照上面的提示操作,应该创建好了一个像这样的文件夹:~/Projects/data_wrangling/code。

在基于 Unix 的系统中(Linux 和 Mac),~ 符号代表主目录,用命令行访问比较方便。在 Windows 系统中,主目录位于 Users 文件夹下,所以你的文件夹位置是在 C:\Users\<your_name>\Projects\data_wrangling。

在本书的数据仓库中(https://github.com/jackiekazil/datawrangling)可以下载代码示例,并将其移动到你的项目文件夹中。在阅读本章的过程中,我们假定,从上述仓库下载的数据与你编写的 Python 代码位于同一文件夹下。这样我们就不必担心文件定位问题,可以专心研究用 Python 导入数据。

3.1 CSV数据

我们要学习的第一个机器可读的文件格式是 CSV。CSV 文件(简称为 CSV)是指将数据列用逗号分隔的文件。文件的扩展名是 .csv。

另一种数据类型,叫作制表符分隔值(tab-separated values,TSV)数据,有时也与 CSV 归为一类。TSV 与 CSV 唯一的不同之处在于,数据列之间的分隔符是制表符(tab),而不是逗号。文件的扩展名通常是 .tsv,但有时也用 .csv 作为扩展名。从本质上来看,.tsv 文件与 .csv 文件在 Python 中的作用是相同的。

第 3 章 供机器读取的数据 - 图2 如果文件的扩展名是 .tsv,那么里面包含的很可能是 TSV 数据。如果文件的扩展名是 .csv,那么里面包含的可能是 CSV 数据,但也可能是 TSV 数据。一定要打开文件查看一下,这样你可以在导入数据之前就明确所处理的数据类型。

对于本章的 CSV 实例,我们采用的是来自世界卫生组织(WHO)的数据。WHO 拥有许多大型数据集(http://apps.who.int/gho/data/node.main),并且提供多种格式的数据。本例选择的数据集包含全球各国的预期寿命。访问网页(http://apps.who.int/gho/data/node.main.3?lang=en)查看预期寿命数据,你会发现这个数据集有几个不同的版本。本例中使用的是 CSV(纯文本,下载地址:http://apps.who.int/gho/athena/data/data-text.csv?target=GHO/WHOSIS_000002,WHOSIS_000001,WHOSIS_000015&profile=text&filter=COUNTRY:;REGION:AFR;REGION:AMR;REGION:SEAR;REGION:EUR;REGION:EMR;REGION:WPR;SEX:)。

用文本编辑器 1 打开这个 CSV 文件,你会看到类似表 3-1 的许多行数据。

1为了完成本章练习,你需要一个好用的文本编辑器。如果还没有安装的话,你可以按 1.2.5 节的说明来安装。

表3-1:两个样本数据记录a

CSV标题 样本记录1 样本记录2
指标 60 岁时预期寿命(年) 出生时预期寿命(年)
发布状态 已发布 已发布
年份 1990 1990
WHO 地区 欧洲 美洲
世界银行收入分组 高收入 中低收入
国家 捷克共和国 伯利兹
性别 女性 男女合计
示值 19 71
数值大小 19.00000 71.00000
最低值
最高值
备注

a 加粗项包含在下文的样本数据中。

为了让数据更容易阅读,下面给出一个数据样本,其中只包含经过挑选的特定字段(表 3-1 中的加粗项)。在文本编辑器中打开 CSV 文件,你看到的数据应该与其类似:

  1. "Year","Country","Sex","Display Value","Numeric"
  2. "1990","Andorra","Both sexes","77","77.00000"
  3. "2000","Andorra","Both sexes","80","80.00000"
  4. "2012","Andorra","Female","28","28.00000"
  5. "2000","Andorra","Both sexes","23","23.00000"
  6. "2012","United Arab Emirates","Female","78","78.00000"
  7. "2000","Antigua and Barbuda","Male","72","72.00000"
  8. "1990","Antigua and Barbuda","Male","17","17.00000"
  9. "2012","Antigua and Barbuda","Both sexes","22","22.00000"
  10. "2012","Australia","Male","81","81.00000"

预览 CSV 文件的另一种方法是用电子表格程序打开,比如 Excel 或 Google Spreadsheets。这些程序将每一个数据条目显示为单独的一行。

3.1.1 如何导入CSV数据

前面我们学习了一点关于数据的知识,下面用 Python 打开这个文件,并将数据转换成 Python 可以理解的格式。这只要几行代码:

  1. import csv
  2. csvfile = open('data-text.csv', 'rb')
  3. reader = csv.reader(csvfile)
  4. for row in reader:
  5. print row

我们来一行一行地阅读上面的代码。在上一章里,我们所有的代码都是在 Python 解释器中输入的,但随着代码变得越来越长、越来越复杂,在文件中编写代码并运行要方便一些。读完这段代码,我们将把它保存在一个 .py 文件中(.py 文件是一个 Python 文件),然后在命令行中运行这个文件。

脚本的第一行代码导入了一个叫作 csv 的库:

  1. import csv

Python 是一个代码包,提供了你可以在 Python 程序中使用的功能。这里导入的 csv 库是 Python 标准库(或 stdlib)的一部分,随 Python 一起安装。将库导入到文件中后,我们就可以使用这个库。如果没有这个库的话,脚本将会变得很长——csv 库提供了辅助函数,这样一来,为了完成更复杂的任务,我们就不必写这么多的代码。

第二行代码将 data-text.csv 文件传入 open 函数,这个文件应该和脚本位于同一文件夹下:

  1. csvfile = open('data-text.csv', 'rb')

第 3 章 供机器读取的数据 - 图3 函数(function)是一段代码,在被调用时执行相应的任务。它和我们在第 2 章学过的 Python 数据类型的方法十分相似。函数有时会接收一个(或多个)输入。这些输入叫作参数(argument)。函数的功能是基于参数的。函数有时也会返回一个输出,可以被保存或使用。

open 是 Python 的内置函数(这里列出了 Python 所有的内置函数:https://docs.python.org/2/library/functions.html),也就是说,打开文件的行为是如此常见,所以 Python 核心贡献者认为应把这个行为添加到 Python 的默认安装里。在使用 open 函数时,我们传入一个文件名作为第一个参数(这里用的是 'data-text.csv'),然后选择指定文件打开的模式(我们用的是 'rb')。如果查看 open 函数的文档(https://docs.python.org/2/library/functions.html#open),你会发现,'rb' 参数的意思是我们以只读方式二进制方式打开文件。以二进制方式打开文件,可以让代码在 Windows 上和基于 Unix 的操作系统上都能运行。另一种常见的模式是写入'w''wb',后者表示以二进制方式写入)。

第 3 章 供机器读取的数据 - 图4 如果你想读取文件,以只读方式打开文件。如果你想写入文件,以写入方式打开文件。

我们将这个函数的输出保存在变量 csvfile 中。现在 csvfile 保存一个打开的文件作为它的值。

在下一行代码中,我们将 csvfile 传递给 csv 模块的 reader 函数。这个函数的作用是让 csv 模块将打开的文件当作 CSV 来读取:

  1. reader = csv.reader(csvfile)

函数 csv.reader(csvfile) 的输出保存在变量 reader 中。现在 reader 变量保存的是已打开文件的 Python CSV reader。有了这个 CSV reader,我们用简单的 Python 命令就可以轻松查看文件中的数据。最后一段代码中,我们使用了所谓的 for 循环。

for 循环是一种遍历 Python 对象的方法,通常与列表一起使用。for 循环告诉 Python 代码:“对于这个列表中的每一个元素,做点什么。”在一个 for 循环中,for 后面的第一个单词是用来保存列表(或其他可迭代对象)中每一个对象的变量。for 循环下面的代码利用这个变量对该元素执行更多的操作或计算。因此,最好用有意义的单词做变量名,这样你和其他人都能轻松阅读并理解代码。

第 3 章 供机器读取的数据 - 图5 还记得第 2 章里出现过的“无效的标记”(invalid token)吗? for 是 Python 里另一个特殊的标记(token),只能用来创建 for 循环。标记可以将我们在解释器或脚本中输入的内容翻译成计算机能够执行的命令。

试着在 Python 解释器中运行下面的代码:

  1. dogs = ['Joker', 'Simon', 'Ellie', 'Lishka', 'Fido']
  2. for dog in dogs:
  3. print dog

利用这个 for 循环,我们将每一只狗狗的名字都保存在 for 循环的 dog 变量中。对于 for 循环的每一次迭代,我们打印出狗狗的名字(保存在变量 dog 中)。当程序遍历完每一只狗狗的名字(或列表中的每一个元素)后,代码停止运行。

在 IPython 中退出缩进代码块

在 IPython 终端里编写 for 循环或其他缩进代码块时,一定要检查一下,确保提示符从缩进代码块的样式 变成了一个新的 In 提示符。最简单的方法就是,在完成最后一行缩进代码后敲下 Return 键。你应该看到一个新的 In 提示符,然后再输入循环之外的代码:

  1. In [1]: dogs = ['Joker', 'Simon', 'Ellie', 'Lishka', 'Fido']
  2.  
  3. In [2]: for dog in dogs:
  4. …: print dog
  5. …:
  6. Joker
  7. Simon
  8. Ellie
  9. Lishka
  10. Fido
  11.  
  12. In [3]:

➊ IPython 的自动缩进提示符(…: 后面跟着四个空格)。

➋ 在空行按下 Return 键,退出缩进代码块并运行代码。

➌ IPython 的代码运行结束后,出现了新的提示符。

在我们用来读取 CSV 的代码中,reader 对象是一个保存数据行的 Python 容器。在 readerfor 循环中,我们将每一行数据保存在变量 row 中。下一行代码的意思是,我们让 Python 打印出每一行数据:

  1. for row in reader:
  2. print row

现在我们已经能够导入数据并对数据进行遍历,下面开始对数据进行真正的探索。

3.1.2 将代码保存到文件中并在命令行中运行

作为开发者,在写代码时,即使是中间过程的部分代码片段,你也会希望保存下来,以便后续检查和使用。保持代码结构清晰,并及时保存代码,即使中途被打断,你也可以从上次中断的地方顺畅地继续工作。

我们将到目前为止所有的代码保存到文件中并运行。代码应该是这样的(如果你还没有完成这一步的话,打开文本编辑器,创建一个新文件,将这段代码输入进去):

  1. import csv
  2. csvfile = open('data-text.csv', 'rb')
  3. reader = csv.reader(csvfile)
  4. for row in reader:
  5. print row

第 3 章 供机器读取的数据 - 图6 注意大小写、间距和换行。如果各行的代码间距各不相同或者大小写错误的话,代码是无法正常运行的。一定要严格按上面的代码输入,利用四个空格来缩进。这一点很重要,因为 Python 区分大小写,并利用缩进来表示代码的结构。

用文本编辑器将代码保存为 .py(Python)文件。完整的文件名应该是像这样的:import_csv_data.py。

将数据文件 data-text.csv 放到你刚刚保存 Python 文件的同一个文件夹内。如果你想把文件放到其他位置,需要对文件位置对应的代码做适当修改。

打开不同位置的文件

在目前的代码中,我们将文件路径传入 open 函数,像这样:

  1. open('data-text.csv', 'rb')

但如果数据文件位于一个叫作 data 的子文件夹,我们需要修改脚本,让它去那里寻找数据文件。修改后的代码如下:

  1. open('data/data-text.csv', 'rb')

在上面的例子中,我们的文件结构是像这样的:

  1. data_wrangling/
  2. -- code/
  3. |-- import_csv_data.py data/
  4. `— data-text.csv

如果你找不到自己的文件,可以在 Mac 或 Linux 计算机上打开命令行,使用下列命令来浏览文件夹。

  • ls 返回文件的列表。

  • pwd 给出当前位置。

  • cd ../ 进入上层文件夹。

  • cd ../../ 向上移动两层目录。

  • cd data 进入 data 文件夹,该文件夹位于当前文件夹下(可以用 ls 来查看)。

查阅附录 C 可以了解用命令行在文件夹之间跳转的更多内容,其中还包括针对 Windows 用户的整整一节内容。

保存好文件后,你可以用命令行来运行它。打开命令行(终端或 cmd),跳转到文件所在的位置。假设你把文件放在~/Projects/data_wrangling/code 中。想要在 Mac 命令行中跳转到那里,你可以使用变更目录或文件夹命令(cd):

  1. cd ~/Projects/data_wrangling/code

跳转到正确的位置后,你就可以运行 Python 文件。到目前为止,我们都是在 Python 解释器中运行代码。我们将文件保存为 import_csv_data.py。想要在命令行中运行 Python 文件,你只需要输入 python,敲空格,然后输入文件名即可。我们来试一下运行 Python 文件:

  1. python import_csv_data.py

你得到的输出看起来应该像一串列表——和下面给出的数据类似,但数据量要大得多。

  1. ['Healthy life expectancy (HALE) at birth (years)', 'Published', '2012',
  2. 'Western Pacific', 'Lower-middle-income', 'Samoa', 'Female', '66',
  3. '66.00000', '', '', '']
  4. ['Healthy life expectancy (HALE) at birth (years)', 'Published', '2012',
  5. 'Eastern Mediterranean', 'Low-income', 'Yemen', 'Both sexes', '54',
  6. '54.00000', '', '', '']
  7. ['Healthy life expectancy (HALE) at birth (years)', 'Published', '2000',
  8. 'Africa', 'Upper-middle-income', 'South Africa', 'Male', '49', '49.00000',
  9. '', '', '']
  10. ['Healthy life expectancy (HALE) at birth (years)', 'Published', '2000',
  11. 'Africa', 'Low-income', 'Zambia', 'Both sexes', '36', '36.00000', '', '', '']
  12. ['Healthy life expectancy (HALE) at birth (years)', 'Published', '2012',
  13. 'Africa', 'Low-income', 'Zimbabwe', 'Female', '51', '51.00000', '', '', '']

你得到上面的输出了吗?如果没有的话,花一分钟的时间阅读你得到的错误信息。从中发现出错的地方可能在哪里了吗?花点时间去搜索错误的原因,看看其他人解决相同的错误都用了哪些方法。关于如何解决错误,如果你需要额外的帮助,可查阅附录 E。

第 3 章 供机器读取的数据 - 图7 从现在开始,我们将会在代码编辑器里编写大多数代码,保存文件,然后在命令行中运行。Python 解释器仍然是一个有用的工具,可以用来测试代码片段,但随着代码变得越来越长、越来越复杂,在代码提示符中维护代码将越来越困难。

无论是我们正在写的代码,还是我们将面对的许多其他问题,解决问题的方法往往不止一种。csv.reader() 返回的是一个数据的列表,里面包含的是文件中的每一行数据,在我们刚开始处理问题时,这是一个很容易理解的方法。下面要对脚本做少许修改,将列表行改成字典行。这样在我们探索数据集的过程中,读取数据、对比数据和理解数据会变得更加容易。

在文本编辑器中,将第 4 行 reader = csv.reader(csvfile) 修改成 reader = csv.DictReader(csvfile)。现在你的代码应该是这样的:

  1. import csv
  2. csvfile = open('data-text.csv', 'rb')
  3. reader = csv.DictReader(csvfile)
  4. for row in reader:
  5. print row

保存文件并重新运行,每一个数据记录变成一个字典。字典的键来自于 CSV 文件的第一行。后面所有行都是字典的值。下面是一行数据对应的输出:

  1. {
  2. 'Indicator': 'Healthy life expectancy (HALE) at birth (years)',
  3. 'Country': 'Zimbabwe',
  4. 'Comments': '',
  5. 'Display Value': '49',
  6. 'World Bank income group': 'Low-income',
  7. 'Numeric': '49.00000',
  8. 'Sex': 'Female',
  9. 'High': '',
  10. 'Low': '',
  11. 'Year': '2012',
  12. 'WHO region': 'Africa',
  13. 'PUBLISH STATES': 'Published'
  14. }

现在我们已经成功地将 CSV 数据导入到 Python 中,也就是说,我们能够从文件中获取数据,并将其转换成 Python 可用的格式(字典),for 循环可以让我们直观地查看数据。我们可以利用 csv 库两种不同的 reader 来查看数据,一种是列表形式,一种是字典形式。在开始探索和分析数据集时,我们会再次用到这个库。下面继续学习导入 JSON 数据。

3.2 JSON数据

JSON 数据是数据传输最常用的格式之一。人们喜欢这一格式,是因为它结构清晰、易于阅读且方便解析。网站在向页面的 JavaScript 传输数据时,JSON 也是最常用的数据格式之一。许多网站都提供了支持 JSON 的 API,我们会在第 13 章讲到。本节会继续使用全球预期寿命的数据。WHO 并没有提供这一数据的 JSON 格式,但我们为本书创建了 JSON 版本的数据,你可以在代码仓库(https://github.com/jackiekazil/datawrangling)中找到。

第 3 章 供机器读取的数据 - 图8 如果文件的扩展名是 .json,那里面包含的可能是 JSON 数据。如果文件扩展名是 .js,那可能是 JavaScript 文件,但在少数情况下也可能是命名不规范的 JSON 文件。

在代码编辑器里打开这个 JSON 文件,你会发现每一条数据记录都很像一个 Python 字典。每一行都有键和值,用 : 分隔,数据条目之间用 , 分隔。首尾还有花括号包围 {}。这是 JSON 文件的一条数据记录:

  1. [
  2. {
  3. "Indicator":"Life expectancy at birth (years)",
  4. "PUBLISH STATES":"Published",
  5. "Year":1990,
  6. "WHO region":"Europe",
  7. "World Bank income group":"High-income",
  8. "Country":"Andorra",
  9. "Sex":"Both sexes",
  10. "Display Value":77,
  11. "Numeric":77.00000,
  12. "Low":"",
  13. "High":"",
  14. "Comments":""
  15. },
  16. ]

JSON 文件有时看起来和字典完全相同,这与输出格式有关。在上面的示例中,每一个数据条目就是一个 Python 字典(首尾由 {} 包围),这些字典又包含在一个列表中,列表首尾由 [] 包围。

如何导入JSON数据

在 Python 中导入 JSON 文件比导入 CSV 文件还要简单。下面的代码将对一个 JSON 数据文件执行打开、加载、导入与输出的操作:

  1. import json
  2. json_data = open('data-text.json').read()
  3. data = json.loads(json_data)
  4. for item in data:
  5. print item

❶ 导入 Python 的 json 库(https://docs.python.org/2/library/json.html),我们用它来处理 JSON 文件。

❷ 利用 Python 内置的 open 函数打开 JSON 文件。文件名叫作 data-text.json(这是 open 函数的第一个参数)。本行代码还调用了已打开文件的 read 方法,用来读取该文件,并将读取的内容保存在变量 json_data 中。

❸ 利用 json.loads() 将 JSON 数据载入 Python,并将输出保存在变量 data 中。

❹ 利用 for 循环遍历所有数据,并打印出每一项,这也是本例代码的输出。

在命令行中运行 python import_json_data.py,输出是一个字典,里面包含 JSON 文件中每一条数据记录。这个输出应该和 CSV 的最终输出基本相同。一定要记得将数据文件复制到脚本所在的文件夹,或者将脚本中的文件路径修改为文件实际所在的位置。

在 CSV 一节的最后,我们学习了如何保存文件并在命令行中运行。在本例中,我们从一个空白文件开始,逐步完成这项任务。

首先来快速看一下总体步骤。

(1) 在代码编辑器中创建一个新文件。

(2) 将文件保存为 import_json_data.py,与你的代码位于同一个文件夹下。

(3) 将数据移动(或保存)到代码所在的文件夹。(一定要重命名数据文件,使其与代码中的文件名相同。本书用的文件名是 data-text.json。)

(4) 回到代码编辑器,import_json_data.py 文件应该还处于打开状态。

我们来通读代码,并将其与导入 CSV 的代码文件作对比。首先,导入 Python 内置的 json 库:

  1. import json

然后用学过的 open 函数打开 data-text.json 文件,并调用已打开文件的 read 方法:

  1. json_data = open('data-text.json').read()

在 CSV 文件的例子中,我们并没有调用 read。二者的区别在哪里?在 CSV 的例子中,我们以只读方式打开文件;但在 JSON 的例子中,我们读取文件的内容,并将其保存在变量 json_data 中。在 CSV 的例子中,open 函数返回的是一个文件对象;但在 JSON 的例子中,我们首先打开文件,然后读取文件,所以得到的是一个 str(字符串)。二者的不同是基于下列事实:Python 的 json 库和 csv 库处理输入数据的方式不同。如果你试着将一个字符串传递给 CSV reader,Python 会报错;如果你把文件对象传递给 JSON 的 loads 函数, Python 也会报错。

好消息是,在 Python 中将字符串写入文件非常简单(比如说,你只有字符串,但想使用 CSV reader 来读取),将文件读取成字符串也非常简单。对 Python 来说,一个关闭的文件只是一个文件名字符串,等待被打开并读取。从文件中获取数据、将数据变成字符串并将字符串传递给函数,只需要几行 Python 代码即可完成。

第 3 章 供机器读取的数据 - 图9

进入保存 JSON 文件的文件夹,你可以在 Python 解释器中输入下面的代码,看一下前面两个例子中输出对象的类型:

  1. filename = 'data-text.json'
  2. type(open(filename, 'rb')) # 与csv的代码类似
  3. type(open(filename).read()) # 与json的代码类似

Python json 库的 loads 函数接收字符串作为参数,不接收文件作为参数。Python csv 库的 reader 函数接收打开的文件作为参数。在脚本的下一行代码中,我们将使用 loads 函数,将 JSON 字符串载入 Python。这一函数的输出被赋值给名为 data 的变量:

  1. data = json.loads(json_data)

想要预览数据,我们对每一项进行遍历并将其打印出来。这段代码并不是必需的,但可以帮我们预览数据,检查数据的格式是否正确:

  1. for item in data:
  2. print item

写完上述代码后,保存文件并运行。如你所见,在 Python 中打开 JSON 文件并将其转换成由字典组成的列表是非常容易的。下一节将探索更多自定义文件的处理方法。

3.3 XML数据

XML 格式的数据既便于机器读取,也便于人工读取。但是对于本章的数据集来说,预览并理解 CSV 文件和 JSON 文件要比 XML 文件容易得多。幸运的是,数据本身是相同的,我们也比较熟悉。下载预期寿命数据的 XML 版本(下载地址:http://apps.who.int/gho/athena/data/GHO/WHOSIS_000001,WHOSIS_000002.xml?filter=COUNTRY:*;YEAR:2015),并将 XML 文件与本章其他内容保存在同一文件夹下。

第 3 章 供机器读取的数据 - 图10 如果文件的扩展名是 .xml,那么它是 XML 数据。如果文件扩展名是 .html 或 .xhtml,有时也可以用 XML 解析器来解析。

在处理所有数据时,我们先在代码编辑器中打开文件来预览一下。如果你滚动查看一下文件,会发现我们在 CSV 的例子中已经熟悉的数据。但数据看起来又不大一样,因为它用的是 XML 格式,使用了一种叫作标签的东西。

第 3 章 供机器读取的数据 - 图11 XML 是一种标记语言,也就是说,它具有包含格式化数据的文档结构。XML 文档本质上只是格式特殊的数据文件。

下面的数据片段是我们要处理的 XML 数据的一个样本。在这个例子中, 都是标签。标签(或节点)以层次化和结构化的方式保存数据:

  1. <GHO ...>
  2. <Data>
  3. <Observation FactID="4543040" Published="true"
  4. Dataset="CYCU" EffectiveDate="2014-03-27" EndDate="2900-12-31">
  5. <Dim Category="COUNTRY" Code="SOM"/>
  6. <Dim Category="REGION" Code="EMR">
  7. <Dim Category="WORLDBANKINCOMEGROUP" Code="WB_LI">
  8. <Dim Category="GHO" Code="WHOSIS_000002">
  9. <Dim Category="YEAR" Code="2012">
  10. <Dim Category="SEX" Code="FMLE">
  11. <Dim Category="PUBLISHSTATE" Code="PUBLISHED">
  12. <Value Numeric="46.00000">
  13. <Display>46</Display>
  14. </Value>
  15. </Observation>
  16. <Observation FactID="4209598" Published="true"
  17. Dataset="CYCU" EffectiveDate="2014-03-25" EndDate="2900-12-31">
  18. <Dim Category="WORLDBANKINCOMEGROUP" Code="WB_HI">
  19. <Dim Category="YEAR" Code="2000">
  20. <Dim Category="SEX" Code="BTSX">
  21. <Dim Category="COUNTRY" Code="AND">
  22. <Dim Category="REGION" Code="EUR"/>
  23. <Dim Category="GHO" Code="WHOSIS_000001">
  24. <Dim Category="PUBLISHSTATE" Code="PUBLISHED">
  25. <Value Numeric="80.00000">
  26. <Display>80</Display>
  27. </Value>
  28. </Observation>
  29. </Data>
  30. </GHO>

在 XML 文件中有两个位置可以保存数据值:一个位置是在两个标签之间,比如在 46 中, 标签的值是 46;另一个位置是标签的属性,比如在 中,Category 属性的值是"COUNTRY"Code 属性的值是"SOM"。XML 属性可以保存特定标签的额外信息,这些标签又嵌套在另一个标签中。

在 JSON 中你可以用键 / 值对来保存数据,而在 XML 中保存数据可以是两个一组甚至三四个一组。XML 用标签和属性来保存数据,类似于 JSON 中的键。所以我们再来看一下 Display 标签,这个标签的值保存在开始标签和结束标签之间。再来看一下 Dim 节点,它有两个不同的属性(CategoryCode),两个属性都有对应的值。XML 可以在每个节点中保存不止一个属性。如果你熟悉 HTML 的话,应该很熟悉这一点。这是因为 HTML 与 XML 密切相关:它们都在节点(或标签)内包含有属性,它们也都是标记语言(想了解什么是标记语言,可以去 https://en.wikipedia.org/wiki/Markup_language 查看)。

第 3 章 供机器读取的数据 - 图12 在 XML 标签结构和属性命名方面虽然有许多著名的标准,但主要结构其实是由设计或创建 XML 的人(或机器)决定的。如果你用的是来自不同来源的数据集,你不能认为它们的格式是一致的。想了解 XML 最佳实践的更多内容,IBM 给出了许多好的观点(参见 http://www.ibm.com/developerworks/library/x-eleatt/)。

如何导入XML数据

前面我们对数据已经有了一定的了解,下面将文件导入成 Python 可用的格式。为了从 XML 格式中提取数据并导入 Python,需要编写这些代码:

  1. from xml.etree import ElementTree as ET
  2. tree = ET.parse('data-text.xml')
  3. root = tree.getroot()
  4. data = root.find('Data')
  5. all_data = []
  6. for observation in data:
  7. record = {}
  8. for item in observation:
  9. lookup_key = item.attrib.keys()[0]
  10. if lookup_key == 'Numeric':
  11. rec_key = 'NUMERIC'
  12. rec_value = item.attrib['Numeric']
  13. else:
  14. rec_key = item.attrib[lookup_key]
  15. rec_value = item.attrib['Code']
  16. record[rec_key] = rec_value
  17. all_data.append(record)
  18. print all_data

可以看出来,这比 CSV 和 JSON 的代码都要复杂一些。

我们来仔细看一下。在代码编辑器中创建一个新文件,并将其保存在之前保存代码的文件夹中。文件名叫作 import_xml_data.py。另外,如果你是从 WHO 网站直接下载的数据,而不是从本书的仓库中下载的,你需要将保存的 XML 文件重命名为 data-text.xml,并将它与代码放在同一个文件夹下。

首先导入 ElementTreehttps://docs.python.org/2/library/xml.etree.elementtree.html),这是我们用来解析 XML 的内置库的一部分:

  1. from xml.etree import ElementTree as ET

第 3 章 供机器读取的数据 - 图13 前面说过,解决问题的方法往往有许多种。本例中用的是 ElementTree,你也可以用一个叫作 lxml 的库(http://lxml.de/),或者另一个叫作 minidom 的库(https://docs.python.org/2/library/xml.dom.minidom.html)。这三种方法都可以用来解决相同的问题,如果你发现一个很好的例子,用的是其中一个库,我们建议你再用另外一个库对数据进行探索。在学习 Python 的过程中,选择那些看起来最容易理解的库(在多数情况下,这也是最好的选择)。

与之前相比,这个 import 语句中多了一段:as ET。我们导入的是 ElementTree,但把它叫作 ET。为什么要这么做?因为我们很懒,不想每次用到这个库时都输入 ElementTree。在导入名字很长的类或函数时,这是很常见的做法,但并不是强制性的要求。as 告诉 Python,我们想用 ET 来代表 ElementTree

接下来,调用 ET 类的 parse 方法,这一方法将会对我们传入文件中的数据进行解析。由于我们要解析的文件位于同一文件夹下,所以文件名中不需要包含文件路径:

  1. tree = ET.parse('data-text.xml')

parse 方法返回一个 Python 对象,人们一般会把它保存在变量 tree 中。在谈论 XML 时,(tree)指的是整个 XML 对象,以 Python 能够理解并解析的方式保存。

为了理解遍历树(及其包含的数据)的方法,我们从树的根元素(root)开始。根节点是第一个 XML 标签。调用 getroot 函数来获取树的根元素:

  1. root = tree.getroot()

如果你在上一条语句后面加上 print root,打印出 root 的内容,会发现,输出的是 XML 树中根元素的 Python 表示(看起来应该像这样:2)。从这个表示中我们很快可以看出,ElementTree 找出了 XML 文档的根标签或最外层标签,就是标签名为 GHO 的 XML 节点。

2对于由十六进制数字组成的长字符串表示的 Python 对象,这是 Python 显示内存地址信息的方法。我们的数据处理过程用不到这些内容,所以如果你的内存地址与我们的有所不同,请不必在意。

前面我们找到了根标签,下面要学习如何访问想要的数据。在本章 CSV 和 JSON 两节中,我们对数据进行了分析,知道要处理的是什么数据。我们需要遍历整个 XML 树,将同样的数据提取出来。为了理解要寻找的数据,需要先理解 XML 树的整体结构与格式。下面,将前面的 XML 文件简化一下,删除数据,这样就可以只看核心结构:

  1. <GHO>
  2. <Data>
  3. <Observation>
  4. <Dim >
  5. <Dim >
  6. <Dim >
  7. <Dim >
  8. <Dim >
  9. <Dim >
  10. <Value>
  11. <Display>
  12. </Display>
  13. </Value>
  14. </Observation>
  15. <Observation>
  16. <Dim >
  17. <Dim >
  18. <Dim >
  19. <Dim >
  20. <Dim >
  21. <Dim >
  22. <Value>
  23. <Display>
  24. </Display>
  25. </Value>
  26. </Observation>
  27. </Data>
  28. </GHO>

在纵览文件结构时可以看到,每一“行”数据都包含在一个 Observation 标签中。在每一个 Observation 节点内,这些数据行又分别包含在 DimValueDisplay 节点中。

目前我们有三行代码。为了研究如何用 Python 将这些节点提取出来,在现有代码的末尾加上 print dir(root),然后保存文件并在命令行中运行:

  1. python import_xml_data.py

你会看到变量 root 所有的方法和属性。你的代码应该是这样的:

  1. from xml.etree import ElementTree as ET
  2. tree = ET.parse('data-text.xml')
  3. root = tree.getroot()
  4. print dir(root)

运行该文件,你应该会看到下面的输出:

  1. ['__class__', '__delattr__', '__delitem__', '__dict__', '__doc__',
  2. '__format__', '__getattribute__', '__getitem__', '__hash__', '__init__',
  3. '__len__', '__module__', '__new__', '__nonzero__', '__reduce__',
  4. '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__',
  5. '__str__', '__subclasshook__', '__weakref__', '_children', 'append', 'attrib',
  6. 'clear', 'copy', 'extend', 'find', 'findall', 'findtext', 'get',
  7. 'getchildren', 'getiterator', 'insert', 'items', 'iter', 'iterfind',
  8. 'itertext', 'keys', 'makeelement', 'remove', 'set', 'tag', 'tail', 'text']

假设文件太大,无法打开,我们也不知道文件的结构。在处理大型 XML 数据集时经常会遇到这样的问题。我们能怎么做?首先调用 dir(root) 来查看 root 对象都有哪些方法。我们注意到 getchildren 方法,也许可以用来查看 Observation 节点的子元素。在查阅官方最新文档(https://docs.python.org/2/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.getchildren)以及 Stack Overflow 上的一个问题(http://stackoverflow.com/questions/10408927/how-to-get-all-sub-elements-of-an-elementtree-with-python-elementtree)之后,我们发现,getchildren 方法可以返回子元素,但官方文档不建议继续使用该方法。如果你想用的某个方法已经不建议使用或者即将弃用,你应该尝试换用库作者推荐的方法。

第 3 章 供机器读取的数据 - 图14 如果不建议使用某个方法、类或函数的话,在库或模块的未来版本中很可能会删除它们对应的功能。因此,在任何时候你都应该避免使用不建议使用的方法或类,并通读文档,因为作者很可能会建议未来使用的替代方法或类。

根据官方文档的建议,如果想查看根元素的子元素,应该使用 list(root)。如果我们的文件很大,返回子元素可以让我们查看数据及其结构,而不会产生超级长的输出。让我们来试一下。

将这行代码:

  1. print dir(root)

替换成:

  1. print list(root)

在命令行中再次运行该文件。你应该会得到下面的输出,是一个由 Element 对象构成的列表(对于本例来说,元素指的是 XML 节点):

  1. [<Element 'QueryParameter' at 0x101bfd290>,
  2. <Element 'QueryParameter' at 0x101bfd350>,
  3. <Element 'QueryParameter' at 0x101bfd410>,
  4. <Element 'QueryParameter' at 0x101bfd590>,
  5. <Element 'QueryParameter' at 0x101bfd610>,
  6. <Element 'QueryParameter' at 0x101bfd650>,
  7. <Element 'Copyright' at 0x101bfd690>,
  8. <Element 'Disclaimer' at 0x101bfd710>,
  9. <Element 'Metadata' at 0x101bfd790>,
  10. <Element 'Data' at 0x102540250>]

列表包含的 Element 对象分别叫作 QueryParameterCopyrightDisclaimerMetadataData。我们可以遍历这些元素来探索它们的内容,这样才能更好地理解如何提取我们想要的数据。

在 XML 树里面搜索数据时,Data 元素可能是一个很好的出发点。现在我们已经找到了 Data 元素,可以重点研究这个子元素。获取 Data 元素有好几种方法,这里用 find 方法。根元素的 find 方法可以利用标签名来搜索子元素。然后,我们就可以获取 Data 元素的子元素,看看下一步应该做什么。

将这行代码:

  1. print list(root)

替换成:

  1. data = root.find('Data')
  2. print list(data)

第 3 章 供机器读取的数据 - 图15 我们还可以使用 findall 方法。findfindall 的区别在于,find 返回的是匹配的第一个元素,而 findall 返回的是匹配的所有元素。我们知道只有一个 Data 元素,所以我们用的是 find 而不是 findall。如果有不止一个元素,要用 findall 方法获取所有匹配元素的列表,然后遍历这些元素。

修改完代码后重新运行该文件,你会看到输出一个超级长的列表,列表由 Observation 元素组成。这些是我们的数据点。虽然输出里包含很多信息,但你可以看出它是一个列表,因为最后一个字符是 ],这是列表结束的符号。

我们来遍历这个列表中的数据。每个 Observation 元素代表一行数据,里面应该会包含更多的信息。我们可以分别遍历这些元素,看看都有什么子元素。对于 Python 中的 Element 对象,可以遍历其所有的子元素,就像遍历列表一样。因此,我们可以遍历每一个 Observation 元素及其每一个子元素。这是我们第一次使用包含循环的循环,从中我们应该可以知道是否还有更多包含数据的子元素。

第 3 章 供机器读取的数据 - 图16 由于 XML 以节点、子节点和属性的方式保存数据,你会经常采用探索每一个节点和子节点(或者元素和子元素)的方法,直到你掌握了数据的结构,以及如何用 Python 来查看这些数据。

将这行代码:

  1. print list(root)

替换成:

  1. for observation in data:
  2. for item in observation:
  3. print item

然后重新运行该文件。

输出的是许多 DimValue 对象。我们尝试几种不同的方法,来探索这些元素中可能包含的内容。Python 的 Element 对象有好几种查看数据的方法。每个 Element 节点都有一个属性 text,可以给出节点内包含的文本。

将这行代码:

  1. print item

替换成:

  1. print item.text

然后重新运行该文件。

发生了什么?你得到的返回值应该是许多 None。这是因为很多元素的标签之间没有任何文本,所以这些元素的 item.text 都不存在。我们来看一下数据样本里 的结构。例如:

  1. <Dim Category="YEAR" Code="2000"/>

在 Python 中,只有在节点中包含文本的情况下,item.text 才是有用的,像这样:

  1. <Dim Category="YEAR">2000</Dim>

对于第二个例子,item.text 返回的是 2000

XML 数据可以有许多种结构。我们需要的信息包含在 XML 里,只是不在一眼就能发现的地方。我们来继续探索。

另一个要查看的地方在子元素中。我们来检查一下里面是否包含子元素。将这行代码:

  1. print item.text

替换成:

  1. print list(item)

修改后重新运行代码,从输出中可以看出,某些元素(但不是全部)具有子元素。有意思!这些元素其实是 Value 元素。我们来看一下数据样本中这些元素的结构:

  1. <Value>
  2. <Display>
  3. </Display>
  4. </Value>

如果想探索这些子元素,还需要写一个类似之前写过的循环,来遍历每一个 Observation 中的元素。

Python 中的 Element 对象还有另一个方法可以调用,叫作 attrib,它可以返回每一个节点的属性。在查看 XML 结构时我们已经知道,如果节点的标签之间没有值,那么在标签内通常会有属性。

想要查看节点的属性,将这行代码:

  1. print list(item)

替换成:

  1. print item.attrib

重新运行代码,可以看到,输出是由包含在属性中的数据组成的许多字典。我们希望将每一行数据保存成一个字典,而不是将每一个元素及其属性保存在不同的字典中。下面是 attrib 输出的一条记录:

  1. {'Category': 'PUBLISHSTATE', 'Code': 'PUBLISHED'}
  2. {'Category': 'COUNTRY', 'Code': 'ZWE'}
  3. {'Category': 'WORLDBANKINCOMEGROUP', 'Code': 'WB_LI'}
  4. {'Category': 'YEAR', 'Code': '2012'}
  5. {'Category': 'SEX', 'Code': 'BTSX'}
  6. {'Category': 'GHO', 'Code': 'WHOSIS_000002'}
  7. {'Category': 'REGION', 'Code': 'AFR'}
  8. {'Numeric': '49.00000'}

在 CSV 的例子中,我们得到了每条数据记录组成的字典,我们试着把上面的输出转换成类似的格式。XML 数据字典的键稍有不同,因为 WHO 在 XML 数据集中提供的数据与 CSV 数据集并不相同。我们将会把数据转换成下面的格式,但键名可以不同。这基本不会影响我们对数据的使用。

提醒一下,CSV reader 的样本数据记录如下:

  1. {
  2. 'Indicator': 'Healthy life expectancy (HALE) at birth (years)',
  3. 'Country': 'Zimbabwe',
  4. 'Comments': '',
  5. 'Display Value': '51',
  6. 'World Bank income group': 'Low-income',
  7. 'Numeric': '51.00000',
  8. 'Sex': 'Female',
  9. 'High': '',
  10. 'Low': '',
  11. 'Year': '2012',
  12. 'WHO region': 'Africa',
  13. 'PUBLISH STATES': 'Published'
  14. }

对于样本数据记录,我们希望将 XML 数据转换成下面的样子。在解析完 XML 树之后,我们希望数据的格式就是这样:

  1. {
  2. 'COUNTRY': 'ZWE',
  3. 'GHO': 'WHOSIS_000002',
  4. 'Numeric': '49.00000',
  5. 'PUBLISHSTATE': 'PUBLISHED',
  6. 'REGION': 'AFR',
  7. 'SEX': 'BTSX',
  8. 'WORLDBANKINCOMEGROUP': 'WB_LI',
  9. 'YEAR': '2012'
  10. }

注意 HighLow 字段是缺失的。如果 XML 数据集中这两个字段没有缺失的话,我们会把它们添加到新字典的键中。Display 的值也是缺失的。我们决定不用这个值,因为它和 Numeric 的值相同。

现在你的代码应该是像这样的:

  1. from xml.etree import ElementTree as ET
  2. tree = ET.parse('data-text.xml')
  3. root = tree.getroot()
  4. data = root.find('Data')
  5. for observation in data:
  6. for item in observation:
  7. print item.attrib

想要创建数据结构,首先要为每一条数据记录创建一个空字典。我们向空字典中添加键值对,然后将每一条数据记录添加到一个列表中,这样我们最终的列表中就包含了所有的数据记录(类似于前面的 CSV 数据)。

首先创建用来保存数据的空字典和空列表。在最外层 for 循环上面,添加一行代码:all_data = [],然后将 record = {} 作为 for 循环的第一行,像这样:

  1. all_data = []
  2. for observation in data:
  3. record = {}
  4. for item in observation:
  5. print item.attrib

现在要找出每一行的键和值,并将其添加到数据记录的字典中。每一次调用 attrib,都会得到包含一个或多个键值对的字典,像这样:

  1. {'Category': 'YEAR', 'Code': '2012'}

看上去 Category 键的值(这里是 YEAR)应该是新字典的键,而 Code 的值(这里是 2012)应该被设成对应的值。回想第 2 章学过的内容,字典的键应该易于检索(比如 YEAR),字典的值应该包含与键对应的值(比如 2012)。认识到这一点,前面一行将变成:

  1. 'YEAR': '2012'

将代码中的 print item.attrib 修改成 print item.attrib.keys(),然后重新运行该文件:

  1. for item in observation:
  2. print item.attrib.keys()

输出的是每一个属性字典的键。我们想查看字典的键,这样才能构建新字典的键值对。我们发现只有两种不同的输出:['Category', 'Code']['Numeric']。我们对两种情况分别处理。根据前面的研究,对于同时具有 CategoryCode 的元素,需要用 Category 的值作为键,用 Code 的值作为值。

要做到这一点,在 item.attrib.keys() 的结尾添加 [0]

  1. for item in observation:
  2. lookup_key = item.attrib.keys()[0]
  3. print lookup_key

这叫索引(indexing)。返回的是列表的第一个元素。

使用列表索引

对于 Python 中的列表或其他可迭代对象,索引指的是取出列表的第 n 个对象。Python 的索引从 0 开始,也就是说,第一个元素的编号是 0,第二个元素是 1,以此类推。由于我们的列表中有一个或两个元素,我们只想要第一个元素,所以在代码中加了 [0]

回头看一下上一章中狗狗的例子:

  1. dog_names = ['Joker', 'Simon', 'Ellie', 'Lishka', 'Fido']

如果想从列表中提取出Ellie,那你想要的是列表的第三个元素。由于索引编号从 0 开始,你可以用下面的代码提取出这个元素:

  1. dog_names[2]

在 Python 解释器中试一下上面的代码,然后尝试提取出 Simon。如果用负数作为索引会怎么样?(提示:负数索引是从列表末尾倒着向前数!)

重新运行代码,输出应该是这样的:

  1. Category
  2. Category
  3. Category
  4. Category
  5. Category
  6. Category
  7. Category
  8. Numeric

现在我们有了键的名字,下面要寻找键对应的值。我们要用 Category 键的值作为新字典的键。在内层 for 循环中创建一个新变量 rec_key,用来保存 item.attrib[lookup_key] 返回的值:

  1. for item in observation:
  2. lookup_key = item.attrib.keys()[0]
  3. rec_key = item.attrib[lookup_key]
  4. print rec_key

修改完成后,在命令行中重新运行代码。对于每一条数据记录,我们得到下面的值:

  1. PUBLISHSTATE
  2. COUNTRY
  3. WORLDBANKINCOMEGROUP
  4. YEAR
  5. SEX
  6. GHO
  7. REGION
  8. 49.00000

看起来都很适合做新字典的键,除了最后一个。这是因为最后一个元素是 Numeric 字典,而不是我们要处理的 Category 字典。如果想保留这些数据供后续使用,需要用 if 语句为这些数值元素创建一种特殊情况。

Python 的 if 语句

if 语句最基本的形式,是用来控制代码流的方法。if 语句是在告诉代码:如果满足该条件,那就执行给定的命令。

if 语句的另一种用法是与 else 一起使用。if-else 语句的意思是:如果满足第一个条件,那么执行相应的命令;但如果不满足该条件,那么执行 else 语句中的命令。

除了 ifif-else 之外,你还会将 == 作为比较运算符。== 用来给变量赋值,而 == 用来检验两个值是否相等。另外,!= 用来检验两个值是否不相等。这两个运算符返回的都是布尔值:TrueFalse

在 Python 解释器中试试下面的例子:

  1. x = 5
  2.  
  3. if x == 5:
  4. print 'x is equal to 5.'

你看到了什么? x == 5 返回的是 True,所以打印出了相应的文字。现在试一下这个:

  1. x = 3
  2.  
  3. if x == 5:
  4. print 'x is equal to 5.'
  5. else:
  6. print 'x is not equal to 5.'

本例中 x 等于 3,不等于 5,所以你应该会看到 else 代码块中 print 语句的输出。在 Python 中,你可以用 ifif-else 语句来帮助控制代码流的逻辑。

lookup_key 等于 Numeric 时,我们希望使用 Numeric 作为新字典的键,而不是用它对应的值作为新字典的键(就像 Category 键那样)。将代码修改成:

  1. for item in observation:
  2. lookup_key = item.attrib.keys()[0]
  3. if lookup_key == 'Numeric':
  4. rec_key = 'NUMERIC'
  5. else:
  6. rec_key = item.attrib[lookup_key]
  7. print rec_key

运行修改后的代码,现在所有的键看起来应该都是我们想要的。下面提取出想要保存在新字典中的值,并将它们与这些键相关联。对于 Numeric,问题比较简单,因为我们只想要 Numeric 键对应的值。对代码作如下修改:

  1. if lookup_key == 'Numeric':
  2. rec_key = 'NUMERIC'
  3. rec_value = item.attrib['Numeric']
  4. else:
  5. rec_key = item.attrib[lookup_key]
  6. rec_value = None
  7. print rec_key, rec_value

运行修改后的代码,你会发现 Numericrec_value 已经匹配好了。比如:

  1. NUMERIC 49.00000

对于其他所有的值,我们将 rec_value 设置成 None。在 Python 中,None 用来表示一个空值。我们来将这些空值填上真实的数值。记得每一条数据记录都有一个 Category 键和一个 Code 键,像这样:{'Category': 'YEAR', 'Code': '2012'}。对于这些元素,我们想将 Code 的值保存为 rec_value。修改这一行代码:rec_value = None,将 if-else 语句改成下面这样:

  1. if lookup_key == 'Numeric':
  2. rec_key = 'NUMERIC'
  3. rec_value = item.attrib['Numeric']
  4. else:
  5. rec_key = item.attrib[lookup_key]
  6. rec_value = item.attrib['Code']
  7. print rec_key, rec_value

重新运行代码,你现在应该会看到,rec_keyrec_value 都有相应的值。下面来创建字典:

  1. if lookup_key == 'Numeric':
  2. rec_key = 'NUMERIC'
  3. rec_value = item.attrib['Numeric']
  4. else:
  5. rec_key = item.attrib[lookup_key]
  6. rec_value = item.attrib['Code']
  7. record[rec_key] = rec_value

❶ 将每一个键值对添加到 record 字典中。

我们还需要将每一条数据记录添加到 all_data 列表中。在 2.3.3 节中讲过,可以用列表的 append 方法向列表中添加元素。在外层 for 循环的结尾,record 中已经包含了每一个子元素的键,这时我们将 record 添加到列表中。最后,在文件结尾添加 print 来查看数据。

将 XML 树转换成字典的全部代码应该是这样的:

  1. from xml.etree import ElementTree as ET
  2. tree = ET.parse('data-text.xml')
  3. root = tree.getroot()
  4. data = root.find('Data')
  5. all_data = []
  6. for observation in data:
  7. record = {}
  8. for item in observation:
  9. lookup_key = item.attrib.keys()[0]
  10. if lookup_key == 'Numeric':
  11. rec_key = 'NUMERIC'
  12. rec_value = item.attrib['Numeric']
  13. else:
  14. rec_key = item.attrib[lookup_key]
  15. rec_value = item.attrib['Code']
  16. record[rec_key] = rec_value
  17. all_data.append(record)
  18. print all_data

运行上面的代码,你会看到一个长列表,列表的元素是每一条数据记录组成的字典,与 CSV 例子中的相同:

  1. {'COUNTRY': 'ZWE', 'REGION': 'AFR', 'WORLDBANKINCOMEGROUP': 'WB_LI',
  2. 'NUMERIC': '49.00000', 'SEX': 'BTSX', 'YEAR': '2012',
  3. 'PUBLISHSTATE': 'PUBLISHED', 'GHO': 'WHOSIS_000002'}

可以看出来,从 XML 中提取数据要稍复杂一些。有时 CSV 文件和 JSON 文件也并不像本章的例子那样容易处理,但它们通常比 XML 文件容易处理一些。但是,作为 Python 开发人员,处理 XML 数据让你可以深入探索并成长,让你可以创建空列表和字典,然后向里面填充数据。在研究如何从 XML 树结构里提取数据的过程中,你还锻炼了自己的调试能力。在通往成为更优秀的数据分析员的路上,这些都是宝贵的经验。

3.4 小结

能够用 Python 处理机器可读的数据格式,这是数据处理的必备技能之一。本章讲了 CSV、 JSON 和 XML 三种文件类型。表 3-2 给出了在处理 WHO 数据的不同文件格式时所用到的 Python 库。

表3-2:文件类型和文件扩展名

文件类型 文件扩展名 Python 库
CSV、TSV .csv、.tsv csv 库(https://docs.python.org/2/library/csv.html
JSON .json、.js json 库(https://docs.python.org/2/library/json.html

我们还讲了一些新的 Python 概念。现在你应该知道如何在 Python 解释器中运行 Python 代码,以及如何将代码保存到新文件,并在命令行中运行。我们还学习了用 import 导入文件,以及用 Python 的 readopen 打开本地文件并读取。

我们讲的编程新概念还包括用 for 循环遍历文件、列表或树,还有用 if-else 语句判断特定条件是否满足,然后据此执行对应的命令。表 3-3 对本章学过的新函数和代码逻辑做了总结。

表3-3:Python编程的新概念

概念 作用
importhttps://docs.python.org/2/reference/simple_stmts.html#import 向 Python 中导入模块
openhttps://docs.python.org/2/library/functions.html#open 内置函数,用 Python 打开本地文件
for 循环(http://anh.cs.luc.edu/python/handson/3.1/handsonHtml/loops.html#basic-for-loops 一段代码,运行 n
if-else 语句(http://anh.cs.luc.edu/python/handson/3.1/handsonHtml/ifstatements.html#simple-if-statements 如果满足特定条件,运行一段代码
==(等于运算符,https://docs.python.org/2/reference/expressions.html#not-in 检验两个值是否相等
序列索引(https://docs.python.org/2/library/stdtypes.html#sequence-types-str-unicode-list-tuple-bytearray-buffer-xrange 取出序列(字符串、列表等)中第 n 个对象

最后,我们在本章创建并保存了许多代码文件和数据文件。假如你完成了本章的所有练习,应该有三个代码文件和三个数据文件。本章前面推荐过组织代码的方法。如果你还没有照做的话,现在马上去做。这是目前所有文件的组织结构示例:

  1. data_wrangling/
  2. code/
  3. ch3_easy_data/
  4. import_csv_data.py
  5. import_xml_data.py
  6. import_json_data.py
  7. data-text.csv
  8. data-text.xml
  9. data-json.json
  10. ch4_hard_data/
  11. ...

接下来,我们要学习更复杂的数据格式!