复旦 iCourse 课件批量导出教程
- 2026-09-21 09:38:25
本人在复习本科最后一门课程时突发奇想,用codex临时做了
fdu-icourse-slides课件整理工具(正文几乎由AI撰写,因为本人懒)。该工具的适用范围为:文科或数学等课件中文字、公式较多的课程,且教师授课速度适中,不会飞速带学生过ppt。本文说明如何使用 fdu-icourse-slides ,将复旦 iCourse 回放中的 PPT 课件截图批量导出,并按每节课一个 PDF的形式合成文件。项目中的 Python 脚本可在本人的GitHub仓库中下载:
https://github.com/chichung0921/fdu-icourse-slides本文流程适用于复旦 iCourse 中当前登录账号有权限访问的课程资源。脚本不保存账号密码,不用于绕过课程访问权限。使用时只需要替换目标课程的 course_id、tenant_code 和输出目录。
1. 准备环境
先下载 GitHub 仓库,并进入项目目录。目录中应包含:
scripts/fudan_icourse_slides.py确认 Python 能运行脚本:
python3 scripts/fudan_icourse_slides.py --help如果提示缺少依赖,安装:
python3 -m pip install requests pillow其中:
requests用于请求和下载图片; pillow用于将图片合成为 PDF。
2. 找到课程参数
打开目标课程详情页,URL 通常类似:
https://icourse.fudan.edu.cn/coursedetail?course_id=COURSE_ID&tenant_code=TENANT_CODE记录两个参数:
COURSE_IDTENANT_CODE
例如:
https://icourse.fudan.edu.cn/coursedetail?course_id=35035&tenant_code=222对应参数为:
COURSE_ID=35035TENANT_CODE=222
后续命令中凡是出现 COURSE_ID、TENANT_CODE 的位置,都需要替换成目标课程的真实值。
3. 登录课程页
在 Safari 中打开目标课程详情页,并确认已经登录,且能够看到课程回放列表。
注意:后续生成索引 JSON 时,脚本会在 Safari 当前页面中执行 JavaScript,因此 Safari 当前标签页必须是:
icourse.fudan.edu.cn不要停留在 GitHub、教程页面或其他网页。
如果 Safari 当前页面错误,生成的 JSON 可能会出现:
{”has_token”: false,”lesson_count”: 0,”lessons”: []}
这种情况通常不是课程没有内容,而是脚本运行在了错误页面上。
4. 开启 Safari JavaScript 自动化
Safari 菜单栏打开:
Develop -> Allow JavaScript from Apple Events如果没有 Develop 菜单,先到:
Safari -> Settings -> Advanced -> Show features for web developers然后再回到菜单栏启用:
Develop -> Allow JavaScript from Apple Events5. 生成课件索引 JSON
课件索引 JSON 用于记录课程课次和 PPT 截图接口返回值。该文件不保存账号密码。
先创建临时目录:
mkdir -p tmp/icourse然后运行下面的命令。使用前需要替换其中的 COURSE_ID 和 TENANT_CODE:
osascript <<'APPLESCRIPT' > tmp/icourse/icourse_COURSE_ID_ppt_index.jsontell application id ”com.apple.Safari”set js to ”(async () => {const COURSE_ID = 'COURSE_ID';const TENANT_CODE = 'TENANT_CODE';function authHeaders() {const rawCookieToken = (document.cookie.match(/(?:^|;\\s*)_token=([^;]+)/) || [])[1] || '';let token = rawCookieToken ? decodeURIComponent(rawCookieToken) : (localStorage.getItem('token') || sessionStorage.getItem('token') || '');const match = token.match(/_token\\\”;i:\\d+;s:\\d+:\\\”(.+?)\\\”/) || token.match(/_token[^\\\”]*\\\”(.+?)\\\”/);if (match) token = match[1];const headers = {};if (token) {headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;}return headers;}async function getJson(path, query = {}) {const url = new URL(path, location.origin);Object.entries(query).forEach(([key, value]) => {if (value !== undefined && value !== null && value !== '') {url.searchParams.set(key, value);}});const res = await fetch(url, {headers: authHeaders(),credentials: 'include'});const text = await res.text();try {return JSON.parse(text);} catch (error) {return {parse_error: String(error),status: res.status,text: text.slice(0, 1000)};}}const catalogue = await getJson('/courseapi/v2/course/catalogue', {course_id: COURSE_ID});const rawLessons = [];function walk(value) {if (Array.isArray(value)) {value.forEach(walk);} else if (value && typeof value === 'object') {if (value.sub_id || value.id) rawLessons.push(value);Object.values(value).forEach(walk);}}walk(catalogue);const seen = new Set();const lessons = [];for (const item of rawLessons) {const subId = String(item.sub_id || item.id || '');if (!subId || seen.has(subId)) continue;seen.add(subId);const title = item.sub_title || item.title || item.name || item.subject_name || '';const status = String(item.sub_status || item.status || '');const pptIndex = await getJson('/pptnote/v1/schedule/search-ppt', {course_id: COURSE_ID,sub_id: subId,page: 1,per_page: 200});lessons.push({course_id: COURSE_ID,sub_id: subId,tenant_code: TENANT_CODE,title,date: title,status,ppt_index: pptIndex});}return JSON.stringify({course_id: COURSE_ID,tenant_code: TENANT_CODE,safari_url: location.href,has_token: Boolean(authHeaders().Authorization),generated_at: new Date().toISOString(),lesson_count: lessons.length,lessons}, null, 2);})()”do JavaScript js in current tab of front windowend tellAPPLESCRIPT
生成后检查 JSON 格式:
python3 -m json.tool tmp/icourse/icourse_COURSE_ID_ppt_index.json > /dev/null如果没有报错,说明 JSON 文件格式正常。
6. 检查索引 JSON
打开生成的 JSON,重点检查以下字段:
{”course_id”: ”35035”,”tenant_code”: ”222”,”safari_url”: ”https://icourse.fudan.edu.cn/coursedetail?course_id=35035&tenant_code=222”,”has_token”: true,”lesson_count”: 10,”lessons”: []}
其中最重要的是:
safari_urllesson_countlessons
safari_url 应当是目标课程详情页,而不是 GitHub 或其他页面。
如果生成结果类似:
{”safari_url”: ”https://github.com/...”,”has_token”: false,”lesson_count”: 0,”lessons”: []}
说明 Safari 当前标签页不正确。需要切换到 iCourse 课程详情页后重新运行索引生成命令。
has_token 为 false 不一定表示失败。部分情况下浏览器会通过 Cookie 保持登录态。只要 lesson_count 大于 0,并且 lessons 中包含课次数据,就可以继续执行后续步骤。
7. 批量生成每节课 PDF
索引 JSON 生成后,运行:
python3 scripts/fudan_icourse_slides.py \--browser-json tmp/icourse/icourse_COURSE_ID_ppt_index.json \--out-dir output/icourse_COURSE_ID_slides
脚本会自动完成以下操作:
读取每节课的 sub_id;提取该节课的 PPT 截图 URL; 下载原图; 过滤缩略图; 按顺序合成 PDF; 跳过没有 PPT 截图索引的课次。
输出目录通常包含:
课程名_sub_id.pdf课程名_sub_id/downloads/课程名_sub_id/urls.txt课程名_sub_id/api_payloads.json
其中:
课程名_sub_id.pdf是合成后的 PDF; downloads/保存下载到的图片; urls.txt保存识别到的图片 URL; api_payloads.json用于排查接口数据。
8. 只导出某一节课
如果已经知道某节课的 sub_id,可以只生成这一节:
python3 scripts/fudan_icourse_slides.py \--browser-json tmp/icourse/icourse_COURSE_ID_ppt_index.json \--sub-id SUB_ID \--out-dir output/icourse_COURSE_ID_slides
如果不知道 sub_id,可以从索引 JSON 中查看:
python3 - <<'PY'import jsonfrom pathlib import Pathdata = json.loads(Path(”tmp/icourse/icourse_COURSE_ID_ppt_index.json”).read_text())for lesson in data.get(”lessons”, []):print(lesson.get(”sub_id”), lesson.get(”title”) or lesson.get(”date”))PY
9. 换一门课程时怎么做
换课程只需要重新执行三步:
在 Safari 中打开新课程详情页并登录; 重新运行索引生成命令,替换 COURSE_ID和TENANT_CODE;使用新的索引 JSON 运行批量生成命令。
建议每门课使用独立输出目录:
output/icourse_COURSE_ID_slides这样不同课程的 PDF 和下载图片不会混在一起。
10. 常见问题
10.1 生成的 JSON 中 lesson_count 为 0
优先检查:
”safari_url”: ”...”如果 safari_url 不是 iCourse 课程详情页,说明 Safari 当前页面不正确。切换到课程详情页后重新运行命令。
如果 safari_url 正确,但 lesson_count 仍为 0,检查是否已经登录,或者当前账号是否有权限访问该课程。
10.2 has_token 为 false
has_token 为 false 不一定影响后续流程。重点看 lesson_count 和 lessons 是否有内容。
如果课程目录和 PPT 索引已经成功写入 JSON,可以继续运行 Python 脚本。
如果同时出现:
”lesson_count”: 0,”lessons”: []
则需要重新检查 Safari 当前页面和登录状态。
10.3 出现 Auth Forbidden 或下载失败
通常是登录态失效。解决方法:
重新在 Safari 中登录 iCourse; 回到目标课程详情页; 重新生成索引 JSON; 再运行 Python 脚本。
如果仍然失败,可以从浏览器开发者工具复制完整 Cookie,并在运行脚本时使用:
python3 scripts/fudan_icourse_slides.py \--browser-json tmp/icourse/icourse_COURSE_ID_ppt_index.json \--cookie '这里粘贴完整Cookie' \--out-dir output/icourse_COURSE_ID_slides
10.4 生成的 PDF 数量少于课次数量
常见原因:
某节课本身没有 PPT; 回放还在生成中; 平台 PPT 识别失败; 课程目录中存在未发布、已删除或不可访问的课次; /pptnote/v1/schedule/search-ppt返回 total: 0。
可以用下面的命令检查哪些课没有 PPT 截图索引:
python3 - <<'PY'import jsonfrom pathlib import Pathdata = json.loads(Path(”tmp/icourse/icourse_COURSE_ID_ppt_index.json”).read_text())for lesson in data.get(”lessons”, []):ppt = lesson.get(”ppt_index”) or {}if int(ppt.get(”total”) or 0) == 0:print(lesson.get(”sub_id”), lesson.get(”title”) or lesson.get(”date”), ”no ppt screenshots”)PY
如果某节课 ppt_index.total 为 0,说明平台没有提供 PPT 截图索引,脚本不会生成该节课的 PDF。
10.5 图片顺序不正确
脚本默认按照文件名中的数字自然排序。正常情况下,平台图片名就是页码顺序。
如果某门课文件名异常,可以检查:
output/icourse_COURSE_ID_slides/<lesson>/urls.txt必要时可以手动调整图片文件名后重新合成 PDF。
11. 安全说明
请仅导出本人账号有权限访问的课程资源。
不要将账号密码写入脚本或教程。不要公开包含个人登录态的 Cookie、token 或完整浏览器会话信息。不要将导出的课程资料公开传播。
本流程只使用已登录浏览器当前用户可访问的课程资源,主要用于个人学习资料整理。