Написанная с помощью PyFilesystem функция поиска дубликатов файлов будет работать без изменений с жёстким диском, zip-файом, FTP-сервером, Amazon S3 и т. д., этот API абстрагирует от физического расположения файла. В нём меньше способов выстрелить себе в ногу, чем у модулей os и io. Руководством из документации делимся к старту курса по Fullstack-разработке на Python.


И ещё раз о причинах

Пока для выбранной вами файловой системы (или любого хранилища данных, напоминающего файловую систему) существует объект FS, вы можете использовать тот же API, то есть отложить решение о месте хранения данных. Если вы решите хранить конфигурацию в облаке, это может обойтись изменением одной строки.

PyFilesystem также может быть полезна для юнит-тестирования. Поменяв файловую систему ОС на файловую систему в памяти, вы можете писать тесты без необходимости управляться вводом-выводом или имитацией. И вы можете быть уверены, что ваш код будет работать на Linux, MacOS и Windows.

Открытие файловых систем

Есть два способа открыть файловую систему. Первый и наиболее естественный способ — импортировать соответствующий класс файловой системы и создать его экземпляр. Вот как можно открыть OSFS (файловую систему операционной системы), которая соответствует файлам и каталогам вашего жёсткого диска:

>>> from fs.osfs import OSFS
>>> home_fs = OSFS("~/")

Код создаёт объект FS, который управляет файлами и каталогами по заданному системному пути. В данном случае '~/', — ярлык вашего домашнего каталога. Вот как можно перечислить файлы/каталоги в вашем домашнем каталоге:

>>> home_fs.listdir('/')
['world domination.doc', 'paella-recipe.txt', 'jokes.txt', 'projects']

Обратите внимание: параметр listdir — это один прямой слеш, означающий, что мы хотим список корневой файловой системы. Так написано потому, что, с точки зрения home_fs для конструирования OSFS использовалась корневая директория.

Также обратите внимание, что это прямой слэш, даже в Windows: пути FS согласованы в формате и не зависят от платформы. Такие детали, как разделитель и кодировка, абстрагируются. Подробности смотрите здесь. Другие интерфейсы файловых систем могут иметь другие требования к своему конструктору. Например, вот как можно открыть файловую систему FTP:

>>> from ftpfs import FTPFS
>>> debian_fs = FTPFS('ftp.mirror.nl')
>>> debian_fs.listdir('/')
['debian-archive', 'debian-backports', 'debian', 'pub', 'robots.txt']

Второй, более распространённый способ открытия объектов файловой системы — через opener, который открывает файловую систему, исходя из URL-подобного синтаксиса. Вот как ещё можно открыть домашний каталог:

>>> from fs import open_fs
>>> home_fs = open_fs('osfs://~/')
>>> home_fs.listdir('/')
['world domination.doc', 'paella-recipe.txt', 'jokes.txt', 'projects']

Система opener особенно полезна, когда вы хотите хранить физическое расположение файлов приложения в конфигурационном файле. Если вы не укажете протокол в URL FS, то PyFilesystem будет считать, что вы хотите работать с OSFS относительно текущего рабочего каталога. Поэтому эквивалентный способ открыть домашний каталог следующий:

>>> from fs import open_fs
>>> home_fs = open_fs('.')
>>> home_fs.listdir('/')
['world domination.doc', 'paella-recipe.txt', 'jokes.txt', 'projects']

Печать дерева файлов

Вызов tree() на объекте FS распечатает ASCII-дерево вашей файловой системы. Это полезно в отладке:

>>> from fs import open_fs
>>> my_fs = open_fs('.')
>>> my_fs.tree()
├── locale
│   └── readme.txt
├── logic
│   ├── content.xml
│   ├── data.xml
│   ├── mountpoints.xml
│   └── readme.txt
├── lib.ini
└── readme.txt

Закрытие

Метод close() выполнит необходимую очистку. Во множестве файловых систем (например, OSFS) close делает очень мало. Другие файловые системы могут завершать обработку файлов или освобождать ресурсы только один раз, как только вызван close(). Завершив работу, можно явно вызвать этот метод:

>>> home_fs = open_fs('osfs://~/')
>>> home_fs.writetext('reminder.txt', 'buy coffee')
>>> home_fs.close()

Если вы используете объекты FS в качестве контекстного менеджера, close будет вызываться автоматически. Следующий пример эквивалентен предыдущему:

>>> with open_fs('osfs://~/') as home_fs:
...    home_fs.writetext('reminder.txt', 'buy coffee')

Рекомендуется использовать объекты FS в качестве контекстного менеджера: это гарантирует закрытие каждой FS.

Информация о директории

Объекты файловой системы имеют listdir(), подобный os.listdir; он принимает путь к каталогу и возвращает список имён файлов:

