from appium import webdriver
from appium.options.android import UiAutomator2Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, NoSuchElementException, ElementNotInteractableException
import time
# ==================== 目标应用配置 ====================
TARGET_PACKAGE = "com.brother.ptouch.iprintandlabel"
TARGET_ACTIVITY = ".module.home.home.HomeActivity" # 主页 Activity
APK_XPATH="C:/own/download/iPL5.3.10_signed.apk"
MAX_RESTORE_ATTEMPTS = 2
MAX_HISTORY_REPEAT = 3
# ==================== 配置 Appium Options ====================
def create_driver():
options = UiAutomator2Options()
options.set_capability("platformName", "Android")
options.set_capability("deviceName", "emulator-5554")
options.set_capability("automationName", "UiAutomator2")
options.set_capability("noReset", False)
options.set_capability("fullReset", False)
options.set_capability("autoGrantPermissions", True)
options.set_capability("newCommandTimeout", 600)
options.set_capability("appPackage", TARGET_PACKAGE)
options.set_capability("appActivity", TARGET_ACTIVITY)
options.set_capability("app",APK_XPATH)
driver = webdriver.Remote('http://localhost:4723', options=options)
return driver
class AndroidAppExplorer:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
self.clicked_identifiers = set()
self.recovery_history = []
self.restore_attempt_count = 0
def get_current_state(self):
try:
current_package = self.driver.current_package
current_activity = self.driver.current_activity
return current_package, current_activity
except:
return None, None
def is_in_target_app(self):
pkg, _ = self.get_current_state()
return pkg == TARGET_PACKAGE
def wait_for_page_load(self, timeout=10):
"""等待页面加载完成"""
try:
WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((
'xpath',
'//android.view.ViewGroup[@displayed="true"] | '
'//android.widget.FrameLayout[@displayed="true"]'
))
)
time.sleep(1)
except Exception as e:
print(f"⚠️ 页面等待超时或出错: {e}")
def ensure_in_target_app(self):
current_pkg, current_act = self.get_current_state()
if not self.is_in_target_app():
print(f"⚠️ 当前不在目标应用 ({current_pkg}/{current_act}),正在尝试恢复...")
current_key = (current_pkg, current_act)
recent_entries = self.recovery_history[-10:]
if recent_entries.count(current_key) >= MAX_HISTORY_REPEAT:
print(f"🛑 已多次回到 {current_key},疑似陷入循环,停止恢复。")
return False
self.recovery_history.append(current_key)
# 方法1:back 返回
if self.restore_attempt_count < MAX_RESTORE_ATTEMPTS:
try:
self.driver.back()
time.sleep(1.5)
if self.is_in_target_app():
print("✅ 通过 back 成功返回目标 App")
self.restore_attempt_count = 0
self.wait_for_page_load(10)
return True
except:
pass
# 方法2:activate_app
if self.restore_attempt_count < MAX_RESTORE_ATTEMPTS:
try:
self.driver.activate_app(TARGET_PACKAGE)
print("🔁 正在通过 activate_app 恢复...")
time.sleep(2)
self.wait_for_page_load(10)
if self.is_in_target_app():
print("✅ 成功激活目标 App")
self.restore_attempt_count = 0
return True
except Exception as e:
print(f"❌ activate_app 失败: {e}")
# 方法3:冷重启
if self.restore_attempt_count >= MAX_RESTORE_ATTEMPTS:
try:
print("💀 执行冷重启...")
self.driver.terminate_app(TARGET_PACKAGE)
time.sleep(2)
self.driver.activate_app(TARGET_PACKAGE)
time.sleep(5)
self.wait_for_page_load(10)
if self.is_in_target_app():
print("✅ 冷重启成功")
self.restore_attempt_count = 0
return True
except Exception as e:
print(f"❌ 冷重启失败: {e}")
self.restore_attempt_count += 1
if self.restore_attempt_count > 5:
print("🛑 恢复尝试过多,终止运行")
return False
return True
else:
self.recovery_history.append((current_pkg, current_act))
self.wait_for_page_load(1)
return True
def get_element_identifier(self, element):
try:
attrs = [
element.get_attribute("resource-id") or "",
element.get_attribute("text") or "",
element.get_attribute("content-desc") or "",
element.tag_name or ""
]
return "/".join([a for a in attrs if a.strip()])
except:
return "unknown_element"
def find_clickable_elements(self):
xpath = """
//*[(@clickable='true' or @focusable='true') and
@displayed='true' and
@enabled='true']
"""
try:
elements = self.driver.find_elements('xpath', xpath)
return [e for e in elements if e.is_displayed()]
except Exception as e:
print(f"❌ 查找元素失败: {e}")
return []
def classify_elements(self, elements):
bottom_nav_container = "com.brother.ptouch.iprintandlabel:id/bottomNavigationView"
nav_ids = [
"com.brother.ptouch.iprintandlabel:id/navigation_my_label",
"com.brother.ptouch.iprintandlabel:id/navigation_setting",
"com.brother.ptouch.iprintandlabel:id/navigation_buy"
]
nav_elements = []
non_nav_elements = []
for elem in elements:
try:
res_id = elem.get_attribute("resource-id") or ""
content_desc = elem.get_attribute("content-desc") or ""
text = elem.get_attribute("text") or ""
if (res_id == bottom_nav_container or
res_id in nav_ids or
content_desc in ["My Labels", "Settings", "Shop"]):
nav_elements.append(elem)
else:
non_nav_elements.append(elem)
except Exception as e:
print(f"⚠️ 分类元素时异常: {e}")
continue
return nav_elements, non_nav_elements
def handle_alert_popup(self):
alert_xpath = "//*[contains(@text, 'OK') or contains(@text, '允许') or contains(@text, 'Allow')]"
try:
button = self.driver.find_element('xpath', alert_xpath)
if button.is_displayed():
button.click()
print("✅ 自动处理了弹窗")
time.sleep(1)
except:
pass
def is_interactive_input(self, element):
"""判断是否为可视且可交互的输入框"""
try:
if not element.is_displayed():
return False
focusable = element.get_attribute("focusable") == "true"
enabled = element.get_attribute("enabled") == "true"
clazz = element.get_attribute("className") or ""
is_edit_type = any(kw in clazz for kw in ["EditText", "TextInput"])
return focusable and enabled and is_edit_type
except:
return False
def handle_input_field(self, input_elem):
"""处理输入框:显示提示、当前值,并让用户手动输入后读取结果"""
hint = input_elem.get_attribute("hint") or "null"
current_text = input_elem.get_attribute("text") or ""
print(f"\n🔍 发现输入框")
if hint != "null":
print(f" 提示: '{hint}'")
if current_text:
print(f" 当前文本: '{current_text}'")
print("请在设备上手动输入内容(3秒内完成)...")
time.sleep(3)
try:
entered_value = input_elem.get_attribute("text") or "" # 使用 text 获取输入内容
except:
entered_value = ""
print(f"📝 用户在输入框中输入的内容为: '{entered_value}'")
def click_element_safely(self, element):
try:
identifier = self.get_element_identifier(element)
if identifier in self.clicked_identifiers:
return False
try:
self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
except:
pass
time.sleep(0.5)
element.click()
print(f"🟢 点击成功: {identifier}")
self.clicked_identifiers.add(identifier)
time.sleep(1.5)
return True
except Exception as e:
print(f"🔴 点击失败 {self.get_element_identifier(element)}: {str(e)}")
return False
def is_fully_active(self, button):
"""判断按钮是否处于完全激活状态"""
try:
alpha = button.get_attribute("alpha")
if alpha and float(alpha) < 0.8:
return False
except:
pass
try:
class_name = button.get_attribute("class")
if "disabled" in class_name.lower():
return False
except:
pass
return True
def handle_user_agreement(self):
print("🔍 检测是否需要接受用户协议...")
try:
initial_activity = self.driver.current_activity
except:
initial_activity = None
try:
# 等待协议页面出现
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((
'xpath',
'//*[contains(@text, "License") or contains(@text, "Agreement") or contains(@text, "user")]'
))
)
print("📄 检测到【用户协议】页面")
def find_agree_button():
candidates = [
'//*[@text="I agree"]',
'//*[@text="Agree"]',
'//*[contains(@resource-id, "yes")]',
'//*[contains(@resource-id, "agree")]',
]
for xpath in candidates:
try:
btn = self.driver.find_element(By.XPATH, xpath)
if btn.is_displayed() and btn.is_enabled() and self.is_fully_active(btn):
return btn
except:
continue
return None
# 先尝试直接点击
agree_btn = find_agree_button()
if agree_btn:
print("✅ 发现可点击的【I agree】按钮")
current_act_before = self.driver.current_activity
agree_btn.click()
time.sleep(3)
if self.driver.current_activity != current_act_before:
print("✅ 已跳转至新页面,协议处理完成")
return True
else:
print("🟡 点击了【I agree】但未跳转,可能需滚动")
# 开始滚动
print("👇 协议未读完,开始滚动...")
last_click_failed_id = None
for i in range(10):
size = self.driver.get_window_size()
x = size['width'] // 2
y_start = int(size['height'] * 0.8)
y_end = int(size['height'] * 0.3)
self.driver.swipe(x, y_start, x, y_end, 600)
time.sleep(1)
if i<6:
continue
agree_btn = find_agree_button()
if not agree_btn:
continue
# 防止重复点击同一个无效按钮
if hasattr(agree_btn, 'id') and agree_btn.id == last_click_failed_id:
continue
print(f"✅ 第 {i + 1} 次滑动后发现可点击的【I agree】按钮")
current_act_before = self.driver.current_activity
agree_btn.click()
print("📌 正在等待页面跳转...")
time.sleep(3)
try:
WebDriverWait(self.driver, 10).until(
lambda d: d.current_activity != current_act_before
)
print("✅ 成功跳转出协议页面")
return True
except:
print(f"❌ 第 {i + 1} 次滑动后点击【I agree】无效,仍在原页面")
last_click_failed_id = agree_btn.id # 记录失败 ID
# 兜底:UiAutomator 强制查找 clickable=true 的按钮
try:
final_btn = self.driver.find_element(
By.ANDROID_UIAUTOMATOR,
'new UiSelector().textContains("agree").enabled(true).clickable(true)'
)
final_btn.click()
print("🪄 使用 UiAutomator 强制点击【I agree】")
time.sleep(3)
if self.driver.current_activity != initial_activity:
print("✅ 强制点击后成功跳转")
return True
except Exception as e:
print(f"❌ UiAutomator 点击失败: {e}")
return False
except TimeoutException:
print("🟢 未检测到用户协议页面,跳过")
return True
except Exception as e:
print(f"⚠️ 用户协议处理异常: {type(e).__name__}: {e}")
return False
def handle_printer_selection(self):
"""
处理打印机选择页面
基于设备型号关键词自动选择一台打印机并点击 Done
"""
print("🖨️ 检测是否进入【打印机选择】页面...")
device_keywords = ["PT-", "QL-", "TD-", "HL-", "MFC-"]
try:
# 等待至少一个设备型号文本出现
WebDriverWait(self.driver, 10).until(
lambda d: any(
any(kw in (elem.get_attribute("text") or "")
for kw in device_keywords)
for elem in d.find_elements(
By.XPATH,
'//android.widget.TextView[@text and string-length(@text) > 5]'
)
)
)
print("✅ 进入打印机选择页面")
# 查找所有 TextView 并匹配设备型号
candidates = self.driver.find_elements(
By.XPATH,
'//android.widget.TextView[@text and string-length(@text) > 5]'
)
selected = False
for elem in candidates:
text = elem.get_attribute("text") or ""
if any(kw in text for kw in device_keywords):
try:
elem.click()
print(f"✅ 选择了打印机: {text}")
time.sleep(1.5)
selected = True
break
except Exception as e:
print(f"🟡 无法点击 {text}: {str(e)}")
selected = True # 可能已选中
break
if not selected:
print("ℹ️ 未找到新设备可点击(可能已有默认选中)")
# 点击 Done 按钮(双条件定位增强兼容性)
done_xpath = (
'//android.widget.Button[@text="Done"] | '
'//android.widget.Button[@resource-id="com.brother.ptouch.iprintandlabel:id/wel_download_templates"]'
)
done_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, done_xpath))
)
done_btn.click()
print("✅ 成功点击【Done】进入主界面")
time.sleep(3)
return True
except TimeoutException:
print("🟢 未检测到打印机选择页面,跳过")
return True
except Exception as e:
print(f"⚠️ 打印机选择失败: {type(e).__name__}: {e}")
return False
def explore_page(self):
print("\n🔄 开始探索当前页面...")
while True:
if not self.ensure_in_target_app():
print("❌ 无法恢复至目标应用,退出。")
break
self.handle_alert_popup()
elements = self.find_clickable_elements()
if not elements:
print("📭 当前页面无可点击元素。")
try:
self.driver.back()
print("🔙 返回上一页...")
time.sleep(2)
continue
except:
break
nav_elements, non_nav_elements = self.classify_elements(elements)
print(f"📌 导航栏元素数量: {len(nav_elements)}, 非导航栏元素数量: {len(non_nav_elements)}")
# === 第一阶段:优先点击导航栏元素 ===
navigated = False
for elem in nav_elements:
if self.is_interactive_input(elem):
self.handle_input_field(elem)
continue
if self.click_element_safely(elem):
navigated = True
break
if navigated:
continue
# === 第二阶段:点击非导航栏元素 ===
any_clicked = False
for elem in non_nav_elements:
if self.is_interactive_input(elem):
self.handle_input_field(elem)
continue
if self.click_element_safely(elem):
any_clicked = True
break
if not any_clicked:
print("✅ 当前页面所有可点击元素已处理完毕。")
try:
self.driver.back()
print("🔙 返回上一页继续探索...")
time.sleep(2)
except:
print("🔚 无法返回,探索结束。")
break
time.sleep(1)
def run(self):
print("🚀 启动 Android 应用探索器...")
try:
# === 新增:前置初始化流程 ===
print("\n🔧 正在处理初始化流程...")
# 等待应用启动
time.sleep(5)
# 处理用户协议
if not self.handle_user_agreement():
print("❌ 用户协议处理失败,但仍继续...")
else:
print("✅ 用户协议处理完成")
# 处理打印机选择
if not self.handle_printer_selection():
print("⚠️ 打印机选择未完成,可能已在主界面")
# else:
# print("✅ 打印机选择完成,已进入主界面")
# === 开始主探索流程 ===
self.explore_page()
except KeyboardInterrupt:
print("\n👋 用户中断执行。")
except Exception as e:
print(f"💥 执行过程中发生错误: {type(e).__name__}: {e}")
finally:
print("🔚 自动化结束。")
# =============== 主程序入口 ===============
if __name__ == "__main__":
driver = create_driver()
explorer = AndroidAppExplorer(driver)
try:
explorer.run()
finally:
driver.terminate_app(TARGET_PACKAGE)
driver.remove_app(TARGET_PACKAGE)
driver.quit()
每次运行点击的内容都不一样,有时会有输入框有时没有(运行5次可能有1次),获取元素不全,有些元素通过Bottom Sheet + ViewPager + GridView 分类懒加载的方式加载,不丢失功能的情况下解决我的问题
最新发布