Python Cookbook第三版学习笔记【第五章】

第五章 文件和I/O

5.1 读写文本数据

  • 一般以with…as…关键字及open函数进行文件读写,with语句结束后,文件自动关闭,不必调用f.close():
with open('abc.txt', mode, encoding=encoding) as f:
    pass
  • mode就是读写模式,对于文本文件可以为’rt’(读), ‘wt’(写), ‘at’(追加),encoding为编码方式。
  • 在文件读取时,可能会发生编码错误:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 21: ordinal not in range(128)
  • 我们可以把编码方式换成合适的,也可以提供errors参数进行错误处理,
    • 参数值为"replace", 则把无法识别的字符转换为U+fffd;
    • 参数值为"replace", 则直接跳过该字符。
  • 当然,使用正确的编码形式才是首选方法。

5.2 将输出重定向到文件中

  • print()函数提供了一个参数file, 传入一个文件对象即可:
with open('abc.txt', 'at') as f:
    print("HELLO WORLD", file=f)
  • 必须以文本模式打开,以二进制模式打开就会失败。

5.3 以不同的分隔符或行结尾符完成打印

  • 使用print()函数的sep参数和end参数
print(*["I", "love", "learning", "python"], sep="-", end="!!!")
  • 结果:
I-love-learning-python!!!

5.4 读写二进制数据

  • 以"wb",“rb” 为模式打开文件
  • 写入文件,要使用字节串;从文件中读出,结果也是字节串。
  • 字节串和字符串的语义差异:
  • 在做索引和迭代时,字符串返回的是当前位置的字符;而字符串返回的是该字符的编码值。
>>> a = 'abcd'
>>> a[2]
'c'
>>> b = b'abcd'
>>> b[2]
99

  • 类似数组和C结构体这样的数据结构可以直接以二进制格式写入文件,而无需转换:
import array

a = array.array('i', [1,2,3,4,5])
with open("arr.bin", "wb") as f:
    f.write(a)
  • 使用f.readinto(a) 可以将二进制数据读入到对象a的底层内存中。

5.5 对已不存在的文件执行写入操作

  • 想通过open()函数打开一个文件,并执行写入操作
  • 不明确文件是否存在,希望仅在系统中没有这个文件的时候才执行操作
  • 使用"xt"模式:
with open("abc.txt", "xt") as f:
	f.write(some_string)
  • 对于二进制文件,就是"xb"
  • 也可使用os.path.exists()判断文件是否存在,但不如以上的方法简洁。

5.6 在字符串上执行I/O操作

  • 想将文本(str) 或者 二进制字符串(bytes)写入类似文件的对象上
  • 可以分别使用io.StringIO和io.BytesIO()类
  • 可以像文件对象一样读写它们的实例:
s_io.write('Hello World')
s = s_io.getvalue()
print(s)
  • 使用StringIO和BytesIO可以模拟出一个普通的文件
  • 但它们的实例没有真正的文件描述符来对应

5.7 读写压缩的数据文件

  • 想要读写gzip 或 bz2格式压缩过的文件
  • 使用gzip.open() 和bz2.open()函数,对文本文件的读写,模式选择rt/wt
  • 对于二进制文件的读写,模式选择rb/wb
  • 这里的open()函数和普通的open()函数一样,支持encoding, errors, newline等关键字参数
  • 当写入压缩数据时,使用compresslevel可以指定压缩的级别,默认的级别是9,是最高的级别。可选范围是1-9, 数字越小,则速度越快,压缩比率较小。

5.8 对固定大小的记录进行迭代

  • 打开一个文件对象f, 可以使用f.readline() 对其按行读取,但也可以指定块大小来对其读取,
  • 使用iter() 和functools.partial()来完成此目的:
from functools import partial

CHUNKSIZE = 16

with open("abc.txt", "rt") as f:
    chunk = iter(partial(f.read, CHUNKSIZE), '')
    for i, c in enumerate(chunk):
        print("第{}部分".format(i+1))
        print(c)
  • 结果:
第1部分
#java
#python
ph
第2部分
p
javascript
jul
第3部分
iaHELLOWORLD
  • partial()函数创建了一个可调用对象,每次调用它,都读取指定大小的内容,iter()还接收了一个哨兵值,这里定义为空字符"",当读到它时,则停止迭代。

5.9 将二进制数据读取到可变缓冲区中

  • 打开一个二进制文件,使用readinto()方法就可以将数据读进一个事先分配的缓冲区中:
import os

buf = bytearray(os.path.getsize("abc.bin"))
with open("abc.bin", "rb") as f:
    f.readinto(buf)
  • 和f.read()不同的是,f.readinto()是往预先分配的缓冲区中读入数据,而使用:
buf = f.read()
  • 则是分配新的对象
  • 使用memoryview可以对一个缓冲区作切片操作:
with open("abc.bin", "rb") as f:
    f.readinto(buf)
    mem = memoryview(buf)
    mem_1 = mem[:5]
    mem_1 = b"HELLO"
  • 上面代码中,不仅做了切片,对mem_1的修改也作用到了buf上。
  • f.readinto() 返回值是读入缓冲区的大小,如果小于buf,则说明读取的数据可能被截断或破坏。

