第十一章 文件

第十一章 文件

(1)在pythonk 中打开文件非常的简单:

a_file = open(‘examples/chinese.txt’, encoding=’utf-8′)

  1. 不仅仅是文件名,它还包含了文件夹路径。可以写相对目录或绝对目录。
  2. 路径由斜线分隔,WINDOWS(反斜线\)和MAX OS X、LINUX(斜线/)的不同,但python中使用斜线可以很好的工作在windows和linux中。
  3. 如果路径不以盘符或斜线开头,那么这个路径是相对路径。
  4. 所有当前流行的操作系统使用unicode存储文件名字和路径。python3完全支持非ASCII路径。
  5. 路径也可以不是本地文件。你可以使用网络驱动器路径。这个文件可以是一个虚拟文件系统虚构的文件。只要你的系统允许你访问它。
  6. encoding指定用什么编码方式打开文件。

(2)字符编码

字符相对于字节是抽象的。一个字符串是一个unicode字符序列。但是磁盘上的文件不是一串unicode字符;在磁盘上的文件是一串字节的排序。所以当python读磁盘文件时如何把它转换成一串字符?这就要根据字符编码方式。

如果没有指定编码方式,那么会使用系统默认的编码方式。

(3)流对象

现在我们已经知道python内建函数被称作open()。open()函数返回一个流对象,它有一些操控字符的方法和属性,用于获得信息。

(4)从一个文件中读数据

>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.read() ①
'Dive Into Python 是为有经验的程序员编写的一本 Python 书。\n'
>>> a_file.read() ②
''

  1. 一但你使用正确的编码打开了一个文件,使用read()可以读它的内容,read()返回一个字符串。
  2. 当到达文件结尾时,继续读下去不会出错,只会返回一个空字符串。

如果要重读一个文件怎么办?

>>> a_file.read() ①
''
>>> a_file.seek(0) ②
0
>>> a_file.read(16) ③
'Dive Into Python'
>>> a_file.read(1) ④
' '
>>> a_file.read(1)
'是'
>>> a_file.tell() ⑤
20

  1. 在文件的结尾使用read(),只会返回空字符串。
  2. seek()方法移动到文件的指定字节位置。
  3. read()可以带一个可选的参数来指定要读多少个字符。
  4. 只要你喜欢,甚至你可以一次只读一个字符。
  5. tell()返回当前字节位置。

你应该看到seek()和tell()方法一直以字节为单位,但是只要你以文件的方式打开,read()方法以字符为单位。中文字符在utf-8编码中需要多个字节。而每个英语字符在文件中只要一个字节,所以不要被误导了!这就是为什么例子中读了16+1+1个字符后却在第20个字节处的原因。

看一下这个例子:

>>> a_file.seek(18) ①
18
>>> a_file.read(1) ②
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
a_file.read(1)
File "C:\Python31\lib\codecs.py", line 300, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x98 in position 0: unexpected code byte

  1. 移动到第18个字节处。
  2. 为什么会出错?因为第18个字节不是一个完整的字符,最近的字符是在第17个字节处开始的。尝试在一个字符中间读会出错。

(5)关闭文件

打开的文件占用了系统的资源,这依赖于打开文件的模式,可能会导至别的程序无法打开它们。在使用完文件后关闭它们,这是非常重要的。方法就是在流对象上使用close()方法。

  1. 读已经关闭的文件,会引发IOError异常。
  2. 同样不能seek
  3. 因为文件已经关闭了,所以没有当前位置,也就是不能用tell()方法!
  4. 在一个已经关闭的文件上调用close()不会出错。
  5. 流对象有一个很有用的属性——closed!它用来确定文件已经关闭了,它是一个布尔值。

(6)自动关闭文件

如果显示调用close()之前出现了BUG或程序崩溃了!那文件点用的资源可能会在系统中继续保存一段时间。在python2中的解决方案是使用try…finally块。这在python3中也可以用,自从python2.6引入了with语句后,使这变的更简单了。

with open('examples/chinese.txt', encoding='utf-8') as a_file:
    a_file.seek(17)
    a_character = a_file.read(1)
    print(a_character)

上面这段程序使用open()打开了一个文件,但不需要使用close()关闭文件。with语句开始了一个代码块,就像if或for语句一样,使用a_file做为open()函数返回的流对象。所有流对象方法都是可以使用在这个变量上——seek(),read(), 等等。当with语句块结束时python自动调用流对象上的close()方法。它的优点是无论什么请款都能自动的调用close(),无论是崩溃还是异常。

从技术上讲,with语句创建了“运行时刻的上下文”。在上面的例子中流对象做为“上下文管理器”。python创建a_file流对象并且告诉它,它正在进入一个“运行时刻的上下文”。当离开with块时,流对象调用自己的close()方法关闭“上下文”。

