Модуль pathlib Python 3: Укрощение файловой системы

Модуль pathlib Python 3: Укрощение файловой системы

Вы боролись с обработкой пути к файлу в 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:* https://realpython.com/bonus/python-cheat-sheet-short/[Python 3 Шпаргалка]

Проблема с обработкой пути к файлу Python

Работа с файлами и взаимодействие с файловой системой важны по многим различным причинам. Простейшие случаи могут включать только чтение или запись файлов, но иногда возникают более сложные задачи. Может быть, вам нужно перечислить все файлы в каталоге данного типа, найти родительский каталог данного файла или создать уникальное имя файла, которое еще не существует.

Традиционно Python представлял пути к файлам, используя обычные текстовые строки. При поддержке стандартной библиотеки https://docs.python.org/3/library/os.path.html [+ os.path +] это было достаточно, хотя и немного громоздко (как второй пример во введении шоу). Однако, поскольку paths не являются строками, важные функции распространяются по всей стандартной библиотеке, включая такие библиотеки, как https://docs.python.org/3/library/os.html [+ os +], https://docs.python.org/3/library/glob.html [+ glob +] и https://docs.python.org/3/library/shutil.html [ + 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 + поддерживается во всей стандартной библиотеке, частично из-за добавления протокол пути к файловой системе. Если вы застряли на устаревшем Python, для Python 2 также имеется https://github.com/mcmtroffaes/pathlib2[backportport.

Время действовать: давайте посмотрим, как + pathlib + работает на практике.

Создание путей

Все, что вам действительно нужно знать, это класс + pathlib.Path +. Есть несколько разных способов создания пути. Прежде всего, существуют classmethods наподобие + .cwd () + (текущий рабочий каталог) и `+ .home () + `(домашний каталог вашего пользователя):

>>>

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

_ Примечание: В этом уроке мы будем предполагать, что + pathlib + был импортирован, без указания + import pathlib +, как указано выше. Поскольку вы будете в основном использовать класс + Path +, вы также можете сделать + из pathlib import Path + и написать + Path + вместо + pathlib.Path +. _

Путь также может быть явно создан из его строкового представления:

>>>

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

Небольшой совет для работы с путями Windows: в Windows разделитель пути - это обратный слеш, + \ +. Однако во многих случаях обратная косая черта также используется в качестве символа escape для представления непечатаемых символов. Чтобы избежать проблем, используйте raw string literals для представления путей 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 + представлен либо + WindowsPath +, либо + PosixPath +. Фактический объект, представляющий путь, зависит от базовой операционной системы. (То есть пример + WindowsPath + был выполнен в Windows, в то время как примеры + PosixPath + были выполнены в Mac или Linux.) См. Ссылку на раздел: # operating-system-Отличия [Различия операционной системы] для получения дополнительной информации. ,

Чтение и запись файлов

Традиционно для чтения или записи файла в 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))

Эквивалентной альтернативой является вызов + .open () + для объекта + Path +:

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

Обратите внимание, что при сравнении путей сравниваются их представления. В приведенном выше примере + path.parent + не равно + pathlib.Path.cwd () +, поскольку + path.parent + представляется + '.' +, А + pathlib.Path .cwd () + `представляется как + '/home/gahjelle/realpython/' + `.

Выделение компонентов пути

Различные части пути удобно доступны как свойства. Основные примеры включают в себя:

  • + .name +: имя файла без какого-либо каталога

  • + .parent +: каталог, содержащий файл, или родительский каталог, если путь является каталогом

  • + .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)

Тем не менее, это оставляет дверь открытой для возможного состояния гонки. Другой процесс может добавить файл по пути + destination + между выполнением оператора + if + и метода + .replace () +. Если это вызывает озабоченность, более безопасный способ - открыть путь назначения для создания exclusive и явно скопировать исходные данные:

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 () + с классом + collection.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-strings работают только в Python 3.6 и более поздних версиях. В старых Pythons выражение + 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_ +, представляет секунды с 1 января 1970 года. В дополнение к + 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 +, приведенный выше код установит для + path + значение + test003.txt +.

Отличия операционной системы

Ранее мы отмечали, что когда мы создавали экземпляр + pathlib.Path +, возвращался либо объект + WindowsPath +, либо + PosixPath +. Тип объекта будет зависеть от операционной системы, которую вы используете. Эта функция позволяет довольно легко писать кросс-платформенный код. Можно явно запросить + WindowsPath + или + PosixPath +, но вы будете ограничивать свой код только этой системой без каких-либо преимуществ. Такой конкретный путь не может быть использован в другой системе:

>>>

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

В некоторых случаях может потребоваться представление пути без доступа к базовой файловой системе (в этом случае также может иметь смысл представлять путь Windows в системе, отличной от Windows, или наоборот). Это можно сделать с помощью объектов + PurePath +. Эти объекты поддерживают операции, обсуждаемые в ссылке: # picking-out-components-of-a-path [раздел Компоненты пути], но не методы, обращающиеся к файловой системе:

>>>

>>> 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 + вернет один из этих объектов в зависимости от используемой операционной системы.

Пути как правильные объекты

В ссылке: # the-problem-with-python-file-path-processing [введение] мы кратко отметили, что пути не являются строками, и одной из причин + pathlib + является представление файловой системы с надлежащими объектами. Фактически, https://docs.python.org/3/library/pathlib.html Побочный документ официальной документации + pathlib +] называется ` + pathlib + - Пути объектно-ориентированной файловой системы. Https://realpython.com/python3-object-oriented-programming/[Object-oriented подход] уже довольно хорошо виден в приведенных выше примерах (особенно если вы противопоставляете его старому способу `+ os.path +