pathlib,一个优雅的python库

前言

如果你需要在 Python 里进行文件处理,那么标准库中的osos.path兄弟俩一定是你无法避开的两个模块。它们提供了非常多与文件路径处理、文件读写、文件状态查看相关的工具函数。

os.path一直是Python中处理路径事实上的标准,但它可能会显得有些繁琐。与之相比,pathlib模块提供了更简单、更直观的方式来完成绝大多数任务。

在Python3.4开始,官方提供了pathlib面向对象的文件系统路径,核心的点在于面向对象, 这也是os.pathpathlib的本质区别。

2019年Django也将os.path替换成了pathlib

为什么需要pathlib?

pathlib出现之前,Python的标准库osos.path支持操作文件路径,使用字符串表示文件路径。

>>> import os.path
 
>>> os.path.abspath('test')
'F:\\spug-3.0\\spug-3.0\\spug_api\\test'

或者写出下面这种长长的代码:

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

但是路径并不只是一个字符串,如果需要对文件进行操作,需要结合使用多个标准库的功能,如: 需要移动当前目录下的一些文件到备份目录,需要使用osglobshutil库。

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)

而且,由于不同的操作系统使用的分隔符不同,使用字符串拼接路径就容易出现问题。

有了pathlib,使得上述的问题变得更加轻松,pathlib创建的Path对象,可以直接通过正斜杠运算符/连接字符串生成新的对象。

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41
import pathlib
from pathlib import WindowsPath

path = pathlib.Path()

if __name__ == '__main__':
    print(path)  # .
    print(path.absolute() / 'test' / 'data.txt')  # F:\spug-3.0\spug-3.0\spug_api\test\data.txt

pathlib的基本使用

Path类的常用属性和方法

descriptor:
    parts: 每一层路径
    parent: 父目录
    parents: 所有父目录
    stem: 不带后缀的文件名
    name: 文件名或目录名
    suffix: 文件名后缀
    suffixes: 文件名后缀列表

function:
    is_absolute: 是否为绝对路径
    joinpath: 组合路径
    cwd: 当前工作目录
    home: 根目录
    rename: 重命名
    replace: 覆盖
    touch: 新建文件
    exists: 是否存在路径
    expanduser: 返回带~~user的路径
    glob: 列出匹配的文件或目录
    rglob: 递归列出匹配的文件或目录
    is_dir: 是否为目录
    is_file: 是否为文件
    iterdir: 列出路径下的文件和目录
    mkdir: 新建目录
    open: 打开文件
    resolve: 转成绝对路径
    rmdir: 删除目录
    ...

创建路径

前面用到了pathlib.Path()获取当前路径的方法,也可以显示的传入路径字符串进行路径创建,支持相对路径和绝对路径字符串的传递。

os.path

from os.path import abspath, dirname, join

manage_path = abspath("./manage.py")  # 绝对路径
base_dir = dirname(manage_path)  # 父目录
another_manage_path = join(base_dir, "another_manage.py")  # 构成新路径

print("manage_path:", manage_path)  
print("base_dir:", base_dir)  
print("another_manage_path:", another_manage_path)

# manage_path: F:\spug-3.0\spug-3.0\spug_api\manage.py
# base_dir: F:\spug-3.0\spug-3.0\spug_api
# another_manage_path: F:\spug-3.0\spug-3.0\spug_api\another_manage.py

pathlib

from pathlib import Path

manage_path = Path("manage.py").resolve()  # 绝对路径
base_dir = manage_path.parent  # 父目录
another_manage_path = base_dir / "another_manage.py"  # 构成新路径

print("manage_path:", manage_path)
print("base_dir:", base_dir)
print("another_manage_path:", another_manage_path)

显然用pathlib更加便捷和优雅!!

创建文件 Path.touch()

from pathlib import Path

path = Path()
new_path = path / "hello.py"
new_path.touch()

创建目录和重命名

os.path

import os
import os.path

os.makedirs(os.path.join("./src", "stuff"), exist_ok=True)  # 构建目录./src/stuff
os.rename("./src/stuff", "./src/config")  # 将./src/stuff重命名为./src/config

pathlib

from pathlib import Path

