13. Python 的文件操作

茶桁的 AI 秘籍

Hi,大家好。我是茶桁。

在之前的几节课程中,我们学习了 Python 的数据类型。和市面上大多数的 Python 教程不同的是,我先为大家介绍完函数之后才开始介绍数据类型,其中原因就是很多数据类型的方法及理解都需要先搞懂函数的基本语法。

在结束了 Python 数据类型学习之后,我们今天开始进入一个新的篇章。今天,让我们来详细了解一下在 Python 中如何去进行文件操作。

我们大家都使用过智能手机,电脑,iPad 等电子产品。那我们肯定有打开文件的经验,比如说打开一个 Word、Excel 文档。最基础的操作实际上就两步,分别是 1. 打开文件, 2. 关闭文件。

我们要理解的一点是,文件都是放在存储设备中的,这才是我们能打开它的基础。那我们在存储设备中对文件进行打开之后进行的读写操作,实际上就是文件I/O

什么是I/O?I代表Input(输入),O代表Output(输出)。当你打开一个文件的时候,就算你没有对文件进行更改,也依然已经有了I/O操作,毕竟文件只有读取之后,才能显示到你的屏幕上。

那么文件读写到底分了几步呢?让我们引用一下宋丹丹的经典小品中的一段:

问,把大象装进冰箱分几步?

我们就不在这里进行分步讨论了,因为流程步骤实际上是一模一样的:

  1. 打开文件open() : 打开冰箱。
  2. 读取文件read()/ 写入内容write(): 把大象装进冰箱。
  3. 关闭文件close(): 关闭冰箱。

可以说,你在你的设备上做的任何操作都逃不开这几步,区别无非就是你有没有写入内容,从哪里打开的,读取的文件是什么类型的。

那么复杂一点的,就是当你打开一个 App 的时候,这个 App 执行某项操作的时候去互联网上的服务器找相应的文件然后到本地之后打开,读取。我们不讨论在打开文件之前的一系列例如下载(这个下载动作有时是主动的,有时是被动的)操作,就只说到本地之后读取文件并展示,就一定包含这三步。

理解到这,可以了。我们接着正式来学习 Python 如何对文件进行操作。

文件操作

open()

open函数就是用于最初的打开文件的动作,其基本格式为:open(文件路径, 打开方式, [字符集]), 完整的格式为:open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)

在大部分时候,我们使用基本格式就足够了。

1
2
3
4
5
6
'''
打开文件 open()
参数 1: 文件路径
参数 2: 打开的方式
参数 3: 字符集
'''

路径,也就是url是一种统一资源定位符。其中包括相对路径绝对路径

相对路径,比如说我们被路人问路,我们就说:这条街往前,前面十字路口就是交道口,然后左转,再走 100 米左右就到了。

绝对路径, 这个就非常好理解了,北京市西城区鼓楼东大街 28 号,特别准确了对吧?

这两个路径的描述呢,其实指向的是一个地方。只是一个是针对人所在的位置来告知你怎么走,另外一个是从最上层给到你一个绝对的地址。而电脑里的相对路径和绝对路径也基本就是这么个意思。

我们来看相对路径,主要是使用./../来进行描述,这两个都有一个共同点,就是以当前文件为准。也就是当前文件向我们问路,我们站在当前文件的地方告诉它该怎么走去到达自己的目的地。

举例, 假设我们现在正在编辑index.py这个文件,也就是说,向我们问路的文件是index.py

1
2
3
4
5
6
7
8
9
- project
| - index.py
| - test.txt
| img
| - person.jpg
| - dog.jpg
| - cat.jpg
- data
| - person.csv

这样一个路径关系中:

  1. 当我们需要去访问person.jpg并打开的话,那就是index.py同目录下的img目录里面去寻找person.jpg, 那我们相对路径的写法为./img/person.jpg
  2. 当我们要去找person.csv的时候,由于这个 csv 是存在于上一层目录的同级目录data内,那我们需要向上去寻找,就是../data/person.csv

这就是./../的区别,一个是当前目录同级内去寻找,一个是向上一级的目录内去寻找。那如果文件存储于上两层目录中呢?那就向上翻两层呗:../../这样,多层的时候,依次类推。

相对路径介绍完了,我们来看看绝对路径

