目录
- 一、分析
- 二、Python 实现
- 三、总结
免责声明:本文内容仅用于合法授权范围内的技术学习、安全研究、逆向分析方法交流与风控防护理解,不针对任何网站、产品或服务提供绕过、攻击、滥用或破坏性使用建议。文中涉及的接口分析、参数加解密、调试定位、代码复现、数据请求等内容,仅用于说明相关技术原理和分析流程。读者应在遵守相关法律法规、平台规则、robots 协议、用户协议以及获得合法授权的前提下进行学习和实验。请勿将本文中的方法、脚本或思路用于未授权访问、批量采集、账号撞库、绕过风控、破坏验证码体系、规避平台限制、侵犯数据权益、商业化滥用或影响线上系统稳定性的行为。对于真实网站案例,读者不应直接复制代码对线上服务进行高频请求或非授权调用。若相关网站、产品方、权利方或平台认为本文内容存在不适宜公开展示之处,可通过评论区、私信或作者主页提供的联系方式联系我;核实后将及时删除、替换或调整相关内容。读者因不当使用本文内容造成的任何法律责任、业务风险或经济损失,均由使用者自行承担,与作者无关。
一、分析
目标地址:
https://www.jinglingshuju.com/articles
本案例需要抓取精灵数据的资讯列表,循环采集前 3 页,提取每条新闻的标题、发布时间、新闻来源和详情页链接。
F12 打开 DevTools,切到 Network 面板,清空请求记录。列表页数据通常会通过 Ajax 动态加载,因此先过滤 Fetch/XHR,再通过翻页触发请求,定位到目标数据包:
目标接口是一个 POST 请求:
POST https://vapi.jinglingshuju.com/Data/getNewsList
多次翻页后对比 Form Data:
page=3&num=20
page=4&num=20
page=5&num=20
参数含义如下:
| page | 页码 | 翻页时变化的参数 |
| num | 每页条数 | 固定为 20 |
请求头中没有看到自定义签名、时间戳或加密参数。实际测试时,即使不携带 Cookie 也能请求成功,服务端对请求头校验不严格;保留 User-Agent 即可拿到响应。为了让请求更接近浏览器环境,正式脚本中仍建议保留 Referer、Origin、X-Requested-With 等常见头。
接口外层返回的是普通 JSON,但 data 字段是一段密文:
{
"code": 0,
"msg": "success",
"data": "Ou1c1U7XU3pQRu0GosMzTD85Gpd/…"
}
接下来需要定位前端在哪里把这段密文还原成业务数据。响应解密通常有两条定位思路:
本案例优先采用第二种方式。全局搜索 decrypt 后出现两处相关代码:
两处代码分别是:
// 第 1 处:Cookie 相关解密
return c.a.AES.decrypt(t, d.cookieKey).toString(c.a.enc.Utf8)
// 第 2 处:接口响应 data 解密
var e = y.a.AES.decrypt(data, z, {
iv: y.a.enc.Utf8.parse(j.substr(0, 16)),
mode: y.a.mode.ECB,
padding: y.a.pad.Pkcs7
});
第一处使用的是 cookieKey,更像是本地 Cookie 或用户信息解密。第二处变量名直接是 data,并且后面紧跟 JSON.parse,更符合接口响应解密的特征。为了确认判断,可以分别打断点,然后翻页触发 getNewsList 请求。
翻页后命中第二处断点,说明当前接口响应确实走到了这里:
为了避免只凭断点位置下结论,可以继续做两步验证。第一步,对比 Network 面板中接口返回的 data 密文,与断点处函数入参 data 是否一致。如果二者一致,就可以确认这里处理的正是接口返回的密文。注意接口响应 JSON 中的 / 可能会显示为 \\/,这是 JSON 转义,不是密文本身发生了变化。
第二步,在 JSON.parse(e.toString(y.a.enc.Utf8)) 附近继续断住,然后在 Console 中执行同样的表达式,查看返回对象是否就是页面上的资讯数据。
整理后的核心解密逻辑如下:
var j = "DXZWdxUZ5jgsUFPF";
var z = y.a.enc.Utf8.parse(j);
var e = y.a.AES.decrypt(data, z, {
iv: y.a.enc.Utf8.parse(j.substr(0, 16)),
mode: y.a.mode.ECB,
padding: y.a.pad.Pkcs7
});
return JSON.parse(e.toString(y.a.enc.Utf8));
其中 y.a 是打包后的 CryptoJS 对象。展开对象可以看到 AES、enc、mode、pad 等模块。这里的关键参数是:
| 算法 | AES |
| 模式 | ECB |
| 填充 | Pkcs7 |
| Key | DXZWdxUZ5jgsUFPF |
| IV | 代码中传入了 j.substr(0, 16),但 ECB 模式实际不使用 IV |
也就是说,复现时只需要对响应中的 data 做 Base64 解码,再用 AES-ECB-PKCS7 解密,最后对明文 JSON 执行 json.loads 即可。先用一个最小 Python 片段验证算法:
# -*- coding: utf-8 -*-
"""
@File : live_jinglingshuju_demo.py
@Author : XAMO Lab
@Date : 2026/6/12 21:58
@Blog : https://blog.csdn.net/xw1680
@Tool : PyCharm
@Desc :
"""
import base64
import json
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
}
url = "https://vapi.jinglingshuju.com/Data/getNewsList"
data = {
"page": "3",
"num": "20"
}
response = requests.post(url, headers=headers, data=data)
print(response.status_code)
print(response.text)
enc_data = response.json().get("data")
aes_obj = AES.new(b'DXZWdxUZ5jgsUFPF', AES.MODE_ECB)
print(json.loads(unpad(aes_obj.decrypt(base64.b64decode(enc_data)), AES.block_size).decode('utf-8')))
执行结果如下:
二、Python 实现
前面的最小验证代码已经能证明算法可行,但直接用于采集还不够完整:缺少封装、并发、异常隔离、日志输出和字段结构化处理。这里把代码整理成面向对象版本,使用 ThreadPoolExecutor 并发请求前 3 页,并用 loguru 输出每页请求和解密状态。
这里保留前面抓包测试时使用的请求头,尤其是 user-agent,同时补充 Referer、Origin、X-Requested-With 等常见浏览器请求头,让请求更接近页面环境。
需要解析的字段包括:
| 标题 | title | 资讯标题 |
| 发布时间 | news_time / time_str | 优先使用接口中的 news_time |
| 新闻来源 | news_source | 文章来源 |
| 详情页链接 | detail / article_id | 接口通常直接返回 detail,缺失时用 article_id 拼接 |
完整代码如下:
# -*- coding: utf-8 -*-
"""
@File : jinglingshuju_articles_spider.py
@Author : XAMO Lab
@Date : 2026/6/12 23:17
@Blog : https://blog.csdn.net/xw1680
@Tool : PyCharm
@Desc : 精灵数据资讯列表采集(AES/ECB/PKCS7 响应解密 + 多线程)
"""
import base64
import json
import time
import warnings
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Dict, List
warnings.filterwarnings(
"ignore",
message=r"urllib3 .* or chardet .*charset_normalizer .* doesn't match a supported version!",
)
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from loguru import logger
class JinglingshujuArticlesSpider:
"""精灵数据资讯列表爬虫"""
API_URL = "https://vapi.jinglingshuju.com/Data/getNewsList"
SITE_URL = "https://www.jinglingshuju.com/articles"
NEWS_URL = "https://news.jinglingshuju.com"
AES_KEY = b"DXZWdxUZ5jgsUFPF"
def __init__(self, page_size: int = 20, max_workers: int = 3, retries: int = 3) –> None:
"""
:param page_size: 每页条数,页面默认 20
:param max_workers: 并发线程数
:param retries: 单页请求失败后的重试次数
"""
self.page_size = page_size
self.max_workers = max_workers
self.retries = retries
self.session = requests.Session()
self.session.headers.update({
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Origin": "https://www.jinglingshuju.com",
"Referer": self.SITE_URL,
# 保留抓包测试时使用的 User-Agent,不删减请求头。
"user-agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36"
),
"X-Requested-With": "XMLHttpRequest",
})
def _decrypt(self, ciphertext: str) –> Dict[str, Any]:
"""Base64 密文 -> AES/ECB/PKCS7 解密 -> dict"""
raw = base64.b64decode(ciphertext)
cipher = AES.new(self.AES_KEY, AES.MODE_ECB)
plaintext = unpad(cipher.decrypt(raw), AES.block_size).decode("utf-8")
return json.loads(plaintext)
def _parse_item(self, item: Dict[str, Any], page: int, index: int) –> Dict[str, str]:
"""提取文章字段,统一输出结构。"""
article_id = str(item.get("article_id") or "")
detail_url = item.get("detail") or ""
if not detail_url and article_id:
detail_url = f"{self.NEWS_URL}/article/{article_id}"
return {
"page": str(page),
"index": str(index),
"title": item.get("title", ""),
"publish_time": item.get("news_time") or item.get("time") or item.get("time_str", ""),
"source": item.get("news_source", ""),
"detail_url": detail_url,
"article_id": article_id,
}
def fetch_page(self, page: int) –> List[Dict[str, str]]:
"""请求单页、解密响应并返回结构化资讯列表。"""
payload = {
"page": str(page),
"num": str(self.page_size),
}
for attempt in range(1, self.retries + 1):
try:
response = self.session.post(self.API_URL, data=payload, timeout=20)
response.raise_for_status()
result = response.json()
if result.get("code") != 0:
logger.warning("page={} 接口异常 code={} msg={}", page, result.get("code"), result.get("msg"))
return []
data = self._decrypt(result["data"])
rows = data.get("list", [])
articles = [self._parse_item(item, page, idx) for idx, item in enumerate(rows, 1)]
logger.success("page={} 解密成功,获取 {} 条资讯", page, len(articles))
return articles
except Exception as exc:
if attempt >= self.retries:
raise
logger.warning("page={} 第 {} 次请求失败,准备重试: {}", page, attempt, exc)
time.sleep(0.5 * attempt)
return []
def run(self, pages: int = 3) –> List[Dict[str, str]]:
"""并发采集前 pages 页资讯。"""
logger.info("开始采集精灵数据资讯 | 共 {} 页 | 每页 {} 条 | 并发 {}", pages, self.page_size, self.max_workers)
all_articles: List[Dict[str, str]] = []
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
future_map = {executor.submit(self.fetch_page, page): page for page in range(1, pages + 1)}
for future in as_completed(future_map):
page = future_map[future]
try:
all_articles.extend(future.result())
except Exception as exc:
logger.error("page={} 采集失败: {}", page, exc)
all_articles.sort(key=lambda row: (int(row["page"]), int(row["index"])))
logger.info("采集完成,共 {} 条资讯", len(all_articles))
return all_articles
if __name__ == "__main__":
spider = JinglingshujuArticlesSpider(page_size=20, max_workers=3, retries=3)
articles = spider.run(pages=3)
for idx, article in enumerate(articles, 1):
logger.info(
"[{}] page={} | {} | {} | {} | {}",
idx,
article["page"],
article["publish_time"],
article["source"],
article["title"],
article["detail_url"],
)
运行脚本后,日志会先输出每一页的解密状态,再按页码和页内顺序打印结构化结果。当前测试前 3 页共获取 60 条数据:
三、总结
这个案例整体难度不高,主要考察的是接口定位和响应体解密。请求参数本身是明文的,分页逻辑也很直观,真正需要处理的是响应中的 data 字段。
分析时可以先通过 getNewsList 定位业务接口,再搜索 decrypt / AES 找到统一响应拦截器中的解密逻辑。确认密文入参与 Network 返回值一致后,就可以提取 Key、模式和填充方式,用 Python 复现。
最终流程可以概括为:
POST /Data/getNewsList
→ 返回 code/msg/data
→ data 为 Base64 形式的 AES 密文
→ AES/ECB/PKCS7 解密
→ json.loads 得到资讯列表
本题没有复杂签名、动态参数或混淆控制流,适合作为 AES 响应解密类案例的入门练习。