一.读写文本格式的数据
因为其简单的文件交互语法、直观的数据结构,以及诸如元组打包解包之类的便利功能。Python在文本和文件处理方面已经成为一门招人喜欢的语言。
pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。下表对它们进行了总结,其中read_csv和read_table可能是我们今后用的最多的。
函数 说明
read_excel 将excel表的数据读取为DataFrame
read_csv 从文件、URL、文件型对象中加载带分隔符的数据。默认分隔符为逗号
read_table 从文件、URL、文件型对象中加载带有分隔符的数据。默认分隔符为制表符("\t")
read_fwf 读取定宽列格式数据(也就是说,没有分隔符)
read_clipboard 读取剪贴板中的数据,可以看做read_table的剪贴板版,在将网页转换为表格时很有用
下面大致介绍一下这些函数在将文本数据转换为DataFrame时所用到的一些技术。这些函数的选项可以划分为一下几个大类:
索引:讲一个或多个列当做返回的DataFrame处理,以及是否从文件、用户获取列名
类型推断和数据转换:包括用户定义值的转换、缺失值标记列表等。
日期解析:包括组合功能,比如将分散在多个列中的日期时间信息组合成结果中的单个列。
迭代:支持对大文件进行逐块迭代。
不规整数据问题:跳过一些行、页脚、注释或其他一些不重要的东西(比如由成千上万个逗号隔开的数值数据)。
类型推断(type inference)是这些函数中最重要的功能之一,也就是说,我们不需要指定列的类型到底是数值、整数、布尔值,还是字符串。日期和其他自定义类型的处理需要多花点工夫才行。首先我们来看一个以逗号分隔的csv文本文件:
In [17]: !type test.csv
a,b,c,d,kobe
1,3,5,7,calvin
2,4,6,8,hello
9,10,11,12 ,world
由于该文件以逗号分隔,所以我们可以使用read_csv将其读入一个DataFrame:
In [18]: import pandas as pd
In [21]: df=pd.read_csv('test.csv')
In [22]: df
Out[22]:
a b c d kobe
0 1 3 5 7 calvin
1 2 4 6 8 hello
2 9 10 11 12 world
我们也可以用read_table。只不过需要制定分隔符而已:
In [23]: pd.read_table('test.csv',sep=',')
Out[23]:
a b c d kobe
0 1 3 5 7 calvin
1 2 4 6 8 hello
2 9 10 11 12 world
并不是所有文件都有标题行。看看下面这个文件:
In [24]: !type test02.csv
1,3,5,7,calvin
2,4,6,8,hello
9,10,11,12,world
读入该文件的办法有两个。我们可以让pandas为其分配默认的列名,也可以自己定义列名:
In [25]: pd.read_csv('test02.csv',header=None)
Out[25]:
0 1 2 3 4
0 1 3 5 7 calvin
1 2 4 6 8 hello
2 9 10 11 12 world
In [27]: pd.read_csv('test02.csv',names=['a','b','c','d','name'])
Out[27]:
a b c d name
0 1 3 5 7 calvin
1 2 4 6 8 hello
2 9 10 11 12 world
假如我们希望将name列做成DataFrame的索引,我们可以明确表示要将该列放到索引4的位置上,也可以通过index_col参数指定"name":
In [28]: pd.read_csv("test02.csv",names=['a','b','c','d','name'],index_col='nam
...: e')
Out[28]:
a b c d
name
calvin 1 3 5 7
hello 2 4 6 8
world 9 10 11 12
如果希望将多个列做成一个层次化索引,只需传入由列编号或列名组成的列表即可:
In [35]: !type test03.csv
key1,key2,value1,value2
one,a,1,2
one,b,3,4
one,c,5,6
one,d,7,8
two,a,9,10
two,b,11,12
two,c,13,14
two,d,15,16
In [36]: parsed=pd.read_csv('test03.csv',index_col=['key1','key2'])
In [37]: parsed
Out[37]:
value1 value2
key1 key2
one a 1 2
b 3 4
c 5 6
d 7 8
two a 9 10
b 11 12
c 13 14
d 15 16
有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其他模式)。对于这种情况,可以编写一个正则表达式来作为read_table的分隔符。看下面这个文本文件:
In [3]: import pandas as pd
In [4]: from pandas import Series,DataFrame
In [5]: import numpy as np
In [15]: list(open('test04.txt'))
Out[15]:
[' A B C\n',
'aaa -123 -2.4 -0.88\n',
'bbb -0.66 -2.88 -0.83\n',
'ccc -6.5 -2.6 -0.89\n',
'ddd -3.5 -2.9 -0.99']
该文件各个字段由数量不定的空白符分隔,虽然我们可以对其做一些手工调整,但这个情况还是处理比较好。本例的这个情况可以用正则表达式\s+表示,于是我们就有了:
In [16]: result=pd.read_table('test04.txt',sep='\s+')
In [17]: result
Out[17]:
A B C
aaa -123.00 -2.40 -0.88
bbb -0.66 -2.88 -0.83
ccc -6.50 -2.60 -0.89
ddd -3.50 -2.90 -0.99
这里,由于列名比数据行的数量少,所以read_table推断第一列应该是DataFrame的索引。
这些解析器函数还有许多参数可以帮助我们处理各种各样的异形文件格式(见下表)。比如说,我们可以用skiprows跳过文件的第一行、第三行和第四行:
In [1]: import numpy as np
In [2]: import pandas as pd
In [3]: from pandas import Series,DataFrame
In [5]: !type test05.csv
#hello !
a,b,c,d,info
#just wanted to learning something difficult
#who reads CSV files,anyway?
1,2,3,4,calvin
5,6,7,8,kobe
9,10,11,12,michale
In [6]: pd.read_csv('test05.csv',skiprows=[0,2,3])
Out[6]:
a b c d info
0 1 2 3 4 calvin
1 5 6 7 8 kobe
2 9 10 11 12 michale
缺失值处理是文件解析任务中的一个重要组成部分。缺失数据经常是要么没有(空字符串),要么用某个标记值表示。默认情况下,pandas会用一组经常出现的标记值进行识别,如NA、-1.#IND以及NULL等:
In [14]: !type test06.csv
numbers,a,b,c,d,info
one,1,2,3,4,NA
two,5,6,,8,calvin
three,9,10,11,12,kobe
In [15]: result=pd.read_csv('test06.csv')
In [16]: result
Out[16]:
numbers a b c d info
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11.0 12 kobe
In [17]: pd.isnull(result)
Out[17]:
numbers a b c d info
0 False False False False False True
1 False False False True False False
2 False False False False False False
na_values可以接受一组用于表示缺失值的字符串:
In [25]: result=pd.read_csv('test06.csv',na_values=['NULL'])
In [26]: result
Out[26]:
numbers a b c d info
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11.0 12 kobe
可以用一个字典为各列指定不同的NA标记值:
In [33]: sentinels={'info':['kobe','NA'],'numbers':['one','NA']}
In [34]: result=pd.read_csv('test06.csv',na_values=sentinels)
In [35]: result
Out[35]:
numbers a b c d info
0 NaN 1 2 3.0 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11.0 12 NaN
下表为read_csv/read_table函数的参数:
参数 说明
path 表示文件系统位置、URL/文件型对象的字符串
sep或delimiter 用于对行中各字段进行拆分的字符序列或正则表达式
header 用作列名的行号。默认为0(第一行),如果没有header行就应该设置为None
index_col 用作行索引的列编号或列名。可以是单个名称/数字或由多个名称/数字组成的列表(层次化索引)
names 用于结果的列名列表,结合header=None
skiprows 需要忽略的行数(从文件开始处算起),或需要跳过的行号列表(从0开始)
na_values 一组用于替换NA的值
comment 用于将注释信息从行尾拆分出去的字符(一个或多个)
parse_dates 尝试将数据解析为日期,默认为False.如果为True,则尝试解析所有列。此外,还可以指定需要解析的一组列号或列名。如果列表的元素为列表或元组,就会将多个列组合到一起再进行日期解析工作(例如,日期/时间分别位于两个列中)
keep_date_col 如果连接多列解析日期,则保持参与连接的列。默认为False
converters 由列号/列名跟函数之间的映射关系组成的字典。例如,{‘foo’:f}会对foo列的所有值应用函数f
dayfirst 当解析有歧义的日期时,将其看做国际格式(例如,11/27/2018->Nov 27,2018.默认为False
date_parser 用于解析日期的函数
nrows 需要读取的行数(从文件开始处算起)
iterator 返回一个TextParser以便逐块读取文件
chunksize 文件块的大小(用于迭代)
skip_footer 需要忽略的行数(从文件末尾处算起)
verbose 打印各种解析器输出信息,比如“非数值列中缺失值的数量”等
encoding 用于unicode的文本编码格式。例如,“utf-8”表示用UTF-8编码的文本
squeeze 如果数据经解析后仅含一列,则返回Series
thousands 千分位分隔符,如“,”或“.”
二.逐块读取文本文件
在处理很大的文件时,或找出大文件中的参数集以便于后续处理时,我们可能只想读取文件的一小部分或逐块对文件进行迭代。
In [38]: result=pd.read_csv('test07.csv')
In [39]: result
Out[39]:
numbers a b c d info
0 one 1 2 3 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11 12 kobe
3 numbers a b c d info
4 one 1 2 3 4 NaN
5 two 5 6 NaN 8 calvin
6 three 9 10 11 12 kobe
7 numbers a b c d info
8 one 1 2 3 4 NaN
9 two 5 6 NaN 8 calvin
10 three 9 10 11 12 kobe
11 numbers a b c d info
12 one 1 2 3 4 NaN
13 two 5 6 NaN 8 calvin
14 three 9 10 11 12 kobe
如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:
In [40]: pd.read_csv('test07.csv',nrows=5)
Out[40]:
numbers a b c d info
0 one 1 2 3 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11 12 kobe
3 numbers a b c d info
4 one 1 2 3 4 NaN
要逐块读取文件,需要设置chunksize(行数):
In [41]: chunker=pd.read_csv('test07.csv',chunksize=10)
In [42]: chunker
Out[42]: <pandas.io.parsers.TextFileReader at 0x6905278>
read_csv所返回的这个TextParser对象使我们可以根据chunksize对文件进行逐块迭代:
In [46]: for i in chunker:
...: print(i)
...:
numbers a b c d info
10 three 9 10 11 12 kobe
11 numbers a b c d info
12 one 1 2 3 4 NaN
13 two 5 6 NaN 8 calvin
14 three 9 10 11 12 kobe
TextParser还有一个get_chunk方法,它使我们可以读取任意大小的快。
In [47]: reader=pd.read_csv('test07.csv',iterator=True)
In [48]: reader.get_chunk(5)
Out[48]:
numbers a b c d info
0 one 1 2 3 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11 12 kobe
3 numbers a b c d info
4 one 1 2 3 4 NaN
三.将数据写出到文本格式
数据也可以被输出为分隔符格式的文本。我们再来看看之前读过的一个CSV文件:
In [51]: data=pd.read_csv('test06.csv')
In [52]: data
Out[52]:
numbers a b c d info
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 calvin
2 three 9 10 11.0 12 kobe
利用DataFrame的to_csv方法,我们可以将数据写到一个以逗号分隔的文件中:
In [53]: data.to_csv('out.csv')
In [54]: !type out.csv
,numbers,a,b,c,d,info
0,one,1,2,3.0,4,
1,two,5,6,,8,calvin
2,three,9,10,11.0,12,kobe
In [58]: data.to_csv('out.csv',sep='|')
In [59]: !type out.csv
|numbers|a|b|c|d|info
0|one|1|2|3.0|4|
1|two|5|6||8|calvin
2|three|9|10|11.0|12|kobe
缺失值在输出结果中会被表示为空字符串。我们可能希望将其表示为别的标记值:
In [60]: data.to_csv('out.csv',na_rep='NULL')
In [61]: !type out.csv
,numbers,a,b,c,d,info
0,one,1,2,3.0,4,NULL
1,two,5,6,NULL,8,calvin
2,three,9,10,11.0,12,kobe
如果没有设置其他选项,则会写出行和列的标签。当然,它们也都可以被禁用:
In [62]: data.to_csv('out.csv',index=False,header=False)
In [63]: !type out.csv
one,1,2,3.0,4,
two,5,6,,8,calvin
three,9,10,11.0,12,kobe
此外,我们可以只写出一部分的列,并以我们指定的顺序排列:
In [72]: data.to_csv('out.csv',index=False,columns=['a','b'])
In [73]: !type out.csv
a,b
1,2
5,6
9,10
Series也有一个to_csv方法:
In [74]: dates=pd.date_range('11/27/2018',periods=7)
In [75]: ts=Series(np.arange(7),index=dates)
In [76]: ts.to_csv('tseries.csv')
In [77]: !type tseries.csv
2018-11-27,0
2018-11-28,1
2018-11-29,2
2018-11-30,3
2018-12-01,4
2018-12-02,5
2018-12-03,6
只需一点整理工作(无header行,第一列作索引)就能用read_csv将csv文件读取为Series,
In [81]: pd.read_csv('tseries.csv',parse_dates=True)
Out[81]:
2018-11-27 0
0 2018-11-28 1
1 2018-11-29 2
2 2018-11-30 3
3 2018-12-01 4
4 2018-12-02 5
5 2018-12-03 6
四.手工处理分隔符格式
大部分存储在磁盘上的表格型数据都能用pandas.read_table进行加载。然而,有时还是需要做一些手工处理。由于接收到含有畸形行的文件而使read_table出毛病的情况并不少见。为了说明这些基本工具,看看下面这个简单的csv文件:
In [82]: !type test08.csv
"a","b","c"
"3","4","5"
"6","7","8","9"
对于任何单字符分隔符文件,可以直接使用Python内置的csv模块。将任意已打开的文件或文件型的对象传给csv.reader:
In [83]: import csv
In [84]: f=open('test08.csv')
In [85]: reader=csv.reader(f)
对这个reader进行迭代将会为每行产生一个列表(并移除了所有的引号):
In [87]: for line in reader:
...: print (line)
...:
['a', 'b', 'c']
['3', '4', '5']
['6', '7', '8', '9']
现在,为了使数据格式合乎要求,我们需要对其做一些整理工作:
In [88]: lines=list(csv.reader(open('test08.csv')))
In [89]: header,values=lines[0],lines[1:]
In [90]: data_dict={h:v for h,v in zip(header,zip(*values))}
In [91]: data_dict
Out[91]: {'a': ('3', '6'), 'b': ('4', '7'), 'c': ('5', '8')}
CSV文件的形式有很多。只需定义csv.Dialect的一个子类即可定义出新格式(如专门的分隔符、字符串引用约定、行结束符等):
In [10]: class my_dialect(csv.Dialect):
...: lineterminator='\n'
...: delimiter=';'
...: quotechar='"'
...: quoting=csv.QUOTE_MINIMAL
reader=csv.reader(f,dialect=my_dialect)
各个CSV语支的参数也可以关键字的形式提供给csv.reader,儿无需定义子类
reader=csv.reader(f,delimiter='|')
可用的选项(csv.Dialect的属性)及其功能如下表所示
下表为CSV语支选项
参数 说明
delimiter 用于分隔字段的单字符字符串。默认为“,”
lineterminator 用于写操作的行结束符,默认为“\r\n”.读操作将忽略此选项,它能认出跨平台行结束符
quotechar 用于带有特殊字符(如分隔符)的字段的引用符号。默认为““””
quoting 引用约定。可选值包括csv.QUOTE_ALL(引用所有字段)、csv.QUOTE_MINIMAL(只引用带有诸如分隔符之类特殊字符的字段)、csv.QUOTE_NONNUMBERIC以及csv.QUOTE_NON(不引用)。默认为QUOTE_MINIMAL
skipinitialspace 忽略分隔符后面的空白符。默认为False
doublequote 如何处理字段内的引用符号。如果为True,则双写。完整信息及行为可以参见在线文档
escaperchar 用于对分隔符进行转义的字符串(如果quoting被设置为csv.QUOTE_NONE的话)。默认禁用
要手工输出分隔符文件,我们可以使用csv.writer。它接收一个一打开且可写的文件对象以及跟csv.reader相同的那些语支盒格式化选项:
In [11]: with open ('test001.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'))
五.JSON数据
JSON(JavaScript Object Notation的简称)已经成为通过HTTP请求在Web浏览器和其他应用程序之间发送数据的标准格式之一。它是一种比表格型文本格式(如CSV)灵活得多的数据格式。下面是一个例子:
In [12]: obj="""
...: {"country":"China",
...: "places_lived":["Xiantao","Shenzhen","Wuhan"],
...: "pet":null,
...: "siblings":[{"name":"Scott","age":25,"pet":"Zuko"},
...: {"name":"Kate","age":20,"pet":"Jackie"}]
...: }
...: """
除其空值null和一些其他的细微差别(如列表末尾不允许存在多余的逗号)之外,JSON非常接近于有效的Python代码。基本类型有对象(字典)、数组(列表)、字符串、数值、布尔值以及null。对象中所有的键都必须是字符串。许多Python库都可以读写JSON数据。我们将使用json,因为它是构建于Python标准库中的,通过json.loads即可将json字符串转换成Python形式:
In [13]: import json
In [17]: result=json.loads(obj)
In [18]: result
Out[18]:
{'country': 'China',
'places_lived': ['Xiantao', 'Shenzhen', 'Wuhan'],
'pet': None,
'siblings': [{'name': 'Scott', 'age': 25, 'pet': 'Zuko'},
{'name': 'Kate', 'age': 20, 'pet': 'Jackie'}]}
相反,json.dumps则将Python对象转换成JSON格式:
In [19]: asjson=json.dumps(result)
In [20]: asjson
Out[20]: '{"country": "China", "places_lived": ["Xiantao", "Shenzhen", "Wuhan"],
"pet": null, "siblings": [{"name": "Scott", "age": 25, "pet": "Zuko"}, {"name":
"Kate", "age": 20, "pet": "Jackie"}]}'
如何将(一个或一组)JSON对象转换为DataFrame或其他便于分析的数据结构就由我们决定了,最简便的方式是:向DataFrame构造器传入一组JSON对象,并选取数据字段的子集。
In [22]: from pandas import DataFrame
In [23]: siblings=DataFrame(result['siblings'],columns=['name','age','pet'])
In [24]: siblings
Out[24]:
name age pet
0 Scott 25 Zuko
1 Kate 20 Jackie
下面看下DataFrame数据结构转换为JSON数据格式和以及转换之后读取JSON数据
In [29]: import numpy as np
In [30]: si = DataFrame(np.zeros((4, 4)), columns=list(range(4)),index=[str(i)
...: for i in range(4)])
In [31]: si
Out[31]:
0 1 2 3
0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0
In [32]: si.index
Out[32]: Index(['0', '1', '2', '3'], dtype='object')
In [33]: si.columns
Out[33]: Int64Index([0, 1, 2, 3], dtype='int64')
In [32]: si.index
Out[32]: Index(['0', '1', '2', '3'], dtype='object')
In [33]: si.columns
Out[33]: Int64Index([0, 1, 2, 3], dtype='int64')
In [34]: si_json=si.to_json()
In [35]: import pandas as pd
In [36]: sij=pd.read_json(si_json,convert_axes=False) #不须将轴的类型转换为合适的类型
In [37]: sij
Out[37]:
0 1 2 3
0 0 0 0 0
1 0 0 0 0
2 0 0 0 0
3 0 0 0 0
In [38]: sij.index
Out[38]: Index(['0', '1', '2', '3'], dtype='object')
In [39]: sij.columns
Out[39]: Index(['0', '1', '2', '3'], dtype='object')
六.XML和HTML:Web信息收集
Python有许多可以读写HTML和XML格式数据的库。lxml(python3需要单独安装)就是其中之一,它能够高效且可靠地解析大文件。lxml有多个编程接口。首先我要用lxml.html处理HTML,然后再用lxml.objectify做一些XML处理。
许多网站都将数据放到HTML表格中以便在浏览器中查看,但不能以一种更易于机器阅读的格式(如JSON、HTML或XML)进行下载。
首先,找到你希望获取数据的URL,利用urllib.request将其打开,然后用lxml解析得到的数据流,如下所示:
In [24]: from urllib import request
In [25]: url="http://www.hao123.com/nba"
In [26]: wp=request.urlopen(url) #打开链接
In [27]: content=wp.read() #获取页面内容
#利用lxml.objectify解析XML
XML(Extensible Markup Language)是另一种常见的支持分层、嵌套数据以及元数据的结构化数据格式。本书所使用的这些文件实际上来自于一个很大的XML文档。下面介绍另一个用于操作XML数据的接口,即lxml.objectify。
我们先用lxml.objectify解析文件,然后通过getroot得到该XML文件的根节点的引用:
test002.xml的内容如下:
<INDICATOR>
<INDICATOR_SEQ>373889</INDICATOR_SEQ>
<PARENT_SEQ></PARENT_SEQ>
<AGENCY_NAME>Metro-nORTH Railroad</AGENCY_NAME>
<DESCRIPTION>Percent of the time that escalator are operational systemwide.The availability rate is based on physical observations performed the morning of regular business days only,This is a new indicator the agency began reporting in 2009.</DESCRIPTION>
<PERIOD_YEAR>2018</PERIOD_YEAR>
<PERIOD_MONTH>12</PERIOD_MONTH>
<CATEGORY>Service Indicator</CATEGORY>
<FREQUENCY>M</FREQUENCY>
<DESIRED_CHANGE>U</DESIRED_CHANGE>
<INDICATOR_UNIT>%</INDICATOR_UNIT>
<DECIMAL_PLACES>1</DECIMAL_PLACES>
<YTD_TARGET>97.00</YTD_TARGET>
<YTD_ACTUAL></YTD_ACTUAL>
<MONTHLY_TARGET>97.00</MONTHLY_TARGET>
<MONTHLY_ACTUAL></MONTHLY_ACTUAL>
</INDICATOR>
In [6]: from lxml import objectify
In [7]: path='test002.xml'
In [8]: parsed=objectify.parse(open(path))
In [9]: root=parsed.getroot()
对于每条记录,我们可以用标记吗(如YTD_ACTUAL)和数据值填充一个字典(排除几个标记):
In [17]:data=[]
In [18]:skip_fields=['PARENT_SEQ','INDICATOR_SEQ','DESIRED_CHANGE','DECIMAL_PL
ACES']
In [19]:for elt in root:
el_data={}
for cd in elt.getchildren():
if cd.tag in skip_fields:
continue
el_data[cd.tag]=cd.pyval
data.append(el_data)
最后将这组字典转换为一个DataFrame:
In [23]: from pandas import DataFrame
In [24]: perf=DataFrame(data)
In [25]: perf
Out[25]:
AGENCY_NAME CATEGORY ... YTD_ACTUAL YTD_TARGET
0 Metro-nORTH Railroad Service Indicator ................... 97.0
1 Metro-nORTH Railroad Service Indicator ... ............... 97.0
[2 rows x 11 columns]
XML数据可以比本例复杂得多。每个标记都可以有元数据。看看下面的这个HTML的链接标记(它也算是一段有效的XML):
In [2]: from io import StringIO
In [3]: tag='<a href="http://www.baidu.com">Baidu</a>'
In [5]: from lxml import objectify
In [6]: root=objectify.parse(StringIO(tag)).getroot()
现在就可以访问链接文本或标记中的任何字段了(如href):
In [7]: root
Out[7]: <Element a at 0x4c93348>
In [8]: root.get('href')
Out[8]: 'http://www.baidu.com'
In [9]: root.text
Out[9]: 'Baidu'