绝对路径的前提是必须找到根目录。在windows中我们其实都熟悉一个东西就是盘符。比如说C:\,不严谨的说,盘符就算是绝对路径的根目录了。那为什么说不严谨的说呢?因为我们输入文件路径的时候可以输入:C:\data\person.csv这样去寻找。但是,盘符之上其实是整个硬盘,我们只是将硬盘虚拟成了不同的盘符用于划分空间而已。

Mac或者Linux中,就是以整个硬盘为准去寻找文件的。比如说/Users/xx/Downloads,就是我们的下载目录。

那我们如果想要打开文件,这两种方式其实都可以,一般来说,为了代码能够适应环境变化,我们都会选择使用相对路径。

说完文件路径,让我们来说说打开方式,我先介绍一个模式,后面咱们再慢慢讲:

w模式: write, 写入

如果文件不存在,创建这个文件; 如果文件存在,则打开这个文件,并且清空文件内容。文件打开后,文件的指针在文件的最前面。什么是指针呢? 可以这么理解,当我们打开一个 word 文档的时候,我们的光标是不是都在这个文档的最上面?这个光标的位置,就是指针的位置。

write()

write()是用于对文件写入内容来使用的,格式为:文件对象.write(内容)

close()

格式为: 文件对象.close() , 可以关闭打开的文件。

我们需要注意一点,我们在对文件进行操作的时候,一定记得操作完要关闭它。否则,这个文件就会一直存在于内存地址中。

下面,让我们看看在 Python 中如何打开操作一个文件的。

以下所有的操作演示都会在../Python/13.ipynb 中进行编写,所以我们的操作路径都会以这个文件为准。

让我们现在当前文件的中创建一个文件夹data,然后在其中放入一个文件13-1.txt,我们说要做的事情,就是打开这个文件,然后将我们之前写的内容复制一部分写入到这个txt文件中去,路径关系如下图:

1
2
3
4
5
6
7
8
# 打开 13-1 并且写入内容
fp = open('./data/13-1.txt', 'w')
print(fp, type(fp))
fp.write('相对路径: 比如说我们被路人问路,我们就说:这条街往前,前面十字路口就是交道口,然后左转,再走 100 米左右就到了。\n 绝对路径: 这个就非常好理解了,北京市西城区鼓楼东大街 28 号,特别准确了对吧?')
fp.close()

---
<_io.TextIOWrapper name='./data/13-1.txt' mode='w' encoding='UTF-8'> <class '_io.TextIOWrapper'>

打印区打印的内容,实际上是我们print函数执行的结果,可以看到,我们打印fp这个变量的时候,显示的是<_io.TextIOWrapper name='./data/13-1.txt' mode='w' encoding='UTF-8'>, 其类型是<class '_io.TextIOWrapper'>

这些先放在一边,让我们看看文件到底写入没有:

写入是写入了,可是这是什么鬼?

啊,差点忘了,整个open()方法内后面还有一个参数encoding=, 这个参数是告诉我们这个文件以什么字符集去打开。默认的就是UTF-8,显然,我们保存的这个文件并不是,所以最终导致了乱码。

让我们修改一下代码,在open()内添加一下encoding,其他不变:

1
2
3
4
5
fp = open('./data/13-1.txt', 'w', encoding='GBK')
...

---
<_io.TextIOWrapper name='./data/13-1.txt' mode='w' encoding='GBK'> <class '_io.TextIOWrapper'>

可以看到,打印出来的fp最后的encoding值已经发生了变化。让我们再去看看文件如何了:

果然没问题,内容能够正确显示而不会乱码了,我们注意到下方文件字符集确实为GBK

关于字符集编码的问题这里有疑问的,自己回过头再去把我之前讲的课程好好翻腾一下,复习一下。

整段代码中,我们引用了刚才介绍的三个文件操作的函数: open(), write(), close()

在简单了解了文件的操作步骤之后,我们接下来再继续看文件操作中另外一个比较重要的函数: read()

read()

在对文件进行操作的时候,一定要记得流程一定是打开open在最前面,close关闭在最后面。至于中间你是要读取,写入还是别的什么操作,那都不违反文件操作的整个流程。

所以在下面一段代码里,我们可以尝试把之前的write()替换为read(),顺便可以学一下如何在代码中看看我们刚修改过的文件:

1
2
3
4
5
6
7
8
9
fp = open('./data/13-1.txt', 'r', encoding='GBK')
res = fp.read()
fp.close()

