cook book:5:文件与IO+6:数据编码和处理
1:读写文本数据 :读写各种不同编码的文本数据ASCII,UTF-8或UTF-16编码等
# rt模式下,python在读取文本时会自动把\r\n转换成\n,文本文件用二进制读取用‘rt’; # 使用带有 rt 模式的 open() 函数读取文本文件 with open('aaaa', "rt") as f: data = f.read() print(data, type(data)) 输出aaaa文件读取出来的数据,\r\n都转换成\n,读取出来的数据是字符串str类型 with open('aaaa', "rt") as f: for line in f: print(line)
# 写入一个文本文件,使用带有 wt 模式的 open() 函数, 如果之前文件内容存在则清除并覆盖掉。 # 编写文本数据块 with open('somefile.txt', 'wt') as f: f.write(text1) f.write(text2) ... # 重定向打印语句 with open('somefile.txt', 'wt') as f: print(line1, file=f) print(line2, file=f) ...
# 已存在文件中添加内容,使用模式为 at 的 open() 函数。 # 文件的读写操作默认使用系统编码,可以通过调用 sys.getdefaultencoding() 来得到。 # 在大多数机器上面都是utf-8编码。如果你已经知道你要读写的文本是其他编码方式,
那么可以通过传递一个可选的 encoding 参数给open()函数 with open('somefile.txt', 'rt', encoding='latin-1') as f:
# Python支持非常多的文本编码。几个常见的编码是ascii, latin-1, utf-8和utf-16。
在web应用程序中通常都使用的是UTF-8。 ascii对应从U+0000到U+007F范围内的7位字符。
latin-1是字节0-255到U+0000至U+00FF范围内Unicode字符的直接映射。
当读取一个未知编码的文本时使用latin-1编码永远不会产生解码错误。
使用latin-1编码读取一个文件的时候也许不能产生完全正确的文本解码数据,
但是它也能从中提取出足够多的有用数据。同时,如果你之后将数据回写回去,原先的数据还是会保留的
# with语句 # with语句给被使用到的文件创建了一个上下文环境, 但 with 控制块结束时,文件会自动关闭。你也可以不使用 with 语句,但是这时候你就必须记得手动关闭文件 f = open('somefile.txt', 'rt') data = f.read() f.close() # 换行符的识别Unix和Windows中是不一样,分别是 \n 和 \r\n # 默认情况下,Python会以统一模式处理换行符。 这种模式下,在读取文本的时候,
Python可以识别所有的普通换行符并将其转换为单个 \n 字符。 类似的,在输出时会将换行符 \n 转换为系统默认的换行符。
如果你不希望这种默认的处理方式,可以给 open() 函数传入参数 newline='' ,
with open('somefile.txt', 'rt', newline='') as f:
# 在Unix机器上面读取一个Windows上面的文本文件 内容是 hello world!\r\n >>> # 已启用换行转换(默认) >>> f = open('hello.txt', 'rt') >>> f.read() 'hello world!\n' >>> # 新行翻译已禁用 >>> g = open('hello.txt', 'rt', newline='') >>> g.read() 'hello world!\r\n' >>>
# 读取文件编码错误问题,读取或者写入一个文本文件时,你可能会遇到一个编码或者解码错误 >>> f = open('sample.txt', 'rt', encoding='ascii') >>> f.read() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.3/encodings/ascii.py", line 26, in decode return codecs.ascii_decode(input, self.errors)[0] UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 12: ordinal not in range(128) >>> 这种错误通常表示你读取文本时指定的编码不正确, # 仔细阅读说明并确认你的文件编码是正确的(比如使用UTF-8而不是Latin-1编码或其他)。
如果编码错误还是存在的话,你可以给 open() 函数传递一个可选的 errors 参数来处理这些错误。 下面是一些处理常见错误的方法 >>> # 用Unicode U+fffd替换字符替换坏字符 >>> f = open('sample.txt', 'rt', encoding='ascii', errors='replace') >>> f.read() 'Spicy Jalape?o!' >>> # 完全忽略坏字符 >>> g = open('sample.txt', 'rt', encoding='ascii', errors='ignore') >>> g.read() 'Spicy Jalapeo!' >>> # 对于文本处理的首要原则是确保你总是使用的是正确编码。当模棱两可的时候,就使用默认的设置(通常都是UTF-8)
2:打印输出至文件中 将 print()
函数的输出重定向到一个文件中去 ——>在 print()
函数中指定 file
关键字参数
with open('d:/work/test.txt', 'wt') as f: print('Hello World!', file=f)
文件必须是以文本模式打开。 如果文件是二进制模式的话,打印就会出错
3:使用其他分隔符或行终止符打印 使用 print()
函数输出数据,改变默认的分隔符或者行尾符
# print() 函数中使用 sep 和 end 关键字参数,改变输出方式 print('ACME', 50, 91.5) # ACME 50 91.5 print('ACME', 50, 91.5, sep=',') # ACME,50,91.5 print('ACME', 50, 91.5, sep=',', end='!!\n') # ACME,50,91.5!! # end 参数可以在输出中禁止换行 for i in range(5): print(i, end=' ') # str.join和sep最后输出一样 print(','.join(('ACME', '50', '91.5'))) print('ACME', 50, 91.5, sep=',') # str.join() 的问题在于它仅仅适用于字符串 row = ('ACME', 50, 91.5) print(','.join(row)) # 报错join里面的参数需要每个都是字符串 print(','.join(str(x) for x in row)) # 需要这样写 # 直接使用sep参数也可以 row = ('ACME', 50, 91.5) print(*row, sep=',')
4:读写字节数据 读写二进制文件,比如图片,声音文件等, 使用rb
或 wb
的 open()
函数来读取或写入二进制数据
# rb 或 wb 的 open() 函数来读取或写入二进制数据 # 以单字节字符串的形式读取整个文件 with open('somefile.bin', 'rb') as f: data = f.read() # 将二进制数据写入文件 with open('somefile.bin', 'wb') as f: f.write(b'Hello World') # 读取二进制数据时,需要指明的是所有返回的数据都是字节字符串格式的,而不是文本字符串。 # 在写入的时候,必须保证参数是以字节形式对外暴露数据的对象(比如字节字符串,字节数组对象等)
# 读取二进制数据的时候,字节字符串和文本字符串的语义差异可能会导致一个潜在的陷阱,索引和迭代动作返回的是字节的值而不是字节字符串 t = 'Hello World' print(t[0]) # H b = b'Hello World' print(b[0]) # 72 返回的是字节H的十进制的值72,不是返回的b'H'这个字节
# 从二进制模式的文件中读取或写入文本数据,必须确保要进行解码和编码操作 with open('somefile.bin', 'rb') as f: data = f.read(16) text = data.decode('utf-8') with open('somefile.bin', 'wb') as f: text = 'Hello World' f.write(text.encode('utf-8')) decode解码,encode编码
# 二进制I/O还有一个鲜为人知的特性就是数组和C结构体类型能直接被写入,而不需要中间转换为自己对象 import array nums = array.array('i', [1, 2, 3, 4]) with open('data.bin','wb') as f: f.write(nums)
# 这个把数组对象写入文本,可以直接写入这个数组对象,并不是写入一个字符串 # 这个适用于任何实现了被称之为”缓冲接口”的对象,这种对象会直接暴露其底层的内存缓冲区给能处理它的操作。 二进制数据的写入就是这类操作之一 # 很多对象还允许通过使用文件对象的 readinto() 方法直接读取二进制数据到其底层的内存中去 >>> import array # array是数组,这个数组里面只能放数字 >>> a = array.array('i', [0, 0, 0, 0, 0, 0, 0, 0]) >>> with open('data.bin', 'rb') as f: ... f.readinto(a) # 从f文件句柄中读取数据后放到a这个容器中,所以返回[1, 2, 3, 4, 0, 0, 0, 0] ... 16 >>> a array('i', [1, 2, 3, 4, 0, 0, 0, 0]) >>>
# mmap,内存映射文件,文件data.bin可以放字符串 import mmap a = mmap.mmap(-1, 20) with open('data.bin', 'rb') as f: f.readinto(a) print(a.read())
5:文件不存在才能写入 一个文件中写入数据,但是前提必须是这个文件在文件系统上不存在,不允许覆盖已存在的文件内容 open()
函数中使用 x
模式
# open() 函数中使用 x 模式 # 如果文件是二进制的,使用 xb 来代替 xt with open('somefile', 'wt') as f: f.write('hello\n') with open('somefile', 'xt') as f: f.write('Hello\n') # 报错 FileExistsError: [Errno 17] File exists: 'somefile'
# 写文件时候不小心覆盖一个已存在的文件,替代方案是os模块测试这个文件是否存在 import os if not os.path.exists('somefile1'): with open('./somefile1', 'wt') as f: f.write('Hello\n') else: print("somefile这个文件已经存在")
# 使用x文件模式更加简单。要注意的是x模式是一个Python3对open()
函数特有的扩展。
# 在Python的旧版本或者是Python实现的底层C函数库中都是没有这个模式的
6:字符串的I/O操作 使用操作类文件对象的程序来操作文本或二进制字符串
# 使用io.StringIO() 和 io.BytesIO() 类来创建类文件对象操作字符串数据 import io s = io.StringIO() # 创建类文件对象 print(s.write('Hello World\n')) # 输出12 print('This is a test', file=s) s.seek(0) print(s.read()) # 把s当成文件句柄,write写和print+file写的时候文件指针到了末尾,需要seek移动指针 输出: Hello World This is a test print(s.getvalue()) # 这个不需要移动指针就能读取当前文件全部内容,类似先移动指针到0再全部读取文件内容 输出: Hello World This is a test
import io s = io.StringIO('Hello\nWorld\n') print(s.read(4)) # Hell print(s.read()) # 'o\nWorld\n'
# io.StringIO 只能用于文本。如果你要操作二进制数据,要使用 io.BytesIO 类来代替 import io s = io.BytesIO() s.write(b'binary data') print(s.getvalue()) # b'binary data' # 模拟一个普通的文件的时候 StringIO 和 BytesIO 类是很有用的 # 在单元测试中,你可以使用 StringIO 来创建一个包含测试数据的类文件对象, 这个对象可以被传给某个参数为普通文件对象的函数 # StringIO 和 BytesIO 实例并没有正确的整数类型的文件描述符。
因此,它们不能在那些需要使用真实的系统级文件如文件,管道或者是套接字的程序中使用
7:读写压缩文件 读写一个gzip或bz2格式的压缩文件, gzip
和 bz2
模块
# gzip 和 bz2 模块的使用(读写压缩文件) # 两个模块都为 open() 函数提供了另外的实现 # gzip压缩 import gzip with gzip.open('somefile.gz', 'rt') as f: text = f.read() # bz2压缩 import bz2 with bz2.open('somefile.bz2', 'rt') as f: text = f.read()
# gzip和bz2写入压缩数据 # gzip 压缩 import gzip with gzip.open('somefile.gz', 'wt') as f: f.write(text) # bz2压缩 import bz2 with bz2.open('somefile.bz2', 'wt') as f: f.write(text) 上面所有的I/O读写操作都使用文本模式并执行Unicode的编码/解码。 类似的,如果你想操作二进制数据,使用 rb 或者 wb 文件模式即可
# 选择一个正确的文件模式是非常重要的。 如果你不指定模式,那么默认的就是二进制模式,如果这时候程序想要接受的是文本数据,那么就会出错。 # gzip.open() 和 bz2.open() 接受跟内置的 open() 函数一样的参数, 包括 encoding,errors,newline 等等 # 写入压缩数据时,可以使用 compresslevel 这个可选的关键字参数来指定一个压缩级别 with gzip.open('somefile.gz', 'wt', compresslevel=5) as f: f.write(text) # 默认的等级是9,也是最高的压缩等级。等级越低性能越好,但是数据压缩程度也越低
# gzip.open() 和 bz2.open() 还有一个很少被知道的特性, 它们可以作用在一个已存在并以二进制模式打开的文件上 import gzip f = open('somefile.gz', 'rb') with gzip.open(f, 'rt') as g: text = g.read() # 这样就允许 gzip 和 bz2 模块可以工作在许多类文件对象上,比如套接字,管道和内存中文件等。
8:固定大小记录的文件迭代 想在一个固定长度记录或者数据块的集合上迭代,而不是在一个文件中一行一行的迭代
# iter 和 functools.partial() 函数 from functools import partial RECORD_SIZE = 32 with open("somefile.data", "rb") as f: records = iter(partial(f.read, RECORD_SIZE), b"") for r in records: print(r) # records 对象是一个可迭代对象,它会不断的产生固定大小的数据块,直到文件末尾。 # 要注意的是如果总记录大小不是块大小的整数倍的话,最后一个返回元素的字节数会比期望值少。
# functools模块用于高阶函数:作用于或返回其他函数的函数。一般而言,任何可调用对象都可以作为本模块用途的函数来处理。 # functools.partial返回的是一个可调用的partial对象, # partial(func,*args,**kw) func是必须要传入的,而且至少需要一个args或是kw参数 # 创建一个功能函数,实现三个数的相加,如果其中的一个或是多个参数不变,那么可以使用partial,实例化一个传入了add和12参数的对象 from functools import partial def add(a, b, c): return a+b+c p = partial(add, 12) print(p(1, 2)) # 15 # print(p(1)) # 报错 TypeError: add() missing 1 required positional argument: 'c' print(p(2, 3)) # 17
# iter() 函数有一个鲜为人知的特性就是,如果你给它传递一个可调用对象和一个标记值,它会创建一个迭代器。
这个迭代器会一直调用传入的可调用对象直到它返回标记值为止,这时候迭代终止
# functools.partial 用来创建一个每次被调用时从文件中读取固定数目字节的可调用对象。 标记值 b'' 就是当到达文件结尾时的返回值
9:读取二进制数据到可变缓冲区中 直接读取二进制数据到一个可变缓冲区中 f.readinto()
# 读取数据到一个可变数组中,使用文件对象的 readinto() 方法 import os.path def read_into_buffer(filename): # os.path.getsize(filename) 获取文件filename的大小 # bytearray接收一个整数返回一个长度为 文件大小 的初始化字节数组buf buf = bytearray(os.path.getsize(filename)) with open(filename, 'rb') as f: f.readinto(buf) return buf # 返回一个字节数组 with open('sample.bin', 'wb') as f: f.write(b'Hello World') buf = read_into_buffer('sample.bin') print(buf) # bytearray(b'Hello World') print(buf[0:5]) # bytearray(b'Hello') with open('newsample.bin', 'wb') as f: f.write(buf) print(read_into_buffer('./aaaa')) 打印: bytearray(b'1111111111111111\r\n2222222222222222\r\n33333333333333333\r\n 44444444444444444\r\n55555555555555555\r\n6666666666666666\r\n777777777777777777\r\n 888888888888888888888\r\n999999999999999999999\r\n000000000000000000000\r\n') # 直接使用rb模式读取数据也能得到字节类型 with open("./aaaa", "rb") as f: print(f.read()) 输出: b'1111111111111111\r\n2222222222222222\r\n33333333333333333\r\n44444444444444444\r\n 55555555555555555\r\n6666666666666666\r\n777777777777777777\r\n 888888888888888888888\r\n999999999999999999999\r\n000000000000000000000\r\n'
# 文件对象的 readinto() 方法能被用来为预先分配内存的数组填充数据,包括由 array 模块或 numpy 库创建的数组,
和普通 read() 方法不同的是, readinto() 填充已存在的缓冲区而不是为新对象重新分配内存再返回它们。 因此,你可以使用它来避免大量的内存分配操作 # 读取一个由相同大小的记录组成的二进制文件时 record_size = 32 # 每个记录的大小(调整值) buf = bytearray(record_size) with open('somefile', 'rb') as f: while True: n = f.readinto(buf) if n < record_size: break # 使用buf的内容 ...
# memoryview:可以通过零复制的方式对已存在的缓冲区执行切片操作,甚至还能修改它的内容 import os size = os.path.getsize('./aaaa') # 11个字节 buf = bytearray(size) with open('./aaaa', "rb") as f: f.readinto(buf) # 填充这么多个字节 m1 = memoryview(buf) print(m1) # <memory at 0x0000019913A8A340> m2 = m1[-5:] # 取最后5位 print(m2) # <memory at 0x00000239B7A6E700> m2[:] = b'WORLD' # 在这里改变值相当于直接在buf上操作(因为memoryview是零复制的方式,所有的操作都是在原数据上面) print(buf) # bytearray(b'hello WORLD') # 使用 f.readinto() 必须检查它的返回值,也就是实际读取的字节数。 如果字节数小于缓冲区大小,表明数据被截断或者被破坏了(比如你期望每次读取指定数量的字节)。
10:内存映射的二进制文件 内存映射一个二进制文件到一个可变字节数组中,随机访问它的内容或者是原地做些修改 mmap
模块
# mmap 模块来内存映射文件 # 打开一个文件后内存映射这个文件 import os import mmap def memory_map(filename, access=mmap.ACCESS_WRITE): """ :param filename: 文件路径 :param access: :return: """ size = os.path.getsize(filename) fd = os.open(filename, os.O_RDWR) return mmap.mmap(fd, size, access=access) # 使用这个函数,需要有一个已创建并且内容不为空的文件 size = 1000000 with open('data', 'wb') as f: f.seek(size-1) f.write(b'\x00') # 利用 memory_map() 函数类内存映射文件内容的例子 m = memory_map('data') len(m) # 1000000 m[0:10] # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' m[0] # 0 m[0:11] = b'Hello World' m.close() with open('data', 'rb') as f: print(f.read(11)) # b'Hello World'
# os.open open是builtin函数,返回文件对象,此对象自带读写接口。 os.open是low-level接口,返回文件描述符(file descriptor),读写要使用 os.read或os.write接口。
flags – 该参数可以是以下选项,多个使用 “|” 隔开: os.O_RDONLY: 以只读的方式打开 os.O_WRONLY: 以只写的方式打开 os.O_RDWR : 以读写的方式打开 os.O_NONBLOCK: 打开时不阻塞 os.O_APPEND: 以追加的方式打开 os.O_CREAT: 创建并打开一个新文件 os.O_TRUNC: 打开一个文件并截断它的长度为零(必须有写权限) os.O_EXCL: 如果指定的文件存在,返回错误 os.O_SHLOCK: 自动获取共享锁 os.O_EXLOCK: 自动获取独立锁 os.O_DIRECT: 消除或减少缓存效果 os.O_FSYNC : 同步写入 os.O_NOFOLLOW: 不追踪软链接
import os, sys # 打开文件 fd = os.open( "foo.txt", os.O_RDWR|os.O_CREAT ) # 写入字符串 os.write(fd, "This is test") # 关闭文件 os.close( fd )
mmap是一种虚拟内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系 m=mmap.mmap(fileno, length[, flags[, prot[, access[, offset]]]])
fileno: 文件描述符,可以是file对象的fileno()方法,或者来自 os.open(),在调用mmap()之前打开文件,不再需要文件时要关闭。 os.O_RDONLY 以只读的方式打开 Read only os.O_WRONLY 以只写的方式打开 Write only os.O_RDWR 以读写的方式打开 Read and write os.O_APPEND 以追加的方式打开 os.O_CREAT 创建并打开一个新文件 os.O_EXCL os.O_CREAT| os.O_EXCL 如果指定的文件存在,返回错误 os.O_TRUNC 打开一个文件并截断它的长度为零(必须有写权限) os.O_BINARY 以二进制模式打开文件(不转换) os.O_NOINHERIT 阻止创建一个共享的文件描述符 os.O_SHORT_LIVED os.O_TEMPORARY 与O_CREAT一起创建临时文件 os.O_RANDOM 缓存优化,但不限制从磁盘中随机存取 os.O_SEQUENTIAL 缓存优化,但不限制从磁盘中序列存取 os.O_TEXT 以文本的模式打开文件(转换) length:要映射文件部分的大小(以字节为单位),这个值为0,则映射整个文件,如果大小大于文件当前大小,则扩展这个文件。 flags:MAP_PRIVATE:这段内存映射只有本进程可用;
mmap.MAP_SHARED:将内存映射和其他进程共享,所有映射了同一文件的进程,都能够看到其中一个所做的更改; prot:mmap.PROT_READ, mmap.PROT_WRITE 和 mmap.PROT_WRITE | mmap.PROT_READ。最后一者的含义是同时可读可写。 access:在mmap中有可选参数access的值有 ACCESS_READ:读访问。 ACCESS_WRITE:写访问,默认。 ACCESS_COPY:拷贝访问,不会把更改写入到文件,使用flush把更改写到文件。
# mmap 对象的方法 m.close() 关闭 m 对应的文件; m.find(str, start=0) 从 start 下标开始,在 m 中从左往右寻找子串 str 最早出现的下标; m.flush([offset, n]) 把 m 中从offset开始的n个字节刷到对应的文件中; m.move(dstoff, srcoff, n) 等于 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],把从 srcoff 开始的 n 个字节复制到从 dstoff 开始的n个字节,可能会覆盖重叠的部分。 m.read(n) 返回一个字符串,从 m 对应的文件中最多读取 n 个字节,将会把 m 对应文件的位置指针向后移动; m.read_byte() 返回一个1字节长的字符串,从 m 对应的文件中读1个字节,要是已经到了EOF还调用 read_byte(),则抛出异常 ValueError; m.readline() 返回一个字符串,从 m 对应文件的当前位置到下一个'\n',当调用 readline() 时文件位于 EOF,则返回空字符串; m.resize(n) ***有问题,执行不了*** 把 m 的长度改为 n,m 的长度和 m 对应文件的长度是独立的; m.seek(pos, how=0) 同 file 对象的 seek 操作,改变 m 对应的文件的当前位置; m.size() 返回 m 对应文件的长度(不是 m 对象的长度len(m)); m.tell() 返回 m 对应文件的当前位置; m.write(str) 把 str 写到 m 对应文件的当前位置,如果从 m 对应文件的当前位置到 m 结尾剩余的空间不足len(str),则抛出 ValueError; m.write_byte(byte) 把1个字节(对应一个字符)写到 m 对应文件的当前位置,实际上 m.write_byte(ch) 等于 m.write(ch)。
如果 m 对应文件的当前位置在 m 的结尾,也就是 m 对应文件的当前位置到 m 结尾剩余的空间不足1个字节,write() 抛出异常ValueError,而 write_byte() 什么都不做。
# mmap内存映射的使用 # m.close(),关闭对象 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) # 创建内存映射对象 os.O_RDWR:以读写的方式打开文件 print(m.read(10)) # b'-- MySQL d' m.close() # 关闭对象 print(m.read(10)) # 报错:ValueError: mmap closed or invalid # m.find(str, start=0),从start的位置开始寻找第一次出现的str import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) info = m.read().decode("utf8") print(info.find('SET')) # 203 print(m.find('SET'.encode('utf8'), 0)) # 203 # m.read(n),返回一个从 m对象文件中读取的n个字节的字符串,将会把 m 对象的位置指针向后移动,后续读取会继续往下读 # 现在m.read()输出的是字节 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.read(10)) # b'-- MySQL d' print(m.read(10)) # b'ump 10.13 ' # m.read_byte(),返回一个1字节长的字符串,从 m 对应的文件中读1个字节 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.read_byte()) # m.readline():返回一个字符串,从 m 对应文件的当前位置到下一个'\n',当调用 readline() 时文件位于 EOF,则返回空字符串 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.readline()) # b'-- MySQL dump 10.13 Distrib 5.6.19, for osx10.7 (x86_64)\r\n' print(m.readline()) # b'--\r\n' print(m.readline()) # b'-- Host: localhost Database: test\r\n' # m.size():返回 m 对应文件的长度(不是 m 对象的长度len(m)) import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.size()) # 796 # m.tell():返回 m 对应文件的当前光标位置 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.tell()) # 0 m.read(10) print(m.tell()) # 10 # m.seek(pos, how=0),改变 m 对应的文件的当前位置 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) m.seek(100) print(m.tell()) # 100 print(m.read(10)) # b'\n-- ------' # m.move(dstoff, srcoff, n):等于 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],把从 srcoff 开始的 n 个字节复制到从 dstoff 开始的n个字节 import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) info_1 = m[101:108] # b'-- ----' info_2 = m[1:8] # b'- MySQL' m.move(1, 101, 8) # 从101开始到后面的8字节(108),替换从1开始到后面的8字节(8)效果:m[1:8]=m[101:108] print(m[1:8]) # b'-- ----' # m.write(str):把 str 写到 m 对应文件的当前光标位置(覆盖对应长度),
如果从 m 对应文件的当前光标位置到 m 结尾剩余的空间不足len(str),则抛出 ValueError import os import mmap m = mmap.mmap(os.open('test.txt', os.O_RDWR), 0) print(m.tell()) # 0 m.write(b'zhoujy') # 写入str,要是写入的大小大于原本的文件,会报错。m.write_byte(byte)不会报错。 print(m.tell()) m.seek(0) print(m.read(10)) # b'zhoujy---d'
# m.flush():把 m 中从offset开始的n个字节刷到对应的文件中
对于m的修改操作,可以当成一个列表进行切片操作,但是对于切片操作的修改需要改成同样长度的字符串,
否则都会报错。如m中的10个字符串进行修改,必须改成10个字符的长度
# mmap读文件,ACCESS_READ # 读取整个文件 import mmap import contextlib f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as m: # readline需要循环才能读取整个文件 while True: line = m.readline().strip() print(line) # 光标到最后位置(读完),就退出 if m.tell() == m.size(): break # contextlib上下文管理器,协助对象调用完成后执行close()方法 # 逐步读取指定字节数文件 import mmap import contextlib with open('test.txt', 'r') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: print '读取10个字节的字符串 :', m.read(10) print '支持切片,对读取到的字符串进行切片操作:', m[2:10] print '读取之前光标后的10个字符串', m.read(10)
# mmap:查找文件,ACCESS_READ # 从整个文件查找所有匹配的 import mmap import contextlib word = 'ZHOUJY' print '查找:', word f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: #也可以通过find(str,pos)来处理 while True: line = m.readline().strip() if line.find(word)>=0: print "结果:" print line elif m.tell()==m.size(): break else: pass # 从整个文件里查找,找到就退出(确认到底是否存在) import mmap import contextlib word = 'ZHOUJY' print '查找:', word f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: #不需要循环,只要找到一个就可以了 loc = m.find(word) if loc >= 0: print loc print m[loc:loc+len(word)] # 通过正则查找,(找出40开头的数字) import mmap import re import contextlib pattern = re.compile(r'(40\d*)') with open('test.txt', 'r') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: print pattern.findall(m)
# mmap() 返回的 mmap 对象同样也可以作为一个上下文管理器来使用, 这时候底层的文件会被自动关闭 with memory_map('data') as m: print(len(m)) print(m[0:10]) m.closed # 查看m对象的关闭状态,返回True
# 默认情况下, memeory_map() 函数打开的文件同时支持读和写操作。 任何的修改内容都会复制回原来的文件中。
如果需要只读的访问模式,可以给参数 access 赋值为 mmap.ACCESS_READ m = memory_map(filename, mmap.ACCESS_READ) # 本地修改数据,但是又不想将修改写回到原始文件中,可以使用 mmap.ACCESS_COPY : m = memory_map(filename, mmap.ACCESS_COPY)
# 随机访问文件的内容,使用 mmap 将文件映射到内存中是一个高效和优雅的方法。
例如,你无需打开一个文件并执行大量的 seek() , read() , write() 调用, 只需要简单的映射文件并使用切片操作访问数据即 # mmap() 所暴露的内存看上去就是一个二进制数组对象。 但是,你可以使用一个内存视图来解析其中的数据 import os import mmap def memory_map(filename, access=mmap.ACCESS_WRITE): size = os.path.getsize(filename) fd = os.open(filename, os.O_RDWR) return mmap.mmap(fd, size, access=access) # size = 1000000 # with open('data2', 'wb') as f: # f.seek(size-1) # f.write(b'\x00') m = memory_map('data2') m[0:4] = b'\x07\x00\x00\x00' v = memoryview(m).cast('I') print(v) # <memory at 0x00000240A48CE700> print(v[0]) # 7 0x07对应十进制的7 # m[0:4] = b'\x07\x01\x00\x00' # print(v[0]) # 263 上面的memoryview生成的v对象取值的时候是小端优先的 v[0] = 7 = 0x00000007 v[0] = 263 = 0x00000107
# 内存映射一个文件并不会导致整个文件被读取到内存中
# 文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。
当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。
而那些从没被访问到的部分还是留在磁盘上。所有这些过程是透明的,在幕后完成!
# 如果多个Python解释器内存映射同一个文件,得到的 mmap 对象能够被用来在解释器直接交换数据。
也就是说,所有解释器都能同时读写数据,并且其中一个解释器所做的修改会自动呈现在其他解释器中。
很明显,这里需要考虑同步的问题。但是这种方法有时候可以用来在管道或套接字间传递数据
11:文件路径名的操作 os.path
模块中的函数来操作路径名
import os path = r'E:\Users\ywt\PycharmProjects\ywt_test\data.csv' print(os.path.basename(path)) # data.csv 获取路径的最后一个组件,也就是最后一个\后面的内容 print(os.path.dirname(path)) # E:\Users\ywt\PycharmProjects\ywt_test 获取目录名 print(os.path.join('tmp', 'data', os.path.basename(path))) # tmp\data\data.csv 将路径组件连接在一起 path = '~/Data/data.csv' print(os.path.splitext(path)) # ('~/Data/data', '.csv') 分割文件扩展名扩展名称位csv也就是得到一个csv格式
# 任何的文件名的操作,你都应该使用 os.path 模块,而不是使用标准字符串操作来构造自己的代码。 特别是为了可移植性考虑的时候更应如此,
# os.path 模块知道Unix和Windows系统之间的差异并且能够可靠地处理类似 Data/data.csv 和 Data\data.csv 这样的文件名。
# 不应该浪费时间去重复造轮子。通常最好是直接使用已经为你准备好的功能
12:测试文件是否存在 os.path.exists
# 测试一个文件或目录是否存在。 # 使用 os.path 模块来测试一个文件或目录是否存在 import os print(os.path.exists('/etc/passwd')) # False print(os.path.exists(r'./data.bin')) # True 可以测试相对路径存在不存在 print(os.path.exists(r'E:\Users\ywt\PycharmProjects\ywt_test\data.bin')) # True 可以测试绝对路径存在不存在
# 进一步测试这个文件时什么类型的
# 下面方法如果测试的文件不存在的时候,结果都会返回False import os # 是否是一个常规文件 print(os.path.isfile('./data.bin')) # True print(os.path.isfile(r'E:\Users\ywt\PycharmProjects\ywt_test\data.bin')) # True print(os.path.isfile(r'E:\Users\ywt\PycharmProjects\ywt_test')) # False # 是否是一个目录 print(os.path.isdir(r'E:\Users\ywt\PycharmProjects\ywt_test')) # True # 是否是一个符号链接(linux里面的软链接和硬链接使用这个方法) print(os.path.islink(r'E:\Program Files\Python39\python.exe')) # 获取链接到的文件的全路径 print(os.path.realpath('PycharmProjects\ywt_test\data.bin'))
# E:\Users\ywt\PycharmProjects\ywt_test\PycharmProjects\ywt_test\data.bin
# os.path获取元数据(比如文件大小或者是修改日期) import os import time print(os.path.getsize('./data.bin')) # 4 print(os.path.getmtime('./data.bin')) # 1628239825.8724988 返回一个创建文件的时间戳 print(time.ctime(os.path.getmtime('./data.bin'))) # Fri Aug 6 16:50:25 2021
# os.path 来进行文件测试是很简单的。 # 在写这些脚本时,可能唯一需要注意的就是你需要考虑文件权限的问题,特别是在获取元数据时候 >>> os.path.getsize('/Users/guido/Desktop/foo.txt') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.3/genericpath.py", line 49, in getsize return os.stat(filename).st_size PermissionError: [Errno 13] Permission denied: '/Users/guido/Desktop/foo.txt' >>> Permission denied没有权限报错,在Linux里才有这种情况
13:获取文件夹中的文件列表 os.listdir()
# 获取某个目录中的文件列表,os.listdir() import os print(os.listdir("./")) # 返回当前路径下的文件列表 # 结果会返回目录中所有文件列表,包括所有文件,子目录,符号链接等等 # 需要通过某种方式过滤数据,可以考虑结合 os.path 库中的一些函数来使用列表推导 import os.path # 收集所有常规文件 names = [name for name in os.listdir('somedir') if os.path.isfile(os.path.join('somedir', name))] # 得到所有dirs dirnames = [name for name in os.listdir('somedir') if os.path.isdir(os.path.join('somedir', name))]
# 字符串的 startswith() 和 endswith() 方法对于过滤一个目录的内容也是很有用的 pyfiles = [name for name in os.listdir('somedir') if name.endswith('.py')] # 对于文件名的匹配,使用 glob 或 fnmatch 模块 import glob pyfiles = glob.glob('somedir/*.py') from fnmatch import fnmatch pyfiles = [name for name in os.listdir('somedir') if fnmatch(name, '*.py')]
# glob:查找符合特定规则的文件路径名 # 查找文件只用到三个匹配符:”*”, “?”, “[]”。”*”匹配0个或多个字符;”?”匹配单个字符;”[]”匹配指定范围内的字符,如:[0-9]匹配数字 # glob 文件名模式匹配,用来判断每个文件是不是符合模式 import glob # 获取指定目录下的所有png格式的图片 print(glob.glob(r"E:/Users/ywt/PycharmProjects/*/*.png"), "\n") # 加上r让字符串不转义 # 输出['E:/Users/ywt/PycharmProjects\\ywt_test\\aaa.png'] # 获取上级目录的所有.py文件 print((glob.glob(r'../*.py'))) # 相对路径 # glob.iglob:获取一个可编历对象,使用它可以逐个获取匹配的文件路径名。与glob.glob()的区别是:
glob.glob同时获取所有的匹配路径,而glob.iglob一次只获取一个匹配路径 import glob f = glob.iglob(r'./*.py') print(f) # <generator object _iglob at 0x000001D492B95D60> 返回一个迭代器 for i in f: print(i, end=',') # .\script.py,.\test_001.py,.\test_002.py,.\test_003.py,
# fnmatch模块:主要用于文件名的匹配 # 匹配文件名,是否一样 fnmatch import fnmatch import os pattern = 'server_*.py' files = os.listdir('.') for name in sorted(files): print(fnmatch.fnmatch(name, pattern)) # 需要遍历的name里面有个文件以 server_xxxxx.py 文件匹配上了才会返回一个True # 区分文件名大小写的比较 fnmatchcase import fnmatch import os pattern = 'SERVER_*.PY' files = os.listdir('.') for name in sorted(files): print(fnmatch.fnmatchcase(name, pattern)) # 区分大小写,只有SERVERxxxx*.PY 的格式才会返回True # 从列表中过滤出匹配的文件名,返回一个列表 filter import fnmatch import os pattern = 'server_*.py' files = list(sorted(os.listdir('.'))) print(fnmatch.filter(files, pattern)) # ['server_001.py'] # 将指定好的规则转为成正则表达式 :translate import fnmatch pattern = 'server_*.py' print('正则表达式', fnmatch.translate(pattern)) # 正则表达式 (?s:server_.*\.py)\Z
# 获取目录中的列表是很容易的,但是其返回结果只是目录中实体名列表而已。 # 如果你还想获取其他的元信息,比如文件大小,修改时间等等,
你或许还需要使用到 os.path 模块中的函数或着 os.stat() 函数来收集数据 import os.path import glob pyfiles = glob.glob('*.py') # 匹配以.py结尾的文件 # 获取文件大小和修改日期 name_sz_data = [(name, os.path.getsize(name), os.path.getmtime(name))for name in pyfiles] for i in name_sz_data: print(i) # 输出: # ('script.py', 148, 1627553211.0650272) # ('server_001.py', 144, 1628497825.6993222) # ('test_001.py', 55, 1627970578.21603) # ('test_002.py', 427, 1626847282.9799867) # ('test_003.py', 504, 1628499035.9111543)
# 备选方案:获取文件元数据 file_metadata = [(name, os.stat(name)) for name in pyfiles] for i in file_metadata: print(i) # 输出: # ('script.py', os.stat_result(st_mode=33206, st_ino=21110623253303213, st_dev=1411808220, st_nlink=1, # st_uid=0, st_gid=0, st_size=148, st_atime=1627553211, st_mtime=1627553211, st_ctime=1627552974)) # ('server_001.py', os.stat_result(st_mode=33206, st_ino=23643898043699181, st_dev=1411808220, st_nlink=1, # st_uid=0, st_gid=0, st_size=144, st_atime=1628497825, st_mtime=1628497825, st_ctime=1628497825)) # ('test_001.py', os.stat_result(st_mode=33206, st_ino=15762598695800281, st_dev=1411808220, # st_nlink=1, st_uid=0, st_gid=0, st_size=55, st_atime=1627970578, st_mtime=1627970578, st_ctime=1622288185)) # ('test_002.py', os.stat_result(st_mode=33206, st_ino=1125899906846170, st_dev=1411808220, st_nlink=1, # st_uid=0, st_gid=0, st_size=427, st_atime=1626847282, st_mtime=1626847282, st_ctime=1622288191)) # ('test_003.py', os.stat_result(st_mode=33206, st_ino=19984723346460123, st_dev=1411808220, st_nlink=1, # st_uid=0, st_gid=0, st_size=673, st_atime=1628499192, st_mtime=1628499192, st_ctime=1622426524))
15:忽略文件名编码 使用原始文件名执行文件的I/O操作,也就是说文件名并没有经过系统默认编码去解码或编码过
# 默认情况下,所有的文件名都会根据 sys.getfilesystemencoding() 返回的文本编码来编码或解码 import sys print(sys.getfilesystemencoding()) # utf-8
# 某种原因忽略这种编码,可以使用一个原始字节字符串来指定一个文件名 import os with open('jalape\xf1o.txt', 'w') as f: f.write('Spicy!') # 目录列表(已解码) print(os.listdir('.')) # ['.idea', 'aaa.png', 'aaaa', 'bz2_test.bz2', 'cookbook_test', # 'data', 'data.bin', 'data.csv', 'data1', 'data2', 'gzip_test.gz', # 'jalapeño.txt', 'myfile.txt', 'newsample.bin', 'passwd', 'reba.jpg', # 'sample.bin', 'script.py', 'server_001.py', 'somefile', 'somefile1', 'test.txt', # 'test_001.py', 'test_002.py', 'test_003.py', '__pycache__'] # 目录列表(原始) print(os.listdir(b'.')) # [b'.idea', b'aaa.png', b'aaaa', b'bz2_test.bz2', b'cookbook_test', # b'data', b'data.bin', b'data.csv', b'data1', b'data2', b'gzip_test.gz', # b'jalape\xc3\xb1o.txt', b'myfile.txt', b'newsample.bin', b'passwd', # b'reba.jpg', b'sample.bin', b'script.py', b'server_001.py', b'somefile', # b'somefile1', b'test.txt', b'test_001.py', b'test_002.py', b'test_003.py', b'__pycache__'] with open(b'jalape\xc3\xb1o.txt') as f: print(f.read()) # Spicy! # 给文件相关函数如 open() 和 os.listdir() 传递字节字符串时,文件名的处理方式会稍有不同
# 不需要担心文件名的编码和解码,普通的文件名操作应该就没问题了。 # 但是,有些操作系统允许用户通过偶然或恶意方式去创建名字不符合默认编码的文件。
这些文件名可能会神秘地中断那些需要处理大量文件的Python程序 # 读取目录并通过原始未解码方式处理文件名可以有效的避免这样的问题
15:打印不合法的文件名 程序获取了一个目录中的文件名列表,打印文件名的时候程序崩溃, 出现了 UnicodeEncodeError
异常和一条消息—— surrogates not allowed
# 打印未知的文件名 def bad_filename(filename): return repr(filename)[1:-1] # repr() 函数将对象转化为供解释器读取的形式,返回一个对象的string格式 try: print(filename) except UnicodeEncodeError: print(bad_filename(filename))
使用repr得到一个文件名称供解释器读取的字符串
# 默认情况下,Python假定所有文件名都已经根据 sys.getfilesystemencoding() 的值编码过了 # 有一些文件系统并没有强制要求这样做,因此允许创建文件名没有正确编码的文件。 这种情况不太常见,
但是总会有些用户冒险这样做或者是无意之中这样做了( 可能是在一个有缺陷的代码中给 open() 函数传递了一个不合规范的文件名) # 当执行类似 os.listdir() 这样的函数时,这些不合规范的文件名就会让Python陷入困境。
一方面,它不能仅仅只是丢弃这些不合格的名字。而另一方面,它又不能将这些文件名转换为正确的文本字符串。
Python对这个问题的解决方案是从文件名中获取未解码的字节值比如 \xhh 并将它映射成Unicode字符 \udchh 表示的所谓的”代理编码” # 含有一个文件名为bäd.txt(使用Latin-1而不是UTF-8编码) >>> import os >>> files = os.listdir('.') >>> files ['spam.py', 'b\udce4d.txt', 'foo.txt'] >>> # 如果你有代码需要操作文件名或者将文件名传递给 open() 这样的函数,一切都能正常工作。
只有当你想要输出文件名时才会碰到些麻烦(比如打印输出到屏幕或日志文件等)。
特别的,当你想打印上面的文件名列表时,你的程序就会崩溃 for name in files: print(name) Traceback (most recent call last): File "<stdin>", line 2, in <module> UnicodeEncodeError: 'utf-8' codec can't encode character '\udce4' in position 1: surrogates not allowed # 程序崩溃的原因就是字符 \udce4 是一个非法的Unicode字符。
它其实是一个被称为代理字符对的双字符组合的后半部分。 由于缺少了前半部分,因此它是个非法的Unicode # 唯一能成功输出的方法就是当遇到不合法文件名时采取相应的补救措施 def bad_filename(filename): return repr(filename)[1:-1] for name in files: try: print(name) except UnicodeEncodeError: print(bad_filename(name)) # 在 bad_filename() 函数中怎样处置取决于你自己 # 另外一个选择就是通过某种方式重新编码 def bad_filename(filename): temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape') # 按照系统编码读取文件然后再解码(因为python默认都是unicode解码显示的) return temp.decode('latin-1') surrogateescape: 这种是Python在绝大部分面向OS的API中所使用的错误处理器, 它能以一种优雅的方式处理由操作系统提供的数据的编码问题。 在解码出错时会将出错字节存储到一个很少被使用到的Unicode编码范围内。 在编码时将那些隐藏值又还原回原先解码失败的字节序列。 它不仅对于OS API非常有用,也能很容易的处理其他情况下的编码错误。 >>> for name in files: ... try: ... print(name) ... except UnicodeEncodeError: ... print(bad_filename(name)) ... spam.py bäd.txt foo.txt >>>
16:增加或改变已打开文件的编码 在不关闭一个已打开的文件前提下增加或改变它的Unicode编码 io.TextIOWrapper()
# 给一个以二进制模式打开的文件添加Unicode编码/解码方式, 可以使用 io.TextIOWrapper() 对象包装它 import urllib.request import io u = urllib.request.urlopen('http://www.python.org') # print(u.read()) # 返回bytes类型 f = io.TextIOWrapper(u, encoding='utf-8') print(f.read(), type(f.read())) # 返回str字符串类型,而且是经过utf-8解码的
# 修改一个已经打开的文本模式的文件的编码方式,可以先使用 detach() 方法移除掉已存在的文本编码层, 并使用新的编码方式代替 # sys.stdout 上修改编码 import sys import io print(sys.stdout.encoding) # utf-8 sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='latin-1') print(sys.stdout) # <_io.TextIOWrapper name='<stdout>' encoding='latin-1'> # sys.stdout的编码已经改变了 # 这样做可能会中断你的终端,这里仅仅是为了演示而已。
# I/O系统由一系列的层次构建而成 f = open('./sample.txt', 'w') print(f) # <_io.TextIOWrapper name='./sample.txt' mode='w' encoding='cp936'> print(f.buffer) # <_io.BufferedWriter name='./sample.txt'> print(f.buffer.raw) # <_io.FileIO name='./sample.txt' mode='wb' closefd=True> # io.TextIOWrapper 是一个编码和解码Unicode的文本处理层 # io.BufferedWriter 是一个处理二进制数据的带缓冲的I/O层 # io.FileIO 是一个表示操作系统底层文件描述符的原始文件 # 增加或改变文本编码会涉及增加或改变最上面的 io.TextIOWrapper 层 # 通过访问属性值来直接操作不同的层是很不安全的 # 使用io.TextIOWrapper改变编码 import io f = open('./sample.txt', 'w') print(f) # <_io.TextIOWrapper name='./sample.txt' mode='w' encoding='cp936'> f = io.TextIOWrapper(f.buffer, encoding='latin-1') print(f) # <_io.TextIOWrapper name='./sample.txt' encoding='latin-1'> f.write('Hello') # ValueError: I/O operation on closed file. # write写入出错了,因为f的原始值已经被破坏了并关闭了底层的文件。
# detach() 方法会断开文件的最顶层并返回第二层,之后最顶层就没什么用了 f = open('sample.txt', 'w') print(f) # <_io.TextIOWrapper name='sample.txt' mode='w' encoding='cp936'> b = f.detach() print(b) # <_io.BufferedWriter name='sample.txt'> f.write('hello') write写入报错,最顶层断开了 # 一旦断开最顶层后,你就可以给返回结果添加一个新的最顶层 import io f = open('sample.txt', 'w') print(f) # <_io.TextIOWrapper name='sample.txt' mode='w' encoding='cp936'> b = f.detach() print(b) # <_io.BufferedWriter name='sample.txt'> # f.write('hello') f = io.TextIOWrapper(b, encoding='latin-1') print(f) # <_io.TextIOWrapper name='sample.txt' encoding='latin-1'> f.write('hello') # 现在write写入正常不会报错了
# 其他技术改变文件行处理、错误机制以及文件处理的其他方面 import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='ascii', errors='xmlcharrefreplace') print('Jalape\u00f1o') # Jalapeño # 最后输出中的非ASCII字符 ñ 是如何被 ñ 取代的 sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf8', errors='xmlcharrefreplace') print('Jalape\u00f1o') # Jalapeño
标准输出stdout为ascll码的时候输出非ascill码字符会呗转换成unicode输出
17:将字节写入文本文件 在文本模式打开的文件中写入原始的字节数据
# 将字节数据直接写入文件的缓冲区即可 import sys # sys.stdout.write(b'Hello\n') # 报错:TypeError: write() argument must be str, not bytes # sys.stdout.write('Hello\n') # 输出Hello sys.stdout.buffer.write(b'Hello\n')
# 读取文本文件的 buffer 属性来读取二进制数据 with open('ywt.txt', 'r') as f: print(f.buffer.read()) 输出字节:b'hello'
# I/O系统以层级结构的形式构建而成。 文本文件是通过在一个拥有缓冲的二进制模式文件上增加一个Unicode编码/解码层来创建。
buffer 属性指向对应的底层文件。如果你直接访问它的话就会绕过文本编码/解码层
# 默认情况下,sys.stdout 总是以文本模式打开的。
但是如果你在写一个需要打印二进制数据到标准输出的脚本的话,你可以使用上面演示的技术来绕过文本编码层
18:将文件描述符包装成文件对象 对应于操作系统上一个已打开的I/O通道(比如文件、管道、套接字等)的整型文件描述符, 你想将它包装成一个更高层的Python文件对象
# 一个文件描述符和一个打开的普通文件是不一样的。 os.open()打开返回的fd是文件描述符,open函数打开返回的f是文件对象
文件描述符仅仅是一个由操作系统指定的整数,用来指代某个系统的I/O通道。 如果你碰巧有这么一个文件描述符,
你可以通过使用 open() 函数来将其包装为一个Python的文件对象。 你仅仅只需要使用这个整数值的文件描述符作为第一个参数来代替文件名即可 import os # 打开低级文件描述符 fd = os.open('somefile.txt', os.O_WRONLY | os.O_CREAT) # fd输出3 # 变成一个合适的文件对象 f = open(fd, 'wt') f.write('hello world\n') f.close()
# 当高层的文件对象被关闭或者破坏的时候,底层的文件描述符也会被关闭。
如果这个并不是你想要的结果,你可以给 open() 函数传递一个可选的 colsefd=False # 创建文件对象,但完成后不要关闭基础fd f = open(fd, 'wt', closefd=False) ...
# Unix系统中,这种包装文件描述符的技术可以很方便的将一个类文件接口作用于一个以不同方式打开的I/O通道上, 如管道、套接字等 from socket import socket, AF_INET, SOCK_STREAM def echo_client(client_sock, addr): print('Got connection from', addr) # 为套接字读/写制作文本模式文件包装器 client_in = open(client_sock.fileno(), 'rt', encoding='latin-1', closefd=False) client_out = open(client_sock.fileno(), 'wt', encoding='latin-1', closefd=False) # 使用文件I/O将线路回显到客户端 for line in client_in: client_out.write(line) client_out.flush() client_sock.close()
client_sock.fileno():fileno返回文件描述符,返回一个整数,指向i/o文件通道
把文件描述符使用open函数转化成文件对象,client_in:读对象,client_out:写对象
def echo_server(address): sock = socket(AF_INET, SOCK_STREAM) sock.bind(address) sock.listen(1) while True: client, addr = sock.accept() echo_client(client, addr) # 想将一个类文件接口作用在一个套接字并希望你的代码可以跨平台,使用套接字对象的 makefile() 方法。
但是如果不考虑可移植性的话,那上面的解决方案会比使用 makefile() 性能更好一点
上面的例子仅仅适合在unix的系统
# 使用这种技术来构造一个别名,允许以不同于第一次打开文件的方式使用它 # 创建一个文件对象,它允许你输出二进制数据到标准输出(通常以文本模式打开) import sys # 为标准输出创建二进制模式文件 bstdout = open(sys.stdout.fileno(), 'wb', closefd=False) bstdout.write(b'Hello World\n') bstdout.flush() # 可以将一个已存在的文件描述符包装成一个正常的文件对象,
但是要注意的是并不是所有的文件模式都被支持,并且某些类型的文件描述符可能会有副作用 (特别是涉及到错误处理、文件结尾条件等等的时候)。
在不同的操作系统上这种行为也是不一样,特别的,上面的例子都不能在非Unix系统上运行。
19:创建临时文件和文件夹 程序执行时创建一个临时文件或目录,使用完之后可以自动销毁掉。 tempfile
模块
# 创建一个匿名的临时文件,可以使用 tempfile.TemporaryFile from tempfile import TemporaryFile with TemporaryFile('w+t') as f: # 读取/写入文件 f.write('Hello World\n') f.write('Testing\n') # 返回开始并读取数据 f.seek(0) print(f.read()) # 执行完成后临时文件f自动销毁
f = TemporaryFile('w+t')
f.close()
不使用with上下文管理器这样也可以使用
with TemporaryFile('w+t', encoding='utf-8', errors='ignore') as f:
...
第一个参数是文件模式,通常来讲文本模式使用 w+t ,二进制模式使用 w+b,这个模式同时支持读和写操作,
在这里是很有用的,因为当你关闭文件去改变模式的时候,文件实际上已经不存在了
TemporaryFile() 另外还支持跟内置的 open() 函数一样的参数encoding等
# 大多数Unix系统上,通过 TemporaryFile() 创建的文件都是匿名的,甚至连目录都没有。
想打破这个限制,使用 NamedTemporaryFile() 来代替 from tempfile import NamedTemporaryFile with NamedTemporaryFile('w+t') as f: print('filename is:', f.name) # 输出:filename is: C:\Users\ywt\AppData\Local\Temp\tmph8a_j0xn
# NamedTemporaryFile()创建一个临时文件,被打开文件的 f.name 属性包含了该临时文件的文件名,
将文件名传递给其他代码来打开这个文件的时候,这个就很有用了。 和 TemporaryFile() 一样,结果文件关闭时会被自动删除掉。
如果不想文件关闭就删除,可以传递一个关键字参数 delete=False from tempfile import NamedTemporaryFile with NamedTemporaryFile('w+t', delete=False) as f: print('filename is:', f.name) # 创建一个临时目录,可以使用 tempfile.TemporaryDirectory() from tempfile import TemporaryDirectory with TemporaryDirectory() as dirname: print('dirname is:', dirname) # 输出:dirname is: C:\Users\ywt\AppData\Local\Temp\tmpmpbmwkci # Use the directory
# TemporaryFile() 、NamedTemporaryFile() 和 TemporaryDirectory()
函数 应该是处理临时文件目录的最简单的方式了,因为它们会自动处理所有的创建和清理步骤 # mkstemp() 和 mkdtemp() 也可以来创建临时文件和目录 import tempfile print(tempfile.mkstemp()) # (3, 'C:\\Users\\ywt\\AppData\\Local\\Temp\\tmp5wk3s4pv') print(tempfile.mkdtemp()) # C:\Users\ywt\AppData\Local\Temp\tmp4q0yzqnm # mkstemp() 和 mkdtemp()这些函数并不会做进一步的管理了。函数 mkstemp() 仅仅就返回一个原始的OS文件描述符,
你需要自己将它转换为一个真正的文件对象。 同样你还需要自己清理这些文件
# 临时文件在系统默认的位置被创建,比如 /var/tmp 或类似的地方。 获取真实的位置,可以使用 tempfile.gettempdir() 函数 import tempfile print(tempfile.gettempdir()) # C:\Users\ywt\AppData\Local\Temp # 所有和临时文件相关的函数都允许你通过使用关键字参数 prefix 、suffix 和 dir 来自定义目录以及命名规则 from tempfile import NamedTemporaryFile f = NamedTemporaryFile("rt", prefix='mytemp', suffix='.txt', dir='E:') print(f.name) # E:\Users\ywt\PycharmProjects\ywt_test\mytemp6xm9vwqf.txt
20:与串行端口的数据通信 串行端口读写数据,典型场景就是和一些硬件设备打交道(比如一个机器人或传感器)。
# Python内置的I/O模块可以和串行端口读写数据 # 对于串行通信最好的选择是使用 pySerial包 # pySerial需要pip安装,不是py自带的 # 打开一个串行端口 import serial ser = serial.Serial('/dev/tty.usbmodem641', # Device name varies baudrate=9600, bytesize=8, parity='N', stopbits=1) # 设备名对于不同的设备和操作系统是不一样的。在Windows系统上,你可以使用0, 1等表示的一个设备来打开通信端口”COM0”和”COM1”。
一旦端口打开,那就可以使用 read(),readline() 和 write() 函数读写数据了 import serial ser = serial.Serial('/dev/tty.usbmodem641', # Device name varies baudrate=9600, bytesize=8, parity='N', stopbits=1) ser.write(b'G1 X50 Y50\r\n') resp = ser.readline()
# 类似一个摄像头使用串口线连接到电脑,serial模块生成一个类代替xshell的功能,
去和摄像头进行串口通信,读取串口信息,串口输入命令
# pySerial 它提供了对高级特性的支持 (比如超时,控制流,缓冲区刷新,握手协议等等)。
比如你想启用 RTS-CTS 握手协议, 你只需要给 Serial() 传递一个 rtscts=True 的参数即可。
# 所有涉及到串口的I/O都是二进制模式的。因此,确保你的代码使用的是字节而不是文本
(或有时候执行文本的编码/解码操作)。 另外当你需要创建二进制编码的指令或数据包的时候,使用struct 模块
21:序列化Python对象 pickle
模块
# 一个Python对象序列化为一个字节流,以便将它保存到一个文件、存储到数据库或者通过网络传输它 # 序列化pickle 模块,可以将一个对象保存到一个文件中 import pickle class A: pass data = A() f = open('somefile', 'wb') pickle.dump(data, f) # 把A这个类的实例化对象序列化到somefile这个文件
# pickle.dump(data, f) 直接把data这个对象序列化到f文件句柄 # pickle.dumps(data) 将一个对象转储为一个字符串 import pickle class A: pass data = A() print(pickle.dumps(data)) # 输出:b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01A\x94\x93\x94)\x81\x94.'
# 从字节流中恢复一个对象,使用 pickle.load() 或 pickle.loads() # 从文件还原 import pickle class A: def print_a(self): print("a") data = A() f = open('somefile', 'wb') pickle.dump(data, f) # 从文件还原 f = open('somefile', 'rb') data = pickle.load(f) data.print_a() # 从字符串还原 import pickle class A: def print_a(self): print("a") data = A() s = pickle.dumps(data) data = pickle.loads(s) data.print_a()
# pickle 模块适用于绝大部分Python数据类型和用户自定义类的对象实例。
如果你碰到某个库可以让你在数据库中保存/恢复Python对象或者是通过网络传输对象的话,
那么很有可能这个库的底层就使用了 pickle 模块 # pickle 是一种Python特有的自描述的数据编码。 通过自描述
,被序列化后的数据包含每个对象开始和结束以及它的类型信息。无需担心对象记录的定义,它总是能工作 # pickle处理多个对象 import pickle f = open('somedata', 'wb') pickle.dump([1, 2, 3, 4], f) pickle.dump('hello', f) pickle.dump({'Apple', 'Pear', 'Banana'}, f) f.close() f = open('somedata', 'rb') print(pickle.load(f)) # [1, 2, 3, 4] print(pickle.load(f)) # hello print(pickle.load(f)) # {'Apple', 'Pear', 'Banana'}
# pickle还能序列化函数,类,还有接口,但是结果数据仅仅将它们的名称编码成对应的代码对象 import math import pickle print(pickle.dumps(math.cos)) # b'\x80\x04\x95\x10\x00\x00\x00\x00\x00\x00\x00\x8c\x04math\x94\x8c\x03cos\x94\x93\x94.' # 当数据反序列化回来的时候,会先假定所有的源数据是可用的。 模块、类和函数会自动按需导入进来。
对于Python数据被不同机器上的解析器所共享的应用程序而言, 数据的保存可能会有问题,因为所有的机器都必须访问同一个源代码 千万不要对不信任的数据使用pickle.load()。 pickle在加载时有一个副作用就是它会自动加载相应模块并构造实例对象。 但是某个坏人如果知道pickle的工作原理, 他就可以创建一个恶意的数据导致Python执行随意指定的系统命令。 因此,一定要保证pickle只在相互之间可以认证对方的解析器的内部使用。
# 有些类型的对象是不能被序列化的。这些通常是那些依赖外部系统状态的对象,
比如打开的文件,网络连接,线程,进程,栈帧等等。 用户自定义类可以通过提供 __getstate__() 和 __setstate__() 方法来绕过这些限制。
如果定义了这两个方法,pickle.dump() 就会调用 __getstate__() 获取序列化的对象。 类似的,__setstate__() 在反序列化时被调用。 # 一个在内部定义了一个线程但仍然可以序列化和反序列化的类 import time import threading import pickle class Countdown: def __init__(self, n): self.n = n self.thr = threading.Thread(target=self.run) self.thr.daemon = True # 设置守护线程,守护进程会在主进程代码执行结束后就终止 self.thr.start() def run(self): while self.n > 0: print('T-minus', self.n) self.n -= 1 time.sleep(1) def __getstate__(self): return self.n def __setstate__(self, n): self.__init__(n) c = Countdown(30) # 输出:T-minus 30,主线程很快就结束了设置了守护线程所以子线程马上也gg time.sleep(10) f = open('cstate.p', 'wb') pickle.dump(c, f) f.close() 打印:30-21 # 上面创建一个类Countdown的实例化对象,然后pickle序列化倒了文件cstate.p # 上面程序运行完成后继续跑下面的代码 import time import threading import pickle class Countdown: def __init__(self, n): self.n = n self.thr = threading.Thread(target=self.run) self.thr.daemon = True # 设置守护线程,守护进程会在主进程代码执行结束后就终止 self.thr.start() def run(self): while self.n > 0: print('T-minus', self.n) self.n -= 1 time.sleep(1) def __getstate__(self): return self.n def __setstate__(self, n): self.__init__(n) f = open('cstate.p', "rb") pickle.load(f) time.sleep(20) 打印:20-1
上面的类Countdown自己定义了实现了getstate和setstate两个双下方法,pickle.dump的时候调用getstate写入文件,
pick,load的时候调用setstate传入记录下来的写入的n参数
上面的方法类似线程又奇迹般的重生了,从你第一次序列化它的地方又恢复过来。记录了上次线程运行的状态
# pickle 对于大型的数据结构比如使用 array 或 numpy 模块创建的二进制数组效率并不是一个高效的编码方式。
如果需要移动大量的数组数据,最好是先在一个文件中将其保存为数组数据块或使用更高级的标准编码方式如HDF5 (需要第三方库的支持)
# pickle 是Python特有的并且附着在源码上,所有如果需要长期存储数据的时候不应该选用它。
例如,如果源码变动了,你所有的存储数据可能会被破坏并且变得不可读取。 坦白来讲,对于在数据库和存档文件中存储数据时,
你最好使用更加标准的数据编码格式如XML,CSV或JSON。 这些编码格式更标准,可以被不同的语言支持,并且也能很好的适应源码变更
22:读写CSV数据
stocks.csv 文件名称+内容 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
# 读取csv文件里面的数据,读取为一个元组的序列 import csv with open('stocks.csv') as f: f_csv = csv.reader(f) # print(f_csv) # <_csv.reader object at 0x000001E868C1E1C0>返回一个文件对象,迭代器 headers = next(f_csv) for i in f_csv: print(i, type(i)) 输出: ['AA', '39.48', '6/11/2007', '9:36am', '-0.18', '181800'] <class 'list'> ['AIG', '71.38', '6/11/2007', '9:36am', '-0.15', '195500'] <class 'list'> ['AXP', '62.58', '6/11/2007', '9:36am', '-0.46', '935000'] <class 'list'> ['BA', '98.31', '6/11/2007', '9:36am', '+0.12', '104800'] <class 'list'> ['C', '53.08', '6/11/2007', '9:36am', '-0.25', '360900'] <class 'list'> ['CAT', '78.29', '6/11/2007', '9:36am', '-0.23', '225400'] <class 'list'> # 返回的i是一个列表,可以使用下标取值
# 命名元组读取csv格式 import csv from collections import namedtuple with open('stocks.csv') as f: f_csv = csv.reader(f) headings = next(f_csv) # ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume'] Row = namedtuple('Row', headings) for r in f_csv: row = Row(*r) print(row) print(row.Symbol) 输出: Row(Symbol='AA', Price='39.48', Date='6/11/2007', Time='9:36am', Change='-0.18', Volume='181800') AA Row(Symbol='AIG', Price='71.38', Date='6/11/2007', Time='9:36am', Change='-0.15', Volume='195500') AIG Row(Symbol='AXP', Price='62.58', Date='6/11/2007', Time='9:36am', Change='-0.46', Volume='935000') AXP Row(Symbol='BA', Price='98.31', Date='6/11/2007', Time='9:36am', Change='+0.12', Volume='104800') BA Row(Symbol='C', Price='53.08', Date='6/11/2007', Time='9:36am', Change='-0.25', Volume='360900') C Row(Symbol='CAT', Price='78.29', Date='6/11/2007', Time='9:36am', Change='-0.23', Volume='225400') CAT # 这个只有在列名是合法的Python标识符的时候才生效。如果不是的话, 你可能需要修改下原始的列名(如将非标识符字符替换成下划线之类的)
# 将csv数据读取到一个字典序列中去 import csv with open('stocks.csv') as f: csv_f = csv.DictReader(f) for i in csv_f: print(i) 输出: {'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'} {'Symbol': 'BA', 'Price': '98.31', 'Date': '6/11/2007', 'Time': '9:36am', 'Change': '+0.12', 'Volume': '104800'} {'Symbol': 'C', 'Price': '53.08', 'Date': '6/11/2007', 'Time': '9:36am', 'Change': '-0.25', 'Volume': '360900'} {'Symbol': 'CAT', 'Price': '78.29', 'Date': '6/11/2007', 'Time': '9:36am', 'Change': '-0.23', 'Volume': '225400'} 自己组合一个字典,把第一行目录和下面的每一行
字典通过列名访问数据:row['Symbol']
或者row['Change']
# 写入CSV数据,先创建一个 writer 对象
import csv
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('stocks1.csv', 'w') as f:
f_csv = csv.writer(f)
f_csv.writerow(headers)
f_csv.writerows(rows)
# 字典序列的数据的写入 import csv 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('stocks3.csv', 'w') as f: f_csv = csv.DictWriter(f, headers) # 创建一个write对象 f_csv.writeheader() # 写入头 f_csv.writerows(rows) # 写入行
# 以tab分割的数据 import csv with open('stocks.csv') as f: f_tsv = csv.reader(f, delimiter='\t') # 默认每行以,分割数据,现在改成\t分割数据 for row in f_tsv: print(row)
# 读取CSV数据并将它们转换为命名元组,对列名进行合法性认证。 例如,一个CSV格式文件有一个包含非法标识符的列头行 Street Address,Num-Premises,Latitude,Longitude 5412 N CLARK,10,41.980262,-87.668452 创建一个命名元组时产生一个 ValueError 异常而失败 # 在非法标识符上使用一个正则表达式替换 import csv import re from collections import namedtuple with open('stocks.csv') as f: f_csv = csv.reader(f) headers = [re.sub('[^a-zA-Z_]', '_', h) for h in next(f_csv)] print(headers) Row = namedtuple('Row', headers) # 正则表达式把所有非字母的替换成_,所以可以创建命名元组
# csv产生的数据都是字符串类型的,它不会做任何其他类型的转换。 如果你需要做这样的类型转换,必须自己手动去实现 # CSV数据上执行其他类型转换 import csv 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: row = tuple(convert(value) for convert, value in zip(col_types, row))
# 转换字典中特定字段 import csv 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): # 把字典数据根据field_types里的key找到对应的字段然后对字段使用对应的值的转化函数然后update到row每行里面 row.update((key, conversion(row[key]))for key, conversion in field_types) print(row) # CSV数据的目的是做数据分析和统计。Pandas模块,Pandas 包含了一个非常方便的函数叫 pandas.read_csv() ,
它可以加载CSV数据到一个 DataFrame 对象中去。 然后利用这个对象你就可以生成各种形式的统计、过滤数据以及执行其他高级操作了
23:读写JSON数据 JSON(JavaScript Object Notation)编码格式的数据。
# json 模块提供了一种很简单的方式来编码和解码JSON数据,json.dumps() 和 json.loads() , 要比其他序列化函数库如pickle的接口少得多 # Python数据结构转换为JSON import json data = { 'name' : 'ACME', 'shares' : 100, 'price' : 542.23 } print(json.dumps(data), type(json.dumps(data))) # {"name": "ACME", "shares": 100, "price": 542.23} <class 'str'> # JSON编码的字符串转换回一个Python数据结构 data = json.loads(json_str) # 要处理的是文件而不是字符串,你可以使用 json.dump() 和 json.load() 来编码和解码JSON数据 with open('data.json', 'w') as f: json.dump(data, f) with open('data.json', 'r') as f: data = json.load(f)
# JSON编码支持的基本数据类型为 None , bool , int , float 和 str ,
以及包含这些类型数据的lists,tuples和dictionaries。 对于dictionaries,keys需要是字符串类型(字典中任何非字符串类型的key在编码时会先转换为字符串)。
为了遵循JSON规范,你应该只编码Python的lists和dictionaries。 而且,在web应用程序中,顶层对象被编码为一个字典是一个标准做法 # JSON编码的格式对于Python语法而已几乎是完全一样的,除了一些小的差异之外。
比如,True会被映射为true,False被映射为false,而None会被映射为null import json print(json.dumps(False)) # false d = {'a': True, 'b': 'Hello', 'c': None} print(json.dumps(d)) # {"a": true, "b": "Hello", "c": null}
# 完美打印:pprint模块的 pprint() 函数来代替普通的 print() 函数 # 它会按照key的字母顺序并以一种更加美观的方式输出 from urllib.request import urlopen import re import pprint import json ip_addr = '10.10.10.10' u = urlopen(f"https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query={ip_addr}" f"&co=&resource_id=5809&t=1628673876520&ie=utf8&oe=gbk&cb=" f"op_aladdin_callback&format=json&tn=baidu&cb=jQuery110207887838466598913_1628673863705&_=1628673863706") reps = json.loads(re.search('\((.*?)\)', u.read().decode('utf8')).group(1)) pprint.pprint(reps)
# JSON解码会根据提供的数据创建dicts或lists。 如果你想要创建其他类型的对象,
可以给 json.loads() 传递object_pairs_hook或object_hook参数 # 解码JSON数据并在一个OrderedDict中保留其顺序 import json from collections import OrderedDict # 有序字典 s = '{"name": "ACME", "shares": 50, "price": 490.1}' data = json.loads(s, object_pairs_hook=OrderedDict) print(data) 输出:OrderedDict([('name', 'ACME'), ('shares', 50), ('price', 490.1)])
# 将一个JSON字典转换为一个Python对象例子: import json class JSONObject: def __init__(self, d): self.__dict__ = d s = '{"name": "ACME", "shares": 50, "price": 490.1}' data = json.loads(s, object_hook=JSONObject) print(data) # <__main__.JSONObject object at 0x000001E6106E9F40> print(data.name) # ACME print(data.shares) # 50 print(data.price) # 490.1 # JSON解码后的字典作为一个单个参数传递给 __init__() 。 然后,你就可以随心所欲的使用它了,比如作为一个实例字典来直接使用它
class JSONObject:
def __init__(self, d):
self.__dict__ = d
data = JSONObject({"a": 10})
print(data.a) # 10
# 编码JSON的时候,想获得漂亮的格式化字符串后输出,使用 json.dumps() 的indent参数 import json s = {"price": 542.23, "name": "ACME", "shares": 100} print(json.dumps(s)) # {"price": 542.23, "name": "ACME", "shares": 100} print(json.dumps(s, indent=4)) # 输出 { "price": 542.23, "name": "ACME", "shares": 100 } ¥ indent代表字典输出每一行间隔前面多少个字符长度
# 对象实例通常并不是JSON可序列化的 import json class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(2, 3) print(json.dumps(p)) # 报错:TypeError: Object of type Point is not JSON serializable
# 想序列化对象实例,你可以提供一个函数,它的输入是一个实例,返回一个可序列化的字典 def serialize_instance(obj): d = { '__classname__' : type(obj).__name__ } d.update(vars(obj)) return d # 反过来获取这个实例 # 将名称映射到已知类的字典 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 class Point: def __init__(self, x, y): self.x = x self.y = y # 具体使用 p = Point(2, 3) s = json.dumps(p, default=serialize_instance) print(s, type(s)) # {"__classname__": "Point", "x": 2, "y": 3} <class 'str'> a = json.loads(s, object_hook=unserialize_object) # s转成一个字典后参数传递给unserialize_object函数生成一个对象 print(a) # <__main__.Point object at 0x00000262E29BCEE0> print(a.x) print(a.y)
# 序列化类对象 import json import sys class Point: def __init__(self, x, y): self.x = x self.y = y def serialize_instance(obj): d = {'__classname__': type(obj).__name__} d.update(vars(obj)) # vars() 函数返回对象object的属性和属性值的字典对象。如果没有参数,就打印当前调用位置的属性和属性值 类似 locals()。 return d def unserialize_object(d): clsname = d.pop('__classname__', None) if clsname: cls = getattr(sys.modules["__main__"], clsname) obj = cls.__new__(cls) # 生成实例而不调用_init__ for key, value in d.items(): setattr(obj, key, value) return obj else: return d p = Point(2, 3) s = json.dumps(p, default=serialize_instance) print(s, type(s)) # {"__classname__": "Point", "x": 2, "y": 3} <class 'str'> a = json.loads(s, object_hook=unserialize_object) # s转成一个字典后参数传递给unserialize_object函数生成一个对象 print(a) # <__main__.Point object at 0x00000262E29BCEE0> print(a.x) print(a.y)
一个实例化对象p,json.dumps(p, default=serialize_instance),
先使用default=serialize_instanc这个函数把实例化对象p转成一个字典,字典里有类名和各种属性,然后把这个字典dumps一个类似字典的字符串
然后调用json.loads(s, object_hook=unserialize_object)把这个类似字典的字符串激活成一个类的实例,
24:解析简单的XML数据 从一个简单的XML文档中提取数据 xml.etree.ElementTree
# xml.etree.ElementTree 模块从简单的XML文档中提取数据 from urllib.request import urlopen from xml.etree.ElementTree import parse # 下载RSS提要并对其进行解析 u = urlopen('http://planet.python.org/rss20.xml') doc = parse(u) # 提取并输出感兴趣的标签 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()
<?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() 函数解析整个XML文档并将其转换成一个文档对象 使用 find() 、iterfind() 和 findtext() 等方法来搜索特定的XML元素了,
这些函数的参数就是某个指定的标签名,例如 channel/item 或 title 指定某个标签时,你需要遍历整个文档结构。每次搜索操作会从一个起始元素开始进行。
同样,每次操作所指定的标签名也是起始元素的相对路径。 例如,执行 doc.iterfind('channel/item')
来搜索所有在 channel 元素下面的 item 元素。 doc 代表文档的最顶层(也就是第一级的 rss 元素)。
然后接下来的调用 item.findtext() 会从已找到的 item 元素位置开始搜索
# ElementTree 模块中的每个元素有一些重要的属性和方法,在解析的时候非常有用。
tag 属性包含了标签的名字,text 属性包含了内部的文本,而 get() 方法能获取属性值 from urllib.request import urlopen from xml.etree.ElementTree import parse # 下载RSS提要并对其进行解析 u = urlopen('http://planet.python.org/rss20.xml') doc = parse(u) print(doc) # <xml.etree.ElementTree.ElementTree object at 0x00000209B5B8BA00> e = doc.find('channel/title') # <Element 'title' at 0x0000029496F03130> print(e.tag) # title print(e.text) # Planet Python print(e.get('some_attribute')) # None
25:增量式解析大型XML文件 迭代器和生成器来提取数据
from xml.etree.ElementTree import iterparse def parse_and_remove(filename, path): path_parts = path.split('/') doc = iterparse(filename, ('start', 'end')) # 跳过根元素 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
# 写一个脚本来按照坑洼报告数量排列邮编号码 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文件加载到内存中然后解析
# 修改版本
from xml.etree.ElementTree import iterparse
from collections import Counter
# parse_and_remove('country.xml', 'country/year')
def parse_and_remove(filename, path):
path_parts = path.split('/') # ['country', 'year']
doc = iterparse(filename, ('start', 'end'))
# 跳过根元素
next(doc)
tag_stack = [] # 元素tag标签
elem_stack = [] # 元素对象
for event, elem in doc:
if event == 'start':
tag_stack.append(elem.tag)
elem_stack.append(elem)
elif event == 'end':
# print(tag_stack)
if tag_stack == path_parts:
yield elem
print('-----', elem_stack[-2])
elem_stack[-2].remove(elem) # yield 产生的元素从它的父节点中删除掉,这个元素就被销毁并回收内存
try:
tag_stack.pop() # 删除最后一个元素
elem_stack.pop() # 删除最后一个元素
except IndexError:
pass
类似一个栈的方式返回元素,找标签的一半,标签前面标注为start,结尾标注为end,根据上下标签关系匹配元素
potholes_by_zip = Counter()
data = parse_and_remove('country.xml', 'country/year') # 找country标签下的year标签
for pothole in data:
potholes_by_zip[pothole.findtext('zip')] += 1 # 计数器统计pothole标签下第一个匹配的子节点的text文本出现的次数
# potholes_by_zip[pothole.findtext('aaa')] += 1
# print(pothole.findtext('aaa')) # element.findtext(match, default=None):获取第一个匹配的子节点的text
for zipcode, num in potholes_by_zip.most_common(): # 找到计数器最大的一个元素打印
print(zipcode, num)
# collections.Counter计数器 # Counter实现计数 from collections import Counter colors = ['red', 'blue', 'red', 'green', 'blue', 'blue'] c = Counter(colors) print(dict(c)) # {'red': 2, 'blue': 3, 'green': 1} # Counter操作 cnt = Counter() # 创建一个空的Counter,之后在空的Counter上进行一些操作 c1 = Counter('gallahad') # 传进字符串 c2 = Counter({'red': 4, 'blue': 2}) # 传进字典 c3 = Counter(cats=4, dogs=8) # 传进元组 # 判断是否包含某元素,可以转化为dict然后通过dict判断,包含返回1,不包含返回0 c = Counter(['eggs', 'ham']) # Counter({'eggs': 1, 'ham': 1}) print(c['eggs']) # 1 print(c['plans']) # 0 # 删除元素 c = Counter(['eggs', 'ham']) # Counter({'eggs': 1, 'ham': 1}) c['sausage'] = 0 print(c) # Counter({'eggs': 1, 'ham': 1, 'sausage': 0}) del c['sausage'] print(c) # Counter({'eggs': 1, 'ham': 1}) # 获得所有元素:c.elements c = Counter(a=4, b=2, c=0, d=-2) # Counter({'a': 4, 'b': 2, 'c': 0, 'd': -2}) print(c.elements()) # <itertools.chain object at 0x000001EA30BAB9A0> 返回一个迭代器 print(list(c.elements())) # ['a', 'a', 'a', 'a', 'b', 'b'] # 查看最常见出现的k个元素:most_common(3) print(Counter('abracadabra').most_common(3)) # [('a', 5), ('b', 2), ('r', 2)] # Counter更新 c = Counter(a=3, b=1) d = Counter(a=1, b=2) print(c + d) # 相加:Counter({'a': 4, 'b': 3}) print(c - d) # 相减,如果小于等于0,删去:Counter({'a': 2}) print(c & d) # 求最小:Counter({'a': 1, 'b': 1}) print(c | d) # 求最大:Counter({'a': 3, 'b': 2})
# ElementTree 模块中的两个核心功能 # iterparse() 方法允许对XML文档进行增量操作。
使用时,你需要提供文件名和一个包含下面一种或多种类型的事件列表: start , end, start-ns 和 end-ns 。 由 iterparse() 创建的迭代器会产生形如 (event, elem) 的元组, 其中 event 是上述事件列表中的某一个,而 elem 是相应的XML元素 from xml.etree.ElementTree import iterparse data = iterparse('potholes.xml', ('start', 'end')) print(next(data)) # ('start', <Element 'response' at 0x0000025A38FC7B30>) print(next(data)) # ('start', <Element 'row' at 0x0000024477316A90>) # start 事件在某个元素第一次被创建并且还没有被插入其他数据(如子元素)时被创建。
而 end 事件在某个元素已经完成时被创建。 # start 和 end 事件被用来管理元素和标签栈。 栈代表了文档被解析时的层次结构,
还被用来判断某个元素是否匹配传给函数 parse_and_remove() 的路径。 如果匹配,就利用 yield 语句向调用者返回这个元素 # 在 yield 之后的下面这个语句才是使得程序占用极少内存的ElementTree的核心特性 elem_stack[-2].remove(elem) # 这个语句使得之前由 yield 产生的元素从它的父节点中删除掉。
假设已经没有其它的地方引用这个元素了,那么这个元素就被销毁并回收内存 # 对节点的迭代式解析和删除的最终效果就是一个在文档上高效的增量式清扫过程。
文档树结构从始自终没被完整的创建过。尽管如此,还是能通过上述简单的方式来处理这个XML数据 # 这种方案的主要缺陷就是它的运行性能了,读取整个文档到内存中的版本的运行速度差不多是增量式处理版本的两倍快。
但是它却使用了超过后者60倍的内存。 因此,如果你更关心内存使用量的话,那么增量式的版本完胜
iterparse()方法的原理是当遇到标签的“>”符号时触发start,当遇到标签的结束标志是会触发end,
# python 使用ElementTree解析xml xml内容如下: <?xml version="1.0"?> <data> <country name="Liechtenstein"> <rank updated="yes">2</rank> <year>2008</year> <gdppc>141100</gdppc> <neighbor name="Austria" direction="E"/> <neighbor name="Switzerland" direction="W"/> </country> <country name="Singapore"> <rank updated="yes">5</rank> <year>2011</year> <gdppc>59900</gdppc> <neighbor name="Malaysia" direction="N"/> </country> <country name="Panama"> <rank updated="yes">69</rank> <year>2011</year> <gdppc>13600</gdppc> <neighbor name="Costa Rica" direction="W"/> <neighbor name="Colombia" direction="E"/> </country> </data> # 1:调用parse()方法,返回解析树 try: import xml.etree.cElementTree as ET # 优先导入c库的,后面的版本才有 except ImportError: import xml.etree.ElementTree as ET tree = ET.parse("country.xml") # <class 'xml.etree.ElementTree.ElementTree'> root = tree.getroot() # 获取根节点 <Element 'data' at 0x02BF6A80> # 2:调用from_string(),返回解析树的根元素 try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET data = open("country.xml").read() root = ET.fromstring(data) # <Element 'data' at 0x036168A0> # 3:调用ElementTree类ElementTree(self, element=None, file=None) # 这里的element作为根节点 try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET tree = ET.ElementTree(file="country.xml") # <xml.etree.ElementTree.ElementTree object at 0x03031390> root = tree.getroot() # <Element 'data' at 0x030EA600> # 4:简单遍历 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() print(root.tag, ":", root.attrib) # 打印根元素的tag和属性 # 遍历xml文档的第二层 for child in root: # 第二层节点的标签名称和属性 print(child.tag, ":", child.attrib) # 遍历xml文档的第三层 for children in child: # 第三层节点的标签名称和属性 print(children.tag, ":", children.attrib) # 5:通过下标的方式直接访问节点 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() year = root[0][1].text print(year) # root表示根也就是一级元素,一级元素下的第一个二级,第一个二级下的第二个三级的text文本 # 访问根节点下第一个country的第二个节点year,获取对应的文本 # 6:ElementTree提供的方法 find(match) # 查找第一个匹配的子元素, match可以时tag或是xpaht路径 findall(match) # 返回所有匹配的子元素列表 findtext(match, default=None) # iter(tag=None)# 以当前元素为根节点 创建树迭代器,如果tag不为None,则以tag进行过滤 iterfind(match) # 7:过滤出所有neighbor标签 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() for neighbor in root.iter("neighbor"): print(neighbor.tag, ":", neighbor.attrib) # 根据neighbor过滤根内部所有的neighbor标签 # 8:遍历所有的counry标签 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() for country in root.findall("country"): # 查找country标签下的第一个rank标签 rank = country.find("rank").text # 获取country标签的name属性 name = country.get("name") print(name, rank) 这样遍历所有的contry也可以 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() for con in root.iter('country'): print(f"{con.get('name')}:{con.find('rank').text}") 9:修改xml结构 # 将所有的rank值加1,并添加属性updated为yes import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() for rank in root.iter("rank"): new_rank = int(rank.text) + 1 rank.text = str(new_rank) # 必须将int转为str,rank标签的text的文本重新赋值 rank.set("updated", "no") # 添加属性,rank标签添加属性或者更改属性值 ET.dump(root) # 在终端显示整个xml tree.write("output.xml") # 修改的内容保存到文件output.xm,写入到了一个新文件 # 删除对应的属性 del import xml.etree.ElementTree as ET tree = ET.parse("output.xml") root = tree.getroot() for rank in root.iter("rank"): # attrib为属性字典 # 删除对应的属性updated del rank.attrib['updated'] ET.dump(root) 10:关于class xml.etree.ElementTree.Element 属性相关 attrib 为包含元素属性的字典 keys() 返回元素属性名称列表 items() 返回(name,value)列表 get(key, default=None) 获取属性 set(key, value) # 跟新/添加 属性 del xxx.attrib[key] # 删除对应的属性
11:节点/元素 相关 # 删除子元素remove() import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() # 删除rank大于50的国家 for country in root.iter("country"): rank = int(country.find("rank").text) if rank > 50: # remove()方法 删除子元素 root.remove(country) ET.dump(root) # 添加子元素 import xml.etree.ElementTree as ET tree = ET.parse("country.xml") root = tree.getroot() country = root[0] last_ele = country[len(list(country)) - 1] # 找到contry标签下层标签的最后一个 last_ele.tail = '\n\t\t' # tail 属性会存放元素的结束标记及下一个标记之间的文本 # 创建一个新的元素tag为test_append,元素的文本为elem 1,元素的结束标记和下一个标记的内容为'\n\t\t',然后append把创建的元素添加进去 elem1 = ET.Element("test_append") elem1.text = "elem 1" elem1.tail = '\n\t\t' country.append(elem1) # SubElement() 其实内部调用的时append(),创建元素elem2后自动调用append elem2 = ET.SubElement(country, "test_subelement") elem2.text = "elem 2" elem2.tail = '\n\t\t' # extend():创建元素elem3,elem4,写好中间的text内容然后extend添加到country下级 elem3 = ET.Element("test_extend") elem3.text = "elem 3" elem4 = ET.Element("test_extend") elem4.text = "elem 4" elem4.tail = '\n\t\t' country.extend([elem3, elem4]) # insert():创建一个元素后插入到counry的第5个位置 elem5 = ET.Element("test_insert") elem5.text = "elem 5" country.insert(5, elem5) ET.dump(country) # append(subelement) # extend(subelements) # insert(index, element)
# 12:创建xml文档 # 创建root Element,然后创建SubElement,最后将root element传入ElementTree(element),创建tree,调用tree.write()方法写入文件 # 创建元素的3个方法: 使用ET.Element、Element对象的makeelement()方法以及ET.SubElement import xml.etree.ElementTree as ET def subElement(root, tag, text): """ xml元素创建函数 """ ele = ET.SubElement(root, tag) ele.text = text ele.tail = '\n' root = ET.Element("note") # 创建一个root标签 to = root.makeelement("to", {}) # root标签创建一个子标签to to.text = "peter" to.tail = '\n' root.append(to) subElement(root, "from", "marry") subElement(root, "heading", "Reminder") subElement(root, "body", "Don't forget the meeting!") tree = ET.ElementTree(root) tree.write("note.xml", encoding="utf-8", xml_declaration=True) # ET.dump(tree)
# 原生保存的XML时默认无缩进,如果想要设置缩进的话, 需要修改保存方式 import xml.etree.ElementTree as ET from xml.dom import minidom def subElement(root, tag, text): ele = ET.SubElement(root, tag) ele.text = text def saveXML(root, filename, indent="\t", newl="\n", encoding="utf-8"): rawText = ET.tostring(root) dom = minidom.parseString(rawText) with open(filename, 'w') as f: dom.writexml(f, "", indent, newl, encoding) root = ET.Element("note") to = root.makeelement("to", {}) to.text = "peter" root.append(to) subElement(root, "from", "marry") subElement(root, "heading", "Reminder") subElement(root, "body", "Don't forget the meeting!") # 保存xml文件 saveXML(root, "note.xml")
26:将字典转换为XML 使用一个Python字典存储数据,并将它转换成XML格式
# xml.etree.ElementTree也可以创建XML文档 from xml.etree.ElementTree import Element from xml.etree.ElementTree import tostring #tostring()
函数将Element对象转换成一个字节字符串 def dict_to_xml(tag, d): """ tag:传入外面的大标签,d传入字典元素 """ elem = Element(tag) # 创建一个大标签元素。指定标签名称tag for key, val in d.items(): child = Element(key) # 创建大标签元素的子元素,指定标签名称key child.text = str(val) # 子元素写入文本 elem.append(child) # 大标签里面加入子标签 return elem # 返回大标签 s = {'name': 'GOOG', 'shares': 100, 'price': 490.1} e = dict_to_xml('stock', s) # stock表示外面的大标签 # print(e) # <Element 'stock' at 0x0000011BFBF184A0> print(tostring(e)) # b'<stock><name>GOOG</name><shares>100</shares><price>490.1</price></stock>'
# 给某个元素添加属性值,可以使用set()
方法:
e.set('_id', '1234')
print(tostring(e)) # b'<stock _id="1234"><name>GOOG</name><shares>100</shares><price>490.1</price></stock>'
保持元素的顺序,可以考虑构造一个OrderedDict
来代替一个普通的字典
# 当创建XML的时候,被限制只能构造字符串类型的值 from xml.etree.ElementTree import Element from xml.etree.ElementTree import tostring def dict_to_xml_str(tag, d): """ 将简单的键/值对转换为XML,通过字符串的拼接转化成str格式返回,而不是返回的element元素对象 """ parts = ['<{}>'.format(tag)] # ['<item>'] for key, val in d.items(): parts.append('<{0}>{1}</{0}>'.format(key, val)) # ['<item>', '<name><spam></name>'] parts.append('</{}>'.format(tag)) # ['<item>', '<name><spam></name>', '</item>'] return ''.join(parts) # <item><name><spam></name></item> def dict_to_xml(tag, d): elem = Element(tag) for key, val in d.items(): child = Element(key) child.text = str(val) elem.append(child) return elem # dict_to_xml_str手动通过字符串拼接的去构造的时候可能会碰到一些麻烦, 当字典的值中包含一些特殊字符的的情况 d = {'name': '<spam>'} dict_to_xml_str('item', d) print(dict_to_xml_str('item', d), type(dict_to_xml_str('item', d))) # <item><name><spam></name></item>, <class 'str'> e = dict_to_xml('item', d) print(tostring(e), type(e)) # b'<item><name><spam></name></item>' <class 'xml.etree.ElementTree.Element'> # 字符 ‘<’ 和 ‘>’ 被替换成了 < 和 >
# 手动去转换在xml里面传输的某些特殊字符:xml.sax.saxutils 中的 escape() 和 unescape() 函数 from xml.sax.saxutils import escape, unescape print(escape('<spam>')) # <spam> escape:编码 print(unescape('<spam>')) # <spam> 解码 # 除了能创建正确的输出外, 使用字符串组合构造一个更大的文档并不是那么容易。
而 Element 实例可以不用考虑解析XML文本的情况下通过多种方式被处理。
可以在一个高级数据结构上完成你所有的操作,并在最后以字符串的形式将其输出
27:解析和修改XML 读取一个XML文档,做一些修改,然后将结果写回XML文档
pred.xml 文档(根据这个xml改写并写入新的xml文档) <?xml version="1.0"?> <stop> <id>14791</id> <nm>Clark & Balmoral</nm> <sri> <rt>22</rt> <d>North Bound</d> <dd>North Bound</dd> </sri> <cr>22</cr> <pre> <pt>5 MIN</pt> <fd>Howard</fd> <v>1378</v> <rn>22</rn> </pre> <pre> <pt>15 MIN</pt> <fd>Howard</fd> <v>1867</v> <rn>22</rn> </pre> </stop> from xml.etree.ElementTree import parse, Element, dump doc = parse('pred.xml') # 生成为xml文本树对象 root = doc.getroot() # <Element 'stop' at 0x0000020DD193BA40> 获取root根节点 root.remove(root.find('sri')) # 移除找到的sri标签 root.remove(root.find('cr')) # 移除找到的cr标签 print(list(root).index(root.find('nm'))) # 获取nm这个标签在root标签里面的位置,位置1就是2号位置 e = Element('span') e.text = '李二狗' root.insert(2, e) # print(dump(root)) # dump(root)输出root的文本内容 doc.write('newpred.xml', xml_declaration=True)
# 修改一个XML文档结构是很容易的,但是你必须牢记的是所有的修改都是针对父节点元素,
将它作为一个列表来处理。例如,如果你删除某个元素,通过调用父节点的 remove() 方法从它的直接父节点中删除。
如果你插入或增加新的元素,你同样使用父节点元素的 insert() 和 append() 方法。
还能对元素使用索引和切片操作,比如 element[i] 或 element[i:j]
28:利用命名空间解析XML文档 解析某个XML文档,文档中使用了XML命名空间
# 命名空间文档 <?xml version="1.0" encoding="utf-8"?> <top> <author>David Beazley</author> <content> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Hello World</title> </head> <body> <h1>Hello World!</h1> </body> </html> </content> </top> # 解析代码 from xml.etree.ElementTree import parse, Element, dump doc = parse('test.xml') # root = doc.getroot() print(doc.findtext('author')) # David Beazley print(doc.find('content')) # <Element 'content' at 0x000002A03677BA90> print(doc.find('content/html')) # None print(doc.find('content/{http://www.w3.org/1999/xhtml}html')) # <Element '{http://www.w3.org/1999/xhtml}html' at 0x0000024D2BD47B30> print(doc.findtext('content/{http://www.w3.org/1999/xhtml}html/head/title')) # None print(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 # xmlns="http://www.w3.org/1999/xhtml"命名空间
# 通过将命名空间处理逻辑包装为一个工具类来简化xml数据处理过程 from xml.etree.ElementTree import parse, Element, dump 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 + '}' # {'html': '{http://www.w3.org/1999/xhtml}'} def __call__(self, path): print(self.namespaces) return path.format_map(self.namespaces) doc = parse('test.xml') ns = XMLNamespaces(html='http://www.w3.org/1999/xhtml') ns('content/{html}html') # 把{html}使用format_map转化成{http://www.w3.org/1999/xhtml}
# ns()相当于调用__call__方法 # print(doc.find(ns('content/{html}html'))) # <Element '{http://www.w3.org/1999/xhtml}html' at 0x000002BC8964DE00> # print(doc.findtext(ns('content/{html}html/{html}head/{html}title'))) # Hello World
# format_map # format_map仅使用于字符串格式中可变数据参数来源于字典等映射关系数据时才可以使用 student = {'name': '小明', 'class': '20190301', 'score': 597.5} s1 = '{st[class]}班{st[name]}总分:{st[score]}'.format(st=student) print(s1) # 20190301班小明总分:597.5 student = {'name': '小明', 'class': '20190301', 'score': 597.5} s2 = '{class}班{name}总分:{score}'.format_map(student) print(s2) # 20190301班小明总分:597.5
format_map 直接根据字符串里面的{}的键找字典里面的值来封装成一个字符串
# 解析含有命名空间的XML文档会比较繁琐。 XMLNamespaces 仅仅是允许你使用缩略名代替完整的URI将其变得稍微简洁一点 # ElementTree 解析中没有任何途径获取命名空间的信息 # 使用 iterparse() 函数的话就可以获取更多关于命名空间处理范围的信息 from xml.etree.ElementTree import iterparse for evt, elem in iterparse('test.xml', ('end', 'start-ns', 'end-ns')): print(evt, "-", elem) #输出: end - <Element 'author' at 0x000001DF2801BB30> start-ns - ('', 'http://www.w3.org/1999/xhtml') end - <Element '{http://www.w3.org/1999/xhtml}title' at 0x000001DF2801BE00> end - <Element '{http://www.w3.org/1999/xhtml}head' at 0x000001DF2801BD10> end - <Element '{http://www.w3.org/1999/xhtml}h1' at 0x000001DF2801BF40> end - <Element '{http://www.w3.org/1999/xhtml}body' at 0x000001DF2801BEA0> end - <Element '{http://www.w3.org/1999/xhtml}html' at 0x000001DF2801BC70> end-ns - None end - <Element 'content' at 0x000001DF2801BB80> end - <Element 'top' at 0x000001DF2801BAE0>
29:与关系型数据库的交互 在关系型数据库中查询、增加或删除记录
# Python中表示多行数据的标准方式是一个由元组构成的序列 stocks = [ ('GOOG', 100, 490.1), ('AAPL', 50, 545.75), ('FB', 150, 7.45), ('HPQ', 75, 33.2), ] # 所有数据库上的操作都通过SQL查询语句来完成。每一行输入输出数据用一个元组来表示。 # 1:连接数据库connect() 函数 import sqlite3 db = sqlite3.connect('database.db') # 2:创建一个游标。 一旦你有了游标,就可以执行SQL查询语句 c = db.cursor() c.execute('create table portfolio (symbol text, shares integer, price real)') db.commit() # 3:向数据库表中插入多条记录 c.executemany('insert into portfolio values (?,?,?)', stocks) db.commit() # 4:某个查询 for row in db.execute('select * from portfolio'): print(row) # 5:接受用户输入作为参数来执行查询操作 min_price = 100 for row in db.execute('select * from portfolio where price >= ?',(min_price,)): print(row)
# 数据库中的数据和Python类型直接的映射。 对于日期类型,通常可以使用 datetime 模块中的 datetime 实例,
或者可能是 time 模块中的系统时间戳。 对于数字类型,特别是使用到小数的金融数据,
可以用 decimal 模块中的 Decimal 实例来表示。
不幸的是,对于不同的数据库而言具体映射规则是不一样的,你必须参考相应的文档
# 另外一个更加复杂的问题就是SQL语句字符串的构造。
你千万不要使用Python字符串格式化操作符(如%)或者 .format() 方法来创建这样的字符串。
如果传递给这些格式化操作符的值来自于用户的输入,那么你的程序就很有可能遭受SQL注入攻击(参考 http://xkcd.com/327 )。
查询语句中的通配符 ? 指示后台数据库使用它自己的字符串替换机制,这样更加的安全
# 不幸的是,不同的数据库后台对于通配符的使用是不一样的。
大部分模块使用 ? 或 %s , 还有其他一些使用了不同的符号,
比如:0或:1来指示参数。 同样的,你还是得去参考你使用的数据库模块相应的文档。
一个数据库模块的 paramstyle 属性包含了参数引用风格的信息
# 对于简单的数据库数据的读写问题,使用数据库API通常非常简单。
如果你要处理更加复杂的问题,建议你使用更加高级的接口,
比如一个对象关系映射ORM所提供的接口。 类似 SQLAlchemy 这样的库允许你使用Python类来表示一个数据库表,
并且能在隐藏底层SQL的情况下实现各种数据库的操作。
30:编码和解码十六进制数 将一个十六进制字符串解码成一个字节字符串或者将一个字节字符串编码成一个十六进制字符串 binascii
模块
import binascii s = b'hello' h = binascii.b2a_hex(s) # b'68656c6c6f' # ord('h') = 104 ord('e') = 101 ord('l') = 108 ord('l') = 108 ord('o') = 111 # 把上面ord转化整数十进制数使用hex转化成16进制 # hex(104)='0x68' hex(101)='0x65' hex(108)='0x6c' hex(108)='0x6c' hex(111)='0x6f' # 所以hello转化成十六进制就是:0x68656c6c6f # 16进制一个ff表示1位8字节,1ff表示2位16字节, # b'68656c6c6f'十六进制的字节转化成字符串字节 print(binascii.a2b_hex(h)) # b'hello' # 拿到一个16进制的68表示一位,先转化成十进制,然后使用chr把十进制转化成字符串 # chr(int('68', 16)) = 'h'
# base64 模块中的16进制编码和解码 import base64 s = b'hello' h = base64.b16encode(s) # b'68656C6C6F' print(base64.b16decode(h)) # b'hello' # 函数 base64.b16decode() 和 base64.b16encode() 只能操作大写形式的十六进制字母, 而 binascii 模块中的函数大小写都能处理。 # 编码函数所产生的输出总是一个字节字符串。 如果想强制以Unicode形式输出,你需要增加一个额外的界面步骤decode把字节字符串解码成字符 import base64 s = b'hello' h = base64.b16encode(s) # b'68656C6C6F' print(h) # b'68656C6C6F' print(h.decode('utf8')) # 68656C6C6F # 在解码十六进制数时,函数 b16decode() 和 a2b_hex() 可以接受字节或unicode字符串。
但是,unicode字符串必须仅仅只包含ASCII编码的十六进制数
31:编码解码Base64数据 Base64是压缩格式,Base64格式解码或编码二进制数据。
# base64 模块中有两个函数 b64encode() b64decode() import base64 s = b'hello' a = base64.b64encode(s) # b'aGVsbG8=' print(base64.b64decode(a)) # b'hello' # Base64编码仅仅用于面向字节的数据比如字节字符串和字节数组。 # 编码处理的输出结果总是一个字节字符串。 如果你想混合使用Base64编码的数据和Unicode文本,你必须添加一个额外的解码步骤decode a = base64.b64encode(s).decode('ascii') # 'aGVsbG8=' # 当解码Base64的时候,字节字符串和Unicode文本都可以作为参数。 但是,Unicode字符串只能包含ASCII字符
32:读写二进制数组数据 读写一个二进制数组的结构化数据到Python元组中 struct模块
# struct 模块处理二进制数据 一个Python元组列表写入一个二进制文件,并使用 struct 将每个元组编码为一个结构体 from struct import Struct def write_records(records, format): """ 将元组序列写入结构的二进制文件。 """ data = b"" record_struct = Struct(format) # 先把转码规则写进类里然后使用pack和unpack来操作 for r in records: # f.write(record_struct.pack(*r)) data += record_struct.pack(*r) return data if __name__ == '__main__': records = [(1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7)] # '<idd':转码成bytes类型的规则,<小于号小段优先,也就是小端在前面, # i:有符号4字节整数 d:有符号8字节浮点数 # pack按照规则把1,2.3,4.5等按照<idd转成bytes类型 # 转数字1的时候对应<i,也就是转成小端优先的4个有符号的字节,正常大端优先转码4个字节应该是\0x00\0x00\0x00\0x01 # 现在如果是<小端优先的化应该是\0x01\0x00\0x00\0x00 后面的浮点数和str字符串转成bytes都类似这逻辑 print(Struct('<idd').pack(*(1, 2.3, 4.5)), len(Struct('<idd').pack(*(1, 2.3, 4.5)))) # i:4字节 d:8字节 # 输出:b'\x01\x00\x00\x00ffffff\x02@\x00\x00\x00\x00\x00\x00\x12@' 20 # x01表示1字节,前面带x的2个表示1字节,前面不带x的1位表示1字节,所以ffffff是6字节,@是一个字节 print(Struct('<idd').pack(*(6, 7.8, 9.0))) # b'\x06\x00\x00\x00333333\x1f@\x00\x00\x00\x00\x00\x00"@' print(Struct('<idd').pack(*(12, 13.4, 56.7)), len(Struct('<idd').pack(*(12, 13.4, 56.7)))) # b'\x0c\x00\x00\x00\xcd\xcc\xcc\xcc\xcc\xcc*@\x9a\x99\x99\x99\x99YL@' data = write_records(records, '<idd') with open('data.txt', 'wb') as f: f.write(data)
# 读取上个例子写data.txt这个文件并返回一个元组列表 # 以块的形式增量读取文件 from struct import Struct def read_records(format, f): record_struct = Struct(format) # record_struct.size 根据'<idd'这个匹配规则就直到size等于4+8+8=20 chunks = iter(lambda: f.read(record_struct.size), b'') # 循环read每次读取20字节,分三次读出60个字节的全部数据,直到返回为b""停止,chunks是一个包含三个数据的迭代器 # iter循环执行lambda函数,直到返回值是b""就停止,返回一个迭代器,lambda函数只有一个参数那么这个就是返回值 return (record_struct.unpack(chunk) for chunk in chunks) # 继续返回一个解码后包含三个数据原始值的迭代器 if __name__ == '__main__': with open('data.b', 'rb') as f: for rec in read_records('<idd', f): print(rec)
# 整个文件一次性读取到一个字节字符串中,然后在分片解析 from struct import Struct def unpack_records(format, data): record_struct = Struct(format) return (record_struct.unpack_from(data, offset) for offset in range(0, len(data), record_struct.size)) # offset分别取值:0 20 40表示起始位置,起始位置从0开始读读到20,20开始读到40 if __name__ == '__main__': with open('data.b', 'rb') as f: data = f.read() # data一次性读取文件里全部的内容 # print(len(data)) # 60 for rec in unpack_records('<idd', data): print(rec) 输出: (1, 2.3, 4.5) (6, 7.8, 9.0) (12, 13.4, 56.7)
# 编码和解码二进制数据使用 struct 模块 # 1:为了声明一个新的结构体,只需要像这样创建一个 Struct 实例 # 声明的规则:小尾数32位4字节整数,两个8字节双精度浮点,<小段优先 record_struct = Struct('<idd') # 结构体通常会使用一些结构码值i, d, f等 , 这些代码分别代表某个特定的二进制数据类型如32位整数,
64位浮点数,32位浮点数等。 第一个字符 < 指定了字节顺序,它表示”低位在前”。
更改这个字符为 > 表示高位在前,或者是 ! 表示网络字节顺序 # 2:Struct 实例有很多属性和方法用来操作相应类型的结构 size 属性包含了结构的字节数 pack() 和 unpack() 方法被用来打包和解包数据 from struct import Struct record_struct = Struct('<idd') print(record_struct.size) # 20 data = record_struct.pack(1, 2.0, 3.0) # b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@' print(record_struct.unpack(data)) # (1, 2.0, 3.0) # 3:pack() 和 unpack() 含可以以模块级别函数被调用 import struct data = struct.pack('<idd', 1, 2.0, 3.0) print(struct.unpack('<idd', data)) # (1, 2.0, 3.0) # 同样的结构出现在多个地方的时候。 通过创建一个 Struct 实例,格式代码只会指定一次并且所有的操作被集中处理。
这样一来代码维护就变得更加简单了(因为你只需要改变一处代码即可)
# iter() 被用来创建一个返回固定大小数据块的迭代器,这个迭代器会不断的调用一个用户提供的可调用对象
(比如 lambda: f.read(record_struct.size) ), 直到它返回一个特殊的值(如b’‘),这时候迭代停止 import struct f = open('data.b', 'rb') chunks = iter(lambda: f.read(20), b'') # <callable_iterator object at 0x000001752D14B9A0> for chunk in chunks: print(chunk) print(struct.unpack('<idd', chunk)) 输出: b'\x01\x00\x00\x00ffffff\x02@\x00\x00\x00\x00\x00\x00\x12@' (1, 2.3, 4.5) b'\x06\x00\x00\x00333333\x1f@\x00\x00\x00\x00\x00\x00"@' (6, 7.8, 9.0) b'\x0c\x00\x00\x00\xcd\xcc\xcc\xcc\xcc\xcc*@\x9a\x99\x99\x99\x99YL@' (12, 13.4, 56.7)
# 创建一个可迭代对象的一个原因是它能允许使用一个生成器推导来创建记录 # 如下也可以 def read_records(format, f): record_struct = Struct(format) while True: chk = f.read(record_struct.size) if chk == b'': break yield record_struct.unpack(chk) # while 1循环读取数据,直到读取到的数据是b''结束,循环读取,读取到就解码返回
# unpack_from()和unpack() # unpack_from() 对于从一个大型二进制数组中提取二进制数据非常有用,
它不会产生任何的临时对象或者进行内存复制操作。 你只需要给它一个字节字符串(或数组)和一个字节偏移量,它会从那个位置开始直接解包数据 # 使用 unpack() 来代替 unpack_from() ,要构造大量的小的切片以及进行偏移量的计算 from struct import Struct def unpack_records(format, data): record_struct = Struct(format) # 格式 return (record_struct.unpack(data[offset:offset + record_struct.size]) for offset in range(0, len(data), record_struct.size)) # unpack执行了大量的偏移量计算, 复制数据以及构造小的切片对象。 取到的一个大型字节字符串中解包大量的结构体的话,unpack_from() 会表现的更出色
# 解包的时候,collections 模块中的命名元组对象。 返回元组设置属性名称 from struct import Struct from collections import namedtuple Record = namedtuple('Record', ['kind', 'x', 'y']) # 命名元组 def read_records(format, f): record_struct = Struct(format) while True: chk = f.read(record_struct.size) if chk == b'': break yield record_struct.unpack(chk) with open('data.txt', 'rb') as f: records = (Record(*r) for r in read_records('<idd', f))
# 返回一个生成器,生成器里面三个元素,都是存放的三个命名元组的实例, r返回的是每次yield返回回来的元组数据使用*解包成三个元素 for r in records: print(r.kind, r.x, r.y)
# 处理大量的二进制数据,使用 numpy 模块。 # 将一个二进制数据读取到一个结构化数组中(数组非元组和列表) import numpy as np f = open('data.txt', 'rb') records = np.fromfile(f, dtype='<i,<d,<d') print(records) # 输出:[( 1, 2.3, 4.5) ( 6, 7.8, 9. ) (12, 13.4, 56.7)] print(records[0]) # (1, 2.3, 4.5) # 从已知的文件格式(如图片格式,图形文件,HDF5等)中读取二进制数据时, 先检查看看Python是不是已经提供了现存的模块
33:读取嵌套和可变长二进制数据 读取包含嵌套或者可变长记录集合的复杂二进制格式的数据图片、视频、电子地图文件
# struct 模块可被用来编码/解码几乎所有类型的二进制的数据结构 # Python数据结构 :表示一个组成一系列多边形的点的集合 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-N | Points | (X,Y) 坐标,以浮点数表示 | +------+--------+-------------------------------------------+ # Python代码import struct import itertools def write_polys(filename, polys): # 确定边界框 flattened = list(itertools.chain(*polys)) #chain()
可以把一组迭代对象串联起来,形成一个更大的迭代器 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')
# [48, 64, 48] d代表8字节两个d代表16 # 一个点一组浮点数据,需要2个d,也就是一组数据需要16位,len(poly)表示几个点, # 也就是写入的总长度是 几个点*一个点的长度+头四字节记录的长度 f.write(struct.pack('<i', size + 4)) for pt in poly: f.write(struct.pack('<dd', *pt)) 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)], ] write_polys('polys.txt', polys) # 将数据读取回来的时候,可以利用函数 struct.unpack()def read_polys(filename): with open(filename, 'rb') as f: # 阅读标题 header = f.read(40) # 读取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)) # pt是解码出来的两个数据代表一个点的x,y,一个元组 poly.append(pt) polys.append(poly) return polys print(read_polys('polys.txt')) # 很多读取、解包数据结构和其他细节的代码,处理真实的数据文件,太繁杂了点
itertools迭代器操作对象函数详解:https://www.liaoxuefeng.com/wiki/897692888725344/983420006222912
# 读取字节数据的时候,通常在文件开始部分会包含文件头和其他的数据结构 # struct模块可以解包这些数据到一个元组中去,另外一种表示这种信息的方式就是使用一个类 import struct class StructField: """ 表示简单结构字段的描述符 """ def __init__(self, format, offset): self.format = format self.offset = offset def __get__(self, instance, cls): if instance is None: # 如果是类访问那么instance就是None return self else: r = struct.unpack_from(self.format, instance._buffer, self.offset) return r[0] if len(r) == 1 else r class Structure: def __init__(self, bytedata): self._buffer = memoryview(bytedata) # memoryview() 函数从指定对象返回内存视图对象
# 使用了一个描述器来表示每个结构字段,每个描述器包含一个结构兼容格式的代码以及一个字节偏移量,
存储在内部的内存缓冲中。在 __get__() 方法中,struct.unpack_from() 函数被用来从缓冲中解包一个值,省去了额外的分片或复制操作步骤 # Structure 类就是一个基础类,接受字节数据并存储在内部的内存缓冲中,并被 StructField 描述器使用。 这里使用了 memoryview() # 使用这个代码,你现在就能定义一个高层次的结构对象来表示上面表格信息所期望的文件格式 class PolyHeader(Structure): # 继承Stucture类 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) # 利用这个类来读取之前我们写入的多边形数据的头部数据 f = open('polys.bin', 'rb') phead = PolyHeader(f.read(40)) phead.file_code == 0x1234 phead.min_x # 0.5 phead.min_y # 0.5 phead.max_x # 7.0 phead.max_y # 9.2 phead.num_polys # 3
# python描述器 class Stduent1: def __init__(self): self.course = 'Python' print('Stduent1.__init__') def __get__(self, instance, owner): # 这里的self为Stduent1的实例. instance为实例, 如果是类访问,那么instance为None. owner是调用者的类 print('self={} instance={} owner={}'.format(self, instance, owner)) return self # 返回Student1的实例self class Stduent2: stu1 = Stduent1() def __init__(self): print('Stduent2.__init__') print(Stduent2.stu1.course) # 类 stu2 = Stduent2() print(stu2.stu1.course)
输出如下:
Stduent1.__init__
self=<__main__.Stduent1 object at 0x0000021CAC815D00> instance=None owner=<class '__main__.Stduent2'>
Python
Stduent2.__init__
self=<__main__.Stduent1 object at 0x0000021CAC815D00> instance=<__main__.Stduent2 object at 0x0000021CAC815AF0> owner=<class '__main__.Stduent2'>
Python
描述器:https://zhuanlan.zhihu.com/p/121459013
# 使用类装饰器或元类来修改上面代码 # 元类有一个特性就是它能够被用来填充许多低层的实现细节,从而释放使用者的负担 # 使用元类改造Structure 类 # 元类:https://www.jianshu.com/p/c1ca0b9c777d import struct class StructField: """ 表示简单结构字段的描述符 """ 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 class StructureMeta(type): """ 自动创建StructField描述符的元类 """ # 这个类继承了type元类,那么他就是一个元类,并且在__new__方法里自动调用type方法创建一个类实例 # 并且返回了三个参数clsname, bases, clsdict,init方法里面必填这三个参数,不然报错 # metaclass=StructureMeta,有类指定了这个类是元类的话一定会传递四个参数给元类,不然报错
# init里面的self指代下面调用元类的类
def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) # 得到[('<i', 'file_code'), ('d', 'min_x'), ('d', 'min_y'), # ('d', 'max_x'), ('d', 'max_y'), ('i', 'num_polys')] 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)) # 设置self类属性 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)) 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('polys.txt', 'rb') phead = PolyHeader.from_file(f) print(phead.file_code == 0x1234) print(phead.min_x)
# PolyHeade继承Structure,Structure里面有个自定义元类StructureMeta
# Python先会在类的定义中寻找metaclass
属性,如果找到了,Python就会用它来创建类Structure,如果没有找到,就会用内建的type来创建这个类。
# 现在解释器一运行就会调用元类创建一个类PolyHeade,往PolyHeade里面加内容和属性
# 支持嵌套的字节结构 # 对元类改进,提供了一个新的辅助描述器来达到想要的效果 import struct class StructField: """ 表示简单结构字段的描述符 """ 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 class NestedStruct: """ 表示嵌套结构的描述符 """ def __init__(self, name, struct_type, offset): # struct_type是类point, name是min或者max self.name = name self.struct_type = struct_type self.offset = offset def __get__(self, instance, cls): if instance is None: return self else: # struct_type.struct_size为16 读取16个 data = instance._buffer[self.offset: self.offset + self.struct_type.struct_size] result = self.struct_type(data) # 传入读取的字节实例化Point(data) # 将结果结构保存回实例以避免 # 进一步重新计算此步骤 setattr(instance, self.name, result) # 往instance下层point类里面加属性 return result class StructureMeta(type): """ 自动创建StructField描述符的元类 """ def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) byte_order = '' offset = 0 for format, fieldname in fields: if isinstance(format, StructureMeta): # 在 self:PolyHeader类里设置一个类属性,键min或者max:值:NestedStruc类 setattr(self, fieldname, NestedStruct(fieldname, format, offset)) # format是Point类 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)) class Point(Structure): _fields_ = [ ('<d', 'x'), ('d', 'y') ] class PolyHeader(Structure): _fields_ = [ ('<i', 'file_code'), (Point, 'min'), # 嵌套结构 (Point, 'max'), # 嵌套结构 ('i', 'num_polys') ] f = open('polys.txt', 'rb') phead = PolyHeader.from_file(f) print(phead.file_code == 0x1234) print(phead.min.x) # 在这段代码中,NestedStruct 描述器被用来叠加另外一个定义在某个内存区域上的结构。
它通过将原始内存缓冲进行切片操作后实例化给定的结构类型。由于底层的内存缓冲区是通过一个内存视图初始化的,
所以这种切片操作不会引发任何的额外的内存复制。相反,它仅仅就是之前的内存的一个叠加而已。
另外,为了防止重复实例化,描述器保存了该实例中的内部结构对象
# 组件记录是变长,多边形文件包含变长的部分(未理解) 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) def iter_as(self, code): 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 参数指定了字节数是否包含头部大小 f = open('polys.bin', 'rb') phead = PolyHeader.from_file(f) phead.num_polys # 3 polydata = [ SizedRecord.from_file(f, '<i') for n in range(phead.num_polys) ] polydata [<__main__.SizedRecord object at 0x1006a4d50>, <__main__.SizedRecord object at 0x1006a4f50>, <__main__.SizedRecord object at 0x10070da90>] # 可以看出,SizedRecord 实例的内容还没有被解析出来。 可以使用 iter_as() 方法来达到目的,
这个方法接受一个结构格式化编码或者是 Structure 类作为输入。 这样子可以很灵活的去解析数据 for n, poly in enumerate(polydata): print('Polygon', n) for p in poly.iter_as('<dd'): print(p) 输出: Polygon 0 (1.0, 2.5) (3.5, 4.0) (2.5, 1.5) Polygon 1 (7.0, 1.2) (5.1, 3.0) (0.5, 7.5) (0.8, 9.0) Polygon 2 (3.4, 6.3) (1.2, 0.5) (4.6, 9.2) for n, poly in enumerate(polydata): print('Polygon', n) for p in poly.iter_as(Point): print(p.x, p.y) 输出 Polygon 0 1.0 2.5 3.5 4.0 2.5 1.5 Polygon 1 7.0 1.2 5.1 3.0 0.5 7.5 0.8 9.0 Polygon 2 3.4 6.3 1.2 0.5 4.6 9.2 # 结合起来,下面是一个 read_polys() 函数的另外一个修正版 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
# 当一个 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 可以叠加同一内存区域上定义的机构的不同部分。 这个特性比较微妙,
但是它关注的是内存视图与普通字节数组的切片操作行为。 如果你在一个字节字符串或字节数组上执行切片操作,
你通常会得到一个数据的拷贝。 而内存视图切片不是这样的,它仅仅是在已存在的内存上面叠加而已。因此,这种方式更加高效
34:数据的累加与统计操作 很大的数据集并需要计算数据总和或其他统计量 统计、时间序列以及其他相关技术的数据分析问题,可以使用 Pandas库
# pandas库需要pip安装 import pandas # 读取CSV文件,跳过最后一行 skipfooter=1:意思是跳过最后一行 rats = pandas.read_csv('rats.csv', skipfooter=1, engine='python') # print(rats, type(rats)) # 返回的是一个对象 # 调查某个字段的值范围 # print(rats['number'], type(rats['number'])) # 返回的是一个对象 # print(rats['number'].unique(), type(rats['number'].unique())) # 跳过最后一行,输出number列的数据,返回一个类列表 # 过滤数据 # crew_dispatched = rats[rats['number'] == 1111] # print(crew_dispatched) # print(len(crew_dispatched)) # 查找芝加哥10个鼠患最多的邮政编码 # crew_dispatched = rats[rats['number'] == 1111] # print(crew_dispatched['name'].value_counts()[:10]) # 按age分组 # crew_dispatched = rats[rats['number'] == 1111] # dates = crew_dispatched.groupby('age') # print(len(dates)) # 确定每天的计数 # crew_dispatched = rats[rats['number'] == 1111] # dates = crew_dispatched.groupby('age') # 按照age进行分组 # date_counts = dates.size() # print(date_counts[0:10]) # 对计数进行排序 crew_dispatched = rats[rats['number'] == 1111] dates = crew_dispatched.groupby('age') # 按照age进行分组 date_counts = dates.size() print(date_counts) date_counts.sort() print(date_counts[-10:])