荐 Python基础语法全体系 | 文件IO与常用文件操作模块

《Python基础语法全体系》系列博文第六篇,本篇博文将讲解Python的文件IO操作,包括文件的打开、读取和写入。本文整理自疯狂python编程。

文章目录

Python提供了非常丰富的I/O支持,它既提供了 pathlib和os.path来操作各种路径 ,也提供了 全局的open()函数 来打开文件——在打开文件之后,程序既可以读取文件的内容,也可向文件输出内容,并且Python提供了多种方式来读取文件的内容。此外, 在Python的os模块下也包含了大量进行文件I/O的函数 ,使用这些函数来读取、写入文件也很方便。

Python还提供了 tempfile模块来创建临时文件和临时目录 ,tempfile模块下的高级API会自动管理临时文件的创建和删除。

使用pathlib操作目录

由于文章篇幅原因,我们在这里对pathlib模块的使用不做介绍,大家可以参考 《Python pathlib模块用法详解》 做深入学习。

下面我们简单示例一下Path模块的目录操作方法,不做讲解。

from pathlib import *

# 获取当前目录
p = Path('.')

# 遍历当前目录下所有文件和子目录
for x in p.iterdir():
    print(x)

# 获取上一级目录
p = Path('../')

# 获取上级目录及其所有子目录下的的py文件
for x in p.glob('**/*.py'):
    print(x)

# 获取g:/publish/codes对应的目录
p = Path('g:/publish/codes')

# 获取上级目录及其所有子目录下的的py文件
for x in p.glob('**/Path_test1.py'):
    print(x)

使用os.path操作目录

os.path 模块下提供了一些操作目录的方法,这些函数可以操作系统的目录本身。例如,该模块提供了 exists() 函数 判断该目录是否存在,也提供了 getctime()、getmtime()、getatime() 函数 来获取该目录的创建时间、最后一次修改时间、最后一次访问时间,还提供了 getsize() 函数 来获取指定文件的大小。

import os
import time

# 获取绝对路径
print(os.path.abspath("abc.txt")) # D:\Learning\Python Project\abc.txt

# 获取共同前缀
print(os.path.commonprefix(['/usr/lib', '/usr/local/lib'])) # /usr/l

# 获取共同路径
print(os.path.commonpath(['/usr/lib', '/usr/local/lib'])) # \usr

# 获取目录
print(os.path.dirname('abc/xyz/README.txt')) #abc/xyz

# 判断指定目录是否存在
print(os.path.exists('abc/xyz/README.txt')) # False

# 获取最近一次访问时间
print(time.ctime(os.path.getatime('os.path_test.py'))) # Fri Dec 27 18:10:53 2019

# 获取最后一次修改时间
print(time.ctime(os.path.getmtime('os.path_test.py'))) # Mon Jun 18 00:49:11 2018

# 获取创建时间
print(time.ctime(os.path.getctime('os.path_test.py'))) # Fri Dec 27 18:10:53 2019

# 获取文件大小
print(os.path.getsize('os.path_test.py')) # 2105

# 判断是否为文件
print(os.path.isfile('os.path_test.py')) # True

# 判断是否为目录
print(os.path.isdir('os.path_test.py')) # False

# 判断是否为同一个文件
print(os.path.samefile('os.path_test.py', './os.path_test.py')) # True

使用fnmatch处理文件名匹配

fnmatch模块可以支持类似于UNIX shell风格的文件名匹配。支持如下通配符:

  • * :可匹配任意个任意字符
  • ? :可匹配一个人任意字符
  • [字符序列] :可匹配括号里字符序列中的任意字符。该字符序列也支持划线表示法。比如[a-c]可代表a、b和c字符序列中的任意一个。
  • [!字符序列] :可匹配不在中括号里字符序列中的任意字符。

在该模块下提供了如下函数:

fnmatch.fnmatch(filename, pattern) :判断指定文件名是否匹配指定 pattern。

from pathlib import *
import fnmatch

# 遍历当前目录下所有文件和子目录
for file in Path('.').iterdir():
    # 访问所有以_test.py结尾的文件
    if fnmatch.fnmatch(file, '*_test.PY'):
        print(file)

