函数使用(五)
前言
- 这一篇是
os的目录操作、os.path的路径处理,以及文件读写(open的七种模式、文件指针、编码、with)。 - 所有示例都在 Python 3.13.9 / Windows 上实跑过,输出原样贴在代码块下面;涉及建目录、删文件的片段都跑在
tempfile.mkdtemp()开出的临时目录里,不会碰到真实文件。标「POSIX」的是 Linux/macOS 上的通用表现,本机没实测。 - 原笔记里几处凭印象写的说法已经改掉:不指定
encoding并不是「系统自动识别」;removedirs也不是「递归删除目录」;文本模式下seek的限制不止「相对当前」这一种。
目录管理(os)
⚠️注意:用之前先
import os。
-
system:执行系统命令
- 传入一条 shell 命令并执行,返回的是命令的退出码,不是命令的输出;要拿输出得用
os.popen(...).read()或subprocess。 cls是 Windows 的清屏命令,类 Unix 上要用clear,跨平台写法:os.system('cls' if os.name == 'nt' else 'clear')。
1
2
3import os
print(os.system('cmd /c exit 3')) # 让 cmd 以退出码 3 结束输出:
1
3
- 传入一条 shell 命令并执行,返回的是命令的退出码,不是命令的输出;要拿输出得用
-
name:操作系统名称
os.name:Windows 是'nt',Linux / macOS 等类 Unix 是'posix',在 JVM(Jython)上是'java'。- 想再细分(区分 Linux 和 macOS)用
sys.platform:Windows'win32'、Linux'linux'、macOS'darwin'。
1
2
3
4import os, sys
# nt 代表 Windows;要细分平台看 sys.platform
print(os.name, sys.platform)输出:
1
nt win32
-
environ:环境变量
os.environ是个映射;Windows 上它大小写不敏感(env['path']能取到PATH),POSIX 上区分大小写,别赌。- 取不存在的键:下标写法抛
KeyError,get写法返回None(os.getenv和os.environ.get一回事)。探测变量一律走get。
1
2
3
4
5
6import os
env = os.environ
print(env['path'] == env['PATH']) # Windows 上为 True
print(env.get('HELLO'))
print(env.get('HELLO', '给个默认值'))输出:
1
2
3True
None
给个默认值 -
getcwd:当前工作目录
1
2
3import os
print(os.path.isabs(os.getcwd())) # 给的一定是绝对路径输出:
1
True
相关的还有
os.chdir(路径)切换工作目录、os.curdir('.')、os.pardir('..')。 -
mkdir 与 makedirs:建目录
函数 中间目录 目录已存在 典型异常 os.mkdir('a/b')不创建,父目录不在就失败 报错 FileNotFoundError/FileExistsErroros.makedirs('a/b')一路补齐 报错,除非 exist_ok=TrueFileExistsError1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import os, tempfile
os.chdir(tempfile.mkdtemp()) # 进临时目录,别脏了自己的盘
os.mkdir('hello') # 单层没问题
try:
os.mkdir('deep/x') # 父目录 deep 不存在
except FileNotFoundError:
print('mkdir 只能建单层')
os.makedirs('deep/x/y') # 同样的路径交给 makedirs
print(os.path.isdir('deep/x/y'))
os.makedirs('deep/x/y', exist_ok=True) # 重复创建不报错
try:
os.makedirs('deep/x/y') # 不给 exist_ok 还是报错
except FileExistsError:
print('makedirs 已存在会报 FileExistsError')输出:
1
2
3mkdir 只能建单层
True
makedirs 已存在会报 FileExistsError -
rmdir / remove / unlink / removedirs:删除
os.rmdir(目录):只能删空目录。非空抛OSError(Windows 上e.winerror是 145「目录不是空的」),不存在抛FileNotFoundError。os.remove(文件)/os.unlink(文件):删文件,两个名字一回事(unlink是 Unix 的习惯叫法)。传目录进去,Windows 抛PermissionError,POSIX 抛IsADirectoryError。os.removedirs(路径):不是「递归删除目录」。它做的是「删掉叶子目录,然后一路往上删那些空了的上级目录」;往上删时遇到删不掉的(非空、没权限)就静默停下、不报错。只有叶子本身删不掉时才会抛OSError。- 真要把非空目录整棵删掉,用
shutil.rmtree(路径)——标准库里的rm -rf,别自己写递归。 - Windows 上有个坑:
shutil.rmtree删不掉进程当前所在的目录(PermissionError,winerror32),要先os.chdir跳出去再删。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import os, tempfile
os.chdir(tempfile.mkdtemp())
os.makedirs('a/b/c')
open('a/b/c/secret.txt', 'w').close()
try:
os.rmdir('a/b/c') # 里面还有文件
except OSError as e:
print('rmdir 非空目录:', type(e).__name__, e.winerror)
os.remove('a/b/c/secret.txt') # 先清文件(os.unlink 效果一样)
os.rmdir('a/b/c')
print('清空后 rmdir 成功:', not os.path.exists('a/b/c'))
os.makedirs('p/q/r') # p 里放个文件,让「往上删」的时候卡住
open('p/top.txt', 'w').close()
os.removedirs('p/q/r') # 不抛异常
print('r 删了:', not os.path.exists('p/q/r'), 'q 删了:', not os.path.exists('p/q'), ' p 还在:', os.path.exists('p'))
try:
os.removedirs('p') # 这次卡在第一下:叶子 p 非空
except OSError:
print('removedirs 只有叶子删不掉时才抛 OSError')输出:
1
2
3
4rmdir 非空目录: OSError 145
清空后 rmdir 成功: True
r 删了: True q 删了: True p 还在: True
removedirs 只有叶子删不掉时才抛 OSError -
rename / renames / replace:改名与移动
os.rename(old, new):文件和目录都能改;改成不同目录的路径,效果就是「移动」。- 平台差异:目标已存在时,Windows 抛
FileExistsError,POSIX 直接覆盖。要跨平台地覆盖,用os.replace(old, new)。 os.renames(old, new):比rename多做两件事——新路径的中间目录帮你建出来,旧路径留下的空上级目录顺手删掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import os, tempfile
os.chdir(tempfile.mkdtemp())
open('old.txt', 'w').write('1')
open('target.txt', 'w').write('2')
os.rename('old.txt', 'renamed.txt')
print('改名:', os.path.exists('renamed.txt'), '旧名字没了:', not os.path.exists('old.txt'))
try:
os.rename('renamed.txt', 'target.txt') # Windows 上目标已存在
except FileExistsError:
print('Windows 上 rename 不覆盖已存在的目标')
os.replace('renamed.txt', 'target.txt') # 覆盖语义明确
print('replace 之后:', os.path.exists('target.txt'), '源消失:', not os.path.exists('renamed.txt'))
os.makedirs('rn/a/b')
os.renames('rn/a/b', 'rn2/c/d') # 新的中间目录自动建
print('renames:', os.path.isdir('rn2/c/d'), '旧的空上级 rn 被顺手删了:', not os.path.exists('rn'))输出:
1
2
3
4改名: True 旧名字没了: True
Windows 上 rename 不覆盖已存在的目标
replace 之后: True 源消失: True
renames: True 旧的空上级 rn 被顺手删了: True -
stat / listdir / scandir / walk:看信息
os.stat(路径)返回os.stat_result,常用字段:st_size(字节数)、st_mtime(修改时间戳,浮点数)、st_mode(类型与权限位)。os.listdir(目录)返回名字列表,不含路径——要判断或打开,必须先os.path.join拼回去。os.scandir(目录)返回DirEntry迭代器,entry.name/entry.is_file()/entry.is_dir()都现成,比listdir之后再一个个isdir快(少一轮系统调用)。os.walk(目录)是生成器,每层产出(当前目录, 子目录列表, 文件列表)三元组,递归遍历目录树就靠它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import os, tempfile
os.chdir(tempfile.mkdtemp())
os.makedirs('tree/sub')
open('tree/x.py', 'w').write('12345')
open('tree/y.py', 'w').close()
# 取大小、时间戳等元信息
st = os.stat('tree/x.py')
print('st_size =', st.st_size, ' st_mtime 是浮点数:', isinstance(st.st_mtime, float))
print('listdir 只给名字:', sorted(os.listdir('tree')))
print('scandir:', sorted((e.name, e.is_file()) for e in os.scandir('tree')))
# 每层给一个三元组,递归遍历靠它
for dirpath, dirs, files in os.walk('tree'):
print('walk:', os.path.relpath(dirpath, 'tree'), sorted(dirs), sorted(files))输出:
1
2
3
4
5st_size = 5 st_mtime 是浮点数: True
listdir 只给名字: ['sub', 'x.py', 'y.py']
scandir: [('sub', False), ('x.py', True), ('y.py', True)]
walk: . ['sub'] ['x.py', 'y.py']
walk: sub [] [] -
相对路径、绝对路径与分隔符
- 相对路径:有参考目录的写法。
.表示当前目录(./xxx里的./通常可省),..表示上一级。 - 绝对路径:Windows 以盘符开头(
C:\...)或以\\开头(UNC 网络路径),类 Unix 以/开头。 - 分隔符:类 Unix 只有
/;Windows 两种都认(/和\)。代码里统一写/,或者干脆全部用os.path.join,别手拼反斜杠——路径以\结尾时还会和字符串转义打架。 - 相对路径的参考点是当前工作目录(
os.getcwd()),不是脚本所在目录。脚本要读自己旁边的文件,用os.path.dirname(os.path.abspath(__file__))拼。
- 相对路径:有参考目录的写法。
-
整体示例(目录管理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import os, shutil, sys, tempfile
root = tempfile.mkdtemp(prefix='fn5_')
os.chdir(root) # 之后的相对路径都落在临时目录里
# 1) 建
os.mkdir('hello')
os.makedirs('a/b/c')
print('建目录:', os.path.isdir('hello'), os.path.isdir('a/b/c'))
# 2) 看
open('a/b/c/secret.txt', 'w').write('12345')
st = os.stat('a/b/c/secret.txt')
print('stat:', st.st_size, isinstance(st.st_mtime, float))
print('listdir:', os.listdir('a/b/c'), '拼回去才能判断:',
os.path.isfile(os.path.join('a/b/c', 'secret.txt')))
for dirpath, dirs, files in os.walk('a'):
print('walk:', dirpath.replace(os.sep, '/'), dirs, files)
# 3) 改
os.replace('a/b/c/secret.txt', 'a/b/c/keep.txt')
print('replace 改名:', os.path.isfile('a/b/c/keep.txt'))
# 4) 删:先文件后目录,往上删空的交给 removedirs
os.remove('a/b/c/keep.txt')
os.rmdir('a/b/c')
os.removedirs('a/b') # b 空了,连带 a 一起删
print('删完:', not os.path.exists('a'), 'hello 还在:', os.path.isdir('hello'))
# 5) 平台与环境
print('os.name:', os.name, 'sys.platform:', sys.platform)
print('PATH 取到没有:', bool(os.environ.get('PATH')), '不存在的键:', os.environ.get('HELLO'))
print('system 返回退出码:', os.system('cmd /c exit 3') if os.name == 'nt' else os.system('exit 3'))
# 6) 收尾:非空目录整棵删掉用 shutil.rmtree
# Windows 上不能删「自己正站着的目录」(PermissionError, winerror 32),先跳出去
os.chdir(tempfile.gettempdir())
shutil.rmtree(root)
print('清理:', not os.path.exists(root))实跑输出:
1
2
3
4
5
6
7
8
9
10
11
12建目录: True True
stat: 5 True
listdir: ['secret.txt'] 拼回去才能判断: True
walk: a ['b'] []
walk: a/b ['c'] []
walk: a/b/c [] ['secret.txt']
replace 改名: True
删完: True hello 还在: True
os.name: nt sys.platform: win32
PATH 取到没有: True 不存在的键: None
system 返回退出码: 3
清理: True
os.path:路径处理
这些函数大多只做字符串运算(只有
exists/isdir/isfile/getsize/realpath会真的去问文件系统),所以传进去的路径存不存在它们都不报错。写法上import os之后用os.path.xxx,或者from os import path之后用path.xxx。
-
常用函数实测
在 Windows 上跑,注意分隔符会被换成本平台的
\:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21from os import path
# Windows 上会拼出混合分隔符
print(path.join('abc/def', '123.py'))
# 可以多段一次拼
print(path.join('a', 'b', 'c'))
print(path.join('a', 'C:/x'), '(遇到绝对路径参数,前面的全丢)')
# 切后缀,注意无后缀和隐藏文件
print(path.splitext('789.py'), path.splitext('noext'), path.splitext('.hidden'))
# 最后一个分隔符之前
print(path.dirname('132/456/789.py'))
# 之后;反斜杠也认
print(path.basename('132/456/789.py'), path.basename('a\\b\\c.py'))
# 一次拿到上面两个
print(path.split('132/456/789.py'))
# 就地化简 . 和 ..
print(path.normpath('a/./b/../c'))
# 拆出盘符
print(path.splitdrive('C:/a/b'))
# 判断是否绝对路径
print(path.isabs('ld'), path.isabs('C:/a'))输出:
1
2
3
4
5
6
7
8
9
10abc/def\123.py
a\b\c
C:/x (遇到绝对路径参数,前面的全丢)
('789', '.py') ('noext', '') ('.hidden', '')
132/456
789.py c.py
('132/456', '789.py')
a\c
('C:', '/a/b')
False True几个要点:
join只负责「按本平台的规则拼」,输入里混着/它不帮你换,所以产物可能是abc/def\123.py这种混合分隔符——Windows 上照样能用,但别拿它去和字符串字面量比相等。join的参数里一旦出现绝对路径(C:/x或/y),前面全部作废。这是「以某个绝对路径为基准拼子路径」的常用写法,不是 bug。splitext('.hidden')认为它没有后缀(返回('.hidden', '')),开头的点不算后缀分隔点。dirname/basename/split就是「最后一个分隔符前 / 后」的关系,split一次给你两个。normpath会把.、..就地化简,并把分隔符统一成本平台的。
-
判断类函数
exists(存在吗,文件或目录都算)、isdir、isfile、getsize、abspath/realpath。1
2
3
4
5
6
7
8
9
10
11
12
13import os, tempfile
from os import path
os.chdir(tempfile.mkdtemp())
os.makedirs('c')
with open(path.join('c', '123.py'), 'w', encoding='utf-8') as f:
f.write('12345')
print(path.exists('c'), path.exists('ghost'))
print(path.isdir('c'), path.isfile('c'), path.isfile(path.join('c', '123.py')))
print(path.getsize(path.join('c', '123.py')))
print(path.isabs(path.abspath('c')))
print(path.realpath('c') == path.abspath('c')) # 没有软链接时两者相同输出:
1
2
3
4
5True False
True False True
5
True
True判断一定要先把目录和文件名拼好。
path.exists('123.py')和path.exists(path.join('c', '123.py'))判的是两个完全不同的位置——练习题里那句「中间使用的所有文件目录的判断都需要进行拼接」说的就是这个。 -
整体示例(路径处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import os, shutil, tempfile
from os import path
os.chdir(tempfile.mkdtemp())
os.makedirs('project/src/pkg')
open('project/src/pkg/main.py', 'w').write('print(1)')
open('project/readme', 'w').close() # 没有后缀的文件
base = 'project'
for name in os.listdir(base):
full = path.join(base, name) # listdir 给的是名字,先拼
print(name, '目录' if path.isdir(full) else '文件',
'有后缀' if path.splitext(name)[1] else '无后缀')
# 用 splitext 归类(目录整理练习的核心)
for dirpath, dirs, files in os.walk('project'):
for f in files:
ext = path.splitext(f)[1].lstrip('.').upper() or 'OTHERS'
print(path.relpath(path.join(dirpath, f), 'project'), '->', ext)
# 路径信息一次拿全
target = 'project/src/pkg/main.py'
print('dirname:', path.dirname(target))
print('basename:', path.basename(target))
print('split:', path.split(target))
print('abspath 比原路径长:', len(path.abspath(target)) > len(target))
print('normpath:', path.normpath('project/./src/../src/pkg'))
here = os.getcwd()
os.chdir(tempfile.gettempdir()) # Windows 上不能删自己站着的目录
shutil.rmtree(here)实跑输出:
1
2
3
4
5
6
7
8
9readme 文件 无后缀
src 目录 无后缀
readme -> OTHERS
src\pkg\main.py -> PY
dirname: project/src/pkg
basename: main.py
split: ('project/src/pkg', 'main.py')
abspath 比原路径长: True
normpath: project\src\pkg
文件管理
-
打开文件:
open(文件路径, 打开方式, encoding=...)- 参数一:文件路径(相对、绝对都行)。
- 参数二:打开方式(模式),见下表。
- 参数三:
encoding。原笔记写「通常可以不指定,系统会自动识别处理」,这句是错的:不指定时 Python 用的是系统区域编码(本机locale.getpreferredencoding(False)返回cp936,就是 GBK),它不会去「识别」文件本身是什么编码。后果是 Windows 上写的文件,拿到 Linux/macOS 上读就乱码甚至直接抛异常。
1
2
3
4
5
6
7
8
9
10
11
12
13import os, tempfile
os.chdir(tempfile.mkdtemp())
with open('gbk.txt', 'w') as f: # 故意不写 encoding
f.write('中文')
print(open('gbk.txt', 'rb').read()) # 落盘其实是 GBK 字节
try:
open('gbk.txt', encoding='utf-8').read()
except UnicodeDecodeError:
print('按 utf-8 读它会直接报错')
with open('u8.txt', 'w', encoding='utf-8') as f: # 显式指定才是稳的
f.write('中文')
print(open('u8.txt', encoding='utf-8').read())输出:
1
2
3b'\xd6\xd0\xce\xc4'
按 utf-8 读它会直接报错
中文结论:文本文件永远显式写
encoding='utf-8'。 -
七种模式
模式 读 写 文件不存在 文件已存在 初始指针 r(默认)是 否 FileNotFoundError保留内容 0 w否 是 新建 清空内容 0 a否 是 新建 保留;写入总在末尾 0(写之前会跳到末尾) r+是 是 FileNotFoundError保留内容 0 w+是 是 新建 清空内容 0 a+是 是 新建 保留;写入总在末尾 末尾 x否 是 新建 FileExistsError0 模式后加
b(rb/wb/ab/rb+/wb+/ab+/xb)就是二进制:读写的是bytes;不加b的都是str。1
2
3
4
5
6
7
8
9
10
11
12
13
14import os, tempfile
os.chdir(tempfile.mkdtemp())
# 每个模式配一个自己的文件名,不然前面的 w 已经把文件建出来了,x 会被抢跑
for mode in ('r', 'r+', 'w', 'w+', 'a', 'a+', 'x'):
try:
open(f'p_{mode}.txt', mode).close()
print(f'{mode:3} 打开成功')
except OSError as e:
print(f'{mode:3} {type(e).__name__}')
try:
open('p_x.txt', 'x') # 这个已经存在了
except OSError as e:
print('x 遇到已存在的文件:', type(e).__name__)输出:
1
2
3
4
5
6
7
8r FileNotFoundError
r+ FileNotFoundError
w 打开成功
w+ 打开成功
a 打开成功
a+ 打开成功
x 打开成功
x 遇到已存在的文件: FileExistsError读、写两种模式对「文件不存在」的态度一目了然:要读就必须已存在,要写就帮你建。
x的意义是「我只新建,已存在就别动它」,常用来防覆盖。 -
为什么
w+/a+直接读是空w+:一打开就把文件清空,所以读到空——哪怕你一个字节都没写。a+:内容还在,但指针默认在末尾,而读是从指针位置往后读,所以读出空;要先seek(0)。a/a+的写操作永远落在末尾,中途seek(0)也改变不了这件事(想原位覆盖得用r+/w+)。- 空文件的话,所有模式打开后指针都是 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import os, tempfile
os.chdir(tempfile.mkdtemp())
with open('ap.txt', 'w') as f:
f.write('hello')
# 可读写,但指针一开始在末尾
with open('ap.txt', 'a+') as f:
print('a+ 初始指针:', f.tell(), '直接 read:', repr(f.read()))
f.seek(0)
print('seek(0) 之后 read:', repr(f.read()))
# 一打开就清空
with open('ap.txt', 'w+') as f:
print('w+ 一打开就清空:', repr(f.read()))
with open('ap.txt', 'w') as f:
f.write('hello')
# 只写;写永远落在末尾
with open('ap.txt', 'a') as f:
f.seek(0)
f.write('X')
print('a 模式 seek 无效,还是追加:', repr(open('ap.txt').read()))输出:
1
2
3
4a+ 初始指针: 5 直接 read: ''
seek(0) 之后 read: 'hello'
w+ 一打开就清空: ''
a 模式 seek 无效,还是追加: 'helloX' -
文件指针:
tell与seekfp.tell():当前指针位置。fp.seek(偏移, 参考位置):参考位置0从开头算(默认)、1从当前算、2从末尾算,对应常量io.SEEK_SET/io.SEEK_CUR/io.SEEK_END。- 文本模式的限制(原笔记只说了「相对当前不行」,其实相对末尾也不行):
seek(1, 1)、seek(-5, 2)都会抛UnsupportedOperation。文本模式只认seek(0)、seek(0, 2),以及「之前tell()拿回来的那个值」。二进制模式三种参考位置随便用。 tell()在文本模式下是不透明值:它不等于字符数,只能原样拿回去喂给seek,别拿它做算术。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import os, tempfile
os.chdir(tempfile.mkdtemp())
with open('t.txt', 'w', encoding='utf-8') as f:
f.write('hello world')
with open('t.txt', encoding='utf-8') as f:
# 参考位置 2 = 从末尾算
f.seek(0, 2)
print('seek(0,2) 到末尾:', f.tell(), repr(f.read()))
# 参考位置 0 = 从开头算
f.seek(6, 0)
print('seek(6,0):', repr(f.read()))
# 文本模式这两种参考位置都不让用
for whence in (1, 2):
try:
f.seek(1, whence)
except OSError:
print(f'文本模式 seek(1,{whence}) 报错: UnsupportedOperation')
# 换成二进制,限制就没了
with open('t.txt', 'rb') as f:
f.seek(1, 1)
print('二进制 seek(1,1):', f.tell())
# 从末尾倒着数
f.seek(-5, 2)
print('二进制 seek(-5,2):', f.read())输出:
1
2
3
4
5
6seek(0,2) 到末尾: 11 ''
seek(6,0): 'world'
文本模式 seek(1,1) 报错: UnsupportedOperation
文本模式 seek(1,2) 报错: UnsupportedOperation
二进制 seek(1,1): 1
二进制 seek(-5,2): b'world'中文文件最能看出
tell()不可解释:1
2
3
4
5
6
7
8
9
10
11
12import os, tempfile
os.chdir(tempfile.mkdtemp())
with open('zh.txt', 'w', encoding='utf-8') as f:
f.write('中文字\n')
f = open('zh.txt', encoding='utf-8')
# 读一个字符(UTF-8 下是 3 字节)
f.read(1)
# tell 给的是不透明凭据,不是字符数
print('读 1 个字符后 tell():', f.tell())
print('拿这个值 seek 回去:', repr(f.read()))
f.close()输出:
1
2读 1 个字符后 tell(): 3
拿这个值 seek 回去: '文字\n'一个汉字在 UTF-8 里占 3 个字节,所以
tell()给的是 3——它是字节层面的凭据,不是「读了 3 个字符」。 -
read(n):文本模式数「字符」,二进制模式数「字节」同一句
read(2),两种模式给的不是一回事:文本模式已经按encoding解码完了,给你 2 个字符;二进制模式不解码,给你 2 个字节。而一个汉字在 UTF-8 里要占 3 个字节,2 个字节只是它的三分之二,是一段残缺的字节序列——拿去decode('utf-8')会直接报错。写代码时这条的用处是:「二进制分块读 + 每块立刻解码」的写法,只要块大小不是字符字节数的整数倍,就会在半个汉字处切断。要么攒够了整块再解,要么用增量解码器把断点续上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import codecs, os, tempfile
os.chdir(tempfile.mkdtemp())
with open('zh.txt', 'w', encoding='utf-8', newline='') as f: # newline='' 不换行符
f.write('中文字')
print('文本模式 read(2):', repr(open('zh.txt', encoding='utf-8').read(2)))
part = open('zh.txt', 'rb').read(2)
print('二进制模式 read(2):', part, '—— 一个汉字要 3 字节,这里只有 2 字节')
try:
part.decode('utf-8')
except UnicodeDecodeError as e:
print('拿它解码:', type(e).__name__, e)
print('二进制 read(3) 才凑满一个汉字:', open('zh.txt', 'rb').read(3).decode('utf-8'))
dec = codecs.getincrementaldecoder('utf-8')()
out = []
with open('zh.txt', 'rb') as f:
while chunk := f.read(2): # 每次只读 2 字节,一定会把汉字劈断
out.append(dec.decode(chunk)) # 增量解码器会把断点续上
print('增量解码器读全文:', repr(''.join(out)))输出:
1
2
3
4
5文本模式 read(2): '中文'
二进制模式 read(2): b'\xe4\xb8' —— 一个汉字要 3 字节,这里只有 2 字节
拿它解码: UnicodeDecodeError 'utf-8' codec can't decode bytes in position 0-1: unexpected end of data
二进制 read(3) 才凑满一个汉字: 中
增量解码器读全文: '中文字'顺带一提:拷贝文件那种「二进制读一块、写一块」不受影响,因为压根不解码,字节原样搬运——所以分块拷贝用二进制模式,见下面整体示例的第 6 步。
-
编码方式
ASCII:7 位,128 个字符,只有英文字母、数字和常用符号。ANSI:Windows 语境下对「本地编码」的统称,不是「扩展 ASCII」:中文 Windows 的 ANSI 是 GBK 系(代码页 936),西欧是 Windows-1252,各说各话。gb2312/gbk/gb18030:中国的汉字编码标准,收录范围逐级扩大(GBK 向下兼容 GB2312)。中文 Windows 的系统编码就属于这一族。Unicode:给每个字符编一个号(码点)的字符集标准,本身不规定怎么存成字节。UTF-8:Unicode 的一种存储实现,变长 1~4 字节——英文 1 字节、中文通常 3 字节,既省空间又兼容 ASCII。另有UTF-16(Windows 内部用)、UTF-32。utf-8-sig:写文件时在开头带 3 字节 BOM(\xef\xbb\xbf),给只认「带 BOM 的 UTF-8」的老程序(比如老版 Excel)用;用普通utf-8读带 BOM 的文件,会把 BOM 读成一个\ufeff字符。
1
2
3
4
5
6print(b'hello'.decode('utf-8')) # 解码 bytes -> str
print('hello'.encode('utf-8')) # 编码 str -> bytes
print('中文'.encode('utf-8'), len('中文'.encode('utf-8')))
print('中文'.encode('gbk'), len('中文'.encode('gbk')))
print('中'.encode('utf-8-sig'))
print(hex(ord('中')))输出:
1
2
3
4
5
6hello
b'hello'
b'\xe4\xb8\xad\xe6\x96\x87' 6
b'\xd6\xd0\xce\xc4' 4
b'\xef\xbb\xbf\xe4\xb8\xad'
0x4e2d -
读写接口
方法 作用 实测备注 read(n)读 n 个单位 文本模式是 n 个字符,二进制是 n 个字节;不传就一次读完 readline()读一行 返回值包含行尾的 \nreadlines()读所有行 返回列表,每项含 \n;大文件别用,会一次进内存write(s)写 不自动加换行, \n自己写;返回写入的长度writelines(seq)写一串 同样不补换行,元素得自己带 readable()/writable()能否读 / 能否写 r是True/False,w、a是False/True,r+两个都Trueflush()把缓冲区落盘 with结束或close()时也会做close()关闭 关闭后再读写抛 ValueError: I/O operation on closed filefor line in fp逐行遍历文件对象,结果和readlines()一致(也含\n),但它一次只拿一行,读大文件不爆内存,优先用这种。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import os, tempfile
os.chdir(tempfile.mkdtemp())
with open('lines.txt', 'w', encoding='utf-8') as f:
# writelines 不补换行,元素自己带
f.writelines(['hello\n', 'world\n', 'really\n'])
f = open('lines.txt', encoding='utf-8')
# 文件对象的 repr:模式和编码一目了然
print(f)
print('readable / writable:', f.readable(), f.writable())
# 读完指针就越过这一行了
print('readline:', repr(f.readline()))
# 所以上一行已经不在结果里
print('readlines 剩下:', f.readlines())
f.close()
with open('lines.txt', encoding='utf-8') as f:
# 遍历文件对象等价于 readlines,但一次只拿一行
print('for 逐行:', [x.rstrip() for x in f])
print('close 之后:', f.closed)输出:
1
2
3
4
5
6<_io.TextIOWrapper name='lines.txt' mode='r' encoding='utf-8'>
readable / writable: True False
readline: 'hello\n'
readlines 剩下: ['world\n', 'really\n']
for 逐行: ['hello', 'world', 'really']
close 之后: Truereadline()之后指针已经越过第一行,所以紧接着的readlines()只给剩下两行——这就是「读之前一定要确认指针位置」的意思。 -
with 语句
- 场景:文件操作中间无论读写、无论有没有异常,最后都必须关闭。
with:语句块结束时(包括抛异常、return提前离开)保证调用close(),不用自己操心。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import os, tempfile
os.chdir(tempfile.mkdtemp())
open('test.txt', 'w').write('hello world')
# 手工版:中间一旦抛异常,close() 就跑不到
fp = open('test.txt', 'rb')
print(fp.read(5))
fp.close()
# with 版
with open('test.txt', 'rb') as fp:
print(fp.read(5))
try:
with open('test.txt', 'rb') as fp:
raise RuntimeError('boom')
except RuntimeError:
print('块里抛异常也照样关闭:', fp.closed)输出:
1
2
3b'hello'
b'hello'
块里抛异常也照样关闭: Truewith不限于文件,任何实现了上下文协议(__enter__/__exit__)的对象都能用;多个资源可以写在一行:with open('a') as fa, open('b') as fb: ...。 -
文件删除
1
2
3
4
5
6import os, tempfile
os.chdir(tempfile.mkdtemp())
open('02-test.py', 'w').close()
os.remove('02-test.py') # os.unlink 效果一样
print('删掉了:', not os.path.exists('02-test.py'))输出:
1
删掉了: True
目录用
os.rmdir(只能空目录)或shutil.rmtree(整棵树)。 -
整体示例(文件读写全流程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50import os, shutil, tempfile
os.chdir(tempfile.mkdtemp())
# 1) 写:显式指定编码;write 不补换行要自己加
with open('note.txt', 'w', encoding='utf-8') as f:
f.write('第一行\n')
f.writelines(['第二行\n', '第三行\n'])
# 2) 读法一:逐行遍历(大文件友好)
with open('note.txt', encoding='utf-8') as f:
for line in f:
print('行:', line.rstrip())
# 3) 读法二:read(n) 在文本模式按字符算
with open('note.txt', encoding='utf-8') as f:
print('前 3 个字符:', repr(f.read(3)))
print('指针位置:', f.tell())
# 4) 指针:seek 三种参考位置,文本模式只认 0、2 和 tell 回来的值
with open('note.txt', encoding='utf-8') as f:
# 参考位置 2 = 从末尾算
f.seek(0, 2)
print('到末尾 tell:', f.tell(), '读:', repr(f.read()))
f.seek(0)
print('回到开头读一行:', repr(f.readline()))
# 5) a+ 的坑:指针在末尾,读之前先 seek(0);写永远落在末尾
with open('note.txt', 'a+', encoding='utf-8') as f:
print('a+ 直接读:', repr(f.read()))
f.seek(0)
print('seek(0) 后读第一行:', repr(f.readline()))
f.write('追加的一行\n')
print('文件现在:', repr(open('note.txt', encoding='utf-8').read()))
# 6) 拷贝(超大文件分块读写)+ 校验
CHUNK = 4
with open('note.txt', 'rb') as src, open('copy.txt', 'wb') as dst:
while True:
block = src.read(CHUNK)
if not block:
break
dst.write(block)
print('拷贝后大小一致:', os.path.getsize('note.txt') == os.path.getsize('copy.txt'))
# 7) 收尾
print('目录内容:', sorted(os.listdir('.')))
here = os.getcwd()
os.chdir(tempfile.gettempdir()) # Windows 上不能删自己站着的目录
shutil.rmtree(here)实跑输出:
1
2
3
4
5
6
7
8
9
10
11
12行: 第一行
行: 第二行
行: 第三行
前 3 个字符: '第一行'
指针位置: 9
到末尾 tell: 33 读: ''
回到开头读一行: '第一行\n'
a+ 直接读: ''
seek(0) 后读第一行: '第一行\n'
文件现在: '第一行\n第二行\n第三行\n追加的一行\n'
拷贝后大小一致: True
目录内容: ['copy.txt', 'note.txt']
总练习
- 实现一个拷贝文件的功能。提醒:要考虑超大文件,一次读 1024 字节,循环读、循环写(上面整体示例第 6 步就是这个思路,把
CHUNK换成 1024 即可)。 - 递归删除一个文件夹(自己用
os.listdir+os.remove/os.rmdir实现一遍,再和shutil.rmtree对比)。 - 统计一个文件夹的大小(
os.walk+os.path.getsize,注意getsize要拼完整路径)。 - 拷贝一个文件夹(先
os.makedirs建出目录树,再逐个拷贝文件)。 - 移动一个文件夹(同盘用
os.rename最快;跨盘会抛OSError,这时得「拷完再删」)。 - 目录整理:
- 一个目录中有各种文件,也有文件夹
- 将所有的文件夹统一放到 DIR 目录下
- 将没有后缀的文件统一放到 others 目录下
- 将有后缀的文件放到后缀名大写的文件夹下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26# 处理前
test/
dir1/
dir2/
1.py
2.py
3.txt
4.pdf
123
456
# 处理后
test/
DIR/
dir1/
dir2/
PY/
1.py
2.py
TXT/
3.txt
PDF/
4.pdf
OTHERS/
123
456友情提醒:中间用到的每一个「存在吗 / 是目录吗 / 多大」判断,都要先把目录和文件名
os.path.join拼好;直接拿裸名字判断,判的是当前工作目录下的同名对象。
参考
os模块(目录、进程相关):os — 操作系统接口os.path各函数:os.path — 常用路径操作open的参数与模式:内置函数 open- 文件对象的方法(
read/seek/tell与文本模式限制):标准类型 — 文件对象 with语句:复合语句 — with 上下文管理器- 字节串与编码:bytes 对象 — 编码说明