Optimize parsing of search results

PR #22906.
This commit is contained in:
Vladimir Golovnev 2025-06-26 08:49:58 +03:00 committed by GitHub
parent 71af105a89
commit 41d7d672ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 48 deletions

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -38,6 +38,7 @@
#include "base/global.h"
#include "base/path.h"
#include "base/utils/bytearray.h"
#include "base/utils/foreignapps.h"
#include "base/utils/fs.h"
#include "searchpluginmanager.h"
@ -139,28 +140,23 @@ void SearchHandler::processFinished(const int exitcode)
// line to SearchResult calling parseSearchResult().
void SearchHandler::readSearchOutput()
{
QByteArray output = m_searchProcess->readAllStandardOutput();
output.replace('\r', "");
const QByteArray output = m_searchResultLineTruncated + m_searchProcess->readAllStandardOutput();
QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(output, "\n", Qt::KeepEmptyParts);
QList<QByteArray> lines = output.split('\n');
if (!m_searchResultLineTruncated.isEmpty())
lines.prepend(m_searchResultLineTruncated + lines.takeFirst());
m_searchResultLineTruncated = lines.takeLast().trimmed();
m_searchResultLineTruncated = lines.takeLast().trimmed().toByteArray();
QList<SearchResult> searchResultList;
searchResultList.reserve(lines.size());
for (const QByteArray &line : asConst(lines))
for (const QByteArrayView &line : asConst(lines))
{
SearchResult searchResult;
if (parseSearchResult(QString::fromUtf8(line), searchResult))
searchResultList << searchResult;
if (SearchResult searchResult; parseSearchResult(line, searchResult))
searchResultList.append(std::move(searchResult));
}
if (!searchResultList.isEmpty())
{
for (const SearchResult &result : searchResultList)
m_results.append(result);
m_results.append(searchResultList);
emit newSearchResults(searchResultList);
}
}
@ -174,17 +170,17 @@ void SearchHandler::processFailed()
// Parse one line of search results list
// Line is in the following form:
// file url | file name | file size | nb seeds | nb leechers | Search engine url
bool SearchHandler::parseSearchResult(const QStringView line, SearchResult &searchResult)
bool SearchHandler::parseSearchResult(const QByteArrayView line, SearchResult &searchResult)
{
const QList<QStringView> parts = line.split(u'|');
const QList<QByteArrayView> parts = Utils::ByteArray::splitToViews(line, "|");
const int nbFields = parts.size();
if (nbFields <= PL_ENGINE_URL)
return false; // Anything after ENGINE_URL is optional
searchResult = SearchResult();
searchResult.fileUrl = parts.at(PL_DL_LINK).trimmed().toString(); // download URL
searchResult.fileName = parts.at(PL_NAME).trimmed().toString(); // Name
searchResult.fileUrl = QString::fromUtf8(parts.at(PL_DL_LINK).trimmed()); // download URL
searchResult.fileName = QString::fromUtf8(parts.at(PL_NAME).trimmed()); // Name
searchResult.fileSize = parts.at(PL_SIZE).trimmed().toLongLong(); // Size
bool ok = false;
@ -197,11 +193,11 @@ bool SearchHandler::parseSearchResult(const QStringView line, SearchResult &sear
if (!ok || (searchResult.nbLeechers < 0))
searchResult.nbLeechers = -1;
searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed().toString(); // Search engine site URL
searchResult.siteUrl = QString::fromUtf8(parts.at(PL_ENGINE_URL).trimmed()); // Search engine site URL
searchResult.engineName = m_manager->pluginNameBySiteURL(searchResult.siteUrl); // Search engine name
if (nbFields > PL_DESC_LINK)
searchResult.descrLink = parts.at(PL_DESC_LINK).trimmed().toString(); // Description Link
searchResult.descrLink = QString::fromUtf8(parts.at(PL_DESC_LINK).trimmed()); // Description Link
if (nbFields > PL_PUB_DATE)
{

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -81,7 +81,7 @@ private:
void readSearchOutput();
void processFailed();
void processFinished(int exitcode);
bool parseSearchResult(QStringView line, SearchResult &searchResult);
bool parseSearchResult(QByteArrayView line, SearchResult &searchResult);
const QString m_pattern;
const QString m_category;

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -34,16 +34,32 @@
#include <QByteArrayView>
#include <QList>
QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep)
QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep, const Qt::SplitBehavior behavior)
{
if (in.isEmpty())
return {};
if (sep.isEmpty())
return {in};
if (behavior == Qt::SkipEmptyParts)
{
if (in.isEmpty())
return {};
if (sep.isEmpty())
return {in};
}
else
{
if (in.isEmpty())
{
if (sep.isEmpty())
return {{}, {}};
return {{}};
}
}
const QByteArrayMatcher matcher {sep};
QList<QByteArrayView> ret;
ret.reserve(1 + (in.size() / (sep.size() + 1)));
ret.reserve((behavior == Qt::SkipEmptyParts)
? (1 + (in.size() / (sep.size() + 1)))
: (1 + (in.size() / sep.size())));
qsizetype head = 0;
while (head < in.size())
{
@ -51,14 +67,16 @@ QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, co
if (end < 0)
end = in.size();
// omit empty parts
const QByteArrayView part = in.sliced(head, (end - head));
if (!part.isEmpty())
if (!part.isEmpty() || (behavior == Qt::KeepEmptyParts))
ret += part;
head = end + sep.size();
}
if ((behavior == Qt::KeepEmptyParts) && (head == in.size()))
ret.emplaceBack();
return ret;
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -37,8 +37,8 @@ class QByteArrayView;
namespace Utils::ByteArray
{
// Inspired by QStringView(in).split(sep, Qt::SkipEmptyParts)
QList<QByteArrayView> splitToViews(QByteArrayView in, QByteArrayView sep);
// Inspired by QStringView(in).split(sep, behavior)
QList<QByteArrayView> splitToViews(QByteArrayView in, QByteArrayView sep, Qt::SplitBehavior behavior = Qt::SkipEmptyParts);
QByteArray asQByteArray(QByteArrayView view);
QByteArray toBase32(const QByteArray &in);

View file

@ -47,7 +47,7 @@ private slots:
{
using BAViews = QList<QByteArrayView>;
const auto check = [](const QByteArrayView in, const QByteArrayView sep, const BAViews expected)
const auto checkSkipEmptyParts = [](const QByteArrayView in, const QByteArrayView sep, const BAViews expected)
{
// verify it works
QCOMPARE(Utils::ByteArray::splitToViews(in, sep), expected);
@ -56,26 +56,56 @@ private slots:
using Latin1Views = QList<QLatin1StringView>;
const Latin1Views reference = QLatin1StringView(in)
.tokenize(QLatin1StringView(sep), Qt::SkipEmptyParts).toContainer();
.tokenize(QLatin1StringView(sep), Qt::SkipEmptyParts).toContainer();
Latin1Views expectedStrings;
for (const auto &string : expected)
expectedStrings.append(QLatin1StringView(string));
QCOMPARE(reference, expectedStrings);
};
check({}, {}, {});
check({}, "/", {});
check("/", "/", {});
check("/a", "/", {"a"});
check("/a/", "/", {"a"});
check("/a/b", "/", (BAViews {"a", "b"}));
check("/a/b/", "/", (BAViews {"a", "b"}));
check("/a/b", "//", {"/a/b"});
check("//a/b", "//", {"a/b"});
check("//a//b", "//", (BAViews {"a", "b"}));
check("//a//b/", "//", (BAViews {"a", "b/"}));
check("//a//b//", "//", (BAViews {"a", "b"}));
check("///a//b//", "//", (BAViews {"/a", "b"}));
checkSkipEmptyParts({}, {}, {});
checkSkipEmptyParts({}, "/", {});
checkSkipEmptyParts("/", "/", {});
checkSkipEmptyParts("/a", "/", {"a"});
checkSkipEmptyParts("/a/", "/", {"a"});
checkSkipEmptyParts("/a/b", "/", (BAViews {"a", "b"}));
checkSkipEmptyParts("/a/b/", "/", (BAViews {"a", "b"}));
checkSkipEmptyParts("/a/b", "//", {"/a/b"});
checkSkipEmptyParts("//a/b", "//", {"a/b"});
checkSkipEmptyParts("//a//b", "//", (BAViews {"a", "b"}));
checkSkipEmptyParts("//a//b/", "//", (BAViews {"a", "b/"}));
checkSkipEmptyParts("//a//b//", "//", (BAViews {"a", "b"}));
checkSkipEmptyParts("///a//b//", "//", (BAViews {"/a", "b"}));
const auto checkKeepEmptyParts = [](const QByteArrayView in, const QByteArrayView sep, const BAViews expected)
{
// verify it works
QCOMPARE(Utils::ByteArray::splitToViews(in, sep, Qt::KeepEmptyParts), expected);
// verify it has the same behavior as `split(Qt::KeepEmptyParts)`
using Latin1Views = QList<QLatin1StringView>;
const Latin1Views reference = QLatin1StringView(in)
.tokenize(QLatin1StringView(sep), Qt::KeepEmptyParts).toContainer();
Latin1Views expectedStrings;
for (const auto &string : expected)
expectedStrings.append(QLatin1StringView(string));
QCOMPARE(reference, expectedStrings);
};
checkKeepEmptyParts({}, {}, {{}, {}});
checkKeepEmptyParts({}, "/", {{}});
checkKeepEmptyParts("/", "/", {"", ""});
checkKeepEmptyParts("/a", "/", {"", "a"});
checkKeepEmptyParts("/a/", "/", {"", "a", ""});
checkKeepEmptyParts("/a/b", "/", (BAViews {"", "a", "b"}));
checkKeepEmptyParts("/a/b/", "/", (BAViews {"", "a", "b", ""}));
checkKeepEmptyParts("/a/b", "//", {"/a/b"});
checkKeepEmptyParts("//a/b", "//", {"", "a/b"});
checkKeepEmptyParts("//a//b", "//", (BAViews {"", "a", "b"}));
checkKeepEmptyParts("//a//b/", "//", (BAViews {"", "a", "b/"}));
checkKeepEmptyParts("//a//b//", "//", (BAViews {"", "a", "b", ""}));
checkKeepEmptyParts("///a//b//", "//", (BAViews {"", "/a", "b", ""}));
}
void testAsQByteArray() const