Работа с файлами и директориями в Qt

Поиск файлов по маске и рекурсивный обход директорий с QDirIterator

Введение

Поиск всех файлов с определённым расширением в дереве каталогов, подсчёт суммарного размера логов, построение индекса медиатеки — все эти задачи требуют эффективного обхода файловой системы. QDir::entryList() подходит для одного уровня, но для рекурсивного обхода больших деревьев каталогов оптимальным инструментом в Qt является QDirIterator.

Концепция

QDirIterator — это итератор, который последовательно обходит содержимое директории, не загружая сразу весь список в память (в отличие от QDir::entryInfoList(), который сначала формирует полный список, а потом возвращает его). Это особенно важно при работе с большими деревьями файлов — счёт может идти на сотни тысяч записей.

Флаг QDirIterator::Subdirectories включает рекурсивный обход, а фильтры QDir::Filters (например, QDir::Files, QDir::NoDotAndDotDot) и маски имён (QStringList с шаблонами типа "*.cpp") позволяют точно настроить, что именно требуется найти.

Пример кода: Поиск файлов по

#include <QDirIterator>
#include <QDebug>
#include <QFileInfo>
#include <QElapsedTimer>

// Рекурсивный поиск всех файлов с заданными расширениями
QStringList findFilesByExtension(const QString &rootPath, const QStringList &nameFilters)
{
    QStringList result;

    QDirIterator it(rootPath, nameFilters,
                     QDir::Files | QDir::NoSymLinks,
                     QDirIterator::Subdirectories);

    while (it.hasNext()) {
        result.append(it.next());
    }

    return result;
}

// Подсчёт суммарного размера всех файлов в дереве, с фильтрацией по дате
qint64 calculateTotalSize(const QString &rootPath, const QDateTime &modifiedAfter)
{
    qint64 totalSize = 0;
    int fileCount = 0;

    QDirIterator it(rootPath, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories);

    while (it.hasNext()) {
        it.next();
        QFileInfo info = it.fileInfo();

        if (info.lastModified() >= modifiedAfter) {
            totalSize += info.size();
            ++fileCount;
        }
    }

    qDebug() << "Найдено файлов:" << fileCount << "общий размер:" << totalSize << "байт";
    return totalSize;
}

// Поиск пустых директорий — пример обхода без фильтра по файлам
QStringList findEmptyDirectories(const QString &rootPath)
{
    QStringList emptyDirs;

    QDirIterator it(rootPath, QDir::Dirs | QDir::NoDotAndDotDot,
                     QDirIterator::Subdirectories);

    while (it.hasNext()) {
        QString dirPath = it.next();
        QDir dir(dirPath);
        if (dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) {
            emptyDirs.append(dirPath);
        }
    }

    return emptyDirs;
}

int main()
{
    QElapsedTimer timer;
    timer.start();

    QStringList cppFiles = findFilesByExtension(".", {"*.cpp", "*.h", "*.hpp"});
    qDebug() << "Найдено исходных файлов:" << cppFiles.size()
             << "за" << timer.elapsed() << "мс";

    for (const QString &file : cppFiles.mid(0, 5))
        qDebug() << " -" << file;

    QDateTime weekAgo = QDateTime::currentDateTime().addDays(-7);
    calculateTotalSize(".", weekAgo);

    QStringList empties = findEmptyDirectories(".");
    qDebug() << "Пустых директорий найдено:" << empties.size();

    return 0;
}

Пояснения к коду

Конструктор QDirIterator(path, nameFilters, filters, flags) принимает маски имён файлов (поддерживает стандартные wildcard-шаблоны * и ?), фильтры типов записей и флаг рекурсии. Метод next() продвигает итератор и возвращает абсолютный путь к найденному элементу, а fileInfo() даёт доступ к полной информации (QFileInfo) без дополнительного системного вызова stat.

Флаг QDir::NoSymLinks исключает символические ссылки из обхода — это важно, чтобы избежать бесконечного цикла, если симлинк создаёт круговую ссылку на родительский каталог.

Подводные камни

  1. Циклы через символические ссылки. Без исключения симлинков (QDir::NoSymLinks) рекурсивный обход может попасть в бесконечный цикл, если где-то в дереве есть ссылка, указывающая на родительскую или текущую директорию. QDirIterator не имеет встроенной защиты от таких циклов.
  1. Производительность entryInfoList() против QDirIterator. Для больших деревьев каталогов накопление полного списка через entryInfoList() на каждом уровне рекурсии (если обход реализован вручную) расходует значительно больше памяти и времени, чем потоковый обход через QDirIterator — особенно заметно на сетевых дисках с высокой латентностью.
  1. Права доступа к отдельным поддиректориям. Если у процесса нет прав на чтение какой-либо вложенной директории, QDirIterator просто пропустит её содержимое без исключения — ошибку нужно дополнительно проверять через QFileInfo::isReadable(), если это критично для логики приложения.
  1. Регистр символов в масках имён. На Linux маски имён файлов чувствительны к регистру (*.CPP не совпадёт с file.cpp), а на Windows — нет. Если код должен работать одинаково на всех платформах, стоит явно задавать QDirIterator::NoIteratorFlags совместно с собственной фильтрацией через QString::compare(Qt::CaseInsensitive).
  1. Блокировка UI при обходе больших деревьев. Обход файловой системы с десятками тысяч файлов в основном потоке приложения с интерфейсом вызывает заметные задержки. Для отзывчивого UI стоит выполнять обход в отдельном потоке (QThread/QtConcurrent) и периодически сообщать о прогрессе через сигналы.
  1. Изменение файловой системы во время обхода. Если файлы добавляются или удаляются параллельно с обходом (например, другим процессом), результат может быть неполным или содержать ссылки на уже удалённые файлы — это нормальное поведение для большинства файловых систем, и код должен быть готов к ошибкам при последующей работе с найденными путями.

QDirIterator остаётся самым практичным инструментом Qt для обхода файловых деревьев — он сочетает простоту API с эффективностью по памяти, что особенно ценно при работе с большими файловыми хранилищами.

================================================================================