Python指南:文件处理

大多数程序都需要向文件中存储或从文件中加载信息,比如数据或状态信息。本文将深入全面地介绍文件处理的相关知识与方法。

哪种文件格式最适合用于存储整个数据集——二进制、文本还是XML?这严重依赖于具体的上下文。

  • 二进制格式的存储与加载通常是非常快的,并且也是非常紧凑的。但二进制数据不是那种适合阅读或可编辑的数据格式。
  • 文本格式适合阅读,并且是可编辑的,这使得单独的工具对文本文件处理变得容易,也很容易对其进行修改。
  • XML格式适合阅读,并且是可编辑的,可以使用单独的工具进行处理。XML文件格式的分析是直接的,XML分析器速度可能会较慢,因此,读入很大的XML文件回避读入同样大小的二进制文件或文本文件耗费更多的时间资源。

1、文件操作函数

1.1

open()

提到文件操作,那就必须提到 open 函数,因为无论是读取还是写入,都需要先把文件打开,然后才能进行读写操作。 open 函数的作用是打开一个文件,返回一个 file 对象,相关的方法才可以调用它进行读写。其语法如下:

file_object = open(file_name, [,access_mode][, buffering])

  • file_name:字符串类型的文件名称
  • access_mode:打开文件的模式,下面会详细介绍可取值
  • buffering:如果该值为0,这不会有寄存;如果其值为1,访问文件时会寄存行;如果其值大于1,表明了这就是寄存区的缓冲大小;如果为负值,寄存去的缓冲大小为系统默认。

测试一下 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.dump(obj, file, [,protocol]) 序列化对象,并将结果数据流写入到文件对象中。参数protocol是序列化模式,有三个值可选:0 为ASCII,1为旧式二进制,2为新式二进制,默认值为0。
  • pickle.load(file) 反序列化对象。将文件中的数据解析为一个Python对象。

2.1.1 序列化

下面代码用来演示如何将数据保存到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"两个单词,而其他部分并不可读。

2.1.2 反序列化

要读回 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之间。

2.2.1 写入二进制文件

创建自定义的二进制文件时,创建一个用于标识文件类型的魔数以及用于标识文件版本的版本号是有意义的:

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')

2.2.2 读取二进制文件

数据的读回不像写入那么直接,首先,我们需要更多的错误检查操作。并且读回可变长度的字符串也是棘手的。下面代码实现数据读回功能:

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 的内容。

上述代码用到了几个方法:

  • find(match, namespaces=None ) :寻找匹配的第一个子元素。
  • findall(match, namespaces=None ):寻找所有匹配的子元素。
  • get(key, default=None ):获取元素的属性值。

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指南:面向对象程序设计

原文发布于微信公众号 - C与Python实战(CPythonPractice)

原文发表时间:2018-05-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT笔记

如何限制input输入类型

1.只能输入和粘贴汉字 <input onkeyup="value=value.replace(/1/g,'')" onbeforepaste="clipbo...

42470
来自专栏程序员互动联盟

android apk 防止反编译技术第二篇-运行时修改字节码

上一篇我们讲了apk防止反编译技术中的加壳技术,如果有不明白的可以查看我的上一篇博客http://my.oschina.net/u/2323218/blog/3...

417110
来自专栏青青天空树

mfc学习之路--如何删除通过控件新增的变量

   刚刚学校mfc的人都会遇到这样一个问题(比如我),在照做书做一个mfc程序,给控件新增变量时变量类型错了,但是变量名对了,然后想要加个正确的时候提示"已经...

11850
来自专栏从零开始学自动化测试

pytest文档14-函数传参和fixture传参数request

为了提高代码的复用性,我们在写用例的时候,会用到函数,然后不同的用例去调用这个函数。 比如登录操作,大部分的用例都会先登录,那就需要把登录单独抽出来写个函数,其...

51920
来自专栏阮一峰的网络日志

Make 命令教程

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。 Make是最常用的构建工具,诞生于1977...

33540
来自专栏Python自动化测试

Jmeter4.0接口测试之断言实战(六)

在接口测试用例中得有断言,没有断言的接口用例是无效的,一个接口的断言有三个层面,一个是HTTP状态码的断言,另外一个是业务状态码的断言,最后是某一接口请求后服...

1K40
来自专栏数据之美

shell 学习笔记(17)

声明:转载需署名出处,严禁用于商业用途! 1601.关于rsync相同文件后 du 大小不一样的问题: 不一样大小很正常,因为文件系统的block...

31680
来自专栏从零开始学 Web 前端

从零开始学 Web 之 Ajax(二)PHP基础语法

浏览器是不识别 PHP 文件的,用浏览器发开 PHP 文件,只会显示 PHP 的源代码,所以 PHP 文件必须在服务器中执行。其实 apache 服务器也识别不...

15820
来自专栏xcywt

关于 getsockname、getpeername和gethostname、gethostbyname

一、gethostname,gethostbyname的用法 这两个函数可以用来获取主机的信息。 gethostname:获取主机的名字 gethostbyna...

22350
来自专栏Vamei实验室

Python深入02 上下文管理器

上下文管理器(context manager)是Python2.5开始支持的一种语法,用于规定某个对象的使用范围。一旦进入或者离开该使用范围,会有特殊操作被调用...

20970

扫码关注云+社区

领取腾讯云代金券