▒ 目录 ▒
- 🛫 导读
- 本篇知识路线
- 1️⃣ 文件IO
- 读文件
- 写文件
- 文件对象
- 2️⃣ 操作文件和目录
- windows 和 linux 命令行操作文件和目录
- Python 操作文件的函数
- Python 操作目录的函数
- Python 操作文件名和目录名的函数
- 使用高级模块 shutil 操作文件和路径
- 3️⃣ StringIO和BytesIO
- StringIO
- BytesIO
- 内存 IO(StringIO 和 BytesIO)和文件 IO 的主要区别
- 4️⃣ 路径和编码要在边界处明确
- 🛬 文章小结
🛫 导读
文件操作的关键不只是读写成功,还包括编码正确、资源及时关闭和路径能够跨平台工作。
本篇覆盖文件打开、读取、写入、文件指针、目录操作以及StringIO、BytesIO。原文保留了大量直接示例,学习时要同时关注文本与二进制的区别,并养成使用上下文管理器自动释放资源的习惯。
本篇知识路线
外部文件 ──► open() ──► 文件对象 ──► read()/write() │ └─ with 代码块结束后自动关闭 内存文本 ──► StringIO 内存字节 ──► BytesIO1️⃣ 文件IO
大家回顾一下我们前面所学,我们程序中需要存储数据都是存储在内存中,然而当进程(运行中的程序)结束的时候, 我们的内存就会被操作系统所回收,我们的数据也就没了。
如果我们能把数据存储在磁盘中,别说是进程结束,哪怕是关机,我们的数据都不会丢失。
操作系统不允许我们自己写的普通的程序直接操作磁盘,在磁盘上读写文件的功能都是由操作系统提供的, 所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符), 然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
这种在程序中通过文件对象对磁盘进行读写的操作我们叫做磁盘 IO,当然磁盘 IO 属于文件 IO。在本节课的讲解中我们统一称磁盘 IO 为文件 IO(大家都习惯于这样称呼),我们知道 IO 是指输入和输出,也就是读写,文件 IO 我们也称之为文件读写,大家要知道这种叫法。
读文件
读文件是最常见的 IO 操作,Python 内置了读文件的函数,比如要以读文件的模式打开一个文件对象,使用 Python 内置的 open 函数,传入文件名和标识符,其中标识符 “r” 表示读文本文件,“rb” 表示读二进制文件(图片,音乐,视频等)。但是如果读取的文件不存在,open 函数就会抛出一个 IOError 的错误,并且给出错误码和详细的信息告诉你文件不存在。最后要注意,文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的,调用 close 方法关闭文件。
f=open("d:/test.txt","r")# 要确保 d 盘下存在 test.txt 文件,否则会报异常data=f.read()print(data)f.close()# 用完后关闭文件对象在程序中我们成功打开一个文件进行操作,并且最后我们没有调用 close 函数关闭文件对象,进程结束,操作系统会自动回收该进程所申请的所有资源(文件对象也是资源)。 但是最好不要这样去做,我们要养成一个好的习惯,因为我们的程序很有可能循环调用多次这个 open 函数,或者我们是一个提供服务功能的程序,是需要长期一直执行的。
f=open("d:/test.txt","r")# 要确保 d 盘下存在 test.txt 文件data=f.read()print(data)# 进程结束,操作系统会回收改进程申请的所有资源(文件对象)我们成功打开一个文件进行操作时,由于后面的语句错误可能导致程序异常,这样的话,程序就不会执行到我们调用文件对象的 close 函数。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用 try … except 来实现。
try:f=open("d:/test.txt","r")data=f.read()print(data)print(data.fuck)# 报异常,退出 try 语句,进入 except 语句f.close()# 解释器不会执行这条语句except:f.close()raise("文件操作出现错误")每次操作文件都要写这种异常处理语句感觉很繁琐,这完全不符合 Python 高大上的形象, 我们可以使用 Python 引入的 with 语句来自动帮我们自动调用 close 函数。
withopen("d:/test.txt","r")asf:data=f.read()print(data)print(data.fuck)# 虽然报异常,但在 with 块内,解释器会自动调用 close 函数,关闭文件对象当然在 with 语句内不报异常,解释器在离开 with 语句块的时候,也会调用 close 函数关闭文件对象。
withopen("d:/test.txt","r")asf:data=f.read()print(data)print(f.read())# 报异常,因为解释器在离开 with 语句块的时候,会调用 close 函数关闭文件对象。我们的 open 函数返回的是向操作系统申请的文件资源, 该文件资源我们也叫作文件对象或者文件描述符,同学们可能会想程序执行过程中出现异常,进程就会退出, 进程退出操作系统就会回收进程申请的所有资源,我们通过 open 函数申请的文件对象就会被操作系统所回收, 自然就不用我们调用文件对象的 close 函数了,其实如果我们程序只有一个进程倒是无所谓,但如果我们的程序有多个进程, 且这个出现异常的语句是在子进程中,子进程出现异常,我们的主进程并不会退出, 而且我们主进程经常会调用到这个向操作系统申请文件对象的崩溃的子进程,那就会耗干操作系统维护的文件资源。 在后面章节中我们会学习到多进程编程。
调用 read 函数会一次性读取文件的全部内容,如果我们读取的文件很大,就有可能出现内存不够用而失败, 所以,要保险起见,可以反复调用 read(size) 方法,每次最多读取 size 个字节的内容。 注意:不要用记事本写入文件内容,最好使用 notepad++,保存为 utf8 格式。
withopen("d:/test.txt","r")asf:whileTrue:data=f.read(5)ifdata=="":# 没有数据了退出循环breakprint(data)调用 readline 函数可以每次读取一行内容。
withopen("d:/test.txt","r")asf:whileTrue:dataline=f.readline()ifdataline=="":# 没有数据了退出循环breakprint(dataline)调用 readlines 函数一次读取所有内容并按行返回 list。
withopen("d:/test.txt","r")asf:datalines=f.readlines()print(datalines)我们知道用 “rb” 的方式表示读二进制文件(图片,音乐,视频等),但要注意,在 Python3 中读取的内容是 bytes 类型。
f=open("d:/test.png","rb")# 要确保 d 盘下存在 test.png 文件,否则会报异常data=f.read()print(type(data))# <class 'bytes'>f.close()# 用完后关闭文件对象最后要注意:我们打开文件,一定要掉用 close 函数关闭文件对象,否则进程会占用该文件对象的句柄,导致对文件本身的操作无效,比如删除该文件(大家可以做个测试)。
写文件
写文件和读文件打开使用的函数是一样的,唯一区别是调用 open 函数时,传入标识符 “w” 或者 “wb”,其中标识符 “w” 表示写文本文件,“wb” 表示写二进制文件。
f = open("d:/test.txt", "w") f.write("birdpython") f.close() # 调用 close 函数关闭文件对象我们可以反复调用 write 函数来写入文件,文件对象有一个指针会指向每一次写入内容的最后一个位置, 这样下一次要写入的内容会紧跟着上一次写入内容的末尾,当然我们也可以修改这个指针的指向。
f = open("d:/test.txt", "w") f.write("birdpython") f.write("byebye") # 紧跟着 "n" 后面的位置写入 "byebye" f.seek(1) f.write("haha") # 会从 "i" 位置开始往后写如 "haha" f.close()对文件写操作执行完成后要调用 close 函数来关闭文件,当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用 close 函数时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用 close 的后果是数据可能只写了一部分到磁盘,或者没有写入。
f = open("d:/test.txt", "w") f.write("birdpython") input("waiting...") # 进程阻塞在此处,没有调用 close 函数且进程没有退出,操作系统并不会把 "birdpython" 写入磁盘 f.close()但如果我们要间断性的写入很多内容,我们想每次执行 write 函数后,就把内容写入到磁盘中,如果每写一句 write函数,就调用一次 close 函数释放文件对象,我们下次写的话,还要重新申请文件对象,这样频繁的向操作系统申请和释放文件对象是很耽搁时间的,是一种很 low 的解决方案。 我们可以使用 flush 函数告诉操作系统不需要等待 close 函数,可以马上写入内容到磁盘。
f = open("d:/test.txt", "w") f.write(u"birdpython") f.flush() f.write(u"byebye") f.flush() f.write(u"hahaha") f.close()我们每次使用标识符 “w” 写文件的方式返回一个文件对象,如果打开的文件存在,操作系统会删除该文件,然后重新写入, 这样会丢失已有的文件。
f = open("d:/test.txt", "w") f.write(u"birdpython") f.close() f = open("d:/test.txt", "w") # 操作系统会删除掉 test.txt 文件,然后重新创建 f.write(u"byebye") f.close() # 最后文件里的内容只有 "byebye"我们可以使用标识符 “a”,这样可以追加方式写文件,会在原有的文件内容后面写入我们的内容。
f = open("d:/test.txt", "w") # 操作系统会删除掉 test.txt 文件,然后重新创建 f.write(u"birdpython") f.close() f = open("d:/test.txt", "a") # 操作系统会在 test.txt 文件内容最后一个位置追加写 f.write(u"byebye") f.close()读写文件还有标识符 “w+”,“r+”,“a+” 等等,大家用到时候可以自行 google 搜索。
注意:当我们以写方式通过文件对象打开一个文件,在未调用 close 函数之前,最好不要做对该文件内容的修改操作(比如写文件)或文件本身的修改操作(比如删除文件)。
文件对象
像 open 函数返回的这种有个 read 函数的对象,在 Python 中统称为 file-like Object。除了 file ,还可以是内存的字节流,网络流,自定义流等等。file-like Object 不要求从特定类继承,只要写个 read 函数就行。
我们后面会学习各种文件对象,特别是在网络编程中,我们要学习的套接字也是文件对象,套接字的 read 函数是从网卡中读入字节流,write 函数是告诉操作系统把字节流写入到网卡,这样就可以和远程机器进行通信了。
文件对象不一定是必须要和磁盘,网卡这种外部设备打交道,和内存打交道的具有 read 和 write 函数的对象,我们也叫文件对象,下节我们会学习。
2️⃣ 操作文件和目录
我们平时在电脑上操作(读,写,删除,复制等等)文件和目录,主流的操作系统(带界面的 linux 系统,mac 系统,windows 系统等等)不但都有提供友好的可视化操作,也提供命令行操作。
无论是可视化操作还是通过命令行操作,最终都是转化为程序调用操作系统的 API 函数对文件和目录进行操作。
本节课我们来学习 Python 内置的操作文件和目录的函数。
windows 和 linux 命令行操作文件和目录
如果我们要操作文件或目录,可以在命令行下面输入操作系统提供的各种命令来完成,比如 linux下的 pwd 显示当前路径, ll 和 ls 显示当前目录下的文件和文件夹,rm 删除文件,mv 移动文件等等。
Windows 下的 dir 命令显示当前目录下的文件和文件夹,copy 复制文件,move 移动文件,del 删除当前路径下的文件等等。
对于操作系统提供的操作文件和目录的命令,大家用到时可以自行搜索,我们下面重点学习如何用 Python 提供的函数来操作文件和目录。
Python 操作文件的函数
我们可以使用 Python 内置的 os 模块提供的函数来检查一个文件是否存在。
importos flag=os.path.exists("d:/test.txt")print(flag)# 如果文件存在返回 True,否则返回 False我们上节课学习的写文件操作中,可以用标识符 ‘w’ 打开一个不存在文件,我们可以使用这种方法来创建一个文件, 但要注意,我们最好在创建文件时检查一下是否存在有重名的文件,如果存在就不要创建,否则的话会把原来的文件给冲掉。
importosifnotos.path.exists("d:/test.txt"):# 如果文件不存在f=open("d:/test.txt","w")# 在 d 盘下创建一个 test.txt 文件f.close()else:print("文件已存在,请换个文件名")我们可以使用 Python 内置的 os 模块提供的函数来删除一个文件,如果删除的文件不存在 Python 解释器会报异常。
importosifos.path.exists("d:/test.txt"):# 如果文件存在os.remove("d:/test.txt")# 删除 d 盘下的 test.txt 文件else:print("要删除的文件不存在")我们可以使用 Python 内置的 os 模块提供的函数来给一个存在的文件重命名,要确保被命名的文件存在以及新命名的文件名不存在,否则Python 解释器会报异常。
importosifos.path.exists("d:/test.txt")andnotos.path.exists("d:/newtest.txt"):os.rename("d:/test.txt","d:/newtest.txt")else:print("不存在该文件或重命名的文件名已存在")我们还需要对文件进行拷贝,移动等等操作,后面我们会介绍更高级的模块来完成这些操作。
Python 操作目录的函数
我们可以使用 Python 内置的 os 模块提供的函数,查看当前文件所在目录的全路径。
importosprint(os.path.abspath("."))我们可以使用 Python 内置的 os 模块提供的函数,在已经存在的目录下创建一个新目录。
importos os.mkdir("d:/test/tt")# 要确保 d:/test/ 目录存在,否则会报异常我们可以使用 Python 内置的 os 模块提供的函数,删除一个存在的空目录(该目录下没有文件和目录)。
importos os.rmdir("d:/test/tt")# 要确保 d:/test/tt 目录下没有文件和目录,否则会报异常如何删除一个存在的非空目录以及更复杂的目录操作,后面我们会介绍更高级的模块来完成这些操作。
Python 操作文件名和目录名的函数
把目录和文件进行路径拼接时,如果直接拼字符串,我们还要判断操作系统,因为在 Linux/Unix/Mac 下,os.path.join 函数返回的字符串是用 “/” 衔接的,在 windows 下,os.path.join 函数返回的字符串是用 “” 衔接的。我们可以使用 Python 内置的 os 模块的函数进行智能拼接路径。
importos filepath="d:/img"# 我们在项目开发中一般不会使用绝对路径,在此只是为了举例说明filename="ruhua.png"''' 注意: 1.filepath 中的字符串中的目录不要求在系统中存在,我们拼接的 filepath 和 filename 仅仅只是字符串拼接而已 2.我们 定义的 filepath 仅仅是在 windows 下做测试,实际项目开发中,我们不会用这种绝对路径的方式,因为我们 的项目很有可能部署在不同的操作系统上,那样的话我们还要修改连接符 '\\' 或者 '/' 来对应我们的操作系统,这 显然不符合软件工程的理念。 3.所以下面这条语句在 windows 下会拼接成 "d:/img\ruhua.png",在 linux 下会拼接成 "d:/img/ruhua.png", 我们想要的拼接是字符串 "d:/test" 和 字符串 "ruhua.png" 的拼接,所以实现了智能拼接。 '''print(os.path.join(filepath,filename))我们要拆分一个已存在文件或目录的全路径时,也不要直接去拆字符串,我们可以使用 Python 内置的 os 模块的函数,这样可以把一个路径拆分为两部分放在一个 tuple 里面,tuple 中的第二个值总是最后级别的目录或文件名。
importos allfilepath="d:/img/head/ruhua.png"# 不要求系统中存在该路径,拆分的仅仅是字符串print(os.path.split(allfilepath))# 返回值为 ('d:/img/head', 'ruhua.png')我们可以使用 Python 内置的 os 模块的函数得到文件扩展名,同样我们操作的对象是字符串,并不要求这个路径存在。
importos allfilepath="d:/img/head/ruhua.png"# 不要求系统中存在该路径,拆分的仅仅是字符串print(os.path.splitext(allfilepath))# 返回值为 ('d:/img/head/ruhua', '.png')使用高级模块 shutil 操作文件和路径
Python 内置的 os 模块只提供基本的文件操作,对于一些复杂的操作,比如对文件的拷贝操作, 删除非空文件夹等等,我们可以我们可以使用高级模块比如 shutil 模块。
importshutil shutil.copy("d:/test.txt","d:/testcp.txt")# 拷贝文件shutil.rmtree("d:/testdir")# testdir 是非空文件夹当然即使我们不使用 shutil 模块,使用 Python 提供的 os 模块也可以实现拷贝文件,删除非空文件夹等等所有的对文件的操作,其实 shutil 模块里面也是调用 os 模块来完成这些复杂的文件操作。
3️⃣ StringIO和BytesIO
我们第一节学习了文件 IO(我们所说的文件 IO 特指磁盘 IO),我们所指的文件是存储在磁盘上的文件。
大家还记的文件对象的定义吗,我们知道像 open 函数返回的这种有个 read 函数的对象,在 Python 中统称为文件对象。文件对象不一定是必须要和磁盘,网卡这种外部设备打交道,和内存打交道的具有 read 和 write 函数的对象,我们也叫文件对象。
对磁盘的读写操作速度没有直接对内存操作快,很多时候,数据读写不一定是文件,也可以在内存中读写,本节我们就来学习对内存操作的文件对象 StringIo 和 BytesIO。
StringIO
要把字符串写入 StringIO,我们需要先创建一个 StringIO 对象,然后调用 StringIO 对象的 write 函数写入字符串(字符串必须为 unicode 类型),然后我们可以通过 StringIO 对象的 getvalue 函数读取字符串,注意使用完后不要忘记调用文件对象的 close 函数。
fromioimportStringIO f=StringIO()f.write("hello")print(f.getvalue())f.close()# 不要忘记调用文件对象的 close 函数我们也可以在创建 StringIO 对象的时候写入字符串。
fromioimportStringIO f=StringIO(u"hello")# 调用 StringIO 的构造函数写入字符串print(f.getvalue())f.close()我们也可以使用文件对象的 read 函数读取字符串,但要注意调用 write 函数 和 read 函数会导致把指针指向字符串的末尾。
fromioimportStringIO f=StringIO()f.write("hello")print(f.read())# 从字符串末尾开始读,所以没有内容f.close()# 不要忘记调用文件对象的 close 函数我们可以使用 seek 函数把指针定位到开始,然后调用 read 函数读取,或者直接使用 getvalue 函数读取(总是从字符串开始位置读取)。
fromioimportStringIO f=StringIO()f.write("hello")f.seek(0)# 指针重新定位到字符串开始print(f.read())# 读完后指针又定位到字符串末尾f.seek(0)# 指针重新定位到字符串开始print(f.read())# 读完后指针又定位到字符串末尾print(f.getvalue())# getvalue 函数总是从字符串开始位置开始读f.close()# 不要忘记调用文件对象的 close 函数用 StringIO 模块进行读写操作和我们自己定义对象读写字符串实质上是一样的,只是 StringIO 模块里面有类似文件对象提供的函数,更方便于我们对字符串进行操作,同理我们可以自己定义一个类来实现一个简易型的 myStringIO。
classmyStringIO():def__init__(self,data):self.data=datadefread(self):returnself.datadefwrite(self,data):self.data=datadefgetvalue(self):returnself.datadefclose(self):delself.data f=myStringIO(u"hello")print(f.getvalue())f.close()BytesIO
StringIO 操作的只能是字符串,如果要操作二进制数据(视频,图片,音频等等非字符流数据),就需要使用 BytesIO,下面我们使用 BytesIO 进行读写图片。注意,BytesIO 接收的参数和返回的结果都是字节类型。
fromioimportBytesIO fother=open("d:/test.png","rb")# 确保 d 盘下有 test.png 文件data=fother.read()fother.close()f=BytesIO(data)# data 是字节类型print(f.getvalue())# 结果为字节类型f.close()为了弄明白 BytesIO 使用的场景,我们从一个例子说起。我们写个程序:从网络上下载二进制数据(视频,图片,音频等等非字符流数据),然后存储在磁盘中。
fromioimportBytesIOfromurllibimportrequest# 网络库rst=request.urlopen("http://birdpython.com/static/img/logo.png")# 下载图片f=BytesIO(rst.read())# 把图片二进制文件放入 BytesIO 对象中fdisk=open("d:/xx.png","wb")fdisk.write(f.getvalue())fdisk.close()f.close()同学们可能会想,我们也可以直接把网上下载的二进制数据存到磁盘中啊,为什么还要用 BytesIO 对象存储起来,这不是多此一举吗!如下代码。
fromurllibimportrequest# 网络库importssl context=ssl._create_unverified_context()rst=request.urlopen("http://birdpython.com/static/img/logo.png",context=context)# 下载图片data=rst.read()fdisk=open("d:/xx.png","wb")fdisk.write(data)fdisk.close()当然,如果只是这种简单的需求,我们完全不需要使用 BytesIO,那么,现在我们增加一项需求,要求把下载的图片大小处理成 64 像素高和 64 像素宽,然后再存储起来。这个时候,我们可以使用专业的图片处理模块 PIL 提供的函数来修改图片大小,然而 PIL 模块需要传入一个文件对象或者类文件对象(不能直接传入字节类型变量)来操作,这时候我们可以传入一个存储该二进制图片数据的 BytesIO 对象给 PIL 对象。
fromioimportBytesIOfromurllibimportrequest# 网络库fromPILimportImage# 第三方图形处理模块(安装命令:pip install pillow)importssl context=ssl._create_unverified_context()rst=request.urlopen("http://birdpython.com/static/img/logo.png",context=context)# 下载图片data=rst.read()f=BytesIO(data)# 把图片二进制文件放入 BytesIO 对象中img=Image.open(f)# 需要传入一个文件对象newimg=img.resize((64,64))# 修改图片像素大小为 64 x 64newimg.save("d:/xx.png")# 存储处理后的图片f.close()如果你是个杠精,你说你可以不用这些专业的图形处理模块来完成需求,比如自己去写业务逻辑来改变图片大小,那如果有更复杂的图片处理需求呢?我可以告诉你,这几乎是不可能的事情!
内存 IO(StringIO 和 BytesIO)和文件 IO 的主要区别
内存 IO 和文件 IO 的本质区别就在于文件 IO 是程序通过操作系统操作磁盘文件,而内存 IO 是我们直接用程序操作内存。
如果我们想要存储的内容持久化就只能使用文件 IO,因为内存 IO 是对内存进行操作,在我们进程结束后,内存就被操作系统回收了。
文件 IO 有读写标识符,内存 IO 没有读写标识符,大家想想这是不是理所当然的。
4️⃣ 路径和编码要在边界处明确
文本文件读写时应明确编码,路径拼接则应使用路径工具表达目录关系,避免手工拼接不同平台的分隔符。写入重要文件时,可以先写临时文件,确认成功后再替换目标文件,减少程序中断造成半截文件的风险。
🛬 文章小结
- 区分文本与二进制读写、打开模式和文件指针;用
with确保文件及时关闭。 - 用
pathlib、os或shutil处理路径、目录和文件;StringIO存放文本,BytesIO存放二进制数据。 - 可列出非空目录中的
.py文件,并尝试将图片读入BytesIO、调整尺寸后写回磁盘,观察内存流与磁盘文件的关系。