Python 读写文件的正确方式

当你用 Python 写程序时,不论是简单的脚本,还是复杂的大型项目,其中最常见的操作就是读写文件。不管是简单的文本文件、繁杂的日志文件,还是分析图片等媒体文件中的字节数据,都需要用到 Python 中的文件读写。

本文包含以下内容

  • 文件的构成部分
  • Python 读写文件的基本操作
  • 在一些场景下读写文件的技巧

这篇文章主要是面向 Python 的初级和中级开发者的,当然高级开发者也可能从中有所收获 : )

文件由哪些部分构成的?

在我们正式进入 Python 文件读写之前,首要的一件事,是要搞明白,到底什么是文件,以及操作系统如何识别不同文件的。

从根本上讲,文件实际上就是一组连续的字节存储下来的数据。这些数据,基于某些规范,组织成了不同的文件类型,可以是一个简单的文本文件,异或是复杂的可执行程序文件。但其实最终,不管它们原来是何种文件类型,最终都会被计算机翻译成 10 这种二机制的表示,以交给 CPU 进行数据处理。

在现代的大部分的文件系统中,文件由以下3个部分构成:

  1. 文件头: 文件的元数据【文件名、大小、类型 等等】
  2. 文件数据: 由文件的创建者或编辑者,编辑的内容【比如:文本、图片、音频、视频 内容等等】
  3. 文件结束: 由特殊的字符来标记出来,这是文件的结束了

Python 读写文件的正确方式

文件所表示的到底是什么数据,具体由其 类型 所决定,通常情况下,体现在扩展名上【当然这主要在 windows 中较为常见,linux 中则对文件扩展名不是那么的在意】。例如,如果一个文件的扩展名是 .gif,那么通常情况下,它可能是一个动图【极端情况下,它可能不是动图,而是一个病毒或恶意脚本程序】。文件的扩展名类型有成百上千个,在本文中,你只需要操作 .txt 文本文件。

文件的路径

当你访问一个文件的时候,文件的路径是必需的。文件的路径就是一个字符串,代表了它所在文件系统中的位置,它由以下3个部分组成:

  1. 文件目录: 文件所处的目录名称,在 windows 系统中,多个目录由 \ 分隔,在 unix 系统中,由 / 分隔
  2. 文件名: 扩展名如 .txt 前面的名称,如果没有扩展名,则整个都是文件名
  3. 扩展名: 最后一个 . 和后面的字符,组成扩展

注意换行符的不同

在处理文件数据时,我们经常遇到的一个问题,就是 换行符 的不同。美国标准协会规定了换行符是由 \r\n 组成,这在 windows 系统上,是通行的换行符标准,而在 unix 系统上,像各种 linux 发行版 和 mac,换行符是 \n,这就给我们程序员在判断和处理换行符时,带来了麻烦,尤其是当你写出的程序,需要兼容 windowsunix 的时候。

让我们来看下面这个例子,这是一个在 windows 上创建的,描述狗的品种的文件:dog_breeds.txt

Pug\r\n
Jack Russell Terrier\r\n
English Springer Spaniel\r\n
German Shepherd\r\n
Staffordshire Bull Terrier\r\n
Cavalier King Charles Spaniel\r\n
Golden Retriever\r\n
West Highland White Terrier\r\n
Boxer\r\n
Border Terrier\r\n

它的换行符,明显是 \r\n,那么在 unix 系统上,它将显示成这样:

Pug\r
\n
Jack Russell Terrier\r
\n
English Springer Spaniel\r
\n
German Shepherd\r
\n
Staffordshire Bull Terrier\r
\n
Cavalier King Charles Spaniel\r
\n
Golden Retriever\r
\n
West Highland White Terrier\r
\n
Boxer\r
\n
Border Terrier\r
\n

当你在 unix 系统上,运行你写的 Python 程序的时候,你以为的换行符 \n 就不是你以为的了,每一行内容后面,都会多一个 \r,这让你的程序处理每行文本的时候,都要多一些兼容性处理。

字符编码

你极有可能遇到的另一个问题,是字符编码问题。字符编码实际上是计算机把二机制的字节数据,转换成人类可以看明白的字符的过程。字符编码后,通常由一个整型数字来代表一个字符,像最常见的 asciiunicode 字符编码方式。

asciiunicode 的子集,也就是说,它们共用相同的字符集,只不过 unicode 所能表示的字符数量,要比 ascii 多的多。值得注意的是,当你用一个错误的编码方式,解析一个文件内容的时候,通常会得到意想不到的后果。比如,一个文件的内容是 utf-8 编码的,而你用 ascii 的编码方式去解析读取此文件内容,那么,你大概率会得到一个满是乱码的文本内容。

文件的打开和关闭

