我们目前的操作都是很直观地执行程序,要么是在交互模式下执行,要么是执行py文件,还没有涉及对文件的操作。
运行程序时,用变量保存数据是一个比较通用的方法。如果希望程序结束后数据仍然能够保存,就不能使用变量保存数据了,需要寻找其他方式保存数据,文件就是一个不错的选择。在程序运行过程中将数据保存到文件中,程序运行结束后,相关数据就保存到文件中了。当然,这涉及对文件的操作。
通过接下来的学习,你将将了解如何使用Python在硬盘上创建、读取和保存文件。
打开文件
在Python中,打开文件使用的是open函数。open函数的基本语法如下:
open(file_name [, access_mode][, buffering])
【参数解析】
- file_name变量:是一个包含要访问的文件名称的字符串值。
- access_mode变量:指打开文件的模式,对应有只读、写入、追加等。access_mode变量值不是必需的(不带access_mode变量时,要求-
file_name存在,否则报异常),默认的文件访问模式为只读(r)。- buffering:如果buffering的值被设为0,就不会有寄存;如果buffering的值取1,访问文件时就会寄存行;如果将buffering的值设为大于1的整数,表示这就是寄存区的缓冲大小;如果取负值,寄存区的缓冲大小就是系统默认的值。
open函数返回一个File(文件)对象。File对象代表计算机中的一个文件,是Python中另一种类型的值,就像我们熟悉的列表和字典。
例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = 'd:/test.txt'
f_name = open(path)
print(f_name.name)
执行结果如下:
d:/test.txt
执行结果告诉我们打开的是d盘下的test.txt文件(执行该程序前,已经创建了一个名为test.txt的文件)。
这里有几个概念要先弄清楚。
- 文件路径:在该程序中,我们先定义了一个path变量,变量值是一个文件的路径。文件的路径是指文件在计算机上的位置,如该程序中的d:/test.txt是指文件在d盘、文件名为test.txt。文件路径又分为绝对路径和相对路径。
1.绝对路径:总是从根文件夹开始。比如在Windows环境下,一般从c盘、d盘等开始,c盘、d盘被称为根文件夹,在该盘中的文件都得从根文件夹开始往下一级一级查找。在Linux环境下,一般从usr、home等根文件开始。比如在上面的示例程序中,path变量值就是一个绝对路径,在文件搜索框中输入绝对路径可以直接找到该文件。
2. 相对路径:相对于程序当前工作目录的路径。比如当前工作文件存放的绝对路径是D:\python\workspace,如果使用相对路径,就可以不写这个路径,用一个“.”号代替这个路径值。
例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
print(f_name.name)
执行结果如下:
./test.txt
执行完程序后,到D:\python\workspace路径下查看,可以看到创建了一个名为test.txt的文件。
除了单个点(.),还可以使用两个点(..)表示父文件夹(或上一级文件夹)。此处不具体讨论,有兴趣可以自己尝试。
1 文件模式
我们在前面讲到,使用open函数时可以选择是否传入mode参数。在前面的示例中,mode传入了一个值为w的参数,这个参数是什么意思呢?mode可以传入哪些值呢?具体信息如下表所示。使用open函数时,明确指定读模式和什么模式都不指定的效果是一样的,我们在前面的示例已经验证。
使用写模式可以向文件写入内容。+参数可以用到其他任何模式中,指明读和写都是允许的。比如w+可以在打开一个文件时用于文件的读写。
当参数带上字母b时,表示可以用来读取一个二进制文件。Python在一般情况下处理的都是文本文件,有时也不能避免处理其他文件格式的文件。
2 缓冲
open函数的第3个参数是可选择的,该参数控制文件的缓存。如果该参数赋值为0或False, I/O(输入/输出)就是无缓存的。如果是1或True, I/O就是有缓存的。大于1的整数代表缓存的大小(单位是字节),-1或小于0的整数代表使用默认的缓存大小。
读者可能对缓存和I/O有些不明白。缓存一般指的是内存,计算机从内存中读取数据的速度远远大于从磁盘读取数据的速度,一般内存大小远小于磁盘大小,内存的速度比较快,但资源比较紧张,所以这里有是否对数据进行缓存的设置。
I/O在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据在内存中驻留,由CPU这个超快的计算核心执行,涉及数据交换的地方通常是磁盘、网络等,因此需要I/O接口。
比如打开浏览器,访问百度首页,浏览器需要通过网络I/O获取百度网页。浏览器首先会发送数据给百度服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output;随后百度服务器把网页发过来,这个动作是从外面接收数据,叫Input。通常,程序完成I/O操作会有Input和Output两个数据流。当然也有只用一个数据流的情况,比如从磁盘读取文件到内存,只有Input操作;反过来,把数据写到磁盘文件里,只有Output操作。
基本文件方法
上面介绍了打开文件的open函数,也做了一些简单操作,接下来介绍一些基本文件方法。在开始介绍之前,首先需要了解一下流的概念。
I/O编程中,Stream(流)是一个很重要的概念。可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。浏览网页时,浏览器和服务器之间至少需要建立两根水管,才能既发送数据又接收数据。
1 读和写
open函数返回的是一个File对象,有了File对象,就可以开始读取内容。如果希望将整个文件的内容读取为一个字符串值,可以使用File对象的read()
方法。
Read()
方法从一个打开的文件中读取字符串。需要注意,Python字符串可以是二进制数据,而不是仅仅是文字。语法如下:
fileObject.read([count]);
fileObject为open函数返回的File对象,count参数是从已打开的文件中读取的字节计数。该方法从文件的开头开始读入,如果没有传入count,就会尝试尽可能多地读取内容,很可能一直读取到文件末尾。
比如我们在test.txt文件中写入“Hello world!Welcome!”,执行如下代码:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path,'r')
print('read result:', f_name.read(12))
执行结果如下:
read result: Hello world!
由执行结果看到,通过read方法我们读取了文件中从头开始的12个字符串。
将print('read result:', f_name.read(12))
更改为print('read result:', f_name.read())
,得到的执行结果如下:
read result: Hello world!Welcome!
由执行结果看到,没有指定读取字节数时,read方法会读取打开文件中的所有字节。
除了读取数据外,我们还可以向文件中写入数据。在Python中,将内容写入文件的方式与print函数将字符串输出到屏幕上类似。
如果打开文件时使用读模式,就不能写入文件,即不能用下面这种形式操作文件:
open(path, 'rw')
在Python中,用write()方法向一个文件写入数据。write()
方法可将任何字符串写入一个打开的文件。需要注意,Python字符串可以是二进制数据,而不是仅仅是文字。
write()
方法不会在字符串结尾添加换行符('\n'),语法如下:
fileObject.write(string);
fileObject为open函数返回的File对象,string参数是需要写入文件中的内容。
该方法返回写入文件的字符串的长度。
例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
执行结果如下:
write length: 12
由执行结果看到,我们向test.txt文件中写入了12个字符。下面验证一下写入的是否是我们指定的字符,在上面的程序中追加两行代码并执行:
f_name = open(path,'r')
print('read result:', f_name.read())
执行结果如下:
write length: 12
read result: Hello world!
由执行结果看到,写入文件的是我们指定的内容。不过这里有一个疑问,我们在这里执行了两次写入操作,得到的结果怎么只写入了一次?
写文件(write)方法的处理方式是:将覆写原有文件,从头开始,每次写入都会覆盖前面所有内容,就像用一个新值覆盖一个变量的值。若需要在当前文件的字符串后追加字符,该怎么办呢?
可以将第二个参数w更换为a,即以追加模式打开文件,例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
f_name = open(path,'r')
print('read result:', f_name.read())
f_name = open(path, 'a')
print(add length:', f_name.write('welcome!'))
f_name = open(path,'r')
print('read result:', f_name.read())
执行结果如下:
write length: 12
read result: Hello world!
add length: 8
read result: Hello world!welcome!
由执行结果看到,输出结果在文件末尾成功添加了对应字符串。
注意:如果传递给open函数的文件名不存在,写模式(w)和追加模式(a)就会创建一个新的空文件,然后执行写入或追加。
如果想追加的字符串在下一行,该怎么办呢?
在Python中,用\n表示换行。对于上面的示例,若需要追加的内容在下一行,可以如下操作:
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
f_name = open(path,'r')
print('read result:', f_name.read())
f_name = open(path, 'a')
print(add length:', f_name.write('welcome!'))
f_name = open(path,'r')
print('read result:', f_name.read())
执行结果如下:
write length: 13
read result: Hello world!
add length: 8
read result: Hello world!
welcome!
由执行结果看到,后面追加的内容在下一行了。
提示:若需要读或写特定编码方式的文本,则需要给open函数传入encoding参数;若需要读取GBK编码的文件,则前面示例可以改写为f_name=open(path, 'r', encoding='gbk'),这样读取到的文件就是GBK编码方式的文件了。
2 读写行
我们目前对文件的读操作是按字节读或整个读取,而写操作是全部覆写或追加,这样的操作在实际应用中很不实用。Python为我们提供了readline()
、readlines()
和writelines()
等方法用于行操作,例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
f_name.write('Hello world!\n')
f_name = open(path, 'a')
f_name.write('welcome!')
f_name = open(path,'r')
print('readline result:', f_name.readline())
执行结果为:
readline result: Hello world!
由执行结果得知,readline方法会从文件中读取单独一行,换行符为\n。readline方法如果返回一个空字符串,说明已经读取到最后一行了。
readline方法也可以像read方法一样传入数值读取对应的字符数,传入小于0的数值表示整行都输出。
如果将上面示例的最后一行:
print('readline result:', f_name.readline())
更改为:
print('readline result:', f_name.readlines())
得到的输出结果为:
readline result: ['Hello world!\n', 'welcome!']
输出结果为一个字符串的列表。列表中的每个字符串就是文本中的每一行,并且换行符也会被输出。
readlines方法可以传入数值参数,当传入的数值小于等于列表中一个字符串的长度值时,该字符串会被读取;当传入小于等于0的数值时,所有字符都会被读取。
例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
str_list = ['Hello world!\n', 'welcome!\n', 'welcome!\n']
print('write length:', f_name.writelines(str_list))
f_name = open(path,'r')
print('read result:', f_name.read())
f_name = open(path,'r')
print('readline result:', f_name.readlines())
执行结果如下:
write length: None
read result: Hello world!
welcome!
welcome!
readline result: ['Hello world!\n', 'welcome!\n', 'welcome!\n']
由执行结果看到,writelines方法和readlines方法相反,传给它一个字符串列表(任何序列或可迭代对象),它会把所有字符串写入文件。如果没有writeline方法,就可以使用write方法代替这个方法的功能。
3 关闭文件
我们前面介绍了很多读取和写入文件的内容,都没有提到在读或写文件的过程中出现异常时该怎么处理。在读或写文件的过程中,出现异常的概率还是挺高的,特别对于大文件的读取和写入,出现异常更是家常便饭。在读或写文件的过程中,出现异常该怎么处理呢?
这就需要用到前面介绍的异常的知识了,用try语句捕获可能出现的异常。在捕获异常前有一个动作要执行,就是使用close方法关闭文件。
一般情况下,一个文件对象在退出程序后会自动关闭,但是为了安全起见,还是要显式地写一个close方法关闭文件。一般显示关闭文件读或写的操作如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
f_name.close()
这段代码和没有加close
方法的执行结果一样。这样处理后的函数比没有加close
方法时更安全,可以避免在某些操作系统或设置中进行无用的修改,也可以避免用完系统中所打开文件的配额。
对内容更改过的文件一定要记得关闭,因为写入的数据可能被缓存,如果程序或系统因为某些原因而崩溃,被缓存部分数据就不会写入文件了。为了安全起见,在使用完文件后一定要记得关闭。
当使用try语句出现异常时,即使使用了close方法,也可能不被执行,这时该怎么办呢?
还记得finally子句吗?可以将close方法放在finally子句中执行,从而保证无论程序执行是否正常都会调用close
方法。上面的示例可以更改成更安全的形式:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
try:
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
finally:
if f_name:
f_name.close()
如果每次都要这么写,就会很烦琐,是否有更简便的方式处理呢?
Python中引入了with语句自动帮我们调用close方法。可以使用with语句将上面的程序更改为:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
with open(path, 'w') as f:
f_name = open(path, 'w')
print('write length:', f_name.write('Hello world!'))
这段代码和上面使用try/finally的效果一样,并且会自动调用close方法,不用显式地写该方法。可以发现,代码比前面简洁多了,后面可以多用这种方式编写。
4 文件重命名
在应用程序的过程中,我们可能需要程序帮助我们重命名某个文件的名字,而不是通过手动的方式进行,这样是否可以呢?
Python的os模块为我们提供了rename方法,即文件重命名。使用这个方法需要导入os模块。rename方法的语法如下:
os.rename(current_file_name, new_file_name)
os为导入的os模块,current_file_name为当前文件名,new_file_name为新文件名。若文件不在当前目录下,则文件名需要带上绝对路径。
该方法没有返回值。
使用示例如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
open('./test1.txt', 'w')
os.rename('test1.txt','test2.txt')
执行结果可以到对应目录下查看,若之前已经创建了名为test1.txt的文件,则将文件名更改为test2.txt;若之前没有创建test1.txt文件,则先创建test1.txt文件,然后将文件名更改为test2.txt。
5 删除文件
在应用程序的过程中,我们是否可以通过程序删除某个文件呢?
Python的os模块为我们提供了remove方法,即删除文件。使用这个方法需要导入os模块。remove方法的语法如下:
os.remove(file_name)
os为导入的os模块,file_name为需要删除的文件名。若文件不在当前目录下,则文件名需要使用绝对路径。
该方法没有返回值。
使用示例如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
try:
print('remove result:', os.remove('test2.txt'))
except Exception:
print('file not found')
执行该方法会把前面的示例中重命名的test2.txt文件删除。当然,该方法只能删除已经存在的文件,文件不存在就会抛异常。
对文件内容进行迭代
前面介绍了文件的基本操作方法。在实际应用中,对文件内容进行迭代和重复执行操作是比较常见的操作。
所谓迭代,是指不断重复某一个动作,直到这些动作都完成为止。
1 按字节处理
在while循环中,read方法是最常见的对文件内容进行迭代的方法,例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
path = './test.txt'
f_name = open(path, 'w')
print('write length:', f_name.write('Hello'))
f_name = open(path)
c_str = f_name.read(1)
while c_str:
print('read str is:', c_str)
c_str = f_name.read(1)
f_name.close()
执行结果如下:
write length: 5
read str is: H
read str is: e
read str is: l
read strr is: l
read str is: o
由执行结果看到,该示例对写入文件的每个字符都进行循环了。这个程序运行到文件末尾时,read方法会返回一个空字符串,未执行到空字符串前,返回的都是非空字符,表示布尔值为真。
该示例中出现了代码的重复使用,可以使用while true/break语句结构进一步优化。优化代码如下:
f_name = open(path)
while True:
c_str = f_name.read(1)
if not c_str:
break
print('read str is:', c_str)
f_name.close()
由代码结构看到,更改后的代码比之前更好。
2 按行操作
在实际操作中,处理文件时可能需要对文件的行进行迭代,而不是单个字符。此时可以使用和处理字符一样的方式,只不过要使用readline方法,例如:
f_name = open(path)
while True:
line = f_name.readline(1)
if not c_str:
break
print('read line is:', line)
f_name.close()
使用该方式得到的是按行读取的字符。
3 使用fileinput实现懒加载式迭代
我们前面介绍过read方法和readlines方法,这两个方法不带参数时将读取文件中所有内容,然后加载到内存中。当文件很大时,使用这种方式会占用太多内存,甚至直接使内存溢出(内存不够),从而导致执行失败。这种情况下,我们可以考虑使用while循环和readline方法代替这些方法。
在Python中,for循环是优先考虑的选择,使用for循环意味着可以对任务进行分隔操作,而不是一步到位。
按行读取文件时,若能使用for循环,则称之为懒加载式迭代,因为在操作过程中只读取实际需要的文件部分。使用fileinput需要导入fileinput模块,例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import fileinput
for line in fileinput.input(path):
print('line is:', line)
在该示例中没有看到文件的打开与关闭操作,是怎么处理文件的呢?其实这些操作被封装在input方法内部了。
4 文件迭代器
从Python 2.2版本开始,文件对象是可迭代的,这意味着可以直接在for循环中使用文件对象,从而进行迭代,例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
f_name = open(path)
for line in f_name:
print('line is:', line)
f_name.close()
该示例使用for循环对文件对象进行迭代,记住迭代结束后要显式关闭文件。
StringIO
函数
数据的读取除了通过文件,还可以在内存中进行。Python中的io模块提供了对str操作的StringIO
函数。
要把str写入StringIO
,我们需要创建一个StringIO
,然后像文件一样写入。操作示例如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
from io import StringIO
io_val = StringIO()
io_val.write('hello')
print('say:', io_val.getvalue())
执行结果为:
say: hello
由执行结果看到,getvalue()
方法用于获得写入后的str。
要读取StringIO
,还可以用str初始化StringIO
,然后像读文件一样读取。操作示例如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
from io import StringIO
io_val = StringIO('Hello\nWorld!\nWelcome!')
while True:
line = io_val.readline()
if line == '':
break
print('line value:', line.strip())
执行结果如下:
line value: Hello
line value: World!
line value: Welcome!
序列化与反序列化
在运行程序的过程中,所有变量都在内存中,我们把变量从内存中变成可存储或传输的过程称为序列化。我们可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称为反序列化。
序列化是指将数据结构或对象转换成二进制串的过程。
反序列化是指将序列化过程中生成的二进制串转换成数据结构或对象的过程。
下面我们介绍Python中序列化和反序列化的方式。
1 一般序列化与反序列化
Python的pickle模块实现了基本数据序列和反序列化。
通过pickle模块的序列化操作,能够将程序中运行的对象信息保存到文件中,从而永久存储。
通过pickle模块的反序列化操作,能够从文件中创建上一次程序保存的对象。
pickle模块的基本接口如下:
pickle.dump(obj, file, [,protocol])
例如:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import pickle
d = dict(name='xiao zhi', num=1002)
print(pickle.dumps(d))
pickle.dumps()
方法把任意对象序列化成一个bytes,然后把这个bytes写入文件。也可以使用另一种方法pickle.dump()
,直接把对象序列化后写入一个文件对象中,程序如下:
try:
f_name = open('dump.txt', 'wb')
pickle.dump(d, f_name)
finally:
f_name.close()
打开dump.txt文件,可以看到里面是一堆看不懂的内容,这些都是Python保存的对象内部信息。
既然已经将内容序列化到文件中了,使用文件时就需要把对象从磁盘读到内存。可以先把内容读到一个bytes,然后用pickle.loads()
方法反序列化对象;也可以直接用pickle.load()
方法从一个文件对象中直接反序列化对象。从dump.txt文件中将序列化的内容反序列化的代码如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import pickle
try:
f_name = open('dump.txt', 'rb')
print('load result:', pickle.load(f_name))
finally:
f_name.close()
执行结果如下:
load result: {'num': 1002, 'name': 'xiao zhi'}
由执行结果看到,变量的内容被正确读取出来了。不过,虽然内容相同,但是对应的变量已经完全不同了。
注意:Pickle的序列化和反序列化只能用于Python,不同版本的Python可能彼此都不兼容,因此Pickle一般用于保存不重要的数据,也就是不能成功反序列化也没关系的数据。
2 JSON序列化与反序列化
我们在上线介绍的pickle模块是Python中独有的序列化与反序列化模块,本节介绍的JSON方式是通用的。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,是基于ECMAScript的一个子集。
Python 3中可以使用json模块对JSON数据进行编码解码,包含以下两个函数。
json.dumps():对数据进行编码。
json.loads():对数据进行解码。
在JSON的编码解码过程中,Python的原始类型与JSON类型会相互转换,具体的转化对照如表1和表2所示。
下面是JSON序列化与反序列化的示例:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import json
data = { 'num': 1002, 'name': 'xiao zhi'}
json_str = json.dumps(data)
print("Python 原始数据:", data)
print("JSON 对象:", json_str)
执行结果如下:
Python 原始数据: {'name': 'xiao zhi', 'num': 1002}
JSON 对象: {"name": "xiao zhi", "num": 1002}
接着以上示例,我们可以将一个JSON编码的字符串转换为一个Python数据结构,代码如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import json
data = { 'num': 1002, 'name': 'xiao zhi'}
json_str = json.dumps(data)
print("Python 原始数据:", data)
print("JSON 对象:", json_str)
data2 = json.loads(json_str)
print ("data2['name']: ", data2['name'])
print ("data2['num']: ", data2['num'])
执行结果如下:
Python 原始数据: {'num': 1002, 'name': 'xiao zhi'}
JSON 对象: {"num": 1002, "name": "xiao zhi"}
data2['name']: xiao zhi
data2['num']: 1002
如果要处理的是文件而不是字符串,就可以使用json.dump()和json.load()编码、解码JSON数据,进行如下处理:
# 写入 JSON 数据
with open('dump.txt', 'w') as f:
json.dump(data, f)
# 读取数据
with open('dump.txt', 'r') as f:
data = json.load(f)