17  文件操作

本章介绍如何用Python操作本地文件,主要是读写操作。

17.1 文件路径操作

17.1.1 文件名称的正确写法

不论是读入本地的特定文件,还是将数据写入本地的某个文件,都需要明确要读/写的文件具体是哪一个,为此你需要向程序提供准确的文件名。 完整文件名由3部分组成:

  1. 路径,即文件所处的文件夹。
  2. 狭义的文件名,如你将一个Python脚本以hello.py的名称存储在本地,其文件名就是hello
  3. 扩展名,提示文件的格式,如.py.txt.exe

如,'C:/Users/HUAWEI/Desktop/工作簿1.xlsx'就是一个文件名。

文件路径中的分隔符有三种写法:

  1. 一个正斜杠/
  2. 一个反斜杠,但文件名字符串前加 r(表示这个反斜杠不是转义字符的一部分)
  3. 两个反斜杠 \\(第 1 个反斜杠转义第 2 个反斜杠)

一个反斜杠代表转义字符,具有特殊含义。如\n会被解读为换行符,而不是表示文件夹的层级关系。

在Windows平台上,右击文件图标,选择属性,可看到文件路径。

注意这里显示的路径是1个反斜杠,不能直接使用,需替换为\\/

Windows可以通过右击-属性查看文件路径。此路径用1个反斜杠作分隔符。

17.1.2 当前工作目录

读数据时提供的文件名如果不包含路径,程序会在当前工作目录(current working directory)中寻找同名文件。 os模块可以查看和设置当前工作目录。

import os
os.getcwd() # 查看当前工作目录
os.setwd('文件路径') # 设置当前工作目录

使用Jupyter Notebook时,当前工作目录是.ipynb文件所在的文件夹的当前工作目录。

在本地项目中,工作目录是创建项目时就设定的项目文件夹。 如果项目涉及数量众多、类型各异的文件, 建议在项目文件夹中建立datafigurecode等文件夹,分门别类地存放数据、图片和代码等文件,读写文件的路径可以统一为形如'./data/'的相对路径 (在文件路径中,.代表当前文件夹)。

形如'C:/Users/HUAWEI/Desktop/工作簿1.xlsx'的完整路径称为绝对路径,形如'./data/工作簿1.xlsx'、用.表示当前文件夹的路径称为相对路径。

import pandas as pd
# 通过绝对路径读取文件
path = '文件路径'
df = pd.read_csv(path + '/file.csv')

# 通过相对路径读取文件
# 项目文件夹的data子文件下放数据文件
df = pd.read_csv('./data/file.csv')

17.2 读写表格数据

表格数据(tabular data)的一行代表一个观测,一列代表一个变量,是数据科学和数据分析中最常见的数据格式。

典型的表格数据。每行代表一个运动员,每列是关于运动员的一项信息,如姓名(name),国籍(NOC),性别(gender),参与项目(event),接受的各类兴奋剂检测的次数(total)。

表格数据有3种常见格式:.csv, .xlsx, .txt。 csv全称comma separated values,即逗号分隔值,每行用逗号分隔各变量;.xlsx是微软Excel处理的文件格式;.txt是纯文本文件。

pandas 是 Python 专注于数据分析的第三方库,对这3种格式的数据都能读写。

import pandas as pd

# 将表格数据读入程序
df = pd.read_csv('./data/file.csv')
df = pd.read_excel('./data/file.xlsx') # 默认读取第1个工作簿
df = pd.read_table('./data/file.txt')

# 将文件保存为表格数据
df.to_csv('./data/file.csv')
df.to_excel('./data/file.xlsx')
df.to_csv('./data/file.txt', sep = '\t') # 各列以制表符分隔

pandas 没有 to_table()方法,但 to_csv()可以将输出文件的扩展名写为 txt,用 sep参数规定分隔符。

pd.read_csv() 默认使用 utf-8 解码。如果文件以其他方式编码,可能报错 UnicodeDecodeError。对中文文件,可尝试参数encoding = 'gbk'

上面的代码中,读入的数据对象df的类型是pandas库定义的类型数据框data.frame。 在用Python做数据分析的课程中,会对pandas的使用做详细讲解。

