120 lines
3.3 KiB
Python
120 lines
3.3 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
DrissionPage 浏览器控制基础封装。
|
|||
|
|
提供连接浏览器、拟人化操作等通用能力,供各任务处理器复用。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import random
|
|||
|
|
import time
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("worker.browser_control")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from DrissionPage import Chromium
|
|||
|
|
except ImportError:
|
|||
|
|
Chromium = None # type: ignore
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── 拟人化参数 ───
|
|||
|
|
|
|||
|
|
HUMAN_DELAY_CLICK = (0.2, 0.6)
|
|||
|
|
HUMAN_DELAY_BETWEEN = (0.1, 0.4)
|
|||
|
|
HUMAN_MOVE_DURATION = (0.25, 0.7)
|
|||
|
|
HUMAN_CLICK_OFFSET = 12
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ensure_drission():
|
|||
|
|
"""确保 DrissionPage 已安装。"""
|
|||
|
|
if Chromium is None:
|
|||
|
|
raise RuntimeError("请先安装: pip install DrissionPage")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def connect_browser(port: int = None, addr: str = None):
|
|||
|
|
"""使用 DrissionPage 连接浏览器。"""
|
|||
|
|
ensure_drission()
|
|||
|
|
if port is not None:
|
|||
|
|
return Chromium(port)
|
|||
|
|
if addr:
|
|||
|
|
return Chromium(addr)
|
|||
|
|
raise ValueError("需要 port 或 addr 以连接浏览器")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def human_delay(low: float = 0.2, high: float = 0.6):
|
|||
|
|
"""随机延迟,模拟人类操作间隔。"""
|
|||
|
|
time.sleep(random.uniform(low, high))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def human_click(tab, ele, scroll_first: bool = True) -> bool:
|
|||
|
|
"""
|
|||
|
|
拟人化点击:先滚动到可见(可选),再动作链「移动 → 短暂停顿 → 点击」,
|
|||
|
|
移动带随机耗时与元素内随机偏移。
|
|||
|
|
"""
|
|||
|
|
if ele is None:
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
if scroll_first and hasattr(ele, "scroll") and getattr(ele.scroll, "to_see", None):
|
|||
|
|
ele.scroll.to_see()
|
|||
|
|
human_delay(0.15, 0.4)
|
|||
|
|
ox = random.randint(-HUMAN_CLICK_OFFSET, HUMAN_CLICK_OFFSET)
|
|||
|
|
oy = random.randint(-HUMAN_CLICK_OFFSET, HUMAN_CLICK_OFFSET)
|
|||
|
|
duration = random.uniform(*HUMAN_MOVE_DURATION)
|
|||
|
|
tab.actions.move_to(ele, offset_x=ox, offset_y=oy, duration=duration)
|
|||
|
|
tab.actions.wait(*HUMAN_DELAY_BETWEEN)
|
|||
|
|
tab.actions.click()
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
try:
|
|||
|
|
ele.click()
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def safe_click(tab, ele) -> bool:
|
|||
|
|
"""多种方式尝试点击元素(拟人 → 普通 → JS),确保触发成功。"""
|
|||
|
|
if ele is None:
|
|||
|
|
return False
|
|||
|
|
# 方式1: 拟人点击
|
|||
|
|
if human_click(tab, ele):
|
|||
|
|
return True
|
|||
|
|
# 方式2: 普通模拟点击
|
|||
|
|
try:
|
|||
|
|
ele.click(by_js=False)
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
# 方式3: JS 点击
|
|||
|
|
try:
|
|||
|
|
ele.click(by_js=True)
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def find_element(tab, selectors: list[str], timeout: int = 3):
|
|||
|
|
"""按优先级尝试多个选择器,返回第一个找到的元素。"""
|
|||
|
|
for sel in selectors:
|
|||
|
|
try:
|
|||
|
|
ele = tab.ele(sel, timeout=timeout)
|
|||
|
|
if ele:
|
|||
|
|
return ele
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def find_elements(tab, selectors: list[str], timeout: int = 3) -> list:
|
|||
|
|
"""按优先级尝试多个选择器,返回第一组找到的元素列表。"""
|
|||
|
|
for sel in selectors:
|
|||
|
|
try:
|
|||
|
|
eles = tab.eles(sel, timeout=timeout)
|
|||
|
|
if eles and len(eles) > 0:
|
|||
|
|
return eles
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
return []
|