executor.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import uiautomator2 as u2
  2. import time
  3. import subprocess
  4. import random
  5. from typing import Optional, Tuple, Union
  6. def find_template_position(small_img_path, big_img_path, threshold=0.8):
  7. """
  8. 在小图中查找大图的位置,返回中心坐标
  9. Args:
  10. small_img_path: 小块碎片路径(要查找的图片)
  11. big_img_path: 整张图片路径(被查找的图片)
  12. threshold: 匹配阈值 0-1,默认0.8
  13. Returns:
  14. (x, y): 中心坐标,如果没找到返回 None
  15. """
  16. # 1. 读取图片
  17. small = cv2.imread(small_img_path)
  18. big = cv2.imread(big_img_path)
  19. if small is None or big is None:
  20. print("❌ 图片读取失败")
  21. return None
  22. # 2. 模板匹配
  23. result = cv2.matchTemplate(big, small, cv2.TM_CCOEFF_NORMED)
  24. min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  25. print(f"匹配度: {max_val:.3f}")
  26. # 3. 判断是否匹配成功
  27. if max_val < threshold:
  28. print(f"❌ 未找到匹配 (阈值: {threshold})")
  29. return None
  30. # 4. 计算中心坐标
  31. h, w = small.shape[:2]
  32. center_x = max_loc[0] + w // 2
  33. center_y = max_loc[1] + h // 2
  34. return (center_x, center_y)
  35. class SafeExecutor:
  36. """
  37. 安全执行器:为 uiautomator2 提供重试、降级、随机延迟等能力
  38. 所有方法失败时直接抛出异常,不返回结果对象
  39. """
  40. def __init__(self,device_id, default_retry: int = 2, human_delay: bool = True):
  41. """
  42. 参数:
  43. driver: uiautomator2 连接对象
  44. default_retry: 默认重试次数
  45. human_delay: 是否添加随机延迟(模拟人类操作)
  46. """
  47. self.code = 5
  48. self.device_id = device_id
  49. self.driver = u2.connect_usb(self.device_id)
  50. self.default_retry = default_retry
  51. self.human_delay = human_delay
  52. w, h = self.driver.window_size()
  53. self.h = h
  54. # ========== 私有工具方法 ==========
  55. def _random_sleep(self, min_sec: float = 0.05, max_sec: float = 0.2):
  56. """随机睡眠,模拟人类操作间隔"""
  57. if self.human_delay:
  58. time.sleep(random.uniform(min_sec, max_sec))
  59. def _wait_before_retry(self, attempt: int):
  60. """重试前等待(指数退避)"""
  61. wait_time = min(0.5 * (2 ** attempt), 3)
  62. time.sleep(wait_time)
  63. # ========== 基础操作 ==========
  64. def safe_click(self,xpath):
  65. try:
  66. if self.No_slider():
  67. if type(xpath)!=str:
  68. elem = xpath
  69. else:
  70. elem = self.driver.xpath(xpath)
  71. if not elem.exists:
  72. return False
  73. elem.click()
  74. time.sleep(2)
  75. if self.No_slider():
  76. return True
  77. else:
  78. return False
  79. except Exception as exc:
  80. print(f"点击失败,已跳过: {xpath}, error: {exc}")
  81. return False
  82. def tap(self, x: int, y: int, retry: Optional[int] = None):
  83. """
  84. 点击坐标,带重试
  85. 失败抛出异常
  86. """
  87. try:
  88. self.driver.click(x, y)
  89. self._random_sleep(0.1, 0.3)
  90. return # 成功,直接返回
  91. except Exception as e:
  92. raise Exception(f"点击失败 {x},{y},{e}")
  93. def swipe(self,distance,times=2):
  94. """
  95. 向上滑动指定距离,分多次完成
  96. Args:
  97. distance: 总滑动距离(如 1200)
  98. times: 分成几次(默认2次)
  99. """
  100. w, h = self.driver.window_size()
  101. x = w // 2 + random.randint(-30, 30) # X轴随机偏移
  102. start_y = int(h * 0.7) + random.randint(-20, 20) # 起始位置随机偏移
  103. per_distance = distance // times
  104. current_y = start_y
  105. # print(f"📏 向上滑动 {distance}px,分 {times} 次")
  106. for i in range(times):
  107. # 每次滑动距离随机波动
  108. if i == times - 1:
  109. end_y = current_y - (distance - (per_distance * (times - 1)))
  110. else:
  111. # 每段距离 ±20px 波动
  112. seg_distance = per_distance + random.randint(-15, 15)
  113. end_y = current_y - seg_distance
  114. end_y = max(50, end_y)
  115. # 滑动时长随机
  116. duration = random.uniform(0.06, 0.12)
  117. # print(f" 第{i+1}次: {current_y} → {end_y}")
  118. self.driver.drag(x, current_y, x, end_y, duration=duration)
  119. if i < times - 1:
  120. # 暂停时间随机
  121. pause_time = random.uniform(0.2, 0.5)
  122. #print(f" ⏸️ 暂停 {pause_time:.2f}s")
  123. time.sleep(pause_time)
  124. current_y = end_y
  125. def input_text(self,xpaths,text):
  126. """
  127. 输入文本,带重试
  128. 失败抛出异常
  129. """
  130. try:
  131. search_input = self.driver.xpath(xpaths)
  132. if search_input.exists:
  133. search_input.set_text('')
  134. time.sleep(1)
  135. # 模拟打字间隔
  136. search_input.set_text(text)
  137. return True
  138. return False
  139. except Exception as e:
  140. raise Exception(f"输入失败 '{text}',: {e}")
  141. def is_connected(self) -> bool:
  142. """检查与设备的连接是否正常"""
  143. try:
  144. # 方式1:检查 atx-agent 是否响应
  145. info = self.driver.info # 会请求 atx-agent
  146. return True
  147. except Exception:
  148. self._recover_service()
  149. return False
  150. def _recover_service(self):
  151. #重新连接
  152. # 停止 atx-agent
  153. subprocess.run(["adb", "-s", self.device_id, "shell",
  154. "/data/local/tmp/atx-agent", "server", "-d", "--stop"],
  155. capture_output=True, timeout=5)
  156. time.sleep(1)
  157. # 启动 atx-agent
  158. subprocess.run(["adb", "-s", self.device_id, "shell",
  159. "/data/local/tmp/atx-agent", "server", "-d"],
  160. capture_output=True, timeout=5)
  161. time.sleep(2)
  162. # 重新连接 uiautomator2
  163. self.driver = u2.connect_usb(self.device_id)
  164. def No_slider(self) -> bool:
  165. """检测当前是否有滑块(默认实现,可覆盖)"""
  166. # 这里用你实际的滑块检测逻辑
  167. # 示例:检测特定图片或UI元素
  168. for i in range(4):
  169. if '检测成功' in '检测成功':
  170. self._solve_slider()
  171. else:
  172. break
  173. return True
  174. return False
  175. def _solve_slider(self):
  176. """解决滑块验证(默认实现,可覆盖"""
  177. num = 0
  178. while(self.driver.xpath('//*[@text="检测到您的账号存在风险,为保障账号安全,请进行安全验证"]').exists or self.driver.xpath('//*[@text="身份安全验证"]').exists):
  179. self.driver.press('back')
  180. time.sleep(0.3)
  181. num+=1
  182. if(num>=6):
  183. break
  184. time.sleep(random.uniform(2, 3))
  185. if self.driver.xpath('//*[@text="检测到您的账号存在风险,为保障账号安全,请进行安全验证"]').exists or self.driver.xpath('//*[@text="身份安全验证"]').exists:
  186. self._solve_slider()
  187. if(self.driver.xpath('//*[@text="验证码"]').exists):
  188. print('其他验证码,无xpath')
  189. self.driver.screenshot(f'test{code}.jpg')
  190. code+=1
  191. # self.driver.screenshot('test.jpg')
  192. #time.sleep(1)
  193. # x= find_template_position('t.jpg','test.jpg')
  194. # self.driver.click(x[0],x[1])
  195. # 这里写你的滑块处理逻辑
  196. # 例如:找到滑块位置,模拟滑动轨迹
  197. pass
  198. def safe_get_all(self,xpath):
  199. try:
  200. return self.driver.xpath(xpath).all()
  201. except Exception as exc:
  202. print(f"获取元素列表失败,已跳过: {xpath}, error: {exc}")
  203. return []
  204. def safe_exists(self,xpath):
  205. try:
  206. return self.driver.xpath(xpath).exists
  207. except Exception as exc:
  208. print(f"xpath不存在或检查失败,已跳过: {xpath}, error: {exc}")
  209. return False
  210. def ele(self,xpath):
  211. try:
  212. return self.driver.xpath(xpath)
  213. except Exception as exc:
  214. print(f"xpath不存在或检查失败,已跳过: {xpath}, error: {exc}")
  215. return False
  216. def safe_get_text(self,xpath):
  217. try:
  218. elem = self.driver.xpath(xpath)
  219. if not elem.exists:
  220. return ""
  221. node = elem.get()
  222. return (node.text or "").strip() if node else ""
  223. except Exception as exc:
  224. print(f"获取文本失败,已跳过: {xpath}, error: {exc}")
  225. return ""
  226. def safe_press_back(self):
  227. try:
  228. self.driver.press('back')
  229. return True
  230. except Exception as exc:
  231. print(f"safe_press_back 返回失败: {exc}")
  232. return False