fnmatch.fnmatchcase(filename, pattern) :该函数与上一个函数的功能大致相同,只是该函数区分大小写。

fnmatch.filter(names, pattern) :该函数对 names 列表进行过滤,返回 names 列表中匹配 pattern 的文件名组成的子集合。

names = ['a.py', 'b.py', 'c.py', 'd.py']
# 对names列表进行过滤
sub = fnmatch.filter(names, '[a-c].py')
print(sub) # ['a.py', 'b.py', 'c.py']

fnmatch.translate(pattern) :该函数用于将一个 UNIX shell 风格的 pattern 转换为正则表达式 pattern。

print(fnmatch.translate('?.py')) # (?s:.\.py)\Z
print(fnmatch.translate('[ac].py')) # (?s:[ac]\.py)\Z
print(fnmatch.translate('[a-c].py')) # (?s:[a-c]\.py)\Z

打开文件

在进行文件读写之前,首先要打开文件。Python提供了一个内置的 open()函数 ,该函数用于打开指定文件,语法格式如下:

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

在上面的语法格式中,只有 第一个参数是必需的 ,该参数代表要打开文件的路径。

在打开文件之后,就可调用文件对象的属性和方法了。文件对象支持如下常见的属性:

  • file.closed:判断文件是否己经关闭。
  • file.mode:返回被打开文件的访问模式。
  • file.name:返回文件的名称。
# 以默认方式打开文件
f = open('main')
# 访问文件的编码方式
print(f.encoding) # cp936
# 访问文件的访问模式
print(f.mode) # r
# 访问文件是否已经关闭
print(f.closed) # False
# 访问文件对象打开的文件名
print(f.name) # main

从上面程序的运行结果可以看出,open()函数默认打开文件的模式是“r”,也就是只读模式。

文件打开模式

open()函数支持的文件打开模式如下表所示:

模式 意义
r 只读模式
w 写模式
a 追加模式
+ 读写模式,可与其他模式结合使用,比如r+代表读写模式
b 二进制模式,可以其他模式结合使用。比如rb代表二进制只读模式,rb+代表二进制读写模式,ab代表二进制追加模式

不管是w还是w+模式,当时用这两种模式打开指定文件时,open()函数都会立即清空文件内容,实际上都无法读取文件内容。

如果希望调用open()函数打开指定文件后,该文件中的内容能被保留下来,那么程序就不能使用w或w+模式。

如果程序使用r或r+模式打开文件,那么要求被打开的文件本身是存在的。也就是说,使用r或r+模式都不能创建文件。但是如果使用w、w+、a、a+模式打开文件,则该文件可以是不存在的,open()函数会自动创建新文件。

b模式可被追加到其他模式上,用于代表以二进制的方式来读写文件内容。对于计算机中的文件来说,文本文件只有很少的一部分,大部分文件其实都是二进制文件,包括图片文件、音频文件、视频文件等。

如果使用文本方式来操作二进制文件,则往往无法得到正确的文件内容。如果程序需要读写文本文件以外的其他文件,则都应该添加b模式。

计算机外设(如硬盘、网络)的I/O速度远远低于访问内存的速度,而程序执行I/O时要么将内存中的数据写入外设,要么将外设中的数据读取到内存,如果不使用缓冲,就必须等外设输入或输出一个字节后,内存中的程序才能输出或输入一个字节,这意味着内存中的程序大部分时间都处于等待状态。

因此,一般建议打开缓冲。在打开缓冲之后,当程序执行输出时,程序会先将数据输出到缓冲区中,而不用等待外设同步输出,当程序把所有数据都输出到缓冲区中之后,程序就可以去做其他事情了,留着缓冲区慢慢同步到外设即可;相反,当程序执行输入时,程序会先等待外设将数据读入缓冲区中,而不用等待外设同步输入。

在使用open()函数时,如果其第三个参数是0(或者False),那么该函数打开的文件就是不带缓冲的;如果其第三个参数是1(或者True),则该函数打开的文件就是带缓冲的,此时程序执行I/O将具有更好的性能。如果其第三个参数是大于1的整数,则该整数用于指定缓冲区的大小(单位是字节);如果其第三个参数为任何负数,则代表使用默认的缓冲区大小。

