Python数据分析NumPy和pandas(十六、文本格式数据的读取与存储:csv、json、xml和html)
一、分段读取文本文件
在处理非常大的文件时,未找到合适的数据处理方法前,我们一般希望只读取文件的一小部分或遍历文件的较小块来做预处理或参考。这种情况可以采用分段读取文本文件的方式。
我们加载一个10000行的ex6.csv文件,其内容如下:
一般情况下,对于pandas读取大文件数据时会默认显示前5行和后5行,其默认设置是:pd.options.display.max_rows = 10
用 result = pd.read_csv("examples/ex6.csv") 加载ex6.csv会输出以下结果:
one | two | three | four | key | |
---|---|---|---|---|---|
0 | 0.467976 | -0.038649 | -0.295344 | -1.824726 | L |
1 | -0.358893 | 1.404453 | 0.704965 | -0.200638 | B |
2 | -0.501840 | 0.659254 | -0.421691 | -0.057688 | G |
3 | 0.204886 | 1.074134 | 1.388361 | -0.982404 | R |
4 | 0.354628 | -0.133116 | 0.283763 | -0.837063 | Q |
... | ... | ... | ... | ... | ... |
9995 | 2.311896 | -0.417070 | -1.409599 | -0.515821 | L |
9996 | -0.479893 | -0.650419 | 0.745152 | -0.646038 | E |
9997 | 0.523331 | 0.787112 | 0.486066 | 1.093156 | K |
9998 | -0.362559 | 0.598894 | -1.843201 | 0.887292 | G |
9999 | -0.096376 | -1.012999 | -0.657431 | -0.573315 | 0 |
10000 rows × 5 columns
中间的省略号......表示 DataFrame 中间的行已被省略。如果只想读取少量行(避免读取整个文件),使用 nrows 指定读取的行数:
import pandas as pd
result = pd.read_csv("examples/ex6.csv", nrows=5)
print(result)
输出前5行:
one | two | three | four | key | |
---|---|---|---|---|---|
0 | 0.467976 | -0.038649 | -0.295344 | -1.824726 | L |
1 | -0.358893 | 1.404453 | 0.704965 | -0.200638 | B |
2 | -0.501840 | 0.659254 | -0.421691 | -0.057688 | G |
3 | 0.204886 | 1.074134 | 1.388361 | -0.982404 | R |
4 | 0.354628 | -0.133116 | 0.283763 | -0.837063 | Q |
要分段读取文件,还可以使用 chunksize 指定每个文件块包含数据的行数,例如:
import pandas as pd
chunker = pd.read_csv("examples/ex6.csv", chunksize=1000)
print(type(chunker))
输出:<class 'pandas.io.parsers.readers.TextFileReader'>
以上代码用chunksize=1000指定块文件大小为1000行,返回的chunker是一个TextFileReader对象。pandas.read_csv 返回的 TextFileReader 对象可以让我们根据 chunksize 迭代文件的各个部分。例如,我们可以迭代 ex6.csv,聚合 “key” 列中的值计数(每个key有多少个,相当于数据库表按key进行group by计数),如以下代码(看注释):
import pandas as pd
#设置读取ex6.csv文件的块大小为1000行数据
chunker = pd.read_csv("examples/ex6.csv", chunksize=1000)
#创建一个空的Series对象,用于存储按key 进行group by统计计数
tot = pd.Series([], dtype='int64')
#按块迭代, 这里因为将chunksize设置为1000,所以有10个块
#对每个块中的key进行计数,然后根据key汇总相加得到整个文档的key计数
for piece in chunker:
tot = tot.add(piece["key"].value_counts(), fill_value=0)
#按降序排列
tot = tot.sort_values(ascending=False)
print(tot)
输出结果:
key
E 368.0
X 364.0
L 346.0
O 343.0
Q 340.0
M 338.0
J 337.0
F 335.0
K 334.0
H 330.0
V 328.0
I 327.0
U 326.0
P 324.0
D 320.0
A 320.0
R 318.0
Y 314.0
G 308.0
S 308.0
N 306.0
W 305.0
T 304.0
B 302.0
Z 288.0
C 286.0
4 171.0
6 166.0
7 164.0
8 162.0
3 162.0
5 157.0
2 152.0
0 151.0
9 150.0
1 146.0
dtype: float64
TextFileReader 还配备了一个 get_chunk 方法,使我们能够读取任意大小的片段,例如上面代码中的chunker.get_chunk(10) 可以获取ex6.csv的前10行。
二、将数据写入文本格式
数据也可以导出为分隔格式。我们看下之前上一篇中的ex5.csv文件,其内容如下:
data = pd.read_csv("examples/ex5.csv") 输出:
something | a | b | c | d | message | |
---|---|---|---|---|---|---|
0 | one | 1 | 2 | 3.0 | 4 | NaN |
1 | two | 5 | 6 | NaN | 8 | world |
2 | three | 9 | 10 | 11.0 | 12 | foo |
使用 DataFrame 的 to_csv 方法,我们可以将数据写出到逗号分隔的文件中 :
import pandas as pd
data = pd.read_csv("examples/ex5.csv")
data.to_csv("examples/out.csv")
执行完成后会在examples文件夹中输出一个out.csv文件。
当然,也可以使用其他分隔符(我们用 sys.stdout看看,以便将文本结果打印到控制台而不是文件):
import sys
import pandas as pd
data = pd.read_csv("examples/ex5.csv")
#输出到控制台
data.to_csv(sys.stdout, sep="|")
这里用sep参数指定了分隔符“|”,控制台输出:
|something|a|b|c|d|message
0|one|1|2|3.0|4|
1|two|5|6||8|world
2|three|9|10|11.0|12|foo
从输出结果来看,缺失值在输出中显示为空字符串。您可能希望用其他一些值来表示它们,例如:data.to_csv(sys.stdout, na_rep="NULL") 用NULL来替换。控制台输出如下:
,something,a,b,c,d,message
0,one,1,2,3.0,4,NULL
1,two,5,6,NULL,8,world
2,three,9,10,11.0,12,foo
用代码来总结下常用的输出格式用法(注意看注释),如下(可以运行看看输出结果):
import sys
import pandas as pd
#加载数据
data = pd.read_csv("examples/ex5.csv")
#输出到控制台
#用sep参数指定分割符
data.to_csv(sys.stdout, sep="|")
#用na_rep指定用什么替换缺失值
data.to_csv(sys.stdout, na_rep="NULL")
#用index参数,header参数 指定是否写入行和列标签。下面这两者设置为False都被禁止写入
data.to_csv(sys.stdout, index=False, header=False)
#可以用columns参数指定列标签
data.to_csv(sys.stdout, index=False, columns=["a", "b", "c"])
三、使用其他分隔格式
我们可以使用 pandas.read_csv 等函数从磁盘加载大多数形式的表格数据。但是,在某些情况下,可能需要进行一些手动处理,因为我们可能会经常加载到包含一行或多行格式错误的文件。我们来看一个小的csv文件ex7.csv,内如如下。
对于任何具有单字符分隔符的文件,我们可以使用 Python 的内置 csv 模块。要使用csv,可以将任何打开的文件或类似文件的对象传递给 csv.reader方法。如下:
import sys
import csv
import pandas as pd
#用python内置的open打开文件ex7.csv,得到一个文件对象f
f = open("examples/ex7.csv")
#将文件对象f传递给python内置的csv.reader
reader = csv.reader(f)
#循环迭代输出每一行
for line in reader:
print(line)
#用完之后要关闭打开的文件
f.close()
以上代码输出(注意看注释):
['a', 'b', 'c']
['1', '2', '3']
['1', '2', '3']
下面我们一步一步(step by step)来对以上数据进行处理,将数据输入到所需要的格式。
import sys
import csv
import pandas as pd
# 首先用python内置的open打开文件ex7.csv,得到一个文件对象f
# 这里用了python内置with模块,
# with open语句通过上下文管理器自动管理文件的打开和关闭,
# 确保在代码块执行完毕后自动关闭文件,无论是否发生异常
with open("examples/ex7.csv") as f:
lines = list(csv.reader(f))
#将第一行设置为列标签,其他行设置为值存储
header, values = lines[0], lines[1:]
# 然后,我们使用字典推导式和表达式 zip(*values) 创建数据列的字典
# (请注意,这将在大文件上占用大量内存),它将行转置为列:
data_dict = {h: v for h, v in zip(header, zip(*values))}
print(data_dict)
输出:{'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}
CSV 文件有许多不同的风格。要定义具有不同分隔符、字符串引用约定或行终止符的新格式,我们可以定义 csv.Dialect的简单子类。例如:
import sys
import csv
import pandas as pd
#定义一个子类
class my_dialect(csv.Dialect):
lineterminator = "\n"
delimiter = ";"
quotechar = '"'
quoting = csv.QUOTE_MINIMAL
f = open("examples/ex7.csv")
reader = csv.reader(f, dialect=my_dialect)
#我们还可以将单个 CSV 方言参数作为关键字提供给 csv.reader,而无需定义子类:
reader = csv.reader(f, delimiter="|")
以下列表是csv.Dialect的可选项参数。
注意:对于具有更复杂或固定的多字符分隔符的文件, csv 模块可以能将无法处理。在这些情况下,必须使用字符串的 split 方法或正则表达式方法 re.split 进行处理。但是,一般情况下用pandas.read_csv 几乎可以处理大部分事情,很少再需要手动解析文件。
下面我将通过csv.write函数将ex7.csv中的输出到一个叫mydata.csv的新文件中,同时用my_dialect指定输出格式。如下:
import sys
import csv
import pandas as pd
#定义一个子类
class my_dialect(csv.Dialect):
lineterminator = "\n"
delimiter = ";"
quotechar = '"'
quoting = csv.QUOTE_MINIMAL
f = open("examples/ex7.csv")
with open("mydata.csv", "w") as f:
writer = csv.writer(f, dialect=my_dialect)
writer.writerow(("one", "two", "three"))
writer.writerow(("1", "2", "3"))
writer.writerow(("4", "5", "6"))
writer.writerow(("7", "8", "9"))
f.close()
输出内容:
四、处理JSON数据
JSON(JavaScript Object Notation 的缩写)已成为在 Web 浏览器和其他应用程序之间通过 HTTP 请求发送数据的标准格式之一。它是一种比 CSV 等表格文本格式更自由的数据格式。下面是一个JSON示例:
obj = """
{"name": "Wes",
"cities_lived": ["Akron", "Nashville", "New York", "San Francisco"],
"pet": null,
"siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]},
{"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}]
}
"""
JSON 除了一些null值和一些小的差别外(例如不允许在列表末尾使用尾随逗号),非常接近的 Python 代码格式,但其 null 值、null 和其他一些细微差别(例如不允许在列表末尾使用尾随逗号)除外。JSON基本类型包括对象 (字典)、数组 (列表)、字符串、数字、布尔值和 null。JSON对象中的所有键都必须是字符串。有几个 Python 库可用于读取和写入 JSON 数据,其中包括Python内置的json模块,下面我们先使用该模块来存取json。
要将 JSON 字符串转换为 Python 格式,可以使用 json.loads:
import json
import pandas as pd
#json格式的字符串
obj = """{"name": "Wes","cities_lived": ["Akron", "Nashville", "New York", "San Francisco"],"pet": null,"siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]},{"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}]}"""
result = json.loads(obj)
print(result)
输出结果:
{'name': 'Wes', 'cities_lived': ['Akron', 'Nashville', 'New York', 'San Francisco'], 'pet': None, 'siblings': [{'name': 'Scott', 'age': 34, 'hobbies': ['guitars', 'soccer']}, {'name': 'Katie', 'age': 42, 'hobbies': ['diving', 'art']}]}
另外,可以用json.dumps 将 Python 对象转换回 JSON。
import json
import pandas as pd
#json格式的字符串
obj = """{"name": "Wes","cities_lived": ["Akron", "Nashville", "New York", "San Francisco"],"pet": null,"siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]},{"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}]}"""
result = json.loads(obj)
asjson = json.dumps(result)
print(asjson)
加载得到json格式对象,我们可以基于此构造DataFrame来处理数据:
import json
import pandas as pd
#json格式的字符串
obj = """{"name": "Wes","cities_lived": ["Akron", "Nashville", "New York", "San Francisco"],"pet": null,"siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]},{"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}]}"""
result = json.loads(obj)
#用result的siblings构造DataFrame,同时用columns设置列标签
siblings = pd.DataFrame(result["siblings"], columns=["name", "age"])
print(siblings)
输出结果:
name | age | |
---|---|---|
0 | Scott | 34 |
1 | Katie | 42 |
回到pandas学习,pandas.read_json 可以自动将特定排列的 JSON 数据集转换为 Series 或 DataFrame。我们准备一个example.json文件,内容如下图:
用pd.read_json("examples/example.json")加载这个json:
import pandas as pd
data = pd.read_json("examples/example.json")
print(data)
输出结果:
a | b | c | |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 5 | 6 |
2 | 7 | 8 | 9 |
对于有关读取和操作 JSON 数据(包括嵌套记录),我在后面的学习过程中还会用大的操作实例来深入学习。
如果需要将数据从 pandas 导出到 JSON,可以在 Series 和 DataFrame 上使用 to_json 方法。
import sys
import pandas as pd
data = pd.read_json("examples/example.json")
data.to_json(sys.stdout)
data.to_json(sys.stdout, orient="records")
data.to_json(sys.stdout)在控制台输出:
{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}
data.to_json(sys.stdout, orient="records")在控制台输出:
[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]
五、XML 和 HTML:网页数据抓取
Python 有许多库,用于处理无处不在的 HTML 和 XML 格式读取和写入数据。包括 lxml、Beautiful Soup 和 html5lib。lxml 通常处理相对较快,但其他库可以更好地处理格式错误的 HTML 或 XML 文件。
pandas 有一个内置函数 pandas.read_html,它会使用所有这些库自动将 HTML 文件中的表格解析为 DataFrame 对象。我们来学习它的。但是首先我们要安装 read_html 使用的一些其他库。命令行分别导入:
pip install lxml
pip install beautifulsoup4
pip install html5lib
pandas.read_html 函数有许多选项,但默认情况下,它会搜索并尝试解析标记中包含的所有表格数据<table>。结果是输出 DataFrame 对象的列表。
我们看一个html文件fdic_failed_bank_list.html,这是一个数据写。其中包含美国的一些银行倒闭表格数据。
import sys
import pandas as pd
#从html文件中抓取表格对象,如果有多个表格那么tables会是一个DataFrame 对象的列表
tables = pd.read_html("examples/fdic_failed_bank_list.html")
print(len(tables))
我们用len函数看下返回的tables中包含多少个DataFrame对象,这里输出1,表示在这个 html页面中只查到有1个table。下面来读取这个表中的数据,用head函数读取前5行。
import sys
import pandas as pd
#从html文件中抓取表格对象,如果有多个表格那么tables会是一个DataFrame 对象的列表
tables = pd.read_html("examples/fdic_failed_bank_list.html")
#取列表的第一个DF对象
failures = tables[0]
#用head输出前5行数据
print(failures.head())
输出:
Bank Name | City | ST | CERT | Acquiring Institution | Closing Date | Updated Date | |
---|---|---|---|---|---|---|---|
0 | Allied Bank | Mulberry | AR | 91 | Today's Bank | September 23, 2016 | November 17, 2016 |
1 | The Woodbury Banking Company | Woodbury | GA | 11297 | United Bank | August 19, 2016 | November 17, 2016 |
2 | First CornerStone Bank | King of Prussia | PA | 35312 | First-Citizens Bank & Trust Company | May 6, 2016 | September 6, 2016 |
3 | Trust Company Bank | Memphis | TN | 9956 | The Bank of Fayette County | April 29, 2016 | September 6, 2016 |
4 | North Milwaukee State Bank | Milwaukee | WI | 20364 | First-Citizens Bank & Trust Company | March 11, 2016 | June 16, 2016 |
在这里我们可以继续进行一些数据清理和分析(后面还会深入学习数据清理和分析),例如按年份计算银行倒闭的数量,按结束日期(Closing Date)中的年份统计:
import sys
import pandas as pd
#从html文件中抓取表格对象,如果有多个表格那么tables会是一个DataFrame 对象的列表
tables = pd.read_html("examples/fdic_failed_bank_list.html")
#取列表的第一个DF对象
failures = tables[0]
#先通过pandas的to_datetime函数将Closing Date转为日期格式,然后取year统计
close_timestamps = pd.to_datetime(failures["Closing Date"])
count = close_timestamps.dt.year.value_counts()
print(count)
输出:
Closing Date
2010 157
2009 140
2011 92
2012 51
2008 25
2013 24
2014 18
2002 11
2015 8
2016 5
2004 4
2001 4
2007 3
2003 3
2000 2
Name: count, dtype: int64
使用 lxml.objectify 解析 XML
XML 是另一种常见的结构化数据格式,支持具有元数据的分层嵌套数据。前面,我学习了 pandas.read_html 函数,该函数在后台使用 lxml 或 Beautiful Soup 库来解析 HTML 中的数据。XML 和 HTML 在结构上相似,但 XML 更通用。下面我将再学习一个例子,说明如何使用 lxml 从 XML 格式解析数据。
示例中用到的数据是,多年来纽约大都会交通管理局 (MTA) 以 XML 格式发布的有关公共汽车和火车服务的数据系列。其中包含一系列 XML 记录形式的月度数据,如下所示 :
<PERFORMANCE>
<INDICATOR>
<INDICATOR_SEQ>28445</INDICATOR_SEQ>
<PARENT_SEQ></PARENT_SEQ>
<AGENCY_NAME>Metro-North Railroad</AGENCY_NAME>
<INDICATOR_NAME>On-Time Performance (West of Hudson)</INDICATOR_NAME>
<DESCRIPTION>Percent of commuter trains that arrive at their destinations within 5 minutes and 59 seconds of the scheduled time. West of Hudson services include the Pascack Valley and Port Jervis lines. Metro-North Railroad contracts with New Jersey Transit to operate service on these lines.
</DESCRIPTION>
<PERIOD_YEAR>2008</PERIOD_YEAR>
<PERIOD_MONTH>1</PERIOD_MONTH>
<CATEGORY>Service Indicators</CATEGORY>
<FREQUENCY>M</FREQUENCY>
<DESIRED_CHANGE>U</DESIRED_CHANGE>
<INDICATOR_UNIT>%</INDICATOR_UNIT>
<DECIMAL_PLACES>1</DECIMAL_PLACES>
<YTD_TARGET>95.00</YTD_TARGET>
<YTD_ACTUAL>96.90</YTD_ACTUAL>
<MONTHLY_TARGET>95.00</MONTHLY_TARGET>
<MONTHLY_ACTUAL>96.90</MONTHLY_ACTUAL>
</INDICATOR>
<INDICATOR>
......
</INDICATOR>
......
<INDICATOR>
......
</INDICATOR>
<PERFORMANCE>
这个我的存储位置是datasets/mta_perf/Performance_MNR.xml。我们使用 lxml.objectify,我们解析文件并使用 getroot 获取对 XML 文件的根节点的引用(请看代码注释)。
import pandas as pd
from lxml import objectify
path = "datasets/mta_perf/Performance_MNR.xml"
with open(path) as f:
parsed = objectify.parse(f)
root = parsed.getroot()
data = []
#设置不需要处理的子节点
skip_fields = ["PARENT_SEQ", "INDICATOR_SEQ",
"DESIRED_CHANGE", "DECIMAL_PLACES"]
#外层循环迭代每个INDICATOR
for elt in root.INDICATOR:
#定义一个空字典,由于存储子节点和子节点内容
el_data = {}
#内层循环迭代INDICATOR节点下的每个子几点,并将节点名作为key,节点内容作为值。
for child in elt.getchildren():
if child.tag in skip_fields:
continue
el_data[child.tag] = child.pyval
#将读取完的一个INDICATOR节点数据放到data列表中。
data.append(el_data)
#用字典列表data构造一个DataFrame对象
perf = pd.DataFrame(data)
#输出前5行
print(perf.head())
打印前5行数据在控制台:
AGENCY_NAME INDICATOR_NAME ... MONTHLY_TARGET MONTHLY_ACTUAL
0 Metro-North Railroad On-Time Performance (West of Hudson) ... 95.0 96.9
1 Metro-North Railroad On-Time Performance (West of Hudson) ... 95.0 95.0
2 Metro-North Railroad On-Time Performance (West of Hudson) ... 95.0 96.9
3 Metro-North Railroad On-Time Performance (West of Hudson) ... 95.0 98.3
4 Metro-North Railroad On-Time Performance (West of Hudson) ... 95.0 95.8
[5 rows x 12 columns]
如果我们使用pandas 的 pandas.read_xml 函数将会极大简化上面的代码,可以用一行代码实现,如下:
import pandas as pd
perf2 = pd.read_xml(path)
print(perf2.head())
打印到控制台输出(这里没设置忽略的子节点):
INDICATOR_SEQ PARENT_SEQ AGENCY_NAME ... YTD_ACTUAL MONTHLY_TARGET MONTHLY_ACTUAL
0 28445 NaN Metro-North Railroad ... 96.90 95.00 96.90
1 28445 NaN Metro-North Railroad ... 96.00 95.00 95.00
2 28445 NaN Metro-North Railroad ... 96.30 95.00 96.90
3 28445 NaN Metro-North Railroad ... 96.80 95.00 98.30
4 28445 NaN Metro-North Railroad ... 96.60 95.00 95.80
[5 rows x 16 columns]
对于 pandas.read_xml 的更深入的了解可以查阅pandas的官方文档。