Работа с файлами с помощью библиотеки pathlib
В Python есть множество библиотек для работы с файлами, которые имеют разную специализацию, но если бы мне надо было бы выбрать одну, то я без колебаний бы выбрал pathlib. Эта библитека совершенно новая, она вошла в дистрибутив только в версии 3.6, но, как у всех хороших инструментов, теперь сложно представить себе жизнь без нее.
Главная особенность pathlib в том, что с его помощью вы можете использовать небольшой микро язык для работы с путями и файлами. С новой библиотекой вместо сложных конструкций, которые склеивали части строк как это было в os.path вы можете использовать знак деления / для указания части пути. И полученный объект обладает необходимым множеством методов упрощающих решение задач для которых раньше надо было использовать множество разрозненных библиотек.
Путь к текущему файлу и папке
Во время работы с файлами надо знать несколько вещей. Первое, это то, что когда вы запускаете интерпретатор, то, когда он работает, то находится в какой-то папке, она еще называется текущая рабочая папка или current working dir. И она не всегда совпадает с папкой, в которой находится скрипт, который вы выполняете. Например, пусть у вас есть такая структура на диске:
~/project/
~/project/src/
~/project/src/script.py
Попробуем поработать с таким script.py:
with open("file.txt", "wt") as f:
f.write("some text")
Если вы откроете терминал перейдете в папку ~/project/ и запустите такую команду python src/script.py файл file.txt создастся в папке ~/project/, потому что-то вашей текущей рабочей папкой будет ~/project/, а не ~/project/src/ в которой находится файл script.py:
~/project/ $ python src/script.py
После запуска даст такую структуру:
~/project/
~/project/file.txt
~/project/src/
~/project/src/script.py
Если перейти в папку src и выполнить похожую команду, то файл file.txt создастся в ~/project/src/:
~/project/ $ rm file.txt
~/project/ $ cd src
~/project/src/ $ python script.py
Новый результат:
~/project/
~/project/src/
~/project/src/file.txt
~/project/src/script.py
Получается странная ситуация! Мы просто пытаемся открыть файл, но он создается не рядом с файлом, который мы запускаем, а в той папке, из которой мы его запускаем. Поэтому всегда лучше указывать полный путь к файлу или строить путь относительно текущего файла используя его в качестве основы.
Для того чтобы узнать расположение текущего файла, в котором выполняется код есть специальная магическая переменная __file__, она всегда указывает на текущий файл, в котором выполняется. И с помощью нее и библиотеки pathlib можно строить пути относительно текущего файла.
__file__ не доступна в интерактивном режиме!
В интерактивном режиме попытка обратиться к этой переменной вызовет ошибку. Эта переменная существует только тогда когда вы запускаете код, который был сохранен в файл.
>>> __file__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '__file__' is not defined
Чтобы проверить это сохраните на диске файл aboutme.py с таким содержимым:
print(__file__)
Результат выполнения:
$ python aboutme.py
aboutme.py
Построение пути
Когда мы разбирались с базовыми типами, то выяснили, что оператор / не позволяет делить строки одну на другую. Но путь, который создается с помощью конструктора Path из библиотеки pathlib благодаря особой внутренней магии позволяет превратить путь к файлу на диске в python объекты, которые уже поддерживают эту операцию. А кроме этого, получить доступ к свойствам объектов, которые связаны с файлами на диске и методами, которые позволяют делать с ними манипуляции.
Давайте построим начальные пути относительно текущего файла:
from pathlib import Path
print("Путь к текущему файлу", Path(__file__))
print("Абсолютный путь к текущему файлу", Path(__file__).absolute())
print(
"Директория в которой расположен текущий файл с помощью относительного пути",
Path(__file__) / '..'
)
print("Преобразование в полный путь", Path(Path(__file__) / '..').resolve())
print("Обращение к родительскому пути от текущего файла", Path(__file__).parent)
# Папка data находящаяся в той же папке где и текущий файл
print("Директория в рядом с текущим файлом", Path(__file__) / '..' / 'data')
Путь не обязательно строить от текущего файла, можно просто передать строку с полным путем в конструктор и она превратится в полноценный Path-объект:
>>> from pathlib import Path
>>> course = Path('/Users/xen/Dev/python-course')
>>> course
PosixPath('/Users/xen/Dev/python-course')
И, конечно, в качестве отправной точки можно использовать встроенные в Path методы, которые указывают на текущую рабочую директорию или на домашнюю директорию текущего пользователя:
>>> Path.cwd()
PosixPath('/Users/xen/Dev/python-course/1-beginner/docs/07-files')
>>> Path.home() / 'Dev/python-course'
PosixPath('/Users/xen/Dev/python-course')
До появления pathlib основной способ объединения путей была функция join модуля os.path, ее синтаксис позволял строить пути объединяя список строк:
>>> Path.home().joinpath('Dev', 'python-course', '00-samples.txt')
PosixPath('/Users/xen/Dev/python-course/00-samples.txt')
Отличия в Windows
В Windows для указания пути к файлам используется обратная наклонная черта (обратный слэш) \. Поскольку символ \ имеет специальное значение в строках, то можно использовать необрабатываемую строку с префиксом r''. Или записывать строки используя прямую наклонную черту. Например эти варианты равнозначны pathlib.Path(r'C:\Users\xen\project\file.txt') и pathlib.Path('C:/Users/xen/project/file.txt').
Создание Path объекта вместо PosixPath создаст WindowsPath. Для большинства операций с файлами это не существенная разница и это отличие проще воспринимать как особенности внутренней реализации. Другое дело, когда вам действительно надо использовать особые возможности операционных систем.
Если попытаться создать WindowsPath объект самостоятельно под другой операционной системой, то вы получите ошибку типа такой:
>>> from pathlib import WindowsPath
>>> WindowsPath(r'C:\Users\xen\project\file.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/xen/.pyenv/versions/3.7.2/lib/python3.7/pathlib.py", line 997, in __new__
% (cls.__name__,))
NotImplementedError: cannot instantiate 'WindowsPath' on your system
Операции с путями и строками
Обратите внимание, что операция / применяется не к строке, а к объекту Path. Например, вот конструкция:
p = Path(__file__) / '..' / 'data' / 'subfolder' / 'file.txt'
Интерпретатор ее разбирает следующим образом:
- Вызывает конструктор объекта
Path()который получает в качестве параметра текущий файл в переменной__file__. - Создается объект типа Path.
- К нему применяется операция деления, которая получает левый операнд строку
... Объект Path умеет обрабатывать такие операции и результатом ее выполнения становится создание нового объекта типа Path который объединяет путь, указывавший на__file__и строку... - Так продолжается до тех пор, пока не построится вся цепочка, то есть за время выполнения этой строки создалось целых 5 объектов Path (первый вызов и потом на каждый знак
/). И финальный объект уже присваивается переменнойp.
Все это происходит внутри и в коде мы работаем с удобным и красивым интерфейсом. Но частая ошибка и не только новичков это попытаться вызвать метод, относящийся к пути от объекта строки:
>>> Path.cwd() / '..' / '..'.resolve()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'resolve'
Вместо этого надо сначала получить Path, а потом уже вызывать его метод:
>>> Path(Path.cwd() / '..' / '..').resolve()
PosixPath('/Users/xen/Dev/python-course/1-beginner')
Или:
>>> p = Path.cwd() / '..' / '..'
>>> p.resolve()
PosixPath('/Users/xen/Dev/python-course/1-beginner')
Красивый и удобный интерфейс появился только недавно. Большинство стандартных модулей были написаны во времена, когда библиотеки pathlib еще не было. И они ожидают строку в качестве параметра. Все эти прекрасные удобства работы с путями как с объектами в какой-то момент окажутся бесполезными! Я думаю, постепенно мир разработчиков будет адаптироваться к более удобной библиотеке, но все еще есть много приложений которые поддерживают более старые версии Python, поэтому они предпочитают работать со путями как со строками. В таких случаях Path объект нужно преобразовать в строку:
>>> str(Path.home().joinpath('Dev', 'python-course'))
'/Users/xen/Dev/python-course'
Обратите внимание, что если при построении пути были использованы части пути для относительного перемещения '..', то они тоже попадут в финальную строку. Для преобразования в полный путь лучше предварительно использовать метод resolve:
>>> p = Path.cwd() / '..' / '..'
>>> str(p)
'/Users/xen/Dev/python-course/1-beginner/docs/07-files/../..'
>>> str(p.resolve())
'/Users/xen/Dev/python-course/1-beginner'
Чтение и запись файлов
И вот мы подошли к кульминации. Путь, построенный с помощью pathlib позволяет не просто строить путь, но и непосредственно работать с ними. У объекта Path есть метод open() который работает так же, как и обычная функция открытия файла. Я испытал огромный восторг когда первый раз осознал, что к пути можно просто дописать .open(), это сэкономило огромное количество лишних строк кода.
Вот окончательный пример программы, которая откроет наш, ставший уже стандартным, файл turing_paper_1936.txt, но в этот раз без проблем с определением текущей рабочей папки (reader_pl.py):
from pathlib import Path
paper = Path(__file__).parent / "turing_paper_1936.txt"
with paper.open("rt") as f:
for line in f:
print(line, end="")
Для простоты помимо метода .open() который внутри вызывает встроенную функцию open() еще есть несколько функций с более понятными именами, упрощающими чтение кода:
.read_text()— открывает файл, на который указывает путь в текстовом режиме и возвращает содержимое как строку..read_bytes()— открывает файл в бинарном режиме и возвращает содержимое как объектbytestring..write_text()— для записи в файл в текстовом виде..write_bytes()— открывает файл в бинарном виде для записи бинарных данных.
Смотрите насколько более читаемый и изящный код получается в результате:
from pathlib import Path
paper = Path(__file__).parent / "turing_paper_1936.txt"
print(paper.read_text())
Следите за памятью
Обратите внимание, что методы .read_text() и .read_bytes() сразу считывают все содержимое файла в память, что может быть не самым оптимальным вариантом если файлы большие или достаточно их обработать как поток.
Дополнительные операции над файлами
Список возможностей pathlib был бы не полным если бы в ней не было функций для выполнения стандартных операций над файлами: удаление, копирование, перемещение. Это привычные и ожидаемые действия, но в реальности гораздо чаще программисты делают проверку данных. И тогда на первое место становятся функции помощники, которые позволяют проверить путь:
>>> from pathlib import Path
>>> p = Path.cwd() / 'turing_paper_1936.txt'
>>> p.exists()
True
>>> new = Path.cwd() / 'new_file.txt'
>>> new.exists()
False
>>> Path.cwd().is_dir()
True
>>> p.is_file()
True
Очень часто используемый метод .exists() проверяет есть ли такой файл на диске, методы .is_file() и .is_dir() проверяют путь на то является ли он файлом или директорией соответственно.
Копирование файла требует чуть более сложной операции:
>>> new.write_text(p.read_text())
1082
В процессе этой операции содержимое файла загрузилось в память, а потом записалось на диск. Переименовывание .rename() и перемещение .replace() файла надо делать аккуратно, потому что если вы попытаетесь переименовать в файл, который уже существует, то старый будет перезаписан новым содержимым:
>>> temp = Path.cwd() / 'temp.txt'
>>> if not temp.exists(): new.rename(temp)
...
>>> temp.replace("delete-me.txt")
>>>
Обратите внимание, что объект типа Path не привязан к файлу, а только является объектом. Поэтому переименовывайте или перемещение файла не изменит старую переменную, а значит переменная new все еще указывает на уже несуществующий файл.
Для того чтобы удалить экспериментальный файл надо вызвать метод .unlink(). Повторный вызов метода для уже удаленного файла вызовет сообщение об ошибке:
>>> f_to_del = Path.cwd() / "delete-me.txt"
>>> f_to_del.unlink()
>>> f_to_del.unlink()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/xen/.pyenv/versions/3.7.2/lib/python3.7/pathlib.py", line 1277, in unlink
self._accessor.unlink(self)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/xen/Dev/python-course/1-beginner/docs/07-files/delete-me.txt'
Работа с директориями
Для пакетной обработки файлов довольно часто надо проанализировать структуру файлов и директорий и последовательно произвести нужные действия. Для того чтобы получить список всех файлов папки используйте метод .iterdir() который возвращает итератор:
>>> [child for child in Path('.').iterdir()]
[PosixPath('new_copy.py'), PosixPath('aboutme.py'),
PosixPath('reader_pl.py'), PosixPath('1-open.md'),
PosixPath('reader2.py'), PosixPath('reader.py'),
PosixPath('2-with.md'), PosixPath('turing_paper_1936.txt'),
PosixPath('work'), PosixPath('3-pathlib.md'),
PosixPath('showpath.py')]
Для поиска файлов по маске есть специальный язык запросов по шаблону на английском называющийся wildcard pattern или glob pattern.
С помощью этого языка можно сформировать шаблон, по которому будут искаться файлы в директориях. Чаще всего употребляются два символа * (звездочка) для обозначения любого количества символов (включая ноль) и ? (знак вопроса) для обозначения любого одиночного символа. Чтобы найти все текстовые файлы в папке можно использовать такой шаблон *.txt. Для более точного поиска используются квадратные скобки, в которых указываются группы возможных символов. Например, для поиска всех файлов с именами readme.txt или Readme.txt можно использовать такой шаблон [Rr]eadme.txt. Для латинских символов не обязательно их все перечислять в скобках если они входят в какой-то диапазон, а можно его указать со знаком -, например так paper[0-9]*.txt найдет все документы начинающиеся со строки paper, после которой идут любое количество чисел и с расширением .txt.
Этот язык шаблонов поиска поддерживает на самом деле достаточно большое количество программ, начиная от оболочки командной строки bash и огромное множество разных утилит, например система управления версиями git которая тоже входит в арсенал основных инструментов любого программиста.
pathlib тоже поддерживает поиск по маске:
>>> sorted(Path('.').glob('*.py'))
[PosixPath('aboutme.py'), PosixPath('new_copy.py'),
PosixPath('reader.py'), PosixPath('reader2.py'),
PosixPath('reader_pl.py'), PosixPath('showpath.py')]
Создание папки осуществляется методом .mkdir(mode=0o777, parents=False, exist_ok=False). Параметр mode устанавливает режим доступа для файловых систем, которые его поддерживают. Если использовать параметр parents то метод создаст всю цепочку папок вложенных друг в друга. Если при попытке создать папку окажется, что она уже существует, то вы получите ошибку FileExistsError, если вам не важно существовала ли папка до этого, то укажите параметр exist_ok=True:
>>> target = Path('.') / 'work' / 'on' / 'some' / 'data'
>>> target.mkdir(parents=True)
>>> target.mkdir(parents=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/xen/.pyenv/versions/3.7.2/lib/python3.7/pathlib.py", line 1241, in mkdir
self._accessor.mkdir(self, mode)
FileExistsError: [Errno 17] File exists: 'work/on/some/data'
>>> target.mkdir(parents=True, exist_ok=True)
Удаления директорий осуществяется методом rmdir():
>>> Path(Path('.') / 'work').rmdir()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/xen/.pyenv/versions/3.7.2/lib/python3.7/pathlib.py", line 1285, in rmdir
self._accessor.rmdir(self)
OSError: [Errno 66] Directory not empty: 'work'
К сожалению, это не сработает если директория не пустая. Если надо удалить не пустую папку со всем содержимым, то для необходимо использовать еще один модуль shutil который тоже был обновлен и поддерживает в качестве параметров Path объекты:
>>> import shutil
>>> shutil.rmtree(Path('.') / 'work')
Задание: Жонглирование файлами с помощью pathlib.
Вам уже дана структура файлов
home/
task1/
1.txt
main.py
task2/
2.txt
3.txt
main.py
4.txt
Вам надо будет перенести все файлы txt из папки home, включая все вложенные папки, в созданную Вами директорию data/.
Должны получить такую структуру
home/
task1/
main.py
task2/
main.py
data/
1.txt
2.txt
3.txt
4.txt
Очень рекомендую внимательно ознакомиться со всеми возможностями этой библиотеки. Даже зная только основные методы этой библиотеки, Вы можете совершить любые действия в своей файловой структуре. Но, если вы чувствуете, что код получается громоздким, то следует глубже копнуть в документацию, и вы найдете дополнительные методы, или атрибуты к методам, которые позволят сделать то же самое более изящно.
Для создания директории используйте метод .mkdir() с дополнительными аргументами parents и exists_ok при необходимости. Для переноса файла лучше использовать .replace().
old_path.replace(new_path)
Надо быть осторожным, потому что этот метод совершит перенос в любом случае, даже если по-новому пути уже есть файл. Поэтому было бы правильно перед переносом проверить новый путь. Проверить существует ли объекты в Вашей файловой структуре, по указанному пути можно с помощью метода .exists().
Найти все файлы, объединенные каким-то общим признаком (имя, расширение, общая буква в имени, и т.д.) можно с помощью метода .glob(). Если Вы уже поглядывали в сторону регулярных выражений, то Вам будет легко разобраться с этим методом. Если, нет, то придется освоить. Когда-нибудь регулярные выражения настигнут Вас, так что от них не убежать.
Метод .glob() вернет Вам список подходящих путей.
Задание 1:
В текущей директории есть папка photos c фотографиями. Мы хотели обучить на них нейросеть, но вот незадача - программа вылетает с ошибкой, если в названии файла есть буквы в верхнем регистре. Испрльзуя библиотеку pathlib, поменяйте названия всех файлов в папке, переведя все заглавные буквы в нижний регистр.
# импортируйте класс Path из pathlib
def make_photos(pth):
pth.mkdir(777, parents=True)
f1 = pth / 'IMG01.jpg'
f2 = pth / '0000002DCIM.jpeg'
f3 = pth / 'Photo.png'
f4 = pth / 'Снимок3.bmp'
f1.touch(mode=777)
f2.touch(mode=777)
f3.touch(mode=777)
f4.touch(mode=777)
return f1, f2, f3, f4
def decapitalise():
# Ваш код
photo_pth = # Запишите путь к папке photos
make_photos(photo_pth)
decapitalise()
Задание 2:
- Создайте новую дирректорию data в дирректории home.
- Перенесите все файлы с расширением .txt в папку data.
- Функция move_all_txt() ничего не возвращает.
from pathlib import Path
def move_all_txt():
# Ваш код