geekdoc-python-zh/docs/realpython/python-pathlib.md

20 KiB
Raw Blame History

Python 3 的 pathlib 模块:驯服文件系统

原文:https://realpython.com/python-pathlib/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 使用 Python 的 pathlib 模块

你是否纠结于 Python 中的文件路径处理?在 Python 3.4 及以上版本中,斗争现在已经结束了!您不再需要为代码而绞尽脑汁,比如:

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

或畏缩于以下的冗长:

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

在本教程中,您将了解如何在 Python 中使用文件路径(目录和文件的名称)。您将学习读写文件、操作路径和底层文件系统的新方法,还将看到一些如何列出文件和遍历文件的示例。使用pathlib模块,上面的两个例子可以用优雅的、可读的 Pythonic 代码重写,比如:

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

免费 PDF 下载: Python 3 备忘单

Python 文件路径处理的问题

由于许多不同的原因,使用文件和与文件系统交互是很重要的。最简单的情况可能只涉及读取或写入文件,但有时更复杂的任务就在手边。也许您需要列出给定类型的目录中的所有文件,找到给定文件的父目录,或者创建一个尚不存在的唯一文件名。

传统上Python 使用常规的文本字符串来表示文件路径。在 os.path 标准库的支持下,这已经足够了,尽管有点麻烦(如简介中的第二个例子所示)。然而,由于路径不是字符串,重要的功能遍布标准库,包括像 osglobshutil 这样的库。下面的例子需要三个 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()它使用操作系统上正确的路径分隔符来连接路径。回想一下Windows 使用\,而 Mac 和 Linux 使用/作为分隔符。这种差异会导致难以发现的错误,比如我们在引言中的第一个例子只适用于 Windows 路径。

Python 3.4 ( PEP 428 )中引入了pathlib模块来应对这些挑战。它将必要的功能集中在一个地方,并通过一个易于使用的Path对象上的方法和属性使其可用。

早期,其他包仍然使用字符串作为文件路径,但是从 Python 3.6 开始,整个标准库都支持pathlib模块,部分原因是添加了一个文件系统路径协议。如果你被困在传统的 Python 上Python 2 也有一个 backport 可用。

行动的时间到了:让我们看看pathlib在实践中是如何工作的。

Remove ads

创建路径

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

>>> import pathlib
>>> pathlib.Path.cwd()
PosixPath('/home/gahjelle/realpython/')

**注意:**在整个教程中,我们将假设pathlib已经被导入,而不像上面那样拼出import pathlib。因为你将主要使用Path类,你也可以做from pathlib import Path并写Path而不是pathlib.Path

路径也可以从其字符串表示形式显式创建:

>>> pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt')
WindowsPath('C:/Users/gahjelle/realpython/file.txt')

处理 Windows 路径的一个小技巧:在 Windows 上,路径分隔符是反斜杠,\。然而,在许多上下文中,反斜杠也被用作一个转义字符,以表示不可打印的字符。为了避免问题,使用原始字符串文字来表示 Windows 路径。这些是前面有一个r的字符串。在原始字符串文字中,\代表一个文字反斜杠:r'C:\Users'

构建路径的第三种方法是使用特殊操作符/连接路径的各个部分。正斜杠运算符的使用独立于平台上的实际路径分隔符:

>>> pathlib.Path.home() / 'python' / 'scripts' / 'test.py'
PosixPath('/home/gahjelle/python/scripts/test.py')

只要至少有一个Path对象,/就可以连接几个路径或者路径和字符串的混合(如上)。如果你不喜欢特殊的/符号,你可以用.joinpath()方法做同样的事情:

>>> pathlib.Path.home().joinpath('python', 'scripts', 'test.py')
PosixPath('/home/gahjelle/python/scripts/test.py')

注意,在前面的例子中,pathlib.PathWindowsPathPosixPath表示。表示路径的实际对象取决于底层操作系统。(也就是说,WindowsPath示例是在 Windows 上运行的,而PosixPath示例是在 Mac 或 Linux 上运行的。)更多信息参见操作系统差异一节。

