yzm.py 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398
  1. import base64
  2. import io
  3. import math
  4. import os
  5. import random
  6. import re
  7. import threading
  8. import time
  9. import cv2
  10. import numpy as np
  11. import requests
  12. from PIL import Image, ImageDraw
  13. import uiautomator2 as u2
  14. API_URL = "http://api.jfbym.com/api/YmServer/customApi"
  15. # 验证码平台 token
  16. API_TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
  17. # 原始截图保存路径
  18. SCREENSHOT_PATH = "./a.jpg"
  19. # 裁剪后图片保存路径z
  20. CROP_PATH = "./b.jpg"
  21. # 滑动轨迹图保存目录
  22. TRACK_DIR = "./slider_tracks"
  23. # 验证码截图日志根目录
  24. CAPTCHA_LOG_ROOT = "./captcha_logs"
  25. UNKNOWN_DEVICE_ID = "unknown_device"
  26. _RUNTIME_CONTEXT = threading.local()
  27. SLIDER_METHODS = ("track", "bezier")
  28. # 图标点选验证码
  29. CAPTCHA_ICON_CLICK = "icon_click"
  30. # 空间推理验证码
  31. CAPTCHA_SPACE_REASON = "space_reason"
  32. # 文字点选验证码
  33. CAPTCHA_TEXT_CLICK = "text_click"
  34. # 滑块验证码
  35. CAPTCHA_SLIDER = "slider"
  36. # 图片内容输入验证码
  37. CAPTCHA_TEXT_INPUT = "text_input"
  38. # 输入型验证码的裁剪区域
  39. INPUT_CROP = {
  40. "x_start": 210,
  41. "x_end": 510,
  42. "y_start": 666,
  43. "y_end": 788,
  44. }
  45. # 图标点选验证码的裁剪区域
  46. ICON_CLICK_CROP = {
  47. "x_start": 40,
  48. "x_end": 680,
  49. "y_start": 471,
  50. "y_end": 1143,
  51. }
  52. # 不同验证码类型对应的平台参数配置
  53. VERIFY_CONFIG = {
  54. CAPTCHA_ICON_CLICK: {"type": "88888", "direction": None, "extra": None, "y_offset": 0, "result": "points"},
  55. CAPTCHA_SPACE_REASON: {"type": "88888", "direction": None, "extra": "请点击", "y_offset": 0, "result": "points"},
  56. CAPTCHA_TEXT_CLICK: {"type": "30114", "direction": None, "extra": "phrase", "y_offset": 0, "result": "points"},
  57. CAPTCHA_SLIDER: {"type": "22222", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  58. CAPTCHA_TEXT_INPUT: {"type": "10103", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  59. # lianxian : {"type": "10114", "direction": None, "extra": None, "y_offset": 0, "result": "text"}
  60. }
  61. def _safe_name(value, default):
  62. if value is None:
  63. return default
  64. text = str(value).strip()
  65. if not text:
  66. return default
  67. text = re.sub(r"[^0-9A-Za-z_\-.]+", "_", text)
  68. return text or default
  69. def _resolve_device_id(d=None, device_id=None):
  70. if device_id:
  71. return _safe_name(device_id, UNKNOWN_DEVICE_ID)
  72. candidates = []
  73. if d is not None:
  74. for attr in ("serial", "_serial", "device_id"):
  75. value = getattr(d, attr, None)
  76. if value:
  77. candidates.append(value)
  78. try:
  79. info = d.device_info
  80. if isinstance(info, dict):
  81. for key in ("serial", "serialno", "udid", "deviceId"):
  82. value = info.get(key)
  83. if value:
  84. candidates.append(value)
  85. except Exception:
  86. pass
  87. for value in candidates:
  88. normalized = _safe_name(value, "")
  89. if normalized:
  90. return normalized
  91. return UNKNOWN_DEVICE_ID
  92. def _set_runtime_device_id(d=None, device_id=None):
  93. _RUNTIME_CONTEXT.device_id = _resolve_device_id(d=d, device_id=device_id)
  94. def _get_runtime_device_id(d=None):
  95. current = getattr(_RUNTIME_CONTEXT, "device_id", None)
  96. if current:
  97. return current
  98. resolved = _resolve_device_id(d=d)
  99. _RUNTIME_CONTEXT.device_id = resolved
  100. return resolved
  101. def _next_slider_method_order():
  102. idx = getattr(_RUNTIME_CONTEXT, "slider_method_idx", 0)
  103. first = SLIDER_METHODS[idx % len(SLIDER_METHODS)]
  104. second = SLIDER_METHODS[(idx + 1) % len(SLIDER_METHODS)]
  105. _RUNTIME_CONTEXT.slider_method_idx = (idx + 1) % len(SLIDER_METHODS)
  106. return [first, second]
  107. def _ensure_captcha_dir(captcha_type):
  108. safe_type = _safe_name(captcha_type, "unknown_captcha")
  109. folder = os.path.join(CAPTCHA_LOG_ROOT, safe_type)
  110. os.makedirs(folder, exist_ok=True)
  111. return folder
  112. def _build_captcha_image_path(captcha_type, d=None, ext=".png", tag=None):
  113. device_id = _get_runtime_device_id(d=d)
  114. random_part = random.randint(10000000, 99999999)
  115. safe_tag = _safe_name(tag, "") if tag else ""
  116. suffix = f"_{safe_tag}" if safe_tag else ""
  117. filename = f"{device_id}_{random_part}{suffix}{ext}"
  118. return os.path.join(_ensure_captcha_dir(captcha_type), filename)
  119. def _save_debug_screenshot(d, captcha_type, tag="full"):
  120. path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag=tag)
  121. try:
  122. d.screenshot(path)
  123. print(f"[captcha-shot] saved: {path}")
  124. return path
  125. except Exception as e:
  126. print(f"[captcha-shot] save failed: {e}")
  127. return None
  128. def post_api(image_path, captcha_type, extra=None, direction=None, label_image_path=None, timeout=20):
  129. with open(image_path, 'rb') as f:
  130. image_base64 = base64.b64encode(f.read()).decode()
  131. data = {
  132. "token": API_TOKEN,
  133. "type": captcha_type,
  134. "image": image_base64,
  135. }
  136. if label_image_path:
  137. with open(label_image_path, 'rb') as f:
  138. data["label_image"] = base64.b64encode(f.read()).decode()
  139. if extra is not None:
  140. data["extra"] = extra
  141. if direction is not None:
  142. data["direction"] = direction
  143. headers = {
  144. "Content-Type": "application/json"
  145. }
  146. response = requests.post(API_URL, headers=headers, json=data, timeout=timeout).json()
  147. print(response)
  148. return response
  149. def parse_points(response, y_offset=0):
  150. tuple_points = []
  151. data = response.get("data", {}).get("data", "")
  152. if not data:
  153. return tuple_points
  154. for s in data.split('|'):
  155. x, y = s.split(',')
  156. tuple_points.append((int(x), int(y) + y_offset))
  157. return tuple_points
  158. def verify(image_path, captcha_type):
  159. config = VERIFY_CONFIG.get(captcha_type)
  160. if not config:
  161. raise ValueError(f"不支持的验证码类型: {captcha_type}")
  162. response = post_api(
  163. image_path,
  164. config["type"],
  165. extra=config["extra"],
  166. direction=config["direction"]
  167. )
  168. if config["result"] == "points":
  169. return parse_points(response, y_offset=config["y_offset"])
  170. return response.get("data", {}).get("data")
  171. def crop_image_xy(
  172. image_path,
  173. output_path=None,
  174. x_start=None,
  175. x_end=None,
  176. y_start=471,
  177. y_end=1143
  178. ):
  179. if output_path is None:
  180. dir_name, file_name = os.path.split(image_path)
  181. name, ext = os.path.splitext(file_name)
  182. output_path = os.path.join(dir_name, f"{name}_cropped{ext}")
  183. with Image.open(image_path) as img:
  184. width, height = img.size
  185. if x_start is None:
  186. x_start = 0
  187. if x_end is None:
  188. x_end = width - 1
  189. x_start = max(0, min(x_start, width - 1))
  190. x_end = max(x_start, min(x_end, width - 1))
  191. y_start = max(0, min(y_start, height - 1))
  192. y_end = max(y_start, min(y_end, height - 1))
  193. cropped = img.crop((x_start, y_start, x_end + 1, y_end + 1))
  194. cropped.save(output_path)
  195. return output_path
  196. def _capture_by_bounds(d, xpath_candidates, output_path=None, screenshot_path=None, captcha_type="generic"):
  197. """
  198. 按元素 bounds 截图并裁剪。
  199. return: (cropped_path, bounds_dict) or (None, None)
  200. """
  201. if isinstance(xpath_candidates, str):
  202. xpath_candidates = [xpath_candidates]
  203. if screenshot_path is None:
  204. screenshot_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="full")
  205. if output_path is None:
  206. output_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="crop")
  207. for xpath in xpath_candidates:
  208. try:
  209. node = d.xpath(xpath)
  210. if not node.exists:
  211. continue
  212. bounds = node.info.get("bounds", {})
  213. if not bounds:
  214. continue
  215. d.screenshot(screenshot_path)
  216. cropped = crop_image_xy(
  217. screenshot_path,
  218. output_path=output_path,
  219. x_start=bounds["left"],
  220. x_end=bounds["right"],
  221. y_start=bounds["top"],
  222. y_end=bounds["bottom"],
  223. )
  224. return cropped, bounds
  225. except Exception:
  226. continue
  227. return None, None
  228. def srwz(d):
  229. captcha_image_xpaths = [
  230. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.widget.Image[1]',
  231. '//*[@resource-id="captchaImg"]',
  232. ]
  233. image_path, _ = _capture_by_bounds(d, captcha_image_xpaths, captcha_type=CAPTCHA_TEXT_INPUT)
  234. if not image_path:
  235. return False
  236. data = verify(image_path, CAPTCHA_TEXT_INPUT)
  237. input_box = d.xpath('//*[@hint="请输入验证码"]')
  238. if input_box.exists:
  239. input_box.click()
  240. time.sleep(0.5)
  241. input_box.set_text(data)
  242. d.xpath('//*[@text="验证"]').click()
  243. return True
  244. else:
  245. print("未找到输入框")
  246. return False
  247. def _clamp(value, min_value, max_value):
  248. return max(min_value, min(value, max_value))
  249. def _slider_duration(distance):
  250. if distance <= 90:
  251. return round(random.uniform(0.18, 0.27), 3)
  252. if distance <= 160:
  253. return round(random.uniform(0.23, 0.34), 3)
  254. return round(random.uniform(0.28, 0.42), 3)
  255. def _build_human_slider_track(start_x, start_y, distance):
  256. # Keep the reference shape: mostly flat first, then a single smooth downward bend.
  257. overshoot = random.randint(1, 2) if distance > 140 and random.random() < 0.18 else 0
  258. move_distance = distance + overshoot
  259. steps = int(_clamp(move_distance / random.uniform(6.0, 8.0), 16, 32))
  260. base_y = start_y + random.randint(-1, 1)
  261. flat_ratio = random.uniform(0.30, 0.42) # front section almost horizontal
  262. drop = distance * random.uniform(0.08, 0.14)
  263. if distance > 260:
  264. drop *= random.uniform(1.35, 1.75)
  265. drop = _clamp(drop, 12.0, 58.0) # obvious tail drop
  266. noise_amp = random.uniform(0.03, 0.22)
  267. points = [(start_x, base_y)]
  268. last_x = start_x
  269. for i in range(1, steps + 1):
  270. t = i / steps
  271. progress = 1 - (1 - t) ** 2.0
  272. progress += random.uniform(-0.0018, 0.0018)
  273. progress = _clamp(progress, 0.0, 1.0)
  274. x = start_x + int(move_distance * progress)
  275. if x <= last_x:
  276. x = last_x + 1
  277. last_x = x
  278. if t < flat_ratio:
  279. # slight tiny rise then back, still near flat
  280. u = t / max(flat_ratio, 1e-6)
  281. y_curve = -0.9 * math.sin(math.pi * u)
  282. else:
  283. u = (t - flat_ratio) / max(1 - flat_ratio, 1e-6)
  284. y_curve = drop * (u ** 1.7)
  285. y_noise = random.uniform(-noise_amp, noise_amp)
  286. y = int(round(base_y + y_curve + y_noise))
  287. points.append((x, y))
  288. final_x = start_x + distance
  289. final_y = int(round(base_y + drop + random.uniform(-0.6, 0.6)))
  290. if overshoot > 0:
  291. points.append((start_x + distance + overshoot, final_y))
  292. points.append((final_x, final_y + random.choice([0, 0, 1])))
  293. return points
  294. def _save_slider_track_image(
  295. points,
  296. distance=None,
  297. duration=None,
  298. drag_ok=True,
  299. screenshot_path=SCREENSHOT_PATH,
  300. captcha_type=CAPTCHA_SLIDER
  301. ):
  302. if not points:
  303. return None
  304. output_path = _build_captcha_image_path(captcha_type, ext=".png", tag="track")
  305. try:
  306. if os.path.exists(screenshot_path):
  307. with Image.open(screenshot_path) as img:
  308. canvas = img.convert("RGB")
  309. else:
  310. canvas = Image.new("RGB", (720, 1280), "white")
  311. draw = ImageDraw.Draw(canvas)
  312. if len(points) >= 2:
  313. draw.line(points, fill=(245, 20, 30), width=9)
  314. sx, sy = points[0]
  315. ex, ey = points[-1]
  316. draw.ellipse((sx - 5, sy - 5, sx + 5, sy + 5), fill=(40, 200, 80))
  317. draw.ellipse((ex - 5, ey - 5, ex + 5, ey + 5), fill=(50, 120, 255))
  318. info = f"ok={drag_ok} dist={distance} dur={duration}s points={len(points)}"
  319. draw.rectangle((8, 8, min(canvas.size[0] - 8, 520), 42), fill=(0, 0, 0))
  320. draw.text((14, 14), info, fill=(255, 255, 255))
  321. canvas.save(output_path)
  322. print(f"[slider-track] saved: {output_path}")
  323. return output_path
  324. except Exception as e:
  325. print(f"[slider-track] save failed: {e}")
  326. return None
  327. def _downsample_track_points(points, target_count):
  328. if not points or len(points) <= target_count:
  329. return points[:]
  330. if target_count < 2:
  331. return [points[0], points[-1]]
  332. sampled = []
  333. last_index = len(points) - 1
  334. for i in range(target_count):
  335. idx = int(round(i * last_index / (target_count - 1)))
  336. sampled.append(points[idx])
  337. return sampled
  338. def _execute_track(d, points, total_duration):
  339. if not points or len(points) < 2:
  340. return False, points
  341. duration = max(0.18, float(total_duration))
  342. # Use fewer points to avoid step explosion, keep curve shape.
  343. max_points = random.randint(14, 22)
  344. exec_points = _downsample_track_points(points, max_points)
  345. try:
  346. if hasattr(d, "swipe_points"):
  347. # u2: duration here means time-per-step; steps = duration/0.005.
  348. # To approximate total duration:
  349. # total ~= (duration/0.005) * (len(exec_points)-1) * 0.005 = duration * segments
  350. seg_count = max(1, len(exec_points) - 1)
  351. per_segment = max(0.01, duration / seg_count)
  352. d.swipe_points(exec_points, duration=per_segment)
  353. return True, exec_points
  354. except Exception:
  355. pass
  356. # fallback
  357. try:
  358. sx, sy = exec_points[0]
  359. ex, ey = exec_points[-1]
  360. d.swipe(sx, sy, ex, ey, duration=duration)
  361. return False, exec_points
  362. except Exception:
  363. return False, exec_points
  364. def _slider_still_exists(d):
  365. xpath_candidates = [
  366. '//*[@text="请拖动下方滑块完成拼图"]',
  367. '//*[contains(@text, "拖动下方滑块")]',
  368. '//*[@resource-id="puzzleSliderBox"]',
  369. '//*[@resource-id="puzzleImageMain"]',
  370. ]
  371. for xpath in xpath_candidates:
  372. try:
  373. if d.xpath(xpath).exists:
  374. return True
  375. except Exception:
  376. continue
  377. return False
  378. def _cubic_bezier(t, p0, p1, p2, p3):
  379. one_minus_t = 1 - t
  380. x = (
  381. one_minus_t ** 3 * p0[0]
  382. + 3 * one_minus_t ** 2 * t * p1[0]
  383. + 3 * one_minus_t * t ** 2 * p2[0]
  384. + t ** 3 * p3[0]
  385. )
  386. y = (
  387. one_minus_t ** 3 * p0[1]
  388. + 3 * one_minus_t ** 2 * t * p1[1]
  389. + 3 * one_minus_t * t ** 2 * p2[1]
  390. + t ** 3 * p3[1]
  391. )
  392. return x, y
  393. def _generate_bezier_slider_points(start, end, deviation=30, steps=50):
  394. sx, sy = start
  395. ex, ey = end
  396. mid_x = (sx + ex) / 2
  397. mid_y = (sy + ey) / 2
  398. p1 = (
  399. mid_x - (ex - sx) / 4 + random.uniform(-deviation, deviation),
  400. mid_y - (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  401. )
  402. p2 = (
  403. mid_x + (ex - sx) / 4 + random.uniform(-deviation, deviation),
  404. mid_y + (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  405. )
  406. points = []
  407. for i in range(steps + 1):
  408. t = i / steps
  409. x, y = _cubic_bezier(t, start, p1, p2, end)
  410. if 0 < i < steps:
  411. x += random.gauss(0, 1.5)
  412. y += random.gauss(0, 1.5)
  413. points.append((int(round(x)), int(round(y))))
  414. return points
  415. def _execute_bezier_slider(d, start_x, start_y, end_x, end_y):
  416. points = _generate_bezier_slider_points(
  417. (int(round(start_x)), int(round(start_y))),
  418. (int(round(end_x)), int(round(end_y))),
  419. deviation=random.randint(20, 40),
  420. steps=50,
  421. )
  422. if len(points) < 2:
  423. return False, points
  424. try:
  425. d.touch.down(points[0][0], points[0][1])
  426. time.sleep(random.uniform(0.1, 0.2))
  427. total = max(1, len(points) - 1)
  428. for i, (x, y) in enumerate(points[1:], 1):
  429. d.touch.move(x, y)
  430. t = i / total
  431. if 0.2 < t < 0.8:
  432. interval = random.uniform(0.02, 0.04)
  433. else:
  434. interval = random.uniform(0.04, 0.08)
  435. time.sleep(interval)
  436. time.sleep(random.uniform(0.05, 0.15))
  437. d.touch.up(points[-1][0], points[-1][1])
  438. return True, points
  439. except Exception:
  440. try:
  441. d.touch.up(points[-1][0], points[-1][1])
  442. except Exception:
  443. pass
  444. return False, points
  445. def _slider_knob_center(d):
  446. slider_xpath = (
  447. '//*[@resource-id="puzzleSliderBox"] | '
  448. '//*[@resource-id="yodaBox"] | '
  449. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]'
  450. )
  451. try:
  452. slider_node = d.xpath(slider_xpath)
  453. if slider_node.exists:
  454. bounds = slider_node.info.get("bounds", {})
  455. if bounds:
  456. return (
  457. (bounds["left"] + bounds["right"]) / 2 + random.uniform(-4, 4),
  458. (bounds["top"] + bounds["bottom"]) / 2 + random.uniform(-3, 3),
  459. )
  460. except Exception:
  461. pass
  462. return None
  463. def _first_existing_bounds(d, xpath_candidates):
  464. if isinstance(xpath_candidates, str):
  465. xpath_candidates = [xpath_candidates]
  466. for xpath in xpath_candidates:
  467. try:
  468. node = d.xpath(xpath)
  469. if not node.exists:
  470. continue
  471. bounds = node.info.get("bounds", {})
  472. if bounds:
  473. return xpath, bounds
  474. except Exception:
  475. continue
  476. return None, None
  477. def _click_captcha_close(d, captcha_xpath=None):
  478. """点击验证码右上角关闭按钮;优先点显式关闭控件,失败后按容器右上角估算点位。"""
  479. close_xpaths = [
  480. '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]',
  481. '//*[@resource-id="btn_close_verify"]',
  482. '//*[@content-desc="关闭"]',
  483. '//*[@text="关闭"]',
  484. ]
  485. _, close_bounds = _first_existing_bounds(d, close_xpaths)
  486. if close_bounds:
  487. cx = int((close_bounds["left"] + close_bounds["right"]) / 2) + random.randint(-2, 2)
  488. cy = int((close_bounds["top"] + close_bounds["bottom"]) / 2) + random.randint(-2, 2)
  489. d.click(cx, cy)
  490. print(f"[captcha-close] click explicit close at ({cx}, {cy})")
  491. return True
  492. popup_xpaths = [
  493. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  494. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  495. '//*[@resource-id="root"]',
  496. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]',
  497. '//*[@text="身份核实"]/android.view.View[1]',
  498. ]
  499. if captcha_xpath:
  500. popup_xpaths.append(captcha_xpath)
  501. _, popup_bounds = _first_existing_bounds(d, popup_xpaths)
  502. if not popup_bounds:
  503. return False
  504. left = popup_bounds["left"]
  505. right = popup_bounds["right"]
  506. top = popup_bounds["top"]
  507. bottom = popup_bounds["bottom"]
  508. width = max(1, right - left)
  509. height = max(1, bottom - top)
  510. # 参考示例:[40,391][680,1223] -> 右上角叉号中心约(640, 431)。
  511. offset_x = int(_clamp(width * 0.06, 20, 56))
  512. offset_y = int(_clamp(height * 0.05, 20, 56))
  513. click_x = int(right - offset_x) + random.randint(-3, 3)
  514. click_y = int(top + offset_y) + random.randint(-3, 3)
  515. d.click(click_x, click_y)
  516. print(f"[captcha-close] click inferred close at ({click_x}, {click_y})")
  517. return True
  518. def _build_directional_track(start_x, start_y, end_x, end_y):
  519. distance_x = end_x - start_x
  520. distance_y = end_y - start_y
  521. if abs(distance_x) < 2 and abs(distance_y) < 2:
  522. return [(int(start_x), int(start_y)), (int(end_x), int(end_y))]
  523. steps = int(_clamp(abs(distance_x) / random.uniform(7.0, 10.0), 22, 48))
  524. points = [(int(start_x), int(start_y))]
  525. last_x = float(start_x)
  526. for i in range(1, steps + 1):
  527. t = i / steps
  528. progress = 1 - (1 - t) ** random.uniform(1.8, 2.25)
  529. x = start_x + distance_x * progress + random.uniform(-0.9, 0.9)
  530. y = start_y + distance_y * progress + random.uniform(-0.8, 0.8)
  531. if distance_x >= 0:
  532. if x < last_x:
  533. x = last_x + random.uniform(0.2, 1.2)
  534. else:
  535. if x > last_x:
  536. x = last_x - random.uniform(0.2, 1.2)
  537. last_x = x
  538. points.append((int(round(x)), int(round(y))))
  539. points.append((int(round(end_x)), int(round(end_y))))
  540. return points
  541. def _move_with_pressed_touch(d, points):
  542. if not points:
  543. return
  544. for x, y in points:
  545. d.touch.move(x, y)
  546. time.sleep(random.uniform(0.0015, 0.0045))
  547. def _screenshot_to_image(d):
  548. shot = d.screenshot()
  549. if isinstance(shot, Image.Image):
  550. return shot.convert("RGB")
  551. if isinstance(shot, bytes):
  552. return Image.open(io.BytesIO(shot)).convert("RGB")
  553. if isinstance(shot, str) and os.path.exists(shot):
  554. return Image.open(shot).convert("RGB")
  555. if hasattr(shot, "convert"):
  556. return shot.convert("RGB")
  557. fallback_path = _build_captcha_image_path("generic", d=d, ext=".png", tag="fallback")
  558. d.screenshot(fallback_path)
  559. return Image.open(fallback_path).convert("RGB")
  560. def hk(d):
  561. screenshot_path = _build_captcha_image_path(CAPTCHA_SLIDER, d=d, ext=".png", tag="full")
  562. d.screenshot(screenshot_path)
  563. data = verify(screenshot_path, CAPTCHA_SLIDER)
  564. if not data:
  565. return False
  566. try:
  567. raw_distance = float(data)
  568. except (TypeError, ValueError):
  569. return False
  570. if raw_distance <= 0:
  571. return False
  572. image_width = 720
  573. try:
  574. with Image.open(screenshot_path) as img:
  575. image_width = img.size[0] or 720
  576. except Exception:
  577. pass
  578. try:
  579. screen_width = int(d.info.get("displayWidth", image_width))
  580. except Exception:
  581. screen_width = image_width
  582. scale = screen_width / image_width if image_width else 1.0
  583. slide_distance = int(raw_distance * scale)
  584. if slide_distance < 80:
  585. slide_distance += random.randint(3, 6)
  586. elif slide_distance < 160:
  587. slide_distance += random.randint(2, 5)
  588. else:
  589. slide_distance += random.randint(1, 4)
  590. start_x = 84 + random.randint(-1, 1)
  591. start_y = 1034 + random.randint(-2, 2)
  592. knob_center = _slider_knob_center(d)
  593. if knob_center:
  594. start_x, start_y = knob_center
  595. max_target_x = screen_width - random.randint(26, 42)
  596. target_x = _clamp(start_x + slide_distance, start_x + 18, max_target_x)
  597. distance = target_x - start_x
  598. if distance < 18:
  599. return False
  600. method_order = _next_slider_method_order()
  601. for method in method_order:
  602. if method == "track":
  603. points = _build_human_slider_track(int(round(start_x)), int(round(start_y)), int(round(distance)))
  604. duration = _slider_duration(distance)
  605. time.sleep(random.uniform(0.015, 0.05))
  606. drag_ok, exec_points = _execute_track(d, points, duration)
  607. _save_slider_track_image(
  608. exec_points,
  609. distance=distance,
  610. duration=duration,
  611. drag_ok=drag_ok,
  612. screenshot_path=screenshot_path,
  613. captcha_type=CAPTCHA_SLIDER
  614. )
  615. else:
  616. end_x = target_x + random.uniform(-3, 3)
  617. end_y = start_y + random.uniform(-1, 1)
  618. drag_ok, exec_points = _execute_bezier_slider(d, start_x, start_y, end_x, end_y)
  619. _save_slider_track_image(
  620. exec_points,
  621. distance=distance,
  622. duration=None,
  623. drag_ok=drag_ok,
  624. screenshot_path=screenshot_path,
  625. captcha_type=CAPTCHA_SLIDER
  626. )
  627. time.sleep(random.uniform(1.0, 1.8))
  628. if not _slider_still_exists(d):
  629. return True
  630. print(f"[slider] method {method} failed, switch to next")
  631. return False
  632. def dianxuan(d):
  633. click_area_xpaths = [
  634. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  635. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  636. '//*[@resource-id="root"]',
  637. ]
  638. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_ICON_CLICK)
  639. if not image_path or not bounds:
  640. return False
  641. left = bounds["left"]
  642. top = bounds["top"]
  643. data = verify(image_path, CAPTCHA_ICON_CLICK)
  644. if not data:
  645. return False
  646. for x, y in data:
  647. time.sleep(random.randint(1, 2))
  648. d.click(left + x + random.randint(-7, 7), top + y + random.randint(-7, 7))
  649. return True
  650. def wenzidianxuan(d):
  651. # 文字点选:按验证码容器 bounds 裁剪后,调用 88888,按返回坐标依次点击
  652. click_area_xpaths = [
  653. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  654. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  655. '//*[@resource-id="root"]',
  656. ]
  657. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_TEXT_CLICK)
  658. if not image_path or not bounds:
  659. return False
  660. image_left = bounds["left"]
  661. image_top = bounds["top"]
  662. result = post_api(image_path, "88888")
  663. verify_data = result.get("data", {})
  664. if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  665. return False
  666. coords_str = verify_data.get("data", "")
  667. if not coords_str:
  668. return False
  669. clicked = 0
  670. for coord in coords_str.split("|"):
  671. try:
  672. x_img_str, y_img_str = coord.split(",")
  673. x_img = int(x_img_str.strip())
  674. y_img = int(y_img_str.strip())
  675. x_screen = image_left + x_img + random.randint(-5, 5)
  676. y_screen = image_top + y_img + random.randint(-5, 5)
  677. d.click(x_screen, y_screen)
  678. clicked += 1
  679. time.sleep(random.uniform(0.8, 1.6))
  680. except Exception:
  681. continue
  682. return clicked > 0
  683. def click_side(d):
  684. """空间推理验证码(请点击数字)。"""
  685. click_area_xpaths = [
  686. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  687. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  688. '//*[@resource-id="root"]',
  689. ]
  690. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, output_path=CROP_PATH)
  691. if not image_path or not bounds:
  692. return False
  693. left = bounds["left"]
  694. top = bounds["top"]
  695. points = verify(image_path, CAPTCHA_SPACE_REASON)
  696. if not points:
  697. return False
  698. x, y = points[0]
  699. d.click(left + x + random.randint(-2, 2), top + y + random.randint(-2, 2))
  700. # d_list = [
  701. # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]',
  702. # '//*[@resource-id="com.sankuai.meituan:id/yoda_toolbar_title"]',
  703. # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]'
  704. # ]
  705. # for i in d_list:
  706. # d.xpath(i).click()
  707. return True
  708. def Swipe_right(d):
  709. """向右拖动到最右侧(非拼图滑块)。"""
  710. track_xpath = (
  711. '//*[@resource-id="yodaBoxWrapper"] | '
  712. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  713. )
  714. slider_xpath = (
  715. '//*[@resource-id="yodaBox"] | '
  716. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  717. )
  718. if not d.xpath(track_xpath).exists or not d.xpath(slider_xpath).exists:
  719. return False
  720. track_bounds = d.xpath(track_xpath).info.get("bounds", {})
  721. slider_bounds = d.xpath(slider_xpath).info.get("bounds", {})
  722. if not track_bounds or not slider_bounds:
  723. return False
  724. start_x = int((slider_bounds["left"] + slider_bounds["right"]) / 2) + random.randint(-2, 2)
  725. start_y = int((slider_bounds["top"] + slider_bounds["bottom"]) / 2) + random.randint(-2, 2)
  726. right_limit = int(track_bounds["right"]) - random.randint(4, 10)
  727. distance = right_limit - start_x
  728. if distance <= 10:
  729. return False
  730. screenshot_path = _save_debug_screenshot(d, "Swipe_right", tag="full")
  731. points = _build_human_slider_track(start_x, start_y, distance)
  732. duration = _slider_duration(distance)
  733. drag_ok, exec_points = _execute_track(d, points, duration)
  734. _save_slider_track_image(
  735. exec_points,
  736. distance=distance,
  737. duration=duration,
  738. drag_ok=drag_ok,
  739. screenshot_path=screenshot_path or SCREENSHOT_PATH,
  740. captcha_type="Swipe_right"
  741. )
  742. return True
  743. def complexs(d):
  744. """Complex slider flow: move to far-right, OCR with label image, then drag back to target."""
  745. slider_xpath_candidates = [
  746. '//*[@resource-id="yodaBox"]',
  747. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]',
  748. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  749. ]
  750. track_xpath_candidates = [
  751. '//*[@resource-id="yodaBoxWrapper"]',
  752. '//*[contains(@text, "请按照说明拖动滑块")]',
  753. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  754. ]
  755. label_xpath_candidates = [
  756. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.widget.TextView[1]',
  757. ]
  758. image_xpath_candidates = [
  759. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  760. ]
  761. _, slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  762. _, track_bounds = _first_existing_bounds(d, track_xpath_candidates)
  763. if not slider_bounds or not track_bounds:
  764. return False
  765. slider_left = slider_bounds["left"]
  766. slider_top = slider_bounds["top"]
  767. slider_right = slider_bounds["right"]
  768. slider_bottom = slider_bounds["bottom"]
  769. slider_width = slider_right - slider_left
  770. slider_center_x = (slider_left + slider_right) / 2
  771. slider_center_y = (slider_top + slider_bottom) / 2
  772. track_left = track_bounds["left"]
  773. track_right = track_bounds["right"]
  774. right_end_center_x = track_right - slider_width / 2
  775. right_end_center_y = slider_center_y
  776. print(f"滑块中心: ({slider_center_x}, {slider_center_y})")
  777. print(f"最右端滑块中心坐标: ({right_end_center_x}, {right_end_center_y})")
  778. touch_down = False
  779. try:
  780. d.touch.down(slider_center_x, slider_center_y)
  781. touch_down = True
  782. time.sleep(random.uniform(0.08, 0.16))
  783. move_right_points = _build_directional_track(
  784. slider_center_x,
  785. slider_center_y,
  786. right_end_center_x,
  787. right_end_center_y,
  788. )
  789. _move_with_pressed_touch(d, move_right_points[1:])
  790. print("滑块已到达最右端")
  791. _, label_bounds = _first_existing_bounds(d, label_xpath_candidates)
  792. _, image_bounds = _first_existing_bounds(d, image_xpath_candidates)
  793. if not label_bounds or not image_bounds:
  794. return False
  795. capture_label_left = label_bounds["left"]
  796. capture_label_top = label_bounds["top"]
  797. capture_label_right = label_bounds["right"]
  798. capture_label_bottom = label_bounds["bottom"]
  799. capture_left = image_bounds["left"]
  800. capture_top = image_bounds["top"]
  801. capture_right = image_bounds["right"]
  802. capture_bottom = image_bounds["bottom"]
  803. print(
  804. "截图区域1(提示文本): "
  805. f"left={capture_label_left}, top={capture_label_top}, "
  806. f"width={capture_label_right - capture_label_left}, "
  807. f"height={capture_label_bottom - capture_label_top}"
  808. )
  809. print(
  810. "截图区域2(图片): "
  811. f"left={capture_left}, top={capture_top}, "
  812. f"width={capture_right - capture_left}, "
  813. f"height={capture_bottom - capture_top}"
  814. )
  815. screenshot_label_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="label")
  816. screenshot_image_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="image")
  817. image = _screenshot_to_image(d)
  818. image.crop(
  819. (capture_label_left, capture_label_top, capture_label_right, capture_label_bottom)
  820. ).save(screenshot_label_path)
  821. image.crop(
  822. (capture_left, capture_top, capture_right, capture_bottom)
  823. ).save(screenshot_image_path)
  824. print(f"截图1已保存: {screenshot_label_path}")
  825. print(f"截图2已保存: {screenshot_image_path}")
  826. result = post_api(
  827. screenshot_image_path,
  828. "29013",
  829. label_image_path=screenshot_label_path,
  830. timeout=30,
  831. )
  832. print(f"API返回结果: {result}")
  833. verify_data = result.get("data", {})
  834. print(f"verify_data={verify_data}")
  835. if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  836. return False
  837. data_str = verify_data.get("data", "")
  838. if not data_str:
  839. return False
  840. data_value = int(data_str)
  841. print(f"云码返回的像素距离: {data_value}")
  842. slider_target_center_x = track_left + data_value
  843. min_x = track_left + slider_width / 2
  844. max_x = track_right - slider_width / 2
  845. slider_target_center_x = _clamp(slider_target_center_x, min_x, max_x)
  846. print(f"滑块中心目标X坐标: {slider_target_center_x}")
  847. _, current_slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  848. if current_slider_bounds:
  849. current_slider_center_x = (current_slider_bounds["left"] + current_slider_bounds["right"]) / 2
  850. else:
  851. current_slider_center_x = right_end_center_x
  852. actual_distance = slider_target_center_x - current_slider_center_x
  853. print(f"实际需要滑动的距离: {actual_distance}")
  854. back_points = _build_directional_track(
  855. current_slider_center_x,
  856. right_end_center_y,
  857. slider_target_center_x,
  858. right_end_center_y,
  859. )
  860. _move_with_pressed_touch(d, back_points[1:])
  861. time.sleep(random.uniform(0.2, 0.4))
  862. d.touch.up(slider_target_center_x, right_end_center_y)
  863. touch_down = False
  864. time.sleep(random.uniform(1.8, 3.2))
  865. return True
  866. except Exception as e:
  867. print(f"complex captcha failed: {e}")
  868. return False
  869. finally:
  870. if touch_down:
  871. try:
  872. d.touch.up(right_end_center_x, right_end_center_y)
  873. except Exception:
  874. pass
  875. def Numbers_English_verify(d):
  876. return srwz(d)
  877. def slider_verify(d):
  878. return hk(d)
  879. def Click_images(d):
  880. # 两种点选入口统一处理
  881. if d.xpath('//*[@text="请按语序依次点击下图文字"]').exists:
  882. return wenzidianxuan(d)
  883. return dianxuan(d)
  884. def Shortest_connection(d):
  885. return lianxian(d)
  886. def _handle_generic_captcha(d, xpath, timeout=60):
  887. """通用验证码处理:等待人工处理完成。"""
  888. start = time.time()
  889. while time.time() - start < timeout:
  890. if xpath and not d.xpath(xpath).exists:
  891. return True
  892. time.sleep(1)
  893. return False
  894. def handle_captcha(d, captcha_type, xpath=None, device_id=None):
  895. _set_runtime_device_id(d=d, device_id=device_id)
  896. handlers = {
  897. "Numbers_English": Numbers_English_verify,
  898. "Swipe_right": Swipe_right,
  899. "Click_images": Click_images,
  900. "slider": slider_verify,
  901. "complexs": complexs,
  902. "Shortest_connection": Shortest_connection,
  903. "click_side": click_side,
  904. }
  905. func = handlers.get(captcha_type)
  906. if func is None:
  907. return _handle_generic_captcha(d, xpath)
  908. return func(d)
  909. def _extract_color_name(api_result):
  910. if not isinstance(api_result, dict):
  911. return ""
  912. if api_result.get("code") == 0 and isinstance(api_result.get("data"), str):
  913. return api_result.get("data", "").strip()
  914. if api_result.get("code") == 10000:
  915. inner = api_result.get("data")
  916. if isinstance(inner, dict) and inner.get("code") == 0:
  917. return str(inner.get("data", "")).strip()
  918. if isinstance(inner, str):
  919. return inner.strip()
  920. return ""
  921. def _normalize_color_name(color_name):
  922. if not color_name:
  923. return ""
  924. alias = {
  925. "红": "红色",
  926. "红的": "红色",
  927. "绿": "绿色",
  928. "蓝": "蓝色",
  929. "黄": "黄色",
  930. "橙": "橙色",
  931. "紫": "紫色",
  932. "黑": "黑色",
  933. "白": "白色",
  934. "棕": "棕色",
  935. "褐": "褐色",
  936. }
  937. if color_name in alias:
  938. return alias[color_name]
  939. for k, v in alias.items():
  940. if k in color_name:
  941. return v
  942. return color_name
  943. def _find_color_coordinates(image_path, color_name):
  944. color_name = _normalize_color_name(color_name)
  945. color_ranges = {
  946. "红色": (([0, 120, 70], [10, 255, 255]), ([170, 120, 70], [180, 255, 255])),
  947. "绿色": (([35, 50, 50], [85, 255, 255]),),
  948. "蓝色": (([90, 50, 50], [130, 255, 255]),),
  949. "黄色": (([20, 100, 100], [30, 255, 255]),),
  950. "橙色": (([5, 100, 100], [18, 255, 255]),),
  951. "紫色": (([130, 50, 50], [165, 255, 255]),),
  952. "黑色": (([0, 0, 0], [180, 255, 50]),),
  953. "白色": (([0, 0, 200], [180, 35, 255]),),
  954. "棕色": (([8, 60, 20], [20, 255, 180]),),
  955. "褐色": (([8, 60, 20], [20, 255, 180]),),
  956. }
  957. if color_name not in color_ranges:
  958. return []
  959. image = cv2.imread(image_path)
  960. if image is None:
  961. return []
  962. hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
  963. ranges = color_ranges[color_name]
  964. if len(ranges) == 2:
  965. lower1 = np.array(ranges[0][0])
  966. upper1 = np.array(ranges[0][1])
  967. lower2 = np.array(ranges[1][0])
  968. upper2 = np.array(ranges[1][1])
  969. mask = cv2.bitwise_or(cv2.inRange(hsv, lower1, upper1), cv2.inRange(hsv, lower2, upper2))
  970. else:
  971. lower = np.array(ranges[0][0])
  972. upper = np.array(ranges[0][1])
  973. mask = cv2.inRange(hsv, lower, upper)
  974. kernel = np.ones((3, 3), np.uint8)
  975. mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  976. mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  977. contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  978. coordinates = []
  979. for contour in contours:
  980. area = cv2.contourArea(contour)
  981. if area < 30:
  982. continue
  983. m = cv2.moments(contour)
  984. if m["m00"] == 0:
  985. continue
  986. cx = int(m["m10"] / m["m00"])
  987. cy = int(m["m01"] / m["m00"])
  988. coordinates.append((cx, cy))
  989. return coordinates
  990. def _nearest_neighbor_path(points):
  991. if not points:
  992. return []
  993. if len(points) <= 2:
  994. return points[:]
  995. unvisited = points[:]
  996. path = [unvisited.pop(0)]
  997. while unvisited:
  998. last_x, last_y = path[-1]
  999. idx = min(
  1000. range(len(unvisited)),
  1001. key=lambda i: math.hypot(last_x - unvisited[i][0], last_y - unvisited[i][1])
  1002. )
  1003. path.append(unvisited.pop(idx))
  1004. return path
  1005. def _human_like_path(points):
  1006. if len(points) < 2:
  1007. return points[:]
  1008. curved = []
  1009. for i in range(len(points) - 1):
  1010. start = points[i]
  1011. end = points[i + 1]
  1012. mid_x = (start[0] + end[0]) / 2
  1013. mid_y = (start[1] + end[1]) / 2
  1014. if abs(end[0] - start[0]) > abs(end[1] - start[1]):
  1015. offset_x = 0
  1016. offset_y = random.uniform(-15, 15)
  1017. else:
  1018. offset_x = random.uniform(-15, 15)
  1019. offset_y = 0
  1020. control_x = mid_x + offset_x
  1021. control_y = mid_y + offset_y
  1022. curved.append(start)
  1023. for t in np.arange(0.1, 1.0, 0.1):
  1024. x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control_x + t ** 2 * end[0]
  1025. y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control_y + t ** 2 * end[1]
  1026. curved.append((int(x), int(y)))
  1027. curved.append(points[-1])
  1028. return curved
  1029. def _simulate_human_drawing(d, path):
  1030. if len(path) < 2:
  1031. return False
  1032. try:
  1033. sx, sy = path[0]
  1034. d.touch.down(sx, sy)
  1035. time.sleep(random.uniform(0.05, 0.1))
  1036. for i in range(1, len(path)):
  1037. x, y = path[i]
  1038. d.touch.move(x + random.randint(-2, 2), y + random.randint(-2, 2))
  1039. time.sleep(random.uniform(0.01, 0.03))
  1040. time.sleep(random.uniform(0.1, 0.2))
  1041. d.touch.up(path[-1][0], path[-1][1])
  1042. return True
  1043. except Exception:
  1044. return False
  1045. def retry_captcha(
  1046. d,
  1047. xpath_text,
  1048. handle_func,
  1049. retry_count=5,
  1050. captcha_name=None,
  1051. allow_close_on_third_fail=True,
  1052. fail_limit_before_close=3
  1053. ):
  1054. # 如果当前页面存在对应验证码,就循环重试处理
  1055. if d.xpath(xpath_text).exists:
  1056. current_name = captcha_name or getattr(handle_func, "__name__", "captcha")
  1057. fail_streak = 0
  1058. for _ in range(retry_count):
  1059. if not d.xpath(xpath_text).exists:
  1060. break
  1061. _save_debug_screenshot(d, current_name, tag="full")
  1062. try:
  1063. handle_func(d)
  1064. except Exception as e:
  1065. print(f"[captcha] {current_name} handler error: {e}")
  1066. time.sleep(3)
  1067. # 验证码消失了,说明处理成功,直接退出
  1068. if not d.xpath(xpath_text).exists:
  1069. break
  1070. fail_streak += 1
  1071. if allow_close_on_third_fail and fail_streak >= fail_limit_before_close:
  1072. closed = _click_captcha_close(d, captcha_xpath=xpath_text)
  1073. print(f"[captcha] {current_name} failed {fail_streak} times, switch captcha: {closed}")
  1074. fail_streak = 0
  1075. time.sleep(1.2)
  1076. def yzm(d=None, device_id=None):
  1077. # 如果没有传设备对象,就默认连接当前设备
  1078. if d is None:
  1079. d = u2.connect()
  1080. _set_runtime_device_id(d=d, device_id=device_id)
  1081. # 向右滑动验证码
  1082. retry_captcha(
  1083. d,
  1084. '//*[contains(@text, "请向右滑动滑块")]',
  1085. Swipe_right,
  1086. captcha_name="Swipe_right",
  1087. allow_close_on_third_fail=False
  1088. )
  1089. # 滑块验证码
  1090. retry_captcha(
  1091. d,
  1092. '//*[@text="请拖动下方滑块完成拼图"]',
  1093. hk,
  1094. captcha_name="slider",
  1095. )
  1096. # 空间推理验证码
  1097. retry_captcha(
  1098. d,
  1099. '//*[contains(@text, "请点击")]',
  1100. click_side,
  1101. captcha_name="click_side",
  1102. allow_close_on_third_fail = False
  1103. )
  1104. # 复杂拖动滑块验证码
  1105. retry_captcha(
  1106. d,
  1107. '//*[contains(@text, "拖动滑块")]',
  1108. complexs,
  1109. captcha_name="complexs",
  1110. allow_close_on_third_fail=False
  1111. )
  1112. # 输入型验证码
  1113. retry_captcha(d, '//*[@text="请输入图片中的内容"]', srwz, captcha_name="text_input")
  1114. # 图标点选验证码
  1115. retry_captcha(d, '//*[@text="请依次点击下图图标"]', dianxuan, captcha_name="icon_click")
  1116. # 文字点选验证码
  1117. retry_captcha(d, '//*[@text="请按语序依次点击下图文字"]', wenzidianxuan, captcha_name="text_click")
  1118. # 最短线连接验证码
  1119. retry_captcha(d, '//*[contains(@text, "用最短线连接")]', lianxian, captcha_name="Shortest_connection")
  1120. def lianxian(d):
  1121. art_text_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  1122. color_points_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]/android.widget.Image[1]'
  1123. art_text_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="art_text")
  1124. color_points_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="color_points")
  1125. art_text_img_path, _ = _capture_by_bounds(
  1126. d,
  1127. art_text_xpath,
  1128. output_path=art_text_img_path,
  1129. captcha_type="Shortest_connection"
  1130. )
  1131. color_points_img_path, color_bounds = _capture_by_bounds(
  1132. d,
  1133. color_points_xpath,
  1134. output_path=color_points_img_path,
  1135. captcha_type="Shortest_connection"
  1136. )
  1137. if not art_text_img_path or not color_points_img_path or not color_bounds:
  1138. return False
  1139. element_left = color_bounds["left"]
  1140. element_top = color_bounds["top"]
  1141. element_width = color_bounds["right"] - color_bounds["left"]
  1142. element_height = color_bounds["bottom"] - color_bounds["top"]
  1143. api_result = post_api(art_text_img_path, "10118")
  1144. color_name = _extract_color_name(api_result)
  1145. if not color_name:
  1146. return False
  1147. relative_points = _find_color_coordinates(color_points_img_path, color_name)
  1148. if len(relative_points) < 2:
  1149. return False
  1150. color_img = cv2.imread(color_points_img_path)
  1151. if color_img is None:
  1152. return False
  1153. img_h, img_w = color_img.shape[:2]
  1154. if img_w <= 0 or img_h <= 0:
  1155. return False
  1156. screen_points = []
  1157. for rx, ry in relative_points:
  1158. sx = element_left + int(rx * (element_width / img_w))
  1159. sy = element_top + int(ry * (element_height / img_h))
  1160. screen_points.append((sx, sy))
  1161. path = _nearest_neighbor_path(screen_points)
  1162. curved = _human_like_path(path)
  1163. return _simulate_human_drawing(d, curved)
  1164. if __name__ == '__main__':
  1165. d = u2.connect("GQIRKB7LVOONM7VW")
  1166. yzm(d)