這一章主要討論使用 Python 處理各種不同方式編碼的數(shù)據(jù),比如 CSV 文件,JSON,XML 和二進制包裝記錄。 和數(shù)據(jù)結(jié)構(gòu)那一章不同的是,這章不會討論特殊的算法問題,而是關(guān)注于怎樣獲取和存儲這些格式的數(shù)據(jù)。
你想讀寫一個 CSV 格式的文件。
對于大多數(shù)的 CSV 格式的數(shù)據(jù)讀寫問題,都可以使用 csv
庫。 例如:假設(shè)你在一個名叫 stocks.csv 文件中有一些股票市場數(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ù)讀取為一個元組的序列:
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
會是一個元組。因此,為了訪問某個字段,你需要使用下標,如 row[0]
訪問 Symbol, row[4]
訪問 Change。
由于這種下標訪問通常會引起混淆,你可以考慮使用命名元組。例如:
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
代替下標訪問。 需要注意的是這個只有在列名是合法的 Python 標識符的時候才生效。如果不是的話, 你可能需要修改下原始的列名(如將非標識符字符替換成下劃線之類的)。
另外一個選擇就是將數(shù)據(jù)讀取到一個字典序列中去??梢赃@樣做:
import csv
with open('stocks.csv') as f:
f_csv = csv.DictReader(f)
for row in f_csv:
# process row
...
在這個版本中,你可以使用列名去訪問每一行的數(shù)據(jù)了。比如,row['Symbol']
或者 row['Change']
。
為了寫入 CSV 數(shù)據(jù),你仍然可以使用 csv 模塊,不過這時候先創(chuàng)建一個 writer
對象。例如:
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)
如果你有一個字典序列的數(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ù)。例如,你可能會像編寫類似下面這樣的代碼:
with open('stocks.csv') as f:
for line in f:
row = line.split(',')
# process row
...
使用這種方式的一個缺點就是你仍然需要去處理一些棘手的細節(jié)問題。 比如,如果某些字段值被引號包圍,你不得不去除這些引號。 另外,如果一個被引號包圍的字段碰巧含有一個逗號,那么程序就會因為產(chǎn)生一個錯誤大小的行而出錯。
默認情況下,csv
庫可識別 Microsoft Excel 所使用的 CSV 編碼規(guī)則。 這或許也是最常見的形式,并且也會給你帶來最好的兼容性。 然而,如果你查看csv
的文檔,就會發(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)換為命名元組,需要注意對列名進行合法性認證。 例如,一個 CSV 格式文件有一個包含非法標識符的列頭行,類似下面這樣:
這樣最終會導致在創(chuàng)建一個命名元組時產(chǎn)生一個 ValueError
異常而失敗。 為了解決這問題,你可能不得不先去修正列標題。 例如,可以像下面這樣在非法標識符上使用一個正則表達式替換:
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ào)的是,csv 產(chǎn)生的數(shù)據(jù)都是字符串類型的,它不會做任何其他類型的轉(zhuǎn)換。 如果你需要做這樣的類型轉(zhuǎn)換,你必須自己手動去實現(xiàn)。 下面是一個在 CSV 數(shù)據(jù)上執(zhí)行其他類型轉(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))
...
另外,下面是一個轉(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)
通常來講,你可能并不想過多去考慮這些轉(zhuǎn)換問題。 在實際情況中,CSV 文件都或多或少有些缺失的數(shù)據(jù),被破壞的數(shù)據(jù)以及其它一些讓轉(zhuǎn)換失敗的問題。 因此,除非你的數(shù)據(jù)確實有保障是準確無誤的,否則你必須考慮這些問題(你可能需要增加合適的錯誤處理機制)。
最后,如果你讀取 CSV 數(shù)據(jù)的目的是做數(shù)據(jù)分析和統(tǒng)計的話, 你可能需要看一看 Pandas
包。Pandas
包含了一個非常方便的函數(shù)叫 pandas.read_csv()
, 它可以加載 CSV 數(shù)據(jù)到一個 DataFrame
對象中去。 然后利用這個對象你就可以生成各種形式的統(tǒng)計、過濾數(shù)據(jù)以及執(zhí)行其他高級操作了。 在6.13小節(jié)中會有這樣一個例子。
你想讀寫 JSON(JavaScript Object Notation)編碼格式的數(shù)據(jù)。
json
模塊提供了一種很簡單的方式來編碼和解碼 JSON 數(shù)據(jù)。 其中兩個主要的函數(shù)是 json.dumps()
和 json.loads()
, 要比其他序列化函數(shù)庫如 pickle 的接口少得多。 下面演示如何將一個 Python 數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為 JSON:
import json
data = {
'name' : 'ACME',
'shares' : 100,
'price' : 542.23
}
json_str = json.dumps(data)
下面演示如何將一個 JSON 編碼的字符串轉(zhuǎn)換回一個 Python 數(shù)據(jù)結(jié)構(gòu):
data = json.loads(json_str)
如果你要處理的是文件而不是字符串,你可以使用 json.dump()
和json.load()
來編碼和解碼 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ù)類型為 None
,bool
,int
, float
和 str
, 以及包含這些類型數(shù)據(jù)的 lists,tuples 和 dictionaries。 對于 dictionaries,keys 需要是字符串類型(字典中任何非字符串類型的key在編碼時會先轉(zhuǎn)換為字符串)。 為了遵循 JSON 規(guī)范,你應(yīng)該只編碼 Python 的 lists 和 dictionaries。 而且,在 web 應(yīng)用程序中,頂層對象被編碼為一個字典是一個標準做法。
JSON 編碼的格式對于 Python 語法而已幾乎是完全一樣的,除了一些小的差異之外。 比如,True 會被映射為 true,F(xiàn)alse 被映射為 false,而 None 會被映射為 null。 下面是一個例子,演示了編碼后的字符串效果:
>>> json.dumps(False)
'false'
>>> d = {'a': True,
... 'b': 'Hello',
... 'c': None}
>>> json.dumps(d)
'{"b": "Hello", "c": null, "a": true}'
>>>
如果你試著去檢查 JSON 解碼后的數(shù)據(jù),你通常很難通過簡單的打印來確定它的結(jié)構(gòu), 特別是當數(shù)據(jù)的嵌套結(jié)構(gòu)層次很深或者包含大量的字段時。 為了解決這個問題,可以考慮使用 pprint 模塊的 pprint()
函數(shù)來代替普通的 print()
函數(shù)。 它會按照 key 的字母順序并以一種更加美觀的方式輸出。 下面是一個演示如何漂亮的打印輸出 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'}
>>>
一般來講,JSON 解碼會根據(jù)提供的數(shù)據(jù)創(chuàng)建 dicts 或 lists。 如果你想要創(chuàng)建其他類型的對象,可以給 json.loads()
傳遞 object_pairs_hook 或 object_hook 參數(shù)。 例如,下面是演示如何解碼 JSON 數(shù)據(jù)并在一個 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)])
>>>
下面是如何將一個 JSON 字典轉(zhuǎn)換為一個 Python 對象例子:
>>> 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
>>>
最后一個例子中,JSON 解碼后的字典作為一個單個參數(shù)傳遞給 __init__()
。 然后,你就可以隨心所欲的使用它了,比如作為一個實例字典來直接使用它。
在編碼 JSON 的時候,還有一些選項很有用。 如果你想獲得漂亮的格式化字符串后輸出,可以使用 json.dumps()
的 indent 參數(shù)。 它會使得輸出和 pprint()函數(shù)效果類似。比如:
>>> print(json.dumps(data))
{"price": 542.23, "name": "ACME", "shares": 100}
>>> print(json.dumps(data, indent=4))
{
"price": 542.23,
"name": "ACME",
"shares": 100
}
>>>
對象實例通常并不是 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
>>>
如果你想序列化對象實例,你可以提供一個函數(shù),它的輸入是一個實例,返回一個可序列化的字典。例如:
def serialize_instance(obj):
d = { '__classname__' : type(obj).__name__ }
d.update(vars(obj))
return d
如果你想反過來獲取這個實例,可以這樣做:
# 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
模塊還有很多其他選項來控制更低級別的數(shù)字、特殊值如 NaN 等的解析。 可以參考官方文檔獲取更多細節(jié)。
你想從一個簡單的 XML 文檔中提取數(shù)據(jù)。
可以使用 xml.etree.ElementTree
模塊從簡單的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()
運行上面的代碼,輸出結(jié)果類似這樣:
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/
很顯然,如果你想做進一步的處理,你需要替換 print()
語句來完成其他有趣的事。
在很多應(yīng)用程序中處理 XML 編碼格式的數(shù)據(jù)是很常見的。 不僅因為 XML 在 Internet 上面已經(jīng)被廣泛應(yīng)用于數(shù)據(jù)交換, 同時它也是一種存儲應(yīng)用程序數(shù)據(jù)的常用格式(比如字處理,音樂庫等)。 接下來的討論會先假定讀者已經(jīng)對 XML 基礎(chǔ)比較熟悉了。
在很多情況下,當使用 XML 來僅僅存儲數(shù)據(jù)的時候,對應(yīng)的文檔結(jié)構(gòu)非常緊湊并且直觀。 例如,上面例子中的 RSS 訂閱源類似于下面的格式:
<?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ù)解析整個XML文檔并將其轉(zhuǎn)換成一個文檔對象。 然后,你就能使用 find()
、iterfind()
和 findtext()
等方法來搜索特定的 XML 元素了。 這些函數(shù)的參數(shù)就是某個指定的標簽名,例如 channel/item
或 title
。
每次指定某個標簽時,你需要遍歷整個文檔結(jié)構(gòu)。每次搜索操作會從一個起始元素開始進行。 同樣,每次操作所指定的標簽名也是起始元素的相對路徑。 例如,執(zhí)行 doc.iterfind('channel/item')
來搜索所有在channel
元素下面的 item
元素。 doc
代表文檔的最頂層(也就是第一級的rss
元素)。 然后接下來的調(diào)用 item.findtext()
會從已找到的 item
元素位置開始搜索。
ElementTree
模塊中的每個元素有一些重要的屬性和方法,在解析的時候非常有用。 tag
屬性包含了標簽的名字,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ào)的是 xml.etree.ElementTree
并不是 XML 解析的唯一方法。 對于更高級的應(yīng)用程序,你需要考慮使用 lxml
。 它使用了和 ElementTree 同樣的編程接口,因此上面的例子同樣也適用于 lxml。 你只需要將剛開始的 import 語句換成 from lxml.etree import parse
就行了。 lxml
完全遵循 XML 標準,并且速度也非???,同時還支持驗證,XSLT,和 XPath 等特性。
你想使用盡可能少的內(nèi)存從一個超大的 XML 文檔中提取數(shù)據(jù)。
任何時候只要你遇到增量式的數(shù)據(jù)處理時,第一時間就應(yīng)該想到迭代器和生成器。 下面是一個很簡單的函數(shù),只使用很少的內(nèi)存就能增量式的處理一個大型 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
為了測試這個函數(shù),你需要先有一個大型的 XML 文件。 通常你可以在政府網(wǎng)站或公共數(shù)據(jù)網(wǎng)站上找到這樣的文件。 例如,你可以下載 XML 格式的芝加哥城市道路坑洼數(shù)據(jù)庫。 在寫這本書的時候,下載文件已經(jīng)包含超過100,000行數(shù)據(jù),編碼格式類似于下面這樣:
假設(shè)你想寫一個腳本來按照坑洼報告數(shù)量排列郵編號碼。你可以像這樣做:
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)
這個腳本唯一的問題是它會先將整個 XML 文件加載到內(nèi)存中然后解析。 在我的機器上,為了運行這個程序需要用到 450 MB 左右的內(nèi)存空間。 如果使用如下代碼,程序只需要修改一點點:
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é)果是:這個版本的代碼運行時只需要 7 MB 的內(nèi)存–大大節(jié)約了內(nèi)存資源。
這一節(jié)的技術(shù)會依賴 ElementTree
模塊中的兩個核心功能。 第一,iterparse()
方法允許對 XML 文檔進行增量操作。 使用時,你需要提供文件名和一個包含下面一種或多種類型的事件列表: start
, end
, start-ns
和 end-ns
。 由 iterparse()
創(chuàng)建的迭代器會產(chǎn)生形如 (event, elem)
的元組, 其中 event
是上述事件列表中的某一個,而 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
事件在某個元素第一次被創(chuàng)建并且還沒有被插入其他數(shù)據(jù)(如子元素)時被創(chuàng)建。 而 end
事件在某個元素已經(jīng)完成時被創(chuàng)建。 盡管沒有在例子中演示, start-ns
和 end-ns
事件被用來處理 XML 文檔命名空間的聲明。
這本節(jié)例子中,start
和end
事件被用來管理元素和標簽棧。 棧代表了文檔被解析時的層次結(jié)構(gòu), 還被用來判斷某個元素是否匹配傳給函數(shù) parse_and_remove()
的路徑。 如果匹配,就利用 yield
語句向調(diào)用者返回這個元素。
在 yield
之后的下面這個語句才是使得程序占用極少內(nèi)存的 ElementTree 的核心特性:
elem_stack[-2].remove(elem)
這個語句使得之前由 yield
產(chǎn)生的元素從它的父節(jié)點中刪除掉。 假設(shè)已經(jīng)沒有其它的地方引用這個元素了,那么這個元素就被銷毀并回收內(nèi)存。
對節(jié)點的迭代式解析和刪除的最終效果就是一個在文檔上高效的增量式清掃過程。 文檔樹結(jié)構(gòu)從始自終沒被完整的創(chuàng)建過。盡管如此,還是能通過上述簡單的方式來處理這個 XML 數(shù)據(jù)。
這種方案的主要缺陷就是它的運行性能了。 我自己測試的結(jié)果是,讀取整個文檔到內(nèi)存中的版本的運行速度差不多是增量式處理版本的兩倍快。 但是它卻使用了超過后者60倍的內(nèi)存。 因此,如果你更關(guān)心內(nèi)存使用量的話,那么增量式的版本完勝。
你想使用一個 Python 字典存儲數(shù)據(jù),并將它轉(zhuǎn)換成 XML 格式。
盡管 xml.etree.ElementTree
庫通常用來做解析工作,其實它也可以創(chuàng)建 XML 文檔。 例如,考慮如下這個函數(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
下面是一個使用例子:
>>> s = { 'name': 'GOOG', 'shares': 100, 'price':490.1 }
>>> e = dict_to_xml('stock', s)
>>> e
<Element 'stock' at 0x1004b64c8>
>>>
轉(zhuǎn)換結(jié)果是一個 Element
實例。對于 I/O 操作,使用 xml.etree.ElementTree
中的 tostring()
函數(shù)很容易就能將它轉(zhuǎn)換成一個字節(jié)字符串。例如:
>>> from xml.etree.ElementTree import tostring
>>> tostring(e)
b'<stock><price>490.1</price><shares>100</shares><name>GOOG</name></stock>'
>>>
如果你想給某個元素添加屬性值,可以使用 set()
方法:
>>> e.set('_id','1234')
>>> tostring(e)
b'<stock _id="1234"><price>490.1</price><shares>100</shares><name>GOOG</name>
</stock>'
>>>
如果你還想保持元素的順序,可以考慮構(gòu)造一個 OrderedDict
來代替一個普通的字典。請參考1.7小節(jié)。
當創(chuàng)建 XML 的時候,你被限制只能構(gòu)造字符串類型的值。例如:
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)
問題是如果你手動的去構(gòu)造的時候可能會碰到一些麻煩。例如,當字典的值中包含一些特殊字符的時候會怎樣呢?
>>> 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>'
>>>
注意到程序的后面那個例子中,字符 ‘<’ 和 ‘>’ 被替換成了 <
和>
下面僅供參考,如果你需要手動去轉(zhuǎn)換這些字符, 可以使用 xml.sax.saxutils
中的 escape()
和 unescape()
函數(shù)。例如:
>>> from xml.sax.saxutils import escape, unescape
>>> escape('<spam>')
'<spam>'
>>> unescape(_)
'<spam>'
>>>
除了能創(chuàng)建正確的輸出外,還有另外一個原因推薦你創(chuàng)建 Element
實例而不是字符串, 那就是使用字符串組合構(gòu)造一個更大的文檔并不是那么容易。 而 Element
實例可以不用考慮解析 XML 文本的情況下通過多種方式被處理。 也就是說,你可以在一個高級數(shù)據(jù)結(jié)構(gòu)上完成你所有的操作,并在最后以字符串的形式將其輸出。
你想讀取一個 XML 文檔,對它最一些修改,然后將結(jié)果寫回 XML 文檔。
使用 xml.etree.ElementTree
模塊可以很容易的處理這些任務(wù)。 第一步是以通常的方式來解析這個文檔。例如,假設(shè)你有一個名為 pred.xml
的文檔,類似下面這樣:
下面是一個利用 ElementTree
來讀取這個文檔并對它做一些修改的例子:
>>> 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é)果是一個像下面這樣新的 XML 文件:
修改一個 XML 文檔結(jié)構(gòu)是很容易的,但是你必須牢記的是所有的修改都是針對父節(jié)點元素, 將它作為一個列表來處理。例如,如果你刪除某個元素,通過調(diào)用父節(jié)點的 remove()
方法從它的直接父節(jié)點中刪除。 如果你插入或增加新的元素,你同樣使用父節(jié)點元素的insert()
和 append()
方法。 還能對元素使用索引和切片操作,比如element[i]
或 element[i:j]
如果你需要創(chuàng)建新的元素,可以使用本節(jié)方案中演示的Element
類。我們在6.5小節(jié)已經(jīng)詳細討論過了。
你想解析某個 XML 文檔,文檔中使用了 XML 命名空間。
考慮下面這個使用了命名空間的文檔:
如果你解析這個文檔并執(zhí)行普通的查詢,你會發(fā)現(xiàn)這個并不是那么容易,因為所有步驟都變得相當?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'
>>>
你可以通過將命名空間處理邏輯包裝為一個工具類來簡化這個過程:
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)
通過下面的方式使用這個類:
>>> 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 文檔會比較繁瑣。 上面的 XMLNamespaces
僅僅是允許你使用縮略名代替完整的 URI 將其變得稍微簡潔一點。
很不幸的是,在基本的 ElementTree
解析中沒有任何途徑獲取命名空間的信息。 但是,如果你使用 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>
>>>
最后一點,如果你要處理的 XML 文本除了要使用到其他高級 XML 特性外,還要使用到命名空間, 建議你最好是使用 lxml
函數(shù)庫來代替 ElementTree
。 例如,lxml
對利用 DTD 驗證文檔、更好的 XPath 支持和一些其他高級 XML 特性等都提供了更好的支持。 這一小節(jié)其實只是教你如何讓 XML 解析稍微簡單一點。
你想在關(guān)系型數(shù)據(jù)庫中查詢、增加或刪除記錄。
Python 中表示多行數(shù)據(jù)的標準方式是一個由元組構(gòu)成的序列。例如:
stocks = [
('GOOG', 100, 490.1),
('AAPL', 50, 545.75),
('FB', 150, 7.45),
('HPQ', 75, 33.2),
]
依據(jù) PEP249,通過這種形式提供數(shù)據(jù), 可以很容易的使用 Python 標準數(shù)據(jù)庫 API 和關(guān)系型數(shù)據(jù)庫進行交互。 所有數(shù)據(jù)庫上的操作都通過 SQL 查詢語句來完成。每一行輸入輸出數(shù)據(jù)用一個元組來表示。
為了演示說明,你可以使用 Python 標準庫中的 sqlite3
模塊。 如果你使用的是一個不同的數(shù)據(jù)庫(比如 MySql、Postgresql 或者 ODBC), 還得安裝相應(yīng)的第三方模塊來提供支持。 不過相應(yīng)的編程接口幾乎都是一樣的,除了一點點細微差別外。
第一步是連接到數(shù)據(jù)庫。通常你要執(zhí)行 connect()
函數(shù), 給它提供一些數(shù)據(jù)庫名、主機、用戶名、密碼和其他必要的一些參數(shù)。例如:
>>> import sqlite3
>>> db = sqlite3.connect('database.db')
>>>
為了處理數(shù)據(jù),下一步你需要創(chuàng)建一個游標。 一旦你有了游標,那么你就可以執(zhí)行 SQL 查詢語句了。比如:
>>> c = db.cursor()
>>> c.execute('create table portfolio (symbol text, shares integer, price real)')
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>
為了向數(shù)據(jù)庫表中插入多條記錄,使用類似下面這樣的語句:
>>> c.executemany('insert into portfolio values (?,?,?)', stocks)
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>
為了執(zhí)行某個查詢,使用像下面這樣的語句:
>>> 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ù)來執(zhí)行查詢操作,必須確保你使用下面這樣的占位符?
來進行引用參數(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)
>>>
在比較低的級別上和數(shù)據(jù)庫交互是非常簡單的。 你只需提供 SQL 語句并調(diào)用相應(yīng)的模塊就可以更新或提取數(shù)據(jù)了。 雖說如此,還是有一些比較棘手的細節(jié)問題需要你逐個列出去解決。
一個難點是數(shù)據(jù)庫中的數(shù)據(jù)和 Python 類型直接的映射。 對于日期類型,通??梢允褂?datetime
模塊中的 datetime
實例, 或者可能是 time
模塊中的系統(tǒng)時間戳。 對于數(shù)字類型,特別是使用到小數(shù)的金融數(shù)據(jù),可以用 decimal
模塊中的 Decimal
實例來表示。 不幸的是,對于不同的數(shù)據(jù)庫而言具體映射規(guī)則是不一樣的,你必須參考相應(yīng)的文檔。
另外一個更加復雜的問題就是 SQL 語句字符串的構(gòu)造。 你千萬不要使用 Python 字符串格式化操作符(如%)或者 .format()
方法來創(chuàng)建這樣的字符串。 如果傳遞給這些格式化操作符的值來自于用戶的輸入,那么你的程序就很有可能遭受 SQL 注入攻擊(參考 http://xkcd.com/327 )。 查詢語句中的通配符 ?
指示后臺數(shù)據(jù)庫使用它自己的字符串替換機制,這樣更加的安全。
不幸的是,不同的數(shù)據(jù)庫后臺對于通配符的使用是不一樣的。大部分模塊使用 ?
或 %s
, 還有其他一些使用了不同的符號,比如:0或:1來指示參數(shù)。 同樣的,你還是得去參考你使用的數(shù)據(jù)庫模塊相應(yīng)的文檔。 一個數(shù)據(jù)庫模塊的 paramstyle
屬性包含了參數(shù)引用風格的信息。
對于簡單的數(shù)據(jù)庫數(shù)據(jù)的讀寫問題,使用數(shù)據(jù)庫 API 通常非常簡單。 如果你要處理更加復雜的問題,建議你使用更加高級的接口,比如一個對象關(guān)系映射 ORM 所提供的接口。 類似 SQLAlchemy
這樣的庫允許你使用 Python 類來表示一個數(shù)據(jù)庫表, 并且能在隱藏底層 SQL 的情況下實現(xiàn)各種數(shù)據(jù)庫的操作。
你想將一個十六進制字符串解碼成一個字節(jié)字符串或者將一個字節(jié)字符串編碼成一個十六進制字符串。
如果你只是簡單的解碼或編碼一個十六進制的原始字符串,可以使用 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'
>>>
類似的功能同樣可以在 base64
模塊中找到。例如:
>>> import base64
>>> h = base64.b16encode(s)
>>> h
b'68656C6C6F'
>>> base64.b16decode(h)
b'hello'
>>>
大部分情況下,通過使用上述的函數(shù)來轉(zhuǎn)換十六進制是很簡單的。 上面兩種技術(shù)的主要不同在于大小寫的處理。 函數(shù) base64.b16decode()
和 base64.b16encode()
只能操作大寫形式的十六進制字母, 而 binascii
模塊中的函數(shù)大小寫都能處理。
還有一點需要注意的是編碼函數(shù)所產(chǎn)生的輸出總是一個字節(jié)字符串。 如果想強制以 Unicode 形式輸出,你需要增加一個額外的界面步驟。例如:
>>> h = base64.b16encode(s)
>>> print(h)
b'68656C6C6F'
>>> print(h.decode('ascii'))
68656C6C6F
>>>
在解碼十六進制數(shù)時,函數(shù) b16decode()
和 a2b_hex()
可以接受字節(jié)或 unicode 字符串。 但是,unicode 字符串必須僅僅只包含 ASCII 編碼的十六進制數(shù)。
你需要使用 Base64 格式解碼或編碼二進制數(shù)據(jù)。
base64
模塊中有兩個函數(shù)b64encode()
and b64decode()
可以幫你解決這個問題。例如;
>>> # 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é)果總是一個字節(jié)字符串。 如果你想混合使用 Base64 編碼的數(shù)據(jù)和 Unicode 文本,你必須添加一個額外的解碼步驟。例如:
>>> a = base64.b64encode(s).decode('ascii')
>>> a
'aGVsbG8='
>>>
當解碼 Base64 的時候,字節(jié)字符串和 Unicode 文本都可以作為參數(shù)。 但是,Unicode 字符串只能包含 ASCII 字符。
你想讀寫一個二進制數(shù)組的結(jié)構(gòu)化數(shù)據(jù)到 Python 元組中。
解決方案
可以使用 struct
模塊處理二進制數(shù)據(jù)。 下面是一段示例代碼將一個 Python 元組列表寫入一個二進制文件,并使用 struct
將每個元組編碼為一個結(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)
有很多種方法來讀取這個文件并返回一個元組列表。 首先,如果你打算以塊的形式增量讀取文件,你可以這樣做:
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