本章会涉及不同类型的数据存储,它们基于不同的目的进行优化:普通文件、结构化文件和数据库。
普通文件输入/输出
数据持久化最简单的类型是普通文件,有时也叫平面文件(flat file)。它仅仅是在一个文件名下的字节流,把数据从一个文件读入内存,然后从内存写入文件。
打开文件
# fileobj是open()返回的文件对象
# filename是该文件的字符串名
# mode是指明文件类型和操作的字符串
fileobj = open(filename, mode)
mode的第一个字母表明对其的操作
- r表示读模式。
- w表示写模式。如果文件不存在则新创建,如果存在则重写新内容。
- x表示在文件不存在的情况下新创建并写文件。
- a表示如果文件存在,在文件末尾追加写内容。
mode的第二个字母是文本类型
- t(或者省略)代表文本类型。
- b代表二进制文件。
写文本文件
使用write()
sa = 'Never\nGive\nUp'
sb = 'Try\nMy\nBest'
fout = open('record', 'wt')
fout.write(sa)
fout.write(sb)
fout.close()
结果
# record1
# 注意Up与Try首尾相接
Never
Give
UpTry
My
Best
使用print()
sa = 'Never\nGive\nUp'
sb = 'Try\nMy\nBest'
fout = open('record', 'wt')
print(sa, file = fout)
print(sb, file = fout)
fout.close()
结果
# record2
# 注意Up与Try首尾有换行
Never
Give
Up
Try
My
Best
print()默认会在每个参数后添加空格,在每行结束处添加换行。这两个特性可以由sep和end两个参数设置。
- sep分隔符:默认是一个空格’ ‘
- end结束字符:默认是一个换行符’\n’
因此,下面的print等价于write
print(sa, file = fout, sep='', end='')
print(sb, file = fout, sep='', end='')
注意
- write函数的参数一定要是字符串,writelines函数的参数一定要是字符串或字符串列表,print函数的参数是任意可以被str函数转化的类型。
- writelines函数写出列表时,不会给列表中每个字符串元素自动添加换行符。如[‘ab’, ‘cd’],则只会输出abcd,不会输出
ab
cd
写出列表[1, 2, ‘ab’, ‘efg’]
# 需要结果
1
2
ab
efg
显然,直接用write、writelines以及print都是不可行的。
方法1:write
lines = [1, 2, 'ab', 'efg']
with open('filename.txt', 'w') as f:
# [str(line) for line in lines]生成字符串列表
# '\n'.join生成一个中间由'\n'连接的字符串
f.write('\n'.join([str(line) for line in lines]))
方法2:writelines
lines = [1, 2, 'ab', 'efg']
with open('filename.txt', 'w') as f:
# [str(line)+'\n' for line in lines]生成字符串列表,每个字符串后面有换行符'\n'
f.writelines([str(line)+'\n' for line in lines])
将数据分块写入
fout = open('record2', 'wt')
sentence = sa + sb
size = len(sentence)
offset = 0
chunk = 100
while True:
if offset > size:
break
fout.write(sentence[offset:offset+chunk])
offset += chunk
使用模式x避免重写文件
try:
fout = open('record2', 'xt')
fout.writ(sentence)
except FileExistsError:
print('record already exists!')
使用read()、readline()或者readlines()读文本文件
使用read()
利用read()读入文件时,1GB的文件会用到同样大小的内存
# record2
Never
Give
Up
Try
My
Best
# read()会把整个文件以字符串的形式读入
# '\n'也算一个字符
fin = open('record2', 'rt')
sentence = fin.read()
fin.close()
print(type(sentence))
结果:
<class 'str'>
同样也可以设置最大的读入字符数限制read()函数一次返回的大小。下面一次读入100个字符,然后把每一块拼接成原来的字符串sentence:
sentence = ''
fin = open('record2', 'rt')
chunk = 100
while True:
fragment = fin.read(chunk)
if not fragment:
break
sentence += fragment
fin.close()
使用readline()
readline()每次读入文件的一行。对于一个文本文件,即使空行也有1字符长度(换行字符’\n’),自然就会返回True。当文件读取结束后,readline()(类似read())同样会返回空字符串。
# record1
Never
Give
UpTry
My
Best
fin = open('record1', 'rt')
sentence = ''
while True:
line = fin.readline()
print(len(line))
if not line :
break
sentence += line
fin.close()
print(sentence)
结果:
# 记住换行符也是一个普通的字符
6 # '\n'也在里面
5
6
3
4 # 没有'\n'
0
Never
Give
UpTry
My
Best
更好的用法
sentence = ''
fin = open('record', 'rt')
for line in fin:
sentence += line
fin.close()
使用readlines()
函数readlines()调用时读入所有行,并返回单行字符串的列表。
fin = open('record1', 'rt')
lines = fin.readlines()
fin.close()
print lines
结果:
['Never\n', 'Give\n', 'UpTry\n', 'My\n', 'Best']
使用write()写二进制文件
bdata = bytes(range(0, 256))
print(len(bdata))
fout = open('bfile', 'wb')
blen = fout.write(bdata)
# blen为write()写入的字节数
print(blen)
fout.close()
分块写入
fout = open('bfile', 'wb')
size = len(bdata)
offset = 0
chunk = 100
while True:
if offset > size:
break
fout.write(bdata[offset:offset+chunk])
offset += chunk
fout.close()
使用read()读二进制文件
fin = open('bfile', 'rb')
bdata = find.read()
print(len(bdata))
fin.close()
使用with自动关闭文件
如果你忘记关闭已经打开的一个文件,在该文件对象不再被引用之后Python会关掉此文件。
这也就意味着在一个函数中打开文件,没有及时关闭它,但是在函数结束时会被关掉。
Python的上下文管理器(context manage)(什么鬼?)会清理一些资料,例如打开的文件。它的形式为with expression as variable:
# fout = open('record', 'wt')
with open('record', 'wt') as fout:
fout.write(poem)
完成上下文管理器的代码后,文件会被自动关闭。
使用seek()改变位置(基于字节)
无论是读或者写文件,Python都会跟踪文件中的文件。
函数tell()返回距离文件开始处的字节偏移量。
函数seek()允许跳转到文件其他字节偏移量的位置。这意味着可以不用从头读取文件的每一个字节,直接跳到最后位置并只读一个字节也是可行的。
这些函数对于二进制文件都是极其重要的。当文件是ASCII编码(么个字符一个字节)时,也可以使用它们,但是计算偏移量回事一件麻烦事。其实,这都取决于文本的编码格式,UTF-8每个字符的字节数都不尽相同。
>>> b'abc'[0]
97
>>> fin = open('bfile', 'rb')
>>> fin.tell()
0
>>> # 初始字节偏移量为0,最后字节偏移量为255
>>> # 使用seek()跳转到文件结束前最后一个字节
>>> fin.seek(255)
255 #255是指当前的偏移量
>>> # 读取最后一个字节
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
seek(offset, origin)
- 如果origin等于0(默认为0),从开头偏移offset个字节;
- 如果origin等于1,从当前位置处偏移offset个字节;
- 如果origin等于2,距离最后结尾处(指的是所有数据后的一个字节的位置)偏移offset个字节;
上述值也在标准os模块中被定义:
>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2
读取bfile最后一个字节的方法
方法一:
fin.seek(-1, 2)
bdata = fin.read()
方法二:
fin.seek(255, 0)
bdata = fin.read()
方法三:
fin.seek(254, 0)
fin.seek(1, 1)
bdata = fin.read()
结构化的文本文件输入/输出
结构化的文本文件
对于简单的文本文件,唯一的结构层次是间隔的行。
结构化的文本有很多格式,区别它们的方法如下所示:
- 分隔符,比如tab(‘\t’)、逗号(‘,’)或者竖线(‘|’)。逗号分隔值(CSV)就是这样的例子。
- ‘<’和’>’标签,例如XML和HTML
- 标点符号,例如JavaScript Object Notation ( JSON2 J S O N 2 )
- 缩进,例如YAML
- 混合的,例如各种配置文件
CSV
带分隔符的文件一般用作数据交换格式或者数据库。
你可以人工读入CSV文件,每一次读取一行,在逗号分隔符处将每行分开,并添加结果到某些数据结构中。
但是,最好使用标准的csv模块,因为这样切分会得到更加复杂的信息。
- 除了逗号,还有其他可代替的分隔符: ‘|’和’\t’很常见。
- 有些数据会有转义字符序列,如果分隔符出现在一块区域内,则整块都要加上引号或者在它之前加上转义字符。
- 文件可能有不同的换行符,Unix系统的文件使用’\n’,Microsoft使用’\r\n’,Apple之前使用’\r’而现在使用’\n’
- 在第一行可以加上列名
写出CSV
import csv
data = [\
['Doctor', 1, 1.01, True, 'a,a,,', [1], [1,2], [1, 'a'], ['a']],\
['Teacher', 'a', 2.02, False, 'b,b,,']]
with open('data', 'wt') as fout: # 一个上下文管理器
csvout = csv.writer(fout)
csvout.writerows(data)
# data
Doctor,1,1.01,True,"a,a,,",[1],"[1, 2]","[1, 'a']",[