232 lines
11 KiB
Python
232 lines
11 KiB
Python
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() |