10.文件操作(IO 技术)

一个完整的程序一般都包括数据的存储和读取;我们在前面写的程序数据都没有进行实际的存储,因此 python 解释器执行完数据就消失了。实际开发中,我们经常需要从外部存储介质(硬盘、光盘、U 盘等)读取数据,或者将程序产生的数据存储到文件中,实现“持久化”保存。
有基础的同学知道,很多软件系统是将数据存储的数据库中;数据库实际也是基于文件形式存储的,本章我们就学习文件的相关操作。
1 文本文件和二进制文件
按文件中数据组织形式,我们把文件分为文本文件和二进制文件两大类。
-
文本文件
文本文件存储的是普通“字符”文本,python 默认为 unicode 字符集(两个字节表示一个字符,最多可以表示: 65536 个),可以使用记事本程序打开。但是,像 word 软件编辑的文档不是文本文件。
-
二进制文件
二进制文件把数据内容用“字节”进行存储,无法用记事本打开。必须使用专用的软件解码。常见的有:MP4 视频文件、MP3 音频文件、JPG 图片、doc 文档等等。

2 文件操作相关模块概述
Python 标准库中,如下是文件操作相关的模块,我们会陆续给大家介绍。
| 名称 | 说明 |
|---|---|
| io 模块 | 文件流的输入和输出操作 input output |
| os 模块 | 基本操作系统功能,包括文件操作 |
| glob 模块 | 查找符合特定规则的文件路径名 |
| fnmatch 模块 | 使用模式来匹配文件路径名 |
| fileinput 模块 | 处理多个输入文件 |
| filecmp 模块 | 用于文件的比较 |
| cvs 模块 | 用于 csv 文件处理 |
| pickle 和 cPickle | 用于序列化和反序列化 |
| xml 包 | 用于 XML 数据处理 |
| bz2、gzip、zipfile、zlib 、tarfile | 用于处理压缩和解压缩文件(分别对应不同的算法) |
3 创建文件对象 open()
open() 函数用于创建文件对象,基本语法格式如下:
open(file, mode='r', buffering=-1, encoding=None, errors= None, newline=None, closefd=True, opener=None)
打开一个文件,返回一个文件对象(流对象)和文件描述符。打开文件失败,则返回异常
基本使用:创建一个文件 test ,然后打开它,用完关闭
f = open("test") # file对象
# windows <_io.TextIOWrapper name='test' mode='r' encoding='cp936'>
# linux <_io.TextIOWrapper name='test' mode='r' encoding='UTF-8'>
print(f.read()) # 读取文件
f.c1ose()
文件操作中,最常用的操作就是读和写。
文件访问的模式有两种:文本模式和二进制模式。不同模式下,操作函数不尽相同,表现的结果也不一样。
注:
windows 中使用 codepage 代码页,可以认为每一个代码页就是一张编码表。cp936 等同于 GBK 。
如果只是文件名,代表在当前目录下的文件。文件名可以录入全路径,比如: D:\a\b.txt 。
为了减少 \ 的输入,可以使用原始字符串: r"d:\b.txt"。示例如下:
f = open (r"d:\b.txt", "w")
3.1 open() 参数
file
打开或者要创建的文件名。如果不指定路径,默认是当前路径
mode 模式 ★★★
打开方式有如下几种:
| 模式 | 描述 |
|---|---|
| r | 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。 |
| rb | 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。 |
| r+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
| rb+ | 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。 |
| w | 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| wb | 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| w+ | 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| wb+ | 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| a | 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
| ab | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
| a+ | 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。 |
| ab+ | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。 |
| x | 只写,不支持读取。 文件存在,抛异常——文件存在异常。 文件不存在,创建新文件,从头写。 |
| t | t 是 text ,文本模式,默认可以不写。 t 模式不能单独使用,必须和 rwax 四种之一配合使用,不影响主模式 |
| b | b 是 binary , 二进制 b 模式不能单独使用,必须和 rwax 四种之一配合使用,不影响主模式 |
虽然 mode 文件操作模式很多,但是我们只需要记住 3 个字符即可。r、w、a
r+、w+、a+,带加号,功能全,既能读,又能写(区别在于指针到底指向不同)
rb、wb、ab,代 b 的字符,代表以二进制的形式对其进行操作,适合读取文本或二进制格式文件,如图片、音频、视频等格式
rb+、wb+、ab+,带加号,功能全,既能读,又能写(区别在于指针到底指向不同)
文本文件对象和二进制文件对象的创建:
- 如果我们没有增加模式
b,则默认创建的是文本文件对象,处理的基本单元是字符。 - 如果是二进制模式
b,则创建的是二进制文件对象,处理的基本单元是字节。
buffering 缓冲区
-1表示使用缺省大小的 buffer。- 如果是二进制模式,使用
io.DEFAULT_BUFFER_SIZE值,默认是 4096 或者 8192 。 - 如果是文本模式,且是终端设备,是行缓存方式;如果不是,则使用二进制模式的策略。
- 0,只在二进制模式使用,表示关闭 buffer
- 1,只在文本模式使用,表示使用行缓冲。意思就是见到换行符就 flush
- 大于 1,用于指定 buffer 的大小
buffer 缓冲区
缓冲区是一个内存空间,一般来说是一个 FIFO 队列,当缓冲区满了或者达到阈值,数据才会 flush 到磁盘。
flush() 将缓冲区数据写入磁盘
close() 关闭前会调用 flush()
io.DEFAULT_BUFFER_SIZE 查看缺省缓冲区大小,单位是字节
-
buffering = -1使用默认缓冲区大小。一般缓冲区默认大小为 4096 (4K) 或 8192 (8K) 。
-
buffering = 0文本模式不支持。
二进制模式无缓冲区,哪怕只有一个字节,立即写入磁盘,文件当做一个 FIFO 队列了。
-
buffering = 1文本模式,行缓冲模式,认
\n,遇到\n就输出,连同当前\n前的字符一起写入磁盘;文本模式下,如果缓冲区撑破了,会被迫写入磁盘。二进制模式,使用默认缓冲区。
-
buffering > 1文本模式,使用默认缓冲区大小。
二进制模式,指定大小字节的缓冲区。
先看二进制模式(jupyter notebook 中一行一行运行,查看效果)
import io
f = open('test', 'w+b')
# 缺省缓冲区大小
print(io.DEFAULT_BUFFER_SIZE) # IPython 中为 8192
f.write("小龙女".encode())
# cat test
f.seek(0)
# cat test
f.write("杨过".encode())
f.flush()
f.close()
#%%
# 缓冲区大小为 0
f = open("test", "w+b", 0)
f.write(b'a')
# cat test
f.write(b'a')
# cat test
f.close()
#%%
# 缓冲区大小为 1
f = open("test", "w+b", 1)
f.write(b'a')
# cat test
f.write(b'a' * 200)
# cat test
f.write(b'a' * 8000)
# cat test
f.close()
#%%
# 设置缓冲区大小
f = open("test", "w+b", 4)
f.write(b'php')
# cat test
f.write(b'python')
# cat test
f.close()
二进制模式
- -1 是缺省值,使用缺省缓冲区大小
- 0 关闭缓冲区,不需要内存的 buffer ,可以看做是一个 FIFO 的文件
- 1 直接使用缺省缓冲区大小
- 大于 1,使用指定缓冲区大小
文本模式
# buffering=0,行吗?
# f = open('test4', 'w+', 0)
# buffering=1,使用行缓冲
f = open('test4', 'w+', 1)
f.write("php")
# cat test4
f.write("python" * 4) #cat test4
f.write('\n')
# cat test4
f.write('Hello\nPython')
# cat test4
f.close()
#%%
# buffering>1,使用默认大小的缓冲区
f = open('test4', 'w+', 15)
f.write("php")
# cat test4
f.write('java')
# cat test4
f.write('Hello\n')
# cat test4
f.write('\nPython')
# cat test4
f.write('a' * (io.DEFAULT_BUFFER_SIZE - 20)) # 设置为大于1没什么用
f.write('\nwww.python.com/python')
f.close()
文本模式
-
-1 是缺省值,使用缺省缓冲区大小
-
0 不支持
![image-20221029152515161]()
-
1 行缓冲模式,认
\n,遇到\n就输出,连同当前\n前的字符一起写入磁盘;文本模式下,如果缓冲区撑破了,会被迫写入磁盘。 -
大于 1,默认缓冲区大小
| buffering | 说明 |
|---|---|
| buffering = -1 | t 和 b,都使用 io.DEFAULT_BUFFER_SIZE 大小 |
| buffering = 0 | b 关闭缓冲区 t 不支持 |
| buffering = 1 | b 不支持 t 行缓冲,遇到换行符才 flush |
| buffering > 1 | b 模式表示缓冲大小。缓冲区的值可以超过 io.DEFAULT_BUFFER_SIZE ,直到设定的值超出后才把缓冲区 flusht 模式,是 io.DEFAULT_BUFFER_SIZE 字节,flush 完后把当前字符串也写入磁盘 |
似乎看起来很麻烦,一般来说,只需要记得:
- 文本模式,一般都用默认缓冲区大小
- 二进制模式,是一个个字节的操作,可以指定 buffer 的大小
- 一般来说,默认缓冲区大小是个比较好的选择,除非明确知道,否则不调整它
- 一般编程中,明确知道需要写磁盘了,都会手动调用一次 flush ,而不是等到自动 flush 或者 close 的时候
encoding
encoding 属性,指定编码,仅文本模式下使用。
None 表示使用缺省编码,依赖操作系统。
- windows 下缺省 GBK(0xB0A1)
- Linux 下缺省 UTF-8(0xE5 95 8A)
errors
什么样的编码错误将被捕获
None 和 strict 表示有编码错误将抛出 ValueError 异常;
ignore 表示忽略
newline
文本模式中,换行的转换。取值可以为 None、""、\r、\n、\r\n
写时
None,缺省值,表示\n都会被替换为系统缺省行分隔符os.linesep;'\n'或''表示\n不替换;- 其它合法换行字符表示文本中的
\n换行符会被替换为指定的换行字符
f = open('test', 'w')
# newline 缺省为 None,windows 下会把 \n 替换为 \r\n
f.write('python\rwww.python.org\nwww.java.org\r\npython3')
# 真正写入的是,将 \n 替换为 windows 的默认换行符 \r\n
# 'python\rwww.python.org\r\nwww.java.org\r\r\npython3'
f.close()

