如何通过 Python 实现招聘平台自动投递
在自动化招聘投递的场景中,传统的基于 DOM 树的浏览器自动化(如 Selenium/Playwright)往往面临极高的维护成本与严格反爬风控。一旦平台更新页面结构,脚本就会立刻失效。
与之相对的另一种解决方案是:降维打击,采用纯视觉与键鼠模拟的 UI 自动化。这种模式与人类肉眼看屏幕、用手点鼠标的逻辑完全一致,平台极难从流量特征上辨别。
核心技术栈
这类框架主要只依赖几个标准库协同:
opencv-python:用于图像色彩分析、轮廓提取。pyautogui/pyscreeze:提供底层的鼠标点击、滚轮滑动以及基础截图找图。pyperclip:用于绕过键鼠输入的字符限制(负责中文文本和表情发送)。keyboard:系统级全局键盘钩子,用于在发生异常时紧急中断。
所谓的自动化其实就三步走"截图 ➔ 找图 ➔ 点击";
在这里最容易导致程序停止的就是"找图"这个关键!
网速不稳定的解决方案
现象 :点击岗位后,详情页有时 0.1 秒加载好,有时卡 3 秒。如果硬写 time.sleep(3) 效率极低;写短了会找不到按钮而报错崩溃。
动态轮询与超时弃权机制(动态找图点击)。 不使用固定等待,通过高频捕捉屏幕元素,一旦出现马上动作,到了时限依然没有就静默跳过。
python
import time
import pyautogui
def click_with_timeout(img_path, confidence=0.8, timeout=3.0):
start_time = time.time()
# 在 timeout 秒内不断重试寻找目标图
while time.time() - start_time < timeout:
try:
pos = pyautogui.locateCenterOnScreen(img_path, confidence=confidence)
if pos:
pyautogui.click(pos)
return True
except pyautogui.ImageNotFoundException:
pass
# 加一点小的 Jitter 避免 CPU 跑满
time.sleep(0.2)
# 彻底超时后不要报错让程序崩溃,返回 False 进行降级处理
print(f"[{img_path}] 未出现,跳过。")
return False
不同投递按钮的解决方案
现象 :招聘平台的按钮是动态的。找人沟通时,它可能是"立即沟通",如果是聊过的 HR 则变成了"继续沟通"。如果当日配额用完,直接弹拉窗告诉你"到达上限"。这就要求我们有一套非线性的决策机制。
If-Else 兜底分支与全局哨兵(决策树抽象)。
python
import sys
def execute_apply_action():
# 1. 哨兵监测点:如果出现了达到上限的提示,说明不能再跑了
if click_with_timeout("img_limit_reached.png", timeout=1.0):
print("今日沟通达上限,直接停止任务。")
sys.exit(0)
# 2. 常规决策树兜底分支
if click_with_timeout("img_chat_now.png", timeout=2.0):
print("发现[立即沟通]按钮,已点击!")
elif click_with_timeout("img_continue_chat.png", timeout=2.0):
print("发现[继续沟通]按钮,此前聊过了,已点击!")
else:
print("未能识别当前的投递按钮,为了防止乱点,直接放弃当前职位。")
辨别已投递的解决方案
现象 :找图库 locateOnScreen 虽然能找元素,但无法理解业务上下文。比如列表里某些岗位是昨天看过的,字体颜色变灰了;而新岗位是深色的。如果无法分辨,程序就会在原地无限重复点击第一张卡片。
OpenCV 色彩空间均值分析(特征截取法)。 我们可以把一块区域抓取下来,转换并计算它的发色偏向(比如 RGB 通道平均亮度)。
python
import cv2
import numpy as np
def is_job_unread(cropped_image_path):
"""
判断该卡片图片区域中的标题是否没看过。
(假设深色字体是未读,浅灰色字体是已读)
"""
# 1. 读取截图
img = cv2.imread(cropped_image_path)
# 2. 计算 B, G, R 单个通道的全局均值
mean_val = cv2.mean(img)
mean_b, mean_g, mean_r = mean_val[0], mean_val[1], mean_val[2]
# 3. 简单的特征判定法:如果 RGB 值整体偏高(发白/浅灰),说明已经看过
# 具体阈值可根据实际平台的色号校准
if mean_b > 150 and mean_g > 150 and mean_r > 150:
print("该岗位为已浏览状态(灰色),跳过。")
return False
print("该岗位标题偏暗/全黑,未访问过,准备点击。")
return True
停止方案
现象:由于程序是通过操控系统光标来运行的,一旦发生死循环(如由于缩放问题点空了),人类用户试图去点关闭按钮,光标下一秒就被代码抢走了------完全失去了控制权。
全局异步监听钩子:(异步监听)。
python
import keyboard
import os
# 这个标志可以让我们的 while 循环提前结束
RUNNING = True
def emergency_stop():
global RUNNING
RUNNING = False
print("【拦截】检测到 ESC 键,紧急制动!")
os._exit(0) # 强杀进程,无论线程在阻塞什么
# 注册系统最高优先级的热键
keyboard.add_hotkey('esc', emergency_stop)
def main_loop():
while RUNNING:
print("我正在自动执行投递...")
# 执行投递动作 ...
time.sleep(2)