#include "ScreenXmlParser.h" #include #include #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "android/xml/Node.h" #include "db/AccountInfo.h" #include "db/TransactionInfo.h" #include "time/DateUtils.h" 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 onlyDigitsAndDotRegex(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\*]+) в)"); static const QRegularExpression reCardData(R"(цифрами\s+(\d\s\d\s\d\s\d).*?Сумма:\s([\d\s]+)\sруб)"); Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { // 1. Ищем push banner - он перекрывает весь экран Node first = m_nodes[0]; for (const Node &node: m_nodes) { // Если это однозначно, что пуш-баннер if (node.resourceId == "aft-popup-pushNotification-iconClose" && node.className == "android.widget.Button") { return node; } // Если есть кнопка "Позже" if (node.text.trimmed() == "Позже" && node.className == "android.widget.Button") { return node; } // Если нашли пустую кнпоку if (node.className == "android.widget.Button" && node.clickable && node.text.isEmpty()) { // if (node.x() > width / 2 && node.y() < width / 2) { return node; } } } return {}; } /** * Смотрим что есть текс "Мы заботимся о вашей безопасности" и * кнопка "Отложить" * @return координаты кнопки для закрытия */ Node ScreenXmlParser::tryToFindSecurityBanner() { Node text; Node closeBtn; for (const Node &node: m_nodes) { if (node.text.trimmed() == "Мы заботимся о вашей безопасности" && node.className == "android.app.Dialog") { text = node; } if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { closeBtn = node; } } // Не нужно проверять все, если хотя бы одна координата есть if (!text.isEmpty() && !closeBtn.isEmpty()) { return closeBtn; } return {}; } Node ScreenXmlParser::findTextNode(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { return node; } } return {}; } Node ScreenXmlParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text && node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) { return node; } } return {}; } Node ScreenXmlParser::findFirstEditText() { for (const Node &node: m_nodes) { if (node.className == "android.widget.EditText" && node.focusable) { return node; } } return {}; } Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { for (const Node &node: m_nodes) { if (contains) { if (node.className == "android.widget.Button" && node.text.contains(text)) { return node; } } else { if (node.className == "android.widget.Button" && node.text.trimmed() == text) { return node; } } } return {}; } bool ScreenXmlParser::isHomeScreen() { // Есть заголовок "Счета и карты" и кнопка "Все продукты" Node text; Node allProductBtn; for (const Node &node: m_nodes) { if (node.text.trimmed() == "Счета и карты" && node.className == "android.widget.TextView") { text = node; } if (node.text.trimmed() == "Все продукты" && node.className == "android.widget.Button") { allProductBtn = node; } } return !text.isEmpty() && !allProductBtn.isEmpty(); } QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { QMap buttons; const QSet numberSet = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; for (const Node &node: m_nodes) { const QString &text = node.text.trimmed(); if (node.className == "android.widget.Button" && numberSet.contains(text)) { buttons[text] = node; } } QList nodes; for (int i = 0; i < pinCode.length(); ++i) { nodes.append(buttons[QString(pinCode.at(i))]); } return nodes; } QList ScreenXmlParser::tryToFindPinCode(const QString &pinCode, const QString &title) { for (const Node &node: m_nodes) { // если там есть такая кнопка значит банер есть, и пин-код не прожать if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { return {}; } } for (const Node &node: m_nodes) { // FIXME определение пин-кода лучше сделать if (node.text.trimmed() == title) { return findPinCodeNodes(pinCode); } } return {}; } QList ScreenXmlParser::getLast2DaysTransactions() { QList nodes; for (const Node &node: m_nodes) { if (node.className == "android.widget.Button" && node.text.contains("рубл") && (node.text.contains("Минус") || node.text.contains("Плюс"))) { nodes.append(node); } } return nodes; } /** * */ void ScreenXmlParser::parseAndSaveXml(const QString &xml) { QDomDocument doc; doc.setContent(xml); m_xml = doc.documentElement(); m_nodes.clear(); QXmlStreamReader reader(xml); while (!reader.atEnd()) { reader.readNext(); if (reader.isStartElement() && reader.name() == "node") { Node node = Node(); if (reader.attributes().hasAttribute("bounds")) { QString bounds = reader.attributes().value("bounds").toString(); if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { node.setBounds( match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt(), match.captured(4).toInt() ); } } if (reader.attributes().hasAttribute("text")) { node.text = reader.attributes().value("text").toString(); } if (reader.attributes().hasAttribute("class")) { node.className = reader.attributes().value("class").toString(); } if (reader.attributes().hasAttribute("clickable")) { node.clickable = reader.attributes().value("clickable").toString() == "true"; } if (reader.attributes().hasAttribute("focusable")) { node.focusable = reader.attributes().value("focusable").toString() == "true"; } if (reader.attributes().hasAttribute("resource-id")) { node.resourceId = reader.attributes().value("resource-id").toString(); } m_nodes.append(node); } } } 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 ""; } } QDomElement findSubElement(const QDomElement &elem, const QString &index) { if (elem.hasChildNodes()) { QDomNodeList subInfoList = elem.childNodes(); for (QDomNode subInfoNode: subInfoList) { if (!subInfoNode.isElement()) continue; QDomElement subInfo = subInfoNode.toElement(); if (subInfo.attribute("index") == index) { return subInfo; } } } return {}; } QString findSubInfo(const QDomElement &elem, const QString &index) { QDomElement subInfo = findSubElement(elem, index); if (!subInfo.isNull()) { 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 } QString transformBankDateTime(const QString &date) { QDateTime dt; if (date.contains(",")) { // 10.05.2025, 15:03 dt = QDateTime::fromString(date, "dd.MM.yyyy, HH:mm"); } else { dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm:ss"); } if (!dt.isValid()) { dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm"); } if (!dt.isValid()) { return date; // не распарсили } return dt.toString("dd.MM.yyyy HH:mm:ss"); } 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.bankTrExternalId = m.captured(1); } else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) { info.bankTrExternalId = 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 = transformBankDateTime(date); // } } } void traverseNodes(const QDomNode &node) { if (node.isElement()) { QDomElement elem = node.toElement(); if (node.isElement()) { QDomElement elem = node.toElement(); if (elem.tagName() == "node" && elem.attribute("class") == "android.widget.Button" && elem.attribute("text") == "Детали операции") { // Все что ниже деталей операции QDomNodeList detailInfo = elem.parentNode().childNodes(); 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, "1"); break; case 5: qDebug() << "Название:" << findSubInfo(infoElement, "1"); qDebug() << "-------------------------------------"; // parseTransfer(findSubInfo(infoElement)); break; case 7: qDebug() << "Комиссия:" << findSubInfo(infoElement, "1"); break; default: qDebug() << "Прочее:" << findSubInfo(infoElement, "1"); break; } /** 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 (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"); // } // } // } // } // } // } } } } } // Рекурсивно обходим всех детей QDomNode child = node.firstChild(); while (!child.isNull()) { traverseNodes(child); child = child.nextSibling(); } } 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; // если не найдено — вернуть всю строку } double parseAmount(const QString &amountAsString) { QString numberString = amountAsString; numberString.remove(QRegularExpression(R"([^\dточка])")); numberString.replace("точка", "."); double amount = numberString.toDouble(); if (amountAsString.startsWith("-")) { amount *= -1; } return amount; } QDomElement findElementWithListView(const QDomElement &parent) { QDomElement element = parent.firstChildElement(); while (!element.isNull()) { QDomElement child = element.firstChildElement(); while (!child.isNull()) { if (child.hasAttribute("class") && child.attribute("class") == "android.widget.ListView") { return child; } child = child.nextSiblingElement(); } QDomElement found = findElementWithListView(element); if (!found.isNull()) { return found; } element = element.nextSiblingElement(); } return {}; } QList ScreenXmlParser::parseTodayAndYesterdayReportHistory() { QList transactions; QDomElement child = findElementWithListView(m_xml); QDomNodeList listView = findElementWithListView(m_xml).childNodes(); qDebug() << listView.count(); if (!listView.isEmpty()) { QString date; for (QDomNode node: listView) { if (node.hasChildNodes()) { QString text; if (node.childNodes().count() > 1) { date = node.firstChildElement().attribute("text"); text = node.lastChildElement().attribute("text"); } else if (node.childNodes().count() == 1) { text = node.firstChildElement().attribute("text"); } TransactionInfo transaction; if (text.startsWith("Перевод")) { QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); QRegularExpressionMatch match = re.match(text); if (match.hasMatch()) { QString amount = match.captured(0); transaction.amount = parseAmount(amount.trimmed()); text.chop(amount.length()); } transaction.description = text.trimmed(); parseTransferInfo(text, transaction); if (transaction.bankTime.isEmpty()) { transaction.bankTime = transformBankDateTime(date); } } else { // карта if (text.contains("без учета комиссии")) { QRegularExpression re(R"( без учета комиссии -\d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); QRegularExpressionMatch match = re.match(text); if (match.hasMatch()) { QString fee = match.captured(0); text.chop(fee.length()); transaction.fee = parseAmount(fee.replace("без учета комиссии", "").trimmed()); } } QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); QRegularExpressionMatch match = re.match(text); if (match.hasMatch()) { QString amount = match.captured(0); transaction.amount = parseAmount(amount.trimmed()); text.chop(amount.length()); } transaction.description = text.trimmed(); if (transaction.bankTime.isEmpty()) { transaction.bankTime = transformBankDateTime(date); } } transactions.append(transaction); } } } else { qDebug() << "ListView is empty"; } return transactions; } QList ScreenXmlParser::parseTodayAndYesterdayHistory() { QDate today = DateUtils::getMoscowNow().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()) { QDate date = {}; for (QDomNode transactionNode: parentNode.childNodes()) { QString transactionShortText = transactionNode.toElement().attribute("text"); const QString titleLowerCase = transactionShortText.toLower(); if (titleLowerCase.startsWith(beforeYesterdayTitle)) { // только сегодня и вчера парсим, позавчера не трогаем break; } 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) { QDate today = DateUtils::getMoscowNow().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 = transformBankDateTime(findSubInfo(e, "1")); break; case 5: transaction.description = findSubInfo(e, "1"); parseTransferInfo(transaction.description, transaction); break; case 7: // Комиссия transaction.fee = findSubInfo(e, "1").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; } TransactionInfo parseExpandedInfo(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; QDomElement infoElement = info.toElement(); QString index = infoElement.attribute("index"); QDomElement ee; switch (index.toInt()) { case 3: transaction.bankTime = transformBankDateTime(findSubInfo(infoElement, "1")); break; case 4: ee = findSubElement(infoElement, "0"); ee = findSubElement(ee, "1"); transaction.phone = findSubInfo(ee, "0").remove(onlyDigitsRegex); break; case 5: transaction.name = findSubInfo(infoElement, "1"); break; case 6: transaction.bankName = findSubInfo(infoElement, "1"); break; case 9: transaction.fee = findSubInfo(infoElement, "1").remove(onlyDigitsAndDotRegex).toFloat(); break; case 10: ee = findSubElement(infoElement, "1"); transaction.bankTrExternalId = findSubInfo(ee, "1"); break; default: break; } } return transaction; } } // Рекурсивно обходим всех детей QDomNode child = root.firstChild(); while (!child.isNull()) { if (TransactionInfo info = parseExpandedInfo(child.toElement()); !info.bankTime.isEmpty()) { return info; } child = child.nextSibling(); } return {}; } TransactionInfo ScreenXmlParser::parseTransactionInfo(int screenWidth, int screenHeight) { // 1. Распарсить описание транзакции TransactionInfo transaction = parseExpandedInfo(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; } QList > ScreenXmlParser::parseAccountsInfo() { QList > accounts; for (const Node &node: m_nodes) { const QString text = node.text; // Проверяем только нужные строки if (!text.contains("Номер карты с последними цифрами")) { continue; } QRegularExpressionMatch match = reCardData.match(text); if (!match.hasMatch()) { continue; } AccountInfo info; info.lastNumbers = match.captured(1).remove(" "); info.amount = match.captured(2).remove(" ").toFloat(); info.description = text.mid(0, text.indexOf("Номер карты")); info.updateTime = DateUtils::getUtcNow(); accounts.append(qMakePair(info, node)); } return accounts; } void ScreenXmlParser::test() { // TransactionInfo info2 = parseExpandedInfo(m_xml); // qDebug().noquote().nospace() << convertTransactionToString(info2); // return; QFile file("assets/rshb/all_products.xml"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot open file"; } QDomDocument doc; if (!doc.setContent(&file)) { qDebug() << "Failed to parse XML"; } file.close(); qDebug() << "Start parsing XML:"; // Номер карты с последними цифрами QDomElement root = doc.documentElement(); parseAndSaveXml(doc.toString()); // for (const AccountInfo &info: parseAccountsInfo()) { // qDebug().noquote().nospace() << convertAccountToString(info); // } // TransactionInfo info = parseExpandedInfo(root); // qDebug().noquote().nospace() << convertTransactionToString(info); // ЕСПП }