读写文件

传统上,在 Python 中读写文件的方法是使用内置的open()函数。这仍然是正确的,因为open()函数可以直接使用Path对象。下面的例子在一个 Markdown 文件中找到所有的头,然后打印它们:

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))

一个等价的替代方法是在Path对象上调用.open():

with path.open(mode='r') as fid:
    ...

实际上,Path.open()是在幕后调用内置的open()。你使用哪个选项主要是个人喜好的问题。

对于简单的文件读写,在pathlib库中有一些方便的方法:

  • .read_text():以文本方式打开路径,以字符串形式返回内容。
  • .read_bytes():以二进制/字节模式打开路径,以字节字符串的形式返回内容。
  • .write_text():打开路径,写入字符串数据。
  • .write_bytes():以二进制/字节模式打开路径,向其中写入数据。

这些方法中的每一个都处理文件的打开和关闭,使得它们使用起来很简单,例如:

>>> path = pathlib.Path.cwd() / 'test.md'
>>> path.read_text()
<the contents of the test.md-file>

路径也可以指定为简单的文件名,在这种情况下,它们被解释为相对于当前工作目录。以下示例等同于上一个示例:

>>> pathlib.Path('test.md').read_text()
<the contents of the test.md-file>

.resolve()方法将找到完整的路径。下面,我们确认当前工作目录用于简单文件名:

>>> path = pathlib.Path('test.md')
>>> path.resolve()
PosixPath('/home/gahjelle/realpython/test.md')

>>> path.resolve().parent == pathlib.Path.cwd()
True

>>> path.parent == pathlib.Path.cwd()
False

注意,当比较路径时,比较的是它们的表示。在上例中,path.parent不等于pathlib.Path.cwd(),因为path.parent'.'表示,而pathlib.Path.cwd()'/home/gahjelle/realpython/'表示。

Remove ads

挑选路径的组成部分

路径的不同部分可以方便地作为属性使用。基本示例包括:

  • .name:没有目录的文件名
  • .parent:包含文件的目录,如果 path 是目录,则为父目录
  • .stem:不带后缀的文件名
  • .suffix:文件扩展名
  • .anchor:目录前的路径部分

下面是这些正在运行的属性:

>>> path
PosixPath('/home/gahjelle/realpython/test.md')
>>> path.name
'test.md'
>>> path.stem
'test'
>>> path.suffix
'.md'
>>> path.parent
PosixPath('/home/gahjelle/realpython')
>>> path.parent.parent
PosixPath('/home/gahjelle')
>>> path.anchor
'/'

注意,.parent返回一个新的Path对象,而其他属性返回字符串。这意味着,例如,.parent可以像上一个例子那样被链接,或者甚至与/结合来创建全新的路径:

>>> path.parent.parent / ('new' + path.suffix)
PosixPath('/home/gahjelle/new.md')

出色的 Pathlib Cheatsheet 提供了这些以及其他属性和方法的可视化表示。

移动和删除文件

通过pathlib,您还可以访问基本的文件系统级操作,比如移动、更新甚至删除文件。在大多数情况下,这些方法不会在信息或文件丢失之前发出警告或等待确认。使用这些方法时要小心。

要移动文件,使用.replace()。注意,如果目的地已经存在,.replace()将覆盖它。不幸的是,pathlib并没有明确支持文件的安全移动。为了避免可能覆盖目标路径,最简单的方法是在替换之前测试目标是否存在:

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

然而,这确实为可能的竞争条件敞开了大门。另一个进程可能会在执行if语句和.replace()方法之间的destination路径添加一个文件。如果这是一个问题,一个更安全的方法是为独占创建打开目标路径,并显式复制源数据:

with destination.open(mode='xb') as fid:
    fid.write(source.read_bytes())

如果destination已经存在,上面的代码将引发一个FileExistsError。从技术上讲,这是复制一个文件。要执行移动,只需在复制完成后删除source(见下文)。但是要确保没有引发异常。

