| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- # coding=utf-8
- """PDD 滑块验证码测试 — 云码平台"""
- import uiautomator2 as u2
- import requests
- import base64
- import time
- import subprocess
- import logging
- import random
- import io
- from PIL import Image
- # ============================================================
- # ★ 填参数
- # ============================================================
- DEVICE_ID = "ZDQWUSSWBEDI896T"
- YUNMA_TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
- MAX_ROUNDS = 1
- # ============================================================
- # PDD 验证码 XPath
- # ============================================================
- SAFE_VERIFY = '//android.widget.TextView[@text="安全验证"]' # 入口按钮(可能没有)
- # 验证码入口容器 → 一定存在,检测用
- CAPTCHA_ENTRY = '//*[@resource-id="intelVerify"]'
- # 真正的验证码弹窗容器 → 截图裁剪用
- CAPTCHA_DIALOG = '//*[@resource-id="captcha-dialog"]'
- # 弹窗内图标 + 标题 → 两个都有即判定在验证码弹窗内
- CAPTCHA_ICON = '//*[@text=""]'
- CAPTCHA_TITLE = '//*[@text="拼多多安全验证"]'
- # 中间验证页面:同时存在「前往验证」和「安全验证」
- GO_TO_VERIFY = '//*[@text="前往验证"]'
- # 安全验证文本 XPath 候选列表(intelVerify 找不到时的兜底检测,≥2 命中即判定验证码存在)
- SAFE_VERIFY_TEXT_XPATHS = [
- '//*[@text="点击下方"安全验证"按钮,完成拼多多官方验证"]',
- '//*[@text="b04804a5-893c-4af7-9076-6fc5f223be44"]',
- '//android.widget.TextView[@text="安全验证"]',
- ]
- # 滑块定位候选列表(按优先级依次尝试)
- SLIDER_CANDIDATES = [
- '//*[@resource-id="slide-button"]',
- '(//android.widget.TextView)[6]',
- ]
- # 距离缩放系数(如果实际拖动距离与云码返回距离有偏差,调整此值)
- DIST_SCALE = 1.0
- # 计算题验证码
- CALC_PROMPT = '//*[@text="请输入图中的计算结果"]'
- SUBMIT_BTN_CANDIDATES = [
- '//*[@text="确定"]',
- '//*[@text="提交"]',
- '//*[@text="确认"]',
- ]
- # 云码
- YUNMA_URL = "http://api.jfbym.com/api/YmServer/customApi"
- YUNMA_TYPE = "22222" # 滑块
- YUNMA_CALC_TYPE = "50100" # 数字计算
- logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
- log = logging.getLogger(__name__)
- def connect(dev):
- d = u2.connect_usb(dev)
- subprocess.run(f'adb -s {dev} shell /data/local/tmp/atx-agent server -d --stop',
- capture_output=True, text=True, shell=True)
- time.sleep(2)
- subprocess.run(f'adb -s {dev} shell /data/local/tmp/atx-agent server -d',
- capture_output=True, text=True, shell=True)
- time.sleep(2)
- return d
- def _find_slider(d):
- """在候选列表中定位滑块元素,返回其 bounds,找不到返回 None"""
- for xpath in SLIDER_CANDIDATES:
- try:
- el = d.xpath(xpath)
- if el.exists:
- b = el.info.get('bounds', {})
- # 校验 bounds 合理性:宽高至少 10px
- w = b.get('right', 0) - b.get('left', 0)
- h = b.get('bottom', 0) - b.get('top', 0)
- if w >= 10 and h >= 10:
- log.info(f"滑块定位成功: {xpath} → bounds={b}")
- return b
- except Exception:
- continue
- return None
- def _get_captcha_bounds(d):
- """获取验证码弹窗 bounds(captcha-dialog 容器,用于截图裁剪)"""
- el = d.xpath(CAPTCHA_DIALOG)
- if el.exists:
- b = el.info.get('bounds', {})
- if b:
- log.info(f"captcha-dialog bounds: {b}")
- return b
- return None
- def _is_captcha_entry_visible(d):
- """验证码入口容器 intelVerify 是否可见"""
- return d.xpath(CAPTCHA_ENTRY).exists
- def _is_captcha_dialog_visible(d):
- """验证码弹窗是否可见(图标 + 标题两个都有)"""
- return d.xpath(CAPTCHA_ICON).exists and d.xpath(CAPTCHA_TITLE).exists
- def _is_captcha_detected_by_safe_text(d):
- """intelVerify 找不到时的回退检测:统计安全验证文本 XPath 命中数,≥2 即判定验证码存在"""
- hits = 0
- for xpath in SAFE_VERIFY_TEXT_XPATHS:
- try:
- if d.xpath(xpath).exists:
- hits += 1
- log.info(f"安全验证文本命中: {xpath}")
- except Exception:
- continue
- log.info(f"安全验证文本命中数: {hits}/{len(SAFE_VERIFY_TEXT_XPATHS)}")
- return hits >= 2
- def _is_on_intermediate_verify_page(d):
- """检测是否在中间验证页面(同时存在「前往验证」+「安全验证」)"""
- has_go = d.xpath(GO_TO_VERIFY).exists
- has_safe = d.xpath(SAFE_VERIFY).exists
- log.info(f"中间页面检测: 前往验证={has_go}, 安全验证={has_safe}")
- return has_go and has_safe
- def _wait_for_captcha_dialog(d, timeout=5):
- """等待 captcha-dialog + 标题出现,返回 True/False"""
- for _ in range(timeout):
- has_icon = d.xpath(CAPTCHA_ICON).exists
- has_title = d.xpath(CAPTCHA_TITLE).exists
- if has_icon and has_title:
- log.info("验证码弹窗确认: 图标 + 拼多多安全验证 ✓")
- return True
- log.info(f"等待弹窗... icon={has_icon}, title={has_title}")
- time.sleep(1)
- return False
- def _enter_captcha_dialog(d):
- """等待弹窗出现,等不到则检测中间页面并点击「前往验证」后再等,成功返回 True"""
- if _wait_for_captcha_dialog(d):
- return True
- # 等了5次都没有 → 检测是否跳到中间页面
- if _is_on_intermediate_verify_page(d):
- log.info("检测到中间页面(前往验证 + 安全验证),点击「前往验证」...")
- d.xpath(GO_TO_VERIFY).click()
- time.sleep(1.5)
- if _wait_for_captcha_dialog(d):
- return True
- log.warning("中间页面导航后仍未出现弹窗")
- return False
- log.warning("等待超时,验证码弹窗未出现")
- return False
- def _screenshot_to_b64(d, crop=False):
- """截图并返回 base64,可选裁剪到 captcha-dialog 区域"""
- im = d.screenshot()
- if crop:
- cb = _get_captcha_bounds(d)
- if cb:
- im = im.crop((
- max(0, cb['left']), max(0, cb['top']),
- min(im.width, cb['right']), min(im.height, cb['bottom']),
- ))
- buf = io.BytesIO()
- im.save(buf, format='PNG')
- return base64.b64encode(buf.getvalue()).decode()
- def solve_calc(d):
- """解决计算题验证码,成功返回 True(50100 直接返回计算结果)"""
- log.info("检测到计算题验证码")
- # 截图 → 云码 50100
- b64 = _screenshot_to_b64(d, crop=True)
- resp = requests.post(YUNMA_URL, json={
- "token": YUNMA_TOKEN, "type": YUNMA_CALC_TYPE, "image": b64
- }, headers={"Content-Type": "application/json"}, timeout=30).json()
- log.info(f"云码(50100): {resp}")
- if resp.get("msg") != "识别成功":
- log.warning(f"云码 50100 识别失败: {resp.get('msg')}")
- return False
- data = resp.get("data", {})
- result_str = data.get("data", "") if isinstance(data, dict) else str(data or "")
- try:
- result = int(str(result_str).strip())
- except (ValueError, TypeError):
- log.warning(f"50100 返回非数字: {result_str}")
- return False
- log.info(f"计算结果: {result}")
- # 找到输入框并输入结果
- edit_text = d(className='android.widget.EditText')
- if not edit_text.exists:
- log.warning("未找到输入框")
- return False
- edit_text.click()
- time.sleep(0.3)
- edit_text.set_text(str(result))
- time.sleep(0.5)
- # 点击确定/提交
- clicked = False
- for xpath in SUBMIT_BTN_CANDIDATES:
- btn = d.xpath(xpath)
- if btn.exists:
- btn.click()
- clicked = True
- log.info(f"点击: {xpath}")
- break
- if not clicked:
- d.press('enter')
- time.sleep(2)
- # 验证是否解开
- if not _is_captcha_dialog_visible(d) and not _is_captcha_entry_visible(d):
- log.info("计算题验证码已解开 ✓")
- return True
- else:
- log.info("计算题验证码未解开")
- return False
- def solve(d):
- """一次验证码处理,自动识别类型(滑块/计算题),成功返回 True"""
- # ── 第一步:检测验证码 ──
- # 优先级1: captcha-dialog 弹窗已存在(重入场景,跳过入口)
- if _is_captcha_dialog_visible(d):
- log.info("已在验证码弹窗内,跳过入口")
- # 优先级2: intelVerify 入口容器 → 点按钮进入
- elif d.xpath(CAPTCHA_ENTRY).exists:
- log.info("检测到验证码入口 intelVerify")
- if not d.xpath(SAFE_VERIFY).exists:
- log.error("入口容器存在但无「安全验证」按钮,无法触发!")
- return False
- log.info("点击「安全验证」...")
- d.xpath(SAFE_VERIFY).click()
- # ── 等待弹窗(等不到则检测中间页面)──
- if not _enter_captcha_dialog(d):
- return False
- else:
- # 兜底:intelVerify 找不到时,通过安全验证文本检测(≥2 命中即判定验证码存在)
- if _is_captcha_detected_by_safe_text(d):
- log.info("兜底检测:安全验证文本命中,确认验证码存在")
- # 先点击「安全验证」再等弹窗
- if d.xpath(SAFE_VERIFY).exists:
- log.info("点击「安全验证」...")
- d.xpath(SAFE_VERIFY).click()
- if not _enter_captcha_dialog(d):
- return False
- else:
- return False
- # ── 第四步:弹窗内循环尝试(失败刷新仍在弹窗内,类型可能变)──
- for attempt in range(5):
- # 弹窗不在 → 检查是否跳到了中间页面/安全验证入口
- if not _is_captcha_dialog_visible(d):
- if _is_on_intermediate_verify_page(d):
- log.info(f"第 {attempt+1} 次重试: 检测到中间页面,点击「前往验证」...")
- d.xpath(GO_TO_VERIFY).click()
- time.sleep(1.5)
- if not _wait_for_captcha_dialog(d):
- log.warning("重新导航失败")
- return False
- elif d.xpath(SAFE_VERIFY).exists:
- log.info(f"第 {attempt+1} 次重试: 检测到安全验证入口,重新等待弹窗...")
- if not _wait_for_captcha_dialog(d):
- log.warning("重新导航失败")
- return False
- else:
- log.info("弹窗消失,验证码已解开 ✓")
- return True
- # 每次重试重新判断类型(刷新后类型可能变)
- if d.xpath(CALC_PROMPT).exists:
- log.info(f"第 {attempt+1} 次尝试 → 计算题")
- if solve_calc(d):
- return True
- else:
- log.info(f"第 {attempt+1} 次尝试 → 滑块")
- if _solve_slider(d):
- return True
- log.info(f"第 {attempt+1} 次失败,等待验证码刷新...")
- time.sleep(2)
- log.warning("重试 5 次仍未解开")
- return False
- def _solve_slider(d):
- """滑块验证码核心逻辑,成功返回 True"""
- slider_bounds = None
- for _ in range(5):
- slider_bounds = _find_slider(d)
- if slider_bounds:
- break
- time.sleep(1)
- if not slider_bounds:
- log.warning("未找到滑块元素")
- return False
- sx = (slider_bounds['left'] + slider_bounds['right']) // 2
- sy = (slider_bounds['top'] + slider_bounds['bottom']) // 2
- # ── 关键:先按住滑块2秒,滑块图会变 ──
- log.info(f"按住滑块 ({sx},{sy})")
- d.touch.down(sx, sy)
- time.sleep(random.uniform(1.5, 2))
- # ── 按住状态下截图 → 云码 ──
- b64 = _screenshot_to_b64(d, crop=False)
- resp = requests.post(YUNMA_URL, json={
- "token": YUNMA_TOKEN, "type": YUNMA_TYPE, "image": b64
- }, headers={"Content-Type": "application/json"}, timeout=30).json()
- log.info(f"云码: {resp}")
- if resp.get("msg") != "识别成功":
- log.warning(f"云码识别失败: {resp.get('msg')}")
- d.touch.up(sx, sy)
- return False
- dist = resp.get("data", {}).get("data", 0)
- dist = float(dist or 0) * DIST_SCALE +random.uniform(-3.1, 3.1)
- if not dist:
- log.warning("距离为0")
- d.touch.up(sx, sy)
- return False
- target_x = sx + int(dist)
- log.info(f"拖动 {dist:.0f}px 从 ({sx},{sy}) 到 ({target_x},{sy})")
- # ── 模拟真人轨迹:多点、变速、微小抖动 ──
- steps = [
- (sx + int(dist * 0.05), sy + random.randint(-2, 2), random.uniform(0.05, 0.1)),
- (sx + int(dist * 0.20), sy + random.randint(-2, 2), random.uniform(0.08, 0.15)),
- (sx + int(dist * 0.40), sy + random.randint(-1, 1), random.uniform(0.1, 0.2)),
- (sx + int(dist * 0.60), sy + random.randint(-1, 1), random.uniform(0.1, 0.2)),
- (sx + int(dist * 0.80), sy + random.randint(-1, 1), random.uniform(0.08, 0.15)),
- (sx + int(dist * 0.95), sy + random.randint(-1, 1), random.uniform(0.05, 0.1)),
- (target_x, sy, 0.3),
- ]
- for step_x, step_y, delay in steps:
- d.touch.move(step_x, step_y)
- time.sleep(delay)
- d.touch.up(target_x, sy)
- time.sleep(2)
- # 等待弹窗消失(最多等5秒,弹窗关闭有延迟)
- for _ in range(5):
- if not _is_captcha_dialog_visible(d):
- log.info("滑块验证通过 ✓")
- return True
- log.info("等待弹窗消失...")
- time.sleep(1)
- log.info("滑块验证未通过")
- return False
- if __name__ == "__main__":
- if not DEVICE_ID or not YUNMA_TOKEN:
- log.error("先填 DEVICE_ID 和 YUNMA_TOKEN")
- exit(1)
- dev = connect(DEVICE_ID)
- log.info(f"已连接 {DEVICE_ID}")
- for i in range(MAX_ROUNDS):
- try:
- if solve(dev):
- log.info(f"第 {i+1} 轮完成")
- else:
- log.info(f"第 {i+1} 轮未检测到")
- time.sleep(3)
- except KeyboardInterrupt:
- break
- except Exception as e:
- log.error(f"异常: {e}")
- time.sleep(3)
- log.info("结束")
|