Python中的内建函数open
是脚本用来在计算机底层系统下访问文件的主要工具。open
函数被调用时将返回一个新的与外部文件相连的文件对象。这个文件对象有一些方法,可以与文件双向传输数据,并且能执行多种文件的相关操作。open
函数还为底层文件系统提供了可移植接口。
回忆一下,可以对打开的对象运行dir(obj)
来查看其属性、方法等;可以运行help(obj)
获取通用帮助信息,还可以运行help(obj.method)
获取method
特定方法的帮助信息。
Python 3.X中的str
字符串总是代表Unicode文本(ASCII或更广的字符集),而bytes
和bytearray
字符串代表原始二进制数据。
str
字符串——字符构成的序列**(准确地说是Unicode“代码点”)。在本章文本文件默认执行换行符自动转换,而且自动将 Unicode 编码应用于文件内容:它们在文件进行双向传输时依照一个给定或默认的编码名称,对原始二进制字节进行编码和解码。编码对 ASCII 文本来说很简单,但在其他情况下可能比较复杂。在实践中,文本文件用于所有真正的文本相关数据,而二进制文件则用于存储内容。
从程序员的角度看,这两种文件类型是在通过传入open
的模式字符串参数来区分的:参数中填上“b”(比如rb
、wb
等),将意味着文件包含二级制数据。在对编码新文件内容时,对文本使用普通字符串(如’spam’或bytes.decode
),而对二进制使用字节字符串(如b’spam’或str.encode
)。
由于文本模式要求文件内容能够按照某个Unicode编码方案的内容进行解码,所以你必须在二进制模式下将不可解码的内容读取为字节字符串(或者在try
语句中捕获Unicode异常,然后整个跳过该文件)。这些文件可能包括真正的二进制文件以及使用非默认的未知编码的文本文件。
>>> file = open('data.txt', 'w') # 打开输出文件对象:创建 >>> file.write('1') # 逐字写入字符串 1 # 返回所写入的字符或字节数目 >>> file.write('2') 1 >>> file.writelines(['3', '4']) # 将列表里的所有字符串一次性写入 >>> file.writelines(['5', '6']) >>> file.close() # 在垃圾回收和退出时也会关闭文件
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat data.txt 123456
open('file', 'w').write('Good night\n') # 写入临时对象 open('file', 'r').read() # 从临时对象中读取
close
方法当程序抛出异常,文件还未手动关闭时,首先要确认是否必须关闭——文件回收时会自动关闭,关闭最终会执行,即便发生异常。
如果必须关闭,那么又两种基本解决方法。
try ... finally ...
语句my_file = open(filename, 'w') try: ...process my_file... finally: my_file.close()
with ...
语句。这个语句依赖于文件对象的上下文管理器:无论发生什么异常行为,代码在进入和离开语句时都能自动运行。with
语句也可指定多个(嵌套)上下文管理器。# 单个上下文管理器 with open(filename, 'w') as my_file: ...process my_file, auto-closed on statement exit... # 多个上下文管理器:可用逗号隔开,或者嵌套 with open(file_name_1, 'w') as file_1, open(file_name_2, 'w') as file_2: ...statements... with open(file_name_1, 'w') as file_1: with open(file_name_2, 'w') as file_2: ...statements...
with
仅应用于支持上下文管理协议的对象,而try...finally
允许任意异常情况下的任意退出操作。
>>> file = open('data.txt') >>> >>> file.read() '123456' >>> >>> file.seek(0) 0 >>> file.read(1) '1' >>> file.read(3) '234' >>> file.read(-1) '56' >>> >>> file.seek(0) 0 >>> file.readline() '123456' >>> >>> file.seek(0) 0 >>> file.readlines() ['123456']
file.seek(N)
:调用seek
只是让我们下次的传输操作移动到偏移值为N的新位置,文件中所有的读取和写入操作都发生于当前位置。file.read()
:返回一个字符串,它包含返回在文件中的所有字符。file.read(N)
:返回一个字符串,它包含文件中接下来的N个字符(或字节)。file.readline()
:读取下一个\n之前的内容并返回一个行字符串。file.readlines()
:读取整个文件并返回一个行字符串列表。演示需要,修改了data.txt:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat data.txt 1 2 3 4 5 6
无需使用readlines
,迭代器会自动读取行:
>>> for line_str in open('data.txt'): ... print(line_str, end='') ... 1 2 3 4 5 6
你可以手动使用文件迭代器:它只是一个__next__
方法(由内建函数next
运行),与每次调用readline
方法类似,只是readline
方法在文件末尾(EOF)返回一个空字符串,而迭代器则抛出一个异常来结束迭代。
>>> file = open('data.txt') >>> >>> file.readline() '1\n' >>> file.readline() '2\n' >>> file.readline() '3\n' >>> file.readline() '4\n' >>> file.readline() '5\n' >>> file.readline() '6\n' >>> file.readline() '' >>> >>> file.seek(0) 0 >>> file.__next__() '1\n' >>> file.__next__() '2\n' >>> file.__next__() '3\n' >>> file.__next__() '4\n' >>> file.__next__() '5\n' >>> file.__next__() '6\n' >>> file.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> >>> file.seek(0) 0 >>> next(file) '1\n' >>> next(file) '2\n' >>> next(file) '3\n' >>> next(file) '4\n' >>> next(file) '5\n' >>> next(file) '6\n' >>> next(file) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
有趣的是,在所有迭代场景下,迭代器都自动得以调用,包括但不仅限于以下几种情况:
>>> list(open('data.txt')) # 强制逐行迭代 ['1\n', '2\n', '3\n', '4\n', '5\n', '6\n'] >>> lines_list = [line_str.rstrip() for line_str in open('data.txt')] # 解析 >>> lines_list ['1', '2', '3', '4', '5', '6'] >>> lines_list = [int(line_str) ** 2 for line_str in open('data.txt')] # 任意操作 >>> lines_list [1, 4, 9, 16, 25, 36] >>> list(map(int, open('data.txt'))) # 应用函数 [1, 2, 3, 4, 5, 6] >>> line = '3\n' >>> line in open('data.txt') # 判断文件是否含有该行 True
<<<<<<< HEAD
>>> file = open('data.txt', 'wb') >>> file = open('data.txt', 'wb')
open
模式参数要加上b。read
、write
方法用来读取文件内容。在这里,readline
和readlines
方法仍可使用,但是对于真正的二进制数据时,这下操作是没有意义的(换行符字节即使存在,也毫无意义)。在所有情况下,文件与程序间传输的数据,即使是二进制数据,在脚本中还是表示为Python字符串。然而如果使用二进制模式打开文件,文件内容则显示为字节字符串。
>>> open('data.txt', 'r').read() '1\n2\n3\n4\n5\n6\n' >>> open('data.txt', 'rb').read() b'1\n2\n3\n4\n5\n6\n' >>> >>> for line_bstr in open('data.txt', 'rb'): ... print(line_bstr) ... b'1\n' b'2\n' b'3\n' b'4\n' b'5\n' b'6\n'
你也必须为二进制输出文件提供字节字符串。
>>> open('data.bin', 'wb').write(b'spam\n') 5 >>> open('data.bin', 'rb').read() b'spam\n'
请注意该文件每行仅以“\n”结尾,在Windows下以“\r\n”结尾。严格说来,二进制模式不仅使Unicode转码无法进行,还阻止了文本模式下默认换行符的自动转换。
下面这个字符串包含一个Unicode字符的字符串,其二进制值在ASCII编码标准的7位范围之外。
>>> open('data.bin', 'wb').write(b'spam\n') 5 >>> open('data.bin', 'rb').read() b'spam\n' >>> data_str = 'sp\xe4m' >>> data_str 'späm' >>> 0xe4, bin(0xe4), chr(0xe4) (228, '0b11100100', 'ä')
也可以手动编码:
>>> data_str.encode('latin1') # 8位字符:ascii + 额外字符 b'sp\xe4m' >>> data_str.encode('utf8') # 仅特殊字符转换为二进制序列 b'sp\xc3\xa4m' >>> data_str.encode('ascii') # 不能遵照ascii进行编码 Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'ascii' codec can't encode character '\xe4' in position 2: ordinal not in range(128) >>> data_str.encode('utf16') # 每个字符2字节,加上前缀 b'\xff\xfes\x00p\x00\xe4\x00m\x00' >>> data_str.encode('cp500') # 另一种ebcdic编码:很不一样 b'\xa2\x97C\x94'
不过,如果我们在二进制模式下打开文件,是不会进行编码转换的。
>>> open('data.txt', 'w', encoding='latin1').write(data_str) 4 >>> open('data.txt', 'r', encoding='latin1').read() 'späm' >>> open('data.txt', 'rb').read() b'sp\xe4m'
>>> open('data.txt', 'w', encoding='utf8').write(data_str) # utf8编码并写入 4 >>> open('data.txt', 'r', encoding='utf8').read() # utf8解码 'späm' >>> open('data.txt', 'r', encoding='latin1').read() # latin1解码 'späm' >>> open('data.txt', 'rb').read() b'sp\xc3\xa4m'
这一次,虽然原始文件内容有所不同,但文本模式的自动解码使得字符串在脚本读取返回之前变得相同。请注意,尝试写入不可编码的数据或阅读不可编码的数据都将引起错误。
除非使用了二进制模式,否则以下代码可以在编码类型已知时重新创建原始字符串,在编码类型未知时操作失败。
>>> open('data.txt', 'w', encoding='cp500').write('I LOVE U\n') 9 >>> open('data.txt', 'r', encoding='cp500').read() 'I LOVE U\n' >>> open('data.txt', 'r').read() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/home/alone/anaconda3/lib/python3.7/codecs.py", line 322, in decode (result, consumed) = self._buffer_decode(data, self.errors, final) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc9 in position 0: invalid continuation byte >>> open('data.txt', 'rb').read() b'\xc9@\xd3\xd6\xe5\xc5@\xe4%'
如果Python脚本在Windows下运行,在默认情况下,文件对象会自动将DOS “\r\n”序列映射为单独的“\n”。
在类Unix平台上之所以不会发生任何转换,是因为文件里使用的是“\n”。这些规则中,有两条重要结果必须要牢记:
最后,需要牢记的是:文本文件的内容一般都应当以“\n”来代表结尾,而二进制数据应当总是在二进制模式下打开,以便阻止换行符转换和Unicode编码。
struct
模块解析打包的二进制数据struct
模块提供用于打包和解压二进制的调用。它能够用你想用的任何一种字节序来组合和分解(字节序决定了二进制数字的最高有效位是居左还是居右)。
>>> import struct >>> >>> data_b = struct.pack('>i4shf', 2, b'spam', 3, 1.234) >>> data_b b'\x00\x00\x00\x02spam\x00\x03?\x9d\xf3\xb6' >>> open('data.bin', 'wb').write(data_b) 14
pack
调用的格式字符串是一个高位优先(>
),它包含一个整数(i
)、一个四字符的字节字符串(4s
)、一个半整数(h
),以及一个浮点数(f
)。也可以解析数据:
>>> values_tuple = struct.unpack('>i4shf', open('data.bin', 'rb').read()) >>> values_tuple (2, b'spam', 3, 1.2339999675750732)
我们可以对字符串和按位运算进行深层探索:
>>> bin(values_tuple[0] | 0b1) '0b11' >>> values_tuple[1], list(values_tuple[1]), values_tuple[1][0] (b'spam', [115, 112, 97, 109], 115)
文件的open
模式字符串添加+
号后可读取又可写入。这种模式通常与文件对象的seek
方法联合使用,以便支持随机读取或写入访问。这种灵活的文件处理模式允许我们执行从一处读取字节,再在另一处写入等操作。当脚本把这一性能与二进制文件模式结合在一起时,便可在一个文件内获取和更新任意字节。
Python的seek
方法也接受可选的第二参数,这个参数有3种值:0
表示绝对文件位置(默认值);1
表示基于当前所在的相对位置;2
表示寻求基于文件结尾的相对位置。
为了证明上述结论,让我们在wb+
的模式下创建一个文件,但如果文件已经存在,此模式会清空文档内容(所有w
模式都会)。
>>> records_list = [bytes([i_str] * 8) for i_str in b'spam'] >>> records_list [b'ssssssss', b'pppppppp', b'aaaaaaaa', b'mmmmmmmm'] >>> >>> file = open('random.bin', 'wb+') >>> file.writelines(records_list) >>> file.seek(0) 0 >>> file.read() b'ssssssssppppppppaaaaaaaammmmmmmm'
现在让我们在rb+
模式下重新打开文件,这个模式也允许读取和写入,但是初始化时并不会清空文件。
>>> file = open('random.bin', 'rb+') >>> file.read() b'ssssssssppppppppaaaaaaaammmmmmmm' >>> file.seek(0) 0 >>> file.write(b'X' * 8) # 更新第一条记录 8 >>> file.seek(0) 0 >>> file.read(8) b'XXXXXXXX' >>> file.write(b'Y' * 8) # 更新第二条记录 8 >>> file.seek(0) 0 >>> file.read() b'XXXXXXXXYYYYYYYYaaaaaaaammmmmmmm'
os
模块中的底层文件工具os
模块包含一个文件处理函数的附加集合。下面列出了部分os
文件的相关调用:
os.open(path, flags, mode)
:打开文件并返回其描述符。os.read(descriptor, N)
:最多读取N个字节并返回字节字符串。os.write(descriptor, string)
:把文件字符串string中的字节写入文件。os.lseek(descriptor, position, how)
:在文件中移至position严格地说,os
调用通过文件的描述符来处理文件,描述符是整数代码或“句柄”,代表着操作系统中的文件。基于描述符的文件以原始字节的形式来处理,而且没有我们之前所学的文本的换行符和Unicode转换的概念。事实上,除了缓冲等额外性能,基于描述符的文件一般都能对应上二进制模式文件对象,而且我们也可以类似地读取和写入bytes字符串而非str字符串。然而,与带有open
内建函数的内建文件对象相比,os
中基于文件描述符的文件工具更底层且更复杂,所以,除非有非常特殊的文件处理需求,否则一般使用open
函数。
os.open
文件>>> import sys >>> >>> for stream_file in (sys.stdin, sys.stdout, sys.stderr): ... print(stream_file.fileno()) ... 0 1 2 >>> >>> >>> import os >>> sys.stdout.write('Hello stdio world!\n') # 借助文件方法写入 Hello stdio world! 19 >>> os.write(1, b'Hello descriptor world!\n') # 借助os模块写入 Hello descriptor world! 24
fileno
文件对象方法返回的整数描述符是与某个内建文件对象相关联的。例如,标准流文件对象拥有描述符0、1、2;调用os.write
函数,通过描述符将数据发送至stdout
,与调用sys.stdout.write
方法的效果是一样的。我们可以通过内建函数open
、os
模块·中的工具或者二者结合起来使用处理给定的外部文件。
>>> file = open('spam.txt', 'w') >>> file.write('Hello stdio world!\n') 19 >>> file.flush() >>> file.fileno() 3 >>> >>> >>> os.write(3, b'Hello descriptor world!\n') 24 >>> file.close()
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt Hello stdio world! Hello descriptor world!
需要注意两点:
os.write
写入的是字节字符串(bytes)。os.write
之后使用file.close()
。os.open
的模式标识符为什么Python会提供额外的文件工具呢?简而言之,它们为文件处理提供更多底层控制。内建的open
函数受限于所使用的的底层文件系统,os
模块让脚本进行更精细地控制。
>>> fdfile = os.open('spam.txt', os.O_RDWR) >>> os.read(fdfile, 20) b'Hello stdio world!\nH' >>> os.lseek(fdfile, 0, 0) # 回到文件起始处 0 >>> os.read(fdfile, 100) # 在二进制模式下保留 b'Hello stdio world!\nHello descriptor world!\n' >>> os.lseek(fdfile, 0, 0) 0 >>> os.write(fdfile, b'HELLO') # 覆盖前5个字节 5
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt HELLO stdio world! Hello descriptor world!
fdfile = os.open('spam.txt', (os.O_RDWR | os.O_BINARY))
,这样通过对os
导出的两个模式标识符进行二进制的“或”操作,从而以读-写和二进制模式打开一个基于描述符的文件。>>> spam_file_rbplus = open('spam.txt', 'rb+') # 同上,只是采用open文件对象 >>> spam_file_rbplus.read() b'HELLO stdio world!\nHello descriptor world!\n' >>> spam_file_rbplus.seek(0) 0 >>> spam_file_rbplus.write(b'Hello') 5 >>> spam_file_rbplus.flush() >>> spam_file_rbplus.close()
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt Hello stdio world! Hello descriptor world!
但在某些系统上,os.open
标识符可指定更高级的参数,因此有些标识符是不可移植的。
我们可以利用os.fdopen
调用把文件描述符封装进文件对象。
>>> import os >>> >>> fd_int = os.open('spam.txt', os.O_RDWR) >>> fdfile = os.fdopen(fd_int, 'rb') >>> fdfile.read() b'Hello stdio world!\nHello descriptor world!\n'
文件描述符封装的文件对象:在文本模式下,读取和写入时将执行我们之前学过的Unicode编码和换行符转换,并且处理的是str字符串,而非bytes字符串:
>>> fd_int = os.open('spam.txt', os.O_RDWR) >>> fdfile = os.fdopen(fd_int, 'r') >>> fdfile.read() 'Hello stdio world!\nHello descriptor world!\n' >>> type(fdfile.read()) <class 'str'>
open
和os.fdopen
都可接受额外的控制参数。
>>> fd_int = os.open('spam.txt', os.O_RDWR) >>> >>> file = open(fd_int, 'r', encoding='latin1', closefd=False) >>> file.read() 'Hello stdio world!\nHello descriptor world!\n' >>> fdfile = os.fdopen(fd_int, 'r', encoding='latin1', closefd=False) >>> fdfile.read() 'Hello stdio world!\nHello descriptor world!\n' >>> fdfile.close()
os
模块文件工具>>> os.chmod('spam.txt', 0o777) # 修改权限 >>> os.rename(fn_1, fn_2) # 文件名fn_1 -> 文件名fn_2 >>> os.remove(fn) # 删除文件fn 与os.unlink同义
>>> open('spam.txt', 'w').write('Hello stat world\n') 17 >>> info_tuple = os.stat('spam.txt') >>> info_tuple os.stat_result(st_mode=33279, st_ino=256914, st_dev=2049, st_nlink=1, st_uid=1000, st_gid=1000, st_size=17, st_atime=1627653023, st_mtime=1627655368, st_ctime=1627655368) >>> info_tuple.st_mode, info_tuple.st_size (33279, 17) >>> import stat >>> info_tuple[stat.ST_MODE], info_tuple[stat.ST_SIZE] (33279, 17) >>> stat.S_ISDIR(info_tuple.st_mode), stat.S_ISREG(info_tuple.st_mode) (False, True)
现在让我们做一个工具,它能演示目前为止我们已经学习过的内容。
下面的模块定义了通用文件扫描例行程序:
示例:scan_file.py
#!/usr/bin/env python def file_scanner(fn_str, function): "通用文件扫描例行函数" file = open(fn_str, 'r') # 创建文件对象 while True: line_str = file.readline() # 调用文件方法 if not line_str: break # 直到文件末尾 function(line_str) # 调用一个函数对象 file.close()
下面是一个进行简单逐行转换的客户端脚本:
示例:commands.py
#!/usr/bin/env python from sys import argv from scan_file import file_scanner class UnknownCommand(Exception): "一个“未知命令”异常的类" pass def process_file(line_str): "一个逐行将“*name”和“+name”转换为“Ms.name”和“Mr.name”的函数" if line_str[0] == '+': # 剥去开头和末尾的字符:\n print('Mr.' + line_str[1:-1]) elif line_str[0] == '*': print('Ms.' + line_str[1:-1]) else: # 抛出异常 raise UnknownCommand(line_str) if len(argv) == 2: # 允许通过文件名命令行参数传入文件 fn_str = argv[1] else: fn_str = 'data.txt' # 运行扫描器 file_scanner(fn_str, process_file)
测试:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat hillbillies.txt *Granny +Jethro *Elly May +"Uncle Jed" beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ ./commands.py hillbillies.txt Ms.Granny Mr.Jethro Ms.Elly May Mr."Uncle Jed"
示例:file_scan.py替代方案
# 替代方案 A def file_scanner(fn_str, function): # 使用文件迭代器逐行扫描 for line_str in open(fn_str, 'r'): # 调用一个函数对象 function(line_str) # 替代方案 B def file_scanner(fn_str, function): # 使用map代替了for循环 list(map(function, open(fn_str, 'r'))) # 替代方案 C def file_scanner(fn_str, function): # 使用列表解析代替for循环 [function(line_str) for line_str in open(fn_str, 'r')] # 替代方案 D def file_scanner(fn_str, function): # 使用列表解析代替for循环 list(function(line_str) for line_str in open(fn_str, 'r'))
示例:commands.py替代方案
# 数据比代码更容易拓展 commands_dict = {'+': 'Mr.', '*': 'Ms.'} def process_file(line_str): try: print(commands_dict[line_str[0]] + line_str[1:-1]) except KeyError: raise UnknownCommand(line_str)
#!/usr/bin/env python import sys def filter_file(fn_str, function): "显示指定文件" input_file = open(fn_str, 'r') # 显示地指出输出文件 output_file = open(fn_str + '.out', 'w') for line_str in input_file: output_file.write(function(line_str)) input_file.close() output_file.close() # 替代方案 # 利用上下文管理器 # def filter_file(fn_str, function): # with open(fn_str, 'r') as input_file, open(fn_str + '.out', 'w') as output_file: # for line_str in input_file: # output_file.write(function(line_str)) def filter_stream(function): "利用标准输入/输出流允许在命令行中重定向" while True: line_str = sys.stdin.readline() # 可替换为input() if not line_str: break print(function(line_str), end='') # 可替换为sys.stdout.write() # 替代方案 # 利用文件对象的行迭代器 # def filter_stream(function): # for line_str in sys.stdin: # print(function(line_str), end='') if __name__ == '__main__': # 将stdin复制到stdout filter_stream(lambda line_str: line_str)
运行结果:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ ./filters.py < hillbillies.txt *Granny +Jethro *Elly May +"Uncle Jed"
>>> from filters import filter_file >>> filter_file('hillbillies.txt', str.upper) >>> print(open('hillbillies.txt.out').read()) *GRANNY +JETHRO *ELLY MAY +"UNCLE JED"
:)学完博客后,是不是有所启发呢?如果对此还有疑问,欢迎在评论区留言哦。
如果还想了解更多的信息,欢迎大佬们关注我哦,也可以查看我的个人博客网站BeacherHou