实战分享:如何用Python+Selenium+超级鹰搞定B站点选验证码(附完整代码)
实战拆解:用Python+Selenium+第三方API自动化处理B站点选验证码
最近在帮一个做内容分析的朋友处理B站数据时,遇到了那个让人头疼的点选验证码。手动操作不仅效率低下,而且当需要批量处理多个账号时,简直是一场噩梦。经过几天的摸索和踩坑,我总结出了一套相对稳定、可复用的自动化解决方案,核心就是利用Python的Selenium库进行浏览器模拟,再结合一个可靠的第三方验证码识别服务来“看懂”那些需要点击的文字或图案。
这套方法的核心思路其实很清晰:让程序像人一样“看到”验证码图片,识别出需要点击的位置,然后模拟鼠标去点击。但真正做起来,你会发现从截图、坐标转换到模拟点击,每一步都有不少细节需要注意,比如验证码的动态加载、坐标系的转换、点击的时机控制等等。下面我就把自己趟过的路、踩过的坑,以及最终跑通的完整代码,毫无保留地分享出来。
1. 环境搭建与核心工具选择
在开始写代码之前,我们需要先把“战场”布置好。工欲善其事,必先利其器,选择对的工具组合能让整个开发过程顺畅很多。
1.1 基础环境配置
首先,确保你的Python环境是3.7或以上版本。我习惯用虚拟环境来管理项目依赖,这样可以避免不同项目间的包版本冲突。
# 创建并激活虚拟环境(以venv为例)
python -m venv bilibili_auto_env
source bilibili_auto_env/bin/activate # Linux/Mac
# 或
bilibili_auto_env\Scripts\activate # Windows
接下来安装核心的Selenium库。Selenium是一个强大的浏览器自动化工具,可以模拟真实用户的所有操作。
pip install selenium
光有Selenium还不够,我们需要一个“司机”——WebDriver。它负责连接Selenium和真实的浏览器。以最常用的Chrome浏览器为例,你需要下载与你的Chrome版本匹配的ChromeDriver。
提示:ChromeDriver的版本必须与本地安装的Chrome浏览器主版本号完全一致,否则会报错。你可以通过访问Chrome的“关于”页面查看版本号,然后去官方或镜像站点下载对应的驱动。
将下载好的chromedriver可执行文件放在一个固定的目录,或者直接添加到系统的PATH环境变量中。我个人的习惯是放在项目根目录下,这样在代码里指定路径比较直观。
1.2 验证码识别服务的选择与接入
点选验证码的难点在于“识别”。我们需要一个能准确识别图片中文字位置的服务。市面上有不少提供此类API的服务商,其基本原理都是你上传图片,它返回图中目标文字的坐标。
在选择服务商时,我主要考量几个点:识别准确率、响应速度、价格以及API的稳定性。经过一番对比测试,我选择了一款口碑不错的服务。注册账号后,通常你会在用户中心获得以下几样东西:
- 用户名:你的登录账号。
- 密码:API调用的密码。
- 软件ID:用于标识你的应用,在后台创建软件即可获得。
此外,你还需要找到点选验证码对应的接口编号。不同服务商的编号可能不同,在其价格页或API文档中通常会明确列出。例如,针对“点击图中汉字”这类验证码,接口号可能是9004。
服务商一般会提供一个官方的Python SDK或示例代码。通常是一个名为chaojiying.py(或其他类似名称)的文件,里面封装了调用API的Chaojiying_Client类。你需要将这个文件下载并放置在你的项目目录中,以便在代码里导入使用。
2. 核心原理与流程拆解
在动手编码前,彻底理解整个自动化流程的每一步至关重要。这不仅能帮你写出更健壮的代码,还能在出现问题时快速定位。
2.1 自动化登录与验证码触发流程
B站的登录流程是标准化的:输入账号密码 -> 点击登录按钮 -> 触发验证码。我们的程序需要完美复现这个过程。
- 导航至登录页:使用Selenium打开B站的登录页面。
- 定位并填充表单:找到用户名和密码的输入框元素,通过
send_keys()方法输入你的账号信息。这里的关键是找到正确的元素定位器(如XPath、CSS Selector)。B站的页面结构可能会更新,所以你可能需要手动检查一下。 - 点击登录按钮:找到登录按钮并点击。这一步会触发网站弹出验证码挑战。
注意:在输入账号密码和点击登录按钮之间,适当加入短暂的等待(例如
time.sleep(1))是良好的实践,这模拟了人类的操作间隔,也能确保页面元素已加载就绪。
2.2 验证码图片的捕获与处理
验证码弹出后,我们需要把它“抓”下来送给识别API。这里有几个技术细节:
- 精准定位验证码区域:验证码通常被包裹在一个特定的HTML元素(如
<div>或<canvas>)中。你需要使用浏览器的开发者工具(F12)仔细检查,找到一个能唯一标识该容器的属性(如ID、特定的class组合)。使用find_element方法定位到这个元素。 - 截图而非下载:验证码图片可能是动态生成的,不一定有直接的图片URL。最可靠的方法是使用Selenium对定位到的元素进行截图。
element.screenshot('filename.png')方法可以将该元素的视觉内容保存为PNG文件。 - 等待渲染完成:点选验证码在出现时可能有动画效果(如渐入、缩放)。在截图前,最好使用
WebDriverWait配合预期条件(如元素可见、样式稳定)进行显式等待,或者简单地time.sleep(2),确保你截取的是最终静止状态的图片。
2.3 坐标识别、转换与模拟点击
这是最核心的一步,连接了“识别”和“动作”。
-
调用识别API:将上一步保存的图片文件以二进制读取模式打开,调用验证码识别服务的接口(如
PostPic(im, 9004))。成功的响应会返回一个字符串,格式通常是"x1,y1|x2,y2|...",即多个坐标用竖线分隔,每个坐标的x和y值用逗号分隔。 -
坐标数据解析:API返回的坐标是相对于你截取的那张图片的。我们需要用Python代码将这个字符串解析成程序可用的列表格式。
# 假设API返回 result = "123,45|67,89" coord_list = [] for point in result.split('|'): x, y = map(int, point.split(',')) coord_list.append([x, y]) # coord_list 现在是 [[123, 45], [67, 89]] -
坐标系转换与点击:解析出的坐标是图片内的像素坐标。而Selenium的点击操作需要基于浏览器页面中的某个元素(我们之前定位的验证码容器)进行偏移。幸运的是,Selenium的
ActionChains提供了move_to_element_with_offset(element, xoffset, yoffset)方法。这里的xoffset和yoffset就是我们解析出来的x和y坐标。然后调用click().perform()执行点击动作。 -
顺序点击:点选验证码通常要求按特定顺序点击。API返回的坐标顺序一般就是正确的点击顺序。我们按解析后列表的顺序遍历并执行点击即可。每次点击后可以加入微小延迟(如
time.sleep(0.5)),模拟人的反应时间。
2.4 处理识别失败与重试机制
没有任何识别服务能保证100%准确。我们必须为失败情况设计兜底方案。
- 失败判断:点击完成后,网站要么验证成功进入下一步/首页,要么验证失败刷新出新验证码。我们可以通过检测页面关键元素的变化来判断结果。例如,验证成功后,登录表单或验证码区域会消失;失败后,页面上可能仍保留“密码登录”等字样,或者出现新的验证码图。
- 重试逻辑:一旦判断失败,程序应该能够自动清理当前状态(如关闭可能弹出的错误提示),重新触发验证码获取流程,然后循环执行识别和点击操作。这通常通过一个
while循环来实现,直到成功标志出现才跳出循环。 - 避免无限循环:为了防止因意外导致的死循环,最好设置一个最大重试次数(例如5次),超过次数后记录错误并人工干预。
3. 完整代码实现与逐行解析
理论讲完了,是时候亮出干货了。下面是我优化后的完整脚本,包含了异常处理、显式等待和重试机制,比网上很多基础教程的代码要健壮得多。
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from chaojiying import Chaojiying_Client # 导入第三方识别SDK
class BilibiliAutoLogin:
def __init__(self, username, password, chaojiying_user, chaojiying_pass, soft_id):
"""
初始化自动化登录器
:param username: B站账号
:param password: B站密码
:param chaojiying_user: 验证码识别平台用户名
:param chaojiying_pass: 验证码识别平台密码
:param soft_id: 验证码识别平台软件ID
"""
self.username = username
self.password = password
# 初始化识别客户端
self.cjy_client = Chaojiying_Client(chaojiying_user, chaojiying_pass, soft_id)
# 初始化浏览器驱动,这里使用Chrome
self.driver = webdriver.Chrome() # 确保chromedriver在PATH中或指定路径
self.wait = WebDriverWait(self.driver, 10) # 创建显式等待对象,最多等10秒
def login_and_handle_captcha(self, max_retries=5):
"""执行登录并处理验证码的主流程"""
try:
self.driver.get("https://passport.bilibili.com/login")
print("已打开登录页面")
# 1. 输入账号密码
user_input = self.wait.until(
EC.presence_of_element_located((By.ID, "login-username"))
)
pass_input = self.driver.find_element(By.ID, "login-passwd")
user_input.send_keys(self.username)
time.sleep(0.5) # 模拟人工输入间隔
pass_input.send_keys(self.password)
print("账号密码已输入")
# 2. 点击登录按钮,触发验证码
login_btn = self.driver.find_element(By.CLASS_NAME, "btn-login")
login_btn.click()
print("已点击登录,等待验证码加载...")
time.sleep(2) # 等待验证码弹出
retry_count = 0
while retry_count < max_retries:
print(f"\n--- 第 {retry_count + 1} 次尝试识别验证码 ---")
if self._process_single_captcha():
print(">>> 验证码处理成功,登录流程完成!")
return True
else:
retry_count += 1
print(f"验证码识别失败,准备第 {retry_count + 1} 次尝试...")
# 失败后,可能需要点击刷新验证码的按钮,这里假设有刷新按钮
# refresh_btn = self.driver.find_element(By.CLASS_NAME, "geetest_refresh")
# refresh_btn.click()
time.sleep(2) # 等待新验证码加载
print(f">>> 经过 {max_retries} 次尝试仍未成功,请手动检查。")
return False
except Exception as e:
print(f"登录过程发生异常: {e}")
return False
finally:
# 暂时不关闭浏览器,方便调试
# self.driver.quit()
pass
def _process_single_captcha(self):
"""处理单次验证码识别的核心逻辑"""
try:
# 3. 定位并截取验证码图片
# 注意:此XPath可能需要根据B站实际页面结构调整
captcha_element = self.wait.until(
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'geetest_widget')]"))
)
captcha_screenshot_path = "captcha_temp.png"
captcha_element.screenshot(captcha_screenshot_path)
print("验证码截图已保存")
# 4. 调用第三方API识别
with open(captcha_screenshot_path, 'rb') as f:
img_bytes = f.read()
# 9004 是点选验证码的常见接口编号,请根据你使用的平台确认
result = self.cjy_client.PostPic(img_bytes, 9004)
print(f"识别API返回原始数据: {result}")
# 检查识别是否成功
if result['err_no'] != 0:
print(f"识别服务返回错误: {result['err_msg']}")
return False
pic_str = result['pic_str'] # 格式如 "123,45|67,89|..."
if not pic_str:
print("识别结果为空")
return False
# 5. 解析坐标
points = []
for point_str in pic_str.split('|'):
x_str, y_str = point_str.split(',')
points.append((int(x_str), int(y_str)))
print(f"解析出的坐标点: {points}")
# 6. 模拟点击每个点
action = ActionChains(self.driver)
for x, y in points:
# move_to_element_with_offset 基于元素的左上角进行偏移点击
action.move_to_element_with_offset(captcha_element, x, y).click().pause(0.3)
action.perform()
print("所有坐标点已模拟点击")
# 7. 点击验证码的确认/提交按钮
# 此按钮的定位器也需要根据实际情况调整
time.sleep(1) # 等待点击动画完成
confirm_btn = self.driver.find_element(By.XPATH, "//div[@class='geetest_commit']/button")
confirm_btn.click()
print("已点击确认按钮")
# 8. 等待并判断是否成功
time.sleep(3) # 等待页面跳转或刷新
# 判断成功的条件:登录表单消失,或者出现用户头像等成功登录元素
# 这里简单通过检查登录按钮是否还存在来判断(可能不严谨,仅作示例)
try:
# 如果还能快速找到登录按钮,可能意味着还在登录页
self.driver.find_element(By.CLASS_NAME, "btn-login")
print("检测到仍在登录页,可能验证失败")
return False
except:
# 找不到登录按钮,假设跳转成功
print("未检测到登录按钮,假设登录成功")
return True
except Exception as e:
print(f"处理验证码过程中出错: {e}")
return False
# ====== 使用示例 ======
if __name__ == "__main__":
# !!! 重要:请在此填入你自己的信息 !!!
BILIBILI_USER = "your_bilibili_username"
BILIBILI_PASS = "your_bilibili_password"
CJY_USER = "your_chaojiying_username"
CJY_PASS = "your_chaojiying_password"
CJY_SOFT_ID = "your_soft_id" # 在平台用户中心查看
auto_login = BilibiliAutoLogin(BILIBILI_USER, BILIBILI_PASS, CJY_USER, CJY_PASS, CJY_SOFT_ID)
success = auto_login.login_and_handle_captcha(max_retries=3)
if success:
print("自动化登录流程执行完毕,看起来成功了!")
# 你可以在这里继续执行登录后的操作,如跳转到个人主页等
# auto_login.driver.get("https://space.bilibili.com")
else:
print("自动化登录失败,建议手动检查网络、账号或验证码识别服务状态。")
# 程序结束后,可以手动关闭浏览器窗口
代码关键点解析:
- 类封装:将功能封装成类
BilibiliAutoLogin,使代码结构更清晰,易于管理和复用。 - 显式等待:大量使用
WebDriverWait配合expected_conditions,代替固定的time.sleep,使代码更高效、更稳定。只有当元素确实出现时才进行操作,避免了因网络延迟导致的元素找不到错误。 - 异常处理:在关键步骤(如截图、识别、点击)周围使用
try...except,捕获并打印错误信息,避免程序因单点失败而完全崩溃。 - 分离关注点:将处理单次验证码的逻辑独立成
_process_single_captcha方法,而主流程login_and_handle_captcha负责重试控制,逻辑更清晰。 - 可配置的重试次数:通过
max_retries参数控制最大重试次数,避免无限循环。
4. 高级优化与避坑指南
直接运行上面的代码可能不会一帆风顺,因为真实环境比本地测试复杂得多。下面分享一些我实践中总结的优化技巧和常见问题的解决方法。
4.1 提升识别成功率的技巧
验证码识别的准确性是整个流程的瓶颈。除了依赖第三方服务的算法能力,我们可以在提交图片前做一些预处理:
- 元素截图 vs 全屏截图:代码中使用了针对验证码元素的截图
element.screenshot(),这能有效排除页面其他部分的干扰。务必确保定位的元素精准覆盖验证码区域。 - 处理动态效果:有些验证码在加载时有阴影、闪烁或拖影。在截图前增加足够的等待时间(如
time.sleep(2)),或使用等待条件直到元素样式稳定,可以确保截到清晰的静态图。 - 备用方案:如果主要识别服务连续失败,可以考虑集成一个备用服务。在代码中设置一个阈值,当主服务失败次数超过阈值时,切换至备用API,并将图片同时发给两个服务,取置信度更高的结果。
4.2 应对反自动化策略
像B站这样的大型网站,一定有反爬虫和反自动化机制。我们的脚本需要表现得更像真人。
- 随机化操作间隔:将所有固定的
time.sleep(1)替换为随机延迟,例如time.sleep(random.uniform(0.5, 1.5))。在输入账号密码、点击按钮、点击验证码坐标之间加入随机等待,能有效规避基于操作时序的检测。 - 模拟人类移动轨迹:直接使用
move_to_element_with_offset然后click,鼠标是“瞬移”过去的。更高级的模拟可以使用ActionChains的move_by_offset进行分步移动,或者引入像pyautogui这样的库来生成贝塞尔曲线移动路径,但复杂度会大大增加。对于大多数情况,加入随机延迟已经足够。 - 浏览器指纹管理:Selenium驱动的浏览器有一些特征可以被网站检测到(如
navigator.webdriver属性)。可以考虑使用undetected-chromedriver这类修改过的驱动来隐藏自动化特征。不过,这属于更进阶的对抗,初期可以暂不考虑。
4.3 代码健壮性与维护性
-
配置外部化:不要将账号、密码、API密钥等敏感信息硬编码在脚本里。应该使用环境变量或配置文件(如
config.ini、config.json)来管理。# config.json { "bilibili": { "username": "your_username", "password": "your_password" }, "captcha_service": { "username": "your_cjy_user", "password": "your_cjy_pass", "soft_id": "your_soft_id" } } -
日志记录:使用Python的
logging模块替代print语句,可以方便地设置日志级别、输出到文件,便于后期排查问题。 -
元素定位器的维护:网站的页面结构可能会改变。将所有用于定位元素的XPath或CSS Selector集中定义在类的顶部或单独的配置文件中。一旦页面改版,你只需要修改一处即可。
class Locators: USERNAME_INPUT = (By.ID, "login-username") PASSWORD_INPUT = (By.ID, "login-passwd") LOGIN_BUTTON = (By.CLASS_NAME, "btn-login") CAPTCHA_AREA = (By.XPATH, "//div[contains(@class, 'geetest_widget')]") CONFIRM_BUTTON = (By.XPATH, "//div[@class='geetest_commit']/button")
4.4 常见错误与排查
| 错误现象 | 可能原因 | 排查建议 |
|---|---|---|
NoSuchElementException |
页面元素未加载出来,或定位器已过期。 | 1. 增加等待时间或使用显式等待。 2. 使用浏览器开发者工具重新检查元素属性,更新定位器。 |
| 识别坐标点击后验证失败 | 1. 坐标识别错误。 2. 点击顺序错误。 3. 坐标系计算有误(如忽略了页面缩放)。 |
1. 手动保存截图,查看识别出的坐标点在图片上是否准确。 2. 确认API返回的坐标顺序是否为点击顺序。 3. 确保截图元素是验证码的直接容器,且没有进行CSS变换。 |
| 无限重试或成功判断失灵 | 成功/失败的判断逻辑不准确。 | 优化成功判断条件。不要仅依赖一个元素,可以组合判断:验证码容器消失 且 用户相关元素出现。 |
InvalidSessionIdException |
浏览器窗口被意外关闭或驱动会话失效。 | 检查浏览器是否被手动关闭,或脚本中是否有driver.quit()被提前执行。确保网络稳定。 |
这套方案已经成功帮我朋友处理了数百次的登录验证。当然,自动化与反自动化是一场持续的博弈,今天有效的方法明天可能需要调整。核心是理解原理,保持代码的灵活性和可维护性,这样当变化来临时,你就能快速适应。最后,请务必在遵守网站服务条款和合理使用原则的前提下应用这些技术。
更多推荐


所有评论(0)