5.10 对二进制文件做内存映射

  • 使用mmap可以将文件映射到内存中的一块缓冲区,对缓冲区的内容进行修改会作用于原始文件中:
import os
import mmap

buf = bytearray(os.path.getsize("abc.bin"))
file_desc = os.open("abc.bin", os.O_RDWR)
file_size = os.path.getsize("abc.bin")
with mmap.mmap(file_desc, file_size, access=mmap.ACCESS_WRITE) as file_map:
    print(file_map.read())
    file_map[:5] = b'HELLO'
  • 以上代码中,file_map就是映射到内存中的缓冲区,mmap.mmap()可以当做上下文管理器使用,print(file_map.read())可以打印文件中的内容:
b'hello world'
  • 使用file_map[:5] = b’HELLO’改变文件的内容,然后:
with open("abc.bin", "rb") as f:
    print(f.read())
  • 结果:
b'HELLO world'
  • 我们当然也可以用seek()可以跳过n个字符,再用read()读,write()写,但是都不如把文件映射到内存中然后采用切片跳过字符、直接赋值来修改文件的内容。
  • 对文件映射不会把整个文件读入到内存中,而是在访问不同区域时,才会读取那部分数据。
  • 如果多个Python解释器对同一个文件进行映射,这样就可以在多个进程之间交换数据。(当然需要引入同步策略,比如管道或socket等)

5.11 处理路径名

  • 使用os.path模块中的函数:
os.path.basename(path) # 取得路径中最后一个元素
os.path.dirname(path) # 取得路径的名称
os.path.expanduser(path) # 常用"~"代表当前用户的HOME目录,这个函数可将~展开为完整路径
os.path.join(path) # 合并各个元素为一个路径
os.path.splitext(path) # 返回一个列表,第一个元素为路径及文件名称、第二个元素为文件的后缀
  • UNIX和Windows中的路径存在一些差异,例如UNIX系统中路径用斜杠,而Windows中使用反斜杠,os.path中的函数兼容了这些差异。

5.12 检测文件是否存在

  • os.path中提供了更多函数,可以判断文件是否存在,当前路径是目录、文件、还是软链接,也可获得文件的一些元数据(大小,创建、最后修改时间):
import os

os.path.exists(p) # 是否存在
os.path.isdir(p) # 是否为目录
os.path.isfile(p) # 是否为文件
os.path.islink(p) # 是否为链接
os.path.getctime(p) # 获取创建时间,得到时间戳
os.path.getmtime(p) # 获取修改时间
os.path.getsize(p) # 获取文件大小
  • 使用这些函数时,要注意当前脚本是否有权限读取文件的信息。

5.13 获取目录内容的列表

  • 使用os.listdir() 函数,返回了一个列表
  • 如果想使用通配符去匹配一些文件,可以使用2.3节提到的fnmatch.fnmatch()函数或者glob.glob()函数,例如下面的代码可以筛选出目录下的python文件:
from fnmatch import fnmatch

python_files = [f for f in os.listdir(some_dir) if fnmatch(f, "*.py")]
  • 如果想要获取文件的信息,可以使用os.stat()函数,如果要获取文件的大小,创建时间,修改时间,可以使用os.path中的函数,这些函数在5.12节中都有提及。

5.14 绕过文件名编码

  • 对于正常的文件,文件名经过了编码,可以使用sys.getfilesystemencoding()函数获取文件的编码格式
  • 有时候,用户可能会(恶意地)使用无法解码(不遵守当前编解码规则)的文件名来命名文件,这样在读取这个文件的时候会出现程序崩溃
  • 因此在读取文件和目录的时候,使用原始字节码,就可以规避这个问题:
with open("jalape\xf1o.txt", 'wt') as f:
    f.write("hello world")
  • 以下是创建的文件:
    在这里插入图片描述

5.15 打印无法解码的文件名

5.16 为已经打开的文件添加或修改编码方式

  • 改变一个已经打开的文件的编码方式,使用io.TextIOWrapper()函数:
import io

with open("jalape\xf1o.txt", 'wt', encoding="utf-8") as f:
    f = io.TextIOWrapper(f.buffer, encoding="latin-1")
    print(f.encoding)
  • 结果:
latin-1
  • 要明白上述代码是如何工作的,先看看I/O系统的层次结构:
    • io.FileIO 原始文件,代表操作系统底层的文件描述符
    • io.BufferedWriter / io.BufferedReader / io.BufferedRWPair 缓冲IO层,负责处理二进制数据
    • io.TextIOWrapper 添加或修改文本编码
  • 使用上述代码直接修改文件的编码,会导致无法对文件进行读写,因为这个过程导致了底层文件被关闭。
  • 因此可以使用f.detach()方法将io.TextIOWrapper层分离开来,再调用io.TextIOWrapper添加一个新的io.TextIOWrapper层

