Files
douban-login/IMPLEMENTATION.md
2025-10-26 10:24:17 +08:00

14 KiB
Raw Permalink Blame History

登录脚本实现笔记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

  1. 读取配置

    • 通过 process.env.DOUBAN_PHONE 获取手机号,缺失时直接退出
    • 检查 process.env.DOUBAN_AUTO_SLIDER 是否启用自动滑块验证
  2. 准备浏览器上下文 (prepareContext)

    • 若存在 ~/douban-cookie.json,以 storageState 形式加载
    • 打开登录页并调用 isLoggedIn 校验是否仍在登录态
    • 失效时关闭旧上下文并创建全新 session
  3. 执行短信登录 (loginWithSms)

    • 输入手机号 → 点击「获取验证码」
    • [v1.1.0] 自动检测并处理滑块验证码:
      • 调用 SliderController.solveSlider()
      • 等待验证码 iframe 出现
      • 截图并保存到 noflag/ 目录
      • 调用检测算法识别滑块位置
      • 计算滑动距离并执行拖动
      • 验证成功后继续,失败则重试(最多 10 次)
    • [v1.2.0] 调用 waitForDoubanCode() 轮询 chat.db,解析验证码
    • 若读取超时或权限不足,提示用户通过 prompt 手动输入验证码
    • 等待 Playwright 检测到页面离开登录地址或抛出超时
  4. 确认状态并写入 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 支持 timeoutMspollIntervalMs 以及 logger 回调,便于定制等待时长和日志输出

main()

作为 CLI 入口,负责整体 orchestrate校验配置 → 启动浏览器 → 调用上述函数 → 捕获异常并设置 process.exitCode

v1.2.0 新增能力

  1. 短信自动读取模块

    • 新增 src/sms/douban-code.ts,通过 better-sqlite3 查询 macOS “信息”数据库;
    • 解析满足“豆瓣 + 验证码”关键字的最新短信,返回验证码及原始消息。
  2. 验证码自动回填

    • login.ts 显式等待 input#code 可见后填入验证码;
    • 日志输出增加 [短信读取] 前缀,便于排查权限或解析问题;
    • 超时或数据库不可用时抛出异常,交由上层降级到 prompt
  3. 依赖与配置更新

    • 新增 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>

工作流程

  1. 等待验证码 iframe 加载(waitForSelector
  2. 等待滑块背景图完全加载
  3. 进入重试循环(最多 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>

工作流程

  1. 使用 Sharp 加载图像
  2. 缩放到 800px 宽度(保持宽高比)
  3. 调用 CandidateSearch.findCandidates() 获取候选框
  4. 对每个候选框计算综合评分
  5. 按评分排序,选择前 2 个
  6. 如果只有 1 个,尝试使用模板匹配找第二个
  7. 绘制红框标注并保存到 outputPath
  8. 返回检测到的滑块位置数组

评分标准

  • 形状评分:宽高比、面积合理性
  • 色调一致性:内部颜色是否统一
  • 边缘密度:边缘特征是否明显
  • 梯度平滑度:是否有明确的边界

CandidateSearch.findCandidates(rawImage)

多策略并行检测候选区域:

async findCandidates(rawImage: RawImage): Promise<BoundingBox[]>

四种策略

  1. 暗区域检测 (findDarkRegions)

    • 基于亮度阈值(< 100
    • 连通组件分析
    • 形状过滤(宽高比、面积)
  2. Canny 边缘检测 (findEdgeDensityRegions)

    • Canny 算法提取边缘
    • 滑动窗口统计边缘密度
    • 局部最大值抑制
  3. 颜色量化 (findColorQuantizationRegions)

    • K-means 聚类k=5
    • 提取少数色块区域
    • 形状验证
  4. 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 技术细节

坐标系统

两套坐标系

  1. 图像坐标系800px 宽度,用于检测
  2. 显示坐标系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 库应用

  1. 图像缩放

    const resized = await sharp(imagePath)
      .resize(targetWidth, null, { fit: 'inside' })
      .raw()
      .toBuffer({ resolveWithObject: true });
    
  2. Sobel 边缘检测

    const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
    const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
    // 卷积计算边缘强度
    
  3. 颜色空间转换

    // 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;
    
  4. 形态学操作

    // 膨胀:扩大白色区域
    // 腐蚀:缩小白色区域
    // 连通组件分析:查找连续区域
    

性能优化

并行检测

const [darkBoxes, edgeBoxes, colorBoxes, labBoxes] = await Promise.all([
  this.findDarkRegions(rawImage),
  this.findEdgeDensityRegions(rawImage),
  this.findColorQuantizationRegions(rawImage),
  this.findLabColorRegions(rawImage),
]);

IoU 去重

  • 避免重复检测同一个滑块
  • 减少后续评分计算量
  • 提高整体检测速度

缓存策略

  • 原始截图保存在 noflag/,可重复使用
  • 标注结果保存在 output/,便于批量验证

后续拓展建议

  1. 多账号支持:通过配置文件或命令行参数管理多组手机号与存储路径
  2. 验证码服务集成:接入外部短信/验证码平台以减少人工步骤
  3. 任务编排:在登录后追加业务逻辑(例如抓取列表、导出数据),可在 main 函数成功分支追加调用
  4. CLI 体验:封装命令行参数解析,避免频繁依赖环境变量
  5. [v1.1.0+] 机器学习模型:
    • 使用 CNN 替代规则式检测
    • 训练分类器识别滑块和缺口
    • 提高复杂背景下的准确率
  6. [v1.1.0+] 更多验证码类型:
    • 点选验证码
    • 文字识别验证码
    • 旋转验证码
  7. [v1.1.0+] 反爬虫对抗:
    • 更自然的鼠标轨迹(贝塞尔曲线)
    • 随机延迟和抖动
    • 模拟人类思考时间

v1.1.0 成功的关键因素

  1. 用户洞察"两只小鸟嘴尖距离"的类比帮助简化了距离计算
  2. 坐标系统一:在同一坐标系中计算相对距离,避免复杂转换
  3. 多策略并行:四种检测算法互补,提高鲁棒性
  4. 视觉调试:红框标注便于人工验证和调试
  5. 自动重试10 次重试机制大幅提高成功率

以上内容覆盖 v1.1.0 的完整实现细节。滑块自动化已成功集成并经过验证。