文件读写

读文件

使用Python内置的open()函数,传入文件名和标示符:

  1. # 标示符'r'表示读
  2. f = open('/User/michael/test.txt', 'r') # 这样就成功打开了一个文件
  3. r.read() # 调用read()方法一次读取文件全部内容
  4. # Python把内容堵到内存用一个str对象表示
  5. f.close() # 最后一步是调用close()方法关闭文件
  6. # 如果文件不存在,就会抛出一个 IOError 的错误
  7. # Traceback (most recent call last):
  8. # File "<stdin>", line 1, in <module>
  9. # FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'

由于文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用try ... finally来实现:

  1. try:
  2. f = open('/path/to/file', 'r')
  3. print(f.read())
  4. finally:
  5. if f:
  6. f.close()

Python引入with语句来自动帮我们调用close()方法:

  1. # 同 try...finally,但更简洁且不必调用f.close()方法
  2. width open('/path/to/file', 'r') as f:
  3. print(f.read())

调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,
可以反复调用**read(size)**方法,每次最多读取**size**个字节的内容
另外,调用**readline()**可以每次读取一行内容调用**readlines()**一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
如果文件很小,**read()**一次性读取最方便;如果不能确定文件大小,反复调用**read(size)**比较保险;如果是配置文件,调用**readlines()**最方便:

  1. for line in f.readlines():
  2. print(line.strip()) # 把末尾的'\n'删掉

file-like Object

open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。
StringIO就是在内存中创建的file-like Object,常用作临时缓冲。

二进制文件

前面讲的默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用**'rb'**模式打开文件即可

  1. f = open('/User/michael/test.jpg', 'rb')
  2. f.read()
  3. # b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节

字符编码

要独处非UTF-8编码的文本文件,需要给open()函数传入**encoding**参数

  1. >>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')
  2. >>> f.read()
  3. '测试'

遇到有些编码不规范的文件,可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个**errors**参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:

  1. f = open('/User/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')

写文件

和读文件一样,区别是调用open()函数时,传入标识符'w'或者'wb'表示写文本文件或者二进制文件:

  1. f = open('/User/michael/test.txt', 'w')
  2. f.write('Hello, world!')
  3. f.close()

可以反复调用write()来写入文件,但是务必调用f.close()来关闭文件。

  1. with open('/User/michael/test.txt', 'w') as f:
  2. f.write('Hello, world!')

要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。

'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果希望追加到文件末尾,可以传入'a'以追加(append)模式写入。

Character Meaning
'r' open for reading (default) 开放阅读(默认)
'w' open for writing, truncating the file first
打开写入,先截断文件
'x' open for exclusive creation, failing if the file already exists
打开独占创建,如果文件已经存在则失败
'a' open for writing, appending to the end of the file if it exists
打开以进行写入,如果存在则附加到文件末尾
'b' binary mode 二进制模式
't' text mode (default) 文本模式(默认)
'+' open for updating (reading and writing) 开放更新(读取和写入)

默认模式是'r'(打开阅读文本,同义词'rt')。模式'w+''w+b'打开和截断文件。模式'r+''r+b'打开文件而不会被截断。

StringIO和ByteslO

数据读写不一定是文件,也可以在内存中读写。

StringIO

StringIO就是在内存中读写str
要把str写入StringIO需要先创建一个StringIO,然后像文件一样写入:

  1. from io import StringIO
  2. f = StringIO
  3. f.write('hello') # 5
  4. f.write(' ') # 1
  5. f.write('world!') # 6
  6. print(f.getvalue()) # hello, world!

getvalue()方法用于获得写入后的str。

要读取StringIO,可以用一个str初始化StringIO,然后,像读文件一样读取:

  1. from io import StringIO
  2. f = StringIO('Hello!\nHi!\nGoodbye!')
  3. while True:
  4. s = f.readline()
  5. if s == '':
  6. break
  7. print(s.strip())
  8. # Hello!
  9. # Hi!
  10. # Goodbye!

BytesIO

StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:

  1. from io import BytesIO
  2. f = BytesIO()
  3. s.write('中文'.encode('utf-8')) # 6
  4. print(f.getvalue()) # b'\xe4\xb8\xad\xe6\x96\x87'

请注意,写入的不是str,而是经过UTF-8编码的bytes。

和StringIO类似,可以用一个bytes初始化BytesIO,然后,像读文件一样读取:

  1. from io import BytesIO
  2. f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
  3. f.read()
  4. # b'\xe4\xb8\xad\xe6\x96\x87'

操作文件和目录

使用Python内置的os模块直接调用操作系统提供的接口函数。

  1. >>> import os
  2. >>> os.name # 操作系统类型
  3. 'posix'

如果是posix,说明系统是LinuxUnixMac OS X,如果是nt,就是Windows系统。

环境变量