(7)一次读取一行数据

不同的系统有不同的换行符,用python读取一行,不需要手动设置换行符,python可以自动处理一行的结束符。不过如果你手动设置告诉python换行符是什么,要在open()函数中使用newline参数。

line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file: ①
    for a_line in a_file: ②
    line_number += 1
    print('{:>4} {}'.format(line_number, a_line.rstrip())) ③

  1. 使用with模式安全的打开一个文档。
  2. 在for循环中读文件一次一行。流对象同样也是一个迭代器它可以每次从文件中获得一行数据。
  3. {:>4}意味着打印这个参数右边留出4个空格位,而字符串的rstrip()意味着移除结尾的空格(包含换行符)。

(8)向文件中写入

写入和读取一样简单,写入文件时,有两种文件打开模式:

  • ”写“模式:会覆盖原文件。传递mode='w'open()函数。
  • ”追加“模式:会添加数据到文件的结尾。传递mode='a'open()函数。

如果文件不存在,那么会创建一个文件。应该一直保持着写完后立即关闭文件,去释放文件句柄并且确定数据已经写入到磁盘了。

>>> with open('test.log', mode='w', encoding='utf-8') as a_file: ①
...     a_file.write('test succeeded') ②
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())
test succeeded
>>> with open('test.log', mode='a', encoding='utf-8') as a_file: ③
...     a_file.write('and again')
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())
test succeededand again ④

(9)还是字符编码(这回是写入)

你是否注意到encoding参数在为了写入而打开文件时被传递给open()函数?这很重要,不要扔下它!!就像在本章开头写的那样,文件中保存的不是字符而是字节。可以从文件中读出字符串的原音是,encoding参数指定了解码方式。写入也有同样的问题,不能以字符方式写入一个文件,“字符”是一种抽象的概念!

(10)二进制文件

>>> an_image = open('examples/beauregard.jpg', mode='rb') ①
>>> an_image.mode ②
'rb'
>>> an_image.name ③
'examples/beauregard.jpg'
>>> an_image.encoding ④
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: '_io.BufferedReader' object has no attribute 'encoding'

  1. 以二进制方式打开文件,mode=’b’。
  2. 以二进制方工打开文件得到的流对象有一样多的属性,包含mode,它映射到传入open()函数的那个值。
  3. 二进制文件一样有name属性,和文本流文件的name是一样的。
  4. 有一点不同,二进制流对象没有encoding属性。因为不需要转换成某种格式的字符,所以没有encoding。

那如何读二进制文件呢?

# continued from the previous example
>>> an_image.tell()
0
>>> data = an_image.read(3) ①
>>> data
b'\xff\xd8\xff'
>>> type(data) ②
<class 'bytes'>
>>> an_image.tell() ③
3
>>> an_image.seek(0)
0
>>> data = an_image.read()
>>> len(data)
3150

  1. 和文本文件一样可以读二进制文件,一次读出一些!但是这和文本文件完全不一样!
  2. 读出的是字节而不是字符。
  3. 这意味着绝对不会有意料之外的未匹配,如seek()到一个中文字符的中间而导至不能读出,或是因为一个中文字符占用了多个字节,用tell()定位当前位置和读出的字符数不符。在二进制文件中seek()和tell()方法返回的字符数就是字节数。

(11)来自非文件源的流对象

想象一下你写了一个库,并且这个库中的一个函数打算从一个文件中读一些数据。这个函数可以简单的打开一个文件,读它,关闭它。但是你没有这么做,而是你的API使用了一个任意的流对象。

在这个简单的例子中,流对象可以用read()读出任何东西。read()函数有一个可选的参数size,它指定了一次最多读出多少个字符,如果没有这个参数,read()会读出全部字符做为一个字符串返回。

这听起来十分像从一个真实文件打开的流对象。但不同的是,我们并没有限制这是一个真实的文件。这个输入源可以是任可能被“读”的东西:web页面、内存中的字符串、甚至是其它程序的输出。只要你的程序有一个流对象并且简单的调用流对象的read()方法,就可以像处理文本文件一样处理任何输入源,不需要任何特别的代码去处理不同类型的输入。