读时
None,缺省值,表示\r、\n、\r\n都被转换为\n;''表示不会自动转换通用换行符,常见换行符都可以认为是换行;- 其它合法换行字符表示按照该字符分行。
newlines = ['', None, '\n', '\r\n']
for nl in newlines:
f = open('test', newline=nl) #缺省替换所有换行符
print(f.readlines())
f.close()
# 运行结果如下
'' 表示什么都不做,常见换行符都用来分行
['python\r', 'www.python.org\r\n', 'www.java.org\r', '\r\n', 'python3']
None 常见换行符都替换为 \n ,用 \n 来分行
['python\n', 'www.python.org\n', 'www.java.org\n', '\n', 'python3']
\n 做为换行符
['python\rwww.python.org\r\n', 'www.java.org\r\r\n', 'python3']
\r\n 做为换行符
['python\rwww.python.org\r\n', 'www.java.org\r\r\n', 'python3']

closefd
每个文件打开时都会得到一个文件描述符,关闭时会把文件描述符交还回去,表示释放资源。
关闭文件描述符,True 表示关闭它。False 会在文件关闭后保持这个描述符。fileobj.fileno() 查看
f = open('test')
# 查看文件描述符
print(f.fileno())
#%%
f.close()
# 查看文件是否被关闭
print(f.closed)
# 文件描述符会在文件关闭后交还
print(f.fileno())