在操作系统中定义的环境变量,全部保存在os.environ这个变量中,可以直接查看:

  1. >>> os.environ
  2. environ({'VERSIONER_PYTHON_PREFER_32_BIT': 'no', 'TERM_PROGRAM_VERSION': '326', 'LOGNAME': 'michael', 'USER': 'michael', 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin', ...})

要获取某个环境变量的值,可以调用os.environ.get('key')

  1. >>> os.environ.get('PATH')
  2. '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin'
  3. >>> os.environ.get('x', 'default')
  4. 'default'

操作文件和目录

操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块中,这一点要注意一下。
查看、创建和删除目录可以这么调用:

  1. # 查看当前目录的绝对路径
  2. >>> os.path.abspath('.')
  3. '/Users/michael'
  4. # 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:
  5. >>> os.path.join('/User/michael', 'testdir')
  6. '/Users/michael/testdir'
  7. # 然后创建一个目录:
  8. >>> os.mkdir('/User/micheal/testdir')
  9. # 删掉一个目录
  10. >>> os.rmdir('/User/micheal/testdir')

把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()函数,这样可以正确处理不同操作系统的路径分隔符。
在Linux/Unix/Mac下,os.path.join()返回part-1/part-2,而Windows下会返回part-1\part-2

要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名:

  1. >>> os.path.split('/User/michael/testdir/file.txt')
  2. ('/User/michael/testdir', 'file.txt')

os.path.splitext()可以让你直接获得文件扩展名:

  1. >>> os.path.splitext('/path/to/file.txt')
  2. ('/path/to/file', '.txt')

这些合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作。

文件操作使用下面的函数。假定当前目录下有一个test.txt文件:

  1. # 对文件重命名:
  2. >>> os.rename('test.txt', 'test.py')
  3. # 删掉文件:
  4. >>> os.remove('test.py')

shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。

  1. # 用Python的特性过滤文件,比如列出当前目录下的所有目录:
  2. >>> [x for x in os.listdir('.') if os.path.isdir(x)]
  3. # 要列出所有的.py文件,也只需一行代码:
  4. >>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1] == '.py']

序列化

Python提供了pickle模块来实现序列化。

  1. >>> import pickle
  2. >>> d = dict(name='Bob', age=20, score=88)
  3. >>> pickle.dumps(d)
  4. b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'

pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。
或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:
(注意:一个有s一个没spickle.dumps() pickle.dump()

  1. >>> f = open('dump.txt', 'wb')
  2. >>> pickle.dump(d, f)
  3. >>> f.close()

看看写入的dump.txt文件,一堆乱七八糟的内容,这些都是Python保存的对象内部信息。

要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:

  1. >>> f = open('dump.txt', 'rb')
  2. >>> d = pickle.load(f)
  3. >>> f.close()
  4. >>> d
  5. {'age': 20, 'score': 88, 'name': 'Bob'}

Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。

JSON

JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:

JSON类型 Python类型
{} dict
[] list
“string” str
1234.56 int或float
true/false True/False
null None

如何把Python对象变成一个JSON:

  1. >>> import json
  2. >>> d = dict(name='Bob', age=20,l score=88)
  3. >>> json.dumps(d)
  4. '{"age": 20, "score": 88, "name": "Bob"}'

dumps()方法返回一个str,内容就是标准的JSON。类似的,dump()可以直接把JSON写入一个file-like Object

要把JSON反序列化为Python对象,用loads()或者对应的load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:

  1. >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
  2. >>> json.loads(json_str)
  3. {'age': 20, 'score': 88, 'name': 'Bob'}

JSON进阶

Python的dict对象可以直接序列化为JSON的{}, 不过很多时候更喜欢用class表示对象:

  1. import json
  2. class Student(object):
  3. def __init__(self, name, age, score):
  4. self.name = name
  5. self.age = age
  6. self.score = score
  7. s = Student('Bob', 20, 88)
  8. print(json.dumps(s))
  9. # Traceback (most recent call last):
  10. # ...
  11. # TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable

运行代码报错,因为Student对象不是一个可序列话的JSON对象, 默认情况下,dumps()方法不知道如何将Student实例变成一个JSON的{}对象。
除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数
可选参数default就是把任意一个对象变成可序列为JSON的对象,只需要为Student专门写一个转换函数,再把函数传进去:

  1. def student2dict(std):
  2. return {
  3. 'name': std.name,
  4. 'age': std.age,
  5. 'score': std.score
  6. }

这样,Student实例首先被student2dict()函数转换成dict,然后再被顺利序列化为JSON:

  1. >>> print(json.dumps(s, default=student2dict))
  2. {"age": 20, "name": "Bob", "score": 88}

不过下次在遇到类似的实例照样无法序列化为JSON。我们可以偷个懒把任意class的实例变成dict

  1. print(json.dumps(s, default=lambda obj: obj.__dict__))

因为通常class的实例都有一个__dict__属性,他就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slot__的class。

JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后传入的object_hook函数负责把dict转换成Student实例:

  1. def dict2student(d):
  2. return Student(d['name'], d['age'], d['score'])

运行结果如下:

  1. >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
  2. >>> print(json.loads(json_str, object_hook=dict2student))
  3. <__main__.Student object at 0x10cd3c190> # 打印出的是反序列化的Student实例对象。