Files
campus_spider/doc/校招鸭.md
T
2026-07-24 15:50:07 +08:00

336 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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#7CryptoJS 兼容) |
| Key | `version 2.2.1bak`UTF-816 字节) |
| IV | `version 2.2.1bak`UTF-816 字节,与 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` 参数覆盖默认值。