Python实战:自动化获取国家中小学智慧教育平台公开资源 (含MAC Token鉴权解析)

声明:本文仅供 Python 网络编程、API 交互技术学习交流使用。请尊重平台资源版权,合理合规使用,切勿用于高并发请求、影响服务器运行或任何商业牟利行为。

一、项目背景

国家中小学智慧教育平台是广大中小学教师备课的宝库,里面有大量优秀的公开教学设计、课件(PPT)和学习任务清单。在日常备课中,老师们往往需要逐个点击预览再进行保存,过程相对繁琐。

为了提高备课效率,我们可以利用 Python 编写一个自动化辅助工具:输入课程链接,即可按顺序将“PPT、教学设计、学习任务清单”等公开备课资源整理到本地。
请添加图片描述
请添加图片描述


二、数据接口与网络请求分析

在编写代码之前,我们需要了解浏览器是如何与服务器交互并获取这些 PDF 文件的。打开浏览器的开发者工具(F12),切换到 Network 面板进行观察。

1. 寻找资源的元数据 JSON

当我们在平台上打开一个特定的课程活动(例如:https://basic.smartedu.cn/syncClassroom/classActivity?activityId=xxx...)时,前端会发起一个 XHR 请求获取当前课程的数据。

接口结构大概如下:
https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/national_lesson/resources/details/{activityId}.json

返回的是一个 JSON 对象,里面包含了这个课程下的所有资源信息。在 ti_storages 字段里,我们可以找到云存储的访问链接。

2. 跨域与权限验证

拿到云存储的 URL 后,如果直接在浏览器开个新标签页访问,或者用最简单的 requests.get() 去请求,会收到 401 Unauthorized403 Forbidden

观察浏览器的正常请求,发现请求真实 PDF 链接时,带有几个特殊的 Request Header:

  • Origin: https://basic.smartedu.cn
  • Referer: https://basic.smartedu.cn/
  • x-nd-auth: MAC id="7F93...",nonce="1784105167:AbCdEfGh",mac="xxxx..."

前两个是常规的跨域来源校验。而 x-nd-auth 则是核心的鉴权字段。

3. 解析 MAC Token 鉴权机制

查阅相关开放文档,发现平台使用的是 OAuth MAC Token 机制(RFC draft-ietf-oauth-v2-http-mac)。

这是一种标准的鉴权方式:

  1. 用户登录后,服务器下发一对密钥对:access_token(身份标识 id)和 mac_key(签名密钥)。
  2. 当客户端去请求云盘文件时,需要在本地用 mac_key当前时间戳请求的URL路径 等信息进行 HMAC-SHA256 摘要计算。
  3. 计算结果即为 x-nd-auth 里的 mac 字段。服务器收到请求后,会采用同样的算法验证签名的有效性。

这意味着:直接复制抓包里的 x-nd-auth 是无效的,因为时间戳和随机数会很快过期。我们需要在 Python 脚本中按规范实现这套签名算法。


三、获取核心凭证 (Token)

在实现签名算法前,我们需要获取自己的 access_tokenmac_key,它们通常存储在浏览器的 localStorage 中。

为了方便提取,我编写了一小段辅助 JavaScript 脚本。

操作步骤:

  1. 登录 basic.smartedu.cn
  2. F12 打开控制台(Console)
  3. 粘贴并回车运行以下 JS 代码:
(function(){function f(o){if(o==null)return null;if(typeof o==='string'){if(o.length>10&&(o[0]==='{'||o[0]==='[')){try{return f(JSON.parse(o))}catch(e){return null}}return null}if(typeof o!=='object')return null;if(o.access_token&&o.mac_key)return{access_token:o.access_token,mac_key:o.mac_key,expires_at:o.expires_at||''};for(var k in o){var r=f(o[k]);if(r)return r}return null}function s(st){for(var i=0;i<st.length;i++){var r=f(st.getItem(st.key(i)));if(r)return r}return null}var r=s(localStorage)||s(sessionStorage);if(r){console.log('%c==== 复制下面这一行 JSON ====','color:green;font-size:14px;font-weight:bold');console.log(JSON.stringify(r));console.log('%c==== 复制上面那一行 ====','color:green;font-size:14px;font-weight:bold')}else{console.log('%c未找到 token','color:red;font-size:14px')}})();

运行后,控制台会输出一行 JSON,里面正好包含我们需要的两把钥匙。保存好备用。


四、Python 代码实现 (核心解析)

环境准备非常简单,只需要安装 requests:
pip install requests

为了方便理解,我将核心逻辑分成了三个模块。

模块 1:MAC 签名生成器 (关键)

这是请求成功的核心。我们需要把 nonce、HTTP方法、请求路径、域名、端口号按照固定顺序拼接,最后用 HMAC-SHA256 计算。

import base64, hashlib, hmac, random, string, time
from urllib.parse import urlparse

def build_mac_auth(url: str, access_token: str, mac_key: str, method: str = "GET") -> str:
    # 1. 解析URL
    parsed = urlparse(url)
    host = parsed.hostname or ""
    port = parsed.port or (443 if parsed.scheme == "https" else 80)
    path = parsed.path or "/"
    if parsed.query:
        path = f"{path}?{parsed.query}"

    # 2. 构造 nonce (时间戳:随机串)
    ts = str(int(time.time()))
    rand = "".join(random.choices(string.ascii_letters + string.digits, k=8))
    nonce = f"{ts}:{rand}"
    
    # 3. 构造基础签名串 (严格按顺序,末尾加换行)
    base = f"{nonce}\n{method}\n{path}\n{host}\n{port}\n\n"
    
    # 4. HMAC-SHA256 加密并 Base64 编码
    mac = base64.b64encode(
        hmac.new(mac_key.encode("utf-8"), base.encode("utf-8"), hashlib.sha256).digest()
    ).decode("utf-8")

    # 5. 拼装为 header 要求的格式
    return f'MAC id="{access_token}",nonce="{nonce}",mac="{mac}"'

模块 2:深度遍历寻找资源

资源的 JSON 结构存在较深嵌套。我们可以编写一个递归扫描函数:只要节点里有 ti_format == 'pdf'ti_storages(云盘地址),我们就提取它。

def find_pdf_items(activity_json: dict) -> list:
    results = []

    def walk(node):
        if isinstance(node, dict):
            fmt = str(node.get("ti_format", "")).lower()
            storages = node.get("ti_storages")
            # 找到目标
            if fmt == "pdf" and isinstance(storages, list) and storages:
                results.append(node)
            # 继续向子节点递归
            for v in node.values():
                walk(v)
        elif isinstance(node, list):
            for v in node:
                walk(v)

    walk(activity_json)
    return results

模块 3:保存文件

利用生成好的鉴权信息,携带 Origin 和 Referer 去请求文件,以流(stream=True)的方式写入本地,防止文件过大占用过多内存。

import requests
import os

UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"

def download_pdf(resource_url: str, save_path: str, access_token: str, mac_key: str) -> None:
    headers = {
        "User-Agent": UA,
        "Origin": "https://basic.smartedu.cn",
        "Referer": "https://basic.smartedu.cn/",
        "x-nd-auth": build_mac_auth(resource_url, access_token, mac_key, "GET"),
        "Accept": "*/*",
    }
    
    with requests.get(resource_url, headers=headers, stream=True, timeout=60) as r:
        if r.status_code == 401:
            raise RuntimeError("请求被拒 (401),token 无效或已过期")
        r.raise_for_status()
        
        os.makedirs(os.path.dirname(save_path), exist_ok=True)
        with open(save_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=64 * 1024):
                if chunk:
                    f.write(chunk)

完整组合

最后,只需将上述模块按流程组装:

  1. 请求 API 拿到元数据。
  2. 调用 find_pdf_items 找出资源。
  3. 提取所需的备课资源。
  4. 循环调用 download_pdf 保存。

(大家可以基于上面的三大核心模块自由发挥编写 main 函数和交互逻辑。)


五、打包为 EXE 便捷使用

为了方便分享给同事使用,我们可以用 PyInstaller 将它打包成独立的程序:

pip install pyinstaller
pyinstaller --onefile --name "教育资源下载器" --clean crawler.py

打包完成后,dist 文件夹下会生成一个可执行文件,双击即可运行,无需配置环境。

六、总结

通过这次实践,我们完成了以下技术要点:

  1. 标准鉴权协议的本地化实现(基于 HMAC 的 MAC Token 签名过程)
  2. 复杂 JSON 数据结构解析(深度遍历算法寻找目标节点)
  3. Python 脚本打包与交互设计
    github源码:https://github.com/lhjgyhj/Education_Resource_Downloader
    希望这篇文章能帮你巩固网络请求分析与协议实现的技巧。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