当你用 Python 写程序时,不论是简单的脚本,还是复杂的大型项目,其中最常见的操作就是读写文件。不管是简单的文本文件、繁杂的日志文件,还是分析图片等媒体文件中的字节数据,都需要用到 Python 中的文件读写。
本文包含以下内容
- 文件的构成部分
- Python 读写文件的基本操作
- 在一些场景下读写文件的技巧
这篇文章主要是面向 Python 的初级和中级开发者的,当然高级开发者也可能从中有所收获 : )
文件由哪些部分构成的?
在我们正式进入 Python 文件读写之前,首要的一件事,是要搞明白,到底什么是文件,以及操作系统如何识别不同文件的。
从根本上讲,文件实际上就是一组连续的字节存储下来的数据。这些数据,基于某些规范,组织成了不同的文件类型,可以是一个简单的文本文件,异或是复杂的可执行程序文件。但其实最终,不管它们原来是何种文件类型,最终都会被计算机翻译成 1 和 0 这种二机制的表示,以交给 CPU 进行数据处理。
在现代的大部分的文件系统中,文件由以下3个部分构成:
- 文件头: 文件的元数据【文件名、大小、类型 等等】
- 文件数据: 由文件的创建者或编辑者,编辑的内容【比如:文本、图片、音频、视频 内容等等】
- 文件结束: 由特殊的字符来标记出来,这是文件的结束了
文件所表示的到底是什么数据,具体由其 类型 所决定,通常情况下,体现在扩展名上【当然这主要在 windows 中较为常见,linux 中则对文件扩展名不是那么的在意】。例如,如果一个文件的扩展名是 .gif,那么通常情况下,它可能是一个动图【极端情况下,它可能不是动图,而是一个病毒或恶意脚本程序】。文件的扩展名类型有成百上千个,在本文中,你只需要操作 .txt 文本文件。
文件的路径
当你访问一个文件的时候,文件的路径是必需的。文件的路径就是一个字符串,代表了它所在文件系统中的位置,它由以下3个部分组成:
-
文件目录: 文件所处的目录名称,在
windows系统中,多个目录由\分隔,在unix系统中,由/分隔 -
文件名: 扩展名如
.txt前面的名称,如果没有扩展名,则整个都是文件名 -
扩展名: 最后一个
.和后面的字符,组成扩展名
注意换行符的不同
在处理文件数据时,我们经常遇到的一个问题,就是 换行符 的不同。美国标准协会规定了换行符是由 \r\n 组成,这在 windows 系统上,是通行的换行符标准,而在 unix 系统上,像各种 linux 发行版 和 mac,换行符是 \n,这就给我们程序员在判断和处理换行符时,带来了麻烦,尤其是当你写出的程序,需要兼容 windows 和 unix 的时候。
让我们来看下面这个例子,这是一个在 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,这让你的程序处理每行文本的时候,都要多一些兼容性处理。
字符编码
你极有可能遇到的另一个问题,是字符编码问题。字符编码实际上是计算机把二机制的字节数据,转换成人类可以看明白的字符的过程。字符编码后,通常由一个整型数字来代表一个字符,像最常见的 ascii 和 unicode 字符编码方式。
ascii 是 unicode 的子集,也就是说,它们共用相同的字符集,只不过 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类:
- Text files
- Buffered binary files
- 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\']
以循环的方式,读取文件中的每一行
其实,最常见的操作,是以循环迭代的方式,一行行的读取文件内容,直至文件结尾。
下面是一个初学者经常会写出来的典型范例【包括几天前的我自己