对于简单的文件,唯一的结构层次是间隔的行。然而有时需要更加结构化的文本,用于后续使用的程序保存数据或者向另一个程序传送数据。
结构化的文本有很多格式,区别他们的方法如下所示:
- 分隔符,比如tab(‘\t’),逗号(‘,’)或者竖线(‘|’)。逗号分割值(CSV)就是这样的栗子
- ‘<‘ 和 ‘>’ 标签,例如 XML 和 HTML。
- 标点符号,例如 JavaScript Object Notation(JSON)
- 缩进,例如 YAML(即 YAML Ain’t Markup Language 的缩写)
- 混合的,例如各种配置文件
每一种结构化文件格式都能够至少被一种Python模块读写。
CSV:Comma-Separated Values
带分隔符的文件一般用作数据交换格式或者数据库。我们可以人工读入CSV文件,每一次读取一行,在逗号分隔符处将每行分开,并添加结果到数据结构中,例如列表或者字典。但是,最好使用标准的csv模块,因为这样切分会得到更加复杂的信息。
- 除了逗号,还有其他可代替的分隔符:’|’ 和 ‘\t’ 很常见。
- 有些数据会有转义字符序列,如果分隔符出现在一块区域内,则整块都要加上引号或者 在它之前加上转义字符。
- 文件可能有不同的换行符,Unix 系统的文件使用 ‘\n’,Microsoft 使用 ‘\r\n’,Apple 之前使用 ‘\r’ 而现在使用 ‘\n’。
- 在第一行可以加上列名
首先读和写一个列表的行,每一行有很多列:
In [1]: import csv
In [2]: villains = [
...: ['Doctor', 'No'],
...: ['Rosa', 'Klebb'],
...: ['Mister', 'Big'],
...: ['Auric', 'Goldfinger'],
...: ['Ernst', 'Blofeld'],
...: ]
In [3]: with open('villains','wt') as fout:
...: csvout = csv.writer(fout)
...: csvout.writerows(villains)
...:
于是创建了包含以下几行的文件:
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
现在,我们重新读这个文件:
In [4]: import csv
In [5]: with open('villains','rt') as fin:
...: cin = csv.reader(fin)
...: villains = [row for row in cin]
...:
In [6]: print(villains)
[['Doctor', 'No'], ['Rosa', 'Klebb'], ['Mister', 'Big'], ['Auric', 'Goldfinger'], ['Ernst', 'Blofeld']]
我们利用函数reader()创建的结构,它在通过for循环提取到的cin对象中创建每一行。
使用 reader() 和 writer() 的默认操作。每一列用逗号分开;每一行用换行符分开。
数据可以是字典的集合(a list of dictionary),不仅仅是列表的集合(a list of list)。这次使 用新函数 DictReader() 读取文件 villains,并且指定每一列的名字:
In [7]: import csv
In [8]: with open('villains','rt') as fin:
...: cin = csv.DictReader(fin,fieldnames=['first','last'])
...: villains = [row for row in cin]
...:
In [9]: print(villains)
[{'last': 'No', 'first': 'Doctor'}, {'last': 'Klebb', 'first': 'Rosa'}, {'last': 'Big', 'first': 'Mister'}, {'last': 'Goldfinger', 'first': 'Auric'}, {'last': 'Blofeld', 'first': 'Ernst'}]
下面使用新函数DictWriter()重写CSV文件,同事调用writeheader()向CSV文件中第一行写入每一列的名字:
In [10]: import csv
In [11]: villains
Out[11]:
[{'first': 'Doctor', 'last': 'No'},
{'first': 'Rosa', 'last': 'Klebb'},
{'first': 'Mister', 'last': 'Big'},
{'first': 'Auric', 'last': 'Goldfinger'},
{'first': 'Ernst', 'last': 'Blofeld'}]
In [13]: with open('villains','wt') as fout:
...: cout = csv.DictWriter(fout,['first','last'])
...: cout.writeheader()
...: cout.writerows(villains)
...:
于是创建了具有标题行的新文件villains:
first,last
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
回过来再读取写入的文件,忽略函数 DictReader() 调用的参数 fieldnames,把第一行的值 (first,last)作为列标签,和字典的键做匹配:
In [14]: import csv
In [15]: with open('villains','rt') as fin:
...: cin = csv.DictReader(fin)
...: villains = [row for row in cin]
...:
In [16]: print(villains)
[{'last': 'No', 'first': 'Doctor'}, {'last': 'Klebb', 'first': 'Rosa'}, {'last': 'Big', 'first': 'Mister'}, {'last': 'Goldfinger', 'first': 'Auric'}, {'last': 'Blofeld', 'first': 'Ernst'}]
XML
带分隔符的文件仅有二维的数据:行和列。如果你想在程序之间交换数据结构,需要一种 方法把层次结构、序列、集合和其他的结构编码成文本。
XML 是最突出的处理这种转换的标记(markup)格式,它使用标签(tag)分隔数据,如 下面的示例文件 menu.xml 所示:
<?xml version="1.0"?>
<menu>
<breakfast hours="7-11">
<item price="$6.00">breakfast burritos</item>
<item price="$4.00">pancakes</item>
</breakfast>
<lunch hours="11-3">
<item price="$5.00">hamburger</item>
</lunch>
<dinner hours="3-10">
<item price="8.00">spaghetti</item>
</dinner>
</menu>
下面是XML的一些重要特性:
- 标签以一个 < 字符开头,例如示例中的标签 menu、breakfast、lunch、dinner 和 item;
- 忽略空格;
- 通常一个开始标签(例如 <menu>)跟一段其他的内容,然后是最后相匹配的结束标签, 例如 </menu>;
- 标签之间是可以存在多级嵌套的,在本例中,标签 item 是标签 breakfast、lunch 和 dinner 的子标签,反过来,它们也是标签 menu 的子标签;
- 可选属性(attribute)可以出现在开始标签里,例如 price 是 item 的一个属性;
- 标签中可以包含值(value),本例中每个 item 都会有一个值,比如第二个 breakfast item 的 pancakes;
- 如果一个命名为 thing 的标签没有内容或者子标签,它可以用一个在右尖括号的前面添 加斜杠的简单标签所表示,例如 <thing/> 代替开始和结束都存在的标签 <thing> 和 </ thing>;
- 存放数据的位置可以是任意的——属性、值或者子标签。例如也可以把最后一个 item 标签写作 <item price =”$8.00″ food =”spaghetti”/>。
XML通常用于数据传送和消息,XML的灵活性导致出现了很多方法和性能各异的Python库。
在Python中解析XML最简单的方法是使用ElementTree,下面的代码用来解析menu.xml文件以及输出一些标签和属性:
import xml.etree.ElementTree as et
tree = et.ElementTree(file='menu.xml')
root = tree.getroot()
print(root.tag)
for child in root:
print('tag:',child.tag,'attributes:',child.attrib)
for grandchild in child:
print('\ttag:',grandchild.tag,'attributes:',grandchild.attrib)
print(len(root)) # 菜单选择的数目
3
print(root[0]) # 早餐项的数目
2
结果:
menu
tag: breakfast attributes: {'hours': '7-11'}
tag: item attributes: {'price': '$6.00'}
tag: item attributes: {'price': '$4.00'}
tag: lunch attributes: {'hours': '11-3'}
tag: item attributes: {'price': '$5.00'}
tag: dinner attributes: {'hours': '3-10'}
tag: item attributes: {'price': '8.00'}
对于嵌套列表中的每一个元素,tag 是标签字符串,attrib 是它属性的一个字典。 ElementTree 有许多查找 XML 导出数据、修改数据乃至写入 XML 文件的方法,它的文档 :https://docs.python.org/3.3/library/xml.etree.elementtree.html
其他标准的 Python XML 库如下。
- xml.dom JavaScript 开发者比较熟悉的文档对象模型(DOM)将 Web 文档表示成层次结构,它 会把整个 XML 文件载入到内存中,同样允许你获取所有的内容。
- xml.sax 简单的XML API或者SAX都是通过在线解析XML,不需要一次载入所有内容到内存中, 因此对于处理巨大的 XML 文件流是一个很好的选择。
JSON
JavaScript Object Notation(JSON,http://www.json.org)是源于 JavaScript 的当今很流行的 数据交换格式,它是 JavaScript 语言的一个子集,也是 Python 合法可支持的语法。对于 Python 的兼容性使得它成为程序间数据交换的较好选择。
不同于众多的XML模块,Python只有一个主要的JSON模块(json)。下面的程序将数据编码成JSON字符串,然后再把JSON字符串解码成数据。用上面XML的数据构建JSON的数据结构:
menu = \
{
"breakfast": {
"hours": "7-11",
"items": {
"breakfast burritos": "$6.00",
"pancakes": "$4.00"
}
},
"lunch" : {
"hours": "11-3",
"items": {
"hamburger": "$5.00"
}
},
"dinner": {
"hours": "3-10",
"items": {
"spaghetti": "$8.00"
}
}
}
接下来使用dumps()将menu编码成JSON字符串(menu_json):
import json
menu_json = json.dumps(menu)
print(menu_json)
{"lunch": {"hours": "11-3", "items": {"hamburger": "$5.00"}}, "dinner": {"hours": "3-10", "items": {"spaghetti": "$8.00"}}, "breakfast": {"hours": "7-11", "items": {"pancakes": "$4.00", "breakfast burritos": "$6.00"}}}
然后使用loads()把JSON字符串menu_json解析成Python的数据结构:
menu2 = json.loads(menu_json)
print(menu2)
{'lunch': {'hours': '11-3', 'items': {'hamburger': '$5.00'}}, 'dinner': {'hours': '3-10', 'items': {'spaghetti': '$8.00'}}, 'breakfast': {'hours': '7-11', 'items': {'pancakes': '$4.00', 'breakfast burritos': '$6.00'}}}
menu 和 menu2 是具有相同键值的字典,和标准的字典用法一样,得到的键的顺序是不尽相 同的。
我们可能会在编码或解析JSON对象时得到异常,包括对象的时间datetime:
In [1]: import datetime
In [2]: now = datetime.datetime.utcnow()
In [3]: now
Out[3]: datetime.datetime(2017, 1, 17, 1, 42, 57, 521828)
In [4]: import json
In [5]: json.dumps(now)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
#......(里面都是报错)
TypeError: datetime.datetime(2017, 1, 17, 1, 42, 57, 521828) is not JSON serializable
上述错误发生是因为标准JSON没有定义日期或时间类型,需要自定义处理方式。我们可以把datetime转换成JSON能够理解的类型,比如字符串或者epoch值。
In [6]: now_str = str(now)
In [7]: json.dumps(now_str)
Out[7]: '"2017-01-17 01:42:57.521828"'
In [8]: from time import mktime
In [9]: noe_epoch = int(mktime(now.timetuple()))
In [10]: json.dumps(noe_epoch)
Out[10]: '1484588577'
如果datetime值出现在正常转换后的数据中间,那么做上面的特殊转化是困难的。我们可以通过继承修改JSON的编码方式。下面为datetime修改编码方式:
In [12]: class DTEncoder(json.JSONEncoder):
...: def default(self,obj):
#isinstance()检查obj的类型
...: if isinstance(obj,datetime.datetime):
...: return int(mktime(obj.timetuple()))
#否则是普通解码器知道的东西
...: return json.JSONEncoder.default(self,obj)
...:
In [13]: json.dumps(now,cls=DTEncoder)
Out[13]: '1484588577'
新类 DTEncoder 是 JSONEncoder 的一个子类。我们需要重载它的 default() 方法来增加处理 datetime 的代码。继承确保了剩下的功能与父类的一致性。
函数 isinstance() 检查 obj 是否是类 datetime.datetime 的对象,因为在 Python 中一切都 是对象,isinstance() 总是适用的:
In [14]: type(now)
Out[14]: datetime.datetime
In [15]: isinstance(now,datetime.datetime)
Out[15]: True
In [16]: type(24)
Out[16]: int
In [17]: isinstance(24,int)
Out[17]: True
In [18]: type('hey')
Out[18]: str
In [19]: isinstance('hey',str)
Out[19]: True
对于 JSON 和其他结构化的文本格式,在不需要提前知道任何东西的情况下 可以从一个文件中解析数据结构。然后使用 isinstance() 和相关类型的方法 遍历数据。例如,如果其中的一项是字典结构,可以通过 keys()、values() 和 items() 抽取内容
YAML
和JSON类似,YAML同样有键和值,但主要用于处理日期和时间这样的数据类型。
。标准的 Python 库没有处理 YAML 的模块,因此需要安装第三方库 yaml操作数据。load() 将 YAML 字符串转换为 Python 数据 结构,而 dump() 正好相反。
下面是一个YAML文件:
name:
first: James
last: McIntyre
dates:
birth: 1828-05-25
death: 1906-03-31
details:
bearded: true
themes: [cheese, Canada]
books:
url: http://www.gutenberg.org/files/36068/36068-h/36068-h.htm
poems:
- title: 'Motto'
text: |
Politeness, perseverance and pluck,
To their possessor will bring good luck.
- title: 'Canadian Charms'
text: |
Here industry is not in vain,
For we have bounteous crops of grain,
And you behold on every field
Of grass and roots abundant yield,
But after all the greatest charm
Is the snug home upon the farm,
And stone walls now keep cattle warm.
类似余true,false,on和off的值可以转换为Python的布尔值。证书和字符串转换为Python等价的。其他语法创建列表和字典。
import yaml
with open('mcintyre.yaml', 'rt') as fin:
text = fin.read()
data = yaml.load(text)
data['details']
{'themes': ['cheese', 'Canada'], 'bearded': True}
len(data['poems'])
2
创建的匹配这个YAML文件的数据结构超过了一层嵌套。如果我们想得到第二首诗歌的题目,需要使用dict/list/dict的引用:
data['poems'][1]['title']
'Canadian Charms'
PyYAML 可以从字符串中载入 Python 对象,但这样做是不安全的。如果导 入你不信任的 YAML,最好还是使用 safe_ load()。
使用pickle序列化
存储数据结构到一个文件中也称为序列化(serializing)。想JSON这样的格式需要定制的序列化数据的转换器。Python提供了pickle模块以特殊的二进制保存和恢复数据对象。
JSON在解析datetime对象时会出现问题,但是对于pickle就不会存在问题:
In [1]: import pickle
In [2]: import datetime
In [3]: now1 = datetime.datetime.utcnow()
In [4]: pickled = pickle.dumps(now1)
In [5]: pickled
Out[5]: b'\x80\x03cdatetime\ndatetime\nq\x00C\n\x07\xe1\x01\x11\x06\x11\t\x04\x86\xc7q\x01\x85q\x02Rq\x03.'
In [6]: now2 = pickle.loads(pickled)
In [7]: now1
Out[7]: datetime.datetime(2017, 1, 17, 6, 17, 9, 296647)
In [8]: now2
Out[8]: datetime.datetime(2017, 1, 17, 6, 17, 9, 296647)
pickle同样适用于自己定义的类和对象。现在,我们定义一个简单的类Tiny,当其对象强制转换为字符串时会返回’tiny’:
In [1]: import pickle
In [2]: class Tiny():
...: def __str__(self):
...: return 'tiny'
...:
In [3]: obj1 = Tiny()
In [4]: obj1
Out[4]: <__main__.Tiny at 0x7f9d1fedc6d8>
In [5]: str(obj1)
Out[5]: 'tiny'
In [6]: pickled = pickle.dumps(obj1)
In [7]: pickled
Out[7]: b'\x80\x03c__main__\nTiny\nq\x00)\x81q\x01.'
In [8]: obj2 = pickle.loads(pickled)
In [9]: obj2
Out[9]: <__main__.Tiny at 0x7f9d1fc11470>
In [10]: str(obj2)
Out[10]: 'tiny'
pickled是从对象obj1转换来的序列化二进制字符串。然后再把字符串还原成对象obj1的副本obj2。使用函数dump()序列化数据到文件,而函数load()用作反序列化。
因为 pickle 会创建 Python 对象,前面提到的安全问题也同样会发生,因此对于不信任的文件反序列化。
注:本文内容来自《Python语言及其应用》欢迎购买原书阅读