1.修改代码,使代码正常运行,不丢失功能
2.修改判断逻辑,不通过当前页面是否访问过来决定是否返回上一页面,而是判断当前页面可点击的元素是否都全部点击,全部点击返回上一界面
from appium import webdriver
from appium.options.android import UiAutomator2Options
from selenium.common.exceptions import ElementClickInterceptedException, StaleElementReferenceException, \
TimeoutException
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import hashlib
# ==================== 目标应用配置 ====================
TARGET_PACKAGE = "com.brother.ptouch.iprintandlabel"
# ==================== 配置 Appium Options ====================
options = UiAutomator2Options()
options.set_capability("platformName", "Android")
options.set_capability("deviceName", "emulator-5554") # 确保设备在线
options.set_capability("automationName", "UiAutomator2")
options.set_capability("noReset", True) # 不清除数据(保留登录状态等)
options.set_capability("fullReset", False)
options.set_capability("autoGrantPermissions", True)
options.set_capability("newCommandTimeout", 600)
# 启动 App(假设已安装)
driver = webdriver.Remote(command_executor='http://localhost:4723', options=options)
# 存储已处理元素和访问过的页面状态(增强版:包+Activity+结构)
processed_elements = set()
visited_page_hashes = set()
# ================== 工具类:MobileElementActions ==================
class MobileElementActions:
def __init__(self, driver):
self.driver = driver
self.processed_inputs = set()
def get_navigation_elements(self):
all_clickable = self.get_all_clickable_elements()
navigation_elements = [ele for ele in all_clickable if self.is_navigation_bar(ele)]
return list(dict.fromkeys(navigation_elements))
def get_all_clickable_elements(self):
return self.driver.find_elements(
By.CSS_SELECTOR,
('button, a, input[type="button"], input[type="submit"], input[type="radio"], input[type="checkbox"], '
'div[role="button"], [role="menuitem"], [role="option"], [data-testid*="button"], [class*="btn"], '
'[class*="clickable"], select')
)
def is_navigation_bar(self, element):
parent = element
while True:
try:
parent = parent.find_element(By.XPATH, '..')
if parent.tag_name.lower() == 'html':
return False
tag_name = parent.tag_name.lower()
class_name = parent.get_attribute('class').lower()
id_name = parent.get_attribute('id').lower()
if tag_name == 'nav' or \
any(kw in class_name for kw in ['navbar', 'navigation', 'menu']) or \
any(kw in id_name for kw in ['nav', 'menu']):
return True
except Exception as e:
return False
def get_element_identifier(self, element):
try:
text = element.text.strip()
id = element.get_attribute("id")
tag = element.tag_name
classes = ' '.join(element.get_attribute('class').split())
vaule = element.get_attribute("vaule") or ' '
return f"{tag}__{classes}__{text}__{vaule}__{id}"
except:
return str(hash(element))
def is_interactable(self, element):
try:
element_id = self.get_element_identifier(element)
if element_id in processed_elements:
return False
if not element.is_displayed() or not element.is_enabled():
return False
except:
return str(hash(element))
def safe_click(self, element):
"""安全点击,支持 fallback 到坐标点击"""
try:
element.click()
return True
except ElementClickInterceptedException:
print("⚠️ 元素被遮挡,尝试通过坐标点击")
try:
loc = element.location
size = element.size
x = loc['x'] + size['width'] // 2
y = loc['y'] + size['height'] // 2
self.driver.tap([(x, y)])
return True
except Exception as tap_e:
print(f"❌ 坐标点击也失败: {tap_e}")
return False
except Exception as e:
print(f"❌ 点击失败: {e}")
return False
def wait_for_page_load(self, timeout=3):
"""等待页面加载完成"""
time.sleep(timeout) # 可替换为显式等待动态内容
def handle_alerts(self):
"""自动关闭常见弹窗按钮"""
alert_texts = ['OK', '确定', 'Cancel', '取消', '知道了', '同意']
for text in alert_texts:
try:
btn = self.driver.find_element(
AppiumBy.XPATH,
f"//*[@text='{text}' or @contentDescription='{text}']")
if btn.is_displayed() and btn.is_enabled():
btn.click()
print(f"✅ 自动关闭弹窗: {text}")
return True
except:
continue
return False
def highlight_element(self, element, duration=1.5, color="yellow", border="2px solid red"):
original_style = element.get_attribute("style")
self.driver.execute_script(
f"arguments[0].setAttribute('style', arguments[1]);",
element,
f"border: {border}; background-color: {color}; {original_style}"
)
time.sleep(duration)
self.driver.execute_script(f"arguments[0].setAttribute('style', arguments[1]);", element, original_style)
def handle_input_fields(self):
try: # 找输入框
inputs = WebDriverWait(self.driver, 5).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'input[type="text"], input[type="password"], textarea'))
)
for index, element in enumerate(inputs):
try: # 处理输入框
element_id = self.get_element_identifier(element)
if element_id in self.processed_inputs:
continue
if not element.is_displayed() or not element.is_enabled():
continue
self.highlight_element(element, color="#ffff99", border="3px dashed red")
print(f"请在输入框 {index + 1} 中输入内容。")
time.sleep(1)
entered_value = element.get_attribute("value")
print(f"您在输入框 {index + 1} 中输入的内容为: {entered_value}")
self.processed_inputs.add(element_id)
except Exception as e:
print(f"处理输入框时出错: {e}")
except TimeoutException:
pass
except Exception as e:
print(f"处理输入框时出错: {e}")
def get_current_page_hash(self):
"""获取当前页面源码的哈希值"""
try:
src = self.driver.page_source
return hashlib.md5(src.encode('utf-8')).hexdigest()
except Exception as e:
print(f"⚠️ 获取页面源码失败: {e}")
return None
def get_current_state_signature(self):
"""返回当前状态唯一标识:包名::Activity::页面哈希"""
try:
pkg = self.driver.current_package
act = self.driver.current_activity
hsh = self.get_current_page_hash()
return f"{pkg}::{act}::{hsh}"
except Exception as e:
print(f"⚠️ 生成状态签名失败: {e}")
return None
# ================== 主流程:自动化遍历 ==================
class MobileAutomationWorkflow:
def __init__(self, driver):
self.driver = driver
self.actions = MobileElementActions(driver)
self.max_iterations = 300
self.iteration_count = 0
def run(self):
print("📱 开始移动应用自动化遍历...")
try:
while self.iteration_count < self.max_iterations:
print(f"\n🔄 第 {self.iteration_count + 1} 轮:扫描当前页面")
print("Current Activity:", self.driver.current_activity)
# ✅ 检查是否仍在目标 App 中
if self.driver.current_package != TARGET_PACKAGE:
print(f"⚠️ 当前不在目标应用 ({self.driver.current_package}),尝试返回...")
try:
self.driver.back()
time.sleep(1.5)
if self.driver.current_package != TARGET_PACKAGE:
print("🔚 已退出目标应用,停止遍历")
break
except:
print("🔙 返回失败,结束运行")
break
continue
# ✅ 使用【包名+Activity+页面结构】作为唯一状态标识
current_state = self.actions.get_current_state_signature()
if current_state in visited_page_hashes:
print("🔁 当前【App+页面】组合已访问过,尝试返回")
try:
self.driver.back()
time.sleep(1.5)
continue
except:
print("🔙 返回失败或已达根页面")
break
else:
visited_page_hashes.add(current_state)
# 获取所有可见元素
try:
elements = self.driver.find_elements(AppiumBy.XPATH, "//*")
except Exception as e:
print(f"❌ 获取元素列表失败: {e}")
break
print(f"找到 {len(elements)} 个元素")
found_new_clickable = False
for elem in elements:
try:
if not self.actions.is_interactable(elem):
continue
sig = self.actions.get_element_signature(elem)
if not sig or sig in processed_elements:
continue
text = elem.get_attribute("text") or "No Text"
desc = elem.get_attribute("contentDescription") or ""
clazz = elem.get_attribute("className").split(".")[-1]
print(f"👉 尝试点击: [{clazz}] '{text}' (描述: '{desc}')")
if self.actions.safe_click(elem):
processed_elements.add(sig)
found_new_clickable = True
self.actions.wait_for_page_load()
# 处理弹窗
self.actions.handle_alerts()
# 处理输入框
self.actions.handle_input_fields()
# 成功点击后跳出,重新分析新页面
break
except StaleElementReferenceException:
print("⚠️ 元素已失效,跳过")
continue
except Exception as inner_e:
print(f"⚠️ 处理元素时发生非致命错误: {inner_e}")
continue
if not found_new_clickable:
print("✅ 当前页面无可点击的新元素")
try:
self.driver.back()
time.sleep(1.5)
back_state = self.actions.get_current_state_signature()
if back_state in visited_page_hashes:
print("🔚 回退后仍无法前进,结束遍历")
break
except Exception as back_e:
print(f"🔙 返回失败或已达根页面: {back_e}")
break
self.iteration_count += 1
print(f"🏁 自动化完成,共执行 {self.iteration_count} 次操作。")
except Exception as e:
print(f"🚨 自动化过程发生严重错误: {e}")
finally:
self.driver.quit()
# ================== 启动入口 ==================
if __name__ == "__main__":
workflow = MobileAutomationWorkflow(driver)
workflow.run()