4 文本文件的写入
4.1 基本的文件写入操作
文本文件的写入一般就是三个步骤:
- 创建文件对象
- 写入数据
- 关闭文件对象
案例:文本写入操作简单测试
f = open(r"a.txt", "a")
s = "itbaizhan\nsxt\n"
f.write(s)
f.close()
或者使用 with 上下文管理:
with open(r"a.txt", "a") as f:
s = "itbaizhan\nsxt\n"
f.write(s)

4.2 常用编码介绍
在操作文本文件时 ,经常会操作中文 ,这时候就经常会碰到乱码问题 。为了让大家有能力解决中文乱码问题,这里简单介绍一下各种编码之间的关系。
常用编码之间的关系如下:

ASCII
全称为 American Standard Code for Information Interchange ,美国信息交换标准代码,这是世界上最早最通用的单字节编码系统,主要用来显示现代英语及其他西欧语言。
ASCII 码用 7 位表示 ,只能表示 128 个字符 。只定义了 $2^7=128$ 个字符 ,用 7bit 即可完全编码,而一字节 8bit 的容量是 256 ,所以一字节 ASCII 的编码最高位总是 0。
0~31 表示控制字符如回车、退格、删除等;32~126 表示打印字符即可以通过键盘输入并且能显示出来的字符;其中 48~57 为 0 到 9 十个阿拉伯数字,65~90 为 26 个大写英文字母,97~122 号为 26 个小写英文字母,其余为一些标点符号、运算符号等,具体可以参考 ASCII 标准表(大家自行百度,不在此赘述)。
ISO8859-1
ISO-8859-1 又称 Latin-1 ,是一个 8 位单字节字符集,它把 ASCII 的最高位也利用起来,并兼容了 ASCII ,新增的空间是 128 ,但它并没有完全用完。
在 ASCII 编码之上又增加了西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号,它是向下兼容 ASCII 编码
GB2312,GBK,GB18030
GB2312
GB2312 全称为信息交换用汉字编码字符集,是中国于 1980 年发布,主要用于计算机系统中的汉字处理。GB2312 主要收录了 6763 个汉字、682 个符号。
GB2312 覆盖了汉字的大部分使用率,但不能处理像古汉语等特殊的罕用字,所以后来出现了像 GBK 、GB18030 这种编码。
GB2312 完全兼容 ISO8859-1。
GBK
全称为 Chinese Internal Code Specification ,即汉字内码扩展规范,于 1995 年制定。它主要是扩展了 GB2312 ,在它的基础上又加了更多的汉字,它一共收录了 21003 个汉字
GB18030
现在最新的内码字集于 2000 年发布,并于 2001 年强制执行,包含了中国大部分少数民族的语言字符,收录汉字数超过 70000 余个。
它主要采用单字节、双字节、四字节对字符编码,它是向下兼容 GB2312 和 GBK 的,虽然是我国的强制使用标准,但在实际生产中很少用到,用得最多的反而是 GBK 和 GB2312
Unicode
Unicode 编码设计成了固定两个字节,所有的字符都用 16 位 ($2^{16}=65536$) 表示,包括之前只占 8 位的英文字符等,所以会造成空间的浪费,UNICODE 在很长的一段时间内都没有得到推广应用。
Unicode 完全重新设计,不兼容 iso8859-1,也不兼容任何其他编码。
UTF-8
对于英文字母,unicode 也需要两个字节来表示。所以 unicode 不便于传输和存储。因此而产生了 UTF 编码,UTF-8 全称是(8-bit Unicode Transformation Format)。
UTF 编码兼容 iso8859-1 编码,同时也可以用来表示所有语言的字符,不过,UTF 编码是不定长编码,每一个字符的长度从 1~4 个字节不等。其中,英文字母都是用一个字节表示,而汉字使用三个字节。
4.3 中文乱码问题
windows 操作系统默认的编码是 GBK ,Linux 操作系统默认的编码是 UTF-8 。当我们用 open() 时,调用的是当前操作系统打开的文件,默认的编码是 GBK (Windows)。
案例:中文字符文件,乱码出现测试
#%%
# 测试写入中文
f = open(r"b.txt", "w")
f.write("尚学堂 \n 百战程序员 \n ")
f.close()
运行结果( Linux 环境中不存在这个问题):

我们在文件编辑区单击右键,选择 FileEncoding ,选择 GBK 即可:

再选择 Reload,文件即显示正常。

案例:通过指定文件编码解决中文乱码问题
#%%
# 测试写入中文
f = open(r"b.txt", "w", encoding="utf-8")
f.write("尚学堂 \n 百战程序员 \n ")
f.close()

4.4 write()、writelines() 写入数据
write(a): 把字符串 a 写入到文件中
writelines(b):把字符串列表写入文件中,不添加换行符
案例:添加字符串列表数据到文件中
#%%
f = open(r"./bb.txt", "w", encoding="utf-8 ")
s = ["高淇\n", "高老三\n", "高老四\n"]
f.writelines(s)
f.close()

4.5 close() 关闭文件流

由于文件底层是由操作系统控制,所以我们打开的文件对象必须显式调用 close() 方法关闭文件对象。当调用 close() 方法时,首先会把缓冲区数据写入文件(也可以直接调用 flush() 方法),再关闭文件,释放文件对象。
为了确保打开的文件对象正常关闭,一般结合异常机制的 finally 或者 with 关键字实现无论何种情况都能关闭打开的文件对象。
案例:结合异常机制 finally 确保关闭文件对象
#%%
try:
f = open(r"my01.txt", "a")
str = "gaoqi"
f.write(str)
except BaseException as e:
print(e)
finally:
f.close()
4.6 with 语句 (上下文管理器 )
with 关键字(上下文管理器)可以自动管理上下文资源,不论什么原因跳出 with 块,都能确保文件正确的关闭,并且可以在代码块执行完毕后自动还原进入该代码块时的现场。
案例:使用 with 管理文件写入操作
#%%
s = ["高淇\n", "高老三\n", "高老五\n"]
with open(r"./bb.txt", "w") as f:
f.writelines(s)
print(f, f.closed)
# 测试 f 是否关闭
f.closed
上下文管理
- 使用
with关键字,上下文管理针对的是with后的对象 - 可以使用
as关键字 - 上下文管理的语句块并不会开启新的作用域
文件对象上下文管理
- 进入
with时,with后的文件对象是被管理对象 as子句后的标识符,指向with后的文件对象with语句块执行完的时候,会自动关闭文件对象
另一种写法
f1 = open('test')
print(f1.closed)
with f1:
# 文件只读,写入失败
# f1.write("abc")
print(f1, f1.closed)
# 测试 f 是否关闭
print(f1.closed)

