# Phase 3: 前程无忧 & 智联重写 — 技术研究

**研究日期:** 2026-03-21  
**阶段目标:** 前程无忧和智联招聘爬虫完全基于 crawler_core 运行，三平台统一使用新基类

---

## 1. 现状分析

### 1.1 crawler_core 现有基础（Phase 1 完成）

| 文件 | 内容 |
|------|------|
| `crawler_core/qcwy/sign.py` | `Job51Sign.build_sign_path()` — HMAC-SHA256 签名 |
| `crawler_core/zhilian/sign.py` | `ZhilianSign.sign_headers()/sign_params()` — 智联多类型签名 |
| `crawler_core/http_client.py` | `HTTPClient` — TLS 伪装 + 代理 + tenacity 重试 |
| `crawler_core/base.py` | `Result[T]`, `BaseFetcher`, `BaseSearcher` |

### 1.2 前程无忧（job51）待迁移层

`spiderJobs/platforms/job51/` 下已有全部文件：

| 文件 | 旧依赖 | 迁移目标 |
|------|--------|---------|
| `client.py` | `spiderJobs.core.http_client.HTTPClient` + `spiderJobs.platforms.job51.sign.Job51Sign` | → `crawler_core.http_client.HTTPClient` + `crawler_core.qcwy.sign.Job51Sign` |
| `api.py` | `spiderJobs.core.base.ApiResult/BaseFetcher/BaseSearcher` | → `crawler_core.base.Result/BaseFetcher/BaseSearcher` |
| `main.py` | `spiderJobs.core.base.BaseFetcher/BaseSearcher` | → `crawler_core.base.BaseFetcher/BaseSearcher` |
| `sign.py` | 独立实现（与 crawler_core/qcwy/sign.py 相同） | → 向后兼容桩，重新导出 `Job51Sign` |

**job51/api.py 具体变更：**
- 第 14 行 import 替换
- `ApiResult` 全量替换为 `Result`（共 11 处）
- 第 164 行：`self._http.get(endpoint)` → `self.http_client.get(endpoint)`
- 第 208 行：`self._http.get(self.ENDPOINT, ...)` → `self.http_client.get(self.ENDPOINT, ...)`

### 1.3 智联招聘（zhilian）待迁移层

`spiderJobs/platforms/zhilian/` 下已有全部文件：

| 文件 | 旧依赖 | 迁移目标 |
|------|--------|---------|
| `client.py` | `spiderJobs.core.http_client.HTTPClient` + `spiderJobs.platforms.zhilian.sign.ZhilianSign` | → `crawler_core.http_client.HTTPClient` + `crawler_core.zhilian.sign.ZhilianSign` |
| `api.py` | `spiderJobs.core.base.BaseFetcher/BaseSearcher` | → `crawler_core.base.BaseFetcher/BaseSearcher` |
| `main.py` | `spiderJobs.core.base.BaseFetcher/BaseSearcher` | → `crawler_core.base.BaseFetcher/BaseSearcher` |
| `sign.py` | 独立实现（与 crawler_core/zhilian/sign.py 相同） | → 向后兼容桩，重新导出 `ZhilianSign` |

**zhilian/api.py 具体变更：**
- 第 10 行 import 替换（无 ApiResult，zhilian 使用 crawler_core 的默认解析器，无需自定义 _parse_response）
- 第 200 行：`return self._http.get(` → `return self.http_client.get(`

**重要差异：** 智联 api.py 中 `SearchCompanyPositions._build_params()` 第 184 行使用了 `self._client.signer.sign_params()`，这是通过 `self._client`（设为传入的 ZhilianClient）间接访问 signer 的，迁移后不受影响（属性名不变）。

---

## 2. 对比 Phase 2（Boss）的工作量

| 维度 | Phase 2 Boss | Phase 3 job51 | Phase 3 zhilian |
|------|-------------|---------------|-----------------|
| `ApiResult` 替换 | 11 处 | 11 处 | 0 处（无自定义解析器） |
| `self._http` 替换 | 3 处 | 2 处 | 1 处 |
| sign.py → 桩 | ✓ | ✓ | ✓ |
| client.py import | ✓ | ✓ | ✓ |
| api.py import | ✓ | ✓ | ✓ |
| main.py import | ✓ | ✓ | ✓ |

---

## 3. mock 测试策略

### 3.1 job51 测试（tests/job51/test_job51_client.py）

job51 的 mock 策略与 Boss 完全一致：用 `MagicMock()` mock `http_client`，测试：
- `_parse_job51_response`（纯函数，覆盖 status/1 成功、非 1 失败、HTTP 错误）
- `SearchRecommendJobs.search()`（正常、HTTP 错误）
- `GetJobDetail.fetch()`（成功，异常捕获）
- `GetCompanyDetail.fetch()`（成功）
- `Job51Client._job51_headers()`（sign 注入）

### 3.2 zhilian 测试（tests/zhilian/test_zhilian_client.py）

智联的请求走 POST（cgate）或 GET（capi），mock 方式相同：
- `SearchPositions.search()`（正常、错误）
- `GetPositionDetail.fetch()`（成功）
- `SearchCompanyPositions.search()`（成功，特别验证 sign_params 被调用）
- `ZhilianClient.post/get`（验证签名头注入）

---

## 4. 成功标准验证

| 标准 | 验证方式 |
|------|---------|
| job51 继承 BaseFetcher/BaseSearcher | `issubclass()` 断言 |
| zhilian 继承 BaseFetcher/BaseSearcher | `issubclass()` 断言 |
| 两平台无内联签名或 HTTP 样板 | `grep` 无 requests import，无 hmac 在 client/api 中 |
| mock 测试通过 | `pytest tests/job51/ tests/zhilian/` |
| 三平台代码结构一致 | 代码审查：client/api/sign/main 四文件结构 |

---

## RESEARCH COMPLETE

**Phase 3 可以规划，分 2 个 PLAN：**
- **Plan 01**：迁移前程无忧（job51）层 + mock 测试
- **Plan 02**：迁移智联招聘（zhilian）层 + mock 测试

两个 Plan 无依赖关系，理论上可并行，但顺序执行更稳妥。