>>> a_string = 'PapayaWhip is the new black.'
>>> import io ①
>>> a_file = io.StringIO(a_string) ②
>>> a_file.read() ③
'PapayaWhip is the new black.'
>>> a_file.read() ④
''
>>> a_file.seek(0) ⑤
0
>>> a_file.read(10) ⑥
'PapayaWhip'
>>> a_file.tell()
10
>>> a_file.seek(18)
18
>>> a_file.read()
'new black.'

  1. io模块定义了StringIO类,它可以让你像处理文件一样处理内存中的字符串。
  2. 根据字符串创建一个流对象;根据字符串创建了一个io.StringIO()实例,并把字符串做为“文件”的数据。之后你就可以像其它流对象一样对它进行操作了。
  3. 调用read()方法,读出全部内容,在StringIO对象的例子中,简单的返回原始字符串。
  4. 就和读取真实文件一样,当没有字符可读了,就返回空串。
  5. 可以使用seek()函数,就像在真实文件上的用法一样。
  6. 同样可以一次读取一大块数据,通过read()函数的size参数。

io.StringIO让你处理字符串时像是对待文本文件一样。同样io.BytesIO类也会让你处理字节数组像是二进制文件一样。

(12)处理压缩文件

python标准库包含了读写压缩文件的模块。

gzip模块允许你创建一个用于读写gzip压缩文件的流对象。之后就可以像操作文本文件一样用read()和write()进行读写,而不用产生临时文件。gzip流对象可以使用with语句。

>>> import gzip
>>> with gzip.open('out.log.gz', mode='wb') as z_file: ①
...     z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8'))
...
>>> exit()
you@localhost:~$ ls -l out.log.gz ②
-rw-r--r-- 1 mark mark 79 2009-07-19 14:29 out.log.gz
you@localhost:~$ gunzip out.log.gz ③
you@localhost:~$ cat out.log ④
A nine mile walk is no joke, especially in the rain.

  1. 打开一个gzip文件,应该一直以二进制方式打开,mode=’b’
  2. 文件大小为79个字节,但实际上这要比我们写入的字符长很多。这是因为gzip格式包含了固定长度的头文件,它包含了一些关于文件的meta数据,所以用gzip格式打包特别小的文件时,效率很低。
  3. gunzip命令可以解压一个gzip文件,名子和原文件一样只不过没有gz扩展名。
  4. cat命令显示文件内容。

(13)标准输出和错误

sys.stdout和sys.stderr都是流对象,不过它们是只写的,对他们使用read()方法会引发异常。

(14)重定向标准输出

sys.stdout和sys.stderr都是流对象,但是只支持写操作。如果对它们赋予一个新值——另一个流对象——就是重定向他们到输出。

import sys
class RedirectStdoutTo:
    def __init__(self, out_new):
        self.out_new = out_new
    def __enter__(self):
        self.out_old = sys.stdout
        sys.stdout = self.out_new
    def __exit__(self, *args):
        sys.stdout = self.out_old
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

检查输出:
you@localhost:~/diveintopython3/examples$ python3 stdout.py
A C
you@localhost:~/diveintopython3/examples$ cat out.log
B

让我们先看一下最后一部分

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

将这个复杂的with语句重写成更容易理理解的方式:

with open('out.log', mode='w', encoding='utf-8') as a_file:
    with RedirectStdoutTo(a_file):
        print('B')

正如重写的那样,实际上有两个with语句,一个嵌套到另一个里面。外层的with语句我们应该比较熟悉了:它以UTF-8编码方式打开了一个名为out.log文件,打开模式为“写”模式,并分配了一个名为a_file的流对象给它。但这还没完。。。

    with RedirectStdoutTo(a_file):

这句没有as分支!实际上with语句并不需要它。就像调用一个程序并忽略它的返回值,而在with语句中可以不分配“上下文”到一个变量上。在本例中只对RedirectStoutTo“上下文”的副作用(side effect)感兴趣。

它的副作用是什么呢?让我们看一下RedirectStdoutTo类。这个类是一个“上下文管理器”。只要定义了__enter__()和__exit__()这两个方法的类,都可以做为“上下文管理器”。

class RedirectStdoutTo:
    def __init__(self, out_new):     ①
        self.out_new = out_new
    def __enter__(self):         ②
        self.out_old = sys.stdout
        sys.stdout = self.out_new
    def __exit__(self, *args):      ③
        sys.stdout = self.out_old

  1. __init__()方法在实例建立后立即执行。本例中它使用一个流对象参数,你想使用它做为“上下文”生存期的标准输出。这个方法仅在例中保存了流对象,用于其它方法以后使用它。
  2. __enter__()方法是一个特殊的类方法。当进入“上下文”时执行(也就是在开始with语句时)。本例中这个方法保存了当前sys.stdout的值在self.out_old中,之后通过self.out_new赋给sys.stdout重定向了标准输出。
  3. __exit__()方法是另一个特殊的类方法。当离开“上下文”时执行(也就是在结束with语句时)。在本例中这个方法通过把之前保存的self.out_old值赋给sys.stdout来恢复标准输出到原来的值。

 

发表评论