Files
campus_spider/校招鸭/API接口说明.md
T
2026-07-24 15:15:01 +08:00

13 KiB
Raw Blame History

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 是自增 IDrecruitmentId 随发布顺序连续递增(例如 41785、41786 … 41796),越大越新。可据此推断数据总量与遍历范围。
  • 详情接口查询不存在的 id/api/recruitment/detail?id={不存在的id} 时,服务端返回 code:500, message:"ok",且 datanull(没有可解密内容)。调用方应先判断 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。登录态下随请求携带

请求示例

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 解密后)

{
  "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/(占位)。
  • jobContentworkAddresssalaryviewAmountlikeAmountrecruitmentCount 等字段在列表接口中均为 null,需调用详情接口获取。
  • 翻页未授权时的返回:{ "code": 401, "message": "请登录后查看更多信息", "data": null }

接口 2:招聘详情 GET /api/recruitment/detail

请求参数(Query

参数 类型 必填 说明
id int 招聘 ID(自增)。不存在时 data 返回 null

请求示例

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 解密后)

{
  "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)。

请求示例(需登录态)

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 '{}'

返回值

  • 未授权(无有效登录态):

    { "code": 401, "message": "请先登录", "data": null }
    
  • 授权成功:data 为加密字符串,解密后为招聘公告地址(结构以实际返回为准)。

授权说明:登录 token 通过 authorization 请求头传递(站点使用 Sa-Token 框架,响应头 access-control-expose-headers: satoken)。token 存在有效期,失效后即便请求头格式正确,服务端仍判定为未登录(列表接口返回中 isLoggedInfalse)。


三、data 字段解密方法

站点对响应体的 data 字段做了对称加密,须解密后才能得到明文 JSON。

加密方案

项目
算法 AES-128
模式 CBC
填充 PKCS#7CryptoJS 兼容)
Key version 2.2.1bakUTF-816 字节)
IV version 2.2.1bakUTF-816 字节,与 Key 相同)
传输编码 Base64

解密流程Base64 解码 → AES-128-CBC 解密 → 去除 PKCS#7 填充 → UTF-8 解码 → JSON 解析。

依赖

cryptography>=41

解密代码(完整源码)

"""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 字段并解密。

调用示例

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 参数覆盖默认值。