# from DrissionPage import ChromiumPage # # 假设上述代码存为 human_keyboard.py # from human_keyboard import HumanKeyboard, Key # page = ChromiumPage() # page.get('https://www.baidu.com') # # 初始化高度拟人化键盘 # keyboard = HumanKeyboard(page) # # 1. 点击搜索框以获得输入焦点 # ele = page.ele('#kw') # ele.click() # # 2. 以极度拟人化的方式输入文本(有概率打错字又自己倒退回删并重新打) # # 因为开了 humanize,你会看到打字速度变化、可能突然思考停顿等效果。 # keyboard.type_text("Hello World, let's test humanized typing!!!", humanize=True) # # 3. 按下回车键 # keyboard.press(Key.ENTER) # # 4. 执行快捷键 (如 Ctrl + A 全选) # keyboard.hotkey(Key.CONTROL, Key.A) import logging import random import time import warnings from dataclasses import dataclass from enum import Enum from typing import Optional, Tuple, List # 引入 DrissionPage 的基础类以进行类型提示 from DrissionPage import ChromiumPage logger = logging.getLogger(__name__) # ========================================== # 常量、枚举与键盘布局配置 # ========================================== DEFAULT_TYPO_PROBABILITY = 0.02 # 模拟错别字时需要的 QWERTY 键盘相邻按键映射表 QWERTY_NEIGHBORS = { 'q': ['w', 'a', 's'], 'w': ['q', 'e', 'a', 's', 'd'], 'e': ['w', 'r', 's', 'd', 'f'], 'r': ['e', 't', 'd', 'f', 'g'], 't': ['r', 'y', 'f', 'g', 'h'], 'y': ['t', 'u', 'g', 'h', 'j'], 'u': ['y', 'i', 'h', 'j', 'k'], 'i': ['u', 'o', 'j', 'k', 'l'], 'o': ['i', 'p', 'k', 'l'], 'p': ['o', 'l'], 'a': ['q', 'w', 's', 'z', 'x'], 's': ['q', 'w', 'e', 'a', 'd', 'z', 'x', 'c'], 'd': ['w', 'e', 'r', 's', 'f', 'x', 'c', 'v'], 'f': ['e', 'r', 't', 'd', 'g', 'c', 'v', 'b'], 'g': ['r', 't', 'y', 'f', 'h', 'v', 'b', 'n'], 'h': ['t', 'y', 'u', 'g', 'j', 'b', 'n', 'm'], 'j': ['y', 'u', 'i', 'h', 'k', 'n', 'm'], 'k': ['u', 'i', 'o', 'j', 'l', 'm'], 'l': ['i', 'o', 'p', 'k'], 'z': ['a', 's', 'x'], 'x': ['a', 's', 'd', 'z', 'c'], 'c': ['s', 'd', 'f', 'x', 'v'], 'v': ['d', 'f', 'g', 'c', 'b'], 'b': ['f', 'g', 'h', 'v', 'n'], 'n': ['g', 'h', 'j', 'b', 'm'], 'm': ['h', 'j', 'k', 'n'] } class TypoType(Enum): """错字类型""" ADJACENT = "adjacent" # 按到相邻键 TRANSPOSE = "transpose" # 字母顺序颠倒 DOUBLE = "double" # 重复按键 SKIP = "skip" # 漏按 MISSED_SPACE = "missed_space" # 漏按空格 class Key(Enum): """ 底层 CDP 需要的特殊按键定义。 格式: (key_name, windows_virtual_key_code) """ BACKSPACE = ("Backspace", 8) TAB = ("Tab", 9) ENTER = ("Enter", 13) SHIFT = ("Shift", 16) CONTROL = ("Control", 17) ALT = ("Alt", 18) ESCAPE = ("Escape", 27) SPACE = (" ", 32) META = ("Meta", 91) # Win / Cmd 键 @dataclass(frozen=True) class TypoResult: """错字生成结果""" typo_type: TypoType wrong_char: str = '' @dataclass(frozen=True) class TimingConfig: """高仿真打字节奏时间配置 (单位:秒)""" keystroke_min: float = 0.03 keystroke_max: float = 0.12 punctuation_min: float = 0.08 punctuation_max: float = 0.18 thinking_probability: float = 0.02 thinking_min: float = 0.3 thinking_max: float = 0.7 distraction_probability: float = 0.005 distraction_min: float = 0.5 distraction_max: float = 1.2 mistake_realize_min: float = 0.1 mistake_realize_max: float = 0.25 after_correction_min: float = 0.03 after_correction_max: float = 0.08 double_press_min: float = 0.02 double_press_max: float = 0.05 hesitation_min: float = 0.15 hesitation_max: float = 0.3 @dataclass(frozen=True) class TypoConfig: """各类错字发生的权重比例""" adjacent_weight: float = 0.55 transpose_weight: float = 0.20 double_weight: float = 0.12 skip_weight: float = 0.08 missed_space_weight: float = 0.05 # ========================================== # 核心控制类 # ========================================== class HumanKeyboard: """ DrissionPage 的高仿真人类键盘控制器。 提供: - 公共按键操作(press, down, up, hotkey) - 私有文本输入,包含高度拟人的错别字产生、自我纠正、思考停顿与节奏变化。 """ PAUSE_CHARS = frozenset(' .,!?;:\n') def __init__( self, page: ChromiumPage, timing: Optional[TimingConfig] = None, typo_config: Optional[TypoConfig] = None, ): """ 初始化键盘控制器。 :param page: DrissionPage 的 ChromiumPage 或 ChromiumTab 实例 :param timing: 节奏时间配置 :param typo_config: 错别字权重配置 """ self._page = page self._timing = timing or TimingConfig() self._typo_config = typo_config or TypoConfig() def press( self, key: Key, modifiers: Optional[int] = None, interval: float = 0.1, ): """ 按下一个键并释放。 :param key: 要按的键 (来自 Key enum) :param modifiers: 组合键掩码 (Alt=1, Ctrl=2, Meta=4, Shift=8) :param interval: 按下和释放之间的延迟 """ logger.debug(f'Pressing key: {key.name} with modifiers: {modifiers}') self.down(key, modifiers) time.sleep(interval) self.up(key) def down(self, key: Key, modifiers: Optional[int] = None): """按下按键(不释放)""" key_name, code = key.value logger.debug(f'Key down: {key_name}') params = { "type": "keyDown", "key": key_name, "windowsVirtualKeyCode": code, "nativeVirtualKeyCode": code, } if modifiers is not None: params["modifiers"] = modifiers self._page.run_cdp("Input.dispatchKeyEvent", **params) def up(self, key: Key): """释放按键""" key_name, code = key.value logger.debug(f'Key up: {key_name}') self._page.run_cdp( "Input.dispatchKeyEvent", type="keyUp", key=key_name, windowsVirtualKeyCode=code, nativeVirtualKeyCode=code, ) def hotkey(self, key1: Key, key2: Key, key3: Optional[Key] = None): """ 执行组合快捷键(最多支持3个键)。 例如:keyboard.hotkey(Key.CONTROL, Key.C) """ logger.debug(f'Hotkey: {key1.name} + {key2.name}' + (f' + {key3.name}' if key3 else '')) keys = [key1, key2] if key3 is not None: keys.append(key3) modifiers, non_modifiers = self._split_modifiers_and_keys(keys) modifier_value = self._calculate_modifier_value(modifiers) for key in non_modifiers: self.down(key, modifiers=modifier_value) time.sleep(0.05) time.sleep(0.1) for key in reversed(non_modifiers): self.up(key) time.sleep(0.05) def type_text( self, text: str, humanize: bool = False, interval: Optional[float] = None, ): """ 逐字输入文本。 :param text: 要输入的文本 :param humanize: 是否开启拟人化(变速、犯错后退格重打、思考停顿) :param interval: (已弃用) 字符输入间隔 """ if interval is not None: warnings.warn( '"interval" 参数已被弃用。请使用 "humanize=True" 开启真实的人类打字模拟。', DeprecationWarning, stacklevel=2, ) if humanize: self._type_text_humanized(text) return for current_char in text: self._type_char(current_char) time.sleep(0.05) def _type_text_humanized(self, text: str): """高拟人化文本输入核心逻辑""" char_index = 0 while char_index < len(text): current_char = text[char_index] next_char = text[char_index + 1] if char_index + 1 < len(text) else None # 判断是否打错字并执行错字修补逻辑,返回 True 表示下个字应跳过 should_skip_next = self._process_char_with_typo(current_char, next_char) if should_skip_next: char_index += 1 self._apply_realistic_delay(current_char) char_index += 1 def _type_char(self, char: str): """通过底层 CDP 输入单个普通字符""" # 下拉字符事件 self._page.run_cdp( "Input.dispatchKeyEvent", type="keyDown", key=char, text=char, unmodifiedText=char ) # 抬起字符事件 self._page.run_cdp( "Input.dispatchKeyEvent", type="keyUp", key=char ) def _type_backspace(self): """输入退格键(用来模拟人打错字回删)""" self.press(Key.BACKSPACE) def _process_char_with_typo( self, current_char: str, next_char: Optional[str], ) -> bool: """处理错别字产生与修复。返回 True 表示下个字符需要跳过处理""" if not self._should_make_typo(): self._type_char(current_char) return False typo = self._generate_typo(current_char, next_char) return self._handle_typo(current_char, next_char, typo) def _handle_typo( self, current_char: str, next_char: Optional[str], typo: TypoResult, ) -> bool: """根据错别字类型执行具体的打错和修正行为""" if typo.typo_type == TypoType.ADJACENT: self._do_adjacent_typo(current_char, typo.wrong_char) return False if typo.typo_type == TypoType.TRANSPOSE and next_char: self._do_transpose_typo(current_char, next_char) return True if typo.typo_type == TypoType.DOUBLE: self._do_double_typo(current_char) return False if typo.typo_type == TypoType.SKIP: self._do_skip_typo(current_char) return False if typo.typo_type == TypoType.MISSED_SPACE and current_char == ' ' and next_char: self._do_missed_space_typo(current_char, next_char) return True self._type_char(current_char) return False def _do_adjacent_typo(self, correct_char: str, wrong_char: str): """打错相邻键 -> 停顿 -> 退格删除 -> 补上正确的""" timing = self._timing self._type_char(wrong_char) time.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max)) self._type_backspace() time.sleep(random.uniform(timing.after_correction_min, timing.after_correction_max)) self._type_char(correct_char) def _do_transpose_typo(self, current_char: str, next_char: str): """字母顺序打反 -> 停顿 -> 删两格 -> 重新打正确的顺序""" timing = self._timing self._type_char(next_char) time.sleep(random.uniform(timing.keystroke_min, timing.keystroke_max)) self._type_char(current_char) time.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max)) self._type_backspace() self._type_backspace() time.sleep(random.uniform(timing.after_correction_min, timing.after_correction_max)) self._type_char(current_char) time.sleep(random.uniform(timing.keystroke_min, timing.keystroke_max)) self._type_char(next_char) def _do_double_typo(self, current_char: str): """不小心连按两次 -> 停顿 -> 退格删掉多余的一个""" timing = self._timing self._type_char(current_char) time.sleep(random.uniform(timing.double_press_min, timing.double_press_max)) self._type_char(current_char) time.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max)) self._type_backspace() def _do_skip_typo(self, current_char: str): """大脑卡壳漏打:犹豫停顿 -> 接着打正常的""" timing = self._timing time.sleep(random.uniform(timing.hesitation_min, timing.hesitation_max)) self._type_char(current_char) def _do_missed_space_typo(self, space_char: str, next_char: str): """漏敲空格 -> 接着敲了下一个字 -> 发现错误 -> 退格删掉 -> 补空格 -> 重打下个字""" timing = self._timing self._type_char(next_char) time.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max)) self._type_backspace() time.sleep(random.uniform(timing.after_correction_min, timing.after_correction_max)) self._type_char(space_char) time.sleep(random.uniform(timing.after_correction_min, timing.after_correction_max)) self._type_char(next_char) def _apply_realistic_delay(self, typed_char: str): """根据输入的不同字符和人的状态,加入随机延迟""" timing = self._timing delay = random.uniform(timing.keystroke_min, timing.keystroke_max) # 遇到标点符号,延迟加长 if typed_char in self.PAUSE_CHARS: delay += random.uniform(timing.punctuation_min, timing.punctuation_max) # 模拟突然思考 if random.random() < timing.thinking_probability: delay += random.uniform(timing.thinking_min, timing.thinking_max) # 模拟被打断分心 if random.random() < timing.distraction_probability: delay += random.uniform(timing.distraction_min, timing.distraction_max) time.sleep(delay) @staticmethod def _should_make_typo() -> bool: """决定此时是否发生手误""" return random.random() < DEFAULT_TYPO_PROBABILITY def _generate_typo(self, current_char: str, next_char: Optional[str]) -> TypoResult: """生成一个具体的错误结果""" typo_type = self._select_typo_type() return self._create_typo(typo_type, current_char, next_char) def _select_typo_type(self) -> TypoType: """通过权重随机挑选错误类型""" config = self._typo_config typo_types = [ TypoType.ADJACENT, TypoType.TRANSPOSE, TypoType.DOUBLE, TypoType.SKIP, TypoType.MISSED_SPACE, ] typo_weights = [ config.adjacent_weight, config.transpose_weight, config.double_weight, config.skip_weight, config.missed_space_weight, ] return random.choices(typo_types, weights=typo_weights, k=1)[0] def _create_typo( self, typo_type: TypoType, current_char: str, next_char: Optional[str], ) -> TypoResult: """构建错字返回结果""" if typo_type == TypoType.ADJACENT: return self._create_adjacent_typo(current_char) elif typo_type == TypoType.TRANSPOSE: return self._create_transpose_typo(current_char, next_char) elif typo_type == TypoType.MISSED_SPACE: return self._create_missed_space_typo(current_char) elif typo_type == TypoType.DOUBLE: return TypoResult(typo_type=TypoType.DOUBLE, wrong_char=current_char) else: return TypoResult(typo_type=TypoType.SKIP) def _create_transpose_typo(self, current_char: str, next_char: Optional[str]) -> TypoResult: """顺序颠倒错误(如果下个不是字母则降级为按错相邻键)""" if next_char and next_char.isalpha(): return TypoResult(typo_type=TypoType.TRANSPOSE, wrong_char=next_char) return self._create_adjacent_typo(current_char) def _create_missed_space_typo(self, current_char: str) -> TypoResult: """漏打空格错误(如果当前本不是空格则降级为按错相邻键)""" if current_char == ' ': return TypoResult(typo_type=TypoType.MISSED_SPACE) return self._create_adjacent_typo(current_char) @staticmethod def _create_adjacent_typo(original_char: str) -> TypoResult: """生成按错相邻键的错误""" lowercase_char = original_char.lower() # 如果字典中没有对应相邻键,降级成连按两下 if lowercase_char not in QWERTY_NEIGHBORS: return TypoResult(typo_type=TypoType.DOUBLE, wrong_char=original_char) adjacent_char = random.choice(QWERTY_NEIGHBORS[lowercase_char]) if original_char.isupper(): adjacent_char = adjacent_char.upper() return TypoResult(typo_type=TypoType.ADJACENT, wrong_char=adjacent_char) @staticmethod def _split_modifiers_and_keys(keys: List[Key]) -> Tuple[List[Key], List[Key]]: """分离修饰键(Ctrl/Shift等)和普通键""" modifier_keys = {Key.CONTROL, Key.SHIFT, Key.ALT, Key.META} modifiers = [key for key in keys if key in modifier_keys] non_modifiers = [key for key in keys if key not in modifier_keys] return modifiers, non_modifiers @staticmethod def _calculate_modifier_value(modifiers: List[Key]) -> Optional[int]: """计算 CDP 事件所需的 modifiers 掩码值""" if not modifiers: return None modifier_map = { Key.ALT: 1, Key.CONTROL: 2, Key.META: 4, Key.SHIFT: 8, } value = sum(modifier_map.get(mod, 0) for mod in modifiers) return value if value > 0 else None