当你想在 Python 中处理文件的时候,首要的事情,就是用 open() 打开文件。open() 是 Python 的内建函数,它需要一个必要参数来指定文件路径,然后返回文件对象:

file = open('dog_breeds.txt')

当你学会打开文件之后,你下一个要知道的是,如何关闭它。

给你一个忠告,在每次 open() 处理完文件后,你一定要记得关闭它。虽然,当你写的应用程序或脚本,在执行完毕后,会自动的关闭文件,回收资源,但你并不确定在某些意外情况下,这一定会执行。这就有可能导致资源泄漏。确保你写的程序,有着合理的结构,清晰的逻辑,优雅的代码 和 不再使用的资源的释放,是一个新时代IT农民工必备的优秀品质【手动狗头】。

当你在处理文件的时候,有2种方式,能够确保你的文件一定会被关闭,即使在出现异常的时候。

第一种方式,是使用 try-finally 异常处理:

reader = open('dog_breeds.txt')
try:
# Further file processing goes here
finally:
reader.close()

第二种方式,是使用 with statement 语句:

with open('dog_breeds.txt') as reader:
# Further file processing goes here

with 语句的形式,可以确保你的代码,在执行到离开 with 结构的时候,自动的执行关闭操作,即使在 with 代码块中出现了异常。我极度的推荐这种写法,因为这会让你的代码很简洁,并且在意想不到的异常处理上,也无需多做考虑。

通常情况下,你会用到 open() 的第2个参数 mode,它用字符串来表示,你想要用什么方式,来打开文件。默认值是 r 代表用 read-only 只读的方式,打开文件:

with open('dog_breeds.txt', 'r') as reader:
# Further file processing goes here

除了 r 之外,还有一些 mode 参数值,这里只简要的列出一些常用的:

Character Meaning
'r' 只读的方式打开文件 (默认方式)
'w' 只写的方式打开文件, 并且在文件打开时,会清空原来的文件内容
'rb' or 'wb' 二进制的方式打开文件 (读写字节数据)

现在让我们回过头,来谈一谈 open() 之后,返回的文件对象:

“an object exposing a file-oriented API (with methods such as read() or write()) to an underlying resource.”

文件对象分为3类:

  1. Text files
  2. Buffered binary files
  3. Raw binary files

Text File Types

文本文件是你最常遇到和处理的,当你用 open() 打开文本文件时,它会返回一个 TextIOWrapper 文件对象:

>>> file = open('dog_breeds.txt')
>>> type(file)
<class '_io.TextIOWrapper'>

Buffered Binary File Types

Buffered binary file type 用来以二进制的形式操作文件的读写。当用 rb 的方式 open() 文件后,它会返回 BufferedReader BufferedWriter 文件对象:

>>> file = open('dog_breeds.txt', 'rb')
>>> type(file)
<class '_io.BufferedReader'>
>>> file = open('dog_breeds.txt', 'wb')
>>> type(file)
<class '_io.BufferedWriter'>

Raw File Types

Raw file type 的官方定义是:

“generally used as a low-level building-block for binary and text streams.”

说实话,它并不常用,下面是一个示例:

>>> file = open('dog_breeds.txt', 'rb', buffering=0)
>>> type(file)
<class '_io.FileIO'>

你可以看到,当你用 rb 的方式 open() 文件,并且 buffering=0 时,返回的是 FileIO 文件对象。

文件的读和写

下面,终于进入正题了。

当你打开一个文件的时候,实际上,你是想 读 或是 写 文件。首先,让我们先来看读文件,下面是一些 open() 返回的文件对象,可以调用的方法:

Method What It Does
.read(size=-1) This reads from the file based on the number of size bytes. If no argument is passed or None or -1 is passed, then the entire file is read.
.readline(size=-1) This reads at most size number of characters from the line. This continues to the end of the line and then wraps back around. If no argument is passed or None or -1 is passed, then the entire line (or rest of the line) is read.
.readlines() This reads the remaining lines from the file object and returns them as a list.

以上文提到的 dog_breeds.txt 文本文件作为读取目标,下面来演示如何用 read() 读取整个文件内容:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read & print the entire file
>>> print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

下面的例子,通过 readline() 每次只读取一行内容:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> print(reader.readline())
>>> print(reader.readline())
Pug
Jack Russell Terrier

下面是通过 readlines() 读取文件的全部内容,并返回一个 list 列表对象:

>>> f = open('dog_breeds.txt')
>>> f.readlines() # Returns a list object
['Pug\n', 'Jack Russell Terrier\n', 'English Springer Spaniel\n', 'German Shepherd\n', 'Staffordshire Bull Terrier\n', 'Cavalier King Charles Spaniel\n', 'Golden Retriever\n', 'West Highland White Terrier\n', 'Boxer\n', 'Border Terrier\n']

