這一章主要討論使用 Python 處理各種不同方式編碼的數(shù)據(jù),比如 CSV 文件,JSON,XML 和二進(jìn)制包裝記錄。 和數(shù)據(jù)結(jié)構(gòu)那一章不同的是,這章不會(huì)討論特殊的算法問(wèn)題,而是關(guān)注于怎樣獲取和存儲(chǔ)這些格式的數(shù)據(jù)。
你想讀寫(xiě)一個(gè) CSV 格式的文件。
對(duì)于大多數(shù)的 CSV 格式的數(shù)據(jù)讀寫(xiě)問(wèn)題,都可以使用 csv
庫(kù)。 例如:假設(shè)你在一個(gè)名叫 stocks.csv 文件中有一些股票市場(chǎng)數(shù)據(jù),就像這樣:
Symbol,Price,Date,Time,Change,Volume
"AA",39.48,"6/11/2007","9:36am",-0.18,181800
"AIG",71.38,"6/11/2007","9:36am",-0.15,195500
"AXP",62.58,"6/11/2007","9:36am",-0.46,935000
"BA",98.31,"6/11/2007","9:36am",+0.12,104800
"C",53.08,"6/11/2007","9:36am",-0.25,360900
"CAT",78.29,"6/11/2007","9:36am",-0.23,225400
下面向你展示如何將這些數(shù)據(jù)讀取為一個(gè)元組的序列:
import csv
with open('stocks.csv') as f:
f_csv = csv.reader(f)
headers = next(f_csv)
for row in f_csv:
# Process row
...
在上面的代碼中,row
會(huì)是一個(gè)元組。因此,為了訪問(wèn)某個(gè)字段,你需要使用下標(biāo),如 row[0]
訪問(wèn) Symbol, row[4]
訪問(wèn) Change。
由于這種下標(biāo)訪問(wèn)通常會(huì)引起混淆,你可以考慮使用命名元組。例如:
from collections import namedtuple
with open('stock.csv') as f:
f_csv = csv.reader(f)
headings = next(f_csv)
Row = namedtuple('Row', headings)
for r in f_csv:
row = Row(*r)
# Process row
...
它允許你使用列名如 row.Symbol
和row.Change
代替下標(biāo)訪問(wèn)。 需要注意的是這個(gè)只有在列名是合法的 Python 標(biāo)識(shí)符的時(shí)候才生效。如果不是的話, 你可能需要修改下原始的列名(如將非標(biāo)識(shí)符字符替換成下劃線之類(lèi)的)。
另外一個(gè)選擇就是將數(shù)據(jù)讀取到一個(gè)字典序列中去??梢赃@樣做:
import csv
with open('stocks.csv') as f:
f_csv = csv.DictReader(f)
for row in f_csv:
# process row
...
在這個(gè)版本中,你可以使用列名去訪問(wèn)每一行的數(shù)據(jù)了。比如,row['Symbol']
或者 row['Change']
。
為了寫(xiě)入 CSV 數(shù)據(jù),你仍然可以使用 csv 模塊,不過(guò)這時(shí)候先創(chuàng)建一個(gè) writer
對(duì)象。例如:
headers = ['Symbol','Price','Date','Time','Change','Volume']
rows = [('AA', 39.48, '6/11/2007', '9:36am', -0.18, 181800),
('AIG', 71.38, '6/11/2007', '9:36am', -0.15, 195500),
('AXP', 62.58, '6/11/2007', '9:36am', -0.46, 935000),
]
with open('stocks.csv','w') as f:
f_csv = csv.writer(f)
f_csv.writerow(headers)
f_csv.writerows(rows)
如果你有一個(gè)字典序列的數(shù)據(jù),可以像這樣做:
headers = ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume']
rows = [{'Symbol':'AA', 'Price':39.48, 'Date':'6/11/2007',
'Time':'9:36am', 'Change':-0.18, 'Volume':181800},
{'Symbol':'AIG', 'Price': 71.38, 'Date':'6/11/2007',
'Time':'9:36am', 'Change':-0.15, 'Volume': 195500},
{'Symbol':'AXP', 'Price': 62.58, 'Date':'6/11/2007',
'Time':'9:36am', 'Change':-0.46, 'Volume': 935000},
]
with open('stocks.csv','w') as f:
f_csv = csv.DictWriter(f, headers)
f_csv.writeheader()
f_csv.writerows(rows)
你應(yīng)該總是優(yōu)先選擇 csv 模塊分割或解析 CSV 數(shù)據(jù)。例如,你可能會(huì)像編寫(xiě)類(lèi)似下面這樣的代碼:
with open('stocks.csv') as f:
for line in f:
row = line.split(',')
# process row
...
使用這種方式的一個(gè)缺點(diǎn)就是你仍然需要去處理一些棘手的細(xì)節(jié)問(wèn)題。 比如,如果某些字段值被引號(hào)包圍,你不得不去除這些引號(hào)。 另外,如果一個(gè)被引號(hào)包圍的字段碰巧含有一個(gè)逗號(hào),那么程序就會(huì)因?yàn)楫a(chǎn)生一個(gè)錯(cuò)誤大小的行而出錯(cuò)。
默認(rèn)情況下,csv
庫(kù)可識(shí)別 Microsoft Excel 所使用的 CSV 編碼規(guī)則。 這或許也是最常見(jiàn)的形式,并且也會(huì)給你帶來(lái)最好的兼容性。 然而,如果你查看csv
的文檔,就會(huì)發(fā)現(xiàn)有很多種方法將它應(yīng)用到其他編碼格式上(如修改分割字符等)。 例如,如果你想讀取以 tab 分割的數(shù)據(jù),可以這樣做:
# Example of reading tab-separated values
with open('stock.tsv') as f:
f_tsv = csv.reader(f, delimiter='\t')
for row in f_tsv:
# Process row
...
如果你正在讀取 CSV 數(shù)據(jù)并將它們轉(zhuǎn)換為命名元組,需要注意對(duì)列名進(jìn)行合法性認(rèn)證。 例如,一個(gè) CSV 格式文件有一個(gè)包含非法標(biāo)識(shí)符的列頭行,類(lèi)似下面這樣:
這樣最終會(huì)導(dǎo)致在創(chuàng)建一個(gè)命名元組時(shí)產(chǎn)生一個(gè) ValueError
異常而失敗。 為了解決這問(wèn)題,你可能不得不先去修正列標(biāo)題。 例如,可以像下面這樣在非法標(biāo)識(shí)符上使用一個(gè)正則表達(dá)式替換:
import re
with open('stock.csv') as f:
f_csv = csv.reader(f)
headers = [ re.sub('[^a-zA-Z_]', '_', h) for h in next(f_csv) ]
Row = namedtuple('Row', headers)
for r in f_csv:
row = Row(*r)
# Process row
...
還有重要的一點(diǎn)需要強(qiáng)調(diào)的是,csv 產(chǎn)生的數(shù)據(jù)都是字符串類(lèi)型的,它不會(huì)做任何其他類(lèi)型的轉(zhuǎn)換。 如果你需要做這樣的類(lèi)型轉(zhuǎn)換,你必須自己手動(dòng)去實(shí)現(xiàn)。 下面是一個(gè)在 CSV 數(shù)據(jù)上執(zhí)行其他類(lèi)型轉(zhuǎn)換的例子:
col_types = [str, float, str, str, float, int]
with open('stocks.csv') as f:
f_csv = csv.reader(f)
headers = next(f_csv)
for row in f_csv:
# Apply conversions to the row items
row = tuple(convert(value) for convert, value in zip(col_types, row))
...
另外,下面是一個(gè)轉(zhuǎn)換字典中特定字段的例子:
print('Reading as dicts with type conversion')
field_types = [ ('Price', float),
('Change', float),
('Volume', int) ]
with open('stocks.csv') as f:
for row in csv.DictReader(f):
row.update((key, conversion(row[key]))
for key, conversion in field_types)
print(row)
通常來(lái)講,你可能并不想過(guò)多去考慮這些轉(zhuǎn)換問(wèn)題。 在實(shí)際情況中,CSV 文件都或多或少有些缺失的數(shù)據(jù),被破壞的數(shù)據(jù)以及其它一些讓轉(zhuǎn)換失敗的問(wèn)題。 因此,除非你的數(shù)據(jù)確實(shí)有保障是準(zhǔn)確無(wú)誤的,否則你必須考慮這些問(wèn)題(你可能需要增加合適的錯(cuò)誤處理機(jī)制)。
最后,如果你讀取 CSV 數(shù)據(jù)的目的是做數(shù)據(jù)分析和統(tǒng)計(jì)的話, 你可能需要看一看 Pandas
包。Pandas
包含了一個(gè)非常方便的函數(shù)叫 pandas.read_csv()
, 它可以加載 CSV 數(shù)據(jù)到一個(gè) DataFrame
對(duì)象中去。 然后利用這個(gè)對(duì)象你就可以生成各種形式的統(tǒng)計(jì)、過(guò)濾數(shù)據(jù)以及執(zhí)行其他高級(jí)操作了。 在6.13小節(jié)中會(huì)有這樣一個(gè)例子。
你想讀寫(xiě) JSON(JavaScript Object Notation)編碼格式的數(shù)據(jù)。
json
模塊提供了一種很簡(jiǎn)單的方式來(lái)編碼和解碼 JSON 數(shù)據(jù)。 其中兩個(gè)主要的函數(shù)是 json.dumps()
和 json.loads()
, 要比其他序列化函數(shù)庫(kù)如 pickle 的接口少得多。 下面演示如何將一個(gè) Python 數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為 JSON:
import json
data = {
'name' : 'ACME',
'shares' : 100,
'price' : 542.23
}
json_str = json.dumps(data)
下面演示如何將一個(gè) JSON 編碼的字符串轉(zhuǎn)換回一個(gè) Python 數(shù)據(jù)結(jié)構(gòu):
data = json.loads(json_str)
如果你要處理的是文件而不是字符串,你可以使用 json.dump()
和json.load()
來(lái)編碼和解碼 JSON 數(shù)據(jù)。例如:
# Writing JSON data
with open('data.json', 'w') as f:
json.dump(data, f)
# Reading data back
with open('data.json', 'r') as f:
data = json.load(f)
JSON 編碼支持的基本數(shù)據(jù)類(lèi)型為 None
,bool
,int
, float
和 str
, 以及包含這些類(lèi)型數(shù)據(jù)的 lists,tuples 和 dictionaries。 對(duì)于 dictionaries,keys 需要是字符串類(lèi)型(字典中任何非字符串類(lèi)型的key在編碼時(shí)會(huì)先轉(zhuǎn)換為字符串)。 為了遵循 JSON 規(guī)范,你應(yīng)該只編碼 Python 的 lists 和 dictionaries。 而且,在 web 應(yīng)用程序中,頂層對(duì)象被編碼為一個(gè)字典是一個(gè)標(biāo)準(zhǔn)做法。
JSON 編碼的格式對(duì)于 Python 語(yǔ)法而已幾乎是完全一樣的,除了一些小的差異之外。 比如,True 會(huì)被映射為 true,F(xiàn)alse 被映射為 false,而 None 會(huì)被映射為 null。 下面是一個(gè)例子,演示了編碼后的字符串效果:
>>> json.dumps(False)
'false'
>>> d = {'a': True,
... 'b': 'Hello',
... 'c': None}
>>> json.dumps(d)
'{"b": "Hello", "c": null, "a": true}'
>>>
如果你試著去檢查 JSON 解碼后的數(shù)據(jù),你通常很難通過(guò)簡(jiǎn)單的打印來(lái)確定它的結(jié)構(gòu), 特別是當(dāng)數(shù)據(jù)的嵌套結(jié)構(gòu)層次很深或者包含大量的字段時(shí)。 為了解決這個(gè)問(wèn)題,可以考慮使用 pprint 模塊的 pprint()
函數(shù)來(lái)代替普通的 print()
函數(shù)。 它會(huì)按照 key 的字母順序并以一種更加美觀的方式輸出。 下面是一個(gè)演示如何漂亮的打印輸出 Twitter 上搜索結(jié)果的例子:
>>> from urllib.request import urlopen
>>> import json
>>> u = urlopen('http://search.twitter.com/search.json?q=python&rpp=5')
>>> resp = json.loads(u.read().decode('utf-8'))
>>> from pprint import pprint
>>> pprint(resp)
{'completed_in': 0.074,
'max_id': 264043230692245504,
'max_id_str': '264043230692245504',
'next_page': '?page=2&max_id=264043230692245504&q=python&rpp=5',
'page': 1,
'query': 'python',
'refresh_url': '?since_id=264043230692245504&q=python',
'results': [{'created_at': 'Thu, 01 Nov 2012 16:36:26 +0000',
'from_user': ...
},
{'created_at': 'Thu, 01 Nov 2012 16:36:14 +0000',
'from_user': ...
},
{'created_at': 'Thu, 01 Nov 2012 16:36:13 +0000',
'from_user': ...
},
{'created_at': 'Thu, 01 Nov 2012 16:36:07 +0000',
'from_user': ...
}
{'created_at': 'Thu, 01 Nov 2012 16:36:04 +0000',
'from_user': ...
}],
'results_per_page': 5,
'since_id': 0,
'since_id_str': '0'}
>>>
一般來(lái)講,JSON 解碼會(huì)根據(jù)提供的數(shù)據(jù)創(chuàng)建 dicts 或 lists。 如果你想要?jiǎng)?chuàng)建其他類(lèi)型的對(duì)象,可以給 json.loads()
傳遞 object_pairs_hook 或 object_hook 參數(shù)。 例如,下面是演示如何解碼 JSON 數(shù)據(jù)并在一個(gè) OrderedDict 中保留其順序的例子:
>>> s = '{"name": "ACME", "shares": 50, "price": 490.1}'
>>> from collections import OrderedDict
>>> data = json.loads(s, object_pairs_hook=OrderedDict)
>>> data
OrderedDict([('name', 'ACME'), ('shares', 50), ('price', 490.1)])
>>>
下面是如何將一個(gè) JSON 字典轉(zhuǎn)換為一個(gè) Python 對(duì)象例子:
>>> class JSONObject:
... def __init__(self, d):
... self.__dict__ = d
...
>>>
>>> data = json.loads(s, object_hook=JSONObject)
>>> data.name
'ACME'
>>> data.shares
50
>>> data.price
490.1
>>>
最后一個(gè)例子中,JSON 解碼后的字典作為一個(gè)單個(gè)參數(shù)傳遞給 __init__()
。 然后,你就可以隨心所欲的使用它了,比如作為一個(gè)實(shí)例字典來(lái)直接使用它。
在編碼 JSON 的時(shí)候,還有一些選項(xiàng)很有用。 如果你想獲得漂亮的格式化字符串后輸出,可以使用 json.dumps()
的 indent 參數(shù)。 它會(huì)使得輸出和 pprint()函數(shù)效果類(lèi)似。比如:
>>> print(json.dumps(data))
{"price": 542.23, "name": "ACME", "shares": 100}
>>> print(json.dumps(data, indent=4))
{
"price": 542.23,
"name": "ACME",
"shares": 100
}
>>>
對(duì)象實(shí)例通常并不是 JSON 可序列化的。例如:
>>> class Point:
... def __init__(self, x, y):
... self.x = x
... self.y = y
...
>>> p = Point(2, 3)
>>> json.dumps(p)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.3/json/__init__.py", line 226, in dumps
return _default_encoder.encode(obj)
File "/usr/local/lib/python3.3/json/encoder.py", line 187, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/local/lib/python3.3/json/encoder.py", line 245, in iterencode
return _iterencode(o, 0)
File "/usr/local/lib/python3.3/json/encoder.py", line 169, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <__main__.Point object at 0x1006f2650> is not JSON serializable
>>>
如果你想序列化對(duì)象實(shí)例,你可以提供一個(gè)函數(shù),它的輸入是一個(gè)實(shí)例,返回一個(gè)可序列化的字典。例如:
def serialize_instance(obj):
d = { '__classname__' : type(obj).__name__ }
d.update(vars(obj))
return d
如果你想反過(guò)來(lái)獲取這個(gè)實(shí)例,可以這樣做:
# Dictionary mapping names to known classes
classes = {
'Point' : Point
}
def unserialize_object(d):
clsname = d.pop('__classname__', None)
if clsname:
cls = classes[clsname]
obj = cls.__new__(cls) # Make instance without calling __init__
for key, value in d.items():
setattr(obj, key, value)
return obj
else:
return d
下面是如何使用這些函數(shù)的例子:
>>> p = Point(2,3)
>>> s = json.dumps(p, default=serialize_instance)
>>> s
'{"__classname__": "Point", "y": 3, "x": 2}'
>>> a = json.loads(s, object_hook=unserialize_object)
>>> a
<__main__.Point object at 0x1017577d0>
>>> a.x
2
>>> a.y
3
>>>
json
模塊還有很多其他選項(xiàng)來(lái)控制更低級(jí)別的數(shù)字、特殊值如 NaN 等的解析。 可以參考官方文檔獲取更多細(xì)節(jié)。
你想從一個(gè)簡(jiǎn)單的 XML 文檔中提取數(shù)據(jù)。
可以使用 xml.etree.ElementTree
模塊從簡(jiǎn)單的XML文檔中提取數(shù)據(jù)。 為了演示,假設(shè)你想解析 Planet Python 上的 RSS 源。下面是相應(yīng)的代碼:
from urllib.request import urlopen
from xml.etree.ElementTree import parse
# Download the RSS feed and parse it
u = urlopen('http://planet.python.org/rss20.xml')
doc = parse(u)
# Extract and output tags of interest
for item in doc.iterfind('channel/item'):
title = item.findtext('title')
date = item.findtext('pubDate')
link = item.findtext('link')
print(title)
print(date)
print(link)
print()
運(yùn)行上面的代碼,輸出結(jié)果類(lèi)似這樣:
Steve Holden: Python for Data Analysis
Mon, 19 Nov 2012 02:13:51 +0000
http://holdenweb.blogspot.com/2012/11/python-for-data-analysis.html
Vasudev Ram: The Python Data model (for v2 and v3)
Sun, 18 Nov 2012 22:06:47 +0000
http://jugad2.blogspot.com/2012/11/the-python-data-model.html
Python Diary: Been playing around with Object Databases
Sun, 18 Nov 2012 20:40:29 +0000
http://www.pythondiary.com/blog/Nov.18,2012/been-...-object-databases.html
Vasudev Ram: Wakari, Scientific Python in the cloud
Sun, 18 Nov 2012 20:19:41 +0000
http://jugad2.blogspot.com/2012/11/wakari-scientific-python-in-cloud.html
Jesse Jiryu Davis: Toro: synchronization primitives for Tornado coroutines
Sun, 18 Nov 2012 20:17:49 +0000
http://feedproxy.google.com/~r/EmptysquarePython/~3/_DOZT2Kd0hQ/
很顯然,如果你想做進(jìn)一步的處理,你需要替換 print()
語(yǔ)句來(lái)完成其他有趣的事。
在很多應(yīng)用程序中處理 XML 編碼格式的數(shù)據(jù)是很常見(jiàn)的。 不僅因?yàn)?XML 在 Internet 上面已經(jīng)被廣泛應(yīng)用于數(shù)據(jù)交換, 同時(shí)它也是一種存儲(chǔ)應(yīng)用程序數(shù)據(jù)的常用格式(比如字處理,音樂(lè)庫(kù)等)。 接下來(lái)的討論會(huì)先假定讀者已經(jīng)對(duì) XML 基礎(chǔ)比較熟悉了。
在很多情況下,當(dāng)使用 XML 來(lái)僅僅存儲(chǔ)數(shù)據(jù)的時(shí)候,對(duì)應(yīng)的文檔結(jié)構(gòu)非常緊湊并且直觀。 例如,上面例子中的 RSS 訂閱源類(lèi)似于下面的格式:
<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Planet Python</title>
<link>http://planet.python.org/</link>
<language>en</language>
<description>Planet Python - http://planet.python.org/</description>
<item>
<title>Steve Holden: Python for Data Analysis</title>
<guid>http://holdenweb.blogspot.com/...-data-analysis.html</guid>
<link>http://holdenweb.blogspot.com/...-data-analysis.html</link>
<description>...</description>
<pubDate>Mon, 19 Nov 2012 02:13:51 +0000</pubDate>
</item>
<item>
<title>Vasudev Ram: The Python Data model (for v2 and v3)</title>
<guid>http://jugad2.blogspot.com/...-data-model.html</guid>
<link>http://jugad2.blogspot.com/...-data-model.html</link>
<description>...</description>
<pubDate>Sun, 18 Nov 2012 22:06:47 +0000</pubDate>
</item>
<item>
<title>Python Diary: Been playing around with Object Databases</title>
<guid>http://www.pythondiary.com/...-object-databases.html</guid>
<link>http://www.pythondiary.com/...-object-databases.html</link>
<description>...</description>
<pubDate>Sun, 18 Nov 2012 20:40:29 +0000</pubDate>
</item>
...
</channel>
</rss>
xml.etree.ElementTree.parse()
函數(shù)解析整個(gè)XML文檔并將其轉(zhuǎn)換成一個(gè)文檔對(duì)象。 然后,你就能使用 find()
、iterfind()
和 findtext()
等方法來(lái)搜索特定的 XML 元素了。 這些函數(shù)的參數(shù)就是某個(gè)指定的標(biāo)簽名,例如 channel/item
或 title
。
每次指定某個(gè)標(biāo)簽時(shí),你需要遍歷整個(gè)文檔結(jié)構(gòu)。每次搜索操作會(huì)從一個(gè)起始元素開(kāi)始進(jìn)行。 同樣,每次操作所指定的標(biāo)簽名也是起始元素的相對(duì)路徑。 例如,執(zhí)行 doc.iterfind('channel/item')
來(lái)搜索所有在channel
元素下面的 item
元素。 doc
代表文檔的最頂層(也就是第一級(jí)的rss
元素)。 然后接下來(lái)的調(diào)用 item.findtext()
會(huì)從已找到的 item
元素位置開(kāi)始搜索。
ElementTree
模塊中的每個(gè)元素有一些重要的屬性和方法,在解析的時(shí)候非常有用。 tag
屬性包含了標(biāo)簽的名字,text
屬性包含了內(nèi)部的文本,而 get()
方法能獲取屬性值。例如:
>>> doc
<xml.etree.ElementTree.ElementTree object at 0x101339510>
>>> e = doc.find('channel/title')
>>> e
<Element 'title' at 0x10135b310>
>>> e.tag
'title'
>>> e.text
'Planet Python'
>>> e.get('some_attribute')
>>>
有一點(diǎn)要強(qiáng)調(diào)的是 xml.etree.ElementTree
并不是 XML 解析的唯一方法。 對(duì)于更高級(jí)的應(yīng)用程序,你需要考慮使用 lxml
。 它使用了和 ElementTree 同樣的編程接口,因此上面的例子同樣也適用于 lxml。 你只需要將剛開(kāi)始的 import 語(yǔ)句換成 from lxml.etree import parse
就行了。 lxml
完全遵循 XML 標(biāo)準(zhǔn),并且速度也非??欤瑫r(shí)還支持驗(yàn)證,XSLT,和 XPath 等特性。
你想使用盡可能少的內(nèi)存從一個(gè)超大的 XML 文檔中提取數(shù)據(jù)。
任何時(shí)候只要你遇到增量式的數(shù)據(jù)處理時(shí),第一時(shí)間就應(yīng)該想到迭代器和生成器。 下面是一個(gè)很簡(jiǎn)單的函數(shù),只使用很少的內(nèi)存就能增量式的處理一個(gè)大型 XML 文件:
from xml.etree.ElementTree import iterparse
def parse_and_remove(filename, path):
path_parts = path.split('/')
doc = iterparse(filename, ('start', 'end'))
# Skip the root element
next(doc)
tag_stack = []
elem_stack = []
for event, elem in doc:
if event == 'start':
tag_stack.append(elem.tag)
elem_stack.append(elem)
elif event == 'end':
if tag_stack == path_parts:
yield elem
elem_stack[-2].remove(elem)
try:
tag_stack.pop()
elem_stack.pop()
except IndexError:
pass
為了測(cè)試這個(gè)函數(shù),你需要先有一個(gè)大型的 XML 文件。 通常你可以在政府網(wǎng)站或公共數(shù)據(jù)網(wǎng)站上找到這樣的文件。 例如,你可以下載 XML 格式的芝加哥城市道路坑洼數(shù)據(jù)庫(kù)。 在寫(xiě)這本書(shū)的時(shí)候,下載文件已經(jīng)包含超過(guò)100,000行數(shù)據(jù),編碼格式類(lèi)似于下面這樣:
假設(shè)你想寫(xiě)一個(gè)腳本來(lái)按照坑洼報(bào)告數(shù)量排列郵編號(hào)碼。你可以像這樣做:
from xml.etree.ElementTree import parse
from collections import Counter
potholes_by_zip = Counter()
doc = parse('potholes.xml')
for pothole in doc.iterfind('row/row'):
potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
print(zipcode, num)
這個(gè)腳本唯一的問(wèn)題是它會(huì)先將整個(gè) XML 文件加載到內(nèi)存中然后解析。 在我的機(jī)器上,為了運(yùn)行這個(gè)程序需要用到 450 MB 左右的內(nèi)存空間。 如果使用如下代碼,程序只需要修改一點(diǎn)點(diǎn):
from collections import Counter
potholes_by_zip = Counter()
data = parse_and_remove('potholes.xml', 'row/row')
for pothole in data:
potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
print(zipcode, num)
結(jié)果是:這個(gè)版本的代碼運(yùn)行時(shí)只需要 7 MB 的內(nèi)存–大大節(jié)約了內(nèi)存資源。
這一節(jié)的技術(shù)會(huì)依賴 ElementTree
模塊中的兩個(gè)核心功能。 第一,iterparse()
方法允許對(duì) XML 文檔進(jìn)行增量操作。 使用時(shí),你需要提供文件名和一個(gè)包含下面一種或多種類(lèi)型的事件列表: start
, end
, start-ns
和 end-ns
。 由 iterparse()
創(chuàng)建的迭代器會(huì)產(chǎn)生形如 (event, elem)
的元組, 其中 event
是上述事件列表中的某一個(gè),而 elem
是相應(yīng)的 XML 元素。例如:
>>> data = iterparse('potholes.xml',('start','end'))
>>> next(data)
('start', <Element 'response' at 0x100771d60>)
>>> next(data)
('start', <Element 'row' at 0x100771e68>)
>>> next(data)
('start', <Element 'row' at 0x100771fc8>)
>>> next(data)
('start', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('end', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('start', <Element 'status' at 0x1006a7f18>)
>>> next(data)
('end', <Element 'status' at 0x1006a7f18>)
>>>
start
事件在某個(gè)元素第一次被創(chuàng)建并且還沒(méi)有被插入其他數(shù)據(jù)(如子元素)時(shí)被創(chuàng)建。 而 end
事件在某個(gè)元素已經(jīng)完成時(shí)被創(chuàng)建。 盡管沒(méi)有在例子中演示, start-ns
和 end-ns
事件被用來(lái)處理 XML 文檔命名空間的聲明。
這本節(jié)例子中,start
和end
事件被用來(lái)管理元素和標(biāo)簽棧。 棧代表了文檔被解析時(shí)的層次結(jié)構(gòu), 還被用來(lái)判斷某個(gè)元素是否匹配傳給函數(shù) parse_and_remove()
的路徑。 如果匹配,就利用 yield
語(yǔ)句向調(diào)用者返回這個(gè)元素。
在 yield
之后的下面這個(gè)語(yǔ)句才是使得程序占用極少內(nèi)存的 ElementTree 的核心特性:
elem_stack[-2].remove(elem)
這個(gè)語(yǔ)句使得之前由 yield
產(chǎn)生的元素從它的父節(jié)點(diǎn)中刪除掉。 假設(shè)已經(jīng)沒(méi)有其它的地方引用這個(gè)元素了,那么這個(gè)元素就被銷(xiāo)毀并回收內(nèi)存。
對(duì)節(jié)點(diǎn)的迭代式解析和刪除的最終效果就是一個(gè)在文檔上高效的增量式清掃過(guò)程。 文檔樹(shù)結(jié)構(gòu)從始自終沒(méi)被完整的創(chuàng)建過(guò)。盡管如此,還是能通過(guò)上述簡(jiǎn)單的方式來(lái)處理這個(gè) XML 數(shù)據(jù)。
這種方案的主要缺陷就是它的運(yùn)行性能了。 我自己測(cè)試的結(jié)果是,讀取整個(gè)文檔到內(nèi)存中的版本的運(yùn)行速度差不多是增量式處理版本的兩倍快。 但是它卻使用了超過(guò)后者60倍的內(nèi)存。 因此,如果你更關(guān)心內(nèi)存使用量的話,那么增量式的版本完勝。
你想使用一個(gè) Python 字典存儲(chǔ)數(shù)據(jù),并將它轉(zhuǎn)換成 XML 格式。
盡管 xml.etree.ElementTree
庫(kù)通常用來(lái)做解析工作,其實(shí)它也可以創(chuàng)建 XML 文檔。 例如,考慮如下這個(gè)函數(shù):
from xml.etree.ElementTree import Element
def dict_to_xml(tag, d):
'''
Turn a simple dict of key/value pairs into XML
'''
elem = Element(tag)
for key, val in d.items():
child = Element(key)
child.text = str(val)
elem.append(child)
return elem
下面是一個(gè)使用例子:
>>> s = { 'name': 'GOOG', 'shares': 100, 'price':490.1 }
>>> e = dict_to_xml('stock', s)
>>> e
<Element 'stock' at 0x1004b64c8>
>>>
轉(zhuǎn)換結(jié)果是一個(gè) Element
實(shí)例。對(duì)于 I/O 操作,使用 xml.etree.ElementTree
中的 tostring()
函數(shù)很容易就能將它轉(zhuǎn)換成一個(gè)字節(jié)字符串。例如:
>>> from xml.etree.ElementTree import tostring
>>> tostring(e)
b'<stock><price>490.1</price><shares>100</shares><name>GOOG</name></stock>'
>>>
如果你想給某個(gè)元素添加屬性值,可以使用 set()
方法:
>>> e.set('_id','1234')
>>> tostring(e)
b'<stock _id="1234"><price>490.1</price><shares>100</shares><name>GOOG</name>
</stock>'
>>>
如果你還想保持元素的順序,可以考慮構(gòu)造一個(gè) OrderedDict
來(lái)代替一個(gè)普通的字典。請(qǐng)參考1.7小節(jié)。
當(dāng)創(chuàng)建 XML 的時(shí)候,你被限制只能構(gòu)造字符串類(lèi)型的值。例如:
def dict_to_xml_str(tag, d):
'''
Turn a simple dict of key/value pairs into XML
'''
parts = ['<{}>'.format(tag)]
for key, val in d.items():
parts.append('<{0}>{1}</{0}>'.format(key,val))
parts.append('</{}>'.format(tag))
return ''.join(parts)
問(wèn)題是如果你手動(dòng)的去構(gòu)造的時(shí)候可能會(huì)碰到一些麻煩。例如,當(dāng)字典的值中包含一些特殊字符的時(shí)候會(huì)怎樣呢?
>>> d = { 'name' : '<spam>' }
>>> # String creation
>>> dict_to_xml_str('item',d)
'<item><name><spam></name></item>'
>>> # Proper XML creation
>>> e = dict_to_xml('item',d)
>>> tostring(e)
b'<item><name><spam></name></item>'
>>>
注意到程序的后面那個(gè)例子中,字符 ‘<’ 和 ‘>’ 被替換成了 <
和>
下面僅供參考,如果你需要手動(dòng)去轉(zhuǎn)換這些字符, 可以使用 xml.sax.saxutils
中的 escape()
和 unescape()
函數(shù)。例如:
>>> from xml.sax.saxutils import escape, unescape
>>> escape('<spam>')
'<spam>'
>>> unescape(_)
'<spam>'
>>>
除了能創(chuàng)建正確的輸出外,還有另外一個(gè)原因推薦你創(chuàng)建 Element
實(shí)例而不是字符串, 那就是使用字符串組合構(gòu)造一個(gè)更大的文檔并不是那么容易。 而 Element
實(shí)例可以不用考慮解析 XML 文本的情況下通過(guò)多種方式被處理。 也就是說(shuō),你可以在一個(gè)高級(jí)數(shù)據(jù)結(jié)構(gòu)上完成你所有的操作,并在最后以字符串的形式將其輸出。
你想讀取一個(gè) XML 文檔,對(duì)它最一些修改,然后將結(jié)果寫(xiě)回 XML 文檔。
使用 xml.etree.ElementTree
模塊可以很容易的處理這些任務(wù)。 第一步是以通常的方式來(lái)解析這個(gè)文檔。例如,假設(shè)你有一個(gè)名為 pred.xml
的文檔,類(lèi)似下面這樣:
下面是一個(gè)利用 ElementTree
來(lái)讀取這個(gè)文檔并對(duì)它做一些修改的例子:
>>> from xml.etree.ElementTree import parse, Element
>>> doc = parse('pred.xml')
>>> root = doc.getroot()
>>> root
<Element 'stop' at 0x100770cb0>
>>> # Remove a few elements
>>> root.remove(root.find('sri'))
>>> root.remove(root.find('cr'))
>>> # Insert a new element after <nm>...</nm>
>>> root.getchildren().index(root.find('nm'))
1
>>> e = Element('spam')
>>> e.text = 'This is a test'
>>> root.insert(2, e)
>>> # Write back to a file
>>> doc.write('newpred.xml', xml_declaration=True)
>>>
處理結(jié)果是一個(gè)像下面這樣新的 XML 文件:
修改一個(gè) XML 文檔結(jié)構(gòu)是很容易的,但是你必須牢記的是所有的修改都是針對(duì)父節(jié)點(diǎn)元素, 將它作為一個(gè)列表來(lái)處理。例如,如果你刪除某個(gè)元素,通過(guò)調(diào)用父節(jié)點(diǎn)的 remove()
方法從它的直接父節(jié)點(diǎn)中刪除。 如果你插入或增加新的元素,你同樣使用父節(jié)點(diǎn)元素的insert()
和 append()
方法。 還能對(duì)元素使用索引和切片操作,比如element[i]
或 element[i:j]
如果你需要?jiǎng)?chuàng)建新的元素,可以使用本節(jié)方案中演示的Element
類(lèi)。我們?cè)?.5小節(jié)已經(jīng)詳細(xì)討論過(guò)了。
你想解析某個(gè) XML 文檔,文檔中使用了 XML 命名空間。
考慮下面這個(gè)使用了命名空間的文檔:
如果你解析這個(gè)文檔并執(zhí)行普通的查詢,你會(huì)發(fā)現(xiàn)這個(gè)并不是那么容易,因?yàn)樗胁襟E都變得相當(dāng)?shù)姆爆崱?/p>
>>> # Some queries that work
>>> doc.findtext('author')
'David Beazley'
>>> doc.find('content')
<Element 'content' at 0x100776ec0>
>>> # A query involving a namespace (doesn't work)
>>> doc.find('content/html')
>>> # Works if fully qualified
>>> doc.find('content/{http://www.w3.org/1999/xhtml}html')
<Element '{http://www.w3.org/1999/xhtml}html' at 0x1007767e0>
>>> # Doesn't work
>>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/head/title')
>>> # Fully qualified
>>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/'
... '{http://www.w3.org/1999/xhtml}head/{http://www.w3.org/1999/xhtml}title')
'Hello World'
>>>
你可以通過(guò)將命名空間處理邏輯包裝為一個(gè)工具類(lèi)來(lái)簡(jiǎn)化這個(gè)過(guò)程:
class XMLNamespaces:
def __init__(self, **kwargs):
self.namespaces = {}
for name, uri in kwargs.items():
self.register(name, uri)
def register(self, name, uri):
self.namespaces[name] = '{'+uri+'}'
def __call__(self, path):
return path.format_map(self.namespaces)
通過(guò)下面的方式使用這個(gè)類(lèi):
>>> ns = XMLNamespaces(html='http://www.w3.org/1999/xhtml')
>>> doc.find(ns('content/{html}html'))
<Element '{http://www.w3.org/1999/xhtml}html' at 0x1007767e0>
>>> doc.findtext(ns('content/{html}html/{html}head/{html}title'))
'Hello World'
>>>
討論
解析含有命名空間的 XML 文檔會(huì)比較繁瑣。 上面的 XMLNamespaces
僅僅是允許你使用縮略名代替完整的 URI 將其變得稍微簡(jiǎn)潔一點(diǎn)。
很不幸的是,在基本的 ElementTree
解析中沒(méi)有任何途徑獲取命名空間的信息。 但是,如果你使用 iterparse()
函數(shù)的話就可以獲取更多關(guān)于命名空間處理范圍的信息。例如:
>>> from xml.etree.ElementTree import iterparse
>>> for evt, elem in iterparse('ns2.xml', ('end', 'start-ns', 'end-ns')):
... print(evt, elem)
...
end <Element 'author' at 0x10110de10>
start-ns ('', 'http://www.w3.org/1999/xhtml')
end <Element '{http://www.w3.org/1999/xhtml}title' at 0x1011131b0>
end <Element '{http://www.w3.org/1999/xhtml}head' at 0x1011130a8>
end <Element '{http://www.w3.org/1999/xhtml}h1' at 0x101113310>
end <Element '{http://www.w3.org/1999/xhtml}body' at 0x101113260>
end <Element '{http://www.w3.org/1999/xhtml}html' at 0x10110df70>
end-ns None
end <Element 'content' at 0x10110de68>
end <Element 'top' at 0x10110dd60>
>>> elem # This is the topmost element
<Element 'top' at 0x10110dd60>
>>>
最后一點(diǎn),如果你要處理的 XML 文本除了要使用到其他高級(jí) XML 特性外,還要使用到命名空間, 建議你最好是使用 lxml
函數(shù)庫(kù)來(lái)代替 ElementTree
。 例如,lxml
對(duì)利用 DTD 驗(yàn)證文檔、更好的 XPath 支持和一些其他高級(jí) XML 特性等都提供了更好的支持。 這一小節(jié)其實(shí)只是教你如何讓 XML 解析稍微簡(jiǎn)單一點(diǎn)。
你想在關(guān)系型數(shù)據(jù)庫(kù)中查詢、增加或刪除記錄。
Python 中表示多行數(shù)據(jù)的標(biāo)準(zhǔn)方式是一個(gè)由元組構(gòu)成的序列。例如:
stocks = [
('GOOG', 100, 490.1),
('AAPL', 50, 545.75),
('FB', 150, 7.45),
('HPQ', 75, 33.2),
]
依據(jù) PEP249,通過(guò)這種形式提供數(shù)據(jù), 可以很容易的使用 Python 標(biāo)準(zhǔn)數(shù)據(jù)庫(kù) API 和關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行交互。 所有數(shù)據(jù)庫(kù)上的操作都通過(guò) SQL 查詢語(yǔ)句來(lái)完成。每一行輸入輸出數(shù)據(jù)用一個(gè)元組來(lái)表示。
為了演示說(shuō)明,你可以使用 Python 標(biāo)準(zhǔn)庫(kù)中的 sqlite3
模塊。 如果你使用的是一個(gè)不同的數(shù)據(jù)庫(kù)(比如 MySql、Postgresql 或者 ODBC), 還得安裝相應(yīng)的第三方模塊來(lái)提供支持。 不過(guò)相應(yīng)的編程接口幾乎都是一樣的,除了一點(diǎn)點(diǎn)細(xì)微差別外。
第一步是連接到數(shù)據(jù)庫(kù)。通常你要執(zhí)行 connect()
函數(shù), 給它提供一些數(shù)據(jù)庫(kù)名、主機(jī)、用戶名、密碼和其他必要的一些參數(shù)。例如:
>>> import sqlite3
>>> db = sqlite3.connect('database.db')
>>>
為了處理數(shù)據(jù),下一步你需要?jiǎng)?chuàng)建一個(gè)游標(biāo)。 一旦你有了游標(biāo),那么你就可以執(zhí)行 SQL 查詢語(yǔ)句了。比如:
>>> c = db.cursor()
>>> c.execute('create table portfolio (symbol text, shares integer, price real)')
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>
為了向數(shù)據(jù)庫(kù)表中插入多條記錄,使用類(lèi)似下面這樣的語(yǔ)句:
>>> c.executemany('insert into portfolio values (?,?,?)', stocks)
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>
為了執(zhí)行某個(gè)查詢,使用像下面這樣的語(yǔ)句:
>>> for row in db.execute('select * from portfolio'):
... print(row)
...
('GOOG', 100, 490.1)
('AAPL', 50, 545.75)
('FB', 150, 7.45)
('HPQ', 75, 33.2)
>>>
如果你想接受用戶輸入作為參數(shù)來(lái)執(zhí)行查詢操作,必須確保你使用下面這樣的占位符?
來(lái)進(jìn)行引用參數(shù):
>>> min_price = 100
>>> for row in db.execute('select * from portfolio where price >= ?',
(min_price,)):
... print(row)
...
('GOOG', 100, 490.1)
('AAPL', 50, 545.75)
>>>
在比較低的級(jí)別上和數(shù)據(jù)庫(kù)交互是非常簡(jiǎn)單的。 你只需提供 SQL 語(yǔ)句并調(diào)用相應(yīng)的模塊就可以更新或提取數(shù)據(jù)了。 雖說(shuō)如此,還是有一些比較棘手的細(xì)節(jié)問(wèn)題需要你逐個(gè)列出去解決。
一個(gè)難點(diǎn)是數(shù)據(jù)庫(kù)中的數(shù)據(jù)和 Python 類(lèi)型直接的映射。 對(duì)于日期類(lèi)型,通??梢允褂?datetime
模塊中的 datetime
實(shí)例, 或者可能是 time
模塊中的系統(tǒng)時(shí)間戳。 對(duì)于數(shù)字類(lèi)型,特別是使用到小數(shù)的金融數(shù)據(jù),可以用 decimal
模塊中的 Decimal
實(shí)例來(lái)表示。 不幸的是,對(duì)于不同的數(shù)據(jù)庫(kù)而言具體映射規(guī)則是不一樣的,你必須參考相應(yīng)的文檔。
另外一個(gè)更加復(fù)雜的問(wèn)題就是 SQL 語(yǔ)句字符串的構(gòu)造。 你千萬(wàn)不要使用 Python 字符串格式化操作符(如%)或者 .format()
方法來(lái)創(chuàng)建這樣的字符串。 如果傳遞給這些格式化操作符的值來(lái)自于用戶的輸入,那么你的程序就很有可能遭受 SQL 注入攻擊(參考 http://xkcd.com/327 )。 查詢語(yǔ)句中的通配符 ?
指示后臺(tái)數(shù)據(jù)庫(kù)使用它自己的字符串替換機(jī)制,這樣更加的安全。
不幸的是,不同的數(shù)據(jù)庫(kù)后臺(tái)對(duì)于通配符的使用是不一樣的。大部分模塊使用 ?
或 %s
, 還有其他一些使用了不同的符號(hào),比如:0或:1來(lái)指示參數(shù)。 同樣的,你還是得去參考你使用的數(shù)據(jù)庫(kù)模塊相應(yīng)的文檔。 一個(gè)數(shù)據(jù)庫(kù)模塊的 paramstyle
屬性包含了參數(shù)引用風(fēng)格的信息。
對(duì)于簡(jiǎn)單的數(shù)據(jù)庫(kù)數(shù)據(jù)的讀寫(xiě)問(wèn)題,使用數(shù)據(jù)庫(kù) API 通常非常簡(jiǎn)單。 如果你要處理更加復(fù)雜的問(wèn)題,建議你使用更加高級(jí)的接口,比如一個(gè)對(duì)象關(guān)系映射 ORM 所提供的接口。 類(lèi)似 SQLAlchemy
這樣的庫(kù)允許你使用 Python 類(lèi)來(lái)表示一個(gè)數(shù)據(jù)庫(kù)表, 并且能在隱藏底層 SQL 的情況下實(shí)現(xiàn)各種數(shù)據(jù)庫(kù)的操作。
你想將一個(gè)十六進(jìn)制字符串解碼成一個(gè)字節(jié)字符串或者將一個(gè)字節(jié)字符串編碼成一個(gè)十六進(jìn)制字符串。
如果你只是簡(jiǎn)單的解碼或編碼一個(gè)十六進(jìn)制的原始字符串,可以使用 binascii
模塊。例如:
>>> # Initial byte string
>>> s = b'hello'
>>> # Encode as hex
>>> import binascii
>>> h = binascii.b2a_hex(s)
>>> h
b'68656c6c6f'
>>> # Decode back to bytes
>>> binascii.a2b_hex(h)
b'hello'
>>>
類(lèi)似的功能同樣可以在 base64
模塊中找到。例如:
>>> import base64
>>> h = base64.b16encode(s)
>>> h
b'68656C6C6F'
>>> base64.b16decode(h)
b'hello'
>>>
大部分情況下,通過(guò)使用上述的函數(shù)來(lái)轉(zhuǎn)換十六進(jìn)制是很簡(jiǎn)單的。 上面兩種技術(shù)的主要不同在于大小寫(xiě)的處理。 函數(shù) base64.b16decode()
和 base64.b16encode()
只能操作大寫(xiě)形式的十六進(jìn)制字母, 而 binascii
模塊中的函數(shù)大小寫(xiě)都能處理。
還有一點(diǎn)需要注意的是編碼函數(shù)所產(chǎn)生的輸出總是一個(gè)字節(jié)字符串。 如果想強(qiáng)制以 Unicode 形式輸出,你需要增加一個(gè)額外的界面步驟。例如:
>>> h = base64.b16encode(s)
>>> print(h)
b'68656C6C6F'
>>> print(h.decode('ascii'))
68656C6C6F
>>>
在解碼十六進(jìn)制數(shù)時(shí),函數(shù) b16decode()
和 a2b_hex()
可以接受字節(jié)或 unicode 字符串。 但是,unicode 字符串必須僅僅只包含 ASCII 編碼的十六進(jìn)制數(shù)。
你需要使用 Base64 格式解碼或編碼二進(jìn)制數(shù)據(jù)。
base64
模塊中有兩個(gè)函數(shù)b64encode()
and b64decode()
可以幫你解決這個(gè)問(wèn)題。例如;
>>> # Some byte data
>>> s = b'hello'
>>> import base64
>>> # Encode as Base64
>>> a = base64.b64encode(s)
>>> a
b'aGVsbG8='
>>> # Decode from Base64
>>> base64.b64decode(a)
b'hello'
>>>
Base64 編碼僅僅用于面向字節(jié)的數(shù)據(jù)比如字節(jié)字符串和字節(jié)數(shù)組。 此外,編碼處理的輸出結(jié)果總是一個(gè)字節(jié)字符串。 如果你想混合使用 Base64 編碼的數(shù)據(jù)和 Unicode 文本,你必須添加一個(gè)額外的解碼步驟。例如:
>>> a = base64.b64encode(s).decode('ascii')
>>> a
'aGVsbG8='
>>>
當(dāng)解碼 Base64 的時(shí)候,字節(jié)字符串和 Unicode 文本都可以作為參數(shù)。 但是,Unicode 字符串只能包含 ASCII 字符。
你想讀寫(xiě)一個(gè)二進(jìn)制數(shù)組的結(jié)構(gòu)化數(shù)據(jù)到 Python 元組中。
解決方案
可以使用 struct
模塊處理二進(jìn)制數(shù)據(jù)。 下面是一段示例代碼將一個(gè) Python 元組列表寫(xiě)入一個(gè)二進(jìn)制文件,并使用 struct
將每個(gè)元組編碼為一個(gè)結(jié)構(gòu)體。
from struct import Struct
def write_records(records, format, f):
'''
Write a sequence of tuples to a binary file of structures.
'''
record_struct = Struct(format)
for r in records:
f.write(record_struct.pack(*r))
# Example
if __name__ == '__main__':
records = [ (1, 2.3, 4.5),
(6, 7.8, 9.0),
(12, 13.4, 56.7) ]
with open('data.b', 'wb') as f:
write_records(records, '<idd', f)
有很多種方法來(lái)讀取這個(gè)文件并返回一個(gè)元組列表。 首先,如果你打算以塊的形式增量讀取文件,你可以這樣做:
from struct import Struct
def read_records(format, f):
record_struct = Struct(format)
chunks = iter(lambda: f.read(record_struct.size), b'')
return (record_struct.unpack(chunk) for chunk in chunks