Python3 中使用 Pathlib 模块进行文件操作

更多参考官方文档 pathlib — 面向对象的文件系统路径

在本教程中,你将了解如何使用 pathlib 模块操作目录和文件的名称。 学习如何读取和写入文件,拼接路径和操作底层文件系统的新方法,以及如何列出文件并迭代它们的一些示例。 大多人处理文件用的最多的还是 os 模快吧,比如下面这样的操作

>>> path.rsplit('\\', maxsplit=1)[0]

或者写出下面这样长长的代码

>>> os.path.isfile(os.path.join(os.path.expanduser('~'), 'realpython.txt'))

使用 pathlib 模块,可以使代码使用优雅,可读和 Pythonic 代码重写上面的两个示例,如:

>>> path.parent
>>> (pathlib.Path.home() / 'realpython.txt').is_file()

Python 文件路径处理问题

由于许多不同的原因,使用文件和与文件系统交互很重要。 最简单的情况可能只涉及读取或写入文件,但有时候会有更复杂的任务。 也许你需要列出给定类型的目录中的所有文件,查找给定文件的父目录,或者创建一个尚不存在的唯一文件名。 一般情况,Python 使用常规文本字符串表示文件路径。 一般在使用 os,glob 和 shutil 等库的时候会使用到路径拼接的操作,使用 os 模块拼接起来显得略显复杂,以下示例仅需要三个 import 语句来将所有文本文件移动到归档目录:

import glob
import os
import shutil

for file_name in glob.glob('*.txt'):
    new_path = os.path.join('archive', file_name)
    shutil.move(file_name, new_path)

使用常规的字符串去拼接路径是可以的,但是由于不同的操作系统使用的分隔符不同,这样就容易出现问题,所以一般我们使用最多的还是使用 os.path.join ()。 Python 3.4 中引入了 pathlib 模块(PEP 428)再一次的优化了路径的拼接。使用 pathlib 库的 Path 方法,可以将一个普通的字符串转换为 pathlib.Path 对象类型的路径。 早期,其他软件包仍然使用字符串作为文件路径,但从 Python 3.6 开始,pathlib 模块在整个标准库中得到支持,部分原因是由于增加了文件系统路径协议。 如果你坚持使用传统的 Python,那么 Python 2 也有一个可用的向后移植。 ok,说了那么多下面让我们看看 pathlib 如何在实践中发挥作用。

创建路径

这里我们首先要知道两个用法,先看代码:

from pathlib import Path

你真正需要知道的是 pathlib.Path 类。 创建路径有几种不同的方式。 首先,有类方法,如 .cwd(当前工作目录)和 .home(用户的主目录):

from pathlib import Path

now_path = Path.cwd()
home_path = Path.home()

print("当前工作目录",now_path,type(now_path))
print("home目录",home_path,type(home_path))

输出内容

当前工作目录 /Users/chennan/pythonproject/demo <class 'pathlib.PosixPath'>
home目录 /Users/chennan <class 'pathlib.PosixPath'>

可以发现路径格式为 pathlib.PosixPath 这是在 unix 系统下的显示。在不同的系统上显示的格式也是不一样,在 windows 系统会显示为 WindowsPath。但是不管什么显示类型,都不影响后面的操作。 前面我们提到过可以通过把字符串类型的路径,转换为 Pathlib.Path 类型的路径,经过测试发现在 Python3.4 以后很多模块以及支持该格式的路径。不用转为成字符串使用了。比起 os.path.join 拼接路径的方式,pathlib 使用起来更加的方便,使用示例如下:

import pathlib
DIR_PATH = pathlib.Path("/Users/chennan/CDM")
print(DIR_PATH,type(DIR_PATH))

输出内容:

/Users/chennan/CDM <class 'pathlib.PosixPath'>

通过 “/“ 我们就可以对路径进行拼接了,怎么样是不是很方便呢。

读文件和写文件

在我们使用 open 来操作文件读写操作的时候,不仅可以使用字符串格式的路径,对于 pathlib 生成的路径完全可以直接使用:

path = pathlib.Path.cwd() / 'test.md'
with open(path, mode='r') as fid:
    headers = [line.strip() for line in fid if line.startswith('#')]
print('\n'.join(headers))

或者在 pathlib 的基础使用 open, 我们推荐使用下面的方式