读取文件

按字节或字符读取

文件对象提供了read()方法来按照字节或字符读取文件内容,到底是读取字节还是字符,则取决于是否使用了b模式。 如果使用了b模式,则每次读取一个字节;如果没有使用b模式,则每次读取一个字符 。在调用该方法时可传入一个整数作为参数,用于指定最多读取多少个字节或字符。

如下程序采用循环读取整个文件的内容。

f = open("read_test.py", 'r', True)
while True:
    # 每次读取一个字符
    ch = f.read(1)
    # 如果没有读到数据,跳出循环
    if not ch: break
    # 输出ch
    print(ch, end='')
f.close()

如果在调用read()方法时不传入参数,该方法默认会读取全部文件内容。

f = open("test.txt", 'r', True)
# 直接读取全部文件
print(f.read())
f.close()

当使用open()函数打开文本文件时,程序总是使用当前操作系统的字符集,如果要读取的文件所使用的字符集和当前操作系统的字符集不匹配,程序便会出现UnicodeDecodeError错误。有两种解决方式:

  • 使用二进制方式读取,然后用bytes的decode()方法恢复成字符串。
  • 利用codecs模块的open()函数来打开文件,该函数在打开文件时允许指定字符集。
# 指定使用二进制方式读取文件内容
f = open("read.py", 'rb', True)
# 直接读取全部文件,并调用bytes的decode将字节内容恢复成字符串
print(f.read().decode('utf-8'))
f.close()

上面程序在调用 open()函数时,传入了rb模式,表明程序采用二进制模式读取文件,此时文件对象的read()方法返回的是bytes对象,程序可调用bytes对象的 decode()方法将它恢复成字符串。由于此时读取read.py文件是以UTF-8的格式保存的,因此程序需要使用 decode()方法恢复字符串时显式指定使用UTF-8字符集。

import codecs
# 指定使用utf-8字符集读取文件内容
f = codecs.open("read.py", 'r', 'utf-8', buffering=True)
while True:
    # 每次读取一个字符
    ch = f.read(1)
    # 如果没有读到数据,跳出循环
    if not ch: break
    # 输出ch
    print(ch, end='')
f.close()

按行读取

如果程序要读取行,通常只能用文本方式来读取,因为只有文本文件才有行的概念,二进制文件没有所谓行的概念。

文件对象提供了如下两个方法来读取行。

  • readline([n]) :读取一行内容。如果指定了参数n,则只读取此行内的n个字符。
  • readlines() :读取文件内所有行
import codecs
# 指定使用utf-8字符集读取文件内容
f = codecs.open("readline.py", 'r', 'utf-8', buffering=True)
while True:
    # 每次读取一行
    line = f.readline()
    # 如果没有读到数据,跳出循环
    if not line: break
    # 输出line
    print(line, end='')
f.close()

# 指定使用utf-8字符集读取文件内容
f = codecs.open("readlines_test.py", 'r', 'utf-8', buffering=True)
# 使用readlines()读取所有行,返回所有行组成的列表
for l in f.readlines():
    print(l, end='')
f.close()

使用fileinput读取多个输入流

