Python读写⽂件的正确⽅式
当你⽤ Python 写程序时,不论是简单的脚本,还是复杂的⼤型项⽬,其中最常见的操作就是读写⽂件。不管是简单的⽂本⽂件、繁杂的⽇志⽂件,还是分析图⽚等媒体⽂件中的字节数据,都需要⽤到 Python 中的⽂件读写。
本⽂包含以下内容
⽂件的构成部分
Python 读写⽂件的基本操作
在⼀些场景下读写⽂件的技巧
这篇⽂章主要是⾯向 Python 的初级和中级开发者的,当然⾼级开发者也可能从中有所收获 : )
⽂件由哪些部分构成的?
在我们正式进⼊ Python ⽂件读写之前,⾸要的⼀件事,是要搞明⽩,到底什么是⽂件,以及操作系统如何识别不同⽂件的。
从根本上讲,⽂件实际上就是⼀组连续的字节存储下来的数据。这些数据,基于某些规范,组织成了不同的⽂件类型,可以是⼀个简单的⽂本⽂件,异或是复杂的可执⾏程序⽂件。但其实最终,不管它们原来是何种⽂件类型,最终都会被计算机翻译成1和0这种⼆机制的表⽰,以交给CPU进⾏数据处理。
在现代的⼤部分的⽂件系统中,⽂件由以下3个部分构成:
1. ⽂件头:⽂件的元数据【⽂件名、⼤⼩、类型等等】
2. ⽂件数据:由⽂件的创建者或编辑者,编辑的内容【⽐如:⽂本、图⽚、⾳频、视频内容等等】
3. ⽂件结束:由特殊的字符来标记出来,这是⽂件的结束了
⽂件所表⽰的到底是什么数据,具体由其类型所决定,通常情况下,体现在扩展名上【当然这主要在windows中较为常见,linux中则对⽂件扩展名不是那么的在意】。例如,如果⼀个⽂件的扩展名是.gif,那么通常情况下,它可能是⼀个动图【极端情况下,它可能不是动图,⽽是⼀个病毒或恶意脚本程序】。⽂件的扩展名类型有成百上千个,在本⽂中,你只需要操作.txt⽂本⽂件。
⽂件的路径
当你访问⼀个⽂件的时候,⽂件的路径是必需的。⽂件的路径就是⼀个字符串,代表了它所在⽂件系统中的位置,它由以下3个部分组成:
1. ⽂件⽬录:⽂件所处的⽬录名称,在windows系统中,多个⽬录由\分隔,在unix系统中,由/分隔
2. ⽂件名:扩展名如.txt前⾯的名称,如果没有扩展名,则整个都是⽂件名
3. 扩展名:最后⼀个.和后⾯的字符,组成扩展名
注意换⾏符的不同
在处理⽂件数据时,我们经常遇到的⼀个问题,就是换⾏符的不同。美国标准协会规定了换⾏符是由\r\n组成,这在windows系统上,是通⾏的换⾏符标准,⽽在unix系统上,像各种linux发⾏版和mac,换⾏符是\n,这就给我们程序员在判断和处理换⾏符时,带来了⿇烦,尤其是当你写出的程序,需要兼容windows和unix的时候。
让我们来看下⾯这个例⼦,这是⼀个在windows上创建的,描述狗的品种的⽂件:
Pug\r\n
Jack Rusll 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 Rusll 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
quiet是什么意思
\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('')
当你学会打开⽂件之后,你下⼀个要知道的是,如何关闭它。
给你⼀个忠告,在每次open()处理完⽂件后,你⼀定要记得关闭它。虽然,当你写的应⽤程序或脚本,在执⾏完毕后,会⾃动的关闭⽂件,回收资源,但你并不确定在某些意外情况下,这⼀定会执⾏。这就有可能导致资源泄漏。确保你写的程序,有着合理的结构,清晰的逻辑,优雅的代码和不再使⽤的资源的释放,是⼀个新时代IT农民⼯必备的优秀品质【⼿动狗头】。
当你在处理⽂件的时候,有2种⽅式,能够确保你的⽂件⼀定会被关闭,即使在出现异常的时候。
第⼀种⽅式,是使⽤try-finally异常处理:
reader = open('')
try:
# Further file processing goes here
finally:
reader.clo()
第⼆种⽅式,是使⽤with statement语句:
with open('') as reader:
# Further file processing goes here
with语句的形式,可以确保你的代码,在执⾏到离开with结构的时候,⾃动的执⾏关闭操作,即使在wi
th代码块中出现了异常。我极度的推荐这种写法,因为这会让你的代码很简洁,并且在意想不到的异常处理上,也⽆需多做考虑。
通常情况下,你会⽤到open()的第2个参数mode,它⽤字符串来表⽰,你想要⽤什么⽅式,来打开⽂件。默认值是r代表⽤read-only只读的⽅式,打开⽂件:
with open('', '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('')
>>> type(file)
<class '_io.TextIOWrapper'>
Buffered Binary File Types
Buffered binary file type ⽤来以⼆进制的形式操作⽂件的读写。当⽤rb的⽅式open()⽂件后,它会返回BufferedReader 或BufferedWriter ⽂件对象:
>>> file = open('', 'rb')
>>> type(file)
<class '_io.BufferedReader'>
>>> file = open('', 'wb')
>>> type(file)
<class '_io.BufferedWriter'>
Raw File Types
Raw file type 的官⽅定义是:
“generally ud as a low-level building-block for binary and text streams.”
说实话,它并不常⽤,下⾯是⼀个⽰例:
>>> file = open('', '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 bad on the number of size bytes. If no argument is pasd or None or -1 is pasd, then the entire file is read.
slide是什么意思
.readline(size=-1)This reads at most size number of characters from the line. This continues to the e
nd of the line and then wraps back around. If no argument is pasd or None or -1 is pasd, 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.
以上⽂提到的⽂本⽂件作为读取⽬标,下⾯来演⽰如何⽤read()读取整个⽂件内容:>>> with open('', 'r') as reader:
>>> # Read & print the entire file
>>> ad())
Pug
Jack Rusll 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('', 'r') as reader:
>>> adline())
>>> adline())
Pug
购物商店Jack Rusll Terrier
下⾯是通过readlines()读取⽂件的全部内容,并返回⼀个list列表对象:
>>> f = open('')
>>> f.readlines() # Returns a list object
['Pug\n', 'Jack Rusll 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('', 'r') as reader:
>>> # Read and print the entire file line by line
>>> line = adline()
>>> while line != '': # The EOF char is an empty string
>>> print(line, end='')
>>> line = adline()
Pug
Jack Rusll Terrier
English Springer Spaniel
board
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
⽽另⼀种写法,则是⽤readlines()来实现的,说实话,这⽐上⾯的那种,要好不少:
>>> with open('', 'r') as reader:
>>> for line adlines():
>>> print(line, end='')
Pug
Jack Rusll 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('', 'r') as reader:
>>> # Read and print the entire file line by line
>>> for line in reader:
>>> print(line, end='')
Pug
Jack Rusll Terrier
English Springer Spaniel
多少钱英语German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
这最后⼀种实现⽅式,更具 Python 风格,更⾼效,所以这是我推荐给你的最佳实现。
cau写⽂件
现在,让我们来看看如何写⽂件。就像读取⽂件⼀样,写⽂件也有⼀些好⽤的⽅法供我们使⽤:
Method What It Does
.write(string)This writes the string to the file.
.writelines(q)This writes the quence to the file. No line endings are appended to each quence item. It’s up to you to add the appropriate line ending(s).
下⾯是⼀个分别使⽤write()和writelines()写⽂件的⽰例:
# 先从原始⽂件中读取狗的品种
with open('', 'r') as reader:
# Note: readlines doesn't trim the line endings
dog_breeds = adlines()
# 以 w 的模式,打开要写⼊的新⽂件
with open('dog_', 'w') as writer:
# 实现⽅式⼀
# writer.writelines(reverd(dog_breeds))
# 实现⽅式⼆,将读取到的狗的品种,写⼊新⽂件,并且⽤了 reverd() 函数,将原⽂的顺序进⾏了反转
for breed in reverd(dog_breeds):
writer.write(breed)
与字节共舞
有时,你可能需要以字节的形式,来处理⽂件。你只需在模式参数中,追加b即可,⽂件对象所提供的所有的⽅法,都⼀样⽤,不同的是,这些⽅法的输⼊和输出,不再是字符串str对象,⽽是字节bytes对象。
这是⼀个简单的⽰例:
>>> with open('', 'rb') as reader:
>>> adline())
b'Pug\n'
使⽤b模式处理⽂本⽂件,并没什么特别的花样,让我们来看看,处理图⽚,会不会⽐较有意思⼀点,像下⾯这样⼀条狗狗的jack_rusll.png图⽚:
你可以写 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_rusll.png', 'rb') as byte_reader:
>>> print(ad(1))
>>> print(ad(3))
>>> print(ad(2))
>>> print(ad(1))
>>> print(ad(1))
b'\x89'
b'PNG'
b'\r\n'
b'\x1a'
b'\n'
⼀些⼩技巧和我的新的领悟
现在,你掌握了⽂件读写的基本操作,这些完全够你⽤的了,正所谓这20%的技能,就能覆盖80%的使⽤场景。下⾯说⼀下上⽂没有提到的,但是使⽤时,也经常会⽤到的⼀些技巧,和我对于某些⽅⾯的新的领悟。
在要写⼊的⽂件后,追加内容
有时,你需要在要写⼊的⽂件后,追加内容,⽽不是像之前的w模式,先把原⽂件清空了再写⼊。此时,可以⽤a模式:
with open('', 'a') as a_writer:
a_writer.write('\nBeagle')
当你再次⽤ Python 代码读取,或是直接打开这个⽂本⽂件的时候,你会发现原始内容还在,只是在最后追加了Beagle:
>>> with open('', 'r') as reader:
>>> ad())
Pug
Jack Rusll Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
ovalBorder Terrier
Beagle
好了,现在我知道了,当要在⽂件最后,追加内容的时候,我应该使⽤a模式,⽽⾮w模式。然⽽其时此刻,我已经陷⼊了⼀个误区,就是所有我感觉要不断的在⽂件后追加新的内容的时候,我都会⽤a模式,⽽这在⼀些场景下⾯,是不合时宜的。
⽐如,我下⾯要做这样⼀件事,读取狗的品种,计算每⼀⾏字符的长度,把品种和品种的字符长度写⼊新的⽂件。因为是每读取⼀⾏,就每写⼊⼀⾏,这⾥我通常会顺其⾃然的想到⽤a模式,追加写⼊的新的⽂件中:
>>> with open('', 'r') as reader:
>>> with open('new_', 'a') as writer:affectionate
>>> for line in reader:元旦祝福语英文
>>> dog = line.rstrip('\n') # 因为读取到的每⼀⾏,是包含换⾏符的,所以,这⾥要先把最后⾯的换⾏符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')
这个程序,只运⾏⼀次,固然没有问题,但是,如果我现在修改了原始⽂件,在⾥⾯增加了⼀些狗的品种,想再次运⾏这个程序,⽣成新的结果的时候,我必须先把之前保存结果的new_⽂件内容清空,再去运⾏。否则,第⼆次运⾏的结果,会追加在new_⽂件原有的内容后⾯,导致⽼的内容重复了,这不是我想要的。
其实,这正是我对w和a的误解。要解决这个不⼤,但确实是有点⼩⿇烦的问题,其实,我们只需把上⾯代码中,第⼆⾏打开写⼊⽂件时的a模式,换成w模式即可:
>>> with open('', 'r') as reader:
>>> with open('new_', '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()则只会不断的在要写⼊的⽂件后⾯,增加新的内容⽽已。
所以,换成⽤w模式open()⽂件,清空原始数据,然后不断的⽤write()写⼊追加新的内容,正好是我上述场景所需要的。
你看,这就是我的新的领悟,还是⼩有所获的吧。如果你也像我之前⼀样,我想你也有了同样的顿悟。
读⽂件和写⽂件,代码放在⼀⾏
就像上⾯的代码,其实的⼀边读,⼀边写,每读取⼀⾏,处理后就写⼊⼀⾏,那么这2个⽂件的open()操作,可以放到⼀⾏,使得代码结构更清爽⼀点:
>>> with open('', 'r') as reader, with open('new_', 'w') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因为读取到的每⼀⾏,是包含换⾏符的,所以,这⾥要先把最后⾯的换⾏符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')
读取和写⼊同⼀个⽂件
r模式,是只读模式,w和a模式,是只写模式。有时,我们想从⼀个⽂件中读取内容,进⾏计算或其他处理后,再写⼊同⼀个⽂件中,对于这种场景,我们可以⽤r+模式,即读写模式:
>>> with open('', 'r+') as file:
>>> for line in file:
>>> dog = line.rstrip('\n') # 因为读取到的每⼀⾏,是包含换⾏符的,所以,这⾥要先把最后⾯的换⾏符清除掉
>>> file.write(dog + ' ' + len(dog) + '\n')
读写模式,⽀持读取并修改⽂件内容,注意,这种模式下写⼊的内容,是追加在⽂件末尾的。
后记
现在,我可以说,这是⼀篇我翻译的⽂章。
翻译,有三种⼿段。第⼀种最简单,⽤ Chrome 浏览器右键翻译,直接出来结果,这种类似的⽅式,称之为【机翻】。第⼆种,在【机翻】的基础上,再进⾏改错、优化,修正⼀些【机翻】错误或不到位的地⽅。第三种,就是基于原⽂,以其整篇⽂章的框架、脉络、核⼼内容为基础,进⾏⼆次创作,增、删、改部分内容,以达到译者想要的效果。⽐如这篇⽂章,我就删除了部分过于简单、直⽩的⼩⽩内容,像⽂件的相对路径,也删除了过于复杂和⾼阶的内容,像⾃定义Context Manager;增加了我在⽤a模式写⽂件时的⼀些误解和感悟,和r+读写⽂件模式等等;同时也修改、调整了原⽂的⼩部分内容,使之更合理和⾃然,能够对初学者更友好。
我们经常说,翻译有三重境界:信、达、雅。我是这样理解的:信是译⽂要准确,不能有根本错误;达是能够让读者很容易的理解意思,简单易懂,要做到⾜够的本地化;雅在前⾯的基础之上,还能做到优雅、美妙,有艺术创作的成分。我⾃认为我这篇译⽂,基本做到了信和达,雅的话,我觉得我那个⼩标题【与字节共舞】(原⽂:Working With Bytes)还勉强能算得上。
如果,你在读这篇⽂章的时候,没有感觉是在读⼀篇有些别扭的⽂字,相反,读起来⾏⽂流畅、通俗易懂,那么我的⽬的就达到了。我就是要让⼈感觉不出,这是⼀篇翻译的⽂章,以检验我在初级英⽂
上的翻译⽔准和创作能⼒【见笑见笑】。