Пятница, 08.08.2025 19:00

Разбор (парсинг) вывода результатов запущенной программы по строкам. Запуск внешних программ в Qt6. Часть 6

Разбор (парсинг) вывода результатов запущенной программы по строкам. Запуск внешних программ в Qt6. Часть 6

В прошлых частях мы рассмотрели разбор (парсинг) результатов работы запущенной программы, но при этом не рассматривали сами результаты вывода.

Сегодня мы рассмотрим построчный разбор (парсинг) вывода результата работы внешней программы в Qt6.

Оптимизация класса DirSizeWorker

Первым делом оптимизируем класс DirSizeWorker.

private:
    RegexParser *parser;

Приведем метод onRead к виду:

void DirSizeWorker::onRead(const QString &data)
{
    qDebug() << "NEW Worker:" << data;


    QString files = this->parser->parseGroup(data,"files");
    QString foldersize = this->parser->parseGroup(data,"foldersize");

    if (!files.isEmpty() && !foldersize.isEmpty())
    {
        qDebug() << files;
        qDebug() << foldersize;

        RegexParser p = RegexParser("\\xa0");
        QString folderFreeSizeNumber = p.replace(foldersize,"");

        qDebug() << "Число: " << folderFreeSizeNumber;

        bool isOk = false;
        double folderFreeSizeMb = folderFreeSizeNumber.toDouble(&isOk);
        qDebug() << "isOk" << isOk;
        qDebug() << folderFreeSizeMb;

        int diskMbSize = folderFreeSizeMb / 1024 / 1024;
        qDebug() << diskMbSize << "Mb";
    }
}

Таким образом мы убрали лишний вызов конструктора RegexParser для каждой обрабатываемой строки.

Разбираем (парсим) вывод программы по строкам

Если мы обратим внимание на результат запуска, то увидим следующее:

Worker: "07.12.2019  20:31    <DIR>          SKB\r\n29.07.2025  08:55   <DIR>         SoftwareDistribution\r\n"
Worker: "07.12.2019  20:14    <DIR>          Speech\r\n07.12.2019  20:14   <DIR>         Speech_OneCore\r\n"
Worker: "12.07.2025  13:30           164 352 splwow64.exe\r\n"
Worker: "07.12.2019  20:14    <DIR>          System\r\n"

Обратите внимание на 

\r\n

Это символы перехода на новую строку.

На самом деле вместо:

07.12.2019  20:31   <DIR>         SKB\r\n29.07.2025  08:55    <DIR>          SoftwareDistribution\r\n
07.12.2019  20:14   <DIR>         Speech\r\n07.12.2019  20:14    <DIR>          Speech_OneCore\r\n
12.07.2025  13:30          164 352 splwow64.exe\r\n
07.12.2019  20:14   <DIR>          System\r\n

Нам нужно :

07.12.2019  20:31   <DIR>          SKB\r\n
29.07.2025  08:55   <DIR>          SoftwareDistribution\r\n
07.12.2019  20:14   <DIR>          Speech\r\n
07.12.2019  20:14   <DIR>         Speech_OneCore\r\n
12.07.2025  13:30          164 352 splwow64.exe\r\n
07.12.2019  20:14   <DIR>          System\r\n

Т.е. нам нужно озаботится нормальной разбивкой на строки.

В одной из прошлых задач мы отметили, что QProcess выводит данные построчно, это на самом деле так, но в зависимости от производительности ПК или загруженности системы, он может склеивать несколько строк.

Чтобы перейти к построчной обработке вывода, нам нужно удостоверится, что при обработке полученных данных, они будут разбиты на строки.

Разбиваем вывод программы на строки с помощью регулярных выражений

В класс RegexParser добавим новый метод:

void parseAll(QString data);

Реализация:

void RegexParser::parseAll(QString data)
{
    QRegularExpressionMatchIterator i = this->regularExp->globalMatch(data);

    if(!this->regularExp->isValid())
    {
        qDebug() << "ERROR: "<< this->regularExp->errorString();
        exit(EXIT_FAILURE);
    }

    while (i.hasNext()) {
        QRegularExpressionMatch next = i.next();
       for (int var = 1; var < next.capturedTexts().count(); ++var) {
            QString cap = next.captured(var);
            if (!cap.trimmed().isEmpty())
            {
                qDebug() << cap.trimmed();
            }

        }
    }
}

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

Обратите внимание, что мы обрабатываем элементы с индекса 1, это необходимо из-за того, что в элемент с индексом 0 помещается вся исходная строка, в которой было обнаружено совпадение!

С помощью: 

cap.trimmed()

Мы удаляем лишние пробелы в начале и конце и переносы строк.

Временно вернемся к использованию старого класса Worker, для этого в main.cpp заменим:

Thread *t = new Thread("cmd", params, new Runner(), new Worker());

