国家中小学智慧教育平台电子课本下载工具 tchMaterial-parser 详解
「国家中小学智慧教育平台」上的电子课本质量非常高——官方排版、可缩放、目录清晰、配套音频齐全,但官方并没有提供「一键导出 PDF」的入口,老师和家长想存到本地就只能靠网页打印或手动截图。
GitHub 上有一个叫 tchMaterial-parser(仓库:happycola233/tchMaterial-parser)的开源工具把这件事做得相当彻底:粘一个 URL,自动解析、自动命名、自动按学段/学科/版本建子目录、自动写章节书签、还能批量下。2025 年 5 月它曾登顶 GitHub Trending 总榜第 3 名,单日新增约 400 Stars,今天这篇文章就带大家把这个工具从功能到实现原理完整拆开。
项目地址:https://github.com/happycola233/tchMaterial-parser
作者:肥宅水水呀(happycola233)+ 晨叶梦春
许可证:MIT(同时使用了 Microsoft Fluent Emoji 中的部分图片资源)
当前版本:v4.4
技术栈:Python 3.10+ · Tkinter · sv-ttk · requests · Pillow · pypdf · pywin32 · PyInstaller
代码规模:约 20 个.py文件、215 次提交,核心模块 3000 余行业务代码
一、它到底解决了什么问题
basic.smartedu.cn/tchMaterial/ 是教育部「国家中小学智慧教育平台」电子课本的入口。点开任意一本教材,网页可以流畅翻页,但:
- 无法直接保存为 PDF:网页用 pdf.js 渲染,没有提供下载入口;
- 音频资源被拆得很碎:英语教材的听力、课件朗读的 MP3 都分散在
relation_audios.json之类的接口里; - 章节没有书签:下载下来的 PDF 想跳到「第一单元·识字」必须手动滚;
- 私有 CDN 需要鉴权签名:核心资源放在
r1-ndr-private.ykt.cbern.com.cn这种私有 CDN 上,每次请求都要附X-ND-AUTH,签名错误立刻 400; - 跨平台体验不一致:网页在 Linux 上的字体、对 macOS 触控板的适配都不如 Windows 顺手。
tchMaterial-parser 的思路非常直接——完全模拟官方 web 的请求链路,但把所有解析、下载、命名、书签、UI 都收到一个本地桌面应用里,并补上网页没有的「下载到本地」能力。

二、三分钟跑起来
1. 选一个适合自己的安装方式
工具在四个分发渠道都做了适配,按自己平台挑一个:
| 平台 | 推荐方式 | 一句话命令 |
|---|---|---|
| Windows 10/11/Server 2025 | WinGet | winget install happycola233.tchMaterial-parser |
| Arch Linux | AUR | yay -S tchmaterial-parser |
| Windows / macOS / Linux | GitHub Releases | 前往 Releases 选 windows-x64.exe / mac-arm64.zip / linux-x64 |
| 任意平台(开发用) | pip + 源码 | pip install tchmaterial-parser 或 python ./src/main.py |
macOS 小坑:由于没签名,第一次启动会报「文件已损坏」,需要先运行
xattr -cr /Applications/tchMaterial-parser.app移除隔离属性。
2. 输入资源页面链接
把电子课本的预览页面 URL(长这样:https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=…)粘到右边的文本框里,每行一个。工具一次能处理几百条链接。
你也可以直接用左侧的资源树——点开「高中 → 语文 → 统编版」,工具会自动把对应 URL 写进文本框。
3. (可选但强烈建议)设置 Access Token
工具默认会用一种「匿名方式」尝试下载,但私有 CDN 上的资源会 400。想要稳定下载,先登录官网获取 Access Token:
打开 auth.smartedu.cn/uias/login 并登录账号;
按
F12打开 DevTools,切到 Console;粘贴下面这段 JS 回车,复制输出的整段 JSON:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17(function () {
const authKey = Object.keys(localStorage).find(
key => /^ND_UC_AUTH-[^&]+&[^&]+&token$/.test(key),
);
if (!authKey) {
console.error("未找到登录凭据,请确保已登录!");
return;
}
const tokenData = JSON.parse(localStorage.getItem(authKey));
const { access_token, mac_key, diff } = JSON.parse(tokenData.value);
const credentials = JSON.stringify({ access_token, mac_key, diff });
console.log(
"%c请复制下面整段 JSON 并粘贴到下载工具:",
"color: green; font-weight: bold",
);
console.log(credentials);
})();回到工具,点「设置 Token」,粘进去保存。Token 只存本机(Windows 注册表 / Linux & macOS 配置文件),不会上传。
4. 点下载
点主按钮「下载」之后,工具会先在后台把每条 URL 解析成 ResourceInfo(直链 + 标题 + 章节树),再开 3 个工作线程(_download_slots)拉文件,同名同路径已下载完成的会自动跳过。v4.4 新增的「下载管理」窗口会实时显示每个任务的状态、速度、错误详情。
下载完成的 PDF 还会被自动加上章节书签:

