python3的pathlib模块:驯服文件系统

python pathlib 文件系统
2021-01-21 09:24:36
22 0 0

您在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()

Python文件路径处理问题

由于许多不同的原因,使用文件和与文件系统进行交互非常重要。最简单的情况可能仅涉及读取或写入文件,但是有时手头上会遇到更复杂的任务。也许您需要列出给定类型的目录中的所有文件,找到给定文件的父目录,或者创建一个不存在的唯一文件名。传统上,Python使用常规文本字符串表示文件路径。在os.path标准库的支持下,虽然有点麻烦(如引言中的第二个示例所示),但这已经足够了。然而,由于路径不是字符串,重要的功能是分散各地的标准库,包括像图书馆os,glob和shutil。以下示例仅需要三个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上,那么Python2也有一个backport可用。

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

创建路径

您真正需要了解的只是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.Path表示为aWindowsPath或a PosixPath。表示路径的实际对象取决于基础操作系统。(也就是说,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/'。

挑选路径的组成部分

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

  • .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备忘单提供了这些以及其他属性和方法的直观表示。

移动和删除文件

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

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

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

但是,这确实会使车门保持打开状态,以防可能发生比赛。另一个进程可能会destination在if语句执行和.replace()方法之间的路径处添加文件。如果这是一个问题,一种更安全的方法是打开用于独占创建的目标路径并显式复制源数据:

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

上面的代码将引发一个(FileExistsError如果destination已经存在)。从技术上讲,这将复制文件。要执行移动,只需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来应对简单挑战的示例。

计数文件

有几种列出许多文件的方法。最简单的.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()(递归水珠)。例如,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及更高版本。在较旧的Python中,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 pathpath = unique_path(pathlib.Path.cwd(), 'test{:03d}.txt')

如果目录已经包含test001.txt和test002.txt,则上面的代码将设置path为test003.txt。

操作系统差异

之前,我们注意到实例化时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对象,读取和写入文件,操作路径和基础文件系统,以及一些如何遍历许多文件路径的示例。

作者介绍

用微信扫一扫

收藏