Получить список файлов и папок в каталоге это естественный первый шаг для многих операций с файлами в Python. Но выполнить его можно по-разному.
Когда вы сталкиваетесь с множеством способов сделать что-то, это может быть хорошим признаком того, что не существует универсального решения для ваших проблем. Скорее всего, у каждого решения будут свои преимущества и недостатки. Именно так обстоит дело с получением списка содержимого каталога в Python.
В этом руководстве мы сосредоточимся на наиболее универсальных методах модуля pathlib для получения списка элементов в каталоге, но также рассмотрим некоторые альтернативные инструменты.
Раньше, до появления pathlib в Python 3.4, для работы с путями к файлам использовали модуль os. Хотя это было очень эффективно с точки зрения производительности, вам приходилось работать со всеми путями как со строками.
На первый взгляд в работе с путями в строковом формате нет ничего плохого. Но когда вы начинаете использовать несколько операционных систем, все усложняется. Кроме того, в итоге вы получаете кучу кода для манипуляций со строками. Этот код может быть очень абстрагирован от того, что представляет собой путь к файлу. Все это может быстро запутаться.
Это не значит, что работа с путями как строками невозможна в принципе. Разработчики прекрасно обходились без pathlib в течение многих лет! Модуль pathlib просто берет на себя многие хлопоты и позволяет вам сосредоточиться на основной логике вашего кода.
Все начинается с создания объекта Path
, который будет отличаться в зависимости от вашей операционной системы (ОС). В Windows вы получите объект WindowsPath
, а Linux и macOS вернут PosixPath
:
Windows:
>>> import pathlib >>> desktop = pathlib.Path("C:/Users/RealPython/Desktop") >>> desktop WindowsPath("C:/Users/RealPython/Desktop")
Linux и macOS:
>>> import pathlib >>> desktop = pathlib.Path("/home/RealPython/Desktop") >>> desktop PosixPath('/home/RealPython/Desktop')
С помощью этих объектов можно воспользоваться множеством доступных методов и свойств, в том числе и для получения списка файлов и папок.
Теперь пришло время перейти к выводу содержимого папок. Имейте в виду, что существует несколько способов сделать это, и выбор подходящего будет зависеть от конкретного случая использования.
Как получить список всех файлов и папок в каталоге
Если вам нужно только перечислить содержимое заданного каталога, без содержимого каждого подкаталога, то вы можете использовать метод .iterdir()
объекта Path
.
Метод .iterdir()
, вызванный для объекта Path
, возвращает генератор, который создает объекты Path
, представляющие дочерние элементы. Если обернуть генератор в конструктор list()
, то можно увидеть список файлов и папок:
>>> import pathlib >>> desktop = pathlib.Path("Desktop") >>> # .iterdir() создает генератор >>> desktop.iterdir() <generator object Path.iterdir at 0x000001A8A5110740> >>> # Генератор можно обернуть в конструктор list() для материализации >>> list(desktop.iterdir()) [WindowsPath('Desktop/Notes'), WindowsPath('Desktop/realpython'), WindowsPath('Desktop/scripts'), WindowsPath('Desktop/todo.txt')]
Передав генератор, созданный .iterdir()
, в конструктор list()
, вы получите список объектов Path
, представляющих все элементы в каталоге Desktop.
Для перебора элементов, выдаваемых генератором, можно использовать цикл for
. Это даст вам возможность изучить некоторые свойства каждого объекта:
>>> desktop = pathlib.Path("Desktop") >>> for item in desktop.iterdir(): ... print(f"{item} - {'dir' if item.is_dir() else 'file'}") ... Desktop\Notes - dir Desktop\realpython - dir Desktop\scripts - dir Desktop\todo.txt - file
В теле цикла for
используется f-строка для отображения некоторой информации о каждом элементе.
Во втором наборе фигурных скобок в f-строке используется условное выражение для вывода dir
, если элемент является каталогом, или file
, если не является. Чтобы получить эту информацию, вы используете метод .is_dir().
Помещение объекта Path
в f-строку автоматически приводит объект к строке, поэтому у вас больше нет аннотации WindowsPath
или PosixPath
.
Итерация объекта в цикле for
может быть очень удобной для отбора файлов или каталогов. Например:
>>> desktop = pathlib.Path("Desktop") >>> for item in desktop.iterdir(): ... if item.is_file(): ... print(item) ... Desktop\todo.txt
Здесь используется условный оператор и метод .is_file(), чтобы вывести элемент только в том случае, если это файл.
Вы также можете помещать генераторы в представления (comprehensions), что позволяет получить очень лаконичный код:
>>> desktop = pathlib.Path("Desktop") >>> [item for item in desktop.iterdir() if item.is_dir()] [WindowsPath('Desktop/Notes'), WindowsPath('Desktop/realpython'), WindowsPath('Desktop/scripts')]
Здесь полученный список отфильтровывается с помощью условного выражения внутри генератора, чтобы проверить, является ли элемент каталогом.
Но что, если вам нужны все файлы и каталоги в подкаталогах вашей папки? Вы можете использовать .iterdir()
рекурсивно, но лучше применить .rglob()
.
Вывод содержимого каталога рекурсивно с помощью .rglob()
Каталоги часто сравнивают с деревьями из-за их рекурсивной природы. В деревьях главный ствол разветвляется на основные ветви. Каждая основная ветвь разветвляется на подветви. Каждая подветвь ветвится дальше, и так далее.
Аналогично, каталоги содержат подкаталоги, которые содержат подкаталоги, которые содержат еще больше подкаталогов, и так далее.
Вывести элементы каталога рекурсивно означает перечислить не только содержимое каталога, но и содержимое подкаталогов, их подкаталогов и так далее.
С помощью pathlib удивительно легко выполнять рекурсивный поиск по каталогу. Вы можете использовать .rglob()
, чтобы вернуть абсолютно все:
>>> import pathlib >>> desktop = pathlib.Path("Desktop") >>> # .rglob() также производит генератор >>> desktop.rglob("*") <generator object Path.glob at 0x000001A8A50E2F00> >>> # Генератор можно обернуть в конструктор list() для материализации >>> list(desktop.rglob("*")) [WindowsPath('Desktop/Notes'), WindowsPath('Desktop/realpython'), WindowsPath('Desktop/scripts'), WindowsPath('Desktop/todo.txt'), WindowsPath('Desktop/Notes/hash-tables.md'), WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md'), WindowsPath('Desktop/scripts/rename_files.py'), WindowsPath('Desktop/scripts/request.py')]
Метод .rglob()
с в качестве аргумента создает генератор, который рекурсивно выдает все файлы и папки из объекта Path
.
Но что это за аргумент “*” в .rglob()
? В следующем разделе вы познакомитесь с шаблонами glob и узнаете, что можно сделать помимо перечисления всех элементов в каталоге.
Использование шаблонов Glob для условного листинга
Иногда вам не нужны все файлы. Бывает, вам нужен только один тип файлов или каталог, или, возможно, все элементы с определенным набором символов в имени.
С .rglob()
связан метод .glob()
. Оба эти метода используют шаблоны glob, которые представляют собой наборы путей. Для поиска по определенным критериям шаблоны glob используют символы подстановки. Например, одиночная звездочка соответствует всему каталогу.
Существует множество различных шаблонов glob, которыми вы можете воспользоваться. Например:
Шаблон glob | Соответствует |
---|---|
* | каждому элементу |
*.txt | каждому элементу, заканчивающемуся на .txt, например, notes.txt или hello.txt |
?????? | каждому элементу, имя которого состоит из шести символов, например, 01.txt, A-01.c или .zshrc |
A* | каждому элементу, начинающемуся с символа A, например, Album, A.txt или AppData |
[abc][abc][abc] | каждому элементу с трехсимвольным именем, которое состоит только из символов a, b и c, например, abc, aaa или cba |
С помощью этих шаблонов вы можете гибко сопоставлять множество различных типов файлов.
Базовый модуль, определяющий поведение .glob()
, – fnmatch
. Ознакомьтесь с его документацией, чтобы получить представление о других шаблонах, которые вы можете использовать в Python.
Обратите внимание, что в Windows шаблоны glob, как и пути в целом, не чувствительны к регистру. В Unix-подобных системах, таких как Linux и macOS, шаблоны glob регистрозависимы.
Условный листинг с использованием .glob()
Метод .glob()
объекта Path
ведет себя примерно так же, как .rglob()
. Если вы передадите аргумент “*”, то получите список элементов в каталоге, но без рекурсии:
>>> import pathlib >>> desktop = pathlib.Path("Desktop") >>> # .glob() также создает генератор >>> desktop.glob("*") <generator object Path.glob at 0x000001A8A50E2F00> >>> # Генератор можно обернуть в конструктор list() для материализации >>> list(desktop.glob("*")) [WindowsPath('Desktop/Notes'), WindowsPath('Desktop/realpython'), WindowsPath('Desktop/scripts'), WindowsPath('Desktop/todo.txt')]
Использование метода .glob()
с шаблоном glob “*” для объекта Path
создает генератор, который выдает все элементы в каталоге, представленном объектом Path
, не заходя в подкаталоги. Таким образом, он дает тот же результат, что и .iterdir()
. Полученный генератор можно использовать в цикле for
или в comprehension, как и в случае с iterdir()
.
Но, как вы уже поняли, методы glob отличает возможность использовать различные шаблоны для поиска только определенных путей. Например, если вам нужны только те пути, которые заканчиваются на .txt
, то вы можете сделать следующее:
>>> desktop = pathlib.Path("Desktop") >>> list(desktop.glob("*.txt")) [WindowsPath('Desktop/todo.txt')]
Поскольку в этом каталоге находится только один текстовый файл, вы получите список с одним элементом. Если бы вы хотели получить только элементы, начинающиеся с real, то вы могли бы использовать следующий шаблон glob:
>>> list(desktop.glob("real*")) [WindowsPath('Desktop/realpython')]
Этот пример также выводит только один элемент, потому что имя только одного элемента начинается с символов real. Помните, что в Unix-подобных системах шаблоны glob чувствительны к регистру.
Примечание: Под именем здесь понимается последняя часть пути, а не другие части пути, которые в данном случае начинаются с Desktop.
Вы также можете получить содержимое подкаталога, указав его имя, прямую косую черту (/) и звездочку. Этот тип шаблона позволит получить все, что находится внутри целевого каталога:
>>> list(desktop.glob("realpython/*")) [WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md')]
В этом примере использование шаблона “realpython/” дает все файлы в каталоге realpython
. Это даст вам тот же результат, что и создание объекта path, представляющего путь Desktop/realpython
, и вызов .glob("*")
для него.
Далее вы поближе познакомитесь с фильтрацией при помощи .rglob()
и узнаете, чем она отличается от применения .glob()
.
Условный листинг с помощью .rglob()
Точно так же, как и в методе .glob()
, вы можете настроить шаблон glob в .rglob()
, чтобы он выдавал вам только определенное расширение файла. Единственное отличие – .rglob()
всегда будет искать рекурсивно:
>>> list(desktop.rglob("*.md")) [WindowsPath('Desktop/Notes/hash-tables.md'), WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md')]
Мы добавили .md
к шаблону glob, и теперь .rglob()
выдает только .md-файлы в различных каталогах и подкаталогах.
Вы можете заставить .glob()
вести себя так же, как .rglob()
, т.е. работать рекурсивно. Для этого нужно изменить шаблон glob, переданный в качестве аргумента:
>>> list(desktop.glob("**/*.md")) [WindowsPath('Desktop/Notes/hash-tables.md'), WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md')]
В этом примере видно, что вызов .glob("**/*.md")
эквивалентен .rglob(*.md)
. Аналогично, вызов .glob("**/*")
эквивалентен .rglob("*")
.
Метод .rglob()
– это немного более явная версия вызова .glob()
с рекурсивным шаблоном. И возможно, лучше использовать более явную версию вместо рекурсивных шаблонов с обычным .glob()
.
Расширенный поиск с помощью методов Glob
Одним из потенциальных недостатков методов glob является то, что вы можете выбирать файлы только на основе шаблонов glob. Если вы хотите выполнить более сложное сопоставление или фильтрацию по атрибутам элемента, то вам придется воспользоваться чем-то еще.
Чтобы выполнить более сложное сопоставление и фильтрацию, можно следовать как минимум трем стратегиям. Вы можете использовать:
- Цикл
for
с условной проверкой - Comprehension с условным выражением
- Встроенную функцию
filter()
Вот как это делается:
>>> import pathlib >>> desktop = pathlib.Path("Desktop") >>> # Using a for loop >>> for item in desktop.rglob("*"): ... if item.is_file(): ... print(item) ... Desktop\todo.txt Desktop\Notes\hash-tables.md Desktop\realpython\iterate-dict.md Desktop\realpython\tictactoe.md Desktop\scripts\rename_files.py Desktop\scripts\request.py >>> # Используем генератор >>> [item for item in desktop.rglob("*") if item.is_file()] [WindowsPath('Desktop/todo.txt'), WindowsPath('Desktop/Notes/hash-tables.md'), WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md'), WindowsPath('Desktop/scripts/rename_files.py'), WindowsPath('Desktop/scripts/request.py')] >>> # Используем функцию filter() >>> list(filter(lambda item: item.is_file(), desktop.rglob("*"))) [WindowsPath('Desktop/todo.txt'), WindowsPath('Desktop/Notes/hash-tables.md'), WindowsPath('Desktop/realpython/iterate-dict.md'), WindowsPath('Desktop/realpython/tictactoe.md'), WindowsPath('Desktop/scripts/rename_files.py'), WindowsPath('Desktop/scripts/request.py')]
В этих примерах мы сперва вызываем метод .rglob()
с шаблоном “*”, чтобы получить все элементы рекурсивно. В результате имеем все элементы в каталоге и его подкаталогах.
Затем мы используем три различных подхода, чтобы отфильтровать элементы, которые не являются файлами. Обратите внимание, что в случае с filter()
используется лямбда-функция.
Методы glob чрезвычайно универсальны, но для больших деревьев каталогов они могут быть немного медленными. В следующем разделе мы разберем пример с более контролируемой итерацией с помощью .iterdir()
.
Исключение нежелательных каталогов из листинга
Допустим, вы хотите найти все файлы в вашей системе. Но у вас есть различные подкаталоги, в которых содержится множество подкаталогов и файлов. Некоторые из самых больших подкаталогов – это временные файлы, которые вас не интересуют.
Например, посмотрите на это дерево каталогов, в котором есть нежелательные каталоги – их очень много! На самом деле полное дерево каталогов имеет длину 1 850 строк. Везде, где вы видите многоточие (…), это означает, что в этом месте находятся сотни нежелательных файлов:
large_dir/ ├── documents/ │ ├── notes/ │ │ ├── temp/ │ │ │ ├── 2/ │ │ │ │ ├── 0.txt │ │ │ │ ... │ │ │ │ │ │ │ ├── 0.txt │ │ │ ... │ │ │ │ │ ├── 0.txt │ │ └── find_me.txt │ │ │ ├── tools/ │ │ ├── temporary_files/ │ │ │ ├── logs/ │ │ │ │ ├──0.txt │ │ │ │ ... │ │ │ │ │ │ │ ├── temp/ │ │ │ │ ├──0.txt │ │ │ │ ... │ │ │ │ │ │ │ ├── 0.txt │ │ │ ... │ │ │ │ │ ├── 33.txt │ │ ├── 34.txt │ │ ├── 36.txt │ │ ├── 37.txt │ │ └── real_python.txt │ │ │ ├── 0.txt │ ├── 1.txt │ ├── 2.txt │ ├── 3.txt │ └── 4.txt │ ├── temp/ │ ├── 0.txt │ ... │ └── temporary_files/ ├── 0.txt ...
Проблема в том, что у вас есть нежелательные каталоги. Их иногда называют temp
, иногда temporary files
, а иногда logs
. Усугубляет ситуацию то, что они повсюду и могут находиться на любом уровне вложенности. Хорошей новостью является то, что их можно отфильтровать.
Использование .rglob() для фильтрации целых каталогов
Если вы используете.rglob()
, можно просто отфильтровать элементы после того, как они будут получены.
Чтобы правильно отбрасывать пути, находящиеся в нежелательном каталоге, вы можете проверить, совпадает ли любой из элементов пути с любым из элементов в списке каталогов, которые нужно пропустить:
>>> SKIP_DIRS = ["temp", "temporary_files", "logs"]
Здесь вы определяете SKIP_DIRS как список, содержащий строки путей, которые вы хотите исключить.
Вызов .rglob()
со звездочкой в качестве аргумента выдаст все элементы, даже те, которые находятся в каталогах, которые вас не интересуют. Вам придется перебрать все элементы, и если вы смотрите только на имя пути, вы можете столкнуться с проблемой:
large_dir/documents/notes/temp/2/0.txt
Поскольку имя просто 0.txt
, оно не будет соответствовать никаким элементам в SKIP_DIRS
. Вам нужно будет проверить весь путь на наличие заблокированного имени.
Все части пути можно получить с помощью атрибута .parts
, содержащего кортеж всех элементов в пути:
>>> import pathlib >>> temp_file = pathlib.Path("large_dir/documents/notes/temp/2/0.txt") >>> temp_file.parts ('large_dir', 'documents', 'notes', 'temp', '2', '0.txt')>>> import pathlib >>> temp_file = pathlib.Path("large_dir/documents/notes/temp/2/0.txt") >>> temp_file.parts ('large_dir', 'documents', 'notes', 'temp', '2', '0.txt')
Остается только проверить, есть ли какой-либо элемент кортежа .parts
в списке каталогов, которые нужно пропустить.
Используя множества, можно проверить, есть ли у двух итерируемых объектов общий элемент. Если привести один из итерируемых объектов ко множеству, то при помощи метода .isdisjoint()
можно определить, есть ли у них общие элементы:
>>> {"documents", "notes", "find_me.txt"}.isdisjoint({"temp", "temporary"}) True >>> {"documents", "temp", "find_me.txt"}.isdisjoint({"temp", "temporary"}) False
Если два множества не имеют общих элементов, то .isdisjoint()
возвращает True. Если два множества имеют хотя бы один общий элемент, то .isdisjoint()
возвращает False.
Эту проверку можно включить в цикл for
, который перебирает все элементы, возвращаемые .rglob("*")
:
>>> SKIP_DIRS = ["temp", "temporary_files", "logs"] >>> large_dir = pathlib.Path("large_dir") >>> # С помощью цикла for >>> for item in large_dir.rglob("*"): ... if set(item.parts).isdisjoint(SKIP_DIRS): ... print(item) ... large_dir\documents large_dir\documents\0.txt large_dir\documents\1.txt large_dir\documents\2.txt large_dir\documents\3.txt large_dir\documents\4.txt large_dir\documents\notes large_dir\documents\tools large_dir\documents\notes\0.txt large_dir\documents\notes\find_me.txt large_dir\documents\tools\33.txt large_dir\documents\tools\34.txt large_dir\documents\tools\36.txt large_dir\documents\tools\37.txt large_dir\documents\tools\real_python.txt
В этом примере выводятся все элементы в large_dir
, которые не находятся ни в одной из ненужных папок.
Чтобы проверить, находится ли путь в одной из ненужных папок, вы приводите item.parts
ко множеству. Затем используете .isdisjoint()
, чтобы проверить, нет ли у SKIP_DIRS
и .parts
общих элементов. Если есть, то этот элемент будет напечатан.
Того же эффекта можно добиться с помощью filter()
и comprehensions, как показано ниже:
>>> # с помощью генератора >>> [ ... item ... for item in large_dir.rglob("*") ... if set(item.parts).isdisjoint(SKIP_DIRS) ... ] >>> # используем filter() >>> list( ... filter( ... lambda item: set(item.parts).isdisjoint(SKIP_DIRS), ... large_dir.rglob("*") ... ) ... )
Однако эти методы уже немного сложны для понимания. Кроме того, они не очень эффективны. Чтобы операция сопоставления могла отбросить результат, генератор .rglob()
должен выдать все элементы.
Безусловно, с помощью .rglob()
можно отфильтровать целые папки. Но в любом случае результирующий генератор будет выдавать все элементы, а затем отфильтровывать нежелательные один за другим. Это может очень замедлить методы glob (в зависимости от вашего случая использования). Именно поэтому вы можете предпочесть рекурсивную функцию .iterdir()
.
Создание рекурсивной функции .iterdir()
В примере с нежелательными каталогами вам в идеале нужна возможность избежать перебора всех файлов в данном подкаталоге, если они соответствуют одному из имен в SKIP_DIRS
:
# skip_dirs.py import pathlib SKIP_DIRS = ["temp", "temporary_files", "logs"] def get_all_items(root: pathlib.Path, exclude=SKIP_DIRS): for item in root.iterdir(): if item.name in exclude: continue yield item if item.is_dir(): yield from get_all_items(item)
В этом модуле мы определяем список строк SKIP_DIRS
, который содержит имена каталогов, которые нужно проигнорировать. Затем мы определяем функцию-генератор, которая использует .iterdir()
для перебора всех элементов.
Функция-генератор использует аннотацию типа : pathlib.Path
после первого аргумента, чтобы указать, что вы не можете просто передать строку, представляющую путь. Аргумент должен быть объектом Path
.
Если имя элемента находится в списке исключений, то вы просто переходите к следующему элементу, пропуская все дерево подкаталогов сразу. Когда элемента нет в списке, вы возвращаете элемент. Если же это каталог, то вы снова вызываете функцию для этого каталога.
То есть внутри тела функции при определенных условиях вызывается та же функция еще раз. Это отличительная черта рекурсивной функции.
Эта рекурсивная функция эффективно выдает все файлы и каталоги, которые вам нужны, исключая всё, что вас не интересует:
>>> import pathlib >>> import skip_dirs >>> large_dir = pathlib.Path("large_dir") >>> list(skip_dirs.get_all_items(large_dir)) [WindowsPath('large_dir/documents'), WindowsPath('large_dir/documents/0.txt'), WindowsPath('large_dir/documents/1.txt'), WindowsPath('large_dir/documents/2.txt'), WindowsPath('large_dir/documents/3.txt'), WindowsPath('large_dir/documents/4.txt'), WindowsPath('large_dir/documents/notes'), WindowsPath('large_dir/documents/notes/0.txt'), WindowsPath('large_dir/documents/notes/find_me.txt'), WindowsPath('large_dir/documents/tools'), WindowsPath('large_dir/documents/tools/33.txt'), WindowsPath('large_dir/documents/tools/34.txt'), WindowsPath('large_dir/documents/tools/36.txt'), WindowsPath('large_dir/documents/tools/37.txt'), WindowsPath('large_dir/documents/tools/real_python.txt')]
Очень важно, что вам удалось избежать просмотра всех файлов в ненужных каталогах. Как только генератор определит, что каталог находится в списке SKIP_DIRS
, он просто пропустит его целиком.
Таким образом, в данном случае использование .iterdir()
будет намного эффективнее, чем эквивалентные методы glob.
Метод .iterdir()
в целом более эффективен, чем методы glob, если для нужной вам фильтрации не хватит возможностей шаблонов glob. Однако, если вам нужно получить список всех .txt-файлов рекурсивно, то методы glob будут быстрее.
Теперь вы можете определить, как лучше всего в вашем случае получить список файлов и папок!
Заключение
В этом руководстве мы разобрали методы .glob()
, .rglob()
и .iterdir()
из модуля Python pathlib. Все они позволяют получить список файлов и папок в заданном каталоге, в том числе рекурсивно.
В целом, если вам нужен только основной список элементов в каталоге, без рекурсии, то .iterdir()
, благодаря своему описательному названию, является наиболее чистым методом. Он также более эффективен в этой работе.
Если же вам нужен рекурсивный список файлов, то лучше всего использовать .rglob()
, который будет быстрее, чем эквивалентный рекурсивный .iterdir()
.
Мы также рассмотрели один пример, в котором использование .iterdir()
для рекурсивного списка может дать огромный выигрыш в производительности – когда у вас есть ненужные папки, которые вы хотите исключить из итераций.
Спасибо за внимание!
Перевод статьи «How to Get a List of All Files in a Directory With Python».
Пингбэк: Как на Python найти файлы, имеющие определенное расширение