164 lines
6.5 KiB
Markdown
164 lines
6.5 KiB
Markdown
# Phase 2: Boss 直聘重写 — 技术研究
|
||
|
||
**研究日期:** 2026-03-21
|
||
**阶段目标:** Boss 直聘爬虫完全基于 crawler_core 运行,旧实现可安全停用
|
||
|
||
---
|
||
|
||
## 1. 现状分析
|
||
|
||
### 1.1 crawler_core 现有基础(Phase 1 完成)
|
||
|
||
| 文件 | 内容 |
|
||
|------|------|
|
||
| `crawler_core/http_client.py` | `HTTPClient` — requests_go + TLS 伪装 + tenacity 重试(min=10s) + 代理池/隧道代理 |
|
||
| `crawler_core/base.py` | `Result[T]`(泛型)、`BaseFetcher`、`BaseSearcher`、`parse_response` |
|
||
| `crawler_core/boss/sign.py` | `BossSign.generate_traceid()` — 已完成且有测试 |
|
||
|
||
### 1.2 待迁移的 Boss 层(spiderJobs)
|
||
|
||
`spiderJobs/platforms/boss/` 下已有:
|
||
|
||
| 文件 | 当前依赖 | 迁移目标 |
|
||
|------|---------|---------|
|
||
| `sign.py` | 独立实现(与 crawler_core/boss/sign.py 功能相同) | 弃用,改为 import crawler_core |
|
||
| `client.py` | `spiderJobs.core.http_client.HTTPClient` | 改为 `crawler_core.http_client.HTTPClient` |
|
||
| `api.py` | `spiderJobs.core.base.ApiResult/BaseFetcher/BaseSearcher` | 改为 `crawler_core.base.Result/BaseFetcher/BaseSearcher` |
|
||
| `main.py` | `spiderJobs.core.base.BaseFetcher/BaseSearcher` | 更新 import,功能保持不变 |
|
||
|
||
### 1.3 待保留的反爬机制(SmartIPManager)
|
||
|
||
旧 `boos_api.py` 中有 `SmartIPManager`(代理轮换+本机 fallback),这套逻辑**已被 crawler_core 的 HTTPClient 代理池简化替代**:
|
||
- `HTTPClient(proxy_pool=...)` 自动随机选择代理(每次请求)
|
||
- `HTTPClient(tunnel_proxy=...)` 每次新建 session(效果类似隧道代理的 IP 轮换)
|
||
- tenacity 重试 min=10s 已满足强制延迟要求
|
||
|
||
结论:**不需要**将 SmartIPManager 迁移进来,crawler_core 已经覆盖了其功能。
|
||
|
||
---
|
||
|
||
## 2. API 接口清单(已确认)
|
||
|
||
`spiderJobs/platforms/boss/api.py` 实现了 4 个接口:
|
||
|
||
| 类 | 端点 | 方式 |
|
||
|----|------|------|
|
||
| `SearchRecJobs` | `/wapi/zpgeek/miniapp/homepage/recjoblist.json` | GET |
|
||
| `GetJobDetail` | `/wapi/batch/requests`(批量子请求) | POST |
|
||
| `GetBrandDetail` | `/wapi/zpgeek/miniapp/brand/detail.json` | GET |
|
||
| `SearchBrandJobs` | `/wapi/zpgeek/miniapp/brand/joblist.json` | GET |
|
||
|
||
Boss 响应格式与 crawler_core 默认解析不同:
|
||
- Boss:`code=0` 表示成功,`zpData` 为业务数据(而非 `statusCode/data`)
|
||
- 需要在迁移后的 api.py 中保留自定义 `_parse_boss_response()` 函数
|
||
|
||
---
|
||
|
||
## 3. 迁移差异分析
|
||
|
||
### 3.1 ApiResult → Result[T]
|
||
|
||
`spiderJobs.core.base.ApiResult` 与 `crawler_core.base.Result[T]` 字段对比:
|
||
|
||
| 字段 | ApiResult | Result[T] | 兼容? |
|
||
|------|-----------|-----------|-------|
|
||
| `success` | ✓ | ✓ | ✅ |
|
||
| `status_code` | ✓ | ✓ | ✅ |
|
||
| `data` | ✓ | ✓ | ✅ |
|
||
| `list` | ✓ | ✓ | ✅ |
|
||
| `count` | ✓ | ✓ | ✅ |
|
||
| `is_end_page` | ✓ | ✓ | ✅ |
|
||
| `error` | ✓ | ✓ | ✅ |
|
||
|
||
完全兼容,仅需修改 import 路径。
|
||
|
||
### 3.2 BaseFetcher._http → BaseFetcher.http_client
|
||
|
||
旧 spiderJobs 的 `BaseFetcher` 使用 `self._http` 引用 HTTP 客户端,而 `crawler_core.base.BaseFetcher` 使用 `self.http_client`。
|
||
|
||
`api.py` 中两处引用需要更新:
|
||
- `SearchRecJobs._request()`: `self._http.get(...)` → `self.http_client.get(...)`
|
||
- `GetJobDetail.fetch()`: `client: BossClient = self._http` → `client: BossClient = self.http_client`
|
||
- `SearchBrandJobs._request()`: `self._http.get(...)` → `self.http_client.get(...)`
|
||
|
||
### 3.3 BossClient 迁移
|
||
|
||
`client.py` 中 `BossClient` 继承的是 `spiderJobs.core.http_client.HTTPClient`,需改为继承 `crawler_core.http_client.HTTPClient`。两个 HTTPClient 接口完全相同,无结构性差异。
|
||
|
||
`sign.py`(spiderJobs 版)与 `crawler_core/boss/sign.py` 功能完全相同,迁移后 `client.py` 直接从 `crawler_core.boss.sign` 导入即可。
|
||
|
||
---
|
||
|
||
## 4. 测试策略(QUAL-03)
|
||
|
||
### 4.1 测试框架
|
||
|
||
项目已有:
|
||
- `conftest.py` 在项目根目录(用于 pytest path 设置)
|
||
- `tests/crawler_core/` 已有 41 个签名单元测试(Phase 1)
|
||
|
||
建议新增测试目录:`tests/boss/`,文件:`test_boss_client.py`
|
||
|
||
### 4.2 Mock/Respx 测试(注意事项)
|
||
|
||
crawler_core 的 HTTPClient 使用 `requests_go`(非标准 requests),respx 专门 mock `httpx`,不适用。
|
||
|
||
**替代方案:使用 `unittest.mock.patch`**
|
||
|
||
```python
|
||
# 正确的 mock 方式
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
def test_search_rec_jobs_success():
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"code": 0, "zpData": {"jobList": [{"title": "测试职位"}], "hasMore": False}
|
||
})
|
||
searcher = SearchRecJobs(client=mock_client)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success
|
||
assert len(result.list) == 1
|
||
```
|
||
|
||
由于 HTTPClient 是作为依赖注入传入的,直接用 `MagicMock()` mock 即可,无需 patch 装饰器。
|
||
|
||
---
|
||
|
||
## 5. 关键词搜索接口说明
|
||
|
||
当前 `SearchRecJobs` 只使用 `cityCode` 过滤,没有关键词搜索参数。
|
||
|
||
查看 Boss API,关键词搜索应使用 `/wapi/zpgeek/miniapp/search.json`(需要 query 参数)或直接使用 recjoblist 接口配合 `query` 字段(如果 API 支持)。
|
||
|
||
**推荐方案:** Phase 2 保持现有 `SearchRecJobs`(推荐列表),关键词过滤通过 `main.py` 的城市映射传入。真正的关键词搜索 API 端点验证留给手动测试阶段确认。
|
||
|
||
---
|
||
|
||
## 6. 反爬机制验证
|
||
|
||
Phase 2 成功标准之一是"反爬机制保留",具体验证点:
|
||
|
||
| 机制 | 实现位置 | 验证方式 |
|
||
|------|---------|---------|
|
||
| 随机延迟 10-20s | `HTTPClient` tenacity min=10s | 单元测试检查 wait_random_exponential 配置 |
|
||
| TLS 指纹伪装 | `HTTPClient._new_session()` → `TLS_CHROME_LATEST` | 代码审查(无法 mock TLS 层) |
|
||
| 代理轮换 | `HTTPClient(tunnel_proxy=...)` 每次新建 session | 单元测试确认 `_new_session()` 被调用 |
|
||
| Traceid 注入 | `BossClient._boss_headers()` | 单元测试检查请求头包含 Traceid |
|
||
|
||
## 7. Validation Architecture(Nyquist)
|
||
|
||
暂不适用本阶段:Phase 2 是代码迁移,无新功能/新接口,验证维度以单元测试+手动验证为主。
|
||
|
||
---
|
||
|
||
## RESEARCH COMPLETE
|
||
|
||
**Phase 2 可以规划。** 迁移任务明确,风险低(接口兼容),主要工作量在:
|
||
1. 更新 3 个文件的 import(client.py、api.py、main.py)
|
||
2. 修正 2 处 `self._http` → `self.http_client` 引用
|
||
3. 新增 mock 测试
|
||
|
||
预计拆分为 2 个 PLAN:
|
||
- Plan 01:迁移 client.py、api.py、main.py(去除 spiderJobs.core 依赖)
|
||
- Plan 02:新增 `tests/boss/test_boss_client.py` mock 测试
|