import pathlib
DIR_PATH = pathlib.Path("/Users/chennan/CDM") / "2000" / "hehe.txt"
with DIR_PATH.open("r") as fs:
     data = fs.read() 
print(data)

这样写的好处就是 open 里面我们不需要再去传入路径了,直接指定文件读写模式即可。实际上这里的 open 方法,底层也是调用了 os.open 的方法。使用哪种方式看个人的喜好。 pathlib 还提供几种文件的读写方式:可以不用再使用 with open 的形式即可以进行读写。

.read_text(): 找到对应的路径然后打开文件,读成str格式。等同open操作文件的"r"格式。
.read_bytes(): 读取字节流的方式。等同open操作文件的"rb"格式。
.write_text(): 文件的写的操作,等同open操作文件的"w"格式。
.write_bytes(): 文件的写的操作,等同open操作文件的"wb"格式。

使用 resolve 可以通过传入文件名,来返回文件的完整路径,使用方式如下

import pathlib
py_path =pathlib.Path("superdemo.py")
print(py_path.resolve())

输出

/Users/chennan/pythonproject/demo/superdemo.py

需要注意的是 “superdemo.py” 文件要和我当前的程序文件在同一级目录。

选择路径的不同组成部分

pathlib 还提供了很多路径操作的属性,这些属性可以选择路径的不用部位,如 .name: 可以获取文件的名字,包含拓展名。 .parent: 返回上级文件夹的名字 .stem: 获取文件名不包含拓展名 .suffix: 获取文件的拓展名 .anchor: 类似盘符的一个东西,

import pathlib

now_path = pathlib.Path.cwd() / "demo.txt"
print("name",now_path.name)
print("stem",now_path.stem)
print("suffix",now_path.suffix)
print("parent",now_path.parent)
print("anchor",now_path.anchor)

输出内容如下

name demo.txt
stem demo
suffix .txt
parent /Users/chennan/pythonproject/demo
anchor /

移动和删除文件

当然 pathlib 还可以支持文件其他操作,像移动,更新,甚至删除文件,但是使用这些方法的时候要小心因为,使用过程不用有任何的错误提示即使文件不存在也不会出现等待的情况。 使用 replace 方法可以移动文件,如果文件存在则会覆盖。为避免文件可能被覆盖,最简单的方法是在替换之前测试目标是否存在。

import pathlib

destination = pathlib.Path.cwd() / "target" 
source = pathlib.Path.cwd() / "demo.txt"
if not destination.exists():
    source.replace(destination)

但是上面的方法存在问题就是,在多个进程多 destination 进行的操作的时候就会现问题,可以使用下面的方法避免这个问题。也就是说上面的方法适合单个文件的操作。

import pathlib

destination = pathlib.Path.cwd() / "target" 
source = pathlib.Path.cwd() / "demo.txt"
with destination.open(mode='xb') as fid: 
    #xb表示文件不存在才操作
    fid.write(source.read_bytes())

当 destination 文件存在的时候上面的代码就会出现 FileExistsError 异常。 从技术上讲,这会复制一个文件。 要执行移动,只需在复制完成后删除源即可。 使用 with_name 和 with.shuffix 可以修改文件名字或者后缀。

import pathlib
source = pathlib.Path.cwd() / "demo.py"
source.replace(source.with_suffix(".txt")) #修改后缀并移动文件,即重命名

可以使用 .rmdir () 和 .unlink () 来删除文件。

import pathlib

destination = pathlib.Path.cwd() / "target" 
source = pathlib.Path.cwd() / "demo.txt"
source.unlink()

几个 pathlib 的使用例子

统计文件个数

我们可以使用.iterdir 方法获取当前文件下的所以文件.

import pathlib
from collections import Counter
now_path = pathlib.Path.cwd()
gen = (i.suffix for i in now_path.iterdir())
print(Counter(gen))

输出内容

Counter({'.py': 16, '': 11, '.txt': 1, '.png': 1, '.csv': 1})

通过配合使用 collections 模块的 Counter 方法,我们获取了当文件夹下文件类型情况。 前面我们说过 glob 模块,同样的 pathlib 也有 glob 方法和 rglob 方法,不同的是 glob 模块里的 glob 方法结果是列表形式的,iglob 是生成器类型,在这里 pathlib 的 glob 模块返回的是生成器类型,然后 pathlib 还有一个支持递归操作的 rglob 方法。 下面的这个操作我通过使用 glob 方法,设定规则进行文件的匹配。

