from appium import webdriver
from appium.options.android import UiAutomator2Options
from selenium.common.exceptions import ElementClickInterceptedException, StaleElementReferenceException
from appium.webdriver.common.appiumby import AppiumBy
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)
# 存储已处理的元素签名(全局去重)
processed_elements = set()
# ================== 工具类:MobileElementActions ==================
class MobileElementActions:
def __init__(self, driver):
self.driver = driver
self.processed_inputs = set() # 已填写的输入框
def get_element_signature(self, element):
"""生成唯一标识符:resourceId + text + desc + className"""
try:
attrs = [
element.get_attribute("resourceId") or "",
element.get_attribute("text") or "",
element.get_attribute("contentDescription") or "",
element.get_attribute("className") or ""
]
raw_sig = "||".join(attrs)
return hashlib.md5(raw_sig.encode()).hexdigest()[:16]
except Exception as e:
print(f"⚠️ 获取元素签名失败: {e}")
return None
def is_interactable(self, element):
"""判断是否为真正可交互的 UI 元素(排除容器)"""
try:
clickable = element.get_attribute("clickable") == "true"
enabled = element.get_attribute("enabled") == "true"
focusable = element.get_attribute("focusable") == "true"
scrollable = element.get_attribute("scrollable") == "true"
clazz = element.get_attribute("className")
# 排除布局容器类
ignored_classes = {
'android.widget.LinearLayout',
'android.widget.RelativeLayout',
'android.widget.FrameLayout',
'android.widget.ConstraintLayout',
'android.widget.ScrollView',
'android.widget.HorizontalScrollView',
'android.view.View',
'android.view.ViewGroup'
}
if clazz in ignored_classes:
return False
return (clickable or (focusable and not scrollable)) and enabled
except:
return False
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 e:
print(f"❌ 坐标点击失败: {e}")
return False
except Exception as e:
print(f"❌ 点击失败: {e}")
return False
def wait_for_page_load(self, timeout=2):
"""等待页面加载完成"""
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 handle_input_fields(self):
"""填写输入框"""
try:
inputs = self.driver.find_elements(
AppiumBy.XPATH,
"//android.widget.EditText | "
"//android.widget.TextView[@focusable='true' and @editable='true']"
)
for inp in inputs:
try:
if not inp.is_displayed() or not inp.is_enabled():
continue
sig = self.get_element_signature(inp)
if sig in self.processed_inputs:
continue
current_value = inp.get_attribute("text") or ""
hint = inp.get_attribute("hint") or ""
print(f"📝 发现输入框 - 当前文本: '{current_value}', 提示: '{hint}'")
inp.clear()
inp.send_keys("AutoInputTest")
self.processed_inputs.add(sig)
except Exception as e:
print(f"⚠️ 输入失败: {e}")
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
# ================== 主流程:自动化遍历 ==================
class MobileAutomationWorkflow:
def __init__(self, driver):
self.driver = driver
self.actions = MobileElementActions(driver)
self.max_iterations = 300
self.iteration_count = 0
def get_all_interactable_elements(self):
"""获取当前页面所有可交互且未处理过的元素"""
try:
all_elements = self.driver.find_elements(AppiumBy.XPATH, "//*")
candidates = []
for elem in all_elements:
if not self.actions.is_interactable(elem):
continue
sig = self.actions.get_element_signature(elem)
if sig and sig not in processed_elements:
candidates.append((elem, sig))
return candidates
except Exception as e:
print(f"❌ 获取可交互元素失败: {e}")
return []
def run(self):
print("📱 开始移动应用自动化遍历...")
stack_depth = 0 # 记录递归深度(用于调试)
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:
break
continue
# 🔍 获取当前页面所有【未点击过的】可交互元素
interactable_list = self.get_all_interactable_elements()
if not interactable_list:
print("✅ 当前页面无可点击的新元素,准备返回")
try:
self.driver.back()
time.sleep(1.5)
stack_depth -= 1
except:
print("🔚 已达根页面,无法返回")
break
continue
print(f"找到 {len(interactable_list)} 个新可点击元素")
# 📝 从列表中取第一个未点击的元素进行点击
element, sig = interactable_list[0]
try:
text = element.get_attribute("text") or "No Text"
desc = element.get_attribute("contentDescription") or ""
clazz = element.get_attribute("className").split(".")[-1]
print(f"👉 尝试点击: [{clazz}] '{text}' (描述: '{desc}')")
if self.actions.safe_click(element):
processed_elements.add(sig) # 标记为已处理
self.actions.wait_for_page_load()
# 处理可能弹出的弹窗或输入框
self.actions.handle_alerts()
self.actions.handle_input_fields()
stack_depth += 1
else:
# 如果点击失败,仍标记为已处理防止无限循环
processed_elements.add(sig)
except StaleElementReferenceException:
print("⚠️ 元素已失效,跳过")
processed_elements.add(sig)
except Exception as e:
print(f"⚠️ 处理元素时出错: {e}")
processed_elements.add(sig)
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()
1.退出app后driverback没有使其从桌面进入到app
2.点击混乱
3.输入框的内容由用户输入,设置等待时间为3s并在控制台输出用户的输入内容
日志如下:
C:\own\app\python_code\.venv\Scripts\python.exe C:\own\app\python_code\work\IBL_click_all\click_all.py
📱 开始移动应用自动化遍历...
🔄 第 1 轮:扫描当前页面
Current Activity: .module.home.home.HomeActivity
找到 3 个新可点击元素
👉 尝试点击: [RecyclerView] 'No Text' (描述: 'null')
🔄 第 2 轮:扫描当前页面
Current Activity: .module.home.home.HomeActivity
找到 2 个新可点击元素
👉 尝试点击: [Button] 'DOWNLOAD TEMPLATES' (描述: 'null')
🔄 第 3 轮:扫描当前页面
Current Activity: .module.home.home.HomeActivity
找到 1 个新可点击元素
👉 尝试点击: [Button] 'NEW LABEL' (描述: 'null')
📝 发现输入框 - 当前文本: '', 提示: 'null'
🔄 第 4 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 5 个新可点击元素
👉 尝试点击: [ImageButton] 'No Text' (描述: 'Navigate up')
🔄 第 5 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 2 个新可点击元素
👉 尝试点击: [Button] 'CANCEL' (描述: 'null')
🔄 第 6 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 6 个新可点击元素
👉 尝试点击: [TextView] 'Undo' (描述: 'null')
🔄 第 7 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 5 个新可点击元素
👉 尝试点击: [TextView] 'Save' (描述: 'null')
📝 发现输入框 - 当前文本: '', 提示: 'null'
🔄 第 8 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 2 个新可点击元素
👉 尝试点击: [EditText] 'AutoInputTest' (描述: 'null')
📝 发现输入框 - 当前文本: 'AutoInputTest', 提示: 'null'
🔄 第 9 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 1 个新可点击元素
👉 尝试点击: [Button] 'SAVE' (描述: 'null')
🔄 第 10 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 4 个新可点击元素
👉 尝试点击: [TextView] 'Print' (描述: 'null')
🔄 第 11 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 5 个新可点击元素
👉 尝试点击: [ImageButton] 'No Text' (描述: 'null')
🔄 第 12 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 3 个新可点击元素
👉 尝试点击: [ImageView] 'No Text' (描述: 'null')
🔄 第 13 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 1 个新可点击元素
👉 尝试点击: [ExpandableListView] 'No Text' (描述: 'null')
🔄 第 14 轮:扫描当前页面
Current Activity: com.google.android.apps.contacts.activities.leaf.ui.picker.ContactPickerActivity
⚠️ 当前不在目标应用 (com.google.android.contacts),返回中...
🔄 第 14 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 2 个新可点击元素
👉 尝试点击: [TextView] '0.70"' (描述: 'null')
✅ 自动关闭弹窗: OK
🔄 第 15 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
找到 1 个新可点击元素
👉 尝试点击: [TextView] '100%' (描述: 'null')
🔄 第 16 轮:扫描当前页面
Current Activity: .edit.EditLabelActivity
✅ 当前页面无可点击的新元素,准备返回
🔄 第 16 轮:扫描当前页面
Current Activity: .module.home.home.HomeActivity
✅ 当前页面无可点击的新元素,准备返回
🔄 第 16 轮:扫描当前页面
Current Activity: .NexusLauncherActivity
⚠️ 当前不在目标应用 (com.google.android.apps.nexuslauncher),返回中...
🔚 已退出目标应用,结束遍历
🏁 自动化完成,共执行 15 次操作。
Process finished with exit code 0
最新发布