fileinput模块提供了如下函数可以把多个输入流合并在一起。 fileinput.input(files=None, inplace=False, backup=", bufsize=0, mode='r', openhook=None) ,该函数中的files参数用于指定多个文件输入流。该函数返回一个Filelnput对象。

当程序使用上面函数创建了Filelnput对象之后,即可通过for循环来遍历文件的每一行。此外,fileinput 还提供了如下全局函数来判断正在读取的文件信息。

  • fileinput.filename(): 返回正在读取的文件的文件名。
  • fileinput.fileno():返回当前文件的文件描述符,该文件描述符是一个整数。
  • fileinput.filelineno():返回当前读取的行在其文件中的行号。
  • fileinput.isfirstline():返回当前读取的行在其文件中是否为第一行。
  • fileinput.isstdin():返回最后一行是否从sys.stdin读取。程序可以使用使用“-”代表从sys.stdin读取。
  • fileinput.nextfile():关闭当前文件,开始读取下一个文件。
  • fileinput.close():关闭Filelnput对象。

fileinput也存在一个缺陷,即在创建filelnput对象时不能指定字符集,因此它所读取的文件的字符集必须与操作系统默认的字符集保持一致。当然,如果文本文件的内容是纯英文,则不存在字符集的问题。

import fileinput
# 一次读取多个文件
for line in fileinput.input(files=('info.txt', 'test.txt')):
    # 输出文件名,当前行在当前文件中的行号
    print(fileinput.filename(), fileinput.filelineno(), line, end='')
# 关闭文件流
fileinput.close()

文件迭代器

实际上, 文件对象本身就是可遍历的 (就像一个序列一样),因此,程序完全可以使用for循环来遍历文件内容。

import codecs
# 指定使用utf-8字符集读取文件内容
f = codecs.open("for_file.py", 'r', 'utf-8', buffering=True)
# 使用for循环遍历文件对象
for line in f:
    print(line, end='')
f.close()

程序也可以 使用list()函数将文件转换成list列表 ,就像文件对象的readlines()方法的返回值一样。

# 将文件对象转换为list列表
print(list(codecs.open("for_file.py", 'r', 'utf-8', buffering=True)))

此外 sys.stdin也是一个类文件对象 (类似于文件的对象, Python 的很多 I/O 流都是类文件对象) ,因此,程序同样可以使用for循环遍历sys.stdin,这意味着程序可以通过for循环来获取用户的键盘输入。

import sys
# 使用for循环遍历标准输入
for line in sys.stdin:
    print('用户输入:', line, end='')

管道输入

系统标准输入sys.stdin也是一个类文件对象,因此,Python程序可以通过sys.stdin来读取键盘输入。但在某些时候,Python程序希望读取的输入不是来自用户,而是来自某个命令,此时就需要使用管道输入了。

管道的作用在于将前一个命令的输出,当成下一个命令的输入。不管是 UNIX 系统还是Windows 系统,它们都支持管道输入 。

管道输入的语法如下: cmd1 | cmd2 | cmd3

上面语法的作用是:cmd1命令的输出,将会传给cmd2命令作为输入;cmd2命令的输入,又会传给 cmd3 命令作为输出。

import sys
import re

# 定义匹配Email的正则表达式
mailPattern = r'([a-z0-9]*[-_]?[a-z0-9]+)*@([a-z0-9]*[-_]?[a-z0-9]+)+'\
    + '[\.][a-z]{2,3}([\.][a-z]{2})?'
# 读取标准输入
text = sys.stdin.read()
# 使用正则表达式执行查找
it = re.finditer(mailPattern, text , re.I)
# 输出所有的电子邮件
for e in it:
    print(str(e.span()) + "-->" + e.group())

上面程序使用sys.stdin来读取标准输入的内容,并使用正则表达式匹配所读取符串中的E-mail地址。

如果程序使用管道输入的方式,就可以把前一个命令的输出当成pipein_test.py这个程序的输入。例如使用如下命令: type ad.txt | python pipein test.py

上面的管道命令由两个命令组成:

  • type ad.txt:该命令使用type读取ad.txt文件的内容,并将文件内容输出到控制台。但由于使用了管道,因此该命令的输出会传给下一个命令。
  • python pipein_test.py 该命令使用 python 执行 pipein_test.py 程序。由于该命令前面有管道,因此它会把前一个命令的输出当成输入。

使用linecache随机读取指定行

linecache模块允许从Python源文件中随机读取指定行,并在内部使用缓存优化存储。由于该模块主要被设计成读取Python源文件,因此它会用UTF-8字符集来读取文本文件。实际上,使用linecache模块也可以读取其他文件,只要该文件使用了UTF-8字符集存储。

linecache模块包含以下常用函数。

  • linecache.getline(filename, lineno, module_globals=None) :读取指定模块中指定文件的行。其中filename指定文件名,lineno指定行号。
  • linecache.clearcache () :清空缓存。
  • linecache.checkcache(filename=None) :检查缓存是否有效。如果没有指定filename参数,则默认检查所有缓存的数据。
import linecache
import random

# 读取random模块的源文件的第3行
print(linecache.getline(random.__file__, 3))
# 读取本程序的第3行
print(linecache.getline('linecache_test.py', 3))
# 读取普通文件的第2行
print(linecache.getline('utf_text.txt', 2))

如果以r+、w、w+、a、a+模式打开文件,则都可以写入 。需要指出的是,当以r+、w、w+模式打开文件时,文件指针位于文件开头处;当以a、a+模式打开文件时,文件指针位于文件结尾处; 当以w、w+模式打开文件时,程序会立即清空文件的内容。

文件指针的概念

文件指针用于标明文件读写的位置。假如把文件看成一个水流,文件中每个数据(以b模式打开,每个数据就是一个字节;以普通模式打开,每个数据就是一个字符)就相当于一个水滴,而文件指针就标明了文件将要读写哪个位置。

文件对象提供了以下方法来操作文件指针。

  • seek(offset[, whence]) :该方法把文件指针移动到指定位置。当whence为0时(这是默认值),表明从文件开头开始计算, 比如将offset设为3,就是将文件指针移动到第3处;当whence为1时,表明从指针当前位置开始计算,比如文件指针当前在第5处,将offset设为 3,就是将文件指针移动到第8处;当whence为2时,表明从文件结尾开始计算,比如将offset设为-3,表明将文件指针移动到文件结尾倒数第3处。
  • tell() :判断文件指针的位置。

此外,当程序使用文件对象读写数据时,文件指针会自动向后移动;读写了多少个数据,文件指针就自动向后移动多少个位置。

f = open('filept_test.py', 'rb')
# 判断文件指针的位置
print(f.tell()) # 0
# 将文件指针移动到3处
f.seek(3)
print(f.tell()) # 3
# 读取一个字节,文件指针自动后移1个数据
print(f.read(1)) # o
print(f.tell())  # 4
# 将文件指针移动到5处
f.seek(5)
print(f.tell())  # 5
# 将文件指针向后移动5个数据
f.seek(5, 1)
print(f.tell())  # 10
# 将文件指针移动到倒数第10处
f.seek(-10, 2)
print(f.tell())
print(f.read(1))  # d

输出内容

文件对象提供的写文件的方法主要有两个。

  • write(str或bytes) :输出字符串或字节串。只有二进制模式(b模式) 打开的文件才能写入字节串。
  • writelines(可迭代对象) :输出多个字符串或多个字节串。
import os
f = open('x.txt', 'w+')
# os.linesep代表当前操作系统上的换行符
f.write('我爱Python' + os.linesep)
f.writelines(('zhaoyang'+ os.linesep,
    'blog'+ os.linesep,
    'csdn'+ os.linesep,
    'net'+ os.linesep))

当采用上面方法输出文件时,程序会使用当前操作系统默认的字符集。如果需要使用指定的字符集来输出文件,则可以采用二进制形式,程序先将所输出的字符串转换成指定字符集对应的二进制数据(字节串),然后输出二进制数据。

import os
f = open('y.txt', 'wb+')
# os.linesep代表当前操作系统上的换行符
f.write(('我爱Python' + os.linesep).encode('utf-8'))
f.writelines((('zhaoyang,'+ os.linesep).encode('utf-8'),
    ('blog'+ os.linesep).encode('utf-8'),
    ('csdn'+ os.linesep).encode('utf-8'),
    ('net'+ os.linesep).encode('utf-8')))

从上面的程序可以看到,当使用w+、wb+模式打开文件时,会导致文件内容被清空。因此,无论程序运行多少次,其输出的文件内容都只保留最近一次的输出数据。 如果程序希望在文件后面追加内容,则应该使用a+或ab+模式。

import os
f = open('z.txt', 'a+')
# os.linesep代表当前操作系统上的换行符
f.write('我爱Python' + os.linesep)
f.writelines(('zhaoyang'+ os.linesep,
    'blog'+ os.linesep,
    'csdn'+ os.linesep,
    'net'+ os.linesep))

上面程序以a+模式打开指定文件,这意味着以追加模式来打开文件,因此,使用open()函数打开文件后,不会立即清空文件内容,并且会将文件指针移动到文件结尾处,程序会在文件结尾处追加内容。