Path("./src/stuff").mkdir(parents=True, exist_ok=True)  # 构建目录./src/stuff
Path("./src/stuff").rename("./src/config")  # 将./src/stuff重命名为./src/config

mkdir方法:

  • parents默认为False,父目录不存在时抛出FileNotFoundError
  • exist_ok默认为False,该目录存在时抛出FileExistsError

递归列出某类型文件

假设目录:

(spug-3.0) PS F:\spug-3.0\spug-3.0\spug_api\path_lib_test> tree /F            

F:.
│  pathlib_test.py
│  __init__.py
│
└─test
        __init__.py

列出所有.py文件

os

# ! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from os import walk
from os.path import join, expanduser, abspath

home = expanduser('~')
python_files = []

for root, dirs, files in walk("./test"):
    python_files.extend([abspath(join(root, file)) for file in files if file.endswith('.mp4')])

# 现在python_files列表包含所有以'.mp4'结尾的文件的绝对路径

print(python_files)  # ['F:\\spug-3.0\\spug-3.0\\spug_api\\path_lib_test\\test\\1.mp4']

glob

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from glob import glob

top_level_py_files = glob("./*.py")
all_py_files = glob("./**/*.py", recursive=True)  # 递归

print(top_level_py_files)
print(all_py_files)

# ['.\\pathlib_test.py', '.\\__init__.py']
# ['.\\pathlib_test.py', '.\\__init__.py', '.\\test\\__init__.py']

pathlib

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

top_level_py_files = Path(".").glob("*.py") # 不进行递归
all_py_files = Path(".").rglob("*.py")  # 递归

print(list(top_level_py_files))
print(list(all_py_files))

# [WindowsPath('pathlib_test.py'), WindowsPath('__init__.py')]
# [WindowsPath('pathlib_test.py'), WindowsPath('__init__.py'), WindowsPath('test/__init__.py')]

glob不会确定地返回路径顺序。

打开多个文件并读取内容

glob

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from glob import glob

contents = []
for fname in glob("./**/*init*.py", recursive=True):
    with open(fname, "r") as f:
        contents.append(f.read())

print(contents)

# ["#! -*-conding=: UTF-8 -*-\n# 2023/12/6 17:20\n\n\nif __name__ == '__main__':\n    pass\n", "#! -*-conding=: UTF-8 -*-\n# 2023/12/6 17:22\n\n\nif __name__ == '__main__':\n    pass\n"]

pathlib

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

contents = []
for path in Path(".").rglob("*__init__.py"):
    with path.open("r", encoding="UTF-8") as file:
        contents.append(file.read())

print(contents)

# ["#! -*-conding=: UTF-8 -*-\n# 2023/12/6 17:20\n\n\nif __name__ == '__main__':\n    pass\n", "#! -*-conding=: UTF-8 -*-\n# 2023/12/6 17:22\n\n\nif __name__ == '__main__':\n    pass\n"]

操作符

使用/取代os.path.join进行路径拼接。

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

base_dir = Path(".")
child_dir = base_dir / "test"
file_path = child_dir / "__init__.py"

print(file_path)
# test\__init__.py

路径的每个位置 Path.parts

from pathlib import Path

file_path = Path("F:/spug-3.0/spug-3.0/spug_api/pathlib_test.py")
print(file_path.parts)
# ('F:\\', 'spug-3.0', 'spug-3.0', 'spug_api', 'pathlib_test.py')

父目录 Path.parents & Path.parent

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

file_path = Path("path_lib_test/test/__init__.py")

print(file_path.parents)   # 

for parent in file_path.parents:
    print(parent)

# path_lib_test\test
# path_lib_test
# .

print(file_path.parent)

# path_lib_test\test

文件名或目录名 Path.name

os.path

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import os

print(os.path.basename("test/__init__.py"))  # __init__.py
print(os.path.basename("test"))  # test

pathlib

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

print(Path("test/__init__.py").name)  # __init__.py
print(Path("test").name)  # test

文件名后缀 Path.suffixes & Path.suffix

os.path

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import os

print(os.path.splitext("test/__init__.py")[-1])

# ('test/__init__', '.py')

pathlib

from pathlib import Path

file_path = Path("test/__init__.py")
print(file_path.suffixes)  # ['.py']
print(file_path.suffix)  # .py

不带后缀文件名 Path.stem