>>> home_fs.listdir('/projects')
['fs', 'moya', 'README.md']

Альтернативный метод составления списков каталогов — scandir() возвращает iterable объектов ресурсной информации (объекты информации ресурсов):

>>> directory = list(home_fs.scandir('/projects'))
>>> directory
[<dir 'fs'>, <dir 'moya'>, <file 'README.md'>]

Эти объекты имеют ряд преимуществ перед простым именем файла. Например, вы можете определить, ссылается ли объект информации на файл или на каталог через атрибут is_dir, без дополнительного системного вызова. Информационные объекты могут содержать размер, время изменения и т. д. если вы запросите её в параметре namespaces.

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

Кроме того, объекты FS имеют метод filterdir(), который расширяет scandir возможностью фильтрации содержимого каталога по подстановочным символам. Вот так вы найдёте все файлы Python в каталоге:

>>> code_fs = OSFS('~/projects/src')
>>> directory = list(code_fs.filterdir('/', files=['*.py']))

По умолчанию объекты информации о ресурсе, возвращаемые scandir и listdir, содержат только имя файла и флаг is_dir. Вы можете запросить дополнительную информацию с помощью параметра namespaces. Вот как можно запросить дополнительные сведения (например, размер файла и время его изменения):

>>> directory = code_fs.filterdir('/', files=['*.py'], namespaces=['details'])

Код добавит свойства size и modified (и другие) к объектам информации о ресурсе, благодаря чему сработает эта конструкция:

>>> sum(info.size for info in directory)

Дополнительную информацию смотрите в этом разделе.

Поддиректории

PyFilesystem не имеет понятия текущего рабочего каталога, поэтому у объекта FS нет метода chdir. К счастью, вы не прогадаете, работать с подкаталогами в PyFilesystem несложно.

Вы всегда можете указать каталог с помощью методов, принимающих путь. Например, home_fs.listdir('/projects') получит список каталогов в projects. В качестве альтернативы можно вызвать opendir(), которая возвращает новый объект FS для подкаталога. Вот так можно перечислить содержимое папки projects вашего домашнего каталога:

>>> home_fs = open_fs('~/')
>>> projects_fs = home_fs.opendir('/projects')
>>> projects_fs.listdir('/')
['fs', 'moya', 'README.md']

Когда вы вызываете opendir, объект FS возвращает экземпляр SubFS. Если вы вызовете любой из методов на объекте SubFS, это будет выглядеть так, как будто вы вызвали тот же метод на родительской файловой системе, а путь указывался относительно подкаталога.

>>> home_fs = open_fs('~/')
>>> game_fs = home_fs.makedirs('projects/game')
>>> game_fs.touch('__init__.py')
>>> game_fs.writetext('README.md', "Tetris clone")
>>> game_fs.listdir('/')
['__init__.py', 'README.md']

Работая с объектами SubFS, вы можете не писать много кода манипуляции путями, который подвержен ошибкам.

Работа с файлами

Открыть файл из объекта FS можно с помощью open(), который очень похож на io.open в стандартной библиотеке. Вот как можно открыть файл под названием “reminder.txt” в домашнем каталоге:

>>> with open_fs('~/') as home_fs:
...     with home_fs.open('reminder.txt') as reminder_file:
...        print(reminder_file.read())
buy coffee

В случае OSFS вернётся стандартный файлоподобный объект. Другие файловые системы могут возвращать другой объект, поддерживающий те же методы: MemoryFS вернёт io.BytesIO.

PyFilesystem также предлагает ряд сокращений общих файловых операций. Например, readbytes() вернёт содержимое файла в виде байтов, а readtext() прочитает текст Unicode. Эти методы обычно предпочтительнее явного открытия файлов: объект FS может иметь оптимизированную реализацию. Другие краткие методы — download(), upload(), writebytes() и writetext().

Обход файлов

Часто вам нужно просканировать файлы в заданном каталоге и всех подкаталогах — обойти файловую систему. Вот так можно вывести пути ко всем файлам Python:

>>> from fs import open_fs
>>> home_fs = open_fs('~/')
>>> for path in home_fs.walk.files(filter=['*.py']):
...     print(path)

Атрибут walk объекта FS — это экземпляр BoundWalker, который должен справиться с большинством требований к обходу директорий. Подробности — в разделе Обход.

Глоббинг

С обходом тесно связан глоббинг: способом сканирования файловых систем на уровне абстракции немного выше. Пути можно фильтровать по шаблону поиска, который похож на подстановочный знак (например, *.py), но может соответствовать нескольким уровням структуры каталогов. Вот пример, где из домашнего каталога удаляются файлы .pyc:

>>> from fs import open_fs
>>> open_fs('~/project').glob('**/*.pyc').remove()
62

Дополнительную информацию смотрите в разделе Глоббинг.

Перемещение и копирование

