鍍金池/ 教程/ Python/ 數(shù)據(jù)編碼和處理
類(lèi)與對(duì)象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測(cè)試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語(yǔ)言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

數(shù)據(jù)編碼和處理

這一章主要討論使用 Python 處理各種不同方式編碼的數(shù)據(jù),比如 CSV 文件,JSON,XML 和二進(jìn)制包裝記錄。 和數(shù)據(jù)結(jié)構(gòu)那一章不同的是,這章不會(huì)討論特殊的算法問(wèn)題,而是關(guān)注于怎樣獲取和存儲(chǔ)這些格式的數(shù)據(jù)。

讀寫(xiě) CSV 數(shù)據(jù)

問(wèn)題

你想讀寫(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.Symbolrow.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 數(shù)據(jù)

問(wèn)題

你想讀寫(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)型為 Nonebool ,int , floatstr, 以及包含這些類(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é)。

解析簡(jiǎn)單的 XML 數(shù)據(jù)

問(wèn)題

你想從一個(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/itemtitle。

每次指定某個(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 等特性。

增量式解析大型 XML 文件

問(wèn)題

你想使用盡可能少的內(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-nsend-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-nsend-ns 事件被用來(lái)處理 XML 文檔命名空間的聲明。

這本節(jié)例子中,startend事件被用來(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)存使用量的話,那么增量式的版本完勝。

將字典轉(zhuǎn)換為 XML

問(wèn)題

你想使用一個(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>&lt;spam&gt;</name></item>'
>>>

注意到程序的后面那個(gè)例子中,字符 ‘<’ 和 ‘>’ 被替換成了 &lt;&gt;

下面僅供參考,如果你需要手動(dòng)去轉(zhuǎn)換這些字符, 可以使用 xml.sax.saxutils中的 escape()unescape() 函數(shù)。例如:

>>> from xml.sax.saxutils import escape, unescape
>>> escape('<spam>')
'&lt;spam&gt;'
>>> unescape(_)
'<spam>'
>>>

除了能創(chuàng)建正確的輸出外,還有另外一個(gè)原因推薦你創(chuàng)建 Element實(shí)例而不是字符串, 那就是使用字符串組合構(gòu)造一個(gè)更大的文檔并不是那么容易。 而 Element實(shí)例可以不用考慮解析 XML 文本的情況下通過(guò)多種方式被處理。 也就是說(shuō),你可以在一個(gè)高級(jí)數(shù)據(jù)結(jié)構(gòu)上完成你所有的操作,并在最后以字符串的形式將其輸出。

解析和修改 XML

問(wèn)題

你想讀取一個(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ò)了。

利用命名空間解析 XML 文檔

問(wèn)題

你想解析某個(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ù)的交互

問(wè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ù)的操作。

編碼和解碼十六進(jìn)制數(shù)

問(wèn)題

你想將一個(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 數(shù)據(jù)

問(wèn)題

你需要使用 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ě)二進(jìn)制數(shù)組數(shù)據(jù)

問(wèn)題

你想讀寫(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