os.path

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import os


print(os.path.splitext(os.path.basename("test/__init__.py"))[0])  # __init__

pathlib

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

print(Path("test/__init__.py").stem)  # __init__

是否为绝对路径 Path.is_absolute()

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

file_path = Path("test/__init__.py")
print(file_path.is_absolute())  # False

组合路径 Path.joinpath(*other)

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

file_path = Path(".").joinpath("test", "__init__.py")
print(file_path)  # test\__init__.py

获取当前工作目录 Path.cwd()

和 os.getcwd() 返回的相同

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

file_path = Path()
print(file_path.cwd())  # F:\spug-3.0\spug-3.0\spug_api\path_lib_test

根目录 Path.home()

返回一个表示用户家目录的新路径对象。

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

print(Path.home())  # C:\Users\lianhf

是否存在路径 Path.exists()

os.path

import os.path

print(os.path.exists("test/aaaa.py"))
# False

pathlib

from pathlib import Path

file_path = Path("test/aaaa.py")
print(file_path.exists())
# False

返回带user的路径 Path.expanduser()

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

file_path = Path("~/test/aaaa.py")
print(file_path.expanduser())  # C:\Users\lianhf\test\aaaa.py

是否为目录或文件 Path.is_dir() & Path.is_file()

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

dir_path = Path("test/")
print(dir_path.is_dir())  # True
print(dir_path.is_file())  # False
file_path = Path("test/__init__.py")
print(file_path.is_dir())  # False
print(file_path.is_file())  # True

列出路径下的文件和目录 Path.iterdir()

产生该路径下的对象的路径

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

base_path = Path(".")
contents = [content for content in base_path.iterdir()]

print(contents)  # [WindowsPath('pathlib_test.py'), WindowsPath('test'), WindowsPath('__init__.py')]

打开文件 Path.open()

事实上,Path.open() 是在幕后调用内置的 open() 函数。这就是为什么你可以在 Path.open() 中使用模式和编码等参数。

#!usr/bin/env python
# -*- coding:utf-8 _*-
# __time__:2023/12/6 21:37


from pathlib import Path

path = Path.cwd() / "简答题.md"
with path.open(mode="r", encoding="utf-8") as md_file:
    content = md_file.read()
    groceries = [line for line in content.splitlines() if line.startswith("##")]
print("\n".join(groceries))

覆盖/移动 Path.replace()

将文件或目录重命名为给定的 target,并返回一个新的指向 target 的 Path 实例。

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

path = Path("test/__init__.py")

path.replace(path.parent / "__init2__.py") 

转成绝对路径 Path.resolve()

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

path = Path("test/__init__.py")

print(path.resolve())  # F:\spug-3.0\spug-3.0\spug_api\path_lib_test\test\__init__.py

strict设为True,如果路径不存在,则抛出FileNotFoundError

删除目录 Path.rmdir()

pathlib,一个优雅的python库_第1张图片

移除此目录。此目录必须为空的。

os.path

import os

os.rmdir("test/hello")

如果目录下不为空,抛出OSError

pathlib

from pathlib import Path

file_path = Path("test/hello")
file_path.rmdir()

如果目录下不为空,抛出OSError

删除文件 os.remove()/Path.unlink()

os

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import os

os.remove("test/hello.txt")

pathlib

#!usr/bin/env python
# -*- coding:utf-8 _*-
# __time__:2023/12/6 21:37
from pathlib import Path

p = Path('./username.txt')
p.unlink()

读写文件

  • .read_text(): 以文本模式打开路径,并以字符串形式返回内容。
  • .read_bytes(): 以二进制模式打开路径,并以字节字符串形式返回内容。
  • .write_text(): 打开路径并写入字符串数据。
  • .write_bytes(): 以二进制模式打开路径并向其中写入数据。
#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

# 除此之外,还有几个很甜的方法,省去了 with open 语句
path = Path("/home/ubuntu/readme.txt")
text = path.read_text(encoding="utf-8")
path.write_text(text)

path = Path("/home/ubuntu/image.png")
image = path.read_bytes()
path.write_bytes(image)

上述每个方法都会处理文件的打开和关闭。

移动文件 shutil.move()/path.rename()/path.replace()

shutil

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import shutil

shutil.move('./__init__.py', './test')