Перемещать и копировать содержимое файлов вы можете методами move() и copy(), методы директорий — movedir() и copydir() соответственно.

Эти методы оптимизированы по возможности, и в зависимости от реализации они производительнее, чем чтение и запись файлов.

Чтобы копировать и перемещать файлы между файловыми системами (а не внутри одной системы), применяйте модули move и copy. Эти методы принимают как объекты FS, так и FS URLS. Например, следующая команда сожмёт содержимое папки projects:

>>> from fs.copy import copy_fs
>>> copy_fs('~/projects', 'zip://projects.zip')

Что эквивалентно такому, подробному, коду:

>>> from fs.copy import copy_fs
>>> from fs.osfs import OSFS
>>> from fs.zipfs import ZipFS
>>> copy_fs(OSFS('~/projects'), ZipFS('projects.zip'))

Функции copy_fs() и copy_dir() также принимают параметр Walker, этим можно воспользоваться для фильтрации копируемых файлов. Если вам нужно создать резервную копию только файлов Python, вы можете написать:

>>> from fs.copy import copy_fs
>>> from fs.walk import Walker
>>> copy_fs('~/projects', 'zip://projects.zip', walker=Walker(filter=['*.py']))

Альтернатива копированию — зеркальное отображение, копирующее файловую систему и поддерживающее её в актуальном состоянии, в дальнейшем копируя только изменённые файлы и каталоги. Смотрите mirror().

Код на Github.

На сегодня всё. Попробовать пакет в действии вы сможете на нашем курсе, а также вы можете перейти на страницы других курсов, чтобы увидеть, как мы готовим специалистов в других направлениях:

Каталог курсов

Комментарии (12)


  1. kai3341
    14.09.2021 23:26
    +6

    В принципе, идея хороша. Но API стоило слизать с pathlib. Парсить слэши в рантайме такое себе -- бьёт по производительности на пустом месте.

    В коде ожидал увидеть вынос по максимуму кода на этап инициализации, однако нашёл дурную тучу кода, исполняемого в рантайме. Оно, конечно, поправимо, но сейчас бьёт по производительности на пустом месте.

    Короче, идея вообще хороша, реализация неоптимальна. Цена на одно обращение не очень большая, но есть один нюанс.

    А, ну да, `syncio`. Библиотека синхронная.


  1. ne555
    14.09.2021 23:50
    -1

    Автор оригинала без малого крут, другая его терминальная библиотека "взлетела до небес". Я ему issues тоже открывал.


  1. untilx
    15.09.2021 09:03
    +2

    pyfs самую малость совсем ни разу не альтернатива pathlib: у них разные задачи. главная фишка pyfs в абстракции доступа к куче fs-образных сервисов: от локальных папок и memfs до s3.

    самый большой минус библиотеки в некоторой неконсистентности, связанной с ограничениями сервисов. например, в s3 отсутствуют каталоги и файлы, есть только объекты, у которых части имени могут разделяться слешем. из-за этого можно очень долго просидеть, пытаясь понять, почему после создания папки не удаётся проверить её существование, при этом в memfs и ftp всё хорошо.

    к сожалению, живых альтернаив я пока не встречал


    1. SemyonSinchenko
      15.09.2021 09:24

      Я что-то не увидел S3 среди built in filesystems. Или где это в документации?


      1. untilx
        15.09.2021 09:53

        Она не встроенная, но это официальная обёртка на основе botocore. https://github.com/PyFilesystem/s3fs


        1. SemyonSinchenko
          15.09.2021 09:56

          Проект выглядит мертвым. Последний коммит 2 года назад.


          1. untilx
            15.09.2021 15:23

            Не то, чтобы совсем мёртвый. Это просто адаптер, который работает и достаточно неплохо. API botocore за это время не сильно изменился, а большего и не нужно. Всё равно других подобных библиотечек для python, где можно было бы сменить способ доступа одной строкой, по типу FlySystem (https://flysystem.thephpleague.com/v2/docs/), я не встречал.


    1. iroln
      15.09.2021 13:27

      к сожалению, живых альтернаив я пока не встречал

      fsspec вполне себе живая альтернатива.


      1. untilx
        15.09.2021 15:24

        Не слышал про неё, посмотрю на досуге


        1. untilx
          15.09.2021 17:16

          Выглядит многообещающе, возможно, это именно то, что мне нужно. Спасибо


  1. RNZ
    15.09.2021 11:12

    А сколько джоулей будет стоит такой поиск в сравнении C/С++ реализацией?


    1. sumej
      15.09.2021 16:46

      Я не забуду один инцидент:

      Нужно было просканировать файлы …много файлов. И пайтон был по скорости равный С/С++ решению. Пока не начал использовать raid с гарантированными iopsами. До этого тормозила файловая система и различия не было.