17.3 读写一般文件

17.3.1 文本文件,二进制文件

文本文件用来保存肉眼可见的字符,扩展名是.txt.py等。

二进制文件用来保存视频、图片、音乐等不可用字符表示的内容。

二进制文件和文本文件都是以二进制数字 0,1 保存在磁盘上的数据。 文本文件用 ASCII、UTF-8、GBK 等字符编码,文本编辑器(如记事本,vscode 等)通过识别出这些编码格式,将编码值转换成字符显示; 二进制数据没有字符编码含义,文本编辑器按照字符编码格式解析,就得到乱码。

17.3.2 缓冲区

存储器的速度越快,能耗越高,材料成本越贵,因此速度快的存储器的容量都比较小。

(a) 存储器的层次结构
(b) 速度越快,成本越高
图 17.1: 小林 coding:图解系统 2.2 磁盘比内存慢几万倍?

计算机在内存中预留一定的存储空间,称为缓冲区(buffer),用来暂存输入或输出数据。 设置缓冲区可以减少输入输出设备的读写次数,提高效率。如:

  • 硬盘的速度远低于 CPU,当向硬盘写入数据时,程序需要等待,不能做任何事情,就好像卡顿了一样,用户体验非常差。
  • 计算机上绝大多数应用程序都需要和硬件打交道,如读写硬盘、向显示器输出、从键盘输入等,如果每个程序都等待硬件,整台计算机也会变得卡顿。
  • 如果将数据先放入缓冲区(内存的读写速度也远高于硬盘),程序可以继续往下执行,等积累了一定量的数据,再将缓冲区的所有数据一次性写入硬盘,这样程序就减少了等待的次数,变得流畅起来。

Python在内存区为每一个正在被使用的文件开辟一个文件缓冲区。 从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。 从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区至充满,然后再从缓冲区逐个地将数据送到程序数据区。

文件缓冲系统

17.3.3 文件的打开,关闭

打开文件,就是建立程序与文件的连接,为文件建立信息区和缓冲区。 文件信息区是内存中的一个区域,用来存放操作相关文件所需的信息,如文件名称,文件状态,文件当前位置等。 程序通过操作文件信息区,实现对本地文件的操作。

关闭文件,指撤销文件信息区和文件缓冲区,,关闭后用户就无法再操作文件。

为了安全、可靠地进行文件操作,操作文件前要打开文件,操作结束后要关闭文件。

你应该能够看出,程序对文件的打开和关闭,与操作系统地图形界面中双击文件图标打开、点击小红叉关闭,并不是相同的概念。

17.3.4 打开关闭文件的Python函数

Python中,open()打开文件,close()关闭文件。

# 打开文件
f = open(file, mode='r', encoding=None,...)

# 关闭文件
f.close()
  • filename:要打开的文件名称,通常是字符串,包括文件路径、文件名称、扩展名
  • mode:文件访问方式,见 表 17.1
  • encoding:文本文件的编码方式,默认编码与平台有关
  • open()返回一个文件对象f,文件对象f本质上就是相关文件的信息区
  • 为了安全和资源管理,文件不用后应用 close() 关闭
表 17.1: 部分文件访问方式字符串
模式 描述 文件存在 文件不存在
“r” 只读 打开成功 打开失败
“w” 只写 清空文件 创建新文件
“a” 追加 追加写入 创建新文件
“x” 仅创建 打开失败 创建新文件

表 17.1 中的字符串还可以与't''b''+'组合。 't'用于文本文件,'b'用于二进制文件。 '+'使得访问方式兼具读写。如mode = 'rt+',就是以读写方式打开文本文件,且不清空文件。 只有含有'w'的访问方式才会清空现有文件。 默认访问方式是'rt'(以只读方式打开文本文件)。

17.3.5 读写文件的Python函数

open()返回的文件对象一个序列,具有 read()write() 等方法。

方法 含义
read(size) 返回文本文件的 size 个字符
返回二进制文件的size个字节
省略 size 则读入所有数据。
readline() 返回一行(字符串),重复应用返回下一行。
readlines(hint) 返回 hint 行(列表),省 hint 则读入所有行。
write(string) 将字符串 string 写入文件,返回写入的字符数。
writelines(lines) lines 是一个列表,列表元素是字符串,每个字符串的最后一个字符应当是行之间的分隔符,如换行符
flush() 刷新写入缓冲区

