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
標準庫的支持下,這已經足夠了,雖然有點麻煩(如前面提到的例子)。然而,由於路徑不是字符串,重要的功能是遍佈在標準庫中的,包括像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路徑的第一個示例。
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.localtime
或time.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對象。對象的類型取決於您使用的操作系統。此功能使編寫跨平臺兼容代碼變得相當容易。可以直接明確的使用 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'
結論
自Python 3.4以來,pathlib已經可以在標準庫中使用。使用pathlib,文件路徑可以由適當的Path對象表示,而不是像以前一樣由純字符串表示。這些對象使代碼處理文件路徑:
更容易閱讀,特別是因爲/用於將路徑連接在一起
更強大,直接在對象上提供大多數必要的方法和屬性
在操作系統中更加一致,因爲不同系統的特性被Path對象隱藏