yzm.py 89 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706
  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. #
  10. # import cv2
  11. # import numpy as np
  12. # import requests
  13. # from PIL import Image, ImageDraw
  14. # import uiautomator2 as u2
  15. #
  16. #
  17. # API_URL = "http://api.jfbym.com/api/YmServer/customApi"
  18. # # 验证码平台 token
  19. # API_TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
  20. # # 原始截图保存路径
  21. # SCREENSHOT_PATH = "./a.jpg"
  22. # # 裁剪后图片保存路径
  23. # CROP_PATH = "./b.jpg"
  24. # # 滑动轨迹图保存目录
  25. # TRACK_DIR = "./slider_tracks"
  26. # # 验证码截图日志根目录
  27. # CAPTCHA_LOG_ROOT = "./captcha_logs"
  28. # UNKNOWN_DEVICE_ID = "unknown_device"
  29. # _RUNTIME_CONTEXT = threading.local()
  30. # SLIDER_METHODS = ("track", "bezier")
  31. #
  32. # # 图标点选验证码
  33. # CAPTCHA_ICON_CLICK = "icon_click"
  34. # # 空间推理验证码
  35. # CAPTCHA_SPACE_REASON = "space_reason"
  36. # # 文字点选验证码
  37. # CAPTCHA_TEXT_CLICK = "text_click"
  38. # # 滑块验证码
  39. # CAPTCHA_SLIDER = "slider"
  40. # # 图片内容输入验证码
  41. # CAPTCHA_TEXT_INPUT = "text_input"
  42. #
  43. # # 输入型验证码的裁剪区域
  44. # INPUT_CROP = {
  45. # "x_start": 210,
  46. # "x_end": 510,
  47. # "y_start": 666,
  48. # "y_end": 788,
  49. # }
  50. #
  51. # # 图标点选验证码的裁剪区域
  52. # ICON_CLICK_CROP = {
  53. # "x_start": 40,
  54. # "x_end": 680,
  55. # "y_start": 471,
  56. # "y_end": 1143,
  57. # }
  58. #
  59. # # 不同验证码类型对应的平台参数配置
  60. # VERIFY_CONFIG = {
  61. # CAPTCHA_ICON_CLICK: {"type": "30332", "direction": "top", "extra": None, "y_offset": 0, "result": "points"},
  62. # CAPTCHA_SPACE_REASON: {"type": "88888", "direction": None, "extra": "请点击", "y_offset": 0, "result": "points"},
  63. # CAPTCHA_TEXT_CLICK: {"type": "30114", "direction": None, "extra": "phrase", "y_offset": 0, "result": "points"},
  64. # CAPTCHA_SLIDER: {"type": "22222", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  65. # CAPTCHA_TEXT_INPUT: {"type": "10103", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  66. # # lianxian : {"type": "10114", "direction": None, "extra": None, "y_offset": 0, "result": "text"}
  67. # }
  68. #
  69. #
  70. # def _safe_name(value, default):
  71. # if value is None:
  72. # return default
  73. # text = str(value).strip()
  74. # if not text:
  75. # return default
  76. # text = re.sub(r"[^0-9A-Za-z_\-.]+", "_", text)
  77. # return text or default
  78. #
  79. #
  80. # def _resolve_device_id(d=None, device_id=None):
  81. # if device_id:
  82. # return _safe_name(device_id, UNKNOWN_DEVICE_ID)
  83. #
  84. # candidates = []
  85. # if d is not None:
  86. # for attr in ("serial", "_serial", "device_id"):
  87. # value = getattr(d, attr, None)
  88. # if value:
  89. # candidates.append(value)
  90. # try:
  91. # info = d.device_info
  92. # if isinstance(info, dict):
  93. # for key in ("serial", "serialno", "udid", "deviceId"):
  94. # value = info.get(key)
  95. # if value:
  96. # candidates.append(value)
  97. # except Exception:
  98. # pass
  99. #
  100. # for value in candidates:
  101. # normalized = _safe_name(value, "")
  102. # if normalized:
  103. # return normalized
  104. # return UNKNOWN_DEVICE_ID
  105. #
  106. #
  107. # def _set_runtime_device_id(d=None, device_id=None):
  108. # _RUNTIME_CONTEXT.device_id = _resolve_device_id(d=d, device_id=device_id)
  109. #
  110. #
  111. # def _get_runtime_device_id(d=None):
  112. # current = getattr(_RUNTIME_CONTEXT, "device_id", None)
  113. # if current:
  114. # return current
  115. # resolved = _resolve_device_id(d=d)
  116. # _RUNTIME_CONTEXT.device_id = resolved
  117. # return resolved
  118. #
  119. #
  120. # def _next_slider_method_order():
  121. # idx = getattr(_RUNTIME_CONTEXT, "slider_method_idx", 0)
  122. # first = SLIDER_METHODS[idx % len(SLIDER_METHODS)]
  123. # second = SLIDER_METHODS[(idx + 1) % len(SLIDER_METHODS)]
  124. # _RUNTIME_CONTEXT.slider_method_idx = (idx + 1) % len(SLIDER_METHODS)
  125. # return [first, second]
  126. #
  127. #
  128. # def _ensure_captcha_dir(captcha_type):
  129. # safe_type = _safe_name(captcha_type, "unknown_captcha")
  130. # folder = os.path.join(CAPTCHA_LOG_ROOT, safe_type)
  131. # os.makedirs(folder, exist_ok=True)
  132. # return folder
  133. #
  134. #
  135. # def _build_captcha_image_path(captcha_type, d=None, ext=".png", tag=None):
  136. # device_id = _get_runtime_device_id(d=d)
  137. # random_part = random.randint(10000000, 99999999)
  138. # safe_tag = _safe_name(tag, "") if tag else ""
  139. # suffix = f"_{safe_tag}" if safe_tag else ""
  140. # filename = f"{device_id}_{random_part}{suffix}{ext}"
  141. # return os.path.join(_ensure_captcha_dir(captcha_type), filename)
  142. #
  143. #
  144. # def _save_debug_screenshot(d, captcha_type, tag="full"):
  145. # path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag=tag)
  146. # try:
  147. # d.screenshot(path)
  148. # print(f"[captcha-shot] saved: {path}")
  149. # return path
  150. # except Exception as e:
  151. # print(f"[captcha-shot] save failed: {e}")
  152. # return None
  153. #
  154. #
  155. # def post_api(image_path, captcha_type, extra=None, direction=None, label_image_path=None, timeout=20):
  156. # with open(image_path, 'rb') as f:
  157. # image_base64 = base64.b64encode(f.read()).decode()
  158. #
  159. # data = {
  160. # "token": API_TOKEN,
  161. # "type": captcha_type,
  162. # "image": image_base64,
  163. # }
  164. # if label_image_path:
  165. # with open(label_image_path, 'rb') as f:
  166. # data["label_image"] = base64.b64encode(f.read()).decode()
  167. # if extra is not None:
  168. # data["extra"] = extra
  169. # if direction is not None:
  170. # data["direction"] = direction
  171. #
  172. # headers = {
  173. # "Content-Type": "application/json"
  174. # }
  175. # response = requests.post(API_URL, headers=headers, json=data, timeout=timeout).json()
  176. # print(response)
  177. # return response
  178. #
  179. #
  180. # def parse_points(response, y_offset=0):
  181. # tuple_points = []
  182. # data = response.get("data", {}).get("data", "")
  183. # if not data:
  184. # return tuple_points
  185. #
  186. # for s in data.split('|'):
  187. # x, y = s.split(',')
  188. # tuple_points.append((int(x), int(y) + y_offset))
  189. # return tuple_points
  190. #
  191. #
  192. # def verify(image_path, captcha_type):
  193. # config = VERIFY_CONFIG.get(captcha_type)
  194. # if not config:
  195. # raise ValueError(f"不支持的验证码类型: {captcha_type}")
  196. #
  197. # response = post_api(
  198. # image_path,
  199. # config["type"],
  200. # extra=config["extra"],
  201. # direction=config["direction"]
  202. # )
  203. #
  204. # if config["result"] == "points":
  205. # return parse_points(response, y_offset=config["y_offset"])
  206. #
  207. # return response.get("data", {}).get("data")
  208. #
  209. #
  210. # def crop_image_xy(
  211. # image_path,
  212. # output_path=None,
  213. # x_start=None,
  214. # x_end=None,
  215. # y_start=471,
  216. # y_end=1143
  217. # ):
  218. # if output_path is None:
  219. # dir_name, file_name = os.path.split(image_path)
  220. # name, ext = os.path.splitext(file_name)
  221. # output_path = os.path.join(dir_name, f"{name}_cropped{ext}")
  222. #
  223. # with Image.open(image_path) as img:
  224. # width, height = img.size
  225. #
  226. # if x_start is None:
  227. # x_start = 0
  228. # if x_end is None:
  229. # x_end = width - 1
  230. #
  231. # x_start = max(0, min(x_start, width - 1))
  232. # x_end = max(x_start, min(x_end, width - 1))
  233. # y_start = max(0, min(y_start, height - 1))
  234. # y_end = max(y_start, min(y_end, height - 1))
  235. #
  236. # cropped = img.crop((x_start, y_start, x_end + 1, y_end + 1))
  237. # cropped.save(output_path)
  238. #
  239. # return output_path
  240. #
  241. #
  242. # def _capture_by_bounds(d, xpath_candidates, output_path=None, screenshot_path=None, captcha_type="generic"):
  243. # """
  244. # 按元素 bounds 截图并裁剪。
  245. # return: (cropped_path, bounds_dict) or (None, None)
  246. # """
  247. # if isinstance(xpath_candidates, str):
  248. # xpath_candidates = [xpath_candidates]
  249. #
  250. # if screenshot_path is None:
  251. # screenshot_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="full")
  252. # if output_path is None:
  253. # output_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="crop")
  254. #
  255. # for xpath in xpath_candidates:
  256. # try:
  257. # node = d.xpath(xpath)
  258. # if not node.exists:
  259. # continue
  260. # bounds = node.info.get("bounds", {})
  261. # if not bounds:
  262. # continue
  263. #
  264. # d.screenshot(screenshot_path)
  265. # cropped = crop_image_xy(
  266. # screenshot_path,
  267. # output_path=output_path,
  268. # x_start=bounds["left"],
  269. # x_end=bounds["right"],
  270. # y_start=bounds["top"],
  271. # y_end=bounds["bottom"],
  272. # )
  273. # return cropped, bounds
  274. # except Exception:
  275. # continue
  276. #
  277. # return None, None
  278. #
  279. #
  280. # def srwz(d):
  281. # captcha_image_xpaths = [
  282. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.widget.Image[1]',
  283. # '//*[@resource-id="captchaImg"]',
  284. # ]
  285. # image_path, _ = _capture_by_bounds(d, captcha_image_xpaths, captcha_type=CAPTCHA_TEXT_INPUT)
  286. # if not image_path:
  287. # return False
  288. #
  289. # data = verify(image_path, CAPTCHA_TEXT_INPUT)
  290. # input_box = d.xpath('//*[@hint="请输入验证码"]')
  291. # if input_box.exists:
  292. # input_box.click()
  293. # time.sleep(0.5)
  294. # input_box.set_text(data)
  295. # d.xpath('//*[@text="验证"]').click()
  296. # return True
  297. # else:
  298. # print("未找到输入框")
  299. # return False
  300. #
  301. #
  302. # def _clamp(value, min_value, max_value):
  303. # return max(min_value, min(value, max_value))
  304. #
  305. #
  306. # def _slider_duration(distance):
  307. # if distance <= 90:
  308. # return round(random.uniform(0.18, 0.27), 3)
  309. # if distance <= 160:
  310. # return round(random.uniform(0.23, 0.34), 3)
  311. # return round(random.uniform(0.28, 0.42), 3)
  312. #
  313. #
  314. # def _build_human_slider_track(start_x, start_y, distance):
  315. # # Keep the reference shape: mostly flat first, then a single smooth downward bend.
  316. # overshoot = random.randint(1, 2) if distance > 140 and random.random() < 0.18 else 0
  317. # move_distance = distance + overshoot
  318. # steps = int(_clamp(move_distance / random.uniform(6.0, 8.0), 16, 32))
  319. #
  320. # base_y = start_y + random.randint(-1, 1)
  321. # flat_ratio = random.uniform(0.30, 0.42) # front section almost horizontal
  322. # drop = distance * random.uniform(0.08, 0.14)
  323. # if distance > 260:
  324. # drop *= random.uniform(1.35, 1.75)
  325. # drop = _clamp(drop, 12.0, 58.0) # obvious tail drop
  326. # noise_amp = random.uniform(0.03, 0.22)
  327. #
  328. # points = [(start_x, base_y)]
  329. # last_x = start_x
  330. #
  331. # for i in range(1, steps + 1):
  332. # t = i / steps
  333. #
  334. # progress = 1 - (1 - t) ** 2.0
  335. # progress += random.uniform(-0.0018, 0.0018)
  336. # progress = _clamp(progress, 0.0, 1.0)
  337. #
  338. # x = start_x + int(move_distance * progress)
  339. # if x <= last_x:
  340. # x = last_x + 1
  341. # last_x = x
  342. #
  343. # if t < flat_ratio:
  344. # # slight tiny rise then back, still near flat
  345. # u = t / max(flat_ratio, 1e-6)
  346. # y_curve = -0.9 * math.sin(math.pi * u)
  347. # else:
  348. # u = (t - flat_ratio) / max(1 - flat_ratio, 1e-6)
  349. # y_curve = drop * (u ** 1.7)
  350. #
  351. # y_noise = random.uniform(-noise_amp, noise_amp)
  352. # y = int(round(base_y + y_curve + y_noise))
  353. # points.append((x, y))
  354. #
  355. # final_x = start_x + distance
  356. # final_y = int(round(base_y + drop + random.uniform(-0.6, 0.6)))
  357. # if overshoot > 0:
  358. # points.append((start_x + distance + overshoot, final_y))
  359. # points.append((final_x, final_y + random.choice([0, 0, 1])))
  360. # return points
  361. #
  362. #
  363. # def _save_slider_track_image(
  364. # points,
  365. # distance=None,
  366. # duration=None,
  367. # drag_ok=True,
  368. # screenshot_path=SCREENSHOT_PATH,
  369. # captcha_type=CAPTCHA_SLIDER
  370. # ):
  371. # if not points:
  372. # return None
  373. #
  374. # output_path = _build_captcha_image_path(captcha_type, ext=".png", tag="track")
  375. #
  376. # try:
  377. # if os.path.exists(screenshot_path):
  378. # with Image.open(screenshot_path) as img:
  379. # canvas = img.convert("RGB")
  380. # else:
  381. # canvas = Image.new("RGB", (720, 1280), "white")
  382. #
  383. # draw = ImageDraw.Draw(canvas)
  384. #
  385. # if len(points) >= 2:
  386. # draw.line(points, fill=(245, 20, 30), width=9)
  387. #
  388. # sx, sy = points[0]
  389. # ex, ey = points[-1]
  390. # draw.ellipse((sx - 5, sy - 5, sx + 5, sy + 5), fill=(40, 200, 80))
  391. # draw.ellipse((ex - 5, ey - 5, ex + 5, ey + 5), fill=(50, 120, 255))
  392. #
  393. # info = f"ok={drag_ok} dist={distance} dur={duration}s points={len(points)}"
  394. # draw.rectangle((8, 8, min(canvas.size[0] - 8, 520), 42), fill=(0, 0, 0))
  395. # draw.text((14, 14), info, fill=(255, 255, 255))
  396. #
  397. # canvas.save(output_path)
  398. # print(f"[slider-track] saved: {output_path}")
  399. # return output_path
  400. # except Exception as e:
  401. # print(f"[slider-track] save failed: {e}")
  402. # return None
  403. #
  404. #
  405. # def _downsample_track_points(points, target_count):
  406. # if not points or len(points) <= target_count:
  407. # return points[:]
  408. # if target_count < 2:
  409. # return [points[0], points[-1]]
  410. #
  411. # sampled = []
  412. # last_index = len(points) - 1
  413. # for i in range(target_count):
  414. # idx = int(round(i * last_index / (target_count - 1)))
  415. # sampled.append(points[idx])
  416. # return sampled
  417. #
  418. #
  419. # def _execute_track(d, points, total_duration):
  420. # if not points or len(points) < 2:
  421. # return False, points
  422. #
  423. # duration = max(0.18, float(total_duration))
  424. #
  425. # # Use fewer points to avoid step explosion, keep curve shape.
  426. # max_points = random.randint(14, 22)
  427. # exec_points = _downsample_track_points(points, max_points)
  428. #
  429. # try:
  430. # if hasattr(d, "swipe_points"):
  431. # # u2: duration here means time-per-step; steps = duration/0.005.
  432. # # To approximate total duration:
  433. # # total ~= (duration/0.005) * (len(exec_points)-1) * 0.005 = duration * segments
  434. # seg_count = max(1, len(exec_points) - 1)
  435. # per_segment = max(0.01, duration / seg_count)
  436. # d.swipe_points(exec_points, duration=per_segment)
  437. # return True, exec_points
  438. # except Exception:
  439. # pass
  440. #
  441. # # fallback
  442. # try:
  443. # sx, sy = exec_points[0]
  444. # ex, ey = exec_points[-1]
  445. # d.swipe(sx, sy, ex, ey, duration=duration)
  446. # return False, exec_points
  447. # except Exception:
  448. # return False, exec_points
  449. #
  450. #
  451. # def _slider_still_exists(d):
  452. # xpath_candidates = [
  453. # '//*[@text="请拖动下方滑块完成拼图"]',
  454. # '//*[contains(@text, "拖动下方滑块")]',
  455. # '//*[@resource-id="puzzleSliderBox"]',
  456. # '//*[@resource-id="puzzleImageMain"]',
  457. # ]
  458. # for xpath in xpath_candidates:
  459. # try:
  460. # if d.xpath(xpath).exists:
  461. # return True
  462. # except Exception:
  463. # continue
  464. # return False
  465. #
  466. #
  467. # def _cubic_bezier(t, p0, p1, p2, p3):
  468. # one_minus_t = 1 - t
  469. # x = (
  470. # one_minus_t ** 3 * p0[0]
  471. # + 3 * one_minus_t ** 2 * t * p1[0]
  472. # + 3 * one_minus_t * t ** 2 * p2[0]
  473. # + t ** 3 * p3[0]
  474. # )
  475. # y = (
  476. # one_minus_t ** 3 * p0[1]
  477. # + 3 * one_minus_t ** 2 * t * p1[1]
  478. # + 3 * one_minus_t * t ** 2 * p2[1]
  479. # + t ** 3 * p3[1]
  480. # )
  481. # return x, y
  482. #
  483. #
  484. # def _generate_bezier_slider_points(start, end, deviation=30, steps=50):
  485. # sx, sy = start
  486. # ex, ey = end
  487. # mid_x = (sx + ex) / 2
  488. # mid_y = (sy + ey) / 2
  489. #
  490. # p1 = (
  491. # mid_x - (ex - sx) / 4 + random.uniform(-deviation, deviation),
  492. # mid_y - (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  493. # )
  494. # p2 = (
  495. # mid_x + (ex - sx) / 4 + random.uniform(-deviation, deviation),
  496. # mid_y + (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  497. # )
  498. #
  499. # points = []
  500. # for i in range(steps + 1):
  501. # t = i / steps
  502. # x, y = _cubic_bezier(t, start, p1, p2, end)
  503. # if 0 < i < steps:
  504. # x += random.gauss(0, 1.5)
  505. # y += random.gauss(0, 1.5)
  506. # points.append((int(round(x)), int(round(y))))
  507. # return points
  508. #
  509. #
  510. # def _execute_bezier_slider(d, start_x, start_y, end_x, end_y):
  511. # points = _generate_bezier_slider_points(
  512. # (int(round(start_x)), int(round(start_y))),
  513. # (int(round(end_x)), int(round(end_y))),
  514. # deviation=random.randint(20, 40),
  515. # steps=50,
  516. # )
  517. # if len(points) < 2:
  518. # return False, points
  519. #
  520. # try:
  521. # d.touch.down(points[0][0], points[0][1])
  522. # time.sleep(random.uniform(0.1, 0.2))
  523. # total = max(1, len(points) - 1)
  524. # for i, (x, y) in enumerate(points[1:], 1):
  525. # d.touch.move(x, y)
  526. # t = i / total
  527. # if 0.2 < t < 0.8:
  528. # interval = random.uniform(0.02, 0.04)
  529. # else:
  530. # interval = random.uniform(0.04, 0.08)
  531. # time.sleep(interval)
  532. # time.sleep(random.uniform(0.05, 0.15))
  533. # d.touch.up(points[-1][0], points[-1][1])
  534. # return True, points
  535. # except Exception:
  536. # try:
  537. # d.touch.up(points[-1][0], points[-1][1])
  538. # except Exception:
  539. # pass
  540. # return False, points
  541. #
  542. #
  543. # def _slider_knob_center(d):
  544. # slider_xpath = (
  545. # '//*[@resource-id="puzzleSliderBox"] | '
  546. # '//*[@resource-id="yodaBox"] | '
  547. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]'
  548. # )
  549. # try:
  550. # slider_node = d.xpath(slider_xpath)
  551. # if slider_node.exists:
  552. # bounds = slider_node.info.get("bounds", {})
  553. # if bounds:
  554. # return (
  555. # (bounds["left"] + bounds["right"]) / 2 + random.uniform(-4, 4),
  556. # (bounds["top"] + bounds["bottom"]) / 2 + random.uniform(-3, 3),
  557. # )
  558. # except Exception:
  559. # pass
  560. # return None
  561. #
  562. #
  563. # def _first_existing_bounds(d, xpath_candidates):
  564. # if isinstance(xpath_candidates, str):
  565. # xpath_candidates = [xpath_candidates]
  566. # for xpath in xpath_candidates:
  567. # try:
  568. # node = d.xpath(xpath)
  569. # if not node.exists:
  570. # continue
  571. # bounds = node.info.get("bounds", {})
  572. # if bounds:
  573. # return xpath, bounds
  574. # except Exception:
  575. # continue
  576. # return None, None
  577. #
  578. #
  579. # def _build_directional_track(start_x, start_y, end_x, end_y):
  580. # distance_x = end_x - start_x
  581. # distance_y = end_y - start_y
  582. # if abs(distance_x) < 2 and abs(distance_y) < 2:
  583. # return [(int(start_x), int(start_y)), (int(end_x), int(end_y))]
  584. #
  585. # steps = int(_clamp(abs(distance_x) / random.uniform(7.0, 10.0), 22, 48))
  586. # points = [(int(start_x), int(start_y))]
  587. # last_x = float(start_x)
  588. #
  589. # for i in range(1, steps + 1):
  590. # t = i / steps
  591. # progress = 1 - (1 - t) ** random.uniform(1.8, 2.25)
  592. #
  593. # x = start_x + distance_x * progress + random.uniform(-0.9, 0.9)
  594. # y = start_y + distance_y * progress + random.uniform(-0.8, 0.8)
  595. #
  596. # if distance_x >= 0:
  597. # if x < last_x:
  598. # x = last_x + random.uniform(0.2, 1.2)
  599. # else:
  600. # if x > last_x:
  601. # x = last_x - random.uniform(0.2, 1.2)
  602. #
  603. # last_x = x
  604. # points.append((int(round(x)), int(round(y))))
  605. #
  606. # points.append((int(round(end_x)), int(round(end_y))))
  607. # return points
  608. #
  609. #
  610. # def _move_with_pressed_touch(d, points):
  611. # if not points:
  612. # return
  613. # for x, y in points:
  614. # d.touch.move(x, y)
  615. # time.sleep(random.uniform(0.0015, 0.0045))
  616. #
  617. #
  618. # def _screenshot_to_image(d):
  619. # shot = d.screenshot()
  620. # if isinstance(shot, Image.Image):
  621. # return shot.convert("RGB")
  622. # if isinstance(shot, bytes):
  623. # return Image.open(io.BytesIO(shot)).convert("RGB")
  624. # if isinstance(shot, str) and os.path.exists(shot):
  625. # return Image.open(shot).convert("RGB")
  626. # if hasattr(shot, "convert"):
  627. # return shot.convert("RGB")
  628. # fallback_path = _build_captcha_image_path("generic", d=d, ext=".png", tag="fallback")
  629. # d.screenshot(fallback_path)
  630. # return Image.open(fallback_path).convert("RGB")
  631. #
  632. #
  633. # def hk(d):
  634. # screenshot_path = _build_captcha_image_path(CAPTCHA_SLIDER, d=d, ext=".png", tag="full")
  635. # d.screenshot(screenshot_path)
  636. # data = verify(screenshot_path, CAPTCHA_SLIDER)
  637. # if not data:
  638. # return False
  639. #
  640. # try:
  641. # raw_distance = float(data)
  642. # except (TypeError, ValueError):
  643. # return False
  644. #
  645. # if raw_distance <= 0:
  646. # return False
  647. #
  648. # image_width = 720
  649. # try:
  650. # with Image.open(screenshot_path) as img:
  651. # image_width = img.size[0] or 720
  652. # except Exception:
  653. # pass
  654. #
  655. # try:
  656. # screen_width = int(d.info.get("displayWidth", image_width))
  657. # except Exception:
  658. # screen_width = image_width
  659. #
  660. # scale = screen_width / image_width if image_width else 1.0
  661. # slide_distance = int(raw_distance * scale)
  662. # if slide_distance < 80:
  663. # slide_distance += random.randint(3, 6)
  664. # elif slide_distance < 160:
  665. # slide_distance += random.randint(2, 5)
  666. # else:
  667. # slide_distance += random.randint(1, 4)
  668. #
  669. # start_x = 84 + random.randint(-1, 1)
  670. # start_y = 1034 + random.randint(-2, 2)
  671. # knob_center = _slider_knob_center(d)
  672. # if knob_center:
  673. # start_x, start_y = knob_center
  674. # max_target_x = screen_width - random.randint(26, 42)
  675. # target_x = _clamp(start_x + slide_distance, start_x + 18, max_target_x)
  676. # distance = target_x - start_x
  677. # if distance < 18:
  678. # return False
  679. #
  680. # method_order = _next_slider_method_order()
  681. # for method in method_order:
  682. # if method == "track":
  683. # points = _build_human_slider_track(int(round(start_x)), int(round(start_y)), int(round(distance)))
  684. # duration = _slider_duration(distance)
  685. # time.sleep(random.uniform(0.015, 0.05))
  686. # drag_ok, exec_points = _execute_track(d, points, duration)
  687. # _save_slider_track_image(
  688. # exec_points,
  689. # distance=distance,
  690. # duration=duration,
  691. # drag_ok=drag_ok,
  692. # screenshot_path=screenshot_path,
  693. # captcha_type=CAPTCHA_SLIDER
  694. # )
  695. # else:
  696. # end_x = target_x + random.uniform(-3, 3)
  697. # end_y = start_y + random.uniform(-1, 1)
  698. # drag_ok, exec_points = _execute_bezier_slider(d, start_x, start_y, end_x, end_y)
  699. # _save_slider_track_image(
  700. # exec_points,
  701. # distance=distance,
  702. # duration=None,
  703. # drag_ok=drag_ok,
  704. # screenshot_path=screenshot_path,
  705. # captcha_type=CAPTCHA_SLIDER
  706. # )
  707. #
  708. # time.sleep(random.uniform(1.0, 1.8))
  709. # if not _slider_still_exists(d):
  710. # return True
  711. # print(f"[slider] method {method} failed, switch to next")
  712. #
  713. # return False
  714. #
  715. #
  716. # def dianxuan(d):
  717. # click_area_xpaths = [
  718. # '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  719. # '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  720. # '//*[@resource-id="root"]',
  721. # ]
  722. # image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_ICON_CLICK)
  723. # if not image_path or not bounds:
  724. # return False
  725. #
  726. # left = bounds["left"]
  727. # top = bounds["top"]
  728. # data = verify(image_path, CAPTCHA_ICON_CLICK)
  729. # if not data:
  730. # return False
  731. #
  732. # for x, y in data:
  733. # time.sleep(random.randint(1, 2))
  734. # d.click(left + x + random.randint(-4, 4), top + y + random.randint(-4, 4))
  735. # return True
  736. #
  737. #
  738. # def wenzidianxuan(d):
  739. # # 文字点选:按验证码容器 bounds 裁剪后,调用 88888,按返回坐标依次点击
  740. # click_area_xpaths = [
  741. # '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  742. # '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  743. # '//*[@resource-id="root"]',
  744. # ]
  745. # image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_TEXT_CLICK)
  746. # if not image_path or not bounds:
  747. # return False
  748. #
  749. # image_left = bounds["left"]
  750. # image_top = bounds["top"]
  751. #
  752. # result = post_api(image_path, "88888")
  753. # verify_data = result.get("data", {})
  754. # if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  755. # return False
  756. #
  757. # coords_str = verify_data.get("data", "")
  758. # if not coords_str:
  759. # return False
  760. #
  761. # clicked = 0
  762. # for coord in coords_str.split("|"):
  763. # try:
  764. # x_img_str, y_img_str = coord.split(",")
  765. # x_img = int(x_img_str.strip())
  766. # y_img = int(y_img_str.strip())
  767. # x_screen = image_left + x_img + random.randint(-5, 5)
  768. # y_screen = image_top + y_img + random.randint(-5, 5)
  769. # d.click(x_screen, y_screen)
  770. # clicked += 1
  771. # time.sleep(random.uniform(0.8, 1.6))
  772. # except Exception:
  773. # continue
  774. #
  775. # return clicked > 0
  776. #
  777. #
  778. # def click_side(d):
  779. # """空间推理验证码(请点击数字)。"""
  780. # click_area_xpaths = [
  781. # '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  782. # '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  783. # '//*[@resource-id="root"]',
  784. # ]
  785. # image_path, bounds = _capture_by_bounds(d, click_area_xpaths, output_path=CROP_PATH)
  786. # if not image_path or not bounds:
  787. # return False
  788. #
  789. # left = bounds["left"]
  790. # top = bounds["top"]
  791. # points = verify(image_path, CAPTCHA_SPACE_REASON)
  792. # if not points:
  793. # return False
  794. #
  795. # x, y = points[0]
  796. # d.click(left + x + random.randint(-2, 2), top + y + random.randint(-2, 2))
  797. #
  798. # # d_list = [
  799. # # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]',
  800. # # '//*[@resource-id="com.sankuai.meituan:id/yoda_toolbar_title"]',
  801. # # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]'
  802. # # ]
  803. # # for i in d_list:
  804. # # d.xpath(i).click()
  805. # return True
  806. #
  807. #
  808. # def Swipe_right(d):
  809. # """向右拖动到最右侧(非拼图滑块)。"""
  810. # track_xpath = (
  811. # '//*[@resource-id="yodaBoxWrapper"] | '
  812. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  813. # )
  814. # slider_xpath = (
  815. # '//*[@resource-id="yodaBox"] | '
  816. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  817. # )
  818. #
  819. # if not d.xpath(track_xpath).exists or not d.xpath(slider_xpath).exists:
  820. # return False
  821. #
  822. # track_bounds = d.xpath(track_xpath).info.get("bounds", {})
  823. # slider_bounds = d.xpath(slider_xpath).info.get("bounds", {})
  824. # if not track_bounds or not slider_bounds:
  825. # return False
  826. #
  827. # start_x = int((slider_bounds["left"] + slider_bounds["right"]) / 2) + random.randint(-2, 2)
  828. # start_y = int((slider_bounds["top"] + slider_bounds["bottom"]) / 2) + random.randint(-2, 2)
  829. # right_limit = int(track_bounds["right"]) - random.randint(4, 10)
  830. # distance = right_limit - start_x
  831. # if distance <= 10:
  832. # return False
  833. #
  834. # screenshot_path = _save_debug_screenshot(d, "Swipe_right", tag="full")
  835. # points = _build_human_slider_track(start_x, start_y, distance)
  836. # duration = _slider_duration(distance)
  837. # drag_ok, exec_points = _execute_track(d, points, duration)
  838. # _save_slider_track_image(
  839. # exec_points,
  840. # distance=distance,
  841. # duration=duration,
  842. # drag_ok=drag_ok,
  843. # screenshot_path=screenshot_path or SCREENSHOT_PATH,
  844. # captcha_type="Swipe_right"
  845. # )
  846. # return True
  847. #
  848. #
  849. # def complexs(d):
  850. # """Complex slider flow: move to far-right, OCR with label image, then drag back to target."""
  851. # slider_xpath_candidates = [
  852. # '//*[@resource-id="yodaBox"]',
  853. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]',
  854. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  855. # ]
  856. # track_xpath_candidates = [
  857. # '//*[@resource-id="yodaBoxWrapper"]',
  858. # '//*[contains(@text, "请按照说明拖动滑块")]',
  859. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  860. # ]
  861. # label_xpath_candidates = [
  862. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.widget.TextView[1]',
  863. # ]
  864. # image_xpath_candidates = [
  865. # '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  866. # ]
  867. #
  868. # _, slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  869. # _, track_bounds = _first_existing_bounds(d, track_xpath_candidates)
  870. # if not slider_bounds or not track_bounds:
  871. # return False
  872. #
  873. # slider_left = slider_bounds["left"]
  874. # slider_top = slider_bounds["top"]
  875. # slider_right = slider_bounds["right"]
  876. # slider_bottom = slider_bounds["bottom"]
  877. # slider_width = slider_right - slider_left
  878. # slider_center_x = (slider_left + slider_right) / 2
  879. # slider_center_y = (slider_top + slider_bottom) / 2
  880. #
  881. # track_left = track_bounds["left"]
  882. # track_right = track_bounds["right"]
  883. # right_end_center_x = track_right - slider_width / 2
  884. # right_end_center_y = slider_center_y
  885. #
  886. # print(f"滑块中心: ({slider_center_x}, {slider_center_y})")
  887. # print(f"最右端滑块中心坐标: ({right_end_center_x}, {right_end_center_y})")
  888. #
  889. # touch_down = False
  890. # try:
  891. # d.touch.down(slider_center_x, slider_center_y)
  892. # touch_down = True
  893. # time.sleep(random.uniform(0.08, 0.16))
  894. #
  895. # move_right_points = _build_directional_track(
  896. # slider_center_x,
  897. # slider_center_y,
  898. # right_end_center_x,
  899. # right_end_center_y,
  900. # )
  901. # _move_with_pressed_touch(d, move_right_points[1:])
  902. # print("滑块已到达最右端")
  903. #
  904. # _, label_bounds = _first_existing_bounds(d, label_xpath_candidates)
  905. # _, image_bounds = _first_existing_bounds(d, image_xpath_candidates)
  906. # if not label_bounds or not image_bounds:
  907. # return False
  908. #
  909. # capture_label_left = label_bounds["left"]
  910. # capture_label_top = label_bounds["top"]
  911. # capture_label_right = label_bounds["right"]
  912. # capture_label_bottom = label_bounds["bottom"]
  913. #
  914. # capture_left = image_bounds["left"]
  915. # capture_top = image_bounds["top"]
  916. # capture_right = image_bounds["right"]
  917. # capture_bottom = image_bounds["bottom"]
  918. #
  919. # print(
  920. # "截图区域1(提示文本): "
  921. # f"left={capture_label_left}, top={capture_label_top}, "
  922. # f"width={capture_label_right - capture_label_left}, "
  923. # f"height={capture_label_bottom - capture_label_top}"
  924. # )
  925. # print(
  926. # "截图区域2(图片): "
  927. # f"left={capture_left}, top={capture_top}, "
  928. # f"width={capture_right - capture_left}, "
  929. # f"height={capture_bottom - capture_top}"
  930. # )
  931. #
  932. # screenshot_label_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="label")
  933. # screenshot_image_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="image")
  934. # image = _screenshot_to_image(d)
  935. # image.crop(
  936. # (capture_label_left, capture_label_top, capture_label_right, capture_label_bottom)
  937. # ).save(screenshot_label_path)
  938. # image.crop(
  939. # (capture_left, capture_top, capture_right, capture_bottom)
  940. # ).save(screenshot_image_path)
  941. # print(f"截图1已保存: {screenshot_label_path}")
  942. # print(f"截图2已保存: {screenshot_image_path}")
  943. #
  944. # result = post_api(
  945. # screenshot_image_path,
  946. # "29013",
  947. # label_image_path=screenshot_label_path,
  948. # timeout=30,
  949. # )
  950. # print(f"API返回结果: {result}")
  951. # verify_data = result.get("data", {})
  952. # print(f"verify_data={verify_data}")
  953. #
  954. # if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  955. # return False
  956. #
  957. # data_str = verify_data.get("data", "")
  958. # if not data_str:
  959. # return False
  960. # data_value = int(data_str)
  961. # print(f"云码返回的像素距离: {data_value}")
  962. #
  963. # slider_target_center_x = track_left + data_value
  964. # min_x = track_left + slider_width / 2
  965. # max_x = track_right - slider_width / 2
  966. # slider_target_center_x = _clamp(slider_target_center_x, min_x, max_x)
  967. # print(f"滑块中心目标X坐标: {slider_target_center_x}")
  968. #
  969. # _, current_slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  970. # if current_slider_bounds:
  971. # current_slider_center_x = (current_slider_bounds["left"] + current_slider_bounds["right"]) / 2
  972. # else:
  973. # current_slider_center_x = right_end_center_x
  974. # actual_distance = slider_target_center_x - current_slider_center_x
  975. # print(f"实际需要滑动的距离: {actual_distance}")
  976. #
  977. # back_points = _build_directional_track(
  978. # current_slider_center_x,
  979. # right_end_center_y,
  980. # slider_target_center_x,
  981. # right_end_center_y,
  982. # )
  983. # _move_with_pressed_touch(d, back_points[1:])
  984. # time.sleep(random.uniform(0.2, 0.4))
  985. # d.touch.up(slider_target_center_x, right_end_center_y)
  986. # touch_down = False
  987. # time.sleep(random.uniform(1.8, 3.2))
  988. # return True
  989. # except Exception as e:
  990. # print(f"complex captcha failed: {e}")
  991. # return False
  992. # finally:
  993. # if touch_down:
  994. # try:
  995. # d.touch.up(right_end_center_x, right_end_center_y)
  996. # except Exception:
  997. # pass
  998. #
  999. #
  1000. # def Numbers_English_verify(d):
  1001. # return srwz(d)
  1002. #
  1003. #
  1004. # def slider_verify(d):
  1005. # return hk(d)
  1006. #
  1007. #
  1008. # def Click_images(d):
  1009. # # 两种点选入口统一处理
  1010. # if d.xpath('//*[@text="请按语序依次点击下图文字"]').exists:
  1011. # return wenzidianxuan(d)
  1012. # return dianxuan(d)
  1013. #
  1014. #
  1015. # def Shortest_connection(d):
  1016. # return lianxian(d)
  1017. #
  1018. #
  1019. # def _handle_generic_captcha(d, xpath, timeout=60):
  1020. # """通用验证码处理:等待人工处理完成。"""
  1021. # start = time.time()
  1022. # while time.time() - start < timeout:
  1023. # if xpath and not d.xpath(xpath).exists:
  1024. # return True
  1025. # time.sleep(1)
  1026. # return False
  1027. #
  1028. #
  1029. # def handle_captcha(d, captcha_type, xpath=None, device_id=None):
  1030. # _set_runtime_device_id(d=d, device_id=device_id)
  1031. # handlers = {
  1032. # "Numbers_English": Numbers_English_verify,
  1033. # "Swipe_right": Swipe_right,
  1034. # "Click_images": Click_images,
  1035. # "slider": slider_verify,
  1036. # "complexs": complexs,
  1037. # "Shortest_connection": Shortest_connection,
  1038. # "click_side": click_side,
  1039. # }
  1040. # func = handlers.get(captcha_type)
  1041. # if func is None:
  1042. # return _handle_generic_captcha(d, xpath)
  1043. # return func(d)
  1044. #
  1045. #
  1046. # def _extract_color_name(api_result):
  1047. # if not isinstance(api_result, dict):
  1048. # return ""
  1049. #
  1050. # if api_result.get("code") == 0 and isinstance(api_result.get("data"), str):
  1051. # return api_result.get("data", "").strip()
  1052. #
  1053. # if api_result.get("code") == 10000:
  1054. # inner = api_result.get("data")
  1055. # if isinstance(inner, dict) and inner.get("code") == 0:
  1056. # return str(inner.get("data", "")).strip()
  1057. # if isinstance(inner, str):
  1058. # return inner.strip()
  1059. #
  1060. # return ""
  1061. #
  1062. #
  1063. # def _normalize_color_name(color_name):
  1064. # if not color_name:
  1065. # return ""
  1066. #
  1067. # alias = {
  1068. # "红": "红色",
  1069. # "红的": "红色",
  1070. # "绿": "绿色",
  1071. # "蓝": "蓝色",
  1072. # "黄": "黄色",
  1073. # "橙": "橙色",
  1074. # "紫": "紫色",
  1075. # "黑": "黑色",
  1076. # "白": "白色",
  1077. # "棕": "棕色",
  1078. # "褐": "褐色",
  1079. # }
  1080. # if color_name in alias:
  1081. # return alias[color_name]
  1082. # for k, v in alias.items():
  1083. # if k in color_name:
  1084. # return v
  1085. # return color_name
  1086. #
  1087. #
  1088. # def _find_color_coordinates(image_path, color_name):
  1089. # color_name = _normalize_color_name(color_name)
  1090. # color_ranges = {
  1091. # "红色": (([0, 120, 70], [10, 255, 255]), ([170, 120, 70], [180, 255, 255])),
  1092. # "绿色": (([35, 50, 50], [85, 255, 255]),),
  1093. # "蓝色": (([90, 50, 50], [130, 255, 255]),),
  1094. # "黄色": (([20, 100, 100], [30, 255, 255]),),
  1095. # "橙色": (([5, 100, 100], [18, 255, 255]),),
  1096. # "紫色": (([130, 50, 50], [165, 255, 255]),),
  1097. # "黑色": (([0, 0, 0], [180, 255, 50]),),
  1098. # "白色": (([0, 0, 200], [180, 35, 255]),),
  1099. # "棕色": (([8, 60, 20], [20, 255, 180]),),
  1100. # "褐色": (([8, 60, 20], [20, 255, 180]),),
  1101. # }
  1102. # if color_name not in color_ranges:
  1103. # return []
  1104. #
  1105. # image = cv2.imread(image_path)
  1106. # if image is None:
  1107. # return []
  1108. #
  1109. # hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
  1110. # ranges = color_ranges[color_name]
  1111. # if len(ranges) == 2:
  1112. # lower1 = np.array(ranges[0][0])
  1113. # upper1 = np.array(ranges[0][1])
  1114. # lower2 = np.array(ranges[1][0])
  1115. # upper2 = np.array(ranges[1][1])
  1116. # mask = cv2.bitwise_or(cv2.inRange(hsv, lower1, upper1), cv2.inRange(hsv, lower2, upper2))
  1117. # else:
  1118. # lower = np.array(ranges[0][0])
  1119. # upper = np.array(ranges[0][1])
  1120. # mask = cv2.inRange(hsv, lower, upper)
  1121. #
  1122. # kernel = np.ones((3, 3), np.uint8)
  1123. # mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  1124. # mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  1125. #
  1126. # contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  1127. # coordinates = []
  1128. # for contour in contours:
  1129. # area = cv2.contourArea(contour)
  1130. # if area < 30:
  1131. # continue
  1132. # m = cv2.moments(contour)
  1133. # if m["m00"] == 0:
  1134. # continue
  1135. # cx = int(m["m10"] / m["m00"])
  1136. # cy = int(m["m01"] / m["m00"])
  1137. # coordinates.append((cx, cy))
  1138. # return coordinates
  1139. #
  1140. #
  1141. # def _nearest_neighbor_path(points):
  1142. # if not points:
  1143. # return []
  1144. # if len(points) <= 2:
  1145. # return points[:]
  1146. #
  1147. # unvisited = points[:]
  1148. # path = [unvisited.pop(0)]
  1149. # while unvisited:
  1150. # last_x, last_y = path[-1]
  1151. # idx = min(
  1152. # range(len(unvisited)),
  1153. # key=lambda i: math.hypot(last_x - unvisited[i][0], last_y - unvisited[i][1])
  1154. # )
  1155. # path.append(unvisited.pop(idx))
  1156. # return path
  1157. #
  1158. #
  1159. # def _human_like_path(points):
  1160. # if len(points) < 2:
  1161. # return points[:]
  1162. #
  1163. # curved = []
  1164. # for i in range(len(points) - 1):
  1165. # start = points[i]
  1166. # end = points[i + 1]
  1167. # mid_x = (start[0] + end[0]) / 2
  1168. # mid_y = (start[1] + end[1]) / 2
  1169. #
  1170. # if abs(end[0] - start[0]) > abs(end[1] - start[1]):
  1171. # offset_x = 0
  1172. # offset_y = random.uniform(-15, 15)
  1173. # else:
  1174. # offset_x = random.uniform(-15, 15)
  1175. # offset_y = 0
  1176. #
  1177. # control_x = mid_x + offset_x
  1178. # control_y = mid_y + offset_y
  1179. # curved.append(start)
  1180. # for t in np.arange(0.1, 1.0, 0.1):
  1181. # x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control_x + t ** 2 * end[0]
  1182. # y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control_y + t ** 2 * end[1]
  1183. # curved.append((int(x), int(y)))
  1184. # curved.append(points[-1])
  1185. # return curved
  1186. #
  1187. #
  1188. # def _simulate_human_drawing(d, path):
  1189. # if len(path) < 2:
  1190. # return False
  1191. # try:
  1192. # sx, sy = path[0]
  1193. # d.touch.down(sx, sy)
  1194. # time.sleep(random.uniform(0.05, 0.1))
  1195. # for i in range(1, len(path)):
  1196. # x, y = path[i]
  1197. # d.touch.move(x + random.randint(-2, 2), y + random.randint(-2, 2))
  1198. # time.sleep(random.uniform(0.01, 0.03))
  1199. # time.sleep(random.uniform(0.1, 0.2))
  1200. # d.touch.up(path[-1][0], path[-1][1])
  1201. # return True
  1202. # except Exception:
  1203. # return False
  1204. #
  1205. #
  1206. # def retry_captcha(d, xpath_text, handle_func, retry_count=5, captcha_name=None):
  1207. # # 如果当前页面存在对应验证码,就循环重试处理
  1208. # if d.xpath(xpath_text).exists:
  1209. # current_name = captcha_name or getattr(handle_func, "__name__", "captcha")
  1210. # for _ in range(retry_count):
  1211. # _save_debug_screenshot(d, current_name, tag="full")
  1212. # handle_func(d)
  1213. # time.sleep(3)
  1214. # # 验证码消失了,说明处理成功,直接退出
  1215. # if not d.xpath(xpath_text).exists:
  1216. # break
  1217. #
  1218. #
  1219. # def yzm(d=None, device_id=None):
  1220. # # 如果没有传设备对象,就默认连接当前设备
  1221. # if d is None:
  1222. # d = u2.connect()
  1223. # _set_runtime_device_id(d=d, device_id=device_id)
  1224. #
  1225. # # 向右滑动验证码
  1226. # retry_captcha(d, '//*[contains(@text, "请向右滑动滑块")]', Swipe_right, captcha_name="Swipe_right")
  1227. #
  1228. # # 滑块验证码
  1229. # retry_captcha(d, '//*[@text="请拖动下方滑块完成拼图"]', hk, captcha_name="slider")
  1230. #
  1231. # # 空间推理验证码
  1232. # retry_captcha(d, '//*[contains(@text, "请点击")]', click_side, captcha_name="click_side")
  1233. #
  1234. # # 复杂拖动滑块验证码
  1235. # retry_captcha(d, '//*[contains(@text, "拖动滑块")]', complexs, captcha_name="complexs")
  1236. #
  1237. # # 输入型验证码
  1238. # retry_captcha(d, '//*[@text="请输入图片中的内容"]', srwz, captcha_name="text_input")
  1239. #
  1240. # # 图标点选验证码
  1241. # retry_captcha(d, '//*[@text="请依次点击下图图标"]', dianxuan, captcha_name="icon_click")
  1242. #
  1243. # # 文字点选验证码
  1244. # retry_captcha(d, '//*[@text="请按语序依次点击下图文字"]', wenzidianxuan, captcha_name="text_click")
  1245. #
  1246. # # 最短线连接验证码
  1247. # retry_captcha(d, '//*[contains(@text, "用最短线连接")]', lianxian, captcha_name="Shortest_connection")
  1248. #
  1249. # def lianxian(d):
  1250. # art_text_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  1251. # color_points_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]/android.widget.Image[1]'
  1252. #
  1253. # art_text_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="art_text")
  1254. # color_points_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="color_points")
  1255. #
  1256. # art_text_img_path, _ = _capture_by_bounds(
  1257. # d,
  1258. # art_text_xpath,
  1259. # output_path=art_text_img_path,
  1260. # captcha_type="Shortest_connection"
  1261. # )
  1262. # color_points_img_path, color_bounds = _capture_by_bounds(
  1263. # d,
  1264. # color_points_xpath,
  1265. # output_path=color_points_img_path,
  1266. # captcha_type="Shortest_connection"
  1267. # )
  1268. # if not art_text_img_path or not color_points_img_path or not color_bounds:
  1269. # return False
  1270. #
  1271. # element_left = color_bounds["left"]
  1272. # element_top = color_bounds["top"]
  1273. # element_width = color_bounds["right"] - color_bounds["left"]
  1274. # element_height = color_bounds["bottom"] - color_bounds["top"]
  1275. #
  1276. # api_result = post_api(art_text_img_path, "10118")
  1277. # color_name = _extract_color_name(api_result)
  1278. # if not color_name:
  1279. # return False
  1280. #
  1281. # relative_points = _find_color_coordinates(color_points_img_path, color_name)
  1282. # if len(relative_points) < 2:
  1283. # return False
  1284. #
  1285. # color_img = cv2.imread(color_points_img_path)
  1286. # if color_img is None:
  1287. # return False
  1288. # img_h, img_w = color_img.shape[:2]
  1289. # if img_w <= 0 or img_h <= 0:
  1290. # return False
  1291. #
  1292. # screen_points = []
  1293. # for rx, ry in relative_points:
  1294. # sx = element_left + int(rx * (element_width / img_w))
  1295. # sy = element_top + int(ry * (element_height / img_h))
  1296. # screen_points.append((sx, sy))
  1297. #
  1298. # path = _nearest_neighbor_path(screen_points)
  1299. # curved = _human_like_path(path)
  1300. # return _simulate_human_drawing(d, curved)
  1301. #
  1302. #
  1303. # if __name__ == '__main__':
  1304. # d = u2.connect("95b2c764")
  1305. # yzm(d)
  1306. import base64
  1307. import io
  1308. import math
  1309. import os
  1310. import random
  1311. import re
  1312. import threading
  1313. import time
  1314. import cv2
  1315. import numpy as np
  1316. import requests
  1317. from PIL import Image, ImageDraw
  1318. import uiautomator2 as u2
  1319. API_URL = "http://api.jfbym.com/api/YmServer/customApi"
  1320. # 验证码平台 token
  1321. API_TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
  1322. # 原始截图保存路径
  1323. SCREENSHOT_PATH = "./a.jpg"
  1324. # 裁剪后图片保存路径z
  1325. CROP_PATH = "./b.jpg"
  1326. # 滑动轨迹图保存目录
  1327. TRACK_DIR = "./slider_tracks"
  1328. # 验证码截图日志根目录
  1329. CAPTCHA_LOG_ROOT = "./captcha_logs"
  1330. UNKNOWN_DEVICE_ID = "unknown_device"
  1331. _RUNTIME_CONTEXT = threading.local()
  1332. SLIDER_METHODS = ("track", "bezier")
  1333. # 图标点选验证码
  1334. CAPTCHA_ICON_CLICK = "icon_click"
  1335. # 空间推理验证码
  1336. CAPTCHA_SPACE_REASON = "space_reason"
  1337. # 文字点选验证码
  1338. CAPTCHA_TEXT_CLICK = "text_click"
  1339. # 滑块验证码
  1340. CAPTCHA_SLIDER = "slider"
  1341. # 图片内容输入验证码
  1342. CAPTCHA_TEXT_INPUT = "text_input"
  1343. # 输入型验证码的裁剪区域
  1344. INPUT_CROP = {
  1345. "x_start": 210,
  1346. "x_end": 510,
  1347. "y_start": 666,
  1348. "y_end": 788,
  1349. }
  1350. # 图标点选验证码的裁剪区域
  1351. ICON_CLICK_CROP = {
  1352. "x_start": 40,
  1353. "x_end": 680,
  1354. "y_start": 471,
  1355. "y_end": 1143,
  1356. }
  1357. # 不同验证码类型对应的平台参数配置
  1358. VERIFY_CONFIG = {
  1359. CAPTCHA_ICON_CLICK: {"type": "88888", "direction": None, "extra": None, "y_offset": 0, "result": "points"},
  1360. CAPTCHA_SPACE_REASON: {"type": "88888", "direction": None, "extra": "请点击", "y_offset": 0, "result": "points"},
  1361. CAPTCHA_TEXT_CLICK: {"type": "30114", "direction": None, "extra": "phrase", "y_offset": 0, "result": "points"},
  1362. CAPTCHA_SLIDER: {"type": "22222", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  1363. CAPTCHA_TEXT_INPUT: {"type": "10103", "direction": None, "extra": None, "y_offset": 0, "result": "text"},
  1364. # lianxian : {"type": "10114", "direction": None, "extra": None, "y_offset": 0, "result": "text"}
  1365. }
  1366. def _safe_name(value, default):
  1367. if value is None:
  1368. return default
  1369. text = str(value).strip()
  1370. if not text:
  1371. return default
  1372. text = re.sub(r"[^0-9A-Za-z_\-.]+", "_", text)
  1373. return text or default
  1374. def _resolve_device_id(d=None, device_id=None):
  1375. if device_id:
  1376. return _safe_name(device_id, UNKNOWN_DEVICE_ID)
  1377. candidates = []
  1378. if d is not None:
  1379. for attr in ("serial", "_serial", "device_id"):
  1380. value = getattr(d, attr, None)
  1381. if value:
  1382. candidates.append(value)
  1383. try:
  1384. info = d.device_info
  1385. if isinstance(info, dict):
  1386. for key in ("serial", "serialno", "udid", "deviceId"):
  1387. value = info.get(key)
  1388. if value:
  1389. candidates.append(value)
  1390. except Exception:
  1391. pass
  1392. for value in candidates:
  1393. normalized = _safe_name(value, "")
  1394. if normalized:
  1395. return normalized
  1396. return UNKNOWN_DEVICE_ID
  1397. def _set_runtime_device_id(d=None, device_id=None):
  1398. _RUNTIME_CONTEXT.device_id = _resolve_device_id(d=d, device_id=device_id)
  1399. def _get_runtime_device_id(d=None):
  1400. current = getattr(_RUNTIME_CONTEXT, "device_id", None)
  1401. if current:
  1402. return current
  1403. resolved = _resolve_device_id(d=d)
  1404. _RUNTIME_CONTEXT.device_id = resolved
  1405. return resolved
  1406. def _next_slider_method_order():
  1407. idx = getattr(_RUNTIME_CONTEXT, "slider_method_idx", 0)
  1408. first = SLIDER_METHODS[idx % len(SLIDER_METHODS)]
  1409. second = SLIDER_METHODS[(idx + 1) % len(SLIDER_METHODS)]
  1410. _RUNTIME_CONTEXT.slider_method_idx = (idx + 1) % len(SLIDER_METHODS)
  1411. return [first, second]
  1412. def _ensure_captcha_dir(captcha_type):
  1413. safe_type = _safe_name(captcha_type, "unknown_captcha")
  1414. folder = os.path.join(CAPTCHA_LOG_ROOT, safe_type)
  1415. os.makedirs(folder, exist_ok=True)
  1416. return folder
  1417. def _build_captcha_image_path(captcha_type, d=None, ext=".png", tag=None):
  1418. device_id = _get_runtime_device_id(d=d)
  1419. random_part = random.randint(10000000, 99999999)
  1420. safe_tag = _safe_name(tag, "") if tag else ""
  1421. suffix = f"_{safe_tag}" if safe_tag else ""
  1422. filename = f"{device_id}_{random_part}{suffix}{ext}"
  1423. return os.path.join(_ensure_captcha_dir(captcha_type), filename)
  1424. def _save_debug_screenshot(d, captcha_type, tag="full"):
  1425. path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag=tag)
  1426. try:
  1427. d.screenshot(path)
  1428. print(f"[captcha-shot] saved: {path}")
  1429. return path
  1430. except Exception as e:
  1431. print(f"[captcha-shot] save failed: {e}")
  1432. return None
  1433. def post_api(image_path, captcha_type, extra=None, direction=None, label_image_path=None, timeout=20):
  1434. with open(image_path, 'rb') as f:
  1435. image_base64 = base64.b64encode(f.read()).decode()
  1436. data = {
  1437. "token": API_TOKEN,
  1438. "type": captcha_type,
  1439. "image": image_base64,
  1440. }
  1441. if label_image_path:
  1442. with open(label_image_path, 'rb') as f:
  1443. data["label_image"] = base64.b64encode(f.read()).decode()
  1444. if extra is not None:
  1445. data["extra"] = extra
  1446. if direction is not None:
  1447. data["direction"] = direction
  1448. headers = {
  1449. "Content-Type": "application/json"
  1450. }
  1451. response = requests.post(API_URL, headers=headers, json=data, timeout=timeout).json()
  1452. print(response)
  1453. return response
  1454. def parse_points(response, y_offset=0):
  1455. tuple_points = []
  1456. data = response.get("data", {}).get("data", "")
  1457. if not data:
  1458. return tuple_points
  1459. for s in data.split('|'):
  1460. x, y = s.split(',')
  1461. tuple_points.append((int(x), int(y) + y_offset))
  1462. return tuple_points
  1463. def verify(image_path, captcha_type):
  1464. config = VERIFY_CONFIG.get(captcha_type)
  1465. if not config:
  1466. raise ValueError(f"不支持的验证码类型: {captcha_type}")
  1467. response = post_api(
  1468. image_path,
  1469. config["type"],
  1470. extra=config["extra"],
  1471. direction=config["direction"]
  1472. )
  1473. if config["result"] == "points":
  1474. return parse_points(response, y_offset=config["y_offset"])
  1475. return response.get("data", {}).get("data")
  1476. def crop_image_xy(
  1477. image_path,
  1478. output_path=None,
  1479. x_start=None,
  1480. x_end=None,
  1481. y_start=471,
  1482. y_end=1143
  1483. ):
  1484. if output_path is None:
  1485. dir_name, file_name = os.path.split(image_path)
  1486. name, ext = os.path.splitext(file_name)
  1487. output_path = os.path.join(dir_name, f"{name}_cropped{ext}")
  1488. with Image.open(image_path) as img:
  1489. width, height = img.size
  1490. if x_start is None:
  1491. x_start = 0
  1492. if x_end is None:
  1493. x_end = width - 1
  1494. x_start = max(0, min(x_start, width - 1))
  1495. x_end = max(x_start, min(x_end, width - 1))
  1496. y_start = max(0, min(y_start, height - 1))
  1497. y_end = max(y_start, min(y_end, height - 1))
  1498. cropped = img.crop((x_start, y_start, x_end + 1, y_end + 1))
  1499. cropped.save(output_path)
  1500. return output_path
  1501. def _capture_by_bounds(d, xpath_candidates, output_path=None, screenshot_path=None, captcha_type="generic"):
  1502. """
  1503. 按元素 bounds 截图并裁剪。
  1504. return: (cropped_path, bounds_dict) or (None, None)
  1505. """
  1506. if isinstance(xpath_candidates, str):
  1507. xpath_candidates = [xpath_candidates]
  1508. if screenshot_path is None:
  1509. screenshot_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="full")
  1510. if output_path is None:
  1511. output_path = _build_captcha_image_path(captcha_type, d=d, ext=".png", tag="crop")
  1512. for xpath in xpath_candidates:
  1513. try:
  1514. node = d.xpath(xpath)
  1515. if not node.exists:
  1516. continue
  1517. bounds = node.info.get("bounds", {})
  1518. if not bounds:
  1519. continue
  1520. d.screenshot(screenshot_path)
  1521. cropped = crop_image_xy(
  1522. screenshot_path,
  1523. output_path=output_path,
  1524. x_start=bounds["left"],
  1525. x_end=bounds["right"],
  1526. y_start=bounds["top"],
  1527. y_end=bounds["bottom"],
  1528. )
  1529. return cropped, bounds
  1530. except Exception:
  1531. continue
  1532. return None, None
  1533. def srwz(d):
  1534. captcha_image_xpaths = [
  1535. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.widget.Image[1]',
  1536. '//*[@resource-id="captchaImg"]',
  1537. ]
  1538. image_path, _ = _capture_by_bounds(d, captcha_image_xpaths, captcha_type=CAPTCHA_TEXT_INPUT)
  1539. if not image_path:
  1540. return False
  1541. data = verify(image_path, CAPTCHA_TEXT_INPUT)
  1542. input_box = d.xpath('//*[@hint="请输入验证码"]')
  1543. if input_box.exists:
  1544. input_box.click()
  1545. time.sleep(0.5)
  1546. input_box.set_text(data)
  1547. d.xpath('//*[@text="验证"]').click()
  1548. return True
  1549. else:
  1550. print("未找到输入框")
  1551. return False
  1552. def _clamp(value, min_value, max_value):
  1553. return max(min_value, min(value, max_value))
  1554. def _slider_duration(distance):
  1555. if distance <= 90:
  1556. return round(random.uniform(0.18, 0.27), 3)
  1557. if distance <= 160:
  1558. return round(random.uniform(0.23, 0.34), 3)
  1559. return round(random.uniform(0.28, 0.42), 3)
  1560. def _build_human_slider_track(start_x, start_y, distance):
  1561. # Keep the reference shape: mostly flat first, then a single smooth downward bend.
  1562. overshoot = random.randint(1, 2) if distance > 140 and random.random() < 0.18 else 0
  1563. move_distance = distance + overshoot
  1564. steps = int(_clamp(move_distance / random.uniform(6.0, 8.0), 16, 32))
  1565. base_y = start_y + random.randint(-1, 1)
  1566. flat_ratio = random.uniform(0.30, 0.42) # front section almost horizontal
  1567. drop = distance * random.uniform(0.08, 0.14)
  1568. if distance > 260:
  1569. drop *= random.uniform(1.35, 1.75)
  1570. drop = _clamp(drop, 12.0, 58.0) # obvious tail drop
  1571. noise_amp = random.uniform(0.03, 0.22)
  1572. points = [(start_x, base_y)]
  1573. last_x = start_x
  1574. for i in range(1, steps + 1):
  1575. t = i / steps
  1576. progress = 1 - (1 - t) ** 2.0
  1577. progress += random.uniform(-0.0018, 0.0018)
  1578. progress = _clamp(progress, 0.0, 1.0)
  1579. x = start_x + int(move_distance * progress)
  1580. if x <= last_x:
  1581. x = last_x + 1
  1582. last_x = x
  1583. if t < flat_ratio:
  1584. # slight tiny rise then back, still near flat
  1585. u = t / max(flat_ratio, 1e-6)
  1586. y_curve = -0.9 * math.sin(math.pi * u)
  1587. else:
  1588. u = (t - flat_ratio) / max(1 - flat_ratio, 1e-6)
  1589. y_curve = drop * (u ** 1.7)
  1590. y_noise = random.uniform(-noise_amp, noise_amp)
  1591. y = int(round(base_y + y_curve + y_noise))
  1592. points.append((x, y))
  1593. final_x = start_x + distance
  1594. final_y = int(round(base_y + drop + random.uniform(-0.6, 0.6)))
  1595. if overshoot > 0:
  1596. points.append((start_x + distance + overshoot, final_y))
  1597. points.append((final_x, final_y + random.choice([0, 0, 1])))
  1598. return points
  1599. def _save_slider_track_image(
  1600. points,
  1601. distance=None,
  1602. duration=None,
  1603. drag_ok=True,
  1604. screenshot_path=SCREENSHOT_PATH,
  1605. captcha_type=CAPTCHA_SLIDER
  1606. ):
  1607. if not points:
  1608. return None
  1609. output_path = _build_captcha_image_path(captcha_type, ext=".png", tag="track")
  1610. try:
  1611. if os.path.exists(screenshot_path):
  1612. with Image.open(screenshot_path) as img:
  1613. canvas = img.convert("RGB")
  1614. else:
  1615. canvas = Image.new("RGB", (720, 1280), "white")
  1616. draw = ImageDraw.Draw(canvas)
  1617. if len(points) >= 2:
  1618. draw.line(points, fill=(245, 20, 30), width=9)
  1619. sx, sy = points[0]
  1620. ex, ey = points[-1]
  1621. draw.ellipse((sx - 5, sy - 5, sx + 5, sy + 5), fill=(40, 200, 80))
  1622. draw.ellipse((ex - 5, ey - 5, ex + 5, ey + 5), fill=(50, 120, 255))
  1623. info = f"ok={drag_ok} dist={distance} dur={duration}s points={len(points)}"
  1624. draw.rectangle((8, 8, min(canvas.size[0] - 8, 520), 42), fill=(0, 0, 0))
  1625. draw.text((14, 14), info, fill=(255, 255, 255))
  1626. canvas.save(output_path)
  1627. print(f"[slider-track] saved: {output_path}")
  1628. return output_path
  1629. except Exception as e:
  1630. print(f"[slider-track] save failed: {e}")
  1631. return None
  1632. def _downsample_track_points(points, target_count):
  1633. if not points or len(points) <= target_count:
  1634. return points[:]
  1635. if target_count < 2:
  1636. return [points[0], points[-1]]
  1637. sampled = []
  1638. last_index = len(points) - 1
  1639. for i in range(target_count):
  1640. idx = int(round(i * last_index / (target_count - 1)))
  1641. sampled.append(points[idx])
  1642. return sampled
  1643. def _execute_track(d, points, total_duration):
  1644. if not points or len(points) < 2:
  1645. return False, points
  1646. duration = max(0.18, float(total_duration))
  1647. # Use fewer points to avoid step explosion, keep curve shape.
  1648. max_points = random.randint(14, 22)
  1649. exec_points = _downsample_track_points(points, max_points)
  1650. try:
  1651. if hasattr(d, "swipe_points"):
  1652. # u2: duration here means time-per-step; steps = duration/0.005.
  1653. # To approximate total duration:
  1654. # total ~= (duration/0.005) * (len(exec_points)-1) * 0.005 = duration * segments
  1655. seg_count = max(1, len(exec_points) - 1)
  1656. per_segment = max(0.01, duration / seg_count)
  1657. d.swipe_points(exec_points, duration=per_segment)
  1658. return True, exec_points
  1659. except Exception:
  1660. pass
  1661. # fallback
  1662. try:
  1663. sx, sy = exec_points[0]
  1664. ex, ey = exec_points[-1]
  1665. d.swipe(sx, sy, ex, ey, duration=duration)
  1666. return False, exec_points
  1667. except Exception:
  1668. return False, exec_points
  1669. def _slider_still_exists(d):
  1670. xpath_candidates = [
  1671. '//*[@text="请拖动下方滑块完成拼图"]',
  1672. '//*[contains(@text, "拖动下方滑块")]',
  1673. '//*[@resource-id="puzzleSliderBox"]',
  1674. '//*[@resource-id="puzzleImageMain"]',
  1675. ]
  1676. for xpath in xpath_candidates:
  1677. try:
  1678. if d.xpath(xpath).exists:
  1679. return True
  1680. except Exception:
  1681. continue
  1682. return False
  1683. def _cubic_bezier(t, p0, p1, p2, p3):
  1684. one_minus_t = 1 - t
  1685. x = (
  1686. one_minus_t ** 3 * p0[0]
  1687. + 3 * one_minus_t ** 2 * t * p1[0]
  1688. + 3 * one_minus_t * t ** 2 * p2[0]
  1689. + t ** 3 * p3[0]
  1690. )
  1691. y = (
  1692. one_minus_t ** 3 * p0[1]
  1693. + 3 * one_minus_t ** 2 * t * p1[1]
  1694. + 3 * one_minus_t * t ** 2 * p2[1]
  1695. + t ** 3 * p3[1]
  1696. )
  1697. return x, y
  1698. def _generate_bezier_slider_points(start, end, deviation=30, steps=50):
  1699. sx, sy = start
  1700. ex, ey = end
  1701. mid_x = (sx + ex) / 2
  1702. mid_y = (sy + ey) / 2
  1703. p1 = (
  1704. mid_x - (ex - sx) / 4 + random.uniform(-deviation, deviation),
  1705. mid_y - (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  1706. )
  1707. p2 = (
  1708. mid_x + (ex - sx) / 4 + random.uniform(-deviation, deviation),
  1709. mid_y + (ey - sy) / 4 + random.uniform(-deviation / 2, deviation / 2),
  1710. )
  1711. points = []
  1712. for i in range(steps + 1):
  1713. t = i / steps
  1714. x, y = _cubic_bezier(t, start, p1, p2, end)
  1715. if 0 < i < steps:
  1716. x += random.gauss(0, 1.5)
  1717. y += random.gauss(0, 1.5)
  1718. points.append((int(round(x)), int(round(y))))
  1719. return points
  1720. def _execute_bezier_slider(d, start_x, start_y, end_x, end_y):
  1721. points = _generate_bezier_slider_points(
  1722. (int(round(start_x)), int(round(start_y))),
  1723. (int(round(end_x)), int(round(end_y))),
  1724. deviation=random.randint(20, 40),
  1725. steps=50,
  1726. )
  1727. if len(points) < 2:
  1728. return False, points
  1729. try:
  1730. d.touch.down(points[0][0], points[0][1])
  1731. time.sleep(random.uniform(0.1, 0.2))
  1732. total = max(1, len(points) - 1)
  1733. for i, (x, y) in enumerate(points[1:], 1):
  1734. d.touch.move(x, y)
  1735. t = i / total
  1736. if 0.2 < t < 0.8:
  1737. interval = random.uniform(0.02, 0.04)
  1738. else:
  1739. interval = random.uniform(0.04, 0.08)
  1740. time.sleep(interval)
  1741. time.sleep(random.uniform(0.05, 0.15))
  1742. d.touch.up(points[-1][0], points[-1][1])
  1743. return True, points
  1744. except Exception:
  1745. try:
  1746. d.touch.up(points[-1][0], points[-1][1])
  1747. except Exception:
  1748. pass
  1749. return False, points
  1750. def _slider_knob_center(d):
  1751. slider_xpath = (
  1752. '//*[@resource-id="puzzleSliderBox"] | '
  1753. '//*[@resource-id="yodaBox"] | '
  1754. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]'
  1755. )
  1756. try:
  1757. slider_node = d.xpath(slider_xpath)
  1758. if slider_node.exists:
  1759. bounds = slider_node.info.get("bounds", {})
  1760. if bounds:
  1761. return (
  1762. (bounds["left"] + bounds["right"]) / 2 + random.uniform(-4, 4),
  1763. (bounds["top"] + bounds["bottom"]) / 2 + random.uniform(-3, 3),
  1764. )
  1765. except Exception:
  1766. pass
  1767. return None
  1768. def _first_existing_bounds(d, xpath_candidates):
  1769. if isinstance(xpath_candidates, str):
  1770. xpath_candidates = [xpath_candidates]
  1771. for xpath in xpath_candidates:
  1772. try:
  1773. node = d.xpath(xpath)
  1774. if not node.exists:
  1775. continue
  1776. bounds = node.info.get("bounds", {})
  1777. if bounds:
  1778. return xpath, bounds
  1779. except Exception:
  1780. continue
  1781. return None, None
  1782. def _click_captcha_close(d, captcha_xpath=None):
  1783. """点击验证码右上角关闭按钮;优先点显式关闭控件,失败后按容器右上角估算点位。"""
  1784. close_xpaths = [
  1785. '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]',
  1786. '//*[@resource-id="btn_close_verify"]',
  1787. '//*[@content-desc="关闭"]',
  1788. '//*[@text="关闭"]',
  1789. ]
  1790. _, close_bounds = _first_existing_bounds(d, close_xpaths)
  1791. if close_bounds:
  1792. cx = int((close_bounds["left"] + close_bounds["right"]) / 2) + random.randint(-2, 2)
  1793. cy = int((close_bounds["top"] + close_bounds["bottom"]) / 2) + random.randint(-2, 2)
  1794. d.click(cx, cy)
  1795. print(f"[captcha-close] click explicit close at ({cx}, {cy})")
  1796. return True
  1797. popup_xpaths = [
  1798. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  1799. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  1800. '//*[@resource-id="root"]',
  1801. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]',
  1802. '//*[@text="身份核实"]/android.view.View[1]',
  1803. ]
  1804. if captcha_xpath:
  1805. popup_xpaths.append(captcha_xpath)
  1806. _, popup_bounds = _first_existing_bounds(d, popup_xpaths)
  1807. if not popup_bounds:
  1808. return False
  1809. left = popup_bounds["left"]
  1810. right = popup_bounds["right"]
  1811. top = popup_bounds["top"]
  1812. bottom = popup_bounds["bottom"]
  1813. width = max(1, right - left)
  1814. height = max(1, bottom - top)
  1815. # 参考示例:[40,391][680,1223] -> 右上角叉号中心约(640, 431)。
  1816. offset_x = int(_clamp(width * 0.06, 20, 56))
  1817. offset_y = int(_clamp(height * 0.05, 20, 56))
  1818. click_x = int(right - offset_x) + random.randint(-3, 3)
  1819. click_y = int(top + offset_y) + random.randint(-3, 3)
  1820. d.click(click_x, click_y)
  1821. print(f"[captcha-close] click inferred close at ({click_x}, {click_y})")
  1822. return True
  1823. def _build_directional_track(start_x, start_y, end_x, end_y):
  1824. distance_x = end_x - start_x
  1825. distance_y = end_y - start_y
  1826. if abs(distance_x) < 2 and abs(distance_y) < 2:
  1827. return [(int(start_x), int(start_y)), (int(end_x), int(end_y))]
  1828. steps = int(_clamp(abs(distance_x) / random.uniform(7.0, 10.0), 22, 48))
  1829. points = [(int(start_x), int(start_y))]
  1830. last_x = float(start_x)
  1831. for i in range(1, steps + 1):
  1832. t = i / steps
  1833. progress = 1 - (1 - t) ** random.uniform(1.8, 2.25)
  1834. x = start_x + distance_x * progress + random.uniform(-0.9, 0.9)
  1835. y = start_y + distance_y * progress + random.uniform(-0.8, 0.8)
  1836. if distance_x >= 0:
  1837. if x < last_x:
  1838. x = last_x + random.uniform(0.2, 1.2)
  1839. else:
  1840. if x > last_x:
  1841. x = last_x - random.uniform(0.2, 1.2)
  1842. last_x = x
  1843. points.append((int(round(x)), int(round(y))))
  1844. points.append((int(round(end_x)), int(round(end_y))))
  1845. return points
  1846. def _move_with_pressed_touch(d, points):
  1847. if not points:
  1848. return
  1849. for x, y in points:
  1850. d.touch.move(x, y)
  1851. time.sleep(random.uniform(0.0015, 0.0045))
  1852. def _screenshot_to_image(d):
  1853. shot = d.screenshot()
  1854. if isinstance(shot, Image.Image):
  1855. return shot.convert("RGB")
  1856. if isinstance(shot, bytes):
  1857. return Image.open(io.BytesIO(shot)).convert("RGB")
  1858. if isinstance(shot, str) and os.path.exists(shot):
  1859. return Image.open(shot).convert("RGB")
  1860. if hasattr(shot, "convert"):
  1861. return shot.convert("RGB")
  1862. fallback_path = _build_captcha_image_path("generic", d=d, ext=".png", tag="fallback")
  1863. d.screenshot(fallback_path)
  1864. return Image.open(fallback_path).convert("RGB")
  1865. def hk(d):
  1866. screenshot_path = _build_captcha_image_path(CAPTCHA_SLIDER, d=d, ext=".png", tag="full")
  1867. d.screenshot(screenshot_path)
  1868. data = verify(screenshot_path, CAPTCHA_SLIDER)
  1869. if not data:
  1870. return False
  1871. try:
  1872. raw_distance = float(data)
  1873. except (TypeError, ValueError):
  1874. return False
  1875. if raw_distance <= 0:
  1876. return False
  1877. image_width = 720
  1878. try:
  1879. with Image.open(screenshot_path) as img:
  1880. image_width = img.size[0] or 720
  1881. except Exception:
  1882. pass
  1883. try:
  1884. screen_width = int(d.info.get("displayWidth", image_width))
  1885. except Exception:
  1886. screen_width = image_width
  1887. scale = screen_width / image_width if image_width else 1.0
  1888. slide_distance = int(raw_distance * scale)
  1889. if slide_distance < 80:
  1890. slide_distance += random.randint(3, 6)
  1891. elif slide_distance < 160:
  1892. slide_distance += random.randint(2, 5)
  1893. else:
  1894. slide_distance += random.randint(1, 4)
  1895. start_x = 84 + random.randint(-1, 1)
  1896. start_y = 1034 + random.randint(-2, 2)
  1897. knob_center = _slider_knob_center(d)
  1898. if knob_center:
  1899. start_x, start_y = knob_center
  1900. max_target_x = screen_width - random.randint(26, 42)
  1901. target_x = _clamp(start_x + slide_distance, start_x + 18, max_target_x)
  1902. distance = target_x - start_x
  1903. if distance < 18:
  1904. return False
  1905. method_order = _next_slider_method_order()
  1906. for method in method_order:
  1907. if method == "track":
  1908. points = _build_human_slider_track(int(round(start_x)), int(round(start_y)), int(round(distance)))
  1909. duration = _slider_duration(distance)
  1910. time.sleep(random.uniform(0.015, 0.05))
  1911. drag_ok, exec_points = _execute_track(d, points, duration)
  1912. _save_slider_track_image(
  1913. exec_points,
  1914. distance=distance,
  1915. duration=duration,
  1916. drag_ok=drag_ok,
  1917. screenshot_path=screenshot_path,
  1918. captcha_type=CAPTCHA_SLIDER
  1919. )
  1920. else:
  1921. end_x = target_x + random.uniform(-3, 3)
  1922. end_y = start_y + random.uniform(-1, 1)
  1923. drag_ok, exec_points = _execute_bezier_slider(d, start_x, start_y, end_x, end_y)
  1924. _save_slider_track_image(
  1925. exec_points,
  1926. distance=distance,
  1927. duration=None,
  1928. drag_ok=drag_ok,
  1929. screenshot_path=screenshot_path,
  1930. captcha_type=CAPTCHA_SLIDER
  1931. )
  1932. time.sleep(random.uniform(1.0, 1.8))
  1933. if not _slider_still_exists(d):
  1934. return True
  1935. print(f"[slider] method {method} failed, switch to next")
  1936. return False
  1937. def dianxuan(d):
  1938. click_area_xpaths = [
  1939. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  1940. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  1941. '//*[@resource-id="root"]',
  1942. ]
  1943. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_ICON_CLICK)
  1944. if not image_path or not bounds:
  1945. return False
  1946. left = bounds["left"]
  1947. top = bounds["top"]
  1948. data = verify(image_path, CAPTCHA_ICON_CLICK)
  1949. if not data:
  1950. return False
  1951. for x, y in data:
  1952. time.sleep(random.randint(1, 2))
  1953. d.click(left + x + random.randint(-7, 7), top + y + random.randint(-7, 7))
  1954. return True
  1955. def wenzidianxuan(d):
  1956. # 文字点选:按验证码容器 bounds 裁剪后,调用 88888,按返回坐标依次点击
  1957. click_area_xpaths = [
  1958. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  1959. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  1960. '//*[@resource-id="root"]',
  1961. ]
  1962. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, captcha_type=CAPTCHA_TEXT_CLICK)
  1963. if not image_path or not bounds:
  1964. return False
  1965. image_left = bounds["left"]
  1966. image_top = bounds["top"]
  1967. result = post_api(image_path, "88888")
  1968. verify_data = result.get("data", {})
  1969. if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  1970. return False
  1971. coords_str = verify_data.get("data", "")
  1972. if not coords_str:
  1973. return False
  1974. clicked = 0
  1975. for coord in coords_str.split("|"):
  1976. try:
  1977. x_img_str, y_img_str = coord.split(",")
  1978. x_img = int(x_img_str.strip())
  1979. y_img = int(y_img_str.strip())
  1980. x_screen = image_left + x_img + random.randint(-5, 5)
  1981. y_screen = image_top + y_img + random.randint(-5, 5)
  1982. d.click(x_screen, y_screen)
  1983. clicked += 1
  1984. time.sleep(random.uniform(0.8, 1.6))
  1985. except Exception:
  1986. continue
  1987. return clicked > 0
  1988. def click_side(d):
  1989. """空间推理验证码(请点击数字)。"""
  1990. click_area_xpaths = [
  1991. '//*[@resource-id="com.sankuai.meituan:id/titans_main_layout"]',
  1992. '//*[@resource-id="com.sankuai.meituan:id/h5_container"]',
  1993. '//*[@resource-id="root"]',
  1994. ]
  1995. image_path, bounds = _capture_by_bounds(d, click_area_xpaths, output_path=CROP_PATH)
  1996. if not image_path or not bounds:
  1997. return False
  1998. left = bounds["left"]
  1999. top = bounds["top"]
  2000. points = verify(image_path, CAPTCHA_SPACE_REASON)
  2001. if not points:
  2002. return False
  2003. x, y = points[0]
  2004. d.click(left + x + random.randint(-2, 2), top + y + random.randint(-2, 2))
  2005. # d_list = [
  2006. # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]',
  2007. # '//*[@resource-id="com.sankuai.meituan:id/yoda_toolbar_title"]',
  2008. # '//*[@resource-id="com.sankuai.meituan:id/btn_close_verify"]'
  2009. # ]
  2010. # for i in d_list:
  2011. # d.xpath(i).click()
  2012. return True
  2013. def Swipe_right(d):
  2014. """向右拖动到最右侧(非拼图滑块)。"""
  2015. track_xpath = (
  2016. '//*[@resource-id="yodaBoxWrapper"] | '
  2017. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  2018. )
  2019. slider_xpath = (
  2020. '//*[@resource-id="yodaBox"] | '
  2021. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  2022. )
  2023. if not d.xpath(track_xpath).exists or not d.xpath(slider_xpath).exists:
  2024. return False
  2025. track_bounds = d.xpath(track_xpath).info.get("bounds", {})
  2026. slider_bounds = d.xpath(slider_xpath).info.get("bounds", {})
  2027. if not track_bounds or not slider_bounds:
  2028. return False
  2029. start_x = int((slider_bounds["left"] + slider_bounds["right"]) / 2) + random.randint(-2, 2)
  2030. start_y = int((slider_bounds["top"] + slider_bounds["bottom"]) / 2) + random.randint(-2, 2)
  2031. right_limit = int(track_bounds["right"]) - random.randint(4, 10)
  2032. distance = right_limit - start_x
  2033. if distance <= 10:
  2034. return False
  2035. screenshot_path = _save_debug_screenshot(d, "Swipe_right", tag="full")
  2036. points = _build_human_slider_track(start_x, start_y, distance)
  2037. duration = _slider_duration(distance)
  2038. drag_ok, exec_points = _execute_track(d, points, duration)
  2039. _save_slider_track_image(
  2040. exec_points,
  2041. distance=distance,
  2042. duration=duration,
  2043. drag_ok=drag_ok,
  2044. screenshot_path=screenshot_path or SCREENSHOT_PATH,
  2045. captcha_type="Swipe_right"
  2046. )
  2047. return True
  2048. def complexs(d):
  2049. """Complex slider flow: move to far-right, OCR with label image, then drag back to target."""
  2050. slider_xpath_candidates = [
  2051. '//*[@resource-id="yodaBox"]',
  2052. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]',
  2053. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  2054. ]
  2055. track_xpath_candidates = [
  2056. '//*[@resource-id="yodaBoxWrapper"]',
  2057. '//*[contains(@text, "请按照说明拖动滑块")]',
  2058. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  2059. ]
  2060. label_xpath_candidates = [
  2061. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.widget.TextView[1]',
  2062. ]
  2063. image_xpath_candidates = [
  2064. '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]',
  2065. ]
  2066. _, slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  2067. _, track_bounds = _first_existing_bounds(d, track_xpath_candidates)
  2068. if not slider_bounds or not track_bounds:
  2069. return False
  2070. slider_left = slider_bounds["left"]
  2071. slider_top = slider_bounds["top"]
  2072. slider_right = slider_bounds["right"]
  2073. slider_bottom = slider_bounds["bottom"]
  2074. slider_width = slider_right - slider_left
  2075. slider_center_x = (slider_left + slider_right) / 2
  2076. slider_center_y = (slider_top + slider_bottom) / 2
  2077. track_left = track_bounds["left"]
  2078. track_right = track_bounds["right"]
  2079. right_end_center_x = track_right - slider_width / 2
  2080. right_end_center_y = slider_center_y
  2081. print(f"滑块中心: ({slider_center_x}, {slider_center_y})")
  2082. print(f"最右端滑块中心坐标: ({right_end_center_x}, {right_end_center_y})")
  2083. touch_down = False
  2084. try:
  2085. d.touch.down(slider_center_x, slider_center_y)
  2086. touch_down = True
  2087. time.sleep(random.uniform(0.08, 0.16))
  2088. move_right_points = _build_directional_track(
  2089. slider_center_x,
  2090. slider_center_y,
  2091. right_end_center_x,
  2092. right_end_center_y,
  2093. )
  2094. _move_with_pressed_touch(d, move_right_points[1:])
  2095. print("滑块已到达最右端")
  2096. _, label_bounds = _first_existing_bounds(d, label_xpath_candidates)
  2097. _, image_bounds = _first_existing_bounds(d, image_xpath_candidates)
  2098. if not label_bounds or not image_bounds:
  2099. return False
  2100. capture_label_left = label_bounds["left"]
  2101. capture_label_top = label_bounds["top"]
  2102. capture_label_right = label_bounds["right"]
  2103. capture_label_bottom = label_bounds["bottom"]
  2104. capture_left = image_bounds["left"]
  2105. capture_top = image_bounds["top"]
  2106. capture_right = image_bounds["right"]
  2107. capture_bottom = image_bounds["bottom"]
  2108. print(
  2109. "截图区域1(提示文本): "
  2110. f"left={capture_label_left}, top={capture_label_top}, "
  2111. f"width={capture_label_right - capture_label_left}, "
  2112. f"height={capture_label_bottom - capture_label_top}"
  2113. )
  2114. print(
  2115. "截图区域2(图片): "
  2116. f"left={capture_left}, top={capture_top}, "
  2117. f"width={capture_right - capture_left}, "
  2118. f"height={capture_bottom - capture_top}"
  2119. )
  2120. screenshot_label_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="label")
  2121. screenshot_image_path = _build_captcha_image_path("complexs", d=d, ext=".png", tag="image")
  2122. image = _screenshot_to_image(d)
  2123. image.crop(
  2124. (capture_label_left, capture_label_top, capture_label_right, capture_label_bottom)
  2125. ).save(screenshot_label_path)
  2126. image.crop(
  2127. (capture_left, capture_top, capture_right, capture_bottom)
  2128. ).save(screenshot_image_path)
  2129. print(f"截图1已保存: {screenshot_label_path}")
  2130. print(f"截图2已保存: {screenshot_image_path}")
  2131. result = post_api(
  2132. screenshot_image_path,
  2133. "29013",
  2134. label_image_path=screenshot_label_path,
  2135. timeout=30,
  2136. )
  2137. print(f"API返回结果: {result}")
  2138. verify_data = result.get("data", {})
  2139. print(f"verify_data={verify_data}")
  2140. if not (result.get("code") == 10000 and isinstance(verify_data, dict) and verify_data.get("code") == 0):
  2141. return False
  2142. data_str = verify_data.get("data", "")
  2143. if not data_str:
  2144. return False
  2145. data_value = int(data_str)
  2146. print(f"云码返回的像素距离: {data_value}")
  2147. slider_target_center_x = track_left + data_value
  2148. min_x = track_left + slider_width / 2
  2149. max_x = track_right - slider_width / 2
  2150. slider_target_center_x = _clamp(slider_target_center_x, min_x, max_x)
  2151. print(f"滑块中心目标X坐标: {slider_target_center_x}")
  2152. _, current_slider_bounds = _first_existing_bounds(d, slider_xpath_candidates)
  2153. if current_slider_bounds:
  2154. current_slider_center_x = (current_slider_bounds["left"] + current_slider_bounds["right"]) / 2
  2155. else:
  2156. current_slider_center_x = right_end_center_x
  2157. actual_distance = slider_target_center_x - current_slider_center_x
  2158. print(f"实际需要滑动的距离: {actual_distance}")
  2159. back_points = _build_directional_track(
  2160. current_slider_center_x,
  2161. right_end_center_y,
  2162. slider_target_center_x,
  2163. right_end_center_y,
  2164. )
  2165. _move_with_pressed_touch(d, back_points[1:])
  2166. time.sleep(random.uniform(0.2, 0.4))
  2167. d.touch.up(slider_target_center_x, right_end_center_y)
  2168. touch_down = False
  2169. time.sleep(random.uniform(1.8, 3.2))
  2170. return True
  2171. except Exception as e:
  2172. print(f"complex captcha failed: {e}")
  2173. return False
  2174. finally:
  2175. if touch_down:
  2176. try:
  2177. d.touch.up(right_end_center_x, right_end_center_y)
  2178. except Exception:
  2179. pass
  2180. def Numbers_English_verify(d):
  2181. return srwz(d)
  2182. def slider_verify(d):
  2183. return hk(d)
  2184. def Click_images(d):
  2185. # 两种点选入口统一处理
  2186. if d.xpath('//*[@text="请按语序依次点击下图文字"]').exists:
  2187. return wenzidianxuan(d)
  2188. return dianxuan(d)
  2189. def Shortest_connection(d):
  2190. return lianxian(d)
  2191. def _handle_generic_captcha(d, xpath, timeout=60):
  2192. """通用验证码处理:等待人工处理完成。"""
  2193. start = time.time()
  2194. while time.time() - start < timeout:
  2195. if xpath and not d.xpath(xpath).exists:
  2196. return True
  2197. time.sleep(1)
  2198. return False
  2199. def handle_captcha(d, captcha_type, xpath=None, device_id=None):
  2200. _set_runtime_device_id(d=d, device_id=device_id)
  2201. handlers = {
  2202. "Numbers_English": Numbers_English_verify,
  2203. "Swipe_right": Swipe_right,
  2204. "Click_images": Click_images,
  2205. "slider": slider_verify,
  2206. "complexs": complexs,
  2207. "Shortest_connection": Shortest_connection,
  2208. "click_side": click_side,
  2209. }
  2210. func = handlers.get(captcha_type)
  2211. if func is None:
  2212. return _handle_generic_captcha(d, xpath)
  2213. return func(d)
  2214. def _extract_color_name(api_result):
  2215. if not isinstance(api_result, dict):
  2216. return ""
  2217. if api_result.get("code") == 0 and isinstance(api_result.get("data"), str):
  2218. return api_result.get("data", "").strip()
  2219. if api_result.get("code") == 10000:
  2220. inner = api_result.get("data")
  2221. if isinstance(inner, dict) and inner.get("code") == 0:
  2222. return str(inner.get("data", "")).strip()
  2223. if isinstance(inner, str):
  2224. return inner.strip()
  2225. return ""
  2226. def _normalize_color_name(color_name):
  2227. if not color_name:
  2228. return ""
  2229. alias = {
  2230. "红": "红色",
  2231. "红的": "红色",
  2232. "绿": "绿色",
  2233. "蓝": "蓝色",
  2234. "黄": "黄色",
  2235. "橙": "橙色",
  2236. "紫": "紫色",
  2237. "黑": "黑色",
  2238. "白": "白色",
  2239. "棕": "棕色",
  2240. "褐": "褐色",
  2241. }
  2242. if color_name in alias:
  2243. return alias[color_name]
  2244. for k, v in alias.items():
  2245. if k in color_name:
  2246. return v
  2247. return color_name
  2248. def _find_color_coordinates(image_path, color_name):
  2249. color_name = _normalize_color_name(color_name)
  2250. color_ranges = {
  2251. "红色": (([0, 120, 70], [10, 255, 255]), ([170, 120, 70], [180, 255, 255])),
  2252. "绿色": (([35, 50, 50], [85, 255, 255]),),
  2253. "蓝色": (([90, 50, 50], [130, 255, 255]),),
  2254. "黄色": (([20, 100, 100], [30, 255, 255]),),
  2255. "橙色": (([5, 100, 100], [18, 255, 255]),),
  2256. "紫色": (([130, 50, 50], [165, 255, 255]),),
  2257. "黑色": (([0, 0, 0], [180, 255, 50]),),
  2258. "白色": (([0, 0, 200], [180, 35, 255]),),
  2259. "棕色": (([8, 60, 20], [20, 255, 180]),),
  2260. "褐色": (([8, 60, 20], [20, 255, 180]),),
  2261. }
  2262. if color_name not in color_ranges:
  2263. return []
  2264. image = cv2.imread(image_path)
  2265. if image is None:
  2266. return []
  2267. hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
  2268. ranges = color_ranges[color_name]
  2269. if len(ranges) == 2:
  2270. lower1 = np.array(ranges[0][0])
  2271. upper1 = np.array(ranges[0][1])
  2272. lower2 = np.array(ranges[1][0])
  2273. upper2 = np.array(ranges[1][1])
  2274. mask = cv2.bitwise_or(cv2.inRange(hsv, lower1, upper1), cv2.inRange(hsv, lower2, upper2))
  2275. else:
  2276. lower = np.array(ranges[0][0])
  2277. upper = np.array(ranges[0][1])
  2278. mask = cv2.inRange(hsv, lower, upper)
  2279. kernel = np.ones((3, 3), np.uint8)
  2280. mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  2281. mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  2282. contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  2283. coordinates = []
  2284. for contour in contours:
  2285. area = cv2.contourArea(contour)
  2286. if area < 30:
  2287. continue
  2288. m = cv2.moments(contour)
  2289. if m["m00"] == 0:
  2290. continue
  2291. cx = int(m["m10"] / m["m00"])
  2292. cy = int(m["m01"] / m["m00"])
  2293. coordinates.append((cx, cy))
  2294. return coordinates
  2295. def _nearest_neighbor_path(points):
  2296. if not points:
  2297. return []
  2298. if len(points) <= 2:
  2299. return points[:]
  2300. unvisited = points[:]
  2301. path = [unvisited.pop(0)]
  2302. while unvisited:
  2303. last_x, last_y = path[-1]
  2304. idx = min(
  2305. range(len(unvisited)),
  2306. key=lambda i: math.hypot(last_x - unvisited[i][0], last_y - unvisited[i][1])
  2307. )
  2308. path.append(unvisited.pop(idx))
  2309. return path
  2310. def _human_like_path(points):
  2311. if len(points) < 2:
  2312. return points[:]
  2313. curved = []
  2314. for i in range(len(points) - 1):
  2315. start = points[i]
  2316. end = points[i + 1]
  2317. mid_x = (start[0] + end[0]) / 2
  2318. mid_y = (start[1] + end[1]) / 2
  2319. if abs(end[0] - start[0]) > abs(end[1] - start[1]):
  2320. offset_x = 0
  2321. offset_y = random.uniform(-15, 15)
  2322. else:
  2323. offset_x = random.uniform(-15, 15)
  2324. offset_y = 0
  2325. control_x = mid_x + offset_x
  2326. control_y = mid_y + offset_y
  2327. curved.append(start)
  2328. for t in np.arange(0.1, 1.0, 0.1):
  2329. x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control_x + t ** 2 * end[0]
  2330. y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control_y + t ** 2 * end[1]
  2331. curved.append((int(x), int(y)))
  2332. curved.append(points[-1])
  2333. return curved
  2334. def _simulate_human_drawing(d, path):
  2335. if len(path) < 2:
  2336. return False
  2337. try:
  2338. sx, sy = path[0]
  2339. d.touch.down(sx, sy)
  2340. time.sleep(random.uniform(0.05, 0.1))
  2341. for i in range(1, len(path)):
  2342. x, y = path[i]
  2343. d.touch.move(x + random.randint(-2, 2), y + random.randint(-2, 2))
  2344. time.sleep(random.uniform(0.01, 0.03))
  2345. time.sleep(random.uniform(0.1, 0.2))
  2346. d.touch.up(path[-1][0], path[-1][1])
  2347. return True
  2348. except Exception:
  2349. return False
  2350. def retry_captcha(
  2351. d,
  2352. xpath_text,
  2353. handle_func,
  2354. retry_count=5,
  2355. captcha_name=None,
  2356. allow_close_on_third_fail=True,
  2357. fail_limit_before_close=3
  2358. ):
  2359. # 如果当前页面存在对应验证码,就循环重试处理
  2360. if d.xpath(xpath_text).exists:
  2361. current_name = captcha_name or getattr(handle_func, "__name__", "captcha")
  2362. fail_streak = 0
  2363. for _ in range(retry_count):
  2364. if not d.xpath(xpath_text).exists:
  2365. break
  2366. _save_debug_screenshot(d, current_name, tag="full")
  2367. try:
  2368. handle_func(d)
  2369. except Exception as e:
  2370. print(f"[captcha] {current_name} handler error: {e}")
  2371. time.sleep(3)
  2372. # 验证码消失了,说明处理成功,直接退出
  2373. if not d.xpath(xpath_text).exists:
  2374. break
  2375. fail_streak += 1
  2376. if allow_close_on_third_fail and fail_streak >= fail_limit_before_close:
  2377. closed = _click_captcha_close(d, captcha_xpath=xpath_text)
  2378. print(f"[captcha] {current_name} failed {fail_streak} times, switch captcha: {closed}")
  2379. fail_streak = 0
  2380. time.sleep(1.2)
  2381. def yzm(d=None, device_id=None):
  2382. # 如果没有传设备对象,就默认连接当前设备
  2383. if d is None:
  2384. d = u2.connect()
  2385. _set_runtime_device_id(d=d, device_id=device_id)
  2386. # 向右滑动验证码
  2387. retry_captcha(
  2388. d,
  2389. '//*[contains(@text, "请向右滑动滑块")]',
  2390. Swipe_right,
  2391. captcha_name="Swipe_right",
  2392. allow_close_on_third_fail=False
  2393. )
  2394. # 滑块验证码
  2395. retry_captcha(
  2396. d,
  2397. '//*[@text="请拖动下方滑块完成拼图"]',
  2398. hk,
  2399. captcha_name="slider",
  2400. )
  2401. # 空间推理验证码
  2402. retry_captcha(
  2403. d,
  2404. '//*[contains(@text, "请点击")]',
  2405. click_side,
  2406. captcha_name="click_side",
  2407. allow_close_on_third_fail = False
  2408. )
  2409. # 复杂拖动滑块验证码
  2410. retry_captcha(
  2411. d,
  2412. '//*[contains(@text, "拖动滑块")]',
  2413. complexs,
  2414. captcha_name="complexs",
  2415. allow_close_on_third_fail=False
  2416. )
  2417. # 输入型验证码
  2418. retry_captcha(d, '//*[@text="请输入图片中的内容"]', srwz, captcha_name="text_input")
  2419. # 图标点选验证码
  2420. retry_captcha(d, '//*[@text="请依次点击下图图标"]', dianxuan, captcha_name="icon_click")
  2421. # 文字点选验证码
  2422. retry_captcha(d, '//*[@text="请按语序依次点击下图文字"]', wenzidianxuan, captcha_name="text_click")
  2423. # 最短线连接验证码
  2424. retry_captcha(d, '//*[contains(@text, "用最短线连接")]', lianxian, captcha_name="Shortest_connection")
  2425. def lianxian(d):
  2426. art_text_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[1]'
  2427. color_points_xpath = '//*[@text="身份核实"]/android.view.View[1]/android.view.View[1]/android.view.View[2]/android.view.View[1]/android.widget.Image[1]'
  2428. art_text_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="art_text")
  2429. color_points_img_path = _build_captcha_image_path("Shortest_connection", d=d, ext=".png", tag="color_points")
  2430. art_text_img_path, _ = _capture_by_bounds(
  2431. d,
  2432. art_text_xpath,
  2433. output_path=art_text_img_path,
  2434. captcha_type="Shortest_connection"
  2435. )
  2436. color_points_img_path, color_bounds = _capture_by_bounds(
  2437. d,
  2438. color_points_xpath,
  2439. output_path=color_points_img_path,
  2440. captcha_type="Shortest_connection"
  2441. )
  2442. if not art_text_img_path or not color_points_img_path or not color_bounds:
  2443. return False
  2444. element_left = color_bounds["left"]
  2445. element_top = color_bounds["top"]
  2446. element_width = color_bounds["right"] - color_bounds["left"]
  2447. element_height = color_bounds["bottom"] - color_bounds["top"]
  2448. api_result = post_api(art_text_img_path, "10118")
  2449. color_name = _extract_color_name(api_result)
  2450. if not color_name:
  2451. return False
  2452. relative_points = _find_color_coordinates(color_points_img_path, color_name)
  2453. if len(relative_points) < 2:
  2454. return False
  2455. color_img = cv2.imread(color_points_img_path)
  2456. if color_img is None:
  2457. return False
  2458. img_h, img_w = color_img.shape[:2]
  2459. if img_w <= 0 or img_h <= 0:
  2460. return False
  2461. screen_points = []
  2462. for rx, ry in relative_points:
  2463. sx = element_left + int(rx * (element_width / img_w))
  2464. sy = element_top + int(ry * (element_height / img_h))
  2465. screen_points.append((sx, sy))
  2466. path = _nearest_neighbor_path(screen_points)
  2467. curved = _human_like_path(path)
  2468. return _simulate_human_drawing(d, curved)
  2469. if __name__ == '__main__':
  2470. d = u2.connect("GQIRKB7LVOONM7VW")
  2471. yzm(d)
  2472. # 地址:https://www.jfbym.com/
  2473. # 手机号:18611188833
  2474. # 密码:123456