pathlib

https://www.jianshu.com/p/c286ef33e5e1

在 python3.4 之前,你是否这样判断一个完全路径是否是文件

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

在本教程中,您将了解如何使用文件路径 - 目录和文件的名称 - 在Python中。您将学习读取和写入文件,操作路径和底层文件系统的新方法,以及查看如何列出文件和迭代它们的一些示例。使用该pathlib模块,可以使用优雅,可读和Pythonic代码重写上述示例,如:

import pathlib
(pathlib.Path.home() / 'realpython.txt').is_file()

文件的路径成为了一个对象。

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路径的第一个示例。

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

早期,其他软件包仍然使用字符串作为文件路径,但是从Python 3.6开始,整个标准库都支持pathlib模块,部分原因是添加了文件系统路径协议。

让我们看看pathlib在实践中的运作方式。

创建路径

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

In [10]: import pathlib

In [11]: pathlib.Path.cwd()
Out[11]: PosixPath('/Users/yanshunjun')

注意:在本教程中,我们将假设已导入pathlib,而不会如上所述显示的让你看到导入的 pathlib的语句 。 由于您将主要使用Path类,您也可以 from pathlib import Path, 之后使用 Path 调用他的其他方法。

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

>>> pathlib.Path(r'C:\Users\yanshunjun\sharkyun\file.txt')
WindowsPath('C:/Users/yanshunjun/sharkyun/file.txt')
# 这是一个 Windows 风格的路径,可以在 Windows 平台上看到这个效果

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

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

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

只要至少有一个Path对象,/就可以连接多个路径或路径和字符串的混合(如上所述)。 如果您不喜欢特殊/符号,则可以使用.joinpath()方法执行相同的操作:

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

请注意,在前面的示例中,pathlib.Path由WindowsPath或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()

path = pathlib.Path.cwd() / 'test.md'
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('/Users/yanshunjun/test.md')
>>> path.resolve().parent == pathlib.Path.cwd()
True

请注意,比较路径时,将比较它们的表示形式。 在上面的示例中,path.parent不等于pathlib.Path.cwd(),因为path.parent由 '.' 表示。 而pathlib.Path.cwd()由'/Users/yanshunjun/test.md'表示。

挑选路径的组成部分

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

.name:没有任何目录的文件名
.parent:包含该文件的目录,如果path是目录,则为父目录
.stem:没有后缀的文件名
.suffix:文件扩展名
.anchor:目录前路径的一部分
以下是这些属性的实际应用:

>>> path = pathlib.Path('/Users/yanshunjun/test.md')
>>> path
PosixPath('/Users/yanshunjun/test.md')
>>> path.name
'test.md'
>>> path.stem
'test'
>>> path.suffix
'.md'
>>> path.parent
PosixPath('/Users/yanshunjun')
>>> path.parent.parent
PosixPath('/Users')
>>> path.anchor
'/'

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

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

优秀的Pathlib Cheatsheet提供了这些以及其他属性和方法的直观表示。

移动和删除文件

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

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

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

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

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

如果目标已存在,上面的代码将引发FileExistsError。 从技术上讲,这会复制一个文件。 要执行移动,只需在复制完成后删除源(请参阅下文)。 确保没有引起异常。

重命名文件时,有用的方法可能是 .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()(递归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.txt和test002.txt,则上面的代码将设置test003.txt的路径。

操作系统差异

之前,我们注意到当我们实例化时pathlib.Path,返回了一个WindowsPath或一个PosixPath对象。对象的类型取决于您使用的操作系统。此功能使编写跨平台兼容代码变得相当容易。可以直接明确的使用 WindowsPathPosixPath,但您只会将代码限制在该系统而且没有任何好处。像这样的具体路径不能在不同的系统上使用:

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342