第五章 文件和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 ( * [ "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. basename( path)
os. path. dirname( path)
os. path. expanduser( path)
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. 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 创建临时文件和目录
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