三、功能一览
工具的 README 里把功能列得很清楚,这里挑几个特别值得一提的:
1. 智能批量下载
- 逐行解析:每行一个 URL,去重、跳过空行;
- 后台解析:批量 200 条链接也不会卡 UI(
parse_urls_in_background把解析放进ThreadPoolExecutor); - 跳过已下载:批量模式下
os.path.isfile(save_path)命中就标skipped,再下一轮时不会再拉; - 随时停止:
_stop_requested这个threading.Event会中止所有进行中的任务,正在写的临时文件会被清理,已经下完的文件保留; - 路径自动归类:通过
tag_list里的tag_dimension_id(学段 / 学科 / 版别)自动建立高中/语文/人教版/子目录。
2. 私有 CDN 与签名重算
这是工具的核心难点之一:r1-ndr-private.ykt.cbern.com.cn 收到每个请求都要校验 X-ND-AUTH 头,而 X-ND-AUTH 是基于完整 URL + 方法 + 时间戳现算的 HMAC-SHA256。也就是说:
- 每条 URL 都必须单独签名一次,不能用「抓一个头到处贴」;
Authorization: Bearer <token>是同一个 token,但X-ND-AUTH的 MAC 是按当前 URL 现算的。
工具在 auth.py 里完整复刻了官方 UC SDK 的签名算法:
1 | def signature_string(url: str, method: str, nonce: str) -> str: |
network.request_headers(url) 会拿到 URL 后现算 X-ND-AUTH,覆盖全局占位头,再用这个新头去请求。每条私有 CDN URL 都吃这个流程。注意:中文路径会先 unquote 再签名,HAR 抓包里看到的中文编码路径直接拿来对 MAC 永远对不上,这是作者踩过的一个大坑(见源码注释)。
3. 自动 PDF 书签
工具通过 ebook_mapping.txt + trees/<ebook_id>.json 两个接口组合拿到「目录标题 + 页码」的映射,再交给 bookmarks.py 写到 PDF 里:
1 | from pypdf import PdfReader, PdfWriter |
写入流程是:先下载到 xxx.pdf.tmp,书签写好后才 os.replace 成正式文件——这一步保证任何中途失败都不会留下损坏的 PDF。
4. 资源树 + 搜索
左侧的 ttk.Treeview 是工具的「门面」,体验比官网更顺手:
- 三级层级:学段 → 学科 → 版别 → 教材,展开后是按封面缩略图 + 标题显示的;
- 跨层级搜索:
filter_resource_items会把分类路径拼成初中/英语/人教版/...,再用空格分词做casefold包含匹配; - 悬停看大封面:
resource_tree.py里的<Enter>事件会异步拉封面图,悬停时显示 240×320 的大图; - 复选框三态:父分类会根据子树的全选/部分选状态显示空白/打勾/横线三种图标(自己用
PIL.ImageDraw画的,跟主题色联动)。
5. 下载管理(v4.4 新增)
旧版本只有底部一条进度条,下载多了根本看不清谁是谁。新版把进度条扩成了一个独立的「下载管理」窗口:
- 表格列:文件名 / 状态 / 进度 / 大小 / 速度 / 错误;
- 筛选器:全部 / 进行中 / 需处理 / 已完成,方便在批量任务里挑出失败的;
- 复制错误详情:失败任务的错误堆栈可以直接复制,调试时不用再去翻终端;
- 重试:「重试失败项」会重新调度所有
failed_reason不为空的任务,已存在的目标文件不会自动覆盖(必须重新走保存对话框)。
6. 主题与高 DPI
- 主题三档:浅色 / 深色 / 跟随系统(Windows 读注册表、macOS 读
defaults、Linux 读gsettings); - 深色标题栏:Windows 上调
DwmSetWindowAttribute(属性 20 / 19,对应 Win10 20H1 与更早版本)让标题栏也变深; - 高 DPI:启动时通过
win32print.GetDeviceCaps算缩放因子,再tk scaling设置,UI 字体全部走scaled(size)按像素算字号; - 跨平台中文字体:
pick_ui_font_family依次尝试Microsoft YaHei UI → 微软雅黑 → PingFang SC → Noto Sans CJK SC → WenQuanYi Zen Hei → Arial Unicode MS,选第一个可用的。
四、代码架构:一张图看清楚
工具源码组织得很干净,模块之间单向依赖:
1 | tchmaterial_parser/ |
最值得说一下的是 runtime 这个共享上下文:Tkinter 的所有 UI 操作只能在主线程,但下载/解析需要后台线程,作者用 runtime.bind_root(root) + ui_call(fn, *args) + thread_it(worker) 三件套把跨线程调用收口到一个文件里——其它模块通过 runtime.root、runtime.ui_scale 这些模块级全局变量访问主线程对象,避免了在十几个文件里到处塞 global root。
关键流程:从一次下载看完整调用链
- 用户点「下载」 →
download_panel.download() parse_urls_in_background把每条 URL 放进后台线程调api.parse(url, bookmarks);api.parse根据 URL 里的contentType与域名分支:assets_document(电子课本)→zxx/ndrv2/resources/tch_material/details/{id}.jsonnational_lesson→.../national_lesson/resources/details/{id}.jsonthematic_course→.../special_edu/thematic_course/{id}/resources/list.json- ……
- 解析完成拿到
ResourceInfo后,回到主线程让用户选择保存目录(批量)或文件对话框(单条); allocate_download_paths在线程启动前统一预留路径,避免同名任务抢同一个.tmp;run_download_tasks用ThreadPoolExecutor(max_workers=3)跑download_file;download_file→request_download(带签名头 + 镜像兜底)→ 流式写.tmp→add_bookmarks→os.replace成正式文件;monitor_downloads每 200 ms 读一次download_states,刷新主窗口进度条与管理窗口。
五、私有 CDN 的限流避坑
这套请求链路里有一个非常细节、但作者花了大量精力打磨的地方——私有 CDN 的限流应对。
r1/r2/r3-ndr-private.ykt.cbern.com.cn 在「短时间连打」时会回 400(有时带 InvalidArgument,有时几乎是空包)。作者做了三件事:
1. 同地址用新 nonce 退避重试,而不是切镜像
1 | _400_RETRY_DELAYS = (1.0, 3.0) |
**直觉是切到 r2/r3 就好,但实测发现切换会打得更快。**因为 400 主要是「签发过快」,不是「这台 CDN 挂了」,换地址只会把限流打得更死。同地址等 1 秒、3 秒,重新算一个 nonce 再签一次,往往就过了。
2. 镜像只在 401/403 时启用
401/403 是「这台 CDN 拒绝我」,那确实该切:
1 | if response.status_code in (401, 403): |
其它情况都按「同地址重试」处理。
3. 同一进程限速到 200ms/请求
1 | _MIN_REQUEST_INTERVAL = 0.2 |
加上 ThreadPoolExecutor(max_workers=3) + _download_slots = threading.BoundedSemaphore(3),一个批次里同时并发最多 3 条,间隔 200 ms,等于稳定的 5 QPS——既能跑满小水管,又不会触发 CDN 限流。
这套细节藏在 download_panel.py 的注释里:「# 立刻换 r2/r3 只会把限流打得更死」是作者用日志堆出来的结论。
六、跨平台分发与打包
工具是纯 Python,依赖只有:requests、Pillow、psutil、pypdf、sv-ttk、pywin32(仅 Windows)。打包用 PyInstaller,仓库根目录有现成的 tchMaterial-parser.spec:
- macOS:产出
.appbundle,禁用控制台窗口; - Windows / Linux:单文件 + UPX 压缩,附加
version_info.txt让 .exe 的文件属性里能显示版本号与公司名; - 运行时资源:
sv_ttk的.tcl/.png和工具自带的 logo 通过collect_data_files+runtime_assets一并打进 bundle; - PIL 子模块:
PIL._tkinter_finder显式加进hiddenimports,否则 Pillow 在非 Windows 平台会因为 C 层动态导入而缺符号。
CI(.github/workflows/*.yml)在 6 个 runner(Windows x64/ARM64、Linux x64/ARM64、macOS x64/ARM64)上跑构建,fail-fast: false,保证单个平台失败不会拖累其它。
Releases 之外还做了:
- WinGet manifest:由社区用户 @PtJade-Ceramic 提交(#64);
- AUR 包:由社区用户 @iamzhz 维护(#26);
- Windows 7 分支:社区维护了一个 tchMaterial-parser-for-Windows7,把语法降到 Python 3.8。
七、安全与合规
工具本身不存储、不分发任何资源,下载下来的所有 PDF/MP3 都来自原平台,作者在 README 里把这一点写得很清楚:
本工具仅提供下载上的便利,不存储、不托管、不分发任何资源内容。所下载资源的版权归原平台及相关权利人所有,请仅用于个人学习与教学参考,请勿用于商业用途或二次分发。
代码层面也对凭据做了几层保护:
redact_access_token会把所有错误日志里的accessToken=…、access_token、mac_key替换成<已隐藏>,防止用户复制错误详情时把凭据贴出去;- Token 只存本地(Windows 注册表 / Linux & macOS 配置文件),没有任何上传逻辑;
parse_token_input对粘贴内容做严格 JSON 校验(access_token、mac_key、diff三项必填且类型正确),防止用户误把账号资料缓存(ND_UC_AUTH-…&sdk_cache)当成 Token 贴进来——#89 就是这个坑。
八、值得关注的小细节
- 文件名清洗:
sanitize_filename把 Windows 禁用的< > : " / \ | ? *替换成全角(< > : " / \ | ? *),既保留可读性又避开 #86 报错的 Windows 报错;同时会避开CON / PRN / COM1..9 / LPT1..9等保留设备名。 - Emoji 渲染:
images.color_emoji_font_paths()会按平台找系统 Emoji 字体(Windowsseguiemj.ttf、macOSApple Color Emoji.ttc、LinuxNoto Color Emoji.ttf),用 Pillow 的embedded_color=True直接画出系统原始彩色字形,而不是被 Tk 文本控件糊成黑白框。 - 404 处理:私有 CDN 4xx 时解析响应 XML 拿
Code,InvalidArgument单独提示 Token 过期,普通 4xx 提示网络问题,避免给用户弹一个看不懂的堆栈。 - 临时文件清理:每个
download_file用xxx.pdf.tmp写,失败时os.remove;allocate_download_paths会主动避开正在被其它实例写的.tmp,防止两台工具同时跑互踩。 - 测试覆盖:仓库里 16 个
tests/文件,覆盖鉴权、签名、解析、并发下载、文件名分配、Token 脚本生成等所有关键路径,CI 里跑pytest+flake8。
九、怎么参与贡献
作者在 CONTRIBUTING.md 里把规矩写得很细:
- 提交前先搜索已有 Issue / PR,避免重复讨论;
- 凭据(Access Token)永远不要贴到 Issue / PR / commit / 代码里;
- 代码风格:4 空格缩进、
snake_case命名、字符串默认用双引号、不出现尾随空格; - UI 改动必须同时检查浅色与深色模式;
- 修改打包配置 / 资源文件 / 程序入口时额外跑一次 PyInstaller 打包验证。
最近的几个有亮点的提交:
| Commit | 内容 |
|---|---|
47899f7 |
fix(auth): 仅从 ND_UC_AUTH 的 &token 项读取登录凭据(#89) |
cd4b6a0 |
fix: 按官网 MAC 签名私有下载,并避免 400 时连打镜像(#81、#76) |
78938b1 |
fix: 用全角标点替换文件名中的非法字符(#86) |
e9a1604 |
feat(download): 批量下载时跳过已下载的文件并支持手动停止(#94) |
d09686d |
fix: 教材听力和听力课件按播放器顺序选择音频 |
14c4e45 |
chore: 版本号从 4.3 升到 4.4 |
v4.x 三个版本(4.2 / 4.3 / 4.4)几乎是同一个主线在「下载管理 / 鉴权修正 / 限流应对」这条轴上滚动,每次都附带一份诚实的 Issue 引用。
十、写在最后
tchMaterial-parser 不是一个「爬虫工具」,它本质上是官方 web 阅读器的能力补全:
- 补了「下载入口」;
- 补了「批量」;
- 补了「书签」;
- 补了「跨平台桌面体验」。
而它实现这一切靠的不是「逆向破解」,而是:
- 老老实实复刻官方签名算法——
auth.py的注释里写满了Fe(diff)、ze(url)、He(...)这些函数名,与官网 UC SDK 一一对应; - 老老实实抓官方公开接口——
tch_material_tag.json、details/{id}.json、trees/{ebook_id}.json都是公开可访问的资源; - 老老实实做工程——错误日志 Token 自动脱敏、临时文件原子重命名、并发限速到 5 QPS、跨平台高 DPI 适配。
这种「把一件小事做到极致」的开源项目,正是中文开源社区里最值得收藏的那一类。如果你身边有老师、家长或学生需要批量保存电子课本,强烈建议把这个工具放进收藏夹。
相关阅读:如果你对「爬虫 + 桌面应用」的其它玩法感兴趣,可以看
《微信视频号下载神器 wx_channels_download 全解析:MIT 开源、8MB 小体积、跨平台、支持 MCP》——同样的「桌面 GUI + 私有协议逆向 + 跨平台打包」的另一种实现。想要批量获取整套教材 PDF / 不想自己写脚本的话,也可以直接看整理好的网盘资源:电子课本合集归档,按学段学科分类下载即可。