5.17 将字节数据写入文本文件

  • I/O系统是以不同层次构建的,文本文件通过在缓冲(buffer)的二进制模式文件上添加一个Unicode编码/解码层构建,buffer属性简单的指向底层文件。因此,我们把二进制数据直接写入这个buffer即可:
with open("abc.txt", "wt") as f:
    # 直接写入字节串会报错
    # f.write(b"hello")

    # 跳过编码/解码层, 将数据直接写入底层buffer
    f.buffer.write(b"hello")

5.18 将已有的文件描述符包装为文件对象

  • 对于一个已经存在文件描述符,它代表了和操作系统中的I/O通道建立起了联系
  • 在Python中,可以通过open()函数用文件对象对其进行包装
  • 以往调用open()函数,第一个参数是文件的路径,只需把它换成这个文件描述符即可:
import os

file_desc = os.open('abc.txt', os.O_RDWR)
f = open(file_desc, 'wt')
f.write("today is a good day")
  • 当调用f.close()关闭文件对象时,这个文件描述符也会被关闭,如果不想关闭它,给open()函数添加closefd=False参数即可,即:
f = open(file_desc, 'wt', closefd=False)
  • 对于管道、标准输出、socket也可以使用类似的方式写入或读出数据

5.19 创建临时文件和目录

  • 使用tempfile模块:
with tempfile.TemporaryFile('w+t') as f:
    f.write('Hello World')
  • 这样就创建了匿名的临时文件,创建命名的临时文件,使用NamedTemporaryFile()函数:
with tempfile.NamedTemporaryFile('w+t') as f:
    f.write('Hello World')
  • 通过f.name可以查看这个文件的名字
  • 使用TemporaryDirectory()函数可以创建临时目录
  • 以上的函数自动创建和清理临时文件和目录,如果想要手动完成这个过程,可以使用mkstemp()创建临时文件, mkdtemp()创建临时目录。
f_info = tempfile.mkstemp()
with open(f_info[0], "wt") as f:
    f.write("hello")
  • mkstemp()返回一个元组,第一个值是文件描述符,第二个值是该文件的路径
  • 对这个文件描述符,可以使用5.18节提到的使用open()函数将其包装为文件对象,并进行读写操作。
  • 使用完后要清理它们,也需要手动完成。

5.20 同串口进行通信

  • 使用pySerial包
  • 使用serial.Serial即可创建一个串口的实例,看下Serial类的__init__()方法:
    def __init__(self,
                 port=None,
                 baudrate=9600,
                 bytesize=EIGHTBITS,
                 parity=PARITY_NONE,
                 stopbits=STOPBITS_ONE,
                 timeout=None,
                 xonxoff=False,
                 rtscts=False,
                 write_timeout=None,
                 dsrdtr=False,
                 inter_byte_timeout=None,
                 exclusive=None,
                 **kwargs):
                 ...
  • 创建好后,就可以调用它的read(), write(), readline()等方法来读写数据了。

5.21 序列化Python对象

  • 希望将python对象转换为字节流,以进行网络传输、或保存到文件中
  • 使用pickle.dump(obj, file)序列化对象
  • 使用pickle.load(file)反序列化对象
import pickle

with open('test.pickle', 'wb') as f:
    pickle.dump('123', f)
    pickle.dump((1,2,3), f)
    pickle.dump({'1': '23'}, f)

with open('test.pickle', 'rb') as f:
    print(pickle.load(f))
    print(pickle.load(f))
    print(pickle.load(f))
  • 结果:
123
(1, 2, 3)
{'1': '23'}
  • 对函数、类、类实例均可以作序列化
  • 一些涉及系统状态的对象无法序列化,例如打开的文件、网络连接、进程、线程等
  • 如果一个类内部维护了一个线程,如:
import code
import pickle
import sys
import threading
from time import sleep


class CountDown:
    def __init__(self, n):
        self.n = n
        self.td = threading.Thread(target=self.count)
        self.td.daemon = True
        self.td.start()

    def count(self):
        while self.n > 0:
            print("Counting down! {}".format(self.n))
            sleep(2)
            self.n -= 1

    def __getstate__(self):
        return self.n

    def __setstate__(self, state):
        self.__init__(state)
  • 可以定义__getstate__()方法,那么在将对象序列化时,就可以获取线程当前的状态,在load()时,又调用了__setstate__()方法,那么线程就会继续进行。
  • 如果是以存储数据为目的,则应该用更加一般的数据格式,如CSV, XML, 或JSON

本章用到的方法、模块:

io.StringIO io.BytesIO # 创建类文件的对象以写入文本、字节串
gzip/bz2 # 读写压缩文件
functools.partial(callabale, size) # 读取指定大小的内容
file.readinto(buffer) # 将文件直接读入到一个缓存区中
mmap.mmap(fd, file_size, access) # 将二进制文件映射到内存
os.path # 定义了一些和路径相关的一些函数
sys.getfilesystemencoding() # 得到系统默认的文件编码
io.TextIOWrapper(file, encoding) # 修改文件的编码格式
tempfile # 包含创建临时文件、目录的函数
serial.Serial # 进行串口通信
pickle # 序列化、反序列化Python对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值