初始化项目
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
.venv/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.history/
|
||||||
|
.fleet/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.swp
|
||||||
|
.swo
|
||||||
|
*.iml
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# offerqingbaoju.cn 招聘数据接口说明
|
||||||
|
|
||||||
|
> 基础域名:`https://offerqingbaoju.cn`
|
||||||
|
> 当前接口返回明文 JSON,不使用 AES 或 Base64 加密。
|
||||||
|
> 匿名访问只允许读取第 1 页;访问第 2 页及以后会返回 `401`,提示需要登录验证。
|
||||||
|
|
||||||
|
## 一、接口
|
||||||
|
|
||||||
|
### 招聘数据
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/simple/navigation/60/data
|
||||||
|
```
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://offerqingbaoju.cn/api/simple/navigation/60/data?page=1&per_page=2000'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、分页限制
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `page` | int | 页码,从 `1` 开始;匿名用户只能访问 `page=1` |
|
||||||
|
| `per_page` | int | 每页数量;参数名是 `per_page`,不是 `size` 或 `siez` |
|
||||||
|
|
||||||
|
测试结果:
|
||||||
|
|
||||||
|
- `page=1&per_page=10`:返回 10 条。
|
||||||
|
- `page=1&per_page=200`:返回 200 条。
|
||||||
|
- `page=1&per_page=2000`:返回 2000 条。
|
||||||
|
- `page=1&per_page=6000`:返回 6000 条,`total_pages=2`。
|
||||||
|
- `page=2&per_page=2000`:返回 `401`,需要登录验证。
|
||||||
|
- `page=1&siez=6000`:`siez` 被忽略,使用默认 `per_page=20`。
|
||||||
|
|
||||||
|
接口返回的分页字段位于 `pagination`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"per_page": 2000,
|
||||||
|
"total_pages": 4,
|
||||||
|
"total_rows": 6191,
|
||||||
|
"has_next": true,
|
||||||
|
"has_prev": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、字段编码格式
|
||||||
|
|
||||||
|
- 响应头为 `application/json`,内容编码为 UTF-8。
|
||||||
|
- 字段名使用中文 Unicode,例如 `企业名称`、`工作地点`。
|
||||||
|
- 原始响应中可能显示为 `\u4f01\u4e1a\u540d\u79f0`,这是 JSON Unicode 转义,不是加密;调用 `response.json()` 后会自动还原为 `企业名称`。
|
||||||
|
- 字段值是普通字符串、数字、数组或 `null`,不需要额外解码。
|
||||||
|
- `公告链接`、`投递地址` 是 URL 字符串;其中的 query 参数按 URL 编码规则处理。
|
||||||
|
|
||||||
|
## 四、返回结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"_row_number": 1,
|
||||||
|
"企业名称": "拼多多集团",
|
||||||
|
"企业性质": "民企",
|
||||||
|
"公告链接": "https://careers.pddglobalhr.com/campus/grad?t=XM8nweKyRe",
|
||||||
|
"学历要求": "详见公告",
|
||||||
|
"工作地点": "上海",
|
||||||
|
"开始时间": "2026-07-21",
|
||||||
|
"截止时间": "招满为止",
|
||||||
|
"投递地址": "https://careers.pddglobalhr.com/campus/grad?t=XM8nweKyRe",
|
||||||
|
"招聘公告": "拼多多集团",
|
||||||
|
"招聘批次": "秋招提前批",
|
||||||
|
"更新时间": "2026-07-21",
|
||||||
|
"毕业年份": "2027",
|
||||||
|
"职位": "商家运营服务端研发工程师...",
|
||||||
|
"行业": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {},
|
||||||
|
"file_info": {},
|
||||||
|
"headers": [],
|
||||||
|
"navigation": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 读取示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://offerqingbaoju.cn/api/simple/navigation/60/data",
|
||||||
|
params={"page": 1, "per_page": 2000},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
rows = payload["data"]
|
||||||
|
```
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
# offershow.cn 招聘接口说明
|
||||||
|
|
||||||
|
> 基础域名:`https://www.offershow.cn`
|
||||||
|
> 招聘业务接口响应体结构统一为 `{ "code": <int>, "data": <object>, "msg": <string> }`;业务成功码为 **`200001`**。
|
||||||
|
> 与 xiaozhaoya 不同,**offershow 的 `data` 为明文 JSON,无加密**,拿到即可直接解析。
|
||||||
|
> 每次请求服务端都会下发一枚匿名访客 Cookie `_uvc_`(`HttpOnly`,有效期约 4 小时),用于访客追踪/风控;当前所有接口**裸调(不带 Cookie / 登录态)即可返回数据**。
|
||||||
|
|
||||||
|
## 一、接口总览
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 功能 | 是否需要授权 |
|
||||||
|
|---|------|------|------|-------------|
|
||||||
|
| 1 | `/json/category.json` | GET | **职位分类字典**:6 位数编码的两级职位分类树 | 否(静态文件,走 CDN 缓存) |
|
||||||
|
| 2 | `/api/od/get_company_tags` | GET | **公司行业标签字典**:行业标签 id / 名称 / 关键词 | 否 |
|
||||||
|
| 3 | `/api/od/get_hot_city` | GET | **热门城市列表**:筛选用城市名数组 | 否 |
|
||||||
|
| 4 | `/api/od/plan_table` | **POST(JSON body)** | **招聘计划列表**:分页返回招聘计划概要(引用上面 2、3 的字典) | **部分**:未登录时 `page`/`size` 及大部分筛选被忽略,恒返回固定 10 条预览 |
|
||||||
|
|
||||||
|
### 重要说明
|
||||||
|
|
||||||
|
- **接口 4 必须用 POST + JSON body**:以 GET 或把参数放 query string 调用一律返回 `404 page not found`(`text/plain`)。参数必须写在请求体 JSON 中。
|
||||||
|
- **未登录的分页限制**:未登录状态下 `plan_table` 的 `page`、`size` 参数均被忽略——无论传 `page=1` 还是 `page=5`、`size=20` 还是 `size=100`,都只返回**固定的第 1 页 10 条**。翻页与自定义每页条数需要登录态。
|
||||||
|
- **未登录的筛选限制**:仅 `city`(城市)筛选生效(会同时改变 `total` 与返回列表);`company_many_tags`(行业)、`search_content`(关键词)等筛选**在未登录时被忽略**(`total` 与结果均不变)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、请求示例、参数与返回值
|
||||||
|
|
||||||
|
### 接口 1:职位分类字典 `GET /json/category.json`
|
||||||
|
|
||||||
|
静态 JSON 文件,无参数,命中腾讯云 CDN 缓存(`x-cache-lookup: Cache Hit`)。
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://www.offershow.cn/json/category.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(明文)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"value": 100000,
|
||||||
|
"label": "技术/开发",
|
||||||
|
"children": [
|
||||||
|
{ "value": 101000, "label": "后端开发" },
|
||||||
|
{ "value": 101700, "label": "前端开发" },
|
||||||
|
{ "value": 102000, "label": "人工智能/算法" }
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// ... 共 16 个一级分类
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编码规律**
|
||||||
|
|
||||||
|
- 一级分类:`100000`(技术/开发)、`110000`(产品)、`120000`(设计/交互)…… 按万位递增。
|
||||||
|
- 二级分类:一级基数 + 千位递增,如技术/开发(`100000`)下的 `101000` 后端开发、`101700` 前端开发。
|
||||||
|
- 一级分类共 16 个:技术/开发、产品、设计/交互、运营、市场/采购、人事/财务/行政、销售/客服、传媒、金融、教育/科研/培训、医疗健康、咨询/翻译/法律、服务业、生产制造、房地产/建筑、其他。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 接口 2:公司行业标签字典 `GET /api/od/get_company_tags`
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://www.offershow.cn/api/od/get_company_tags'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(明文)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200001,
|
||||||
|
"data": {
|
||||||
|
"company_tags": [
|
||||||
|
{ "id": 4, "content": "IT/互联网", "descriptions": "IT|互联网" },
|
||||||
|
{ "id": 19, "content": "游戏", "descriptions": "游戏" },
|
||||||
|
{ "id": 5, "content": "硬件/半导体", "descriptions": "电子|通信|硬件|半导体" }
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"msg": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `id` | 行业标签 id,被 `plan_table` 的 `company_many_tags`、`company.industry_type` 引用 |
|
||||||
|
| `content` | 展示名称 |
|
||||||
|
| `descriptions` | 该标签涵盖的关键词,`|` 分隔 |
|
||||||
|
|
||||||
|
**完整标签表**
|
||||||
|
|
||||||
|
| id | content | descriptions |
|
||||||
|
|----|---------|-------------|
|
||||||
|
| 4 | IT/互联网 | IT\|互联网 |
|
||||||
|
| 19 | 游戏 | 游戏 |
|
||||||
|
| 5 | 硬件/半导体 | 电子\|通信\|硬件\|半导体 |
|
||||||
|
| 7 | 汽车/自动驾驶 | 新能源\|汽车\|自动驾驶 |
|
||||||
|
| 13 | 机械/制造业 | 生产\|制造\|机械 |
|
||||||
|
| 6 | 金融行业 | 金融\|保险\|证券\|投资 |
|
||||||
|
| 12 | 消费生活 | 消费\|生活\|服务\|娱乐 |
|
||||||
|
| 17 | 医疗健康 | 医疗\|健康 |
|
||||||
|
| 14 | 政府/事业单位 | 科研\|政府\|公共事业 |
|
||||||
|
| 8 | 国企央企 | 国企\|央企\|研究所 |
|
||||||
|
| 9 | 广告传媒 | 传媒\|印刷\|艺术\|设计 |
|
||||||
|
| 10 | 建筑/房地产 | 房地产\|建筑\|物业 |
|
||||||
|
| 18 | 材料/能源/化工 | 能源\|化工\|环保 |
|
||||||
|
| 16 | 物流/交通运输 | 采购\|贸易\|交通\|物流 |
|
||||||
|
| 15 | 其他行业 | 其他行业 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 接口 3:热门城市 `GET /api/od/get_hot_city`
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://www.offershow.cn/api/od/get_hot_city'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(明文)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200001,
|
||||||
|
"data": {
|
||||||
|
"city_list": ["全部", "北京", "上海", "广州", "深圳", "杭州", "成都", "武汉",
|
||||||
|
"苏州", "南京", "天津", "郑州", "合肥", "长沙", "济南", "太原", "青岛",
|
||||||
|
"石家庄", "西安", "重庆", "厦门", "宁波", "福州", "昆明", "南昌", "佛山", "东莞"]
|
||||||
|
},
|
||||||
|
"msg": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 首项为占位项 `"全部"`,其余为可用于 `plan_table` 请求体 `city` 参数的城市名。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 接口 4:招聘计划列表 `POST /api/od/plan_table`
|
||||||
|
|
||||||
|
**请求体(JSON)**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `page` | int | 是 | 页码,从 1 开始。**未登录时被忽略**(恒为第 1 页) |
|
||||||
|
| `size` | int | 是 | 每页条数。**未登录时被忽略**(恒返回 10 条) |
|
||||||
|
| `recruit_plan_type` | int | 否 | 招聘计划类型,如 `2`。缺省/传 0 不影响未登录返回 |
|
||||||
|
| `city` | string | 否 | 城市名(取自接口 3)。**未登录时生效**,会改变 `total` 与返回列表 |
|
||||||
|
| `search_content` | string | 否 | 关键词搜索。**未登录时被忽略** |
|
||||||
|
| `company_many_tags` | string | 否 | 行业标签 id(逗号分隔,取自接口 2)。**未登录时被忽略** |
|
||||||
|
| `recruit_type` | int | 否 | 招聘类型(0=不限) |
|
||||||
|
| `company_character` | int | 否 | 公司性质(0=不限) |
|
||||||
|
| `progress_status` | int | 否 | 进度状态(0=不限) |
|
||||||
|
| `is_recommend` | int | 否 | 是否推荐(0=不限) |
|
||||||
|
| `object_id` / `column_type` / `title` / `total` | - | 否 | 前端上下文透传字段,不影响未登录查询结果 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://www.offershow.cn/api/od/plan_table' \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
--data '{"page":1,"size":20,"recruit_plan_type":2}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(明文)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200001,
|
||||||
|
"data": {
|
||||||
|
"total": 29954,
|
||||||
|
"is_login": false,
|
||||||
|
"is_recruit_vip": false,
|
||||||
|
"recruit_vip_num": 134,
|
||||||
|
"plans": [
|
||||||
|
{
|
||||||
|
"uuid": "b67d206c-b93d-4536-8005-6b10ac45eead",
|
||||||
|
"company_name": "网易游戏互娱",
|
||||||
|
"company_uuid": "管理员录入",
|
||||||
|
"company_logo": "/company_logo/2684d697-....jpg",
|
||||||
|
"company_many_tags": "4,19",
|
||||||
|
"company": {
|
||||||
|
"uuid": "", "fullname": "", "nickname": "", "logo": "",
|
||||||
|
"address": "", "intro": "", "website_link": "",
|
||||||
|
"industry_type": 0, "character": 0, "finance": 0, "scale": 0
|
||||||
|
},
|
||||||
|
"recruit_title": "网易游戏(互娱)2027届校园招聘正式启动!",
|
||||||
|
"recruit_city": "广州 | 杭州 | 上海",
|
||||||
|
"positions": "游戏策划类\r\n技术类\r\n用户体验及设计类\r\n产品类\r\n...",
|
||||||
|
"recruit_type": 3,
|
||||||
|
"plan_type": 2,
|
||||||
|
"recruit_plan_type": 1,
|
||||||
|
"graduate_type": 1,
|
||||||
|
"deliver_type": 1,
|
||||||
|
"time_type": 2,
|
||||||
|
"is_official": 2,
|
||||||
|
"is_recommend": 1,
|
||||||
|
"origin": "网易游戏互娱招聘公众号",
|
||||||
|
"notice_url": "https://mp.weixin.qq.com/s/....",
|
||||||
|
"recommend_code": " ZJwCjn",
|
||||||
|
"recommend_type": 1,
|
||||||
|
"view_cnt": 180,
|
||||||
|
"welfares": "[]",
|
||||||
|
"create_time": "2026-07-21T11:31:20+08:00",
|
||||||
|
"start_time": "2026-07-21T00:00:00+08:00",
|
||||||
|
"end_time": "2027-01-17T00:00:00+08:00",
|
||||||
|
"recruit_local_graduate_date_start": "2026-09-01T00:00:00+08:00",
|
||||||
|
"recruit_local_graduate_date_end": "2027-08-31T00:00:00+08:00",
|
||||||
|
"recruit_overseas_graduate_date_start": "2026-09-01T00:00:00+08:00",
|
||||||
|
"recruit_overseas_graduate_date_end": "2027-08-31T00:00:00+08:00"
|
||||||
|
}
|
||||||
|
// ... 未登录固定返回 10 条
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"msg": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`data` 顶层字段**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `total` | 符合查询条件的总条数(`city` 筛选会改变它;`company_many_tags`/`search_content` 未登录时不改变) |
|
||||||
|
| `plans` | 招聘计划数组,未登录固定 10 条 |
|
||||||
|
| `is_login` | 是否已登录(裸调为 `false`) |
|
||||||
|
| `is_recruit_vip` | 是否招聘 VIP |
|
||||||
|
| `recruit_vip_num` | VIP 招聘数量 |
|
||||||
|
|
||||||
|
**单条 plan 关键字段**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `uuid` | 招聘计划唯一标识 |
|
||||||
|
| `company_name` | 公司名 |
|
||||||
|
| `company_many_tags` | 行业标签 id 列表(逗号分隔),查表见接口 2 |
|
||||||
|
| `company` | 公司详情对象,未登录时多为空值 |
|
||||||
|
| `recruit_title` | 招聘标题 |
|
||||||
|
| `recruit_city` | 招聘城市,` | ` 分隔的自由文本 |
|
||||||
|
| `positions` | 招聘职位,`\r\n` 分隔的**自由文本**(非接口 1 的编码) |
|
||||||
|
| `notice_url` | 招聘公告原文链接(常为微信公众号推文) |
|
||||||
|
| `recruit_type` / `plan_type` / `graduate_type` / `deliver_type` / `time_type` / `is_official` / `is_recommend` | 内部小枚举,无独立字典接口,需前端硬编码映射 |
|
||||||
|
| `view_cnt` | 浏览量 |
|
||||||
|
| `create_time` / `start_time` / `end_time` | 时间字段(RFC3339,含 +08:00 时区) |
|
||||||
|
| `recruit_local_graduate_date_*` / `recruit_overseas_graduate_date_*` | 境内/境外毕业时间窗口 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、字段字典与关联关系
|
||||||
|
|
||||||
|
三个"字典接口"(1、2、3)为主数据接口 `plan_table`(4)提供编码含义与筛选选项。
|
||||||
|
|
||||||
|
### 1. `get_company_tags.id` ⟷ `plan_table.company_many_tags`(强关联 / 外键)
|
||||||
|
|
||||||
|
- `plan_table` 中 `company_many_tags: "4,19"` 是**逗号分隔的行业标签 id 列表**。
|
||||||
|
- 查表:`4` = IT/互联网,`19` = 游戏 ⇒ 该公司行业为【IT/互联网 + 游戏】。
|
||||||
|
- 同一字典也对应 `plan_table` 请求体的行业筛选参数 `company_many_tags`(登录后生效),以及 `plan.company.industry_type`(单值主行业)。
|
||||||
|
|
||||||
|
### 2. `get_hot_city.city_list` ⟷ `plan_table` 请求体 `city`(筛选入参)
|
||||||
|
|
||||||
|
- `city_list` 提供可选城市名,作为 `plan_table` 的 `city` 入参使用;`city` 是未登录唯一生效的筛选条件。
|
||||||
|
- 注意返回里的 `recruit_city` 是 ` | ` 分隔的自由文本,可含 `city_list` 之外的地名(如"香港/澳门/海外")。
|
||||||
|
|
||||||
|
### 3. `category.json` ⟷ 职位筛选(弱关联)
|
||||||
|
|
||||||
|
- `category.json` 是职位分类编码表,用于按职位维度的筛选/搜索(前端级联下拉)。
|
||||||
|
- **注意**:`plan_table.positions` 是招聘方录入的自由文本(`\r\n` 分隔),**不回填 category 的 `value`**,两者仅语义相关、无 id 级外键。
|
||||||
|
|
||||||
|
### 关系图
|
||||||
|
|
||||||
|
```
|
||||||
|
get_company_tags.id ──(逗号分隔多值)──► plan_table.company_many_tags [强关联/外键]
|
||||||
|
get_company_tags.id ──(单值主行业)────► plan_table.company.industry_type [强关联/外键]
|
||||||
|
|
||||||
|
get_hot_city.city_list ──(筛选入参)───► plan_table 请求体 city [筛选条件, 未登录唯一生效]
|
||||||
|
|
||||||
|
category.json.value ──(筛选入参)──────► plan_table 职位筛选参数(登录后) [查询条件]
|
||||||
|
category.json.label ~~(语义对应,无id)~ plan_table.positions(自由文本) [弱关联]
|
||||||
|
|
||||||
|
plan_table 独立枚举: recruit_type / plan_type / graduate_type / ... [无字典接口, 需前端硬编码]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、反爬与鉴权说明
|
||||||
|
|
||||||
|
- **访客 Cookie `_uvc_`**:每次请求服务端 `Set-Cookie` 下发,`HttpOnly`、`Domain=www.offershow.cn`、`Max-Age≈15400s`(约 4 小时)。用于匿名访客追踪,当前不影响裸调取数。
|
||||||
|
- **业务码**:成功统一为 `code: 200001`(非 HTTP 200 语义)。
|
||||||
|
- **路由约束**:`plan_table` 仅接受 POST + JSON body,误用 GET 返回 `404 page not found`(`text/plain`)。
|
||||||
|
- **登录门槛**:翻页(`page`≥2)、自定义 `size`、以及 `company_many_tags`/`search_content` 等筛选均需登录态才生效;未登录只能取到第 1 页固定 10 条、且仅 `city` 筛选可用。
|
||||||
|
- **数据形态**:响应为明文 JSON,无 xiaozhaoya 那样的 AES 加密,无需解密步骤。
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 招聘公告数据表设计
|
||||||
|
-- 说明:分析对象为「招聘公告」,多值属性(招聘届数、批次、标签组、
|
||||||
|
-- 热招城市、岗位大类、学历要求)直接拆关联表存原始值,
|
||||||
|
-- 不引入任何字典表,避免归一化对公告解析结果的限制。
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 招聘公告主表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`company_id` BIGINT NULL COMMENT '关联企业主表id',
|
||||||
|
`company_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '公司名称',
|
||||||
|
`title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '公告标题',
|
||||||
|
`company_intro` TEXT NULL COMMENT '公司简介',
|
||||||
|
`target_audience` VARCHAR(500) NULL COMMENT '面向对象,如2027届本硕博',
|
||||||
|
`major_require` VARCHAR(500) NULL COMMENT '专业要求',
|
||||||
|
`remark` VARCHAR(1000) NULL COMMENT '备注',
|
||||||
|
`written_exam` VARCHAR(16) NULL COMMENT '是否笔试:有 / 无 / 未明确',
|
||||||
|
`apply_start_time` DATETIME NULL COMMENT '投递开始时间',
|
||||||
|
`apply_end_time` DATETIME NULL COMMENT '投递结束时间',
|
||||||
|
`apply_end_desc` VARCHAR(64) NULL COMMENT '投递截止描述,如"尽快投递"',
|
||||||
|
`invite_code` VARCHAR(64) NULL COMMENT '专属内推码',
|
||||||
|
`announcement_url` VARCHAR(512) NULL COMMENT '公告地址',
|
||||||
|
`apply_url` VARCHAR(512) NULL COMMENT '投递地址',
|
||||||
|
`apply_email` VARCHAR(128) NULL COMMENT '投递邮箱',
|
||||||
|
`source` VARCHAR(255) NULL COMMENT '信息来源说明',
|
||||||
|
`publish_time` DATETIME NULL COMMENT '公告发布/更新时间',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_company_id` (`company_id`),
|
||||||
|
KEY `idx_company_name` (`company_name`),
|
||||||
|
KEY `idx_written_exam` (`written_exam`),
|
||||||
|
KEY `idx_apply_start_time` (`apply_start_time`),
|
||||||
|
KEY `idx_announcement_url` (`announcement_url`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告主表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 招聘届数关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_year` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`recruit_year` SMALLINT NOT NULL COMMENT '招聘届数,如2027',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_recruit_year` (`recruit_year`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-招聘届数关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 批次关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_batch` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`batch_name` VARCHAR(64) NOT NULL COMMENT '批次值,如实习/暑期实习/寒假实习/秋招专场/春招提前批/春招补招',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_batch_name` (`batch_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-批次关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 标签组关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_tag` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`tag_name` VARCHAR(64) NOT NULL COMMENT '标签值,如秋招提前批/竞争力薪酬/校招',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_tag_name` (`tag_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-标签组关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 热招城市关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_city` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`city_name` VARCHAR(64) NOT NULL COMMENT '城市值,如北京市/天津市',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_city_name` (`city_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-热招城市关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 岗位大类关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_category` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`category_name` VARCHAR(64) NOT NULL COMMENT '岗位大类值,如IT技术/人工智能/通信',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_category_name` (`category_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-岗位大类关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 学历要求关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_education` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`education_name` VARCHAR(32) NOT NULL COMMENT '学历值,如本科/硕士/博士',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_education_name` (`education_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-学历要求关联表';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
cryptography>=41
|
||||||
|
rapidocr>=3.9
|
||||||
|
onnxruntime>=1.17
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
"""OCR 工具
|
||||||
|
|
||||||
|
基于 RapidOCR(ONNXRuntime 后端)的图片文字提取工具。
|
||||||
|
模块级持有单例引擎,导入时完成初始化,后续直接复用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rapidocr import RapidOCR
|
||||||
|
|
||||||
|
# 模块级单例:导入时初始化一次,全局复用
|
||||||
|
# 初始化配置(RapidOCR 3.x 用 params 字典,key 为配置项点路径):
|
||||||
|
# - 关闭方向分类(Cls):公告图基本都是正的,省一道计算
|
||||||
|
# - 其余检测/识别/后端配置沿用 RapidOCR 默认值,避免和新版枚举参数冲突
|
||||||
|
_engine = RapidOCR(
|
||||||
|
params={
|
||||||
|
"Global.use_cls": False,
|
||||||
|
"Global.text_score": 0.5,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ocr(image: bytes) -> str:
|
||||||
|
"""识别图片中的文字。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 图片的字节数据(爬虫下载下来的图片流,无需落盘)。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别出的文字,多段以换行拼接为一段文本;没有文字或识别失败时返回空字符串。
|
||||||
|
"""
|
||||||
|
result = _engine(image)
|
||||||
|
|
||||||
|
# .txts 是识别出的多段文字(元组),没识别到时可能为 None 或空
|
||||||
|
if not result or not result.txts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return "\n".join(result.txts)
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
# xiaozhaoya.com 招聘接口说明
|
||||||
|
|
||||||
|
> 基础域名:`https://api.xiaozhaoya.com`
|
||||||
|
> 所有接口的响应体结构统一为 `{ "code": <int>, "message": <string>, "data": <string|null> }`。
|
||||||
|
> `data` 字段在成功时是一段 **AES 加密后再 Base64 编码的密文**,需解密后才能得到明文 JSON(解密方案与完整代码见文末第三节)。
|
||||||
|
|
||||||
|
## 一、接口总览
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 功能 | 是否需要授权 |
|
||||||
|
|---|------|------|------|-------------|
|
||||||
|
| 1 | `/api/recruitment/list` | GET | 招聘列表:分页返回岗位概要(含公司、行业、城市、投递方式等) | **部分**:未登录只能访问第 1 页;翻页(page ≥ 2)返回 401 |
|
||||||
|
| 2 | `/api/recruitment/detail?id={id}` | GET | 招聘详情:返回单条岗位的完整信息(含岗位职责、工作地址、薪资等) | **否**:可裸调。仅"招聘公告地址、投递方式"两个字段脱敏为"登陆后可查看" |
|
||||||
|
| 3 | `/api/recruitment/{id}/getUrl` | POST | 获取该岗位的**招聘公告原文地址**(`announcementLink`) | **是**:必须携带有效登录态,否则返回 401「请先登录」 |
|
||||||
|
|
||||||
|
### 关于 id 的重要说明
|
||||||
|
|
||||||
|
- **`id` 是自增 ID**:`recruitmentId` 随发布顺序连续递增(例如 41785、41786 … 41796),越大越新。可据此推断数据总量与遍历范围。
|
||||||
|
- **详情接口查询不存在的 id**:`/api/recruitment/detail?id={不存在的id}` 时,服务端返回 `code:500, message:"ok"`,且 **`data` 为 `null`**(没有可解密内容)。调用方应先判断 `data` 是否为 null,再进行解密。
|
||||||
|
- 特例:`id` 为负数时会返回 `401 请登录后继续浏览`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、请求示例、参数与返回值
|
||||||
|
|
||||||
|
### 接口 1:招聘列表 `GET /api/recruitment/list`
|
||||||
|
|
||||||
|
**请求参数(Query)**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `page` | int | 是 | 页码,从 1 开始。未登录时仅 `page=1` 可用 |
|
||||||
|
| `size` | int | 是 | 每页条数。**未登录时服务端强制为 12**,传更大值(如 200)也只返回 12 条 |
|
||||||
|
| `sortRule` | string | 否 | 排序规则,如 `updateDate`(按更新时间倒序)。为默认值,去掉不影响结果 |
|
||||||
|
| `userId` | string | 否 | 登录用户 ID。登录态下随请求携带 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://api.xiaozhaoya.com/api/recruitment/list?page=1&size=12&sortRule=updateDate' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(`data` 解密后)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": "33872",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"recruitmentId": 41796,
|
||||||
|
"updateDate": "2026-07-21",
|
||||||
|
"companyName": "同花顺-AIME顶尖人才计划",
|
||||||
|
"companyTypeName": "民企",
|
||||||
|
"fullName": "浙江核新同花顺网络信息股份有限公司",
|
||||||
|
"shortName": "同花顺",
|
||||||
|
"logoUrl": "https://xiaozhaoya.oss-cn-shanghai.aliyuncs.com/company/logo/....jpg",
|
||||||
|
"batchNameList": "秋招专场",
|
||||||
|
"industryName": "IT/互联网/游戏",
|
||||||
|
"jobCategoryNameList": "设计, 人工智能, 工程师",
|
||||||
|
"jobTitle": "多模态算法工程师;大模型算法工程师;...",
|
||||||
|
"educationLevelNameList": "硕士, 博士",
|
||||||
|
"graduationYearList": "2026, 2027",
|
||||||
|
"majorRequirements": "未明确",
|
||||||
|
"cityNameList": "杭州市",
|
||||||
|
"applicationDeadline": "尽快投递",
|
||||||
|
"sourceName": "offer雷达",
|
||||||
|
"announcementTitle": "同花顺AIME顶尖人才计划|全球招募",
|
||||||
|
"announcementLink": "/",
|
||||||
|
"applicationMethod": "https://campus.10jqka.com.cn/job/list?type=school",
|
||||||
|
"hasWrittenTest": "未明确",
|
||||||
|
"companyProfile": "国内领先的互联网金融信息服务商,...",
|
||||||
|
"isShow": true
|
||||||
|
}
|
||||||
|
// ... 共 12 条;jobContent、workAddress、salary、viewAmount 等字段在列表中为 null
|
||||||
|
],
|
||||||
|
"isMembershipValid": false,
|
||||||
|
"isLoggedIn": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**
|
||||||
|
|
||||||
|
- 列表中的 `applicationMethod`(投递方式)为**明文真实链接**;`announcementLink` 为 `/`(占位)。
|
||||||
|
- `jobContent`、`workAddress`、`salary`、`viewAmount`、`likeAmount`、`recruitmentCount` 等字段在列表接口中均为 `null`,需调用详情接口获取。
|
||||||
|
- 翻页未授权时的返回:`{ "code": 401, "message": "请登录后查看更多信息", "data": null }`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 接口 2:招聘详情 `GET /api/recruitment/detail`
|
||||||
|
|
||||||
|
**请求参数(Query)**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `id` | int | 是 | 招聘 ID(自增)。不存在时 `data` 返回 null |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://api.xiaozhaoya.com/api/recruitment/detail?id=41794' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值(`data` 解密后)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"recruitmentId": 41794,
|
||||||
|
"updateDate": "2026-07-21",
|
||||||
|
"companyName": "网易游戏-互娱",
|
||||||
|
"companyTypeName": "民企",
|
||||||
|
"logoUrl": "https://xiaozhaoya.oss-cn-shanghai.aliyuncs.com/company/logo/....jpg",
|
||||||
|
"batchNameList": "秋招专场",
|
||||||
|
"industryName": "IT/互联网/游戏",
|
||||||
|
"jobCategoryNameList": "运营, 设计, IT技术, 人工智能, ...",
|
||||||
|
"jobTitle": "游戏策划类;游戏程序类;游戏艺术类;...",
|
||||||
|
"educationLevelNameList": "本科, 硕士, 博士",
|
||||||
|
"graduationYearList": "2027",
|
||||||
|
"cityNameList": "上海市, 杭州市, 广州市",
|
||||||
|
"announcementDate": "2026-07-21",
|
||||||
|
"applicationDeadline": "尽快投递",
|
||||||
|
"sourceName": "网易游戏互娱招聘",
|
||||||
|
"announcementTitle": "网易游戏(互娱)2027届校园招聘正式启动!",
|
||||||
|
"announcementLink": "登陆后可查看",
|
||||||
|
"applicationMethod": "登陆后可查看",
|
||||||
|
"hasWrittenTest": "有笔试",
|
||||||
|
"companyProfile": "网易游戏(互娱)是国内领先的游戏厂商,...",
|
||||||
|
"tip": "每位同学最多投递2个岗位,表现优秀可转正",
|
||||||
|
"tags": "大厂校招,多岗热招,福利丰厚",
|
||||||
|
"viewAmount": "10",
|
||||||
|
"likeAmount": "1",
|
||||||
|
"recruitmentCount": "若干",
|
||||||
|
"jobContent": "#### 游戏策划类\n- 负责游戏玩法规则设计...(Markdown 格式的完整岗位职责)",
|
||||||
|
"workAddress": "广州市天河区思成路1号,杭州市滨江区网商路399号,上海市静安区江场路1228弄",
|
||||||
|
"salary": "面议",
|
||||||
|
"isMembershipValid": false,
|
||||||
|
"isLoggedIn": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**
|
||||||
|
|
||||||
|
- 详情接口未登录也可获取几乎全部字段(岗位职责、工作地址、薪资等)。
|
||||||
|
- 仅 `announcementLink`(招聘公告地址)与 `applicationMethod`(投递方式)两个字段被脱敏为 `"登陆后可查看"`。
|
||||||
|
- **不存在的 id** 的返回:`{ "code": 500, "message": "ok", "data": null }`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 接口 3:获取招聘公告地址 `POST /api/recruitment/{id}/getUrl`
|
||||||
|
|
||||||
|
功能:返回该岗位的**招聘公告原文地址**(即详情中被脱敏的 `announcementLink`)。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `id` | int | 是 | 招聘 ID,写在 URL 路径中 |
|
||||||
|
|
||||||
|
**请求体**:空对象 `{}`(`Content-Type: application/json`)。
|
||||||
|
|
||||||
|
**请求示例(需登录态)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://api.xiaozhaoya.com/api/recruitment/41794/getUrl' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-H 'authorization: <有效的登录 token>' \
|
||||||
|
-H 'origin: https://www.xiaozhaoya.com' \
|
||||||
|
-H 'referer: https://www.xiaozhaoya.com/' \
|
||||||
|
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36' \
|
||||||
|
--data '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值**
|
||||||
|
|
||||||
|
- 未授权(无有效登录态):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": 401, "message": "请先登录", "data": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
- 授权成功:`data` 为加密字符串,解密后为招聘公告地址(结构以实际返回为准)。
|
||||||
|
|
||||||
|
**授权说明**:登录 token 通过 `authorization` 请求头传递(站点使用 Sa-Token 框架,响应头 `access-control-expose-headers: satoken`)。token 存在有效期,失效后即便请求头格式正确,服务端仍判定为未登录(列表接口返回中 `isLoggedIn` 为 `false`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、`data` 字段解密方法
|
||||||
|
|
||||||
|
站点对响应体的 `data` 字段做了对称加密,须解密后才能得到明文 JSON。
|
||||||
|
|
||||||
|
**加密方案**
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 算法 | AES-128 |
|
||||||
|
| 模式 | CBC |
|
||||||
|
| 填充 | PKCS#7(CryptoJS 兼容) |
|
||||||
|
| Key | `version 2.2.1bak`(UTF-8,16 字节) |
|
||||||
|
| IV | `version 2.2.1bak`(UTF-8,16 字节,与 Key 相同) |
|
||||||
|
| 传输编码 | Base64 |
|
||||||
|
|
||||||
|
**解密流程**:Base64 解码 → AES-128-CBC 解密 → 去除 PKCS#7 填充 → UTF-8 解码 → JSON 解析。
|
||||||
|
|
||||||
|
**依赖**
|
||||||
|
|
||||||
|
```
|
||||||
|
cryptography>=41
|
||||||
|
```
|
||||||
|
|
||||||
|
**解密代码(完整源码)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Decode the encrypted payload returned by xiaozhaoya.com APIs.
|
||||||
|
|
||||||
|
The site currently uses AES-128-CBC with PKCS#7 padding. The key and IV are
|
||||||
|
part of the public frontend bundle, so keep them configurable in case the site
|
||||||
|
rotates them later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_KEY = "version 2.2.1bak"
|
||||||
|
DEFAULT_IV = "version 2.2.1bak"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_ciphertext(
|
||||||
|
ciphertext: str,
|
||||||
|
*,
|
||||||
|
key: str = DEFAULT_KEY,
|
||||||
|
iv: str = DEFAULT_IV,
|
||||||
|
parse_json: bool = True,
|
||||||
|
) -> Any:
|
||||||
|
"""Decrypt one Base64 payload and optionally parse its UTF-8 JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ciphertext: The API's Base64-encoded ``data`` value.
|
||||||
|
key: AES key encoded as UTF-8 (16 bytes by default).
|
||||||
|
iv: CBC initialization vector encoded as UTF-8 (16 bytes by default).
|
||||||
|
parse_json: Return the decoded JSON value when true; otherwise return
|
||||||
|
the decoded UTF-8 string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(ciphertext, str) or not ciphertext.strip():
|
||||||
|
raise ValueError("ciphertext must be a non-empty Base64 string")
|
||||||
|
|
||||||
|
key_bytes = key.encode("utf-8")
|
||||||
|
iv_bytes = iv.encode("utf-8")
|
||||||
|
if len(key_bytes) not in (16, 24, 32):
|
||||||
|
raise ValueError("AES key must be 16, 24, or 32 UTF-8 bytes")
|
||||||
|
if len(iv_bytes) != 16:
|
||||||
|
raise ValueError("AES-CBC IV must be 16 UTF-8 bytes")
|
||||||
|
|
||||||
|
try:
|
||||||
|
encrypted = base64.b64decode(ciphertext, validate=True)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("ciphertext is not valid Base64") from exc
|
||||||
|
|
||||||
|
if not encrypted or len(encrypted) % 16:
|
||||||
|
raise ValueError("decoded ciphertext length must be a non-zero AES block multiple")
|
||||||
|
|
||||||
|
decryptor = Cipher(
|
||||||
|
algorithms.AES(key_bytes),
|
||||||
|
modes.CBC(iv_bytes),
|
||||||
|
).decryptor()
|
||||||
|
padded = decryptor.update(encrypted) + decryptor.finalize()
|
||||||
|
|
||||||
|
# CryptoJS.pad.Pkcs7 compatibility.
|
||||||
|
padding_length = padded[-1]
|
||||||
|
if not 1 <= padding_length <= 16 or padded[-padding_length:] != bytes([padding_length]) * padding_length:
|
||||||
|
raise ValueError("invalid PKCS#7 padding")
|
||||||
|
plaintext = padded[:-padding_length].decode("utf-8")
|
||||||
|
return json.loads(plaintext) if parse_json else plaintext
|
||||||
|
|
||||||
|
|
||||||
|
def decode_api_data(
|
||||||
|
response: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
key: str = DEFAULT_KEY,
|
||||||
|
iv: str = DEFAULT_IV,
|
||||||
|
) -> Any:
|
||||||
|
"""Extract and decode an API response's encrypted ``data`` field."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
ciphertext = response["data"]
|
||||||
|
except (KeyError, TypeError) as exc:
|
||||||
|
raise ValueError("API response does not contain a data field") from exc
|
||||||
|
return decode_ciphertext(ciphertext, key=key, iv=iv)
|
||||||
|
```
|
||||||
|
|
||||||
|
上面提供两个函数:
|
||||||
|
|
||||||
|
- `decode_ciphertext(ciphertext, *, key=..., iv=..., parse_json=True)`:解密单个 Base64 密文字符串。
|
||||||
|
- `decode_api_data(response, *, key=..., iv=...)`:从完整响应对象中取出 `data` 字段并解密。
|
||||||
|
|
||||||
|
**调用示例**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import gzip
|
||||||
|
# 将上面的解密函数保存为 decoder.py 后即可导入
|
||||||
|
from decoder import decode_api_data
|
||||||
|
|
||||||
|
url = "https://api.xiaozhaoya.com/api/recruitment/detail?id=41794"
|
||||||
|
headers = {
|
||||||
|
"accept": "application/json",
|
||||||
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=30)
|
||||||
|
raw = resp.read()
|
||||||
|
if resp.headers.get("content-encoding") == "gzip":
|
||||||
|
raw = gzip.decompress(raw)
|
||||||
|
|
||||||
|
payload = json.loads(raw.decode("utf-8"))
|
||||||
|
# 注意:先判断 data 是否为 null(如 id 不存在、未授权等场景)
|
||||||
|
if isinstance(payload.get("data"), str):
|
||||||
|
data = decode_api_data(payload)
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print("无数据:", payload.get("code"), payload.get("message"))
|
||||||
|
```
|
||||||
|
|
||||||
|
> 备注:Key/IV 硬编码在前端 JS 中且公开,此加密本质是轻量混淆/反爬,并非真正的机密性保护。若站点后续轮换密钥,`decode_ciphertext` 支持通过 `key`/`iv` 参数覆盖默认值。
|
||||||
Reference in New Issue
Block a user