- 对于类似于文件对象的 IO 对象,一般来说都需要在不使用的时候关闭、注销,以释放资源。
- IO 被打开的时候,会获得一个文件描述符。计算机资源是有限的,所以操作系统都会做限制。就是为了保护计算机的资源不要被完全耗尽,计算资源是很多程序共享的,不是独占的。
- 一般情况下,除非特别明确的知道当前资源情况,否则不要盲目提高资源的限制值来解决问题。
5 文本文件的读取
文件的读取一般使用如下三个方法:
-
read([size])
从文件中读取size个字符,并作为结果返回。如果没有size参数,则读取整个文件。读取到文件末尾,会返回空字符串。 -
readline([limit])
读取一行内容作为结果返回。读取到文件末尾,会返回空字符串。limit表示从本行中读取几个字符。 -
readlines()
文本文件中,每一行作为一个字符串存入列表中,返回该列表。
案例:读取一个文件前 4 个字符
with open(r"bb.txt", "r", encoding="utf-8") as f:
print(f.read(4))
案例:文件较小,一次将文件内容读入到程序中
with open(r"./bb.txt", "r") as f:
print(f.read())
案例:按行读取一个文件
with open(r"bb.txt", "r", encoding='utf-8') as f:
while True:
fragment = f.readline()
if not fragment:
break
else:
print(fragment, end="")

案例:使用迭代器(每次返回一行)读取文本文件
#%%
with open(r"./bb.txt", "r", encoding='utf-8') as f:
for a in f:
print(a, end="")

