1
0
forked from BRT/arc

Refactor ScreenXmlParser to handle transaction parsing

Enhanced transaction parsing by integrating QDomElement for XML handling and adding functions to parse transaction details, history, and metadata. Replaced raw QString operations with structured data extraction and improved formatting for date, amount, and bank information parsing.
This commit is contained in:
slava 2025-05-06 10:28:46 +07:00
parent a269de840b
commit 15a9e788eb
2 changed files with 467 additions and 62 deletions

View File

@ -7,8 +7,12 @@
#include <QXmlStreamReader> #include <QXmlStreamReader>
#include <QDomDocument> #include <QDomDocument>
#include <QFile> #include <QFile>
#include <QDateTime>
#include <QTimeZone>
#include "adb/AdbUtils.h"
#include "android/xml/Node.h" #include "android/xml/Node.h"
#include "db/TransactionInfo.h"
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
} }
@ -16,6 +20,16 @@ ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
ScreenXmlParser::~ScreenXmlParser() = default; ScreenXmlParser::~ScreenXmlParser() = default;
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
static const QRegularExpression onlyDigitsRegex(R"([^0-9])");
static const QRegularExpression rePhone(R"raw((\+7\(?\d{3}\)?[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}))raw");
static const QRegularExpression reFromBank(R"raw(из (.+?) на)raw");
static const QRegularExpression reToBank(R"raw(в (.+?) через)raw");
static const QRegularExpression reSbpId(R"(СБП(?: С2С)? ([A-Z0-9]{30,}))");
static const QRegularExpression reIdAlt(R"(ID перевода (\d+))");
static const QRegularExpression reDate(R"((?:Дата операции|дата) (\d{2}\.\d{2}\.\d{4}(?: \d{2}:\d{2}:\d{2})?))");
static const QRegularExpression reSender(R"(от ([^\(]+))");
static const QRegularExpression reNameAlt(R"(Перевод ([А-Яа-яЁё\s\*]+) в)");
Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) {
// 1. Ищем push banner - он перекрывает весь экран // 1. Ищем push banner - он перекрывает весь экран
@ -179,7 +193,9 @@ QList<Node> ScreenXmlParser::getLast2DaysTransactions() {
selected="false" bounds="[952,159][1039,248]"/> selected="false" bounds="[952,159][1039,248]"/>
*/ */
void ScreenXmlParser::parseAndSaveXml(const QString &xml) { void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
m_xml = xml; QDomDocument doc;
doc.setContent(xml);
m_xml = doc.documentElement();
m_nodes.clear(); m_nodes.clear();
QXmlStreamReader reader(xml); QXmlStreamReader reader(xml);
@ -221,6 +237,95 @@ void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
} }
} }
QString getDayOfWeekAsString(const int day_of_week) {
switch (day_of_week) {
case 1: return "понедельник";
case 2: return "вторник";
case 3: return "среда";
case 4: return "четверг";
case 5: return "пятница";
case 6: return "суббота";
case 7: return "воскресенье";
default: return "";
}
}
QString getMonthAsString(const int month) {
switch (month) {
case 1: return "января";
case 2: return "февраля";
case 3: return "марта";
case 4: return "апреля";
case 5: return "мая";
case 6: return "июня";
case 7: return "июля";
case 8: return "августа";
case 9: return "сентября";
case 10: return "октября";
case 11: return "ноября";
case 12: return "декабря";
default: return "";
}
}
QString findSubInfo(const QDomElement &elem) {
if (elem.hasChildNodes()) {
QDomNodeList subInfoList = elem.childNodes();
for (QDomNode subInfoNode: subInfoList) {
if (!subInfoNode.isElement()) continue;
QDomElement subInfo = subInfoNode.toElement();
if (subInfo.attribute("index") == "1") {
return subInfo.attribute("text");
}
}
}
return "";
}
bool hasTimeComponent(const QString &datetimeStr) {
QDateTime dt = QDateTime::fromString(datetimeStr, "dd.MM.yyyy HH:mm:ss");
if (!dt.isValid()) {
return false; // Строка не соответствует формату даты и времени
}
return dt.time() != QTime(0, 0, 0); // Проверка, что время не равно 00:00:00
}
void parseTransferInfo(const QString &text, TransactionInfo &info) {
if (const auto m = reSender.match(text); m.hasMatch()) {
info.name = m.captured(1).trimmed();
} else if (const auto m1 = reNameAlt.match(text); m1.hasMatch()) {
info.name = m1.captured(1).trimmed();
}
if (const auto m = rePhone.match(text); m.hasMatch()) {
info.phone = m.captured(1).trimmed().remove(onlyDigitsRegex);
}
if (const auto m = reFromBank.match(text); m.hasMatch()) {
if (const QString bank = m.captured(1).trimmed(); !bank.isEmpty()) {
info.bankName = bank;
}
} else if (const auto m1 = reToBank.match(text); m1.hasMatch()) {
if (const QString bank = m1.captured(1).trimmed(); !bank.isEmpty()) {
info.bankName = bank;
}
}
if (const auto m = reSbpId.match(text); m.hasMatch()) {
info.trExternalId = m.captured(1);
} else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) {
info.trExternalId = m1.captured(1);
}
if (const auto m = reDate.match(text); m.hasMatch()) {
const QString date = m.captured(1);
// если не пустая и дата у нас с 00:00
if (hasTimeComponent(date)) {
info.bankTime = date;
}
}
}
void traverseNodes(const QDomNode &node) { void traverseNodes(const QDomNode &node) {
if (node.isElement()) { if (node.isElement()) {
QDomElement elem = node.toElement(); QDomElement elem = node.toElement();
@ -231,68 +336,85 @@ void traverseNodes(const QDomNode &node) {
if (elem.tagName() == "node" && if (elem.tagName() == "node" &&
elem.attribute("class") == "android.widget.Button" && elem.attribute("class") == "android.widget.Button" &&
elem.attribute("text") == "Детали операции") { elem.attribute("text") == "Детали операции") {
// Ищем сиблингов от текущего узла и далее // Все что ниже деталей операции
QDomNodeList siblings = elem.parentNode().childNodes(); QDomNodeList detailInfo = elem.parentNode().childNodes();
// Получение строки для анализа
// QString xmlString;
// QTextStream stream(&xmlString);
// elem.parentNode().save(stream, 0);
// qDebug() << xmlString;
// return;
for (int j = 0; j < siblings.count(); ++j) {
QDomNode sib = siblings.at(j);
if (!sib.isElement()) continue;
QDomElement sibElem = sib.toElement();
qDebug() << "Node:" for (int j = 0; j < detailInfo.count(); ++j) {
<< sibElem.attribute("index") QDomNode info = detailInfo.at(j);
<< sibElem.attribute("text"); if (!info.isElement()) continue;
// << sibElem.attribute("class"); QDomElement infoElement = info.toElement();
QString index = infoElement.attribute("index");
switch (index.toInt()) {
case 2:
qDebug() << "Дата операции:" << findSubInfo(infoElement);
continue;
case 5:
qDebug() << "Название:" << findSubInfo(infoElement);
qDebug() << "-------------------------------------";
// parseTransfer(findSubInfo(infoElement));
continue;
case 7:
qDebug() << "Комиссия:" << findSubInfo(infoElement);;
continue;
default:
qDebug() << "Прочее:" << findSubInfo(infoElement);;
continue;
}
if (sibElem.hasChildNodes()) { /**
QDomNodeList sibChildren = sibElem.childNodes(); Node: "2" ""
for (int k = 0; k < sibChildren.count(); ++k) { ... Node: "0" "Дата операции"
QDomNode sibChild = sibChildren.at(k); ... Node: "1" "30.04.2025 17:46:29"
if (!sibChild.isElement()) continue; Node: "5" ""
QDomElement sibChildElem = sibChild.toElement(); ... Node: "0" "Название"
qDebug() << "... Node:" ... Node: "1" "DBO TRANSFER RSHB INTERNET-BANK"
<< sibChildElem.attribute("index") Node: "7" ""
<< sibChildElem.attribute("text"); ... Node: "0" "Комиссия"
// << sibChildElem.attribute("class"); ... Node: "1" "50 ₽"
*/
// if (sibElem.hasChildNodes()) {
if (sibChildElem.hasChildNodes()) { // QDomNodeList sibChildren = sibElem.childNodes();
QDomNodeList sibChildren2 = sibChildElem.childNodes(); // for (int k = 0; k < sibChildren.count(); ++k) {
for (int k = 0; k < sibChildren2.count(); ++k) { // QDomNode sibChild = sibChildren.at(k);
QDomNode sibChild2 = sibChildren2.at(k); // if (!sibChild.isElement()) continue;
if (!sibChild2.isElement()) continue; // QDomElement sibChildElem = sibChild.toElement();
QDomElement sibChildElem2 = sibChild2.toElement(); // qDebug() << "... Node:"
// << sibChildElem.attribute("index")
qDebug() << "...... Node:" // << sibChildElem.attribute("text");
<< sibChildElem2.attribute("index") // // << sibChildElem.attribute("class");
<< sibChildElem2.attribute("text"); //
// << sibChildElem2.attribute("class"); //
// if (sibChildElem.hasChildNodes()) {
if (sibChildElem2.hasChildNodes()) { // QDomNodeList sibChildren2 = sibChildElem.childNodes();
QDomNodeList sibChildren3 = sibChildElem2.childNodes(); // for (int k = 0; k < sibChildren2.count(); ++k) {
for (int k = 0; k < sibChildren3.count(); ++k) { // QDomNode sibChild2 = sibChildren2.at(k);
QDomNode sibChild3 = sibChildren3.at(k); // if (!sibChild2.isElement()) continue;
if (!sibChild3.isElement()) continue; // QDomElement sibChildElem2 = sibChild2.toElement();
QDomElement sibChildElem3 = sibChild3.toElement(); //
// qDebug() << "...... Node:"
qDebug() << "......... Node:" // << sibChildElem2.attribute("index")
<< sibChildElem3.attribute("index") // << sibChildElem2.attribute("text");
<< sibChildElem3.attribute("text"); // // << sibChildElem2.attribute("class");
// << sibChildElem2.attribute("class"); //
} // if (sibChildElem2.hasChildNodes()) {
} // QDomNodeList sibChildren3 = sibChildElem2.childNodes();
} // for (int k = 0; k < sibChildren3.count(); ++k) {
} // QDomNode sibChild3 = sibChildren3.at(k);
} // if (!sibChild3.isElement()) continue;
} // QDomElement sibChildElem3 = sibChild3.toElement();
//
// qDebug() << "......... Node:"
// << sibChildElem3.attribute("index")
// << sibChildElem3.attribute("text");
// // << sibChildElem2.attribute("class");
// }
// }
// }
// }
// }
// }
} }
} }
} }
@ -306,8 +428,286 @@ void traverseNodes(const QDomNode &node) {
} }
} }
QDomNode findNode(const QDomNode &node, const QString &text) {
if (node.isElement()) {
QString nodeText = node.toElement().attribute("text");
if (nodeText.toLower().startsWith(text)) {
return node;
}
}
QDomNode child = node.firstChild();
while (!child.isNull()) {
QDomNode result = findNode(child, text);
if (!result.isNull()) {
return result;
}
child = child.nextSibling();
}
return {};
}
float extractAmounts(const QString &text) {
// Разрешаем пробелы внутри чисел, захватываем "рубль", "рубля", "рублей" и т.д.
// QRegularExpression re(R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)");
QRegularExpression re(
R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)",
QRegularExpression::UseUnicodePropertiesOption
);
QRegularExpressionMatch match = re.match(text);
if (match.hasMatch()) {
QString signStr = match.captured(1); // "Плюс" или "Минус"
QString rublesStr = match.captured(2).remove(' '); // "1 000" → "1000"
int rubles = rublesStr.toInt();
int kopecks = match.captured(3).toInt();
int totalKopecks = rubles * 100 + kopecks;
if (signStr == "Минус") {
totalKopecks *= -1;
}
return totalKopecks / 100.0f;
}
return 0;
}
QString beforeAmount(const QString &text) {
int index = text.indexOf(QRegularExpression("Плюс|Минус"));
if (index != -1) {
return text.left(index).trimmed();
}
return text; // если не найдено — вернуть всю строку
}
QList<TransactionInfo> ScreenXmlParser::parseTodayAndYesterdayHistory() {
QTimeZone tz("Europe/Moscow");
QDate today = QDateTime::currentDateTime(tz).date();
QDate yesterday = today.addDays(-1);
QDate beforeYesterday = today.addDays(-2);
QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek()));
QString yesterdayTitle = QString("вчера, %1").arg(getDayOfWeekAsString(yesterday.dayOfWeek()));
QString beforeYesterdayTitle = QString("%1 %2, %3").arg(
QString::number(beforeYesterday.day()),
getMonthAsString(beforeYesterday.month()),
getDayOfWeekAsString(beforeYesterday.dayOfWeek())
);
QDomNode todayNode = findNode(m_xml, todayTitle);
QDomNode yesterdayNode = findNode(m_xml, yesterdayTitle);
QDomNode parentNode;
if (!yesterdayNode.isNull()) {
parentNode = yesterdayNode.parentNode();
} else if (!todayNode.isNull()) {
parentNode = todayNode.parentNode();
}
QList<TransactionInfo> transactions;
if (!parentNode.isNull()) {
for (QDomNode transactionNode: parentNode.childNodes()) {
QString transactionShortText = transactionNode.toElement().attribute("text");
const QString titleLowerCase = transactionShortText.toLower();
if (titleLowerCase.startsWith(beforeYesterdayTitle)) {
// только сегодня и вчера парсим, позавчера не трогаем
break;
}
QDate date = {};
if (titleLowerCase.startsWith(todayTitle)) {
date = today;
transactionShortText = transactionShortText.mid(todayTitle.length() + 1);
}
if (titleLowerCase.startsWith(yesterdayTitle)) {
date = yesterday;
transactionShortText = transactionShortText.mid(yesterdayTitle.length() + 1);
}
if (transactionShortText.contains("ЕСПП")) {
continue;
}
float amount = extractAmounts(transactionShortText);
QString shortDesc = beforeAmount(transactionShortText);
TransactionInfo info;
info.amount = amount;
info.bankTime = date.toString("dd.MM.yyyy");
info.shortDescription = shortDesc;
transactions.append(info);
}
}
return transactions;
}
void parse2daysHistory(const QDomNode &node) {
QTimeZone tz("Europe/Moscow");
QDate today = QDateTime::currentDateTime(tz).date();
QDate tomorrow = today.addDays(-1);
QDate after2Days = today.addDays(-2);
QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek()));
QDomNode todayNode = findNode(node, todayTitle);
if (todayNode.isNull()) {
} else {
qDebug() << "todayNode: " << todayNode.toElement().attribute("text");
}
QString tomorrowTitle = QString("вчера, %1").arg(getDayOfWeekAsString(tomorrow.dayOfWeek()));
QDomNode tomorrowNode = findNode(node, tomorrowTitle);
if (tomorrowNode.isNull()) {
} else {
qDebug() << "tomorrowNode: " << tomorrowNode.toElement().attribute("text");
}
// 30 апреля, Среда
QString after2DaysTitle = QString("%1 %2, %3").arg(
QString::number(after2Days.day()),
getMonthAsString(after2Days.month()),
getDayOfWeekAsString(after2Days.dayOfWeek()));
QDomNode after2DaysNode = findNode(node, after2DaysTitle);
if (after2DaysNode.isNull()) {
qDebug() << "No after2DaysNode";
} else {
qDebug() << "after2DaysNode: " << after2DaysNode.toElement().attribute("text");
}
QDomNode parentNode;
if (!todayNode.isNull() || !tomorrowNode.isNull() || !after2DaysNode.isNull()) {
if (!todayNode.isNull()) {
parentNode = todayNode.parentNode();
}
if (!tomorrowNode.isNull()) {
parentNode = tomorrowNode.parentNode();
}
if (!after2DaysNode.isNull()) {
parentNode = after2DaysNode.parentNode();
}
}
if (!parentNode.isNull()) {
for (QDomNode child_node: parentNode.childNodes()) {
QString child_text = child_node.toElement().attribute("text");
if (child_text.toLower().startsWith(todayTitle)) {
qDebug() << "=== Today: " << todayTitle;
qDebug() << "node: " << beforeAmount(child_text.mid(todayTitle.length() + 1));
qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2);
// qDebug() << "";
continue;
}
if (child_text.toLower().startsWith(tomorrowTitle)) {
qDebug() << "=== Yesterday: " << todayTitle;
qDebug() << "node: " << beforeAmount(child_text.mid(tomorrowTitle.length() + 1));
qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2);
// qDebug() << "";
continue;
}
if (child_text.toLower().startsWith(after2DaysTitle)) {
qDebug() << "=== Before yesterday: " << todayTitle;
qDebug() << "node: " << beforeAmount(child_text.mid(after2DaysTitle.length() + 1));
qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2);
// qDebug() << "";
continue;
}
qDebug() << "node: " << beforeAmount(child_node.toElement().attribute("text"));
qDebug() << "--> sum: " <<
QString::number(extractAmounts(child_node.toElement().attribute("text")), 'f', 2);
// qDebug() << "";
}
}
// QString d = today.day() > 9 ? QString::number(today.day()) : "0" + QString::number(today.day());
// QString m = today.month() > 9 ? QString::number(today.month()) : "0" + QString::number(today.month());
// QString y = QString::number(today.year());
// QString w = getDayOfWeekAsString(today.dayOfWeek());
// QString mm = getMonthAsString(today.month());
// qDebug() << "Date:" << d << m << y << w << mm;
// QString d2 = after2Days.day() > 9 ? QString::number(after2Days.day()) : "0" + QString::number(after2Days.day());
// QString m2 = after2Days.month() > 9
// ? QString::number(after2Days.month())
// : "0" + QString::number(after2Days.month());
// QString y2 = QString::number(after2Days.year());
// QString w2 = getDayOfWeekAsString(after2Days.dayOfWeek());
// QString mm2 = getMonthAsString(after2Days.month());
// qDebug() << "Date:" << d2 << m2 << y2 << w2 << mm2;
}
TransactionInfo parseExpandedInfoHistory(const QDomElement &root) {
if (root.isElement()) {
QDomElement elem = root.toElement();
if (elem.tagName() == "node"
&& elem.attribute("class") == "android.widget.Button"
&& elem.attribute("text") == "Детали операции"
) {
TransactionInfo transaction;
// Все что ниже деталей операции
QDomNodeList detailInfo = elem.parentNode().childNodes();
for (int i = 0; i < detailInfo.count(); ++i) {
QDomNode info = detailInfo.at(i);
if (!info.isElement()) continue;
switch (QDomElement e = info.toElement(); e.attribute("index").toInt()) {
case 2:
// Дата операции
transaction.bankTime = findSubInfo(e);
break;
case 5:
transaction.description = findSubInfo(e);
parseTransferInfo(transaction.description, transaction);
break;
case 7:
// Комиссия
transaction.fee = findSubInfo(e).remove(onlyDigitsRegex).toFloat();
break;
default:
// qDebug() << "Прочее:" << findSubInfo(e);
break;
}
}
return transaction;
}
}
// Рекурсивно обходим всех детей
QDomNode child = root.firstChild();
while (!child.isNull()) {
if (TransactionInfo info = parseExpandedInfoHistory(child.toElement()); !info.bankTime.isEmpty()) {
return info;
}
child = child.nextSibling();
}
return {};
}
TransactionInfo ScreenXmlParser::parseTransactionInfoHistory(int screenWidth, int screenHeight) {
// 1. Распарсить описание транзакции
TransactionInfo transaction = parseExpandedInfoHistory(m_xml);
// 1. Найти статус: Исполнен | Операция исполнена
transaction.status = TransactionStatus::Wait;
Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2);
Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2);
if (!status1.isEmpty() || !status2.isEmpty()) {
transaction.status = TransactionStatus::Complete;
}
return transaction;
}
void ScreenXmlParser::test() { void ScreenXmlParser::test() {
QFile file("assets/rshb/pay/pay_by_phone_history_expanded.xml"); return;
QFile file("assets/rshb/test/spb_test_send.xml");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Cannot open file"; qDebug() << "Cannot open file";
} }
@ -321,5 +721,5 @@ void ScreenXmlParser::test() {
qDebug() << "Start parsing XML:"; qDebug() << "Start parsing XML:";
QDomElement root = doc.documentElement(); QDomElement root = doc.documentElement();
traverseNodes(root); // ЕСПП
} }

View File

@ -1,7 +1,9 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QDomElement>
#include "android/xml/Node.h" #include "android/xml/Node.h"
#include "db/TransactionInfo.h"
class ScreenXmlParser final : public QObject { class ScreenXmlParser final : public QObject {
Q_OBJECT Q_OBJECT
@ -31,11 +33,14 @@ public:
void parseAndSaveXml(const QString &xml); void parseAndSaveXml(const QString &xml);
TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight);
QList<TransactionInfo> parseTodayAndYesterdayHistory();
void test(); void test();
private: private:
QList<Node> m_nodes; QList<Node> m_nodes;
QString m_xml; QDomElement m_xml;
QList<Node> findPinCodeNodes(const QString &pinCode); QList<Node> findPinCodeNodes(const QString &pinCode);
}; };