import pathlib
from  collections import Counter
gen =(p.suffix for p in pathlib.Path.cwd().glob('*.py'))
print(Counter(gen))

展示目录树

下一个示例定义了一个函数 tree (),该函数的作用是打印一个表示文件层次结构的可视树,该树以一个给定目录为根。因为想列出其子目录,所以我们要使用 .rglob () 方法:

import pathlib
from  collections import Counter
def tree(directory):
    print(f'+ {directory}')
    for path in sorted(directory.rglob('*')):
        depth = len(path.relative_to(directory).parts)
        spacer = '    ' * depth
        print(f'{spacer}+ {path.name}')

now_path = pathlib.Path.cwd()

if __name__ == '__main__':
    tree(now_path)

其中 relative_to 的方法的作用是返回 path 相对于 directory 的路径。 parts 方法可以返回路径的各部分。例如

import pathlib
now_path = pathlib.Path.cwd()
if __name__ == '__main__':
    print(now_path.parts)

返回

('/', 'Users', 'chennan', 'pythonproject', 'demo')

获取文件最后一次修改时间

iterdir (),.glob () 和.rglob () 方法非常适合于生成器表达式和列表理解。 使用 stat () 方法可以获取文件的一些基本信息,使用.stat ().st_mtime 可以获取文件最后一次修改的信息

import pathlib
now_path = pathlib.Path.cwd()
from datetime import datetime
time, file_path = max((f.stat().st_mtime, f) for f in now_path.iterdir())
print(datetime.fromtimestamp(time), file_path)

甚至可以使用类似的表达式获取上次修改的文件内容

import pathlib
from datetime import datetime
now_path =pathlib.Path.cwd()
result = max((f.stat().st_mtime, f) for f in now_path.iterdir())[1]
print(result.read_text())

.stat ().st_mtime 会返回文件的时间戳,可以使用 datetime 或者 time 模块对时间格式进行进一步转换。

其他内容

关于 pathlib.Path 格式路径转换为字符串类型

因为通过 pathlib 模块操作生成的路径,不能直接应用字符串的一些操作,所以需要转换成字符串,虽然可以使用 str () 函数进行转换,但是安全性不高,建议使用 os.fspath () 方法,因为如果路径格式非法的,可以抛出一个异常。str () 就不能做到这一点。

拼接符号”/“背后的秘密

/ 运算符由 truediv 方法定义。 实际上,如果你看一下 pathlib 的源代码,你会看到类似的东西。

class PurePath(object):

    def __truediv__(self, key):
        return self._make_child((key,))

后记

从 Python 3.4 开始,pathlib 已在标准库中提供。 使用 pathlib,文件路径可以由适当的 Path 对象表示,而不是像以前一样用纯字符串表示。 这些对象使代码处理文件路径:

  • 更容易阅读,特别是可以使用 “/” 将路径连接在一起
  • 更强大,直接在对象上提供最必要的方法和属性
  • 在操作系统中更加一致,因为 Path 对象隐藏了不同系统的特性

在本教程中,你已经了解了如何创建 Path 对象、读取和写入文件、操作路径和底层文件系统,以及如何遍历多个文件路径等一系列实例。 最后,建议下去自己多加练习,我对文章中的代码都进行了验证,不会出现运行错误的情况。 ————————————————————————————————————————————— 原文: https://realpython.com/python-pathlib/

os模块与pathlib模块对应关系

os and os.path	pathlib
os.path.abspath()	Path.resolve()
os.chmod()	Path.chmod()
os.mkdir()	Path.mkdir()
os.rename()	Path.rename()
os.replace()	Path.replace()
os.rmdir()	Path.rmdir()
os.remove(), os.unlink()	Path.unlink()
os.getcwd()	Path.cwd()
os.path.exists()	Path.exists()
os.path.expanduser()	Path.expanduser() and Path.home()
os.path.isdir()	Path.is_dir()
os.path.isfile()	Path.is_file()
os.path.islink()	Path.is_symlink()
os.stat() Path.stat(),	Path.owner(), Path.group()
os.path.isabs()	PurePath.is_absolute()
os.path.join()	PurePath.joinpath()
os.path.basename()	PurePath.name
os.path.dirname()	PurePath.parent
os.path.samefile()	Path.samefile()
os.path.splitext()	PurePath.suffix