大多数程序都需要向文件中存储或从文件中加载信息,比如数据或状态信息。本文将深入全面地介绍文件处理的相关知识与方法。
哪种文件格式最适合用于存储整个数据集——二进制、文本还是XML?这严重依赖于具体的上下文。
1、文件操作函数
1.1
open()
提到文件操作,那就必须提到 open 函数,因为无论是读取还是写入,都需要先把文件打开,然后才能进行读写操作。 open 函数的作用是打开一个文件,返回一个 file 对象,相关的方法才可以调用它进行读写。其语法如下:
file_object = open(file_name, [,access_mode][, buffering])
测试一下 open() 函数:
file_object = open('test.txt')
print(file_object)
file_object.close()
[out]
<_io.TextIOWrapper name='test.txt' mode='r' encoding='cp936'>
从输出结果可以看出,默认打开模式为 'r' ,下面来详细介绍文件打开模式:
模式 | 描述 |
---|---|
r | 以只读方式打开文件。文件指针将会放在文件的开头。这是默认模式。 |
w | 打开一个文件只用于写入。如果该文件存在,则将其覆盖;不存在则创建。 |
a | 打开一个文件用于追加。如果该文件存在,文件指针将放在文件的结尾;不存在则创建。 |
r+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
rb | 以二进制形式打开一个文件用于只读。文件指针将会放在文件的开头,一般用于非文本文件。 |
rb+ | 以二进制形式打开一个文件用于读写。文件指针将会放在文件的开头。 |
w+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
wb | 以二进制形式打开一个文件只用于写入。文件存在则覆盖,不存在则创建。 |
wb+ | 以二进制形式打开一个文件读写。文件存在则覆盖,不存在则创建。 |
a+ | 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。 |
ab | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件进行写入 。 |
ab+ | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。 |
1.2
write()
write()方法可将任何字符串写入一个打开的文件。需要重点注意的是,Python字符串可以是二进制数据,而不是仅仅是文字。
write()方法不会在字符串的结尾添加换行符('\n'):
fo = open("test.txt", "w")
fo.write( "This is a test.\nwrite function.\n")
fo.close()
打开 test.txt 文件,可以看到,文件中有两行文字,正是刚刚写入的。
02.write()函数测试结果
1.3
read()
read()方 法从一个打开的文件中读取一个字符串。需要重点注意的是,Python字符串可以是二进制数据,而不是仅仅是文字。 read() 在未指定参数的情况下读取整个文件,如果传入一个参数,则读取指定个数的字节。
# read()
fo = open("test.txt", "r")
txt = fo.read()
print(txt)
fo.close()
[out]
This is a test.
write function.
注意:read() 在到达文件末尾时返回一个空的字符串,这个空字符串显示出来就是一个空行,所以上面的输出最后有一个空行。
1.4
close()
文件对象的 close(0 方法关闭一个已经打开的文件,关闭后不能再对该文件对象进行读写操作。
当一个文件对象的引用被重新指定给另一个文件时,Python 会关闭之前的文件。用 close() 方法关闭文件是一个很好的习惯。
2、二进制数据的读写
即便在没有进行压缩处理的情况下,二进制格式通常也是占据磁盘空间最小、保存与加载速度最快的数据格式。最简单的方法是使用 pickles,尽管对二进制数据进行手动处理应该会产生更小的文件。
2.1
带可选压缩的Pickle
Pickle模块实现了基本的数据序列和反序列化。Python中几乎所有的数据类型(列表,字典,集合,类等)都可以用Pickle来序列化, 通过Pickle模块的序列化操作我们能够将程序中运行的对象信息保存到文件中去,永久存储;通过Pickle模块的反序列化操作,我们能够从文件中创建上一次程序保存的对象。
基本接口:
下面代码用来演示如何将数据保存到pickle中:
import pickle
import gzip
def export_pickle(data, filename, compress=False):
fh = None
try:
if compress:
fh = gzip.open(filename, 'wb')
else:
fh = open(filename, 'wb')
pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL)
return True
except (EnvironmentError, pickle.PicklingError) as err:
print(err)
return False
finally:
if fh is not None:
fh.close()
如果要求进行压缩,我们可以使用 gzip 模块的 gzip.open()
函数来打开文件,否则就是用内置的 open()
函数。在以二进制模式 picking 数据时,我们必须使用“二进制写”模式(“wb”)。其中 pickle.HIGHEST_PROTOCOL
表示protocol 3。
下面把一个简单的字典{'hello': 'world'}
序列化保存到文件pickle_test.txt
中:
export_pickle({'hello': 'world'}, './pickle_test.txt')
使用notepad++
打开该 txt 文件,可以看到如下结果:
01.pickle_test结果
显然已经进行序列化,从中我们可以看到"hello"和"world"两个单词,而其他部分并不可读。
要读回 pickled 的数据,我们需要区分开压缩的与未压缩的 pickle。使用 gzip 压缩的任意文件都以一个特定的魔数引导,魔数是一个或多个字节组成的序列,位于文件的起始处,用于指明文件的类型。对 gzip 文件, 其魔数为两个字节的 0x1F 0x8B,并存放在一个 bytes 变量中:GZIP_MAGIC = b'\x1F\x8B'
。
下面代码用于读取 pickle 文件:
def import_pickle(filename):
fh = None
try:
fh = open(filename, 'rb')
magic = fh.read(len(GZIP_MAGIC))
if magic == GZIP_MAGIC:
fh.close()
fh = gzip.open(filename, 'rb')
else:
fh.seek(0)
print(pickle.load(fh))
return True
except (EnvironmentError, pickle.PicklingError) as err:
print(err)
return False
finally:
if fh is not None:
fh.close()
使用下面代码进行测试:
import_pickle('./pickle_test.txt')
执行完之后可以看到输出如下:
{'hello': 'world'}
正是之前写入的内容。
2.2
带可选压缩的原始二进制数据
如果编写自己的代码来处理原始二进制数据,就可以对文件格式进行完全控制,这比 pickle 更具安全性,因为恶意的无效数据将由我们自己的代码控制,而不是由解释器执行。
Python提供了两种数据类型用于处理原始字节:固定的数据类型 bytes ,可变的数据类型 bytearray。这两种数据类型都用于存放0个或多个8位的无符号整数(字节),每个字节所代表的值范围在0到255之间。
创建自定义的二进制文件时,创建一个用于标识文件类型的魔数以及用于标识文件版本的版本号是有意义的:
MAGIC = b'AIB\x00'
FORMAT_VERSION = b'\x00\x01'
我们使用4个字节表示魔数,2个字节表示版本号。字节序不是问题,因为数据是以单独的字节形式写入。
下面演示如何将字符串保存成二进制:
import struct
import gzip
def export_binary(string, filename, compress=False):
data = string.encode('utf-8')
format = '<H{0}s'.format(len(data))
fh = None
try:
if compress:
fh = gzip.open(filename, 'wb')
else:
fh = open(filename, 'wb')
fh.write(MAGIC)
fh.write(FORMAT_VERSION)
bytearr = bytearray()
bytearr.extend(struct.pack(format, len(data), data))
fh.write(bytearr)
return True
except (EnvironmentError, pickle.PicklingError) as err:
print(err)
return False
finally:
if fh is not None:
fh.close()
用下面这行代码进行测试:
export_binary('I love Python.', './binary_test.txt')
数据的读回不像写入那么直接,首先,我们需要更多的错误检查操作。并且读回可变长度的字符串也是棘手的。下面代码实现数据读回功能:
def import_binary(filename):
def unpack_string(fh, eof_is_error=True):
uint16 = struct.Struct('<H')
length_data = fh.read(uint16.size)
if not length_data:
if eof_is_error:
raise ValueError('missing or corrupt string size')
return None
length = uint16.unpack(length_data)[0]
if length == 0:
return ''
data = fh.read(length)
if not data or len(data) != length:
raise ValueError('missing or corrupt string')
format = '<{0}s'.format(length)
return struct.unpack(format, data)[0].decode('utf-8')
fh = None
try:
fh = open(filename, 'rb')
magic = fh.read(len(GZIP_MAGIC))
if magic == GZIP_MAGIC:
fh.close()
fh = gzip.open(filename, 'rb')
else:
fh.seek(0)
magic = fh.read(len(MAGIC))
if magic != MAGIC:
raise ValueError('invalid .aib file format')
version = fh.read(len(FORMAT_VERSION))
if version > FORMAT_VERSION:
raise ValueError('unrecognized .aib file version')
string = unpack_string(fh)
if string is not None:
print(string)
except (EnvironmentError, pickle.PicklingError) as err:
print(err)
return False
finally:
if fh is not None:
fh.close()
使用下面一行代码进行测试:
import_binary('./binary_test.txt')
正常输出I love Python.
则成功。
3、文本文件的读写
第一小节已经伴随着 文件操作函数进行了文本文件操作的演示,此处不再赘述。
4、XML文件的读写
本节参考了 Python 官方文档 , https://docs.python.org/3.6/library/xml.etree.elementtree.html 。
Python提供了 3 种写入 XML 文件的方法:手动写入 XML;创建元素树并使用其 write() 方法;创建 DOM 并使用其 write() 方法。XML 文件的读入与分析则有 4 中方法:人工读入并分析;使用元素树;DOM;SAX(Simple API for XML)分析器。
下面这段 XML 是上述参考链接里的内容,下面的写入和解析都采用这段 XML。
<data>
<country name="Liechtenstein">
<rank>1</rank>
<year>2008</year>
<gdppc>141100</gdppc>
<neighbor name="Austria" direction="E"/>
<neighbor name="Switzerland" direction="W"/>
</country>
<country name="Singapore">
<rank>4</rank>
<year>2011</year>
<gdppc>59900</gdppc>
<neighbor name="Malaysia" direction="N"/>
</country>
<country name="Panama">
<rank>68</rank>
<year>2011</year>
<gdppc>13600</gdppc>
<neighbor name="Costa Rica" direction="W"/>
<neighbor name="Colombia" direction="E"/>
</country>
</data>
4.1
元素树
使用元素树写入 XML 数据分为两个阶段:首先,要创建用于表示 XML 数据的元素树;然后将元素写入到文件中。如果数据本身就是 XML 格式的,那就省去了第一阶段。
from xml.etree import ElementTree as ET
countries = [
{
'name': 'Liechtenstein',
'rank': 1,
'year': 2008,
'gdppc': 141100,
'neighbor': [{
'name': 'Austria',
'direction': 'E'
},{
'name': 'Switzerland',
'direction': 'W'
}
]
},{
'name': 'Singapore',
'rank': 4,
'year': 2011,
'gdppc': 59900,
'neighbor': [{
'name': 'Malaysia',
'direction': 'N'
}
]
},{
'name': 'Panama',
'rank': 68,
'year': 2011,
'gdppc': 13600,
'neighbor': [{
'name': 'Costa Rica',
'direction': 'W'
},{
'name': 'Colombia',
'direction': 'E'
}
]
},
]
def export_xml_etree(filename):
root=ET.Element('data')
for country in countries:
country_tag = ET.SubElement(root,'country',name=(country['name']))
rank_tag = ET.SubElement(country_tag,'rank') #子元素
rank_tag.text = '%i'%country['rank'] #节点内容
year_tag = ET.SubElement(country_tag, 'year')
year_tag.text = '%i'%country['year']
gdppc_tag = ET.SubElement(country_tag, 'gdppc')
gdppc_tag.text = '%i'%country['gdppc']
for neighbor in country['neighbor']:
neighbor_tag = ET.SubElement(country_tag, 'neighbor')
neighbor_tag.attrib['name'] = neighbor['name']
neighbor_tag.attrib['direction'] = neighbor['direction']
tree=ET.ElementTree(root)
try:
tree.write(filename, 'UTF-8')
except EnvironmentError as err:
print(err)
return False
return True
export_xml_etree("xml_test_etree.xml")
我们从创建根元素(\)开始,之后对所有的城市进行迭代。所有的属性必须是文本,因此,我们需要对日期、数值型数据、布尔型数据进行相应转换。
下面展示利用元素树对 XML 文件进行解析:
from xml.etree import ElementTree as ET
from xml.parsers import expat
def import_xml_etree(filename):
countries = []
try:
tree = ET.parse(filename)
except (EnvironmentError, expat.ExpatError) as err:
print(err)
return False
for country in tree.findall('country'):
data = {}
data['name'] = country.get('name')
for child_tag in ('rank', 'year', 'gdppc'):
data[child_tag] = int(country.find(child_tag).text)
data['neighbor'] = []
for child in country.findall('neighbor'):
neighbor = {}
for child_tag in ('name', 'direction'):
neighbor[child_tag] = child.get(child_tag)
data['neighbor'].append(neighbor)
countries.append(data)
print(countries)
return True
import_xml_etree("xml_test_etree.xml")
输出内容如下:
[{'name': 'Liechtenstein', 'rank': 1, 'year': 2008, 'gdppc': 141100, 'neighbor': [{'name': 'Austria', 'direction': 'E'}, {'name': 'Switzerland', 'direction': 'W'}]}, {'name': 'Singapore', 'rank': 4, 'year': 2011, 'gdppc': 59900, 'neighbor': [{'name': 'Malaysia', 'direction': 'N'}]}, {'name': 'Panama', 'rank': 68, 'year': 2011, 'gdppc': 13600, 'neighbor': [{'name': 'Costa Rica', 'direction': 'W'}, {'name': 'Colombia', 'direction': 'E'}]}]
除了格式不完美外,基本还原了 countries 的内容。
上述代码用到了几个方法:
4.2
DOM
DOM 是一种用于表示操纵内存中 XML 文档的标准 API。用于创建 DOM 并将其写入到文件的的代码,以及使用 DOM 对 XML 文件进行分析的代码,在结构上与元素树代码非常相似。
# DOM
from xml.dom.minidom import Document
def export_xml_dom(filename):
fh = None
# 创建dom文档
doc = Document()
# 创建根节点
root=doc.createElement('data')
for country in countries:
# 创建节点<country>,然后插入到父节点<data>下
country_tag = doc.createElement('country')
country_tag.setAttribute('name', country['name'])
root.appendChild(country_tag)
# 将<rank>插入<country>
rank_tag = doc.createElement('rank')
rank_tag_text = doc.createTextNode('%i'%country['rank'])
rank_tag.appendChild(rank_tag_text)
country_tag.appendChild(rank_tag)
# 将<year>插入<country>
year_tag = doc.createElement('year')
year_tag_text = doc.createTextNode('%i'%country['year'])
year_tag.appendChild(year_tag_text)
country_tag.appendChild(year_tag)
# 将<gdppc>插入<country>
gdppc_tag = doc.createElement('gdppc')
gdppc_tag_text = doc.createTextNode('%i'%country['gdppc'])
gdppc_tag.appendChild(gdppc_tag_text)
country_tag.appendChild(gdppc_tag)
# 将<neighbor>插入<country>
for neighbor in country['neighbor']:
neighbor_tag = doc.createElement('neighbor')
neighbor_tag.setAttribute('name', neighbor['name'])
neighbor_tag.setAttribute('direction', neighbor['direction'])
country_tag.appendChild(neighbor_tag)
try:
fh = open(filename, 'wb')
fh.write(root.toprettyxml(indent='\t', encoding='utf-8'))
except EnvironmentError as err:
print(err)
return False
return True
export_xml_dom('xml_test_dom.xml')
我们打开生成的 xml_test_dom.xml
,发现已经进行了格式化、排版。
03.xml_dom
下面展示使用 DOM 解析 XML的代码:
from xml.dom import minidom
def import_xml_dom(filename):
countries = []
try:
tree = minidom.parse(filename)
except (EnvironmentError, expat.ExpatError) as err:
print(err)
return False
for country in tree.getElementsByTagName('country'):
data = {}
# 解析<country>的‘name’属性
data['name'] = country.getAttribute('name')
# 解析<country>的子标签:<rank>、<year>、<gdppc>
for child_tag in ('rank', 'year', 'gdppc'):
data[child_tag] = int(country.getElementsByTagName(child_tag)[0].firstChild.data)
# 解析<country>的子标签<neighbor>
data['neighbor'] = []
for child in country.getElementsByTagName('neighbor'):
neighbor = {}
for child_tag in ('name', 'direction'):
neighbor[child_tag] = child.getAttribute(child_tag)
data['neighbor'].append(neighbor)
countries.append(data)
print(countries)
return True
import_xml_dom("xml_test_dom.xml")
输出结果
[{'name': 'Liechtenstein', 'rank': 1, 'year': 2008, 'gdppc': 141100, 'neighbor': [{'name': 'Austria', 'direction': 'E'}, {'name': 'Switzerland', 'direction': 'W'}]}, {'name': 'Singapore', 'rank': 4, 'year': 2011, 'gdppc': 59900, 'neighbor': [{'name': 'Malaysia', 'direction': 'N'}]}, {'name': 'Panama', 'rank': 68, 'year': 2011, 'gdppc': 13600, 'neighbor': [{'name': 'Costa Rica', 'direction': 'W'}, {'name': 'Colombia', 'direction': 'E'}]}]
可以看出,和使用 xtree 进行解析的结果一致。
4.3
手动写入XML
将预存的元素树或 DOM 写成 XML 文档可以使用单独的方法调用完成。如果数据本身不是以这两种形式存在,我们就必须先创建元素树或 DOM ,之后直接写出数据更佳方便。手动写入的主要工作是字符串的拼接和格式化,这里不做详细解释。
插播一条通知:本公众号上次的抽奖活动已结束数天,中奖者“江小白要喝江小白”还没有填写地址信息,请尽快填写!
推荐阅读
Recommended reading
点击下列标题 阅读Python指南系列往期文章
| 精彩文章回顾
| Python指南:Python的8个关键要素
| Python指南:数据类型
| Python指南:组合数据类型
| Python指南:控制结构与函数
| Python指南:面向对象程序设计