pathlib

可以使用path.rename()或者path.replace()移动文件:

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path

path = Path('test/__init2__.py')
folder = Path('.')
path.rename(folder / path.name)

from pathlib import Path

source = Path("hello.py")
destination = Path("goodbye.py")

if not destination.exists():
    source.replace(destination)

上面的代码并非并发安全的!这里仅提供实现思路。

也可以换一种思路:

from pathlib import Path
import fcntl

source = Path("hello.py")
destination = Path("goodbye.py")

lock_file = Path("lock_file.lock")

try:
    with lock_file.open(mode="xb") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)  # 获取独占锁

        try:
            with destination.open(mode="xb") as file:
                file.write(source.read_bytes())
        except FileExistsError:
            print(f"File {destination} exists already.")
        else:
            source.unlink()
        finally:
            fcntl.flock(lock, fcntl.LOCK_UN)  # 释放锁

except FileExistsError:
    print(f"Another process is already handling the operation.")

重命名文件

文件重命名除了可以使用上述的path.rename(),还可以使用.with_stem(), .with_suffix(), 或者.with_name()结合.replace()来实现。

#!usr/bin/env python
# -*- coding:utf-8 _*-
# __time__:2023/12/6 21:37

from pathlib import Path

txt_path = Path("./hello.txt")

md_path = txt_path.with_suffix(".md")

txt_path.replace(md_path)

使用.with_suffix()会返回一个新路径。要真正重命名文件,需要使用.replace()。这将把txt_path移到md_path,并在保存时重命名它。

如果要更改包括扩展名在内的完整文件名,可以使用.with_name()

from pathlib import Path

md_path = Path("./hello.md")

txt_path = md_path.with_name("goodbye.md")

md_path.replace(txt_path)

文件复制

令人惊讶的是,Path并没有复制文件的方法。不过,利用目前学到的pathlib知识,只需几行代码就能创建相同的功能:

from pathlib import Path

source = Path("shopping_list.md")
destination = source.with_stem("shopping_list_02")
destination.write_bytes(source.read_bytes())

使用.with_stem()创建新的文件名,但没有更改扩展名,并使用.read_bytes()读取源文件的内容,然后使用.write_bytes()将这些内容写入目标文件。

虽然使用 pathlib 来处理所有与路径相关的事情很有吸引力,但你也可以考虑使用shutil来复制文件。它是一个很好的选择,也知道如何处理路径对象。

检查路径是否符合规则

p = Path('data.json')
# math 检查匹配规则
print(p.match('*.json'))  # True

转Unix风格

as_posix():返回使用正斜杠(/)的路径字符串

from pathlib import Path

path = Path('/host/share')
print(str(path))  # \host\share
print(path.as_posix())  # /host/share

pathlib 是否比 os.path 更快?

在我尝试运行基准测试之前,我猜它不是。Path()是一种面向对象的路径操作方法。实例化一个对象可能比调用os.path.join(只需吐出一个字符串)更费时。

但即使慢,我也很好奇到底慢了多少。另外,谁知道呢,也许我的直觉是错的?

废话不多说,直接来看下路径拼接的快慢对比:

#!usr/bin/env python
# -*- coding:utf-8 _*-
# __time__:2023/12/6 21:37

# pathlib_benchmarks.py

import os
from pathlib import Path


def os_path_join():
    return os.path.join("/", "how", "do", "you", "do", "hello", "world.txt")


def pathlib_join():
    return Path("/") / "how" / "do" / "you" / "do" / "hello" / "world.txt"

我们定义一个pathlib_benchmarks.py的脚本,os_path_joinpathlib_join方法分别使用os.pathpath模块实现,然后执行测试:

PS E:\projects\mkwang\python_and_go> python -m timeit -s "from pathlib_benchmarks import pathlib_join" "pathlib_join()"
10000 loops, best of 5: 21.9 usec per loop
PS E:\projects\mkwang\python_and_go> python -m timeit -s "from pathlib_benchmarks import os_path_join" "os_path_join()"
50000 loops, best of 5: 6.95 usec per loop

结果和预期的差不太多!更多的对比,如果你有兴趣的话可以自己试试~

常用脚本

统计文件个数

我们可以使用.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})

查看目录中最近修改的文件

