第十二章 存储数据和对象
12.1数据存储概述
12.1.1文本与二进制对比
文本格式易于阅读和调试,跨平台性能好。二进制格式占用空间小,适于按记录存取。
12.1.2压缩
假如对象的大小开始成为问题,或者需要在网络上传输。
12.1.3字节次序(Endianness)
处理器把多字节数字存放在内存中可以是big-endian(低端优先),也可以是little-endian(高端优先):
>>> import sys
>>> print '"... %s-endian",Gulliver said.' % sys.byteorder
"...little-endian",Gulliver said. #在Intel的机器上
大多数Python程序不需要关心这些细节。但如果需要在平台之间传递数据则要考虑。
12.1.4对象状态
某些状态已经不在Python可以控制的范围之内了,比如一个Socket就不需要保存。
12.1.5目的地
磁盘文件、数据库、网络
12.1.6在接收端
可能不仅仅是Python的,可能有C等。用XDR、XML等。
12.2加载并保存对象
保存时需要先转换成一个可以被读回的字节串,对应Java中的marshaling或C++中的serialization,在Python中称为pickling。
12.2.1采用pickle进行转换
pickle模块把很多Python对象转换为字节表示法,或从字节表示法转换回来:
>>> import pickle
>>> stuff=[5,3.5,'Alfred']
>>> pstuff=pickle.dumps(stuff)
>>> pstuff
"(lp0/01215/012aF3.5/012aS/Alfred)/012pl/012a."
>>> pickle.loads(pstuff)
[5,3.5,'Alfred']
上面的pstuff是一个字节串,便于传输了。
pickle.dumps(object[,bin])函数返回对象的串接格式,而pickle.dump(object,file[,bin])把串接格式发给已打开的、类似文件的对象。bin参数默认为0,按照文本格式转换对象,当为1时按照紧凑但难懂的二进制格式保存。每种格式都是跨平台的。
pickle.loads(str)函数将从字节串构造对象。pickle.load(file)从类似文件的对象读取已转换格式的对象,并返回原始的,未转换格式的对象。
下例:
>>> s=StringIO.StringIO() #创建临时文件对象
>>> p=pickle.Pickler(s,1) #二进制类型串行化
>>> p.dump([1,2,3])
>>> p.dump('Hello!')
>>> s.getvalue() #检查pickel格式
'[q/000(K/001K/002K/0C3e.0/006Hello!/q001.'
>>> s.seek(0) #重设文件指针到开始位置
>>> u=pickle.Uppickler(s)
>>> u.load()
[1,2,3]
>>> u.load()
'Hello!'
对于同一类型的连续存储也可以很好的区分。如果需要转换多种格式,(by gashero)或者传递pickler对象,则使用Pickler和Unpickler类是很方便的。也可以细分来定制pickler。
cPickle模块是C语言实现的pickle模块,比纯Python模块速度快几个数量级,且格式完全兼容,但是不可以再细分Pickler和Unpickler。
>>> import cPickle,pickle
>>> s=cPickle.dumps(('one':1,'two':2))
>>> pickle.loads(s)
['one':1,'two':2]
由于升级,各个版本之间的格式可能不同,但是pickle模块是可以自动识别出隐藏再数据中的版本号的,并自动处理。
>>> pickle.format_version
'1.3'
>>> pickle.compatible_formats
['1.0','1.1','1.2'] #能够正确读取的旧版本
试图读取一个unpickle不支持的版本会产生异常。
允许转换的数据类型
数字、字符串、None、包含"可转换格式"对象的包容器(元组、列表、词典)。
当转换一个内置函数、自己的函数或类的对象时,pickle会把数据和类型名和类型所属的模块名一起存储,但是不会存储类型的实现。unpickle恢复对象时,pickle首先导入原属的模块,因此转换一定要在该模块的最上层定义函数或类。
保存实例对象,用pickle.__getstate__方法,返回对象的状态。Python加载对象时,pickle自动按照存储中的类型创建新对象,并调用对象的__setstate__方法,传递一个元组参数来恢复对象状态。
class Point:
def __init__(self,x,y):
self.x=x;self.y=y
def __str__(self):
return '(%d,%d)' % (self.x,self.y)
def __getstate__(self):
print "正在备份对象状态"
return (self.x,self.y)
def __setstate__(self,state):
print "正在恢复对象状态"
self.x,self.y=state
p=Point(10,20)
z=pickle.dumps(p) #正在备份对象状态
newp=pickle.loads(z) #正在恢复对象状态
print newp #(10,20)
如果对象没有__getstate__成员,则pickle保存__dict__的内容。unpickle对象时,load函数通常不会调用对象的构造函数(__init__),如果确实需要,则可用__getinitargs__方法,再重新加载这个对象时,会把参量传递给__init__。
使用copy_reg模块,可以为C扩展模块中的数据类型添加转换格式的支持。要添加这种支持,调用copy_reg.pickle(type, reduction_func [,constructor_ob]),来为给定的类型注册一个缩减函数和一个构造函数。缩减函数reduction_func获取一个对象,返回二元组,第一个元素是该对象的构造函数,第二个元素也是一个元组,是构造函数的参数列表。在为新类型注册了自己的函数之后,任意串接的此类型对象都可以使用。
其他的转换格式问题
格式转换实例时不会存储类实现,所以可以在确保不破坏已经存储的格式数据情况下更改类的定义,设置恢复以前保存的实例对象。
存储和恢复类实例时,对其他对象的引用也是保存的,比如下例。对同一个列表z的引用在恢复之后仍然有效。
>>> z=[1,2,3]
>>> y=[z,z]
>>> y[0] is y[1] #两个引用相同,是同一对象
1
>>> s=pickle.dumps(y)
>>> x=pickle.loads(s)
>>> x
[[1,2,3],[1,2,3]]
>>> x[0] is x[1] #两个成员还是引用同一对象
1
注意:转换一个对象之后,修改它,之后再转换,则只保存此对象的第一个实例版本。
如果转换到一个类似文件的对象时发生了错误,则产生PickImgError异常。已经写入文件的内容是无法预测的,而且不再有用。
12.2.2marshal模块
pickle模块的实现中调用marshal完成一些工作。marshal的一个优点(pickle不具备)是可以处理代码对象的实现(比如函数)。如下是例子:
>>> def adder(a,b):
... return a+b
>>> adder(10,2)
12
>>> import marshal
>>> s=marshal.dumps(adder.func_code)
>>> def newadder():
... pass
>>> newadder.func_code=marshal.loads(s)
>>> newadder(20,10)
30
参考33章介绍代码对象和Python对象。
12.3示例:通过网络移动对象
示例所有的格式转换,swap模块,创建一个后台进程在两个交互模式下的Python解释器之间发送对象。同样允许在两台电脑之间发送对象。
参考15章的网络和26章的线程。
调用swap.send(obj)方法发送对象,对方的接收程序收到后存在swap.obj中。启动Python解释器时使用-c(告知执行import swap命令)和-i(执行命令后仍然保持运行)两个选项。这个特征允许读者用已经加载的swap模块启动并运行。
from socket import *
import cPickle,threading
ADDR="127.0.0.1"
PORT=50000
bConnected=0
def send(obj):
"Sends an object to a remote listener"
if bConnected:
conn.send(cPickle.dumps(obj,1))
else:
print 'Not connected!'
def listenThread():
"Receives objects from remote side"
global bServer, conn, obj, bConnected
while 1:
s=socket(AF_INET,SOCK_STREAM)
try:
s.bind((ADDR,PORT))
s.listen(1)
bServer=1
conn=s.accept()[0]
except Exception, e:
#可能已经被使用过了,所以只做客户端
bServer=0
conn=socket(AF_INET,SOCK_STREAM)
conn.connect((ADDR,PORT))
#永久的接受连接
bConnected=1
while 1:
o=conn.recv(8192)
if not o:
break;
obj=cPickle.loads(o)
print 'Receive new object'
print obj
bConnected=0
#开始监听线程
threading.Thread(target=listenThread).start()
print '线程开始监听'
为了简单起见,程序中省略了很多错误检查,如果真正的应用需要自己加上。listenThread函数保持循环,等待对象的到来。第一次启动时listenThread会尝试绑定到指定位置,(by gashero)如果绑定失败则会尝试连接那个地址。
12.4使用类似数据库的存储
shelve模块允许把Python对象保存在类似dbm的模块中。
dbm和其他数据库信息参考14章。
shelve.open(file[,mode])打开并返回一个shelve对象。mode参数(与dbm.open中的模式相同),默认值为c,意味着读写打开数据库,且不存在时创建。使用完成后用shelve对象的close()方法关闭。
把数据库看作词典来访问:
>>> import shelve
>>> db=shelve.open('objdb') #不要使用文件扩展名
>>> db['secretCombination']=[5,73,17]
>>> db['account']=5671012
>>> db['secretCombination']
[5,23,17]
>>> del db['account']
>>> db.has_key('account')
0
>>> db.keys()
['secretCombination']
>>> db.close()
shelve模块使用pickle,可以存储pickle可以处理的类型。shelve的限定与dbm限度相同。尽量不要用于存储太过于庞大的对象。
12.5转换到C结构或从C结构转换回来
struct模块允许创建一个等效于C结构的字符串,可以读写那些非Python程序生成的二进制文件。或者用于不同程序的网络通信。因为pickle模块的数据类型只能被Python识别。
使用struct需要使用格式字符串,调用struct.pack(format,v1,v2, ...)。格式字符如下:
字符 |
C类型 |
Python类型 |
---|---|---|
c |
Char |
长度为1的字符串 |
s |
char[] |
字符串 |
p |
(pascal字符串) |
字符串 |
i |
Int |
整型 |
I |
Unsigned int |
整型或长整型* |
b |
Signed char |
整型 |
B |
unsigned char |
整型 |
h |
Short |
整型 |
H |
unsigned short |
整型 |
l |
Long |
整型 |
Long |
unsigned Long |
长整型 |
f |
Float |
浮点型 |
d |
Double |
浮点型 |
x |
(pad style) |
- |
P |
void* |
整型或长整型 |
带有星号的表示依赖于平台的指针是32位还是16位。
例如,如下的C结构的等价物用:
struct {
int a;
int b;
int c;
};
采用值10,20,'Z',如下:
>>> import struct
>>> z=struct.pack('iic',10,20,'Z')
>>> z
'/012/000/000/000/024/000/000/000z'
从字节串反向转换用struct.unpack(format,data),返回元组:
>>> struct.unpack('iic',z)
(10,20,'Z')
传递给unpack的格式字符串一定要说明字符串中的所有数据,否则会产生异常。使用struct.calcsize(format)可以计算给定的格式字符串占用的字节数。
可以在格式字符前加上一个编号,表示这个数据类型重复的次数。为了便于理解,可以在格式字符串中的格式字符之间加入空格。
重复器编号的运行方式与's'(字符串)格式字符稍有差别。重复器会告诉字符串的长度(5s意味着5个字符的字符串)。0s意味着一个空字符串,而0c意味着0字符串。
如果C的int和long的大小相同,T格式字符会把给定的编号解包为Python长整型。如果C int比C long小,T把编号转换为Python整数。
'p'格式字符串支持pascal字符串。这种字符串使用第一个字节存储字符串长度,所以最大长度为255字节,其余的截断。如果提供了重复器则是指定整个字符串的字节数,包含长度字节。如果字符串小于指定字节数,则pack会添加空的填充字符。
默认时,struct会把字节顺序和结构成员对齐使用当前平台的C编译器使用的格式。通过下表列出的某个修饰符启动自己的格式字符串,可以超越这种行为。例如使用网络序:
>>> struct.pack('ic',65535,'D') #本机字节序为高序优先
'/377/377/000/000D'
>>> struct.pack('!ic',65535,'D') #强制网络字节序
'/000/000/377/377D'
修饰符 |
字节顺序 |
对齐 |
大小 |
---|---|---|---|
< |
高序优先(little-endian) |
无 |
标准 |
>或! |
低序优先(网络) |
无 |
标准 |
= |
内在的 |
无 |
标准 |
@(默认) |
内在的 |
内在的 |
内在的 |
当使用了一个大小为"standard"的修饰符,则C short会占据2个字节,int、long、float将使用4个字节,double使用8个字节。
如果需要使用对齐,而又不想用@(内在的对齐)时,可用'x'格式字符插入填充字节。如果必须强制一个结构的末端依据特殊类型的对齐规则进行对齐,则可用这种类型的格式代码(计数值为0)。如下例子强制单字符的结构对齐整型边界:
>>> struct.pack('c','A')
'A'
>>> struct.pack('c0l','A')
'A/000/000/000'
'P'(指针)格式字符只能在本地赋值中使用。
可用struct模块读写二进制文件,比如提取.wav文件的前36个字节的文件信息头。内容如下:
"RIFF"(4字节),用于识别文件类型
高序优先的长度字段(4字节)
"WAVE"(4字节),用于识别文件类型
"fmt "(4字节)
格式定义的子数据块长度(4字节)
格式类型(2字节)
声道数(2字节)
采样频率/Hz(4字节)
每秒数据量/字节(4字节)
样本字节数/字节(2字节)
声道宽度/位(2字节)
表示标题的格式字符串:
'<4s i 4s 4s ihhiihh'
如下是提取WAV文件头信息的例子:
>>> s=open('c://winnt//media//ringin.wav','rb').read(36)
>>> struct.unpack('<4si4s4sihhiihh',s)
('RIFF',10018,'WAVE','fmt ',16,1,1,11025,11025,1,8)
12.6把数据转换为标准格式
使用struct模块生成具有工业标准格式的数据。
12.6.1Sun的XDR格式
XDR(eXternal Data Representation,外部数据表示法)格式由Sun Microsystems创建的标准数据格式。RFC1832定义了这种格式,最通用的用法在NFS(Network File System,网络文件系统)中。适合在各个平台和系统之间共享数据。
xdrlib模块实现了XDR格式的子集,去掉了罕见使用的数据类型。转换到XDR只需创建xdrlib.Packer类的实例,反向转换需要创建xdrlib.Unpacker的实例。
Packer的例子:
>>> import xdrlib
>>> p=xdrlib.Packer() #构造函数无需参数
>>> p.pack_float(3.5) #32位浮点数。多种pack_
>>> p.pack_double(10.5) #64位浮点数
>>> p.pack_int(-15) #32位有符号整数
>>> p.pack_uint(15) #32位无符号整数
>>> p.pack_hyper(100) #64位有符号整数
>>> p.pack_uhyper(200) #64位无符号整数
>>> p.pack_enum(3) #枚举类型
>>> p.pack_bool(1) #布尔型,取1或0
>>> p.pack_bool('Hi') #值为True,所以存储1
pack_fstring(count,str)方法打包固定长度的字符串,长度为count。该函数不会存储字符串的大小,所以要解包时需要预先知道长度。pack_string(str)函数先调用pack_uint写入字符串长度,然后再用pack_fstring()来写入字符串,这样就可以指定不定长度的字符串了。
Packer对象还包含pack_bytes和pack_opaque方法,但是只是调用pack_string,而pack_fopaque实际上只调用了pack_fstring方法。
pack_farray(count,list,packFunc)函数打包一个长度固定,数据类型相同的数组(count是数据项长度)。要求计数值与列表长度必须相同,愚蠢的一点。且解包时,必须自己知道数组的长度。同样存在pack_array(list,packFunc),可以自己存储打包长度,在存储数组。packFunc告知Packer打包每个项所使用的方法,例如所有数据项都是整数,则为如下:
>>> p.pack_array([1,2,3,4],p.pack_int)
pack_list(list,packFunc)方法也是打包存储数组,但是可以支持预先并不知道长度的数组。例如一个自定义的不完整序列:
>>> class MySeq:
... def __getitem__(self,i):
... if i<5:
... return i
... raise IndexError
>>> m=MySeq()
>>> for i in m:
... print i
0
1
2
3
4
>>> p.pack_list(m,p.pack_int)
使用get_buffer()方法返回字节串,表达已经打包的所有数据的包格式。reset()方法清空缓冲区。
>>> p.reset()
>>> p.pack_int(10)
>>> p.get_buffer()
'/000/000/000/012'
>>> p.reset()
>>> p.get_buffer()
''
Unpacker对象
每个类型都有pack_
>>> import xdrlib
>>> p=xdrlib.Packer()
>>> p.pack_float(2.0)
>>> p.pack_fstring(4,'Dave')
>>> p.pack_string('/export/home')
>>> u=xdrlib.Unpacker(p.get_buffer())
>>> u.unpack_float()
2.0
>>> u.unpack_fstring(4)
'Dave'
>>> u.unpack_string()
'/export/home'
>>> u.done()
done()方法通知Unpacker,已经完成了解码数据。因为如果Unpacker的缓冲区如果还有数据,则会产生Error异常,告知用户还有剩余数据。
调用reset(str)可以使用str的数据用于解码,并替换缓冲区。任何时候都可用get_buffer()检索数据流的字符串表示法。
可用get_position()和set_position(pos)获取和设置解码位置。
12.6.2其他格式
比较推荐的也只有XML格式,更多信息参见18章。其他的特定文件格式很容易从网上找到定义。
12.7压缩数据
12.7.1zlib
包含GZIP和ZIP文件的压缩支持。
最简单的用法为compress(str[,level]和decompress(str [,wbits [,bufsize]])函数。压缩级别的level可用1到9,默认6,数值大则压缩率高而速度慢。解压缩用的wbits控制历史缓冲区大小,有8到15(默认)的数值,这个数值越大耗内存越多,但是会支持更好的压缩。bufsize设置缓冲区,默认16384,推荐不必自己修改。读入数据和返回数据都是按照字节串进行。
>>> import zlib
>>> longstring=100*'That zlib module sure is fun!'
>>> compressed=zlib.compress(longstring)
>>> len(longstring); len(compressed)
2900
62 @code: #zlib的压缩后数据
>>> zlib.decompress(compressed)[:40] # by gashero
'That zlib module sure is fun!That zlib m'
有关zlib的更多信息参阅如下
http://www.info-zip.org/pub/infozip/zlib/
zlib模块包含两函数crc32(str[,value])和adler32(str[,value]) ,都是用于计算字节串的校验和。value是校验和的起始值,用于支持多块的联合校验值。
>>> data='My dog has no fleas!'
>>> zlib.adler32(data)
1193871046
>>> data=data[:5]+'z'+data[6:]
>>> data
'My doz has no fleas!' #修改了一个字节
>>> zlib.adler32(data)
1212548825 #校验和的数值发生了改变
crc32的返回值比adler32更为可靠,但是计算时间更长。
如果需要压缩的数据量比允许用的内存要多,则可用zlib创建压缩和解压缩对象。compressobj([level])创建压缩对象。重复调用此对象的方法compress(str),每次调用都会压缩并保存数据。方法flush([mode]) 将完成压缩并返回剩余数据。
>>> c=zlib.compressobj(9)
>>> out=c.compress(1000*'I will not throw knives')
>>> out+=c.compress(2000*'or chairs')
>>> out+=c.flush()
>>> len(out)
115
如果mode使用Z_FULL_FLUSH或Z_SYNCH_FLUSH,会返回当前缓冲区的所有压缩数据,并且还可以继续添加压缩数据。如果忽略mode,则对象认为压缩完成,不需要添加数据了。
通过实际测试。仅在第一次调用c.compress()时,有返回2个字节的数据,之后的调用不会返回任何东西。单独调用c.flush()返回的数据并不是完整的,需要与上句的2个字节返回数据加在一起才可以解压缩。
通过zlib的decompressobj([wbits])函数,可以创建解压缩对象,允许一块一块的解压缩数据流。调用decompress(str)方法能够解压缩下一数据块。decompress能返回它能够完成的、最大量的解压缩数据,但是因为需要缓冲一些额外数据,直到读者提供了更多要解压缩的数据为止。如下代码解压缩上例的输出,一次20字节:
>>> d=zlib.decompressobj() #创建解压缩对象
>>> msg=''
>>> while out:
... msg+=d.decompress(out[:20]) #解压缩一部分
... out=out[20:]
>>> msg+=d.flush() #告知所有数据读入完成
>>> len(msg)
24800
提供了足够的数据之后就用flush()方法告知,之后就不能再用decompress()方法了。
解压缩对象还包含unused_data成员,保留了上次调用decompress之后剩余的压缩数据。非空的unused_data字符串意味着解压缩对象还在处理额外的数据,以便完成解压缩这块特殊数据。
12.7.2gzip
gzip模块允许读写.gz(GNU zip)文件,如同读写普通文件一样,忽略压缩与解压缩的细节。
GNU zip和gunzip程序还支持其他多种格式,但是gzip模块只支持一种。
gzip.GzipFile([filename[,mode[,compresslevel[,fileobj]]]])函数构造GzipFile对象,必须提供filename或fileobj参量,文件对象可以是类似文件的任何对象,如cStringIO对象。compresslevel提供压缩等经。
如果没有mode,则gzip会试着使用fileobj的模式,如果也没有则默认用'rb'打开。GzipFile不能为同时读写打开,可以用rb、wb、ab模式。
为了实现与普通文件相同的接口。可用gzip模块的open(filename [,mode[,level]])函数。类似于Python内置的打开文件的方法。
>>> f=gzip.open('small.gz','wb')
>>> f.write("""Old woman!
... Man"""
>>> f.close()
>>> f=gzip.open('small.gz')
>>> print f.read()
Old woman!
Man
zipfile模块允许读、写和获得有关以通用ZIP文件格式存储的文件信息。zipfile模块暂时不支持ZIP文件注释和跨越多个磁盘的文件。
如果给定文件确实是ZIP文件,则zipfile.is_zipfile(filename)返回真。zipfile模块还定义了ZipFile、ZipInfo、PyZipFile类。
ZipFile类,构造函数ZipFile(filename[,mode[,compression]])
>>> import zipfile
>>> z=zipfile.ZipFile('room.zip')
>>> z.printdir() #按可读格式打印存档列表
File Name Modified Size
world 2000-09-05 09:25:14 10919
cryst.cfg 1999-03-07 06:14:34 27
mode可以是r(读,默认值)、w(写)或a(附加)。如果附加到ZIP文件中,会添加新文件,如果附加到非ZIP文件,则会在文件末尾添加一个ZIP存档,而且并非所有的程序都可以正确打开。compression参数可为ZIP_STORED(不压缩)和ZIP_DEFLATED(压缩)。
ZipFile对象的namelist()方法将返回ZIP包含的文件的列表。采用getinfo(name)方法返回任意文件的ZipInfo对象。采用infolist()方法可以获得整个存档的ZipInfo的列表:
>>> z.namelist()
['world','cryst.cfg'] #ZIP档案包含两个文件
>>> z.getinfo('world') #获取文件world的信息
>>> z.getinfo('world').file_size
10919
>>> z.infolist()
[
如果以读或者附加模式打开ZIP文件,则read(name)会解压指定的文件,返回内容。testzip()方法返回第一个已损坏文件的名字,如果所有文件都没有损坏返回None。
如果以写或者附加模式打开ZIP文件,则write(filename[,arcname [,compress_type]])函数把filename文件的内容添加到存档中。如果给arcname提供一个值,则是存档中的文件名。compress_type提供超越创建ZipFile时使用的压缩类型。
更改ZIP文件后,用close()方法保存。ZipFile对象还有debug属性,可以用这个属性更改调试输出信息的层次。最大输出为3,最小的没有输出为0(默认)。
ZipInfo类,提供了存档中每个成员的信息。构造函数ZipInfo( [filename[,date_time]])。getinfo()和infolist()也能返回ZipInfo对象。filename应该是文件的完整路径,而且date_time是个六元组,其中包含最后一次修改时间的信息。ZipInfo包含很多属性,如下最有用的:
名字 |
说明 |
---|---|
filename |
存档文件名 |
compress_size |
压缩后大小 |
file_size |
原始文件大小 |
date_time |
最后一次修改的日期时间。年、月日、时、分、秒 |
compress_type |
压缩类型(stored或deflated) |
CRC |
原始文件的CRC32 |
comment |
项注释 |
extract_version |
解压所需最小软件版本 |
header_offset |
文件标题的字节偏移量 |
file_offset |
文件数据的字节偏移量 |
PyZipFile类,用于创建包含Python模块和包的ZIP文件,是ZipFile的一个子类,构造函数同ZipFile。
PyZipFile添加的唯一方法是writepy(pathname),会搜索*.py文件,并且把响应的字节码文件添加到ZIP文件中。如模块file.py会存档为file.pyo,如果不存在则用file.pyc,如果还不存在则编译创建file.pyc来添加到存档中。
pathname如果是包目录的名字(即包含__init__.py文件的目录),writepy会递归搜索这个目录,找到所有*.py文件。如果pathname只是普通目录名,则只搜索这个目录下的*.py文件,不递归搜索子目录。如pathname只是一个普通的Python模块,writepy就会把它的字节码添加到ZIP文件中。
关于Python包,参考6章。
完成...
第十三章 访问日期和时间
13.1在Python中告知时间
可以把时间表示为数字或元组,time模块提供了时间函数。
13.1.1滴答
把时间中的一个点表示为"滴答"的次数,即新纪元后所经历的秒数。新纪元是任意选择的"时间开始"。对UNIX和Windows来说,新纪元指1/1/1970的上午12:00。
time.time返回当前时间,以滴答为单位。Python会为滴答使用浮点值。因为时间精度是系统相关的,有些系统上time.time总是返回整数值。
滴答的格式是很容易处理的,但是在1970年之前或者2038年之后的日期是无法用滴答表示的,第三方模块如mxDateTime提供了更大范围的日期时间值。
13.1.2时间元组
Python中很多函数以9元素元组表示时间:
索引 |
域 |
值 |
---|---|---|
0 |
4位数字年 |
1993 |
1 |
月 |
1-12 |
2 |
日 |
1-31 |
3 |
小时 |
0-23(0代表12a.m.) |
4 |
分钟 |
0-59 |
5 |
秒 |
0-61(60或61是闰秒) |
6 |
星期 |
0-6(0代表星期一) |
7 |
一年中的哪一天 |
1-366(古罗马历) |
8 |
Daylight savings |
-1,0,1 |
由于元组的元素排列是有顺序的,所以可以对元组进行比较。TimeTuple不包含时区。
13.1.3秒表时间
clock函数可以为Python代码计时。比如在一个函数调用前和调用后分别调用clock,再计算差值得到经历的时间/秒。由clock返回的值是系统无关的,并且与真实的时间无关。只是用于计时。
暂停执行的函数是time.sleep(n),n是一个浮点数,暂停n秒。
13.2时间格式之间的转换
函数localtime可以把滴答转换为TimeTuple。
>>> time.localtime(time.time())
(2006,2,14,11,41,11,1,45,0)
函数gmtime也用于从滴答转换到TimeTuple,但是返回UTC,即通用协调时间。
函数mktime从TimeTuple转换到EpochSeconds(滴答)。根据本地时区解释TimeTuple。与localtime是反函数。
gmtime的反函数是calendar.timegm。
13.3解析及打印日期时间
asctime函数获取一个TimeTuple,并返回一个可阅读的时间信息。适于日志。可以带参数用于准还,或者不带参数而直接放回当前时间。不带参数的版本用于Python2.1。
函数ctime转换滴答值到时间信息。
13.3.1有趣的格式
strftime(format,timetuple)按照指定格式转换TimeTuple格式。如下是strftime的format字符串语法:
代码 |
替换 |
示例/范围 |
---|---|---|
%a |
缩写的星期名 |
Thur |
%A |
完整的星期名 |
Thursday |
%b |
缩写的月名 |
Jan |
%B |
完整的月名 |
January |
%c |
日期和时间表示法(等效于%x%X) |
12/10/00 10:09:41 |
%d |
月份中的一天 |
01-31 |
%H |
小时(24小时制) |
00-23 |
%h |
小时(12小时制) |
01-12 |
%j |
Julian天(一年中的一天) |
001-366 |
%m |
月份 |
01-12 |
%M |
分钟 |
00-59 |
%p |
A.M.或P.M. |
AM |
%S |
秒 |
00-61 |
%L |
周编号,周日开始,一年中第一个周日之前的那些周为第0周 |
00-53 |
%w |
以数字表示星期几(0=星期日) |
0-6 |
%W |
周编号,每周以周一开始,一年中第一个星期一之前的那些为第0周 |
00-53 |
%x |
日期 |
12/10/00 |
%X |
时间 |
10:09:41 |
%y |
2位数字的年 |
00-99 |
%Y |
4位数字的年 |
2000 |
%Z |
时区的名称 |
太平洋标准时间 |
%% |
字面的%符号 |
% |
例如实现ctime的返回格式:
>>> time.strftime("%a %b %d %H:%M:%S %Y",Now)
'Sun Dec 10 10:09:41 2000'
13.3.2解析时间
函数strptime(timestring[,format])是strftime的反函数,解析字符串,并返回一个TimeTuple。按照未指定的时间组件进行猜测,如果采用format无法解析,则会产生ValueError。默认格式是ctime所使用的格式,即"%a %b %d %H:%M:%S %Y"
在很多系统上都可以使用strptime函数,但是Windows上不可用。
13.3.3定位
不同国家的日期格式不同。例如日月,在美国是月在前,英国是日在前。strftime会考虑当前时区格式,"%x"就是使用正确的日月次序。编写代码时也要考虑这个问题,"%m/%d"并不是在所有地区都正确。可以参考34章的有关国际化的信息。
13.4访问日历
13.4.1打印月历和年历
使用calendar模块,内部使用滴答表示日期,所以可用的日历范围有限。
monthcalendar(yearnum,mouthnum)
返回一个列表的列表,表示月历,主列表表示星期,子列表是星期中的几天的日期。子列表的0值表示上个月或下个月的,用于补满7个元素。
month(yearnum,monthnum[,width[,linesperweek]])
返回一个占据多列的字符串。返回一个具有可打印格式的月历,width指定了每一栏的宽度,最小值也是默认值是2。linesperweek参数指定了每周打印多少行,默认值为1,用于在各行之间留出间隔。如下例:
>>> print calendar.month(2002,5)
May 2002
Mo Tu We Th Fr Sa Su
1 2 3 4 6
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
prmonth(yearnum,monthnum[,width[,linesperweek]])
打印month的相应输出。
calendar(yearnum[,width[,linesperweek[,columnpadding]])
可打印年历,每行包含3各月。columnpadding参数指明了要在月栏中添加的间隔,默认值为6。对应的prcalendar函数将会把如上函数的输出打印出来。
13.4.2日历信息
weekday函数将为特殊日期查询星期值:
weekday(year,month,day)
星期值的范围是从Monday(0,星期一)至Sunday(6,星期日)。为了方便使用可以全部用常量:
>>> calendar.weekday(2002,5,1)==calendar.WEDNESDAY
monthrange(yearnum,monthnum)
函数返回二元组,在yearnum年中monthnum月的第一天的周日,及月份的长度。
>>> calendar.monthrange(2000,2) #闰年的闰月
(1,29)
默认情况下calendar将把Monday作为一周的第一天,而Sunday是最后一天。通过setfirstweekday(weekday)可以更改。函数firstweekday可以返回哪一天是每周的第一天的设置。
13.4.3闰年
如果yearnum是闰年则isleap(yearnum)则返回True。
leapdays(firstyear,lastyear)
返回从firstyear到lastyear之间的闰天数。
13.5使用时区
time.daylight值指明是否定义了本地的DST(Daylight Saving Time)时间区。当他值为True时,则DST时区可用。
time.timezone值是从本地时区到UTC的偏移量,以秒为单位。time.altzone值是从本地DST时区到UTC的偏移量/秒。altzone更准确,但是只有在time.daylight=True时才可用。
time.tzname值是一个元组,第一个项是本地时区名称。如果有效,则第二个项是Daylight Saving Time时区的名称。只有time.daylight=True时第二个项才可用。例:
>>> time.tzname
('Pacific Standard Time','Pacific Daylight Time')
13.6允许两位数字的年
两位数字的日期表示比较方便。但是会有一些问题,如2000年问题和2038年问题。Python程序会自动在00到68之间加上2000进行运算,而69到99之间的年份自动加上1900。也可以设置time.accept2dyear值为False,来禁止使用两位数的年份。如果设置了PYTHON2K环境变量,则time.accept2dyear值会初始化为Flase。
完成...
第十四章 使用数据库
14.1使用基于磁盘的词典
Python标准库提供了一个简单的数据库,获取一个基于磁盘的词典(Disktionary)的格式。这个功能以UNIX的实用程序dbm为基础。
anydbm 可移植的数据库,从其他模块之间选择最佳模块
dumbdbm 速度慢而且有局限性,但可在所有平台使用
dbm 包装UNIX dbm实用程序,仅在UNIX上可用
gdbm 包装GNU的改进dbm,仅在UNIX上可用
dbhash 包装BSD数据库库,在UNIX和Windows上可用
通常建议使用anydbm。每个dbm模块都定义了一个dbm对象和一个名为error的异常。本书的描述适合于dbm的每一个版本。
open函数创建了一个新的dbm对象:open(filename[,flag[,mode]]) 。filename是文件名,flag参数可选,但是对dbhash来说是必须的,可用如下值:
r [默认]只读打开数据库
w 读写打开数据库
c 与w相同,但是必要时会自动创建数据库
n 与w相同,但总是创建一个新的空数据库
dbm中的某些版本,包括dumbdbm允许修改只读打开的数据库
mode为UNIX风格的许可权,用于设置数据库文件。
打开了数据库之后,就可以像正常词典一样的访问:
>>> SimpleDB=anydbm.open("test","c")
>>> SimpleDB['Terry']="Gilliam" # add a record
>>> print SimpleDB['Terry'] # 读取记录
Gilliam
>>> del SimpleDB['Terry'] # 删除记录
dbm中的键和值必须都是字符串,甚至不可以是数字。访问一个不存在的键会产生KeyError异常,可用has_key()提前检验,也可用keys()获得键列表。但是不可以使用词典中的安全get方法。
使用完成后使用他的close()方法写入到磁盘并释放资源。
14.2DBM示例:跟踪电话号码
import anydbm
import sys
def AddName(DB):
print "Enter a name.(Null name to cancel):"
# Take the [:-1] slice to remove the /n at the end
NewName=sys.stdin.readline()[:-1]
if(NewName==""): return
print "Enter a phone number."
PhoneNumber=sys.stdin.readline()[:-1]
DB[NewName]=PhoneNumber
def PrintList(DB):
for key in DB.keys():
print key,DB[key]
if __name__="__main__":
PhoneDB=dbhash.open("phone","c")
while(True):
print '/nEnter a name to look up/n+to add a name'
print '* for a full listing/n. to exit'
command=sys.stdin.readline()[:-1]
if command=="":
continue # 无事可做,再次显示提示
if command=="+":
AddName(PhoneDB)
elif command=="*":
PrintList(PhoneDB)
elif command==".":
break #quit
else:
try:
print PhoneDB[command]
except KeyError:
print "Name not found."
print "Saving and closing..."
PhoneDB.close()
14.3基于磁盘的高级词典
dbm的各个版本都不会使用兼容的文件格式。所有平台都兼容的格式仅限于dumbdbm。whichdb模块可以检测一个文件属于哪种数据库版本。whichdb.whichdb(filename)函数返回创建数据库文件的模块的名字。文件不可读或不存在时返回None,文件格式无法识别时返回空字符串。
>>> MysteryDB=anydbm.open('Unknown','c')
>>> MysteryDB.close()
>>> whichdb.whichdb('Unknown')
'dbhash'
14.3.1dbm
提供了一个附加的字符串变量library,是基本ndbm实现的名字。
14.3.2gdbm
提供了改进的键导航。dbm方法firstkey将返回数据库中的第一个键;之后nextkey(currentkey)将继续返回currentkey的下一个键。在完成了一些删除操作之后可用reorganize释放处已删除记录所占用空间。方法sync会把未写的更改刷新到磁盘上。
14.3.3dbhash
也提供了键导航。dbm方法first和last提供了第一个和最后一个键。方法next(currentkey)和previous(currentkey)分别返回下一个和上一个键。sync方法输出到磁盘。
当数据库很大时使用keys方法返回的键列表会占用大量内存。所以键导航方式性能会较好。注意处理KeyError。
14.3.4使用BSD数据库对象
bsddb模块即可在UNIX上使用,也可以在Windows上使用,提供了Berkeley DB库的访问。提供了散列表、B树、记录对象。树构造函数:hashopen、btopen、rnopen,与dbm构造函数使用的参数(filename,flag, mode)相同。该构造函数获取其他可选参数并直接传递给BSD代码,通常不应该使用。
BSD数据对象与dbm对象功能相同,也提供了一些附加的方法。方法first、last、next、previous都将导航并返回数据库中的记录。对于B树对象,这些记录是按照键值排序的;对于散列表和记录对象,并没有定义记录次序。此外,方法set_Location(keyvalue)会跳到keyvalue键的记录。
sync方法将会把数据写入文件。
14.4访问关系数据库
Python通过模块几乎可以访问任何数据库,如Oracle、MySQL、DB/2 、Sybase。Python数据库API为访问关系数据库的Python模块定义了标准界面。大多数第三方数据库模块近似地符合API,但并不是很完美。本章将介绍2.0版本的API。
14.4.1连接对象
connect()方法用于构造数据库连接。构造游标时将使用此连接。使用完连接后要用close方法释放。数据库通常提供一个有限的连接池。connect方法的参数将随着模块而改变,包含dsn(数据源名)、user、 password、host、database。
14.4.2事物处理
commit()连接方法可以提交当前事物处理;rollback()会取消当前事物处理。并非所有的数据库都支持事物。commit方法总是可用的,但rollback仅在支持的地方才可用。
14.4.3游标对象
游标可以执行SQL语句和检索数据。连接方法cursor创建并返回一个新游标。游标方法execute(command[,parameters])执行指定的SQL语句command,并传递必须的参数。执行之后的rowcount属性指明改变或返回的行数;description属性描述受影响的列数。执行完命令之后,方法fetchone()返回下一行数据(以序列的方式,每个列值都带有一个入口)。方法fetchmany([size])将返回行的序列-直到他们的size。方法fetchall()将返回所有行。
在使用游标之后用close方法来释放。数据库有一个有限的可用游标池,所以使用游标之后立即释放是很重要的。
14.5示例:"类似声音的"查询
下例使用mxODBC模块连接多种数据库。查询名字"听起来像"另一个人的名字。对每个名字进行一种编码,编码相同的名字就是相似的。
import ODBC.Windows
创建光标:
NewCursor=Conn.cursor()
执行SQL语句时放入try语句块,并提供finally来释放光标:
try:
NewCursor.execute('select ....')
finally:
NewCursor.close()
创建连接:
因为使用MySQL数据库不支持事物,所以设置一个选项,另外自己替换MyDB为自己的数据源名。
Conn=ODBC.Windows.Connect("MyDB",clear_auto_commit=0)
14.6检验相关的元数据
当游标返回数据时,游标属性description就是元数据-包含的列的定义。列的定义表示为一个7项的元组的序列。
0 列名
1 类型代码
2 显示大小(以列为单位)
3 内部大小(以字符或字节为单位)
4 数字的计数法
5 数字的精度
6 可空(如果为0,则不允许空)
例如一个例子:
>>> mc.execute('select FIRST_NAME,MANAGER_ID from EMPLOYE')
>>> mc.description
(('FIRST_NAME',12,None,None,5,0,0),('MANAGER_ID',3,None, None,1,0,1))
mxODBC模块并不返回显示大小和内部大小。
14.7示例:创建审计表
在更改原表的时候将原数据写入到一个镜像表中,提供数据的历史回溯功能。包括编辑时间,用户ID,数据等。而且一般将插入镜像表的SQL语句与原表编辑语句放入同一个事务中。
细节略...
14.8DB API的高级特征
各种数据库支持不同的类型,如INT和VARCHAR。数据库模块应该导出描述并用于description元数据。
>>> MyCursor.execute('select employee_name from employe where FIRST_NAME="Bob"')
>>> MyCursor.description[0]
('FIRST_NAME',12,None,None,3,0,0)
>>> MyCursor.description[0][1]==ODBC.Windows.VARCHAR
True
对某些具体类型如mxDateTime模块定义了具体的日期格式。
14.8.1输入和输出大小
游标属性arraysize指定了默认情况下每个fetchmany调用将返回多少行,默认值为1,可以增大。处理arraysize把大小传递给fetchmany会更有效。
游标方法setinputsizes(size)设置输入数据的内存限度,用以提高内存使用率。size是一个序列,每个项指定对应参数的最大长度,如果size的某个项为None则该列没有响应的参数值保存在内存里(默认)。
游标方法setoutputsizes(size[,columnindex])在读取庞大列(如LONG或BLOB)中的数据时设置了缓冲区的大小。未指定columnindex时给所有庞大列指定。
14.8.2可重新使用的SQL语句
就是带参数或者说预编译的SQL语句。下例:
>>> SQLQuery='select GAME_NAME from GAME where GAME_ID=?'
>>> MyCursor.execute(SQLQuery,(60,)) #赋予参数值60
>>> MyCursor.fetchall()
[('Air Combat 22',)]
>>> MyCursor.execute(SQLQuery,(200,))
[('Badlands',)]
参数标记的句法是由模块变量paramstyle描述的。游标句法:
executemany(command,parametersequence)多次运行同一个SQL语句,对于parametersequence中的每个参数集合,都运行一次。
14.8.3数据库的库信息
模块变量apilevel是一个字符串,说明支持的DB API层。应为1.0或2.0。如果不可用则为1.0。
模块变量threadsafety描述了模块支持哪一层并行访问:
0 线程不可以共享模块
1 线程可以共享模块
2 线程可以共享连接
3 线程可以共享游标
模块变量paramstyle说明了参数标记句法:
qmark WHERE NAME=?
numberic WHERE NAME=.1
named WHERE NAME=.name
format WHERE NAME=%s
pyformat WHERE NAME=%(name)s
14.8.4错误层次
数据库警告和错误是exceptions模块中StandardError类的子类。可以捕获Error类,完成常规错误处理。
最上层模块是exceptions.StandardError,之下的两个直接子类:Error和Warning。其中Warning没有子类。
Error的子类包括InterfaceError和DatabaseError。而InterfaceError没有子类。
DatabaseError的子类:NotSupportedError、OperationalError、 IntegrityError、ProgrammingError、DataError。
Warning 重大警告,如插入值时的数据截断
Error 错误基类,不直接产生
InterfaceError 数据库模块遇到内部错误,不是来自于数据库
DatabaseError 数据库本身的错误,多作为基类
DataError 无效数据错误,如超范围的数字
OperationalError 操作错误,如连接到数据库失败
IntegrityError 数据完整性错误
InternalError 内部数据库错误,如游标断开连接
ProgrammingError 对数据库模块的无效调用,如使用已关闭的游
标,或未查询时就先fetch
NotSupportedError DB API的某些部分是可选的,但是试图调用而
又没有可选的备用方案时发生
14.9小结
略...
完成...