Python CookBook 学习笔记
第六章 数据编码和处理¶
这一章主要讨论使用Python 处理各种不同方式编码的数据,比如CSV 文件,JSON,XML 和二进制包装记录。和数据结构那一章不同的是,这章不会讨论特殊的算法问题,而是关注于怎样获取和存储这些格式的数据。
比较重要的小节:
- 6.12
6.9 编码和解码十六进制数¶
问题:你想将一个十六进制字符串解码成一个字节字符串或者将一个字节字符串编码成一个十六进制字符串。
解决方案:如果你只是简单的解码或编码一个十六进制的原始字符串,可以使用 binascii 模块。
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import binascii
h = b'68656c6c6f'
# Decode back to bytes
s = binascii.a2b_hex(h)
s
类似的功能同样可以在 base64 模块中找到。
import base64
h = base64.b16encode(s)
h
base64.b16decode(h)
讨论:大部分情况下,通过使用上述的函数来转换十六进制是很简单的。上面两种技术的主要不同在于大小写的处理。 函数 base64.b16decode() 和 base64.b16encode() 只能操作大写形式的十六进制字母,而 binascii 模块中的函数大小写都能处理。编码函数所产生的输出总是一个字节字符串。
在解码十六进制数时,函数 b16decode() 和 a2b_hex() 可以接受字节或 unicode 字符串。如果想强制 unicode 输出
print(h)
print(h.decode('ascii'))
6.10 编码解码Base64数据¶
问题:你需要使用 Base64 格式解码或编码二进制数据。
注:Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2^6=64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节可表示 4 个可打印字符。它可用来作为电子邮件的传输编码
解决方案:base64 模块中有两个函数 b64encode() and b64decode() 可以帮你解决这个问题。
# Some byte data
s = b'hello'
# Encode as Base64
a = base64.b64encode(s)
a
# Decode from Base64
base64.b64decode(a)
讨论:Base64编码仅仅用于面向字节的数据比如字节字符串和字节数组。 此外,编码处理的输出结果总是一个字节字符串。 如果你想混合使用Base64编码的数据和Unicode文本,你必须添加一个额外的解码步骤 decode('ascii')
。
当解码 Base64 的时候,字节字符串和 Unicode 文本都可以作为参数。 但是,Unicode 字符串只能包含 ASCII 字符。
import struct
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))
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)
def unpack_records(format, data):
'''
全部读取,按大小 offset 取数据并解析
'''
record_struct = Struct(format)
return (record_struct.unpack_from(data, offset) for offset in range(0, len(data), record_struct.size))
# Example
if __name__ == '__main__':
records = [ (1, 2.3, 4.5),
(6, 7.8, 9.0),
(12, 13.4, 56.7) ]
with open('data/data.b', 'wb') as f:
write_records(records, '<idd', f)
with open('data/data.b','rb') as f:
for rec in read_records('<idd', f):
print(rec)
with open('data/data.b', 'rb') as f:
data = f.read()
for rec in unpack_records('<idd', data):
print(rec)
讨论:struct 的声明
# Little endian 32-bit integer, two double precision floats
record_struct = Struct('<idd') # i, d, f 等表明 32 位整数,64 位浮点数,32 位浮点数,< 指定了字节顺序,“低位在前”
# 用 Struct 对象调用,比较方便
record_struct = Struct('<idd')
record_struct.size
temp = record_struct.pack(1, 2.0, 3.0)
record_struct.unpack(temp)
# 直接调用
temp = struct.pack('<idd', 1, 2.0, 3.0)
struct.unpack('<idd', temp)
前面有个 iter 的技巧,可以减少代码量和提高程序性能,见 5.18,另外一种方法 unpack_from() 对于从一个大型二进制数组中提取二进制数据非常有用,它不会产生任何的临时对象或者进行内存复制操作,需要给它一个字节序列和一个字节偏移量。也可以利用 namedtuple 进行一些包装
from collections import namedtuple
Record = namedtuple('Record', ['kind','x','y'])
with open('data/data.b', 'rb') as f:
records = (Record(*r) for r in read_records('<idd', f))
for r in records:
print(r.kind, r.x, r.y)
进阶 NumPy
import numpy as np
f = open('data/data.b', 'rb')
records = np.fromfile(f, dtype='<i,<d,<d')
records
records[0]
records[1]
总结:struct 模块的时候和一个 iter 技巧,另外用 NumPy 是个不错的选择
6.12 读取嵌套和可变长二进制数据¶
问题:你需要读取包含嵌套或者可变长记录集合的复杂二进制格式的数据。这些数据可能包含图片、视频、电子地图文件等。
解决方案:struct 模块可被用来编码/解码几乎所有类型的二进制的数据结构。为了解释清楚这种数据,假设你用下面的Python 数据结构来表示一个组成一系列多边形的点的集合
import struct
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 多边形
polys = [
[ (1.0, 2.5), (3.5, 4.0), (2.5, 1.5) ],
[ (7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0) ],
[ (3.4, 6.3), (1.2, 0.5), (4.6, 9.2) ],
]
需要把这些多边形写入如下格式的文件中
+------+--------+------------------------------------+
|Byte | Type | Description |
+======+========+====================================+
|0 | int | 文件代码(0x1234,小端) |
+------+--------+------------------------------------+
|4 | double | x 的最小值(小端) |
+------+--------+------------------------------------+
|12 | double | y 的最小值(小端) |
+------+--------+------------------------------------+
|20 | double | x 的最大值(小端) |
+------+--------+------------------------------------+
|28 | double | y 的最大值(小端) |
+------+--------+------------------------------------+
|36 | int | 三角形数量(小端) |
+------+--------+------------------------------------+
紧跟着头部是一系列的多边形记录,编码格式如下
+------+--------+-------------------------------------------+
|Byte | Type | Description |
+======+========+===========================================+
|0 | int | 记录长度(N 字节) |
+------+--------+-------------------------------------------+
|4 | Points | (X,Y) 坐标,以浮点数表示|
+------+--------+-------------------------------------------+
要写入这样的文件,可以使用如下代码:
import struct
import itertools
def write_polys(filename, polys):
# Determine bounding box
flattened = list(itertools.chain(*polys))
min_x = min(x for x, y in flattened)
max_x = max(x for x, y in flattened)
min_y = min(y for x, y in flattened)
max_y = max(y for x, y in flattened)
with open(filename, 'wb') as f:
f.write(struct.pack('<iddddi', 0x1234,
min_x, min_y,
max_x, max_y,
len(polys)))
for poly in polys:
size = len(poly) * struct.calcsize('<dd')
f.write(struct.pack('<i', size + 4))
for pt in poly:
f.write(struct.pack('<dd', *pt))
write_polys('data/polys.bin', polys)
为了把文件读取回来,最初级的方法如下,基本上就是以上代码的逆操作
def read_polys(filename):
with open(filename, 'rb') as f:
# Read the header
header = f.read(40)
file_code, min_x, min_y, max_x, max_y, num_polys = struct.unpack('<iddddi', header)
polys = []
for n in range(num_polys):
pbytes, = struct.unpack('<i', f.read(4))
poly = []
for m in range(pbytes // 16):
pt = struct.unpack('<dd', f.read(16))
poly.append(pt)
polys.append(poly)
return polys
read_polys('data/polys.bin')
上面的版本尽管可以工作,但是过于繁琐,我们可以使用类进行简化
import struct
class StructField:
'''
Descriptor representing a simple structure field
'''
def __init__(self, format, offset):
self.format = format
self.offset = offset
def __get__(self, instance, cls):
if instance is None:
return self
else:
r = struct.unpack_from(self.format, instance._buffer, self.offset)
return r[0] if len(r) == 1 else r # 如果是单个数字则返回数字否则返回 tuple
class Structure:
def __init__(self, bytedata):
self._buffer = memoryview(bytedata)
class PolyHeader(Structure):
file_code = StructField('<i', 0)
min_x = StructField('<d', 4)
min_y = StructField('<d', 12)
max_x = StructField('<d', 20)
max_y = StructField('<d', 28)
num_polys = StructField('<i', 36)
使用了一个描述器来表示每个结构字段,每个描述器包含一个结构兼容格式的代码以及一个字节偏移量,存储在内部的内存缓冲中。在 get() 方法中,struct.unpack_from() 从缓冲中解包一个值,省去了额外的分片或复制操作步骤。
Structure 类是一个基础类,接受字节数据并存储在内部的内存缓冲中,并被 StructField 描述器使用。
这样可以定义一个高层次的结构对象来表示上面表格信息所期望的文件格式
f = open('data/polys.bin', 'rb')
phead = PolyHeader(f.read(40))
phead.file_code # 具体来说调用的是 phead.file_code.__get__(phead, PolyHeader)
phead.min_x
上面的代码已经很好了,但还是有些臃肿,这时可以考虑使用元类或者类装饰器来避免重复的代码,改造下 Structure 类
class StructureMeta(type):
'''
Metaclass that automatically creates StructField descriptors
'''
def __init__(self, clsname, bases, clsdict):
fields = getattr(self, '_fields_', [])
byte_order = ''
offset = 0
for format, fieldname in fields:
if format.startswith(('<','>','!','@')):
byte_order = format[0]
format = format[1:]
format = byte_order + format
setattr(self, fieldname, StructField(format, offset))
offset += struct.calcsize(format)
setattr(self, 'struct_size', offset) # 增加一个 struct_size 属性
class Structure(metaclass=StructureMeta):
def __init__(self, bytedata):
self._buffer = bytedata
@classmethod
def from_file(cls, f):
return cls(f.read(cls.struct_size))
# 可以这样定义一个数据结构
class PolyHeader(Structure):
_fields_ = [
('<i', 'file_code'),
('d', 'min_x'),
('d', 'min_y'),
('d', 'max_x'),
('d', 'max_y'),
('i', 'num_polys')
]
类方法 from_file() 让我们在不需要知道任何数据的大小和结构的情况下就能轻松的从文件中读取数据
f = open('data/polys.bin', 'rb')
phead = PolyHeader.from_file(f)
phead.file_code
改进元类以支持嵌套的字节结构
class NestedStruct:
'''
Descriptor representing a nested structure
'''
def __init__(self, name, struct_type, offset):
self.name = name
self.struct_type = struct_type
self.offset = offset
def __get__(self, instance, cls):
if instance is None:
return self
else:
data = instance._buffer[self.offset:self.offset+self.struct_type.struct_size] # 对 memory view 进行切片
result = self.struct_type(data)
# Save resulting structure back on instance to avoid
# further recomputation of this step
setattr(instance, self.name, result) # 保存结果 防止重复计算 下次再取用的时候就不再调用 装饰器 而是直接从类属性中获取
#print(instance, self.name, result)
return result
class StructureMeta(type):
'''
Metaclass that automatically creates StructField descriptors
'''
def __init__(self, clsname, bases, clsdict):
fields = getattr(self, '_fields_', [])
byte_order = ''
offset = 0
for format, fieldname in fields:
# 如果 fields 中的对象本身也是个继承自 Structure 的计数据结构
if isinstance(format, StructureMeta):
setattr(self, fieldname,
NestedStruct(fieldname, format, offset))
offset += format.struct_size
else:
if format.startswith(('<','>','!','@')):
byte_order = format[0]
format = format[1:]
format = byte_order + format
setattr(self, fieldname, StructField(format, offset))
offset += struct.calcsize(format)
setattr(self, 'struct_size', offset)
class Structure(metaclass=StructureMeta):
def __init__(self, bytedata):
self._buffer = bytedata
@classmethod
def from_file(cls, f):
return cls(f.read(cls.struct_size))
在这段代码中,NestedStruct 描述器被用来叠加另外一个定义在某个内存区域上的结构。 它通过将原始内存缓冲进行切片操作后实例化给定的结构类型。由于底层的内存缓冲区是通过一个内存视图初始化的, 所以这种切片操作不会引发任何的额外的内存复制。相反,它仅仅就是之前的内存的一个叠加而已。 另外,为了防止重复实例化,通过使用和8.10小节同样的技术,描述器保存了该实例中的内部结构对象
class Point(Structure):
_fields_ = [
('<d', 'x'),
('d', 'y')
]
class PolyHeader(Structure):
_fields_ = [
('<i', 'file_code'),
(Point, 'min'), # nested struct
(Point, 'max'), # nested struct
('i', 'num_polys')
]
f = open('data/polys.bin', 'rb')
phead = PolyHeader.from_file(f)
phead.file_code
phead.min.x
以上情形中数据都是定长的,如果是变长的呢?一种方案是写一个类来表示字节数据,同时写一个工具函数来通过多少方式解析内容。跟 6.11 小节的代码很类似
class SizedRecord:
def __init__(self, bytedata):
self._buffer = memoryview(bytedata)
@classmethod
def from_file(cls, f, size_fmt, includes_size=True):
sz_nbytes = struct.calcsize(size_fmt)
sz_bytes = f.read(sz_nbytes)
sz, = struct.unpack(size_fmt, sz_bytes)
buf = f.read(sz - includes_size * sz_nbytes)
return cls(buf) # 重新返回一个 SizeRecord 类
def iter_as(self, code):
# 分两种情况,一种是 str format 另一种是 继承自 Structure 的数据结构
if isinstance(code, str):
s = struct.Struct(code)
for off in range(0, len(self._buffer), s.size):
yield s.unpack_from(self._buffer, off)
elif isinstance(code, StructureMeta):
size = code.struct_size
for off in range(0, len(self._buffer), size):
data = self._buffer[off:off+size]
yield code(data)
类方法 SizedRecord.from_file() 是一个工具,用来从一个文件中读取带大小前缀的数据块, 这也是很多文件格式常用的方式。作为输入,它接受一个包含大小编码的结构格式编码,并且也是自己形式。 可选的 includes_size 参数指定了字节数是否包含头部大小。使用 iter_as() 方法来解析数据,这个方法接受一个结构格式化编码或者是 Structure 类作为输入
f = open('data/polys.bin', 'rb')
phead = PolyHeader.from_file(f)
phead.num_polys
polydata = [ SizedRecord.from_file(f, '<i') for n in range(phead.num_polys) ]
polydata
for n, poly in enumerate(polydata):
print('Polygon', n)
for p in poly.iter_as('<dd'):
print(p)
最终修正版本
class Point(Structure):
_fields_ = [
('<d', 'x'),
('d', 'y')
]
class PolyHeader(Structure):
_fields_ = [
('<i', 'file_code'),
(Point, 'min'),
(Point, 'max'),
('i', 'num_polys')
]
def read_polys(filename):
polys = []
with open(filename, 'rb') as f:
phead = PolyHeader.from_file(f)
for n in range(phead.num_polys):
rec = SizedRecord.from_file(f, '<i')
poly = [ (p.x, p.y) for p in rec.iter_as(Point) ]
polys.append(poly)
return polys
read_polys('data/Polys.bin')
上面的实现的一个主要特征是它是基于懒解包的思想。当一个 Structure 实例被创建时, init() 仅仅只是创建一个字节数据的内存视图,没有做其他任何事。 特别的,这时候并没有任何的解包或者其他与结构相关的操作发生。 这样做的一个动机是你可能仅仅只对一个字节记录的某一小部分感兴趣。我们只需要解包你需要访问的部分,而不是整个文件。
为了实现懒解包和打包,需要使用 StructField 描述器类。 用户在 fields 中列出来的每个属性都会被转化成一个 StructField 描述器, 它将相关结构格式码和偏移值保存到存储缓存中。元类 StructureMeta 在多个结构类被定义时自动创建了这些描述器。 我们使用元类的一个主要原因是它使得用户非常方便的通过一个高层描述就能指定结构格式,而无需考虑低层的细节问题。
StructureMeta 的一个很微妙的地方就是它会固定字节数据顺序。 也就是说,如果任意的属性指定了一个字节顺序(<表示低位优先 或者 >表示高位优先), 那后面所有字段的顺序都以这个顺序为准。这么做可以帮助避免额外输入,但是在定义的中间我们仍然可能切换顺序的。 比如,你可能有一些比较复杂的结构,就像下面这样:
class ShapeFile(Structure):
_fields_ = [ ('>i', 'file_code'), # Big endian
('20s', 'unused'),
('i', 'file_length'),
('<i', 'version'), # Little endian
('i', 'shape_type'),
('d', 'min_x'),
('d', 'min_y'),
('d', 'max_x'),
('d', 'max_y'),
('d', 'min_z'),
('d', 'max_z'),
('d', 'min_m'),
('d', 'max_m') ]
memoryview() 的使用可以帮助我们避免内存的复制。 当结构存在嵌套的时候,memoryviews 可以叠加同一内存区域上定义的机构的不同部分。 这个特性比较微妙,但是它关注的是内存视图与普通字节数组的切片操作行为。 如果你在一个字节字符串或字节数组上执行切片操作,你通常会得到一个数据的拷贝。 而内存视图切片不是这样的,它仅仅是在已存在的内存上面叠加而已。因此,这种方式更加高效。
还有很多相关的章节可以帮助我们扩展这里讨论的方案。参考 8.13 小节使用描述器构建一个类型系统。8.10 小节有更多关于延迟计算属性值的讨论,并且跟 NestedStruct 描述器的实现也有关。9.19 小节有一个使用元类来初始化类成员的例子,和StructureMeta 类非常相似。Python 的ctypes 源码同样也很有趣,它提供了对定义数据结构、数据结构嵌套这些相似功能的支持。
6.13 数据的累加与统计操作¶
问题:你需要处理一个很大的数据集并需要计算数据总和或其他统计量。
解决方案:对于任何涉及到统计、时间序列以及其他相关技术的数据分析问题,都可以考虑使用 Pandas库。
为了让你先体验下,下面是一个使用Pandas来分析芝加哥城市的 老鼠和啮齿类动物数据库 的例子。 在我写这篇文章的时候,这个数据库是一个拥有大概 74,000 行数据的 CSV 文件。
>>> import pandas
>>> # Read a CSV file, skipping last line
>>> rats = pandas.read_csv('rats.csv', skip_footer=1)
>>> # Investigate range of values for a certain field
>>> rats['Current Activity'].unique()
array([nan, Dispatch Crew, Request Sanitation Inspector], dtype=object)
>>> # Filter the data
>>> crew_dispatched = rats[rats['Current Activity'] == 'Dispatch Crew']
>>> len(crew_dispatched)
65676
>>> # Find 10 most rat-infested ZIP codes in Chicago
>>> crew_dispatched['ZIP Code'].value_counts()[:10]
60647 3837
60618 3530
60614 3284
60629 3251
60636 2801
60657 2465
60641 2238
60609 2206
60651 2152
60632 2071
>>> # Group by completion date
>>> dates = crew_dispatched.groupby('Completion Date')
<pandas.core.groupby.DataFrameGroupBy object at 0x10d0a2a10>
>>> len(dates)
472
>>> # Determine counts on each day
>>> date_counts = dates.size()
>>> date_counts[0:10]
Completion Date
01/03/2011 4
01/03/2012 125
01/04/2011 54
01/04/2012 38
01/05/2011 78
01/05/2012 100
01/06/2011 100
01/06/2012 58
01/07/2011 1
01/09/2012 12
>>> # Sort the counts
>>> date_counts.sort()
>>> date_counts[-10:]
Completion Date
10/12/2012 313
10/21/2011 314
09/20/2011 316
10/26/2011 319
02/22/2011 325
10/26/2012 333
03/17/2011 336
10/13/2011 378
10/14/2011 391
10/07/2011 457