from pathlib import Path
from datetime import datetime

directory = Path.cwd()
time, file_path = max((f.stat().st_mtime, f) for f in directory.iterdir())
print(datetime.fromtimestamp(time), file_path)  # 023-12-06 22:56:09.532177 E:\projects\mkwang\python_and_go\pathlib_test.py

移动子文件夹下的所有文件到本文件夹

from pathlib import Path

current_file = Path(__file__).absolute()

for path in Path('.').rglob('*'):
    if path == current_file or path.parent == Path('.'):
        continue
    if path.is_file():
        try:
            new_path = Path('.') / path.name
            path.rename(new_path)
        except Exception as e:
            print(f"Error moving {path}: {e}")

展示目录树

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

import pathlib


def print_tree(directory, depth=0):
    spacer = '    ' * depth
    print(f'{spacer}+ {directory.name}/')

    for path in sorted(directory.rglob('*')):
        if path.is_file():
            print(f'{spacer}    {path.name}')
        else:
            print_tree(path, depth + 1)


if __name__ == '__main__':
    current_path = pathlib.Path.cwd()
    print_tree(current_path)

批量重命名

#! -*-conding=: UTF-8 -*-
# 2023/12/6 11:41

from pathlib import Path


def batch_rename(directory_path):
    directory = Path(directory_path)

    for file_path in directory.glob('*'):
        if file_path.is_file():
            new_name = file_path.stem.split(' RAW')[0] + '.mp4'
            new_path = file_path.with_name(new_name)
            file_path.rename(new_path)


if __name__ == '__main__':
    target_directory = './test'  # 需要批量重命名的路径
    batch_rename(target_directory)

获取文件大小

import os
from pathlib import Path

print(os.path.getsize('test/1.mp4'))
print(Path('test/1.mp4').stat().st_size)

文件存在且不为空

from pathlib import Path


def path_exists(path):
    """文件存在且不为空"""
    if path and Path(path).exists() and Path(path).stat().st_size > 0:
        return True
    return False


print(path_exists(''))  # False
print(path_exists('abc.txt'))  # False
print(path_exists(__file__))  # True

小结

从 Python 3.4 开始,pathlib已在标准库中提供。创建、删除、读取、写入、查找、移动、复制、拆分,以及其他任何你想对文件路径或文件本身执行的操作,pathlib可能都有相应的功能。使用pathlib,文件路径可以由适当的Path对象表示,而不是像以前一样用纯字符串表示。 这些对象使代码处理文件路径:

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

当然,os.path或其他模块可以更快地完成这些操作。

功能 os 和 os.path pathlib
绝对路径 os.path.abspath() Path.resolve()
改变文件的模式和权限 os.chmod() Path.chmod()
新建目录 os.mkdir() Path.mkdir()
新建目录 os.makedirs() 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(), Path.home()
列出路径下的文件和目录 os.listdir() Path.iterdir()
是否为目录 os.path.isdir() Path.is_dir()
是否为文件 os.path.isfile() Path.is_file()
是否指向符号链接 os.path.islink() Path.is_symlink()
创建硬链接 os.link() Path.link_to()
将此路径创建为符号链接 os.symlink() Path.symlink_to()
返回符号链接所指向的路径 os.readlink() Path.readlink()
返回 os.stat_result 对象包含此路径信息 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
移动文件 shutil.move() Path.rename()

更多内容和细节请参考官方文档~

一起学习

欢迎大家关注我的账号,你的支持,是我更文的最大动力!

也欢迎关注我的公众号: 海哥python,领取更多python学习和面试资料。

微信号:fenggeniubi_2023

参考文献

https://docs.python.org/zh-cn/3.11/library/pathlib.html
https://stackoverflow.com/questions/2104080/how-do-i-check-file-size-in-python
https://docs.python.org/zh-cn/3/library/os.path.html
https://docs.python.org/zh-cn/3/library/shutil.html
https://docs.python.org/zh-cn/3/library/os.html
https://switowski.com/blog/pathlib/
https://betterprogramming.pub/should-you-be-using-pathlib-6f3a0fddec7e
https://peps.python.org/pep-0428/
https://realpython.com/python-pathlib/

你可能感兴趣的:(python基础,Python,模块,python,数据库,java)