以循环的方式,读取文件中的每一行

其实,最常见的操作,是以循环迭代的方式,一行行的读取文件内容,直至文件结尾。

下面是一个初学者经常会写出来的典型范例【包括几天前的我自己 】:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read and print the entire file line by line
>>> line = reader.readline()
>>> while line != '': # The EOF char is an empty string
>>> print(line, end='')
>>> line = reader.readline()
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

而另一种写法,则是用 readlines() 来实现的,说实话,这比上面的那种,要好不少:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> for line in reader.readlines():
>>> print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

需要注意的是,readlines() 返回的是一个 list 列表对象,它里面的每个元素,就代表着文本文件的每一行内容。

然而,上面的2种写法,都可以用下面这样,直接循环迭代文件对象自身的方式,更简单的实现:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read and print the entire file line by line
>>> for line in reader:
>>> print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

这最后一种实现方式,更具 Python 风格,更高效,所以这是我推荐给你的最佳实现。

写文件

现在,让我们来看看如何写文件。就像读取文件一样,写文件也有一些好用的方法供我们使用:

Method What It Does
.write(string) This writes the string to the file.
.writelines(seq) This writes the sequence to the file. No line endings are appended to each sequence item. It’s up to you to add the appropriate line ending(s).

下面是一个分别使用 write()writelines() 写文件的示例:

# 先从原始文件中读取狗的品种
with open('dog_breeds.txt', 'r') as reader:
# Note: readlines doesn't trim the line endings
dog_breeds = reader.readlines() # 以 w 的模式,打开要写入的新文件
with open('dog_breeds_reversed.txt', 'w') as writer:
# 实现方式一
# writer.writelines(reversed(dog_breeds)) # 实现方式二,将读取到的狗的品种,写入新文件,并且用了 reversed() 函数,将原文的顺序进行了反转
for breed in reversed(dog_breeds):
writer.write(breed)

与字节共舞

有时,你可能需要以字节的形式,来处理文件。你只需在模式参数中,追加 r 即可,文件对象所提供的所有的方法,都一样用,不同的是,这些方法的输入和输出,不再是字符串 str 对象,而是字节 bytes 对象。

这是一个简单的示例:

>>> with open('dog_breeds.txt', 'rb') as reader:
>>> print(reader.readline())
b'Pug\n'

使用 b 模式处理文本文件,并没什么特别的花样,让我们来看看,处理图片,会不会比较有意思一点,像下面这样一条狗狗的 jack_russell.png 图片:

Python 读写文件的正确方式

你可以写 Python 代码,读取这张图片,然后检查它的内容。如果一个 png 图片是正儿八经的,那么它的文件头部内容,是8个字节,分别由以下部分组成:

Value Interpretation
0x89 其实就是一个魔术数字,代表这是一个PNG图片的开头
0x50 0x4E 0x47 以 ASCII 码表示的【PNG】这3个字母
0x0D 0x0A DOS 风格的换行符 \r\n
0x1A DOS 风格的 EOF 字符
0x0A Unix 风格的换行符 \n

如果,你用下面的代码,读取这张图片的话,你会发现,它确实是个正儿八经的 png 图片,因为它文件头部的8个字节,同上表一致:

>>> with open('jack_russell.png', 'rb') as byte_reader:
>>> print(byte_reader.read(1))
>>> print(byte_reader.read(3))
>>> print(byte_reader.read(2))
>>> print(byte_reader.read(1))
>>> print(byte_reader.read(1))
b'\x89'
b'PNG'
b'\r\n'
b'\x1a'
b'\n'

一些小技巧和我的新的领悟

现在,你掌握了文件读写的基本操作,这些完全够你用的了,正所谓这20%的技能,就能覆盖80%的使用场景。下面说一下上文没有提到的,但是使用时,也经常会用到的一些技巧,和我对于某些方面的新的领悟。

在要写入的文件后,追加内容

有时,你需要在要写入的文件后,追加内容,而不是像之前的 w 模式,先把原文件清空了再写入。此时,可以用 a 模式:

with open('dog_breeds.txt', 'a') as a_writer:
a_writer.write('\nBeagle')

当你再次用 Python 代码读取,或是直接打开这个文本文件的时候,你会发现原始内容还在,只是在最后追加了 Beagle

>>> with open('dog_breeds.txt', 'r') as reader:
>>> print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
Beagle

好了,现在我知道了,当要在文件最后,追加内容的时候,我应该使用 w 模式,而非 a 模式。然而其时此刻,我已经陷入了一个误区,就是所有我感觉要不断的在文件后追加新的内容的时候,我都会用 a 模式,而这在一些场景下面,是不合时宜的。

