From 2e6d80c61cdb70456f28756af801b88dc6e295d4 Mon Sep 17 00:00:00 2001 From: dimon Date: Wed, 27 May 2026 21:28:42 +0000 Subject: [PATCH] Add get_screens.py --- get_screens.py | 232 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 get_screens.py diff --git a/get_screens.py b/get_screens.py new file mode 100644 index 0000000..8412d92 --- /dev/null +++ b/get_screens.py @@ -0,0 +1,232 @@ +import os +import re +import time +import csv +import cv2 +import xml.etree.ElementTree as ET +from pyzbar.pyzbar import decode +import shutil + +# --- НАСТРОЙКИ --- +ADB_PATH = "adb" +OUTPUT_CSV = "stocard_export.csv" +MANUAL_FOLDER = "pdf417_manual_cards" + +# Увеличенные задержки для старых или медленных телефонов (в секундах) +DELAY_OPEN_CARD = 1.0 # Сколько ждать, пока откроется карта и появится штрихкод +DELAY_BACK = 0.5 # Сколько ждать после нажатия кнопки "Назад" +DELAY_SCROLL = 0.5 # Сколько ждать после прокрутки списка +# ------------------ + +def run_adb(command): + return os.popen(f"{ADB_PATH} {command}").read() + +def ensure_stocard_is_open(): + """Проверяет, открыт ли Stocard, и если нет — запускает его MainActivity""" + activity_info = run_adb("shell dumpsys window displays | grep mCurrentFocus") + if "de.stocard.stocard" not in activity_info: + print("[ЗАЩИТА] Похоже, мы вылетели из приложения. Возвращаю Stocard...") + # Запуск через обход блокировки (MainActivity) + run_adb("shell am start -n de.stocard.stocard/de.stocard.ui.main.MainActivity") + time.sleep(3.0) + +def get_current_xml(): + """Стягивает актуальный снимок экрана XML с телефона""" + ensure_stocard_is_open() + run_adb("shell uiautomator dump /sdcard/current_dump.xml") + run_adb("pull /sdcard/current_dump.xml .") + run_adb("shell rm /sdcard/current_dump.xml") + return "current_dump.xml" + +def click_at(x, y): + ensure_stocard_is_open() + run_adb(f"shell input tap {x} {y}") + time.sleep(DELAY_OPEN_CARD) # Медленное ожидание открытия штрихкода + +def press_back(): + run_adb("shell input keyevent 4") + time.sleep(DELAY_BACK) + +def scroll_down(): + """Листает список карт вниз, чтобы подгрузить новые""" + ensure_stocard_is_open() + print("Прокручиваю список вниз...") + run_adb("shell input swipe 540 1800 540 600 500") + time.sleep(DELAY_SCROLL) + +def parse_card_title(): + """Делает дамп экрана ОТКРЫТОЙ карты и пытается вытащить название магазина""" + run_adb("shell uiautomator dump /sdcard/card_dump.xml") + run_adb("pull /sdcard/card_dump.xml .") + run_adb("shell rm /sdcard/card_dump.xml") + + title = "Неизвестный магазин" + if not os.path.exists("card_dump.xml"): + return title + + try: + tree = ET.parse("card_dump.xml") + root = tree.getroot() + + # Собираем все текстовые строки с экрана + possible_titles = [] + for elem in root.iter('node'): + text = elem.get('text', '').strip() + package = elem.get('package', '') + + # Отсекаем служебные тексты, цифры (номера карт) и пустые строки + if text and 'stocard' in package.lower(): + if not text.isdigit() and len(text) > 1 and text.lower() not in ['назад', 'back', 'share', 'поделиться', 'меню', 'menu']: + possible_titles.append(text) + + # Обычно название магазина — это самый первый или самый верхний текст на экране карточки + if possible_titles: + title = possible_titles[0] + + except Exception as e: + print(f"[ОШИБКА парсинга названия]: {e}") + + if os.path.exists("card_dump.xml"): + os.remove("card_dump.xml") + + return title + +def capture_and_scan(card_number_id): + """Делает скриншот открытой карты, распознает штрихкод и вытаскивает название""" + # 1. Сначала вытаскиваем название из XML открытой карты + card_title = parse_card_title() + + # 2. Делаем скриншот для распознавания штрихкода + run_adb("shell screencap -p /sdcard/screen.png") + run_adb("pull /sdcard/screen.png .") + run_adb("shell rm /sdcard/screen.png") + + img = cv2.imread("screen.png") + if img is None: + return card_title, None, None + + # 3. Пробуем распознать стандартный штрихкод + detected = decode(img) + for barcode in detected: + return card_title, barcode.data.decode('utf-8'), barcode.type + + # 4. ПЛАН Б: Если код не распознан (например, PDF417), сохраняем скриншот + # Используем название магазина в имени файла, чтобы было удобно смотреть глазами + safe_title = "".join([c for c in card_title if c.isalpha() or c.isdigit() or c==' ']).rstrip() + manual_screenshot_path = os.path.join(MANUAL_FOLDER, f"{card_number_id}_{safe_title}.png") + shutil.copy("screen.png", manual_screenshot_path) + + return card_title, f"РУЧНОЙ_ВВОД (См. скриншот {card_number_id}_{safe_title}.png)", "PDF417_OR_UNKNOWN" + +def extract_coordinates(xml_path): + """Ищет координаты только видимых на экране контейнеров карт и фильтрует дубликаты""" + centers = [] + try: + tree = ET.parse(xml_path) + root = tree.getroot() + except Exception as e: + return centers + + for elem in root.iter('node'): + package = elem.get('package', '') + if 'stocard' in package.lower(): + bounds = elem.get('bounds', '') + resource_id = elem.get('resource-id', '') + + if bounds and ('card' in resource_id.lower() or 'item' in resource_id.lower() or 'container' in resource_id.lower()): + match = re.findall(r'\d+', bounds) + if match and len(match) == 4: + x1, y1, x2, y2 = map(int, match) + + if y1 > 250 and y2 < 2100: + center_x = int((x1 + x2) / 2) + center_y = int((y1 + y2) / 2) + + # --- УМНАЯ ФИЛЬТРАЦИЯ ПО ДИСТАНЦИИ --- + # Проверяем, нет ли уже в списке точки, которая находится слишком близко + is_duplicate = False + for (existing_x, existing_y) in centers: + # Если расстояние по X меньше 100 и по Y меньше 200 — это та же самая карта + if abs(existing_x - center_x) < 100 and abs(existing_y - center_y) < 200: + is_duplicate = True + break + + if not is_duplicate: + centers.append((center_x, center_y)) + return centers + +def main(): + # Создаем папку для проблемных карт, если её нет + if not os.path.exists(MANUAL_FOLDER): + os.makedirs(MANUAL_FOLDER) + + print("=== ЗАПУСК ПОЛНОЙ АВТОМАТИЗАЦИИ СБОРА КАРТ (МЕДЛЕННЫЙ РЕЖИМ) ===") + print("Инструкция: Откройте самый верх главного списка в Stocard.") + input("Нажмите Enter для старта...") + + file_exists = os.path.isfile(OUTPUT_CSV) + with open(OUTPUT_CSV, mode="a", encoding="utf-8", newline="") as csv_file: + writer = csv.writer(csv_file) + if not file_exists: + writer.writerow(["Название", "Номер карты", "Тип штрихкода"]) + + saved_numbers = set() + consecutive_duplicates = 0 + card_counter = 1 + + while True: + xml_file = get_current_xml() + coordinates = extract_coordinates(xml_file) + + if not coordinates: + print("[ВНИМАНИЕ] На этом экране карты не найдены. Пробую прокрутить...") + scroll_down() + continue + + new_cards_on_screen = 0 + print(f"На текущем экране обнаружено точек для проверки: {len(coordinates)}") + + for (x, y) in coordinates: + print(f"Кликаю на карту в точку ({x}, {y})...") + click_at(x, y) + + # Получаем Название, Номер и Тип из обновленной функции + card_title, card_number, card_type = capture_and_scan(card_counter) + + if card_number: + # Проверяем на дубликаты (для обычных карт) или сохраняем, если это PDF417 + if card_number not in saved_numbers or card_type == "PDF417_OR_UNKNOWN": + saved_numbers.add(card_number) + + # Записываем РЕАЛЬНОЕ НАЗВАНИЕ вместо "Карта Х" + writer.writerow([card_title, card_number, card_type]) + csv_file.flush() + + print(f"[СОХРАНЕНО] #{card_counter}: {card_title} -> {card_number} ({card_type})") + card_counter += 1 + new_cards_on_screen += 1 + consecutive_duplicates = 0 + else: + print(f"[ДУБЛИКАТ] Карта магазина {card_title} уже в базе.") + else: + print("[ПРОПУСК] Экран пустой или произошла ошибка чтения.") + + print("Возвращаюсь обратно...") + press_back() + + if os.path.exists(xml_file): + os.remove(xml_file) + + if new_cards_on_screen == 0: + consecutive_duplicates += 1 + if consecutive_duplicates >= 2: + print("\n[УСПЕХ] Новые карты больше не появляются. Список полностью обработан!") + break + + scroll_down() + + print(f"\nСбор завершен! Всего сохранено строк: {card_counter - 1}") + print(f"Данные сохранены в: {os.path.abspath(OUTPUT_CSV)}") + +if __name__ == "__main__": + main() \ No newline at end of file