410 lines
14 KiB
Markdown
410 lines
14 KiB
Markdown
# 登录脚本实现笔记(v1.2.0)
|
||
|
||
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 引入了完整的滑块验证码自动破解能力,v1.2.0 在此基础上新增 macOS 短信自动读取与回填流程,让整体登录体验更加无感。
|
||
|
||
## 文件结构
|
||
|
||
```mermaid
|
||
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` 支持 `timeoutMs`、`pollIntervalMs` 以及 `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)`
|
||
|
||
滑块验证的主控制器,负责完整的验证流程:
|
||
|
||
```typescript
|
||
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()` 检测是否成功
|
||
- 成功则返回,失败则刷新验证码重试
|
||
|
||
**返回值**:
|
||
```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<BoundingBox[] | null>
|
||
```
|
||
|
||
**工作流程**:
|
||
1. 使用 Sharp 加载图像
|
||
2. 缩放到 800px 宽度(保持宽高比)
|
||
3. 调用 `CandidateSearch.findCandidates()` 获取候选框
|
||
4. 对每个候选框计算综合评分
|
||
5. 按评分排序,选择前 2 个
|
||
6. 如果只有 1 个,尝试使用模板匹配找第二个
|
||
7. 绘制红框标注并保存到 `outputPath`
|
||
8. 返回检测到的滑块位置数组
|
||
|
||
**评分标准**:
|
||
- 形状评分:宽高比、面积合理性
|
||
- 色调一致性:内部颜色是否统一
|
||
- 边缘密度:边缘特征是否明显
|
||
- 梯度平滑度:是否有明确的边界
|
||
|
||
### `CandidateSearch.findCandidates(rawImage)`
|
||
|
||
多策略并行检测候选区域:
|
||
|
||
```typescript
|
||
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 简化算法**的核心实现:
|
||
|
||
```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<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 宽度,用于拖动
|
||
|
||
**转换公式**:
|
||
```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 的完整实现细节。滑块自动化已成功集成并经过验证。
|