文件打开后必须关闭,一般有两种方法实现:

  1. try...except...finally确保文件打开后被关闭。

try代码块被执行,如果出现报错,执行except代码块的内容;无论是否执行except代码块,最终都会执行finally代码块。

f = None
try:
    f = open("file")
except:
    # 报错时执行的操作
finally:
    if f is not None:
        f.close()
  1. 用with语句定义一个上下文,确保打开的文件自动关闭。

此时不需使用close(),with代码块结束后文件将自动关闭。

with open(filename, 'r', encoding = 'utf-8') as f:
    # 对文件进行的处理操作
# 用with
import sys
filename = sys.argv[0]
line_no = 0
with open(filename, 'r', encoding = 'utf-8') as f:
    for line in f:
        line_no += 1
        print(line_no, ":", line)

# 不用with
import sys
filename = sys.argv[0]
f = open(filename, 'r', encoding = 'utf-8')
line_no = 0
while True:
    line_no += 1
    line = f.readline()
    if line:
        print(line_no, ":", line)
    else:
        break
        
f.close()

17.4 序列化

序列化(serialize)指将对象转换为字节序列; 反序列化是将字节序列还原为对象。

为什么要序列化?虽然对象在内存中也是用字节存储的,但内存中的字节数据不是自描述的,其具体含义依赖于编程语言、编译器、甚至操作系统。换句话说,把 A 机器内存的一段字节直接复制到 B 机器,B 机器无法理解。

可用 pickle, cpickle,json 等模块实现序列化与反序列化:

  • cpickle 用 C 实现,比 pickle 更快。可序列化几乎一切Python 对象,但安全性差。
  • JSON(JavaScript Object Notation,Java 对象标记)是网络数据交换的流行格式。json 模块可以将部分 Python 对象序列化为 json 文件,将 json 文件反序列化为 Python 对象。
# pickle API
import pickle
pickle.dump(obj, file, protocol=None ,...)
pickle.dumps(obj, protocol=None ,...)
pickle.load(file...)
pickle.loads(data...)
pickle/cpickle的API。cpickle,与 pickle 的 API 相同,且速度更快。 将上面代码的第一行改为import cpickle as pickle,即可使用cpickle。
API 含义
pickle.dump(obj, file) 将对象 obj 序列化后的字节数据写入文件对象 file
pickle.dumps(obj) 返回对象 obj 序列化后的 bytes 对象
pickle.load(file) 反序列化,参数是存储序列化字节数据的文件对象 file
pickle.loads(data) 反序列化,参数是 bytes 对象

pickle 函数中有个参数 protocol,表示序列化使用的协议:

  • pickle 协议是 Python 定义的序列化规则,不同版本的协议优化了数据格式、性能或支持新特性。
  • Python 随着版本更新引入新的协议,但保持向后兼容:支持高版本协议的 Python 可以反序列化低版本协议序列化的数据;不支持高版本协议的 Python 不能反序列化高版本协议序列化的数据。
  • 序列化一般不需显式提供 protocol,程序会选择当前 Python版本默认的协议。
