card-recognizer/get_screens.py
2026-05-27 21:28:42 +00:00

232 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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