14 KiB
登录脚本实现笔记(v1.2.0)
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 引入了完整的滑块验证码自动破解能力,v1.2.0 在此基础上新增 macOS 短信自动读取与回填流程,让整体登录体验更加无感。
文件结构
graph TD
src_dir[src/]
src_dir --> login_ts[login.ts<br/>Playwright 入口脚本]
src_dir --> sms_dir[sms/<br/>v1.2.0 新增短信读取模块]
sms_dir --> sms_code[douban-code.ts]
src_dir --> slider_dir[slider/<br/>v1.1.0 滑块验证模块]
slider_dir --> slider_index[index.ts]
slider_dir --> slider_types[types.ts]
slider_dir --> slider_detector[detector.ts]
slider_dir --> slider_self[detector-self-learning.ts]
slider_dir --> slider_controller[slider-controller.ts]
slider_dir --> slider_cli[cli.ts]
slider_dir --> slider_validator[validator.ts]
slider_dir --> slider_detection[detection/]
slider_dir --> slider_utils[utils/]
slider_detection --> slider_candidate[candidate-search.ts]
slider_utils --> slider_geometry[geometry.ts]
slider_utils --> slider_image[image.ts]
辅助文档位于项目根目录:
README.md:使用说明与常见问题ARCHITECTURE.md:整体架构与流程拆解QUICKSTART.md:快速开始指南CHANGELOG.md:版本更新日志login.md:早期需求说明,可作为手动操作参考
核心流程(v1.2.0)
-
读取配置
- 通过
process.env.DOUBAN_PHONE获取手机号,缺失时直接退出 - 检查
process.env.DOUBAN_AUTO_SLIDER是否启用自动滑块验证
- 通过
-
准备浏览器上下文 (
prepareContext)- 若存在
~/douban-cookie.json,以storageState形式加载 - 打开登录页并调用
isLoggedIn校验是否仍在登录态 - 失效时关闭旧上下文并创建全新 session
- 若存在
-
执行短信登录 (
loginWithSms)- 输入手机号 → 点击「获取验证码」
- [v1.1.0] 自动检测并处理滑块验证码:
- 调用
SliderController.solveSlider() - 等待验证码 iframe 出现
- 截图并保存到
noflag/目录 - 调用检测算法识别滑块位置
- 计算滑动距离并执行拖动
- 验证成功后继续,失败则重试(最多 10 次)
- 调用
- [v1.2.0] 调用
waitForDoubanCode()轮询chat.db,解析验证码 - 若读取超时或权限不足,提示用户通过
prompt手动输入验证码 - 等待 Playwright 检测到页面离开登录地址或抛出超时
-
确认状态并写入 Cookie 文件
isLoggedIn再次判断是否登录成功- 调用
context.storageState({ path })将状态写入~/douban-cookie.json - 终端提示成功信息,方便用户确认文件路径
关键函数
isLoggedIn(page: Page): Promise<boolean>
检查 dbcl2 Cookie 是否存在,并确认登录表单元素是否仍可见。该组合可较准确判断是否处于登录态,同时避免依赖豆瓣首页 DOM。
prepareContext(browser: Browser)
负责上下文复用策略:优先尝试加载本地 Cookie 创建上下文,如果判定仍未登录则回退到全新会话并跳转登录页。函数返回 { context, page, usedCookies },调用方可据此判断是否需要重走登录流程。
loginWithSms(page: Page, phone: string)
串联短信验证码登录的主要逻辑,所有用户交互点都通过控制台提示:
- 页面操作由脚本自动完成(填手机号、点击按钮)
- [v1.1.0] 滑块验证自动处理(启用
DOUBAN_AUTO_SLIDER=1时) - [v1.2.0] 优先自动读取短信验证码,失败时降级到命令行输入
- 函数内部对提交过程设置合理的等待时间,避免过早关闭浏览器
waitForDoubanCode(options?: WaitForCodeOptions)
负责从 macOS 信息数据库读取最新的验证码短信:
- 使用
better-sqlite3以只读方式打开~/Library/Messages/chat.db - 记录初始最新消息的
ROWID,避免重复解析旧短信 - 周期性查询包含“豆瓣”“验证码”关键词的消息并解析其中的 4-6 位验证码
- 成功返回
{ code, message },失败在超时后抛出异常供调用方降级处理 options支持timeoutMs、pollIntervalMs以及logger回调,便于定制等待时长和日志输出
main()
作为 CLI 入口,负责整体 orchestrate:校验配置 → 启动浏览器 → 调用上述函数 → 捕获异常并设置 process.exitCode。
v1.2.0 新增能力
-
短信自动读取模块
- 新增
src/sms/douban-code.ts,通过better-sqlite3查询 macOS “信息”数据库; - 解析满足“豆瓣 + 验证码”关键字的最新短信,返回验证码及原始消息。
- 新增
-
验证码自动回填
login.ts显式等待input#code可见后填入验证码;- 日志输出增加
[短信读取]前缀,便于排查权限或解析问题; - 超时或数据库不可用时抛出异常,交由上层降级到
prompt。
-
依赖与配置更新
- 新增
better-sqlite3依赖及类型声明; - 文档统一说明 macOS 完全磁盘访问权限要求。
- 新增
v1.1.0 新增核心函数
SliderController.solveSlider(page, sliderSelector, captchaSelector)
滑块验证的主控制器,负责完整的验证流程:
async solveSlider(
page: Page,
sliderSelector: string = '.tcaptcha_drag_button',
captchaSelector: string = '#tcaptcha_iframe'
): Promise<SliderSolveResult>
工作流程:
- 等待验证码 iframe 加载(
waitForSelector) - 等待滑块背景图完全加载
- 进入重试循环(最多 10 次):
- 调用
captureSliderImage()截图到noflag/ - 调用
SliderDetector.detectSlider()检测滑块 - 调用
calculateDistance()计算移动距离 - 调用
dragSlider()拖动滑块 - 调用
checkSuccess()检测是否成功 - 成功则返回,失败则刷新验证码重试
- 调用
返回值:
interface SliderSolveResult {
success: boolean; // 是否成功
attempts: number; // 尝试次数
distance?: number; // 滑动距离(像素)
}
SliderDetector.detectSlider(imagePath, outputPath, drawBoxes)
滑块检测的核心算法实现:
async detectSlider(
imagePath: string,
outputPath: string,
drawBoxes: boolean = true
): Promise<BoundingBox[] | null>
工作流程:
- 使用 Sharp 加载图像
- 缩放到 800px 宽度(保持宽高比)
- 调用
CandidateSearch.findCandidates()获取候选框 - 对每个候选框计算综合评分
- 按评分排序,选择前 2 个
- 如果只有 1 个,尝试使用模板匹配找第二个
- 绘制红框标注并保存到
outputPath - 返回检测到的滑块位置数组
评分标准:
- 形状评分:宽高比、面积合理性
- 色调一致性:内部颜色是否统一
- 边缘密度:边缘特征是否明显
- 梯度平滑度:是否有明确的边界
CandidateSearch.findCandidates(rawImage)
多策略并行检测候选区域:
async findCandidates(rawImage: RawImage): Promise<BoundingBox[]>
四种策略:
-
暗区域检测 (
findDarkRegions)- 基于亮度阈值(< 100)
- 连通组件分析
- 形状过滤(宽高比、面积)
-
Canny 边缘检测 (
findEdgeDensityRegions)- Canny 算法提取边缘
- 滑动窗口统计边缘密度
- 局部最大值抑制
-
颜色量化 (
findColorQuantizationRegions)- K-means 聚类(k=5)
- 提取少数色块区域
- 形状验证
-
LAB 色彩空间 (
findLabColorRegions)- 转换到 LAB 空间
- 基于 a*、b* 通道的色度检测
- 连通组件分析
去重策略:
- 计算所有候选框的 IoU(交并比)
- IoU > 0.3 认为是同一个滑块
- 保留评分最高的
calculateDistance(boxes, scaleX)
v1.1.0 简化算法的核心实现:
private calculateDistance(
boxes: BoundingBox[],
scaleX: number
): number
逻辑:
if (boxes.length >= 2) {
// 双滑块模式(推荐)
// "两只小鸟嘴尖距离"原理
const distance = (boxes[1].x - boxes[0].x) / scaleX;
return Math.round(distance);
} else if (boxes.length === 1) {
// 单滑块模式(兜底)
const distance = boxes[0].x / scaleX;
return Math.round(distance);
} else {
return 0;
}
为什么除以 scaleX:
- 检测在 800px 宽度图像上进行
- 实际显示宽度是 340px
- scaleX = 800 / 340 ≈ 2.35
- 需要将检测坐标转换回显示坐标
dragSlider(distance)
拖动滑块到指定距离:
private async dragSlider(distance: number): Promise<void>
实现细节:
- 获取滑块按钮的 bounding box
- 计算起始位置(滑块中心)
- 计算目标位置(起始 + 距离)
- 使用
page.mouse.move()拖动 steps参数实现平滑移动(默认 20 步)
拟人化特性:
- 使用 Playwright 的内置缓动函数
- 平滑的加速-减速曲线
- 避免机械化的匀速直线移动
错误处理与提示
- 打印清晰的步骤提示,例如"请等待短信验证码…"、"正在提交验证码…"
- [v1.1.0] 滑块检测过程的详细日志:
[SliderController] 开始滑块验证,最多尝试 10 次 [SliderController] ===== 第 1/10 次尝试 ===== [SliderDetector] 图像已缩放: 340x191 -> 800x449 (scaleX=2.35) [SliderDetector] 检测到 2 个滑块候选框 [SliderController] 计算距离: (195 - 45) / 2.35 = 63.8px [SliderController] ✓ 滑块验证成功! - 捕获 Playwright 的超时异常,允许在页面未完全跳转时通过
isLoggedIn再次确认 - 如登录失败会输出明确日志并保持退出码非零,方便在 CI 或脚本中检测
- [v1.1.0] 视觉调试:
noflag/目录保存原始截图output/目录保存带红框标注的检测结果- 便于人工验证检测准确性
手动操作注意事项
- Playwright 会以非无头模式启动 Chromium,务必保持窗口前台
- [v1.1.0] 启用
DOUBAN_AUTO_SLIDER=1时会自动处理滑块 - 如果自动验证失败(10 次后),仍可手动完成滑块
- 如短信验证码输入错误,可重新运行脚本
- 保存的
douban-cookie.json与账号强绑定,若切换账号需手动删除或覆盖该文件 - [v1.1.0] 可查看
output/目录的标注图验证检测准确性
v1.1.0 技术细节
坐标系统
两套坐标系:
- 图像坐标系:800px 宽度,用于检测
- 显示坐标系:340px 宽度,用于拖动
转换公式:
显示坐标 = 图像坐标 / scaleX
scaleX = 图像宽度 / 显示宽度 ≈ 800 / 340 ≈ 2.35
距离计算演进
v1.0.0:需要人工完成滑块
v1.1.0 早期:复杂的坐标转换
// 错误的复杂逻辑(已废弃)
const iframeBox = await iframe.boundingBox();
const distance = targetBox.x - sliderBox.x + iframeBox.x - sliderBox.x;
v1.1.0 最终:简化为几何原理
// 正确的简洁逻辑(当前实现)
const distance = (box2.x - box1.x) / scaleX;
为什么简化有效:
- 检测坐标和拖动坐标在同一个相对坐标系中
- iframe 偏移量对两个滑块的影响相同
- 直接计算水平距离差,无需考虑绝对位置
图像处理技术
Sharp 库应用:
-
图像缩放
const resized = await sharp(imagePath) .resize(targetWidth, null, { fit: 'inside' }) .raw() .toBuffer({ resolveWithObject: true }); -
Sobel 边缘检测
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1]; const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1]; // 卷积计算边缘强度 -
颜色空间转换
// RGB → LAB const X = r * 0.4124 + g * 0.3576 + b * 0.1805; const Y = r * 0.2126 + g * 0.7152 + b * 0.0722; const Z = r * 0.0193 + g * 0.1192 + b * 0.9505; -
形态学操作
// 膨胀:扩大白色区域 // 腐蚀:缩小白色区域 // 连通组件分析:查找连续区域
性能优化
并行检测:
const [darkBoxes, edgeBoxes, colorBoxes, labBoxes] = await Promise.all([
this.findDarkRegions(rawImage),
this.findEdgeDensityRegions(rawImage),
this.findColorQuantizationRegions(rawImage),
this.findLabColorRegions(rawImage),
]);
IoU 去重:
- 避免重复检测同一个滑块
- 减少后续评分计算量
- 提高整体检测速度
缓存策略:
- 原始截图保存在
noflag/,可重复使用 - 标注结果保存在
output/,便于批量验证
后续拓展建议
- 多账号支持:通过配置文件或命令行参数管理多组手机号与存储路径
- 验证码服务集成:接入外部短信/验证码平台以减少人工步骤
- 任务编排:在登录后追加业务逻辑(例如抓取列表、导出数据),可在
main函数成功分支追加调用 - CLI 体验:封装命令行参数解析,避免频繁依赖环境变量
- [v1.1.0+] 机器学习模型:
- 使用 CNN 替代规则式检测
- 训练分类器识别滑块和缺口
- 提高复杂背景下的准确率
- [v1.1.0+] 更多验证码类型:
- 点选验证码
- 文字识别验证码
- 旋转验证码
- [v1.1.0+] 反爬虫对抗:
- 更自然的鼠标轨迹(贝塞尔曲线)
- 随机延迟和抖动
- 模拟人类思考时间
v1.1.0 成功的关键因素
- 用户洞察:"两只小鸟嘴尖距离"的类比帮助简化了距离计算
- 坐标系统一:在同一坐标系中计算相对距离,避免复杂转换
- 多策略并行:四种检测算法互补,提高鲁棒性
- 视觉调试:红框标注便于人工验证和调试
- 自动重试:10 次重试机制大幅提高成功率
以上内容覆盖 v1.1.0 的完整实现细节。滑块自动化已成功集成并经过验证。