From 15a9e788ebdbe4dc9b483bb4b9f49a10f56f4edd Mon Sep 17 00:00:00 2001 From: slava Date: Tue, 6 May 2025 10:28:46 +0700 Subject: [PATCH] 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. --- android/rshb/ScreenXmlParser.cpp | 522 +++++++++++++++++++++++++++---- android/rshb/ScreenXmlParser.h | 7 +- 2 files changed, 467 insertions(+), 62 deletions(-) diff --git a/android/rshb/ScreenXmlParser.cpp b/android/rshb/ScreenXmlParser.cpp index d4bed02..955c572 100644 --- a/android/rshb/ScreenXmlParser.cpp +++ b/android/rshb/ScreenXmlParser.cpp @@ -7,8 +7,12 @@ #include #include #include +#include +#include +#include "adb/AdbUtils.h" #include "android/xml/Node.h" +#include "db/TransactionInfo.h" ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { } @@ -16,6 +20,16 @@ ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { ScreenXmlParser::~ScreenXmlParser() = default; 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) { // 1. Ищем push banner - он перекрывает весь экран @@ -179,7 +193,9 @@ QList ScreenXmlParser::getLast2DaysTransactions() { selected="false" bounds="[952,159][1039,248]"/> */ void ScreenXmlParser::parseAndSaveXml(const QString &xml) { - m_xml = xml; + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); m_nodes.clear(); 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) { if (node.isElement()) { QDomElement elem = node.toElement(); @@ -231,68 +336,85 @@ void traverseNodes(const QDomNode &node) { if (elem.tagName() == "node" && elem.attribute("class") == "android.widget.Button" && elem.attribute("text") == "Детали операции") { - // Ищем сиблингов от текущего узла и далее - QDomNodeList siblings = 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(); + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); - qDebug() << "Node:" - << sibElem.attribute("index") - << sibElem.attribute("text"); - // << sibElem.attribute("class"); + for (int j = 0; j < detailInfo.count(); ++j) { + QDomNode info = detailInfo.at(j); + if (!info.isElement()) continue; + 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(); - for (int k = 0; k < sibChildren.count(); ++k) { - QDomNode sibChild = sibChildren.at(k); - if (!sibChild.isElement()) continue; - QDomElement sibChildElem = sibChild.toElement(); - qDebug() << "... Node:" - << sibChildElem.attribute("index") - << sibChildElem.attribute("text"); - // << sibChildElem.attribute("class"); + /** + Node: "2" "" + ... Node: "0" "Дата операции" + ... Node: "1" "30.04.2025 17:46:29" + Node: "5" "" + ... Node: "0" "Название" + ... Node: "1" "DBO TRANSFER RSHB INTERNET-BANK" + Node: "7" "" + ... Node: "0" "Комиссия" + ... Node: "1" "50 ₽" + */ - - if (sibChildElem.hasChildNodes()) { - QDomNodeList sibChildren2 = sibChildElem.childNodes(); - for (int k = 0; k < sibChildren2.count(); ++k) { - QDomNode sibChild2 = sibChildren2.at(k); - if (!sibChild2.isElement()) continue; - QDomElement sibChildElem2 = sibChild2.toElement(); - - qDebug() << "...... Node:" - << sibChildElem2.attribute("index") - << sibChildElem2.attribute("text"); - // << 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"); - } - } - } - } - } - } + // if (sibElem.hasChildNodes()) { + // QDomNodeList sibChildren = sibElem.childNodes(); + // for (int k = 0; k < sibChildren.count(); ++k) { + // QDomNode sibChild = sibChildren.at(k); + // if (!sibChild.isElement()) continue; + // QDomElement sibChildElem = sibChild.toElement(); + // qDebug() << "... Node:" + // << sibChildElem.attribute("index") + // << sibChildElem.attribute("text"); + // // << sibChildElem.attribute("class"); + // + // + // if (sibChildElem.hasChildNodes()) { + // QDomNodeList sibChildren2 = sibChildElem.childNodes(); + // for (int k = 0; k < sibChildren2.count(); ++k) { + // QDomNode sibChild2 = sibChildren2.at(k); + // if (!sibChild2.isElement()) continue; + // QDomElement sibChildElem2 = sibChild2.toElement(); + // + // qDebug() << "...... Node:" + // << sibChildElem2.attribute("index") + // << sibChildElem2.attribute("text"); + // // << 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 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 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() { - 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)) { qDebug() << "Cannot open file"; } @@ -321,5 +721,5 @@ void ScreenXmlParser::test() { qDebug() << "Start parsing XML:"; QDomElement root = doc.documentElement(); - traverseNodes(root); + // ЕСПП } diff --git a/android/rshb/ScreenXmlParser.h b/android/rshb/ScreenXmlParser.h index 4cda0f0..624b389 100644 --- a/android/rshb/ScreenXmlParser.h +++ b/android/rshb/ScreenXmlParser.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include "android/xml/Node.h" +#include "db/TransactionInfo.h" class ScreenXmlParser final : public QObject { Q_OBJECT @@ -31,11 +33,14 @@ public: void parseAndSaveXml(const QString &xml); + TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight); + QList parseTodayAndYesterdayHistory(); + void test(); private: QList m_nodes; - QString m_xml; + QDomElement m_xml; QList findPinCodeNodes(const QString &pinCode); };