重命名文件时,有用的方法可能是.with_name().with_suffix()。它们都返回原始路径,但分别替换了名称或后缀。

例如:

>>> path
PosixPath('/home/gahjelle/realpython/test001.txt')
>>> path.with_suffix('.py')
PosixPath('/home/gahjelle/realpython/test001.py')
>>> path.replace(path.with_suffix('.py'))

可以分别使用.rmdir().unlink()删除目录和文件。(还是那句话,小心!)

示例

在本节中,您将看到一些如何使用pathlib处理简单挑战的例子。

Remove ads

清点文件

有几种不同的方法来列出许多文件。最简单的是.iterdir()方法,它遍历给定目录中的所有文件。下面的例子结合了.iterdir()collections.Counter类来计算当前目录中每种文件类型有多少个文件:

>>> import collections
>>> collections.Counter(p.suffix for p in pathlib.Path.cwd().iterdir())
Counter({'.md': 2, '.txt': 4, '.pdf': 2, '.py': 1})

使用方法.glob().rglob()(递归 glob)可以创建更灵活的文件列表。例如,pathlib.Path.cwd().glob('*.txt')返回当前目录中所有带有.txt后缀的文件。以下仅统计以p开头的文件类型:

>>> import collections
>>> collections.Counter(p.suffix for p in pathlib.Path.cwd().glob('*.p*'))
Counter({'.pdf': 2, '.py': 1})

显示目录树

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

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}')

注意,我们需要知道一个文件离根目录有多远。为此,我们首先使用.relative_to()来表示相对于根目录的路径。然后,我们计算表示中目录的数量(使用.parts属性)。运行时,此函数会创建如下所示的可视化树:

>>> tree(pathlib.Path.cwd())
+ /home/gahjelle/realpython
 + directory_1
 + file_a.md
 + directory_2
 + file_a.md
 + file_b.pdf
 + file_c.py
 + file_1.txt
 + file_2.txt

注意:f 串只在 Python 3.6 及更高版本中有效。在更老的蟒蛇身上,表达式f'{spacer}+ {path.name}'可以写成'{0}+ {1}'.format(spacer, path.name)

找到最后修改的文件

.iterdir().glob().rglob()方法非常适合生成器表达式列表理解。要在目录中找到最后修改的文件,您可以使用.stat()方法来获取关于底层文件的信息。例如,.stat().st_mtime给出了文件的最后修改时间:

>>> from datetime import datetime
>>> time, file_path = max((f.stat().st_mtime, f) for f in directory.iterdir())
>>> print(datetime.fromtimestamp(time), file_path)
2018-03-23 19:23:56.977817 /home/gahjelle/realpython/test001.txt

您甚至可以使用类似的表达式来获取最后修改的文件内容:

>>> max((f.stat().st_mtime, f) for f in directory.iterdir())[1].read_text()
<the contents of the last modified file in directory>

从不同的.stat().st_属性返回的时间戳表示自 1970 年 1 月 1 日以来的秒数。除了datetime.fromtimestamp之外,time.localtimetime.ctime可以用来将时间戳转换成更有用的东西。

创建一个唯一的文件名

最后一个例子将展示如何基于模板构造一个唯一的编号文件名。首先,为文件名指定一个模式,并为计数器留出空间。然后,检查通过连接目录和文件名(带有计数器值)创建的文件路径是否存在。如果它已经存在,增加计数器并重试:

def unique_path(directory, name_pattern):
    counter = 0
    while True:
        counter += 1
        path = directory / name_pattern.format(counter)
        if not path.exists():
            return path

path = unique_path(pathlib.Path.cwd(), 'test{:03d}.txt')

如果目录中已经包含了文件test001.txttest002.txt,上面的代码会将path设置为test003.txt

Remove ads

操作系统差异