比如,我下面要做这样一件事,读取 dog_breeds.txt 狗的品种,计算每一行字符的长度,把 品种品种的字符长度 写入新的文件。因为是每读取一行,就每写入一行,这里我通常会顺其自然的想到用 a 模式,追加写入的新的文件中:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> with open('new_dog_breeds.txt', 'a') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因为读取到的每一行,是包含换行符的,所以,这里要先把最后面的换行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')

这个程序,只运行一次,固然没有问题,但是,如果我现在修改了 dog_breeds.txt 原始文件,在里面增加了一些狗的品种,想再次运行这个程序,生成新的结果的时候,我必须先把之前保存结果的 new_dog_breeds.txt 文件内容清空,再去运行。否则,第二次运行的结果,会追加在 new_dog_breeds.txt 文件原有的内容后面,导致老的内容重复了,这不是我想要的。

其实,我正是我对 wa 的误解。要解决这个不大,但确实是有点小麻烦的问题,其实,我们只需把上面代码中,第二行打开写入文件时的 a 模式,换成 w 模式即可:

>>> with open('dog_breeds.txt', 'r') as reader:
>>> with open('new_dog_breeds.txt', 'w') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因为读取到的每一行,是包含换行符的,所以,这里要先把最后面的换行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')

这样,不管我的程序运行多少遍,写入的新文件中,都只会有原始文件中所有狗的品种和长度,而再也不会出现上述问题。

之所以我犯了这个看起来无关紧要的错误,就是因为我先前对于 w 的误解:我以为 w 是在写之前,要清空原始内容,准确的说,是在调用 writer.write() 的时候,会清空原始内容,其实并不是;其实 w 模式是在 open() 的时候清空的,而 writer.write() 则并不会清空,不断的 write() 则只会不断的在要写入的文件后面,增加新的内容而已。

你看,这就是我的新的领悟,还是小有所获的吧。如果你也像我之前一样,我想你也有了同样的顿悟。

读文件和写文件,代码放在一行

就像上面的代码,其实的一边读,一边写,每读取一行,处理后就写入一行,那么这2个文件的 open() 操作,可以放到一行,使得代码结构更清爽一点:

>>> with open('dog_breeds.txt', 'r') as reader, with open('new_dog_breeds.txt', 'w') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因为读取到的每一行,是包含换行符的,所以,这里要先把最后面的换行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')

读取和写入同一个文件

r 模式,是只读模式,wa 模式,是只写模式。有时,我们想从一个文件中读取内容,进行计算或其他处理后,再写入同一个文件中,对于这种场景,我们可以用 r+ 模式,即读写模式:

>>> with open('dog_breeds.txt', 'r+') as file:
>>> for line in file:
>>> dog = line.rstrip('\n') # 因为读取到的每一行,是包含换行符的,所以,这里要先把最后面的换行符清除掉
>>> file.write(dog + ' ' + len(dog) + '\n')

读写模式,支持读取并修改文件内容,注意,这种模式下写入的内容,是追加在文件末尾的。

后记

现在,我可以说,这是一篇我翻译的文章。

翻译,有3种手段。第一种最简单,用 Chrome 浏览器右键翻译,直接出来结果,这种类似的方式,称之为【机翻】。第二种,在【机翻】的基础上,再进行改错、优化,修正一些【机翻】错误或不到位的地方。第三种,就是基于原文,以其整篇文章的框架、脉络、核心内容为基础,进行二次创作,增、删、改 部分内容,以达到译者想要的效果。比如这篇文章,我就删除了部分过于简单、直白的小白内容,像文件的相对路径,也删除了过于复杂和高阶的内容,像自定义 Context Manager;增加了我在用 a 模式写文件时的一些误解和感悟,和 r+ 读写文件模式;同时也修改、调整了原文的小部分内容,使之更合理和自然,能够对初学者更友好。

我们经常说,翻译有3重境界:信、达、雅。我是这样理解的:信 是译文要准确,不能有根本错误;达 是能够让读者很容易的理解意思,简单易懂,要做到足够的本地化;雅 在前面的基础之上,还能做到优雅、美妙,有艺术创作的成分。我自认为我这篇译文,基本做到了信和达,雅的话,我觉得我那个小标题【与字节共舞】(原文:Working With Bytes)还勉强能算得上。

如果,你在读这篇文章的时候,没有感觉是在读一篇有些别扭的文字,相反,读起来行文流畅、通俗易懂,那么我的目的就达到了。我就是要让人感觉不出,这是一篇翻译的文章,以检验我在初级英文上的翻译水准和创作能力【见笑见笑】。

原文出处:https://realpython.com/read-write-files-python

上一篇:JS中的作用域链


下一篇:Laravel 项目开发规范