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()