pickle 协议在各版本 Python 的使用情况
协议版本 Python 引入版本 特点
0 Python 1.4 人类可读的 ASCII 格式(兼容性最强,但速度慢、体积大)
1 Python 1.4 较旧的二进制格式(略高效于协议 0)
2 Python 2.3 支持 Python 2 的类的新特性(如 __slots__
3 Python 3.0 专为 Python 3 设计,不兼容 Python 2(Python 3.0-3.7的默认协议)
4 Python 3.4 支持更大对象、优化存储效率(Python 3.8+的默认协议)
5 Python 3.8 支持外存数据(out-of-band data)和性能优化
# 序列化
import pickle
with open("dataObj1.dat", "wb") as f:
    s1 = "Hello!"
    c1 = 1 + 2j
    t1 = (1,2,3)
    d1 = dict(name = "Mary", age = 19)
    pickle.dump(s1, f)
    pickle.dump(c1, f)
    pickle.dump(t1, f)
    pickle.dump(d1, f)
# 反序列化
import pickle
with open("dataObj1.dat", "rb") as f:
    o1 = pickle.load(f)
    o2 = pickle.load(f)
    o3 = pickle.load(f)
    o4 = pickle.load(f)
    print(type(o1), str(o1))
    print(type(o2), str(o2))
    print(type(o3), str(o3))
    print(type(o4), str(o4))

json 模块能序列化部分 Python 对象为 JSON 字符串,便于传播。但 json 并不支持所有 Python 对象,如它不能直接处理集合,需要转换为列表。

json模块序列化/反序列化API
API 含义
json.dump(obj, file) 将 obj 对象序列化为 JSON 字符串,写入文件 file
json.load(file) 从文件 file 读取 JSON 字符串,返回反序列化后的该对象
json.dumps(obj) 将 obj 对象序列化为 JSON 字符串,返回JSON 字符串
json.loads(s) 将 JSON 字符串 s 反序列化后返回
Python对象与JSON对象类型对应关系
Python对象类型 JSON类型
dict object
list, tuple array
str string
int, float, int- & float-derived Enums number
True true
False false
None null
import json
data = [{'a': 'A', 'b': (2,4), 'c':3.0}]
str_json = json.dumps(data)
str_json
data1 = json.loads(str_json)
data1
'[{"a": "A", "b": [2, 4], "c": 3.0}]'
[{'a': 'A', 'b': [2, 4], 'c': 3.0}]
import json
urls={'baidu':'http://www.baidu.com/',      
    'sina':'http://www.sina.com.cn/',      
    'tencent':'http://www.qq.com/',      
    'taobao':'https://www.taobao.com/'}
    
with open(r'c:\pythonpa\data.json', 'w') as f:    
    json.dump(urls, f)

with open(r'c:\pythonpa\data.json', 'r') as f:    
    urls = json.load(f)
    print(urls)

17.5 练习

  1. 文件类型判断。

传媒从业者经常需要转换文件格式。 如知名视觉小说游戏开发引擎Ren’Py仅支持webm格式的视频文件。 因此视觉小说开发者经常要将mp4文件转webm。 许多视频插入问题失败的问题,也来源于开发者误将mp4文件认为是webm文件使用。

因此判断文件类型是工作刚需。许多用户通过文件扩展名来判断文件类型。 但文件扩展名可以手动更改,而仅仅更改扩展名是不能改变文件格式的,反而给文件类型识别增加难度。 正确转换文件格式需要借助专门工具,如 https://www.freeconvert.com/mp4-to-webm

判断文件类型的更可靠的方式是文件签名(file signature)。 文件签名指文件的前几个字节码,特定类型的文件通常有着给定的文件签名。 你可以查阅文件签名不完全表格。如:

  • MP4文件从第4个字节开始(索引从0开始)的四个字节固定为66 74 79 70,后面接续的字节码可能是4D 53 4E 5669 73 6F 6D6D 70 34 32
  • webm文件必定以1A 45 DF A3开头(虽然mkv文件也具有相同开头)

一些软件(如Windows平台的HxD)可以让你方便地查看文件的字节码。

编写代码,判断一个视频文件是mp4格式,还是webm格式。

(a) 某webm文件的文件头
(b) 某mp4文件的文件头
图 17.2: Windows平台下用HxD查看文件的文件头
def check_video_format(filename):
    '''检测文件是webm还是mp4,只识别特定4种MP4,其他格式返回unknown'''
    try:
        with open(filename, 'rb') as f:
            header = f.read(12)# 读取前12个字节的文件头
    except Exception as e:
        return f'error: {str(e)}' 

    if header[:4] == b'\x1A\x45\xDF\xA3':
        return 'webm'
    elif header[4:8] == b'ftyp':
        if header[8:] in [b'\x4D\x53\x4E\x56', b'\x69\x73\x6F\x6D', b'\x6D\x70\x34\x32']:
            return 'mp4'
        else:
            return 'other mp4'
    else:
        return 'unknown'