案例:为文本文件每一行的末尾增加行号
#%%
with open("bb.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
# 推导式生成列表
lines = [line.rstrip() + " #" + str(index + 1) + "\n" for index, line in enumerate(lines)]
with open("bb.txt", "w", encoding="utf-8") as f:
f.writelines(lines)
执行前:

执行后:

6 二进制文件的读取和写入
二进制文件的处理流程和文本文件流程一致。首先还是要创建文件对象,不过,我们需要指定二进制模式,从而创建出二进制文件对象。例如:
f = open(r"d:\a.txt", 'wb') # 可写的、重写模式的二进制文件对象
f = open(r"d:\a.txt", 'ab') # 可写的、追加模式的二进制文件对象
f = open(r"d:\a.txt", 'rb') # 可读的二进制文件对象
创建好二进制文件对象后,仍然可以使用 write() 、read() 实现文件的读写操作。
案例:读取图片文件,实现文件的拷贝(注意指针位置)

with open(r'ly.jpeg', 'rb') as f:
with open('ly_back.jpeg', 'wb') as w:
print(f.readline())
print(f.tell())
f.seek(0, 0) # 非常重要!!!
print(f.readlines())
print(f.tell())
# 重置指针到开头
f.seek(0, 0) # 非常重要!!!
for line in f.readlines():
w.write(line)
print('图片拷贝完成!')


7 文件对象的常用属性和方法
文件对象封装了文件相关的操作。在前面我们学习了通过文件对象对文件进行读写操作。本节我们详细列出文件对象的常用属性和方法,并进行说明。
文件对象的属性:
| 属性 | 说明 |
|---|---|
| name | 返回文件的名字 |
| mode | 返回文件的打开模式 |
| closed | 若文件被关闭则返回 True |
文件对象的打开模式:
| 模式 | 说明 |
|---|---|
| r | 读模式 |
| w | 写模式 |
| a | 追加模式 |
| b | 二进制模式(可与其他模式组合) |
| + | 读写模式(可以其他模式组合) |
文件对象的常用方法:
| 方法名 | 说明 |
|---|---|
| read([size]) | 从文件中读取 size 个字节或字符的内容返回。若省略 [size],则读取到文件末尾,即一次读取文件所有内容 |
| readline() | 从文本文件中读取一行内容 |
| readlines() | 把文本文件中每一行都作为独立的字符串对象,并将这些对象放入列表返回 |
| write(str) | 将字符串 str 内容写入文件 |
| writelines(s) | 将字符串列表 s 写入文件文件,不添加换行符 |
| seek(offset[,whence]) | 把文件指针移动到新的位置,offset 表示相对于 whence 的多少个字节的偏移量; offset:off 为正往结束方向移动,为负往开始方向移动 whence:不同的值代表不同含义     0:从文件头开始计算(默认值)     1:从当前位置开始计算     2:从文件结尾开始计算 |
| tell() | 返回文件指针的当前位置 |
| truncate([size]) | 不论指针在什么位置,只留下指针前 size 个字节的内容,其余全部删除; 如果没有传入 size ,则将指针当前位置到文件末尾内容全部删除 |
| flush() | 把缓冲区的内容写入文件,但不关闭文件 |
| close() | 把缓冲区内容写入文件,同时关闭文件,释放文件对象相关资源 |
8 文件任意位置操作
-
文件指针 指向当前字节位置
mode=r指针起始在 0mode=a指针起始在 EOF -
tell()显示指针当前位置 -
seek(offset[, whence])移动文件指针位置。
offset偏移多少字节,whence从哪开始
-
seek()方法操作文本文件-
whence0 缺省值,表示从头开始,offset只能正整数,可以右超界,不能左超界![image-20221023230327248]()
-
whence1 表示从当前位置,offset只接受 0whence2 表示从 EOF 开始,offset只接受 0![image-20221023230450162]()
-
文本模式支持从开头向后偏移的方式。
-
whence为 1 表示从当前位置开始偏移,但是只支持偏移 0,相当于原地不动,所以没什么用。 -
whence为 2 表示从 EOF 开始,只支持偏移 0,相当于移动文件指针到 EOF。 -
seek是按照字节偏移的。 -
read在文本模式是按照字符读取的。
-
-
seek()方法操作二进制文件whence0 缺省值,表示从头开始,offset只能正整数whence1 表示从当前位置,offset可正可负whence2 表示从 EOF 开始,offset可正可负- 二进制模式支持任意起点的偏移,从头、从尾、从中间位置开始
- 向后
seek可以超界,但是向前seek的时候,不能超界,否则抛异常。
案例:seek() 移动文件指针示例
#%%
with open("bb.txt", "r", encoding="utf-8") as f:
print("文件名是:{0}".format(f.name))
print(f.tell())
print("读取的内容:{0}".format(str(f.readline())))
print(f.tell())
f.seek(0, 0)
print("读取的内容:{0}".format(str(f.readline())))

9 StringIO 和 BytesIO
9.1 StringIO
- io 模块中的类
from io import StringIO
- 内存中,开辟的一个文本模式的 buffer ,可以像文件对象一样操作它
- 当 close 方法被调用的时候,这个 buffer 会被释放
getvalue() 获取全部内容。跟文件指针没有关系
from io import StringIO
#内存中构建
sio = StringIO() # 像文件对象一样操作
print(sio.readable(), sio.writable(), sio.seekable())
sio.write("java\nPython")
sio.seek(0)
print(sio.readline())
print(sio.getvalue()) # 无视指针,输出全部内容
sio.close()

好处
一般来说,磁盘的操作比内存的操作要慢得多,内存足够的情况下,一般的优化思路是少落地(即写入磁盘),减少磁盘 IO 的过程,可以大大提高程序的运行效率。
9.2 BytesIO
- io 模块中的类
from io import BytesIO
- 内存中,开辟的一个二进制模式的 buffer ,可以像文件对象一样操作它
- 当 close 方法被调用的时候,这个 buffer 会被释放
from io import BytesIO # 内存中构建
bio = BytesIO()
print(bio.readable(), bio.writable(), bio.seekable())
bio.write(b"java\nPython")
bio.seek(0)
print(bio.readline())
print(bio.getvalue()) # 无视指针,输出全部内容
bio.close()

9.3 file-like 对象
- 类文件对象,可以像文件对象一样操作
- socket 对象、输入输出对象(stdin、stdout)、标准错误(stderr)… 都是类文件对象。
这几个文件在程序运行中,默认一直是打开的
stdin标准输入 (设备) 文件,比如键盘。stdout标准输出 (设备) 文件,默认向屏幕输出。stderr标准错误 (设备) 文件,默认向屏幕输出。
都默认向屏幕输出,那 stdout 和 stderr 区别在哪?
stdout 是行缓冲的,他的输出会放在一个 buffer 里面,只有到换行的时候,才会输出到屏幕。而 stderr 是无缓冲的,会直接输出。
from sys import stdin, stdout, stderr
# stdin 对象
f_stdin = stdin
print(type(f_stdin))
print(f"是否可读:{f_stdin.readable()}\n是否可写:{f_stdin.writable()}\n指针是否是可移动:{f_stdin.seekable()}")
# 循环输入
# f_stdin.read()
# 文件描述符是 1print(f"文件描述符:{f_stdin.fileno()}")
print("=" * 40)
# stdout 对象
f_stdout = stdout
print(type(f_stdout))
print(f"是否可读:{f_stdout.readable()}\n是否可写:{f_stdout.writable()}\n指针是否是可移动:{f_stdout.seekable()}")
f_stdout.write('我是stdout')
# 文件描述符是 1print(f"文件描述符:{f_stdout.fileno()}")
print("=" * 40)
# stderr 对象
f_stderr = stderr
print(type(f_stderr))
print(f"是否可读:{f_stderr.readable()}\n是否可写:{f_stderr.writable()}\n指针是否是可移动:{f_stderr.seekable()}")
f_stderr.write('我是stderr\n')
# 文件描述符是 2print(f"文件描述符:{f_stderr.fileno()}")
print("=" * 40)

10 使用 pickle 序列化

Python 中,一切皆对象,对象本质上就是一个存储数据的内存块。有时候,我们需要将内存块的数据保存到硬盘上,或者通过网络传输到其他的计算机上。这时候,就需要对象的序列化和反序列化。对象的序列化机制广泛的应用在分布式、并行系统上。
- 序列化指的是:将对象转化成串行化数据形式,以便可以存储到硬盘或通过网络传输到其他地方。
- 反序列化是指相反的过程,将读取到的串行化数据转化成对象。
我们可以使用 pickle 模块中的函数,实现序列化和反序列化操作。
序列化我们使用:
# obj 就是要被序列化的对象,file 指的是存储的文件
pickle.dump(obj, file)
# 从 file 读取数据,反序列化成对象
pickle.load(file)
案例:将对象序列化到文件中
import pickle
with open(r"./data.dat", "wb") as f:
a1 = "高淇 "
a2 = 234
a3 = [20, 30, 40]
pickle.dump(a1, f)
pickle.dump(a2, f)
pickle.dump(a3, f)

案例:将获得的数据反序列化成对象
import pickle
with open(r"./data.dat", "rb") as f:
a1 = pickle.load(f)
a2 = pickle.load(f)
a3 = pickle.load(f)
print(a1)
print(a2)
print(a3)

11 CSV 文件的操作
csv (Comma Separated Values) 是逗号分隔符文本格式,常用于数据交换、Excel 文件和数据库数据的导入和导出。
与 Excel 文件不同,CSV 文件中:
- 值没有类型,所有值都是字符串
- 不能指定字体颜色等样式
- 不能指定单元格的宽高,不能合并单元格
- 没有多个工作表
- 不能嵌入图像图表
Python 标准库的模块 csv 提供了读取和写入 csv 格式文件的对象。
我们在 excel 中建立一个简单的表格:

另存为csv( 逗号分隔 ),我们打开查看这个 csv 文件内容:
姓名,年龄,工作,薪水
小龙女,20,程序员,50000
赵敏,18,测试工程师,20000
周芷若,19,人工智能开发,50000

11.1 csv.reader 对象和 csv 文件读取
案例:csv.reader 对象从 csv 文件读取数据
使用 utf-8 编码读取文件时,open(r"./test.csv", encoding='utf-8'):,会打印如下结果:

import csv
with open(r"./test.csv", encoding='utf-8-sig') as a:
# 创建 csv 对象,它是一个包含所有数据的列表,每一行为一个元素
a_csv = csv.reader(a)
# 获得列表对象,包含标题行的信息
headers = next(a_csv)
print(headers)
for row in a_csv: # 循环打印各行内容
print(row)

11.2 csv.writer 对象和 csv 文件写入
案例:csv.writer 对象写一个 csv 文件
import csv
headers = ["工号", "姓名", "年龄", "地址", "月薪"]
rows = [("1001", "赵敏", 18, "西三旗 1 号院", "50000"),
("1002", "周芷若", 19, "西三旗 1 号院", "30000")]
with open(r"./test.csv", "w") as b:
# 创建 csv 对象
b_csv = csv.writer(b)
# 写入一行(标题)
b_csv.writerow(headers)
# 写入多行(数据)
b_csv.writerows(rows)

12 os 和 os.path 模块

os 模块可以帮助我们直接对操作系统进行操作。我们可以直接调用操作系统的可执行文件、命令,直接操作文件、目录等等。是系统运维的核心基础。
[[os 模块详解]]
12.1 os 模块——调用操作系统命令
os.system 可以帮助我们直接调用系统的命令
案例:os.system 调用 windows 系统的记事本程序
import os
os.system("notepad.exe")
案例:os.system 调用 windows 系统中 ping 命令
import os
os.system("ping www.baidu.com")

os.startfile 直接调用可执行文件
os.startfile(path) 相当于双击 path:
示例:运行安装好的微信
#%%
import os
os.startfile(r"D:\software\WeChat\WeChat.exe")

示例:path 为一张图片相当于双击这张图片,会使用当前默认的看图工具打开该图片。例如打开 ly.jpeg
import os
# path 为一张图片相当于双击 ly.jpeg,
# 会使用当前默认的看图工具打开 ly.jpeg 图片
os.startfile("ly.jpeg")

os.startfile 和 os.system 的区别
os.system(command) 等价于在控制台中输入 command 回车,比如我们想执行 pip install pkg,那么直接通过 os.system("pip install pkg") 即可。并且在 Windows 系统中,我们在控制台输入了一个文件的路径,如果是二进制文件则会自动执行;如果不是二进制文件,那么会自动尝试用相关可执行程序打开该文件。所以从这个角度上说,os.system 完全可以实现 os.startfile 的功能,只不过 os.system 还可以执行命令。
示例:os.system 方法:
import os
# 等待 notepad 窗口退出后再继续执行
# (主进程打开新的窗口,原窗口进入休眠状态,待新的窗口关闭,主进程再重新唤醒原窗口)
os.system('"notepad.exe"')
print('已经退出了notepad')

关闭记事本窗口:

示例:os.startfile 方法:
import os
# 打开窗口后不等待窗口退出直接继续执行
# (主进程创建一个子进程去打新的窗口,主进程创建完成子进程后立即继续往下执行)
os.startfile('"notepad.exe"')
print('不等待notepad退出直接继续执行')

12.2 os 模块——文件和目录操作
我们可以通过前面讲的文件对象实现对于文件内容的读写操作。如果,还需要对文件和目录做其他操作,可以使用 os 和 os.path 模块。
os 模块下常用 操作文件 的方法:
| 方法名 | 描述 |
|---|---|
| remove(path) | 删除指定的文件 |
| rename(src, dest) | 重命名文件或目录 |
| stat(path) | 返回文件的所有属性 |
| listdir(path) | 返回 path 目录下的文件和目录列表 |
os 模块下关于 目录操作 的相关方法,汇总如下:
| 方法名 | 描述 |
|---|---|
| mkdir(path) | 创建目录 |
| makedirs(path1/path2/path3/…) | 创建多级目录 |
| rmdir(path) | 删除目录 |
| removedirs(path1/path2…) | 删除多级目录 |
| getcwd() | 返回当前工作目录:current work dir |
| chdir(path) | 把 path 设为当前工作目录 |
| walk(top, func, arg) | 递归方式遍历目录 |
| sep | 当前操作系统所使用的路径分隔符 |
示例:os 模块获取文件和文件夹相关的信息
import os
#############获取文件和文件夹相关的信息################
# 返回当前操作系统的信息
# windows->nt linux 和 unix->posix
print(f"当前操作系统的信息:{os.name}")
# 返回当前操作系统的分隔符
# windows->\ linux 和 unix->/
print(f"当前操作系统的分隔符:{os.sep}")
# 返回当前系统的换行符
# windows->\r\n linux-->\n\
print(f"当前系统的换行符:{repr(os.linesep)}")
"""
# 返回文件的信息
st_mode: inode 保护模式
st_ino: inode 节点号。
st_dev: inode 驻留的设备。
st_nlink: inode 的链接数。
st_uid: 所有者的用户ID。
st_gid: 所有者的组ID。
st_size: 普通文件以字节为单位的大小;包含等待某些特殊文件的数据。
st_atime: 上次访问的时间。
st_mtime: 最后一次修改的时间。
st_ctime: 由操作系统报告的"ctime"。
在某些系统上(如Unix)是最新的元数据更改的时间,
在其它系统上(如Windows)是创建时间(详细信息参见平台的文档)。
"""
print(f'文件的信息:{os.stat("ly.jpeg")}')

示例:os 模块关于工作目录的操作
import os
##############关于工作目录的操作###############
# 返回当前的工作目录
print(f"当前工作目录:{os.getcwd()}")
# 改变当前的工作目录为:d:盘根目录
print("改变工作目录...")
os.chdir("D:\\")
print(f"当前工作目录:{os.getcwd()}")
# 创建目录
print("创建目录<书籍>...")
os.mkdir("书籍")
print("再次创建目录<书籍>...")
# 创建同名目录会报文件已经存在错误
os.mkdir("书籍") # FileExistsError

示例:os 模块创建目录、创建多级目录、删除
单级目录的创建和删除:
# 创建目录<书籍>
os.mkdir("书籍")
# 相对路径都是相对于当前的工作目录
os.rmdir("书籍")

删除带文件的单级目录:
# 删除带文件的单级目录
os.rmdir("书籍")


多级目录的创建和删除
# 创建多级目录
os.makedirs("电影/港台/周星驰")
# 删除多级目录,只能删除空目录
os.removedirs("电影/港台/周星驰")
# 使用相对路径创建多级目录 ../指的是上一级目录
os.makedirs("../音乐/香港/刘德华")

目录重命名
os.makedirs("电影/港台/周星驰")
# 重命名
os.rename("电影","movie")

列出 path 目录下的文件和目录
# 列出 path 目录下的文件和目录
dirs = os.listdir("movie")
print(dirs)

12.3 os.path 模块
os.path 模块提供了目录相关(路径判断、路径切分、路径连接、文件夹遍历)的操作
| 方法 | 描述 |
|---|---|
| isabs(path) | 判断 path 是否绝对路径 |
| isdir(path) | 判断 path 是否为目录 |
| isfile(path) | 判断 path 是否为文件 |
| exists(path) | 判断指定路径的文件是否存在 |
| getsize(filename) | 返回文件的大小 |
| abspath(path) | 返回绝对路径 |
| dirname(p) | 返回目录的路径 |
| getatime(filename) | 返回文件的最后访问时间 |
| getmtime(filename) | 返回文件的最后修改时间 |
| join(path,*paths) | 连接多个 path |
| split(path) | 对路径进行分割,以列表形式返回 |
| splitext(path) | 从路径中分割文件的扩展名 |
示例:获得目录、文件基本信息
#################获得目录、文件基本信息###################
import os
import os.path
# 是否绝对路径
print(r'path D:\a.txt 是否为绝对路径:{}'.format(os.path.isabs(r"D:\a.txt")))
# 是否目录
print(r'path D:\a.txt 是否为目录:{}'.format(os.path.isdir(r"D:\a.txt")))
# path 不存在,返回 False
print(r'不存在的path D:\b 是否为目录:{}'.format(os.path.isdir(r"D:\b")))
# path 存在,返回 True
print(r'存在的path D:\software 是否为目录:{}'.format(os.path.isdir(r"D:\software")))
# 是否文件
print(r'不存在的path D:\a.txt 是否为文件:{}'.format(os.path.isfile(r"D:\a.txt")))
print(r'存在的path b.txt 是否为文件:{}'.format(os.path.isfile(r"b.txt")))
# 文件是否存在
print(r'path D:\a.txt 是否存在:{}'.format(os.path.exists(r"D:\a.txt")))
# 文件大小,单位为 B(字节)
try:
print(r'不存在path D:\a.txt 文件的大小:{}'.format(os.path.getsize(r"D:\a.txt")))
except FileNotFoundError as e:
print(r'查看不存在path D:\a.txt 文件的大小报错——FileNotFoundError')
print(e)
print(r'存在path b.txt 文件的大小:{}'.format(os.path.getsize(r"b.txt")))
# 输出绝对路径
print(r'获取不存在path a.txt 文件的绝对路径:{}'.format(os.path.abspath(r"a.txt")))
print(r'获取存在path b.txt 文件的绝对路径:{}'.format(os.path.abspath(r"b.txt")))
# 输出所在目录
print(r'获取不存在path a.txt 文件所在目录:{}'.format(os.path.dirname(r"a.txt")))
print(r'获取不存在path D:/a.txt 文件所在目录:{}'.format(os.path.dirname(r"D:/a.txt")))

示例:获得创建时间、访问时间、最后修改时间
########获得创建时间、访问时间、最后修改时间##########
import os
import os.path
# 返回创建时间
# 获取不存在文件的创建时间
try:
print(r'获取不存在path D:/a.txt 文件创建时间:{}'.format(os.path.getctime(r"D:/a.txt")))
except FileNotFoundError as e:
print(r'获取不存在path D:/a.txt 文件创建时间报错——FileNotFoundError')
print(e)
# 获取存在文件的创建时间
print(r'获取存在path b.txt 文件创建时间:{}'.format(os.path.getctime(r"b.txt")))
# 返回最后访问时间
# 获取不存在文件的最后访问时间
try:
print(r'获取不存在path D:/a.txt 文件最后访问时间:{}'.format(os.path.getctime(r"D:/a.txt")))
except FileNotFoundError as e:
print(r'获取不存在path D:/a.txt 文件最后访问时间报错——FileNotFoundError')
print(e)
# 获取存在文件的最后访问时间
print(r'获取存在path b.txt 最后访问时间:{}'.format(os.path.getatime(r"b.txt")))
# 返回最后修改时间
# 获取不存在文件的最后修改时间
try:
print(r'获取不存在path D:/a.txt 文件最后修改时间:{}'.format(os.path.getmtime(r"D:/a.txt")))
except FileNotFoundError as e:
print(r'获取不存在path D:/a.txt 文件最后修改时间报错——FileNotFoundError')
print(e)
# 获取存在文件的最后修改时间
print(r'获取存在path b.txt 最后修改时间:{}'.format(os.path.getmtime(r"b.txt")))

示例:对路径进行分割、连接操作
################对路径进行分割、连接操作####################
import os
import os.path
# 返回绝对路径-->D:\Project\myPython\learnPython\b.txt
path = os.path.abspath(r"b.txt")
print(r'path b.txt 的绝对路径'.format(path))
# 返回二元组:目录、文件-->('D:\\Project\\myPython\\learnPython', 'b.txt')
print(r'D:\Project\myPython\learnPython\b.txt 使用 os.path.split 的结果:{}' \
.format(os.path.split(path)))
# 返回二元组:路径、扩展名-->('D:\\Project\\myPython\\learnPython\\b', '.txt')
print(r'D:\Project\myPython\learnPython\b.txt 使用 os.path.splitext 的结果:{}' \
.format(os.path.splitext(path)))
# 返回连接路径:"aa", "bb", "cc"-->aa\bb\cc
print(r'["aa", "bb", "cc"] 使用 os.path.join 的结果:{}' \
.format(os.path.join("aa", "bb", "cc")))

示例:列出当前目录下所有的 .py 文件,并输出文件名
# 获取当前路径
path = os.getcwd()
# 列出子目录和子文件
file_list = os.listdir(path)
# 方法一:使用 for 循环 + if 判断
# 列出当前目录下所有的.py 文件,并输出文件名
for file in file_list:
if file.endswith(".py"):
print(file)
print("##################")
# 方法二:使用列表推导式
file_list = [file for file in file_list if file.endswith(".py")]
for file in file_list:
print(file)

12.4 walk() 递归遍历所有文件和目录
os.walk() 方法是一个简单易用的文件、目录遍历器,可以帮助我们高效的处理文件、目录方面的事情。格式如下:
os.walk(top[, topdown=True[, onerror=None[, followlinks=False]]])
其中, top:是要遍历的目录。topdown:可选,True,先遍历 top 目录再遍历子目录。
os.walk() 方法:返回一个 walk 生成器,生成器中每个元素是一个三元组 --> (root, dirs, files)
root:当前正在遍历的文件夹本身dirs:一个列表,该文件夹中所有的目录的名字files:一个列表,该文件夹中所有的文件的名字
案例:使用 walk() 递归遍历所有文件和目录
import os
import os.path
# 创建两个文件夹做示例
if not os.path.exists(r'./电影/港台/李丽珍'):
os.makedirs('./电影/港台/李丽珍')
if not os.path.exists(r'./电影/港台/周慧敏'):
os.makedirs('./电影/港台/周慧敏')
# 存储所有文件的路径
all_files = []
# 获取当前的路径
path = os.getcwd()
# walk 递归遍历
walk_gen = os.walk(path)
print(f"walk_gen 的类型:{type(walk_gen)}")
for root, dirs, files in walk_gen:
for dirname in dirs:
dir_path = os.path.join(root, dirname)
all_files.append(dir_path)
for filename in files:
file_path = os.path.join(root, filename)
all_files.append(file_path)
# 打印子目录和子文件
for file in all_files:
print(file)

13 shutil 模块 (拷贝和压缩)
shutil 模块是 python 标准库中提供的,主要用来做文件和文件夹的拷贝、移动、删除等;还可以做文件和文件夹的压缩、解压缩操作。
os 模块提供了对目录或文件的一般操作。shutil 模块作为补充,提供了移动、复制、压缩、解压等操作,这些 os 模块都没有提供。
案例:实现文件的拷贝
import shutil
# copy 文件内容
shutil.copyfile("1.jpeg", "1_copy.jpeg")

案例:实现递归的拷贝文件夹内容(使用 shutil 模块)
import shutil
# "音乐"文件夹不存在才能用。
shutil.copytree("电影/学习", "音乐", ignore=shutil.ignore_patterns("*.html","*.htm"))
将文件夹“电影/学习”下面的内容拷贝到文件夹“音乐”下。拷贝时忽略所有的 html 和 htm 文件 。运行结果如下:

案例:实现将文件夹所有内容压缩(使用 shutil 模块)
import shutil
import zipfile
# 方法一
# 将"电影/学习"文件夹下所有内容压缩到"音乐2"文件夹下生成 movie.zip
shutil.make_archive("音乐2/movie", "zip", "电影/学习")
# 方法二
# 压缩:将指定的多个文件压缩到一个 zip 文件
z = zipfile.ZipFile("a.zip", "w")
z.write("1.txt")
z.write("2.txt")
z.close()
案例:实现将压缩包解压缩到指定文件夹(使用 shutil 模块)
import shutil
import zipfile
# 解压缩:
z2 = zipfile.ZipFile("a.zip", "r")
# 设置解压的地址
z2.extractall("d:/")
z2.close()
14 递归算法
递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。
利用递归可以用简单的程序来解决一些复杂的问题。比如:斐波那契数列的计算、汉诺塔、快排等问题。
递归结构包括两个部分:
- 定义递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。
- 递归体。解答:什么时候需要调用自身方法。
案例:使用递归求 n!
def factorial(num):
if num == 1:
return 1
return num * factorial(num-1)
res = factorial(10)
print(res)

执行过程如图所示:

递归的缺陷
简单的程序是递归的优点之一。但是递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
案例:使用递归算法遍历目录下所有文件
import os
import os.path
all_files = []
# 创建测试目录
if not os.path.exists("电影/港台/张敏"):
os.makedirs("电影/港台/张敏")
def get_files(path, level=0):
for file in os.listdir(path):
# 获取文件的绝对路径
file_abspath = os.path.join(path, file)
# 判断是文件还是目录
if os.path.isdir(file_abspath):
all_files.append(('\t' * level) + file_abspath)
get_files(file_abspath, level + 1)
# 输出文件路径
all_files.append(('\t' * level) + file_abspath)
path = os.getcwd()
get_files(path)
for i in all_files:
print(i)





浙公网安备 33010602011771号