# 登录脚本实现笔记(v1.1.0) 本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 版本集成了完整的滑块验证码自动破解功能,大幅提升自动化程度。 ## 文件结构 ``` src/ ├── login.ts # Playwright 入口脚本 └── slider/ # v1.1.0 新增滑块验证模块 ├── index.ts ├── types.ts ├── detector.ts ├── detector-self-learning.ts ├── slider-controller.ts ├── cli.ts ├── validator.ts ├── detection/ │ └── candidate-search.ts └── utils/ ├── geometry.ts └── image.ts ``` 辅助文档位于项目根目录: - `README.md`:使用说明与常见问题 - `ARCHITECTURE.md`:整体架构与流程拆解 - `QUICKSTART.md`:快速开始指南 - `CHANGELOG.md`:版本更新日志 - `login.md`:早期需求说明,可作为手动操作参考 ## 核心流程(v1.1.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 次) - 通过 `prompt` 等待用户输入短信验证码并提交 - 等待 Playwright 检测到页面离开登录地址或抛出超时 4. **确认状态并写入 Cookie 文件** - `isLoggedIn` 再次判断是否登录成功 - 调用 `context.storageState({ path })` 将状态写入 `~/douban-cookie.json` - 终端提示成功信息,方便用户确认文件路径 ## 关键函数 ### `isLoggedIn(page: Page): Promise` 检查 `dbcl2` Cookie 是否存在,并确认登录表单元素是否仍可见。该组合可较准确判断是否处于登录态,同时避免依赖豆瓣首页 DOM。 ### `prepareContext(browser: Browser)` 负责上下文复用策略:优先尝试加载本地 Cookie 创建上下文,如果判定仍未登录则回退到全新会话并跳转登录页。函数返回 `{ context, page, usedCookies }`,调用方可据此判断是否需要重走登录流程。 ### `loginWithSms(page: Page, phone: string)` 串联短信验证码登录的主要逻辑,所有用户交互点都通过控制台提示: - 页面操作由脚本自动完成(填手机号、点击按钮) - **[v1.1.0]** 滑块验证自动处理(启用 `DOUBAN_AUTO_SLIDER=1` 时) - 短信验证码输入由用户处理 - 函数内部对提交过程设置合理的等待时间,避免过早关闭浏览器 ### `main()` 作为 CLI 入口,负责整体 orchestrate:校验配置 → 启动浏览器 → 调用上述函数 → 捕获异常并设置 `process.exitCode`。 ## v1.1.0 新增核心函数 ### `SliderController.solveSlider(page, sliderSelector, captchaSelector)` 滑块验证的主控制器,负责完整的验证流程: ```typescript async solveSlider( page: Page, sliderSelector: string = '.tcaptcha_drag_button', captchaSelector: string = '#tcaptcha_iframe' ): Promise ``` **工作流程**: 1. 等待验证码 iframe 加载(`waitForSelector`) 2. 等待滑块背景图完全加载 3. 进入重试循环(最多 10 次): - 调用 `captureSliderImage()` 截图到 `noflag/` - 调用 `SliderDetector.detectSlider()` 检测滑块 - 调用 `calculateDistance()` 计算移动距离 - 调用 `dragSlider()` 拖动滑块 - 调用 `checkSuccess()` 检测是否成功 - 成功则返回,失败则刷新验证码重试 **返回值**: ```typescript interface SliderSolveResult { success: boolean; // 是否成功 attempts: number; // 尝试次数 distance?: number; // 滑动距离(像素) } ``` ### `SliderDetector.detectSlider(imagePath, outputPath, drawBoxes)` 滑块检测的核心算法实现: ```typescript async detectSlider( imagePath: string, outputPath: string, drawBoxes: boolean = true ): Promise ``` **工作流程**: 1. 使用 Sharp 加载图像 2. 缩放到 800px 宽度(保持宽高比) 3. 调用 `CandidateSearch.findCandidates()` 获取候选框 4. 对每个候选框计算综合评分 5. 按评分排序,选择前 2 个 6. 如果只有 1 个,尝试使用模板匹配找第二个 7. 绘制红框标注并保存到 `outputPath` 8. 返回检测到的滑块位置数组 **评分标准**: - 形状评分:宽高比、面积合理性 - 色调一致性:内部颜色是否统一 - 边缘密度:边缘特征是否明显 - 梯度平滑度:是否有明确的边界 ### `CandidateSearch.findCandidates(rawImage)` 多策略并行检测候选区域: ```typescript async findCandidates(rawImage: RawImage): Promise ``` **四种策略**: 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 简化算法**的核心实现: ```typescript private calculateDistance( boxes: BoundingBox[], scaleX: number ): number ``` **逻辑**: ```typescript 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)` 拖动滑块到指定距离: ```typescript private async dragSlider(distance: number): Promise ``` **实现细节**: - 获取滑块按钮的 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 宽度,用于拖动 **转换公式**: ```typescript 显示坐标 = 图像坐标 / scaleX scaleX = 图像宽度 / 显示宽度 ≈ 800 / 340 ≈ 2.35 ``` ### 距离计算演进 **v1.0.0**:需要人工完成滑块 **v1.1.0 早期**:复杂的坐标转换 ```typescript // 错误的复杂逻辑(已废弃) const iframeBox = await iframe.boundingBox(); const distance = targetBox.x - sliderBox.x + iframeBox.x - sliderBox.x; ``` **v1.1.0 最终**:简化为几何原理 ```typescript // 正确的简洁逻辑(当前实现) const distance = (box2.x - box1.x) / scaleX; ``` **为什么简化有效**: - 检测坐标和拖动坐标在同一个相对坐标系中 - iframe 偏移量对两个滑块的影响相同 - 直接计算水平距离差,无需考虑绝对位置 ### 图像处理技术 **Sharp 库应用**: 1. **图像缩放** ```typescript const resized = await sharp(imagePath) .resize(targetWidth, null, { fit: 'inside' }) .raw() .toBuffer({ resolveWithObject: true }); ``` 2. **Sobel 边缘检测** ```typescript const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1]; const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1]; // 卷积计算边缘强度 ``` 3. **颜色空间转换** ```typescript // 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. **形态学操作** ```typescript // 膨胀:扩大白色区域 // 腐蚀:缩小白色区域 // 连通组件分析:查找连续区域 ``` ### 性能优化 **并行检测**: ```typescript 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 的完整实现细节。滑块自动化已成功集成并经过验证。