Как эффективно выполнять параллельный поиск файлов с использованием pathlib `glob` в Python для больших структур каталогов?

Я работаю над проектом Python, где мне нужно искать определенные файлы в очень большой структуре каталогов. В настоящее время я использую метод glob (или rglob) из модуля pathlib, но он довольно медленный из-за большого количества файлов и каталогов.

Вот упрощенная версия моего текущего кода:

from pathlib import Path

base_dir = Path("/path/to/large/directory")
files = list(base_dir.rglob("ind_stat.zpkl"))

Это работает, но слишком медленно, поскольку приходится проходить через огромное количество каталогов и файлов. В идеале я хотел бы разделить работу по обходу каталога между несколькими потоками или процессами, чтобы повысить производительность. Существуют ли оптимизации или альтернативные библиотеки/методы, которые могут помочь повысить производительность?

Честно говоря, я бы, наверное, посмотрел на обертку чего-то вроде crates.io/crates/jwalk с PyO3, если вам действительно нужна вся возможная скорость.

AKX 08.07.2024 12:23

Другой, более быстрый и простой способ, если вы знаете, что /path/to/large/directory не будет меняться очень часто, — это выполнить обход только один раз и сохранить список файлов, например. базу данных SQLite и выполните запрос к ней.

AKX 08.07.2024 12:24

обратите внимание, что параллельный поиск файлов имеет смысл только в медленной сетевой файловой системе, которая допускает одновременные запросы. Но, скорее всего, для этого есть более эффективный API. При поиске на локальной ФС простой rglob будет быстрее практически независимо от типа хранилища. Массовый параллелизм без сравнительного анализа слишком часто является пессимизацией.

LogicDaemon 08.07.2024 13:17

Вы не упоминаете свою файловую систему, тип/конфигурацию диска, объем данных, количество файлов, глубину/расположение файлов или текущую/желаемую производительность...

Mark Setchell 08.07.2024 13:49

@LogicDaemon «При поиске в локальной файловой системе простой rglob будет быстрее почти независимо от типа хранилища» — на чем вы это основываете? Быстрее, чем что? Учитывая, что все биты, включая подстановку, используемые rglob, написаны на Python, вероятно, можно легко придумать более быструю и менее общую функцию для поиска всех ind_stat.zpkl.

AKX 08.07.2024 14:15

@AKX, который в основном основан на двух предположениях: 1. Интерфейс между системой хранения и оперативной памятью является последовательным (SATA или PCIe или что-то еще). 2. Процессор достаточно быстр, чтобы обрабатывать все операции ввода-вывода с помощью одного ядра. Второе предположение не всегда верно, но в тех случаях, когда оно неверно, накладные расходы на многопроцессорную обработку также гораздо более значительны. Упомянутый вами rglob на самом деле не выполняет никакой тяжелой обработки в коде Python. Он использует os.scandir под капотом, что довольно быстро. Также см. Softwareengineering.stackexchange.com/q/408687/436501

LogicDaemon 08.07.2024 16:09

@LogicDaemon Если rglob не выполняет тяжелую обработку, забавно, как можно реализовать задачу OP в 1,8 раза быстрее, чем rglob тогда (см. мой ответ)! Во-вторых, поскольку на самом деле это не считывает фактические (большие) данные с диска, я предполагаю, что в ОС уже кэшировано множество структур FS для каталогов и т. д., поэтому накладные расходы системы хранения не будут иметь большого значения. там.

AKX 08.07.2024 16:33

@AKX мой первоначальный комментарий был о параллельном поиске, о котором говорилось в вопросе. И как показывает пример SIGHUP, это хуже вашего варианта. Для меня до сих пор сюрприз, что rglob так заметно тормозит, спасибо, что открыли мне глаза :)

LogicDaemon 08.07.2024 19:13

@LogicDaemon Я все еще верю, что ядро ​​может обслуживать несколько вызовов opendir и getdirentries для разных пользовательских потоков параллельно. Потоки Python, конечно, совсем другие из-за GIL.

AKX 08.07.2024 19:57

@AKX Я согласен, что ядро ​​делает это. Но накладные расходы, связанные с созданием процесса для работы с GIL и последующей синхронизацией, почти никогда не окупаются в cpython для операций с локальными каталогами (оно того стоит для относительно больших файлов).

LogicDaemon 09.07.2024 06:42

Вы также можете удалить свой вопрос, если вы не собираетесь отвечать на запросы о разъяснениях или голосовать за / принимать любые ответы, которые любезно предложили люди.

Mark Setchell 10.07.2024 13:00
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
11
101
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Эмпирически на моем Mac в дереве каталогов, содержащем 73 429 файлов и 3315 каталогов (и 3 файла с именем Snap.wav):

~/Samples $ pwd
/Users/akx/Samples
~/Samples $ find . -type f | wc -l
   73429
~/Samples $ find . -type d | wc -l
    3315

тривиальная реализация на основе os.walk() в 1,8 раза быстрее, чем rglob:

import os
import pathlib

base_path = pathlib.Path("/Users/akx/Samples")
file_to_find = "Snap.wav"


def find_snaps_rglob():
    paths = []
    for path in base_path.rglob(file_to_find):
        paths.append(path)
    return paths


def find_snaps_walk():
    paths = []
    for dp, dn, fn in os.walk(base_path):
        for f in fn:
            if f == file_to_find:
                paths.append(pathlib.Path(os.path.join(dp, f)))
    return paths


assert sorted(find_snaps_rglob()) == sorted(find_snaps_walk())
name='find_snaps_rglob' iters=10 time=1.622 iters_per_sec=6.17
name='find_snaps_walk' iters=20 time=1.740 iters_per_sec=11.49

Реализация ходьбы со стеком и os.scandir() еще немного ускоряет работу (iters_per_sec=12.98), но для этого, вероятно, не потребуется, например, символические ссылки и ошибки, например walk или rglob, могут быть учтены.

def find_snaps_walk_manually():
    paths = []
    stack = [base_path]
    while stack:
        current = stack.pop()
        for child in os.scandir(current):
            if child.is_dir():
                stack.append(child)
            elif child.name == file_to_find:
                paths.append(pathlib.Path(child.path))
    return paths

Я также попробовал обернуть ящики Rust walkdir и jwalk PyO3 — они были не намного быстрее, чем find_snaps_walk_manually (около 13,14/13,34 RPS).

да, кстати, scandir был поздней реализацией, которая использовала интерактивные API ОС, а не просто быстро получала все результаты из os.walk. Я думаю, что в pathlib Path.iterdir это использует. Он должен нормально работать с многопоточностью (даже с многопоточностью GILocked Python).

jsbueno 08.07.2024 23:11
glob._Globber (что лежит в основе Path.rglob()) также использует scandir. Но учитывая, что даже реализации на основе Rust примерно такие же быстрые, я думаю, что мы почти достигли предела того, что базовые синхронные системные вызовы могут вытолкнуть в пользовательское пространство. Следующим шагом будет что-то вроде io_uring getdents...
AKX 09.07.2024 07:05

Существуют ли оптимизации или альтернативные библиотеки/методы, которые могут помочь повысить производительность?

Да, обновление до более новой версии Python должно обеспечить существенное улучшение производительности. Python 3.13 (в настоящее время находится в бета-версии) включает наибольшее ускорение.

(Я уже пытался дать этот ответ, но несколько модераторов SO бросились мне в глотку. Если они сделают это снова, я впредь не буду предоставлять никакой поддержки pathlib для SO.)

Хороший комментарий! Я попробовал свои примеры на 3.13b0, и rglob работает значительно быстрее, чем в 3.12, но find_snaps_walk_manually по-прежнему с небольшим преимуществом (13,13 ips против 12,19 ips).

AKX 11.07.2024 09:28

Другие вопросы по теме