前面,我们注意到当我们实例化pathlib.Path时,或者返回一个WindowsPath或者一个PosixPath对象。对象的种类取决于您使用的操作系统。这个特性使得编写跨平台兼容的代码变得相当容易。显式地请求一个WindowsPath或者一个PosixPath是可能的,但是你只会把你的代码限制在那个系统中,没有任何好处。像这样的具体路径不能在不同的系统上使用:

>>> pathlib.WindowsPath('test.md')
NotImplementedError: cannot instantiate 'WindowsPath' on your system

有时候,您可能需要一个无法访问底层文件系统的路径表示(在这种情况下,在非 Windows 系统上表示 Windows 路径也是有意义的,反之亦然)。这可以通过PurePath对象来完成。这些对象支持在路径组件的部分中讨论的操作,但不支持访问文件系统的方法:

>>> path = pathlib.PureWindowsPath(r'C:\Users\gahjelle\realpython\file.txt')
>>> path.name
'file.txt'
>>> path.parent
PureWindowsPath('C:/Users/gahjelle/realpython')
>>> path.exists()
AttributeError: 'PureWindowsPath' object has no attribute 'exists'

可以在所有系统上直接实例化PureWindowsPath或者PurePosixPath。根据您使用的操作系统,实例化PurePath将返回这些对象中的一个。

作为适当对象的路径

简介中,我们简要地提到了路径不是字符串,pathlib背后的一个动机是用适当的对象来表示文件系统。事实上,pathlib官方文档名为 pathlib —面向对象文件系统路径。在上面的例子中,面向对象的方法已经很明显了(特别是如果你将它与旧的os.path做事方式对比的话)。然而,让我给你留下一些其他的花絮。

与您使用的操作系统无关,路径以 Posix 样式表示,用正斜杠作为路径分隔符。在 Windows 上,您会看到类似这样的内容:

>>> pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt')
WindowsPath('C:/Users/gahjelle/realpython/file.txt')

尽管如此,当路径被转换为字符串时,它将使用本机形式,例如在 Windows 上使用反斜杠:

>>> str(pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt'))
'C:\\Users\\gahjelle\\realpython\\file.txt'

如果你正在使用一个不知道如何处理pathlib.Path对象的库,这是非常有用的。这在 3.6 之前的 Python 版本上是一个更大的问题。例如,在 Python 3.5 中,configparser标准库只能使用字符串路径来读取文件。处理这种情况的方法是显式转换为字符串:

>>> from configparser import ConfigParser
>>> path = pathlib.Path('config.txt')
>>> cfg = ConfigParser()
>>> cfg.read(path)                     # Error on Python < 3.6
TypeError: 'PosixPath' object is not iterable
>>> cfg.read(str(path))                # Works on Python >= 3.4
['config.txt']

在 Python 3.6 和更高版本中,如果需要进行显式转换,建议使用os.fspath()而不是str()。这稍微安全一点,因为如果你不小心试图转换一个不是路径的对象,它会引发一个错误。

pathlib库最不寻常的部分可能是使用了/操作符。让我们看一下它是如何实现的。这是操作符重载的一个例子:操作符的行为根据上下文而改变。你以前见过这个。想想+对于字符串和数字来说意味着什么。Python 通过使用双下划线方法(又名 dunder 方法)来实现操作符重载。

/操作符由.__truediv__()方法定义。事实上,如果你看一下pathlib源代码,你会看到这样的内容:

class PurePath(object):

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

结论

从 Python 3.4 开始,pathlib已经可以在标准库中使用了。有了pathlib,文件路径可以用合适的Path对象来表示,而不是像以前一样用普通的字符串。这些对象构成了处理文件路径的代码:

  • 更容易阅读,尤其是因为/用于将路径连接在一起
  • 更强大,大多数必需的方法和属性都可以直接在对象上使用
  • 跨操作系统更加一致,因为不同系统的特性被Path对象隐藏了

在本教程中,您已经看到了如何创建Path对象、读写文件、操作路径和底层文件系统,以及如何迭代多个文件路径的一些示例。

免费 PDF 下载: Python 3 备忘单

立即观看本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 使用 Python 的 pathlib 模块***