print(res)

---
相对路径: 比如说我们被路人问路,我们就说:这条街往前,前面十字路口就是交道口,然后左转,再走 100 米左右就到了。
绝对路径: 这个就非常好理解了,北京市西城区鼓楼东大街 28 号,特别准确了对吧?

可以看到,我们讲刚才写入的内容在打印区完整的打印了出来。

不知道小伙伴们有没有注意到,在使用open()函数的时候,其中的第二个参数「打开方式」这次发生了变化,改成了‘r’, 这中打开模式就是专门用于读取文件的,它在打开文件的时候,不会想‘w’的打开方式一样清空文件。

比如,我们讲之前的代码中换一下打开方式来试试:

1
2
3
4
5
6
7
8
fp = open('./data/13-1.txt', 'w', encoding='GBK')
res = fp.read()
fp.close()

print(res)

---
UnsupportedOperation: not readable

报错了,提示不可读。

我们再去直接打开13-1.txt的时候可以看到。文件内空空如也,之前写入的内容全都被清空了。

到这里为止,大家了解了文件操作的四个基本操作函数,在这里我可以教大家一个文件操作中的一些高级技巧,比如,我们可以使用with...as...来进行操作:

1
2
3
4
'''
with open(文件路径, 打开模式) as 变量:
变量.操作()
'''

让我们直接来看示例:

1
2
3
4
5
6
7
with open('./data/13-1.txt', 'r+', encoding='GBK') as fp:
res = fp.read()
print(res)

---
相对路径: 比如说我们被路人问路,我们就说:这条街往前,前面十字路口就是交道口,然后左转,再走 100 米左右就到了。
绝对路径: 这个就非常好理解了,北京市西城区鼓楼东大街 28 号,特别准确了对吧?

这样,我们也就直接完成了之前读取的操作。

read函数内是有参数的:read(count), 接收的值为整型,这里是描述当前我要读取几个字节长度:

1
2
3
4
5
6
with open('./data/13-1.txt', 'r', encoding='GBK') as fp:
res = fp.read(5)
print(res)

---
相对路径:

有的小伙伴看到我写到这可能就有疑问了,我为什么没有写close()函数,那是不是说,现在这个文件都还一直存在内容地址中。

其实并不是如此。在使用with...as...这个方式去打开一个文件的之后,在整个代码结束的时候会自动对当前打开的文件一遍执行close()函数。

好,让我接着继续:

1
2
3
4
5
6
7
8
with open('./data/13-1.txt', 'r+', encoding='GBK') as fp:
res = fp.read()
print(res)
fp.write(res)

---
相对路径: 比如说我们被路人问路,我们就说:这条街往前,前面十字路口就是交道口,然后左转,再走 100 米左右就到了。
绝对路径: 这个就非常好理解了,北京市西城区鼓楼东大街 28 号,特别准确了对吧?

打印区并未发生变化,原因就是我们的写入操作是在print之后进行的,我们直接打开文件来看看:

可以看到,内容确实被写入文件中了。注意我打标记的地方,并没有换行对吧,也就是说,我们在做写入的时候,指针是标记在这个位置的,然后继续往后写入。

另外整个代码中需要注意的就是打开模式了,我们之前已经用过的打开模式有‘w’和``'r', 现在我们用了‘r+’的模式,那么r+呢就是既可以读,也可以写入。并且,不会一开始就清空文件的内容。

对应‘w’的清空模式,就是‘w+’, 虽然‘w+’也是可读可写的模式,但是它和‘w’的模式一致,打开文件的时候直接清空整个文件的内容。

除了这四个模式之外,还有'a'和‘'a+’模式,是追加写的模式,这种模式的特点是打开文件的时候,指针是放在文件最末尾的。所以这种模式使用read()的时候,是读不到任何内容的。

以为到这里就结束了吗?太单纯了,整个文件操作的打开模式中,还有一个‘x+’的模式,这种模式我们可以称它为异或,什么意思呢?就是这种模式只会新建文件来执行后续操作,否则就会报错:

1
2
3
4
5
with open('./data/13-1.txt', 'x+', encoding='GBK') as fp:
print(fp.read())

---
FileExistsError: [Errno 17] File exists: './data/13-1.txt'

提示文件错误:文件存在。

如果我们操作的是一个本来不存在的文件,才可以正常的往下进行:

1
2
3
4
5
6
7
8
9
10
11
with open('./data/13-x+.txt', 'x+', encoding='UTF-8') as fp:
res = fp.read()
print(res)
fp.write('这里是"x+"模式下新加入的内容。')

with open('./data/13-x+.txt', 'r+', encoding='UTF-8') as fp:
res = fp.read()
print(res)

---
这里是"x+"模式下新加入的内容。

我们在用‘x+’模式打开一个文件的时候,它已经新建了这个文件,我们可以看到读取之后并未读取到任何内容,因为这个文件内还是空的。在进行写入操作之后,我们在下面再一次读取这个文件,可以看到内容已经被写入了。

详谈「打开模式」

其实mode这个参数并不只是我们演示的这么点内容,mode这个参数是接收两种值的,一个是刚才我们一直在讲的读写模式, 而另外一个则是文件格式

读写模式

读写模式的参数主要有四种, 分别是r, w,a以及一个特殊+, 其中r, w, a决定了当前文件默认是只读还是只写,还有就是指针位置。+是和前面三个结合使用的,无法单独使用,其主要作用是使的文件读写兼备。

文件格式

文件格式主要是以两种格式为准,一种是普通的文本文件,一种是二进制格式文件。不要以为二进制格式没什么大不了,我们一般谈到非文本文件都属于二进制格式文件,比如:图片。

这两个格式控制字符一个是t: 以文本格式打开文件(默认值), 一个是b: 以二进制格式打开文件。

一般来说,我们大部分时候都不会单独使用某一个参数吗,而是结合着一起使用。比如:

r+, 打开一个文件用于读写,文件存在就打开,文件不存在则报错。指针在文件头。这种模式要注意,因为指针在文件头,所以新写入的内容会在原内容之前。

w+, 打开一个文件用于读写,文件存在就打开,并且会清空所有内容后进入编辑模式,如果文件不存在则会创建一个新文件。虽然指针也在文件头,但是因为它霸道的清空属性,所以也不存在新写入的内容会在原内容之前了。

a+, 以追加的模式打开一个文件用于读写,如果文件存在就打开,如果文件不存在,则会创建一个新文件用于读写。这种模式下和w+不同的地方在于它会将指针放在文件末尾,写入的时候是从文件尾部开始写。并且,它没那么霸道,要清空原内容才可以。

其他的模式就是在打开文件格式和读写模式的组合,一般我们不写是因为大部分时候我们操作的都是文本文件进行操作,而如果我们需要用二进制格式打开文件的时候,就不能使用默认的t而是b了,一般我们会是这样进行组合:rb, rb+, wb, wb+, ab, ab+

当然,最后就是我们刚才用到的x+, 其实它也是一种组合形式,原本应该是x, 这种模式是在 Python3 中新添加的,它在文件不存在的时候它会创建一个新文件用于写入。如果这个文件存在,就会报错。

那么有了x这个参数之后,我们以前为了避免误操作覆盖原文件,那么我们会先去判断一个文件是否存在,然后再去执行后续的写入操作。可是使用x就没那么麻烦了,可以直接操作写入,反正文件如果存在会返回错误。

关于指针位置

那么我们在使用了r+之后,有没有什么办法可以让我们不在原内容之前写入内容而是从后开始写呢?

答案是有办法,也就是调整指针位置,调整完毕之后再进行写入操作就可以了。

调整指针的方法为seek(offset[, whence])。我们来看一个对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 创建一个新文件
with open('./data/13-2.txt', 'w', encoding='UTF-8') as fp:
fp.write('1. Hello Python.\n2. Hello C++. \n3. Hello Ruby.')

# 正常状态下
with open('./data/13-2.txt', 'r', encoding='UTF-8') as fp:
print(fp.readline())
print(fp.readline())
print('--------------')

# 设置指针重新偏移到头部
with open('./data/13-2.txt', 'r', encoding='UTF-8') as fp:
print(fp.readline())
fp.seek(0, 0)
print(fp.readline())

---
1. Hello Python.

2. Hello C++.

------------
1. Hello Python.

1. Hello Python.

在最开始,我们重新创建了一个文件,然后写了三行文字。分别是:

1
2
3
1. Hello Python.
2. Hello C++.
3. Hello Ruby.

然后我们开始用不同的方法进行读取,每次仅读取一行。

正常状态下,readline()这个方法是顺序往下执行的,第一次执行的时候读取的是第一行,第二次执行的时候就是读取的第二行。这种方式是不是感觉有些熟悉,像不像迭代器?

回过头来,我们再来看两次执行的结果,不同的是,第二次我在两个readline()方法中间加入了一段fp.seek(0,0)来将指针再次调整到头部,别着急,我们一会讲为什么这样写,先来看看结果。

因为有了fp.seek(0,0)的存在,第二次执行和第一次完全不同。第一段内容被读取了两次。这就是seek()的作用,讲指针又调整到了文件头部。

现在,让我们来说说seek()内参数的含义,完整的写法是:seek(offset[, whence]),其中offset是偏移量,而whence是从哪开始。whence就只有三个值, 0, 1, 2, 0 就表示是从头部开始偏移,1 就表示从当前位置开始偏移,2 就代表从文件末尾开始偏移。而我们写的(0,0)意思就是从文件头部开始偏移,偏移量为 0。

再来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
# 设置指针重新偏移到头部
with open('./data/13-2.txt', 'r+', encoding='UTF-8') as fp:
fp.seek(0, 2)
fp.write('这里是使用 r+添加到末尾的内容')

with open('./data/13-2.txt', 'r', encoding='UTF-8') as fp:
print(fp.read())

---
1. Hello Python.
2. Hello C++.
3. Hello Ruby.这里是使用 r+添加到末尾的内容

通过前面的学习我们知道,r+在打开文件之后,指针是放在头部的,但是我们这里用seek(0,2)将指针调整到了最末尾,并且写入了一段文字。

学到这里,文件的基本操作也就差不多学完了,让我们来分别总结一下:

相关函数

  • open(), 打开文件, 格式:open(file_name [, access_mode][, buffering])

  • read(), 读取内容, 格式:fileObject.read([count])

不设置count是从当前位置读取到文件末尾,设置count这是读取指定长度的字符。

  • readline(), 读取一行

不设置count是从当前位置读取到这一行末尾,设置count这是读取这一行中指定长度的字符。

  • readlines(), 读取所有行

不设置参数是表示读取所有汗,每一行作为一个参数,返回了一个列表。设置count是按照行进行读取,可以设置读取的字节数,设置的字节数不足一行按一行来读取。

  • write(),写入内容, 格式:fileObject.write(string)
  • writelines(), 写入容器类型数据:

写入容器类数据的时候要注意,这个容器类数据必须是可更新的类型。

  • seek(), 设置文件指针的偏移, 格式: seek(offset[, whence])
  • close(), 关闭文件

当然,除了这几个之外,文件还有很多其他的函数,但是目前我们用这些进行读写操作就足够了。

打开模式(图)

关于打开模式, 我之前写的那些内容看懂理解了,其实也就不需要现在这两张图了,可是我担心的是有些小伙伴理解不了,那有了下面的图,至少操作的时候可以参考:

模式 描述
t 文本模式 (默认)。
x 写模式,新建一个文件,如果该文件已存在则会报错。
b 二进制模式。
+ 打开一个文件进行更新(可读可写)。
U 通用换行模式(不推荐)。
r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
rb 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。
r+ 打开一个文件用于读写。文件指针将会放在文件的开头。
rb+ 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。
w 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
w+ 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+ 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+ 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

总结一下最常用的六种模式:

模式 r r+ w w+ a a+
创建
覆盖
指针在开始
指针在结尾

下面这张经典的流程图可以告诉你在什么时候需要用什么:

结尾与预告

文件的基本操作就介绍到这里了,大家下课之后记得要去多多的熟悉和练习。

那么下一节课呢,我们会根据我们这之前所讲的所有内容,尝试做一个小demo, 实现一个简单的注册和登录功能。

这里先介绍一下这个demo:

1
2
3
4
5
6
实现功能:
1. 用户输入用户名和密码以及确认密码
2. 用户名不能重复
3. 两次密码要一致
4. 用户用已经注册的账户登录
5. 密码如果错误 3 次,锁定,无法再登录。

好了,小伙伴们,咱们下节课再见。

13. Python 的文件操作

https://hivan.me/file-operations/

作者

Hivan Du

发布于

2023-08-08

更新于

2024-01-16

许可协议

评论