В класс Worker в начало метода onRead добавим:

this->parser->parseAll(data);

Добавим приватное поле:

private:
    RegexParser *parser;

В конструктор класса добавим:

this->parser = new RegexParser("(.[^\\r\\n]*)(?:\\n|\\r\\n)?");

Запустим.

Обратите внимание, что теперь в строках вывода, содержащих символы перехода на новую строку, наш парсер выводит две отдельные строки:

"07.10.2024  10:27           764 688 pyw.exe"
"18.05.2024  13:45           370 176 regedit.exe"
Worker: "07.10.2024  10:27           764 688 pyw.exe\r\n18.05.2024  13:45           370 176 regedit.exe"

Приведем метод onRead к первоначальному виду:

void Worker::onRead(const QString &data)
{
   qDebug() << "Worker:" << data;
}

И удалим поле класса и добавленную строку из конструктора класса Worker.

Создаем дочерний класс ReadLineWorker

Разбивка по строкам это одна из задач разбора результатов, они могу быть и в CSV формате, в XML, HTML или JSON. Поэтому оставим класс Worker базовым и создадим отдельный класс, для разбора вывода по строкам:

Заголовок:

#ifndef READLINEWORKER_H
#define READLINEWORKER_H

#include <QObject>

#include "worker.h"
#include "regexparser.h"

class ReadLineWorker: public Worker
{
    Q_OBJECT
public:
    explicit ReadLineWorker(QObject *parent = 0);

    virtual void onRead(const QString &data);

private:
    RegexParser *parser;

public slots:
    void onStringRead(QString data);
};

#endif // READLINEWORKER_H

Реализация:

#include "readlineworker.h"

ReadLineWorker::ReadLineWorker(QObject *parent)
{
    this->parser = new RegexParser("(.[^\\r\\n]*)(?:\\n|\\r\\n)?");

    QObject::connect(this->parser, SIGNAL(matched(QString)),
                     this, SLOT(onStringRead(QString)));

}

void ReadLineWorker::onRead(const QString &data)
{
    this->parser->parseAll(data);

    //     qDebug() << "Worker:" << data;
}

void ReadLineWorker::onStringRead(QString data)
{
    qDebug() << "Worker String" << data;
}

В main.cpp заменим:

Thread *t = new Thread("cmd", params, new Runner(), new ReadLineWorker());

Запустим, результат тот же. 

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

В класс RegexParser добавим сигнал:

signals:
    void matched(QString data);

В методе parseAll класса RegexParser заменим:

qDebug() << cap.trimmed(); 

На:

emit matched(cap.trimmed());

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

Соединим сигнал парсера со слотом  нашего класса, для этого в конструктор класса ReadLineWorker добавим:

    QObject::connect(this->parser, SIGNAL(matched(QString)),
            this, SLOT(onStringRead(QString)));

В класс ReadLineWorker добавим слот:

private slots:
void onStringRead(QString data);

Реализация:

void Worker::onStringRead(QString data)
{
    qDebug() << "Worker String" << data;
}

В методе onRead закомментируем строку:

     qDebug() << "Worker:" << data;

Запустим:

Worker String "D:\\projects\\QtRunnerDemo3\\build\\Work_Desktop_Qt_6_9_1_shared_MinGW_w64_MINGW64_MSYS2-Debug>dir"
Worker String "c:\\windows\\"
Worker String "Том в устройстве C не имеет метки."
Worker String "Серийный номер тома: 5A32-0ABC"
Worker String "Содержимое папки c:\\windows"
Worker String "21.07.2025  12:42    <DIR>          ."
Worker String "21.07.2025  12:42    <DIR>          .."
Worker String "08.12.2019  01:37    <DIR>          addins"
Worker String "15.11.2023  13:57    <DIR>          ADFS"
Worker String "21.10.2024  17:10    <DIR>          appcompat"
Worker String "22.01.2025  14:30    <DIR>          apppatch"
Worker String "06.08.2025  15:52    <DIR>          AppReadiness"
Worker String "12.07.2025  14:06    <DIR>          bcastdvr"
Worker String "19.02.2025  17:40            93 696 bfsvc.exe"
Worker String "16.04.2024  16:42    <DIR>          Boot"
Worker String "07.12.2019  20:14   <DIR>         Branding"
…
Worker String "07.08.2025  11:10               276 WindowsUpdate.log"
Worker String "07.12.2019  20:10            11 776 winhlp32.exe"
Worker String "13.07.2025  12:11    <DIR>          WinSxS"
Worker String "08.12.2019  01:39           316 640 WMSysPr9.prx"
Worker String "07.12.2019  08:29            11 264 write.exe"
Worker String "31 файлов     12 739 689 байт"
Worker String "79 папок   6 070 812 672 байт свободно"
Worker: Внешняя программа успешно завершилась с кодом: 0 и статусом  QProcess::NormalExit

Теперь для каждой найденной парсером строки будет отправляться сигнал и вызываться слот onStringRead.

Теперь вывод корректно разбивается по строкам и его проще обрабатывать!

Модернизируем класс DirSizeWorker

У нас есть новый базовый класс для разбивки результатов по строкам, сделаем класс ReadLineWorker родительским для класса DirSizeWorker.

Для того, чтобы класс DirSizeWorker мог получать доступ к строкам, нам потребуется отправлять сигнал этому классу из класса ReadLineWorker.

В класс ReadLineWorker добавим сигнал:

signals:
    void stringRead(QString data);

Изменим слот:

void ReadLineWorker::onStringRead(QString data)
{
  emit stringRead(data);
}

Теперь при получении новой строки мы просто отправляем новый сигнал.

В результате класс DirSizeWorker изменится:

#ifndef DIRSIZEWORKER_H
#define DIRSIZEWORKER_H

#include "readlineworker.h"

class DirSizeWorker : public ReadLineWorker
{
public:
    explicit DirSizeWorker(QObject *parent = nullptr);

public slots:
    void onStringReady(const QString &data);

private:
    RegexParser *parser;    
};

#endif // DIRSIZEWORKER_H

Реализацию:

#include "dirsizeworker.h"
#include <QDebug>
DirSizeWorker::DirSizeWorker(QObject *parent)
   : ReadLineWorker{parent}
{
   this->parser = new RegexParser("\\s*(?<files>\\d*)\\s*файлов\\s*(?<foldersize>[\\d\\xa0]*)\\sбайт");
   QObject::connect(this, ReadLineWorker::stringRead,
                    this, DirSizeWorker::onStringReady);
}
void DirSizeWorker::onStringReady(const QString &data)
{
   qDebug() << "NEW Worker:" << data;

   QString files = this->parser->parseGroup(data,"files");
   QString foldersize = this->parser->parseGroup(data,"foldersize");
   if (!files.isEmpty() && !foldersize.isEmpty())
   {
       qDebug() << files;
       qDebug() << foldersize;
       RegexParser p = RegexParser("\\xa0");
       QString folderFreeSizeNumber = p.replace(foldersize,"");
       qDebug() << "Число: " << folderFreeSizeNumber;
       bool isOk = false;
       double folderFreeSizeMb = folderFreeSizeNumber.toDouble(&isOk);
       qDebug() << "isOk" << isOk;
       qDebug() << folderFreeSizeMb;
       int diskMbSize = folderFreeSizeMb / 1024 / 1024;
       qDebug() << diskMbSize << "Mb";
   }
}

Мы связали сигнал stringRead родительского класса и слот дочернего.

В main.cpp изменим:

Thread *t = new Thread("cmd", params, new Runner(), new DirSizeWorker());

Запустим:

NEW Worker: "D:\\projects\\QtRunnerDemo3\\build\\Work_Desktop_Qt_6_9_1_shared_MinGW_w64_MINGW64_MSYS2-Debug>"
NEW Worker: "dir"
NEW Worker: "c:\\windows\\"
NEW Worker: "Том в устройстве C не имеет метки."
NEW Worker: "Серийный номер тома: 5A32-0ABC"
NEW Worker: "Содержимое папки c:\\windows"
NEW Worker: "21.07.2025  12:42    <DIR>          ."
NEW Worker: "21.07.2025  12:42    <DIR>          .."
NEW Worker: "08.12.2019  01:37    <DIR>          addins"
NEW Worker: "15.11.2023  13:57    <DIR>          ADFS"
NEW Worker: "21.10.2024  17:10    <DIR>          appcompat"
…
>> "31"
>> "12 739 689"
"31"
"12 739 689"
Число:  "12739689"
isOk true
1.27397e+07
12 Mb
NEW Worker: "79 папок  6 061 297 664 байт свободно"
Worker: Внешнаяя программа успешно завершилась с кодом: 0 и статусом  QProcess::NormalExit

Мы избавились от лишних пробелов и переносов строк и теперь можем обрабатывать результаты построчно.

Заключение

Сегодня мы рассмотрели построчный разбор (парсинг) вывода результата работы внешней программы в Qt6:

Оптимизировали класс DirSizeWorker;

Доработали класс RegexParser добавив в него новый сигнал, отправляемый каждый раз, когда обнаружено совпадение;

Создали класс ReadLineWorker для работы с разбивкой вывода на строки;

Наследовали класс DirSizeWorker от ReadLineWorker.

Категория Qt6
Теги Qt Qt6 Cpp

Добавить комментарий

Простой текст

  • HTML-теги не обрабатываются и показываются как обычный текст
  • Строки и абзацы переносятся автоматически.
  • Адреса веб-страниц и email-адреса преобразовываются в ссылки автоматически.
Просмотров: 119