手把手教你抓包搞定小爱课程表导入(树维系统/TJU通用思路)
逆向工程实战:破解小爱课程表与树维系统的数据对接难题
上周在折腾课程表导入时,发现小爱课程表内置浏览器对树维系统的兼容性简直是个灾难——页面白屏、数据加载失败,官方适配形同虚设。作为技术爱好者,我决定深入HTTP请求层,用抓包工具逆向破解这套系统的工作机制。经过72小时的反复试验,终于总结出一套通用解决方案,适用于所有使用树维系统的高校(包括但不限于TJU)。下面将完整还原我的技术探索路径。
1. 环境准备与抓包工具配置
工欲善其事,必先利其器。在开始逆向分析前,需要搭建完整的抓包环境:
- Fiddler Classic(Windows平台首选)
- Charles Proxy(macOS用户推荐)
- 安卓设备+ProxyDroid(手机抓包必备)
- Chrome开发者工具(网页分析核心)
提示:确保电脑和手机处于同一WiFi网络,手机端需要手动配置代理服务器地址为电脑IP,端口通常为8888
关键配置步骤:
# Fiddler启用HTTPS解密(需安装证书) Tools > Options > HTTPS > 勾选"Decrypt HTTPS traffic"常见问题排查表:
| 问题现象 | 解决方案 | 原理说明 |
|---|---|---|
| 手机无法连接代理 | 关闭电脑防火墙/杀毒软件 | 8888端口被拦截 |
| HTTPS网站显示不安全 | 安装Fiddler根证书到手机 | 中间人攻击需要证书信任 |
| 抓不到目标APP请求 | 检查APP是否禁用代理 | 部分APP会主动绕过系统代理 |
2. 网络请求逆向分析实战
当树维系统在小爱课程表内嵌浏览器中失效时,我们需要找到原始数据接口。通过对比电脑版和手机版的网络请求差异,发现关键数据流:
学期ID获取接口(GET请求)
- 路径:
/edu/admin/semester!getCurrentSemester.action - 响应示例:
{"id":95,"startDate":"2023-09-04","endDate":"2024-01-14"}
- 路径:
课程数据核心接口(POST请求)
- 路径:
/edu/courseTableForStd!courseTable.action - 关键参数:
{ "semester.id": 95, "ids": "2021218001", "weekFlag": "1" }
- 路径:
请求参数获取技巧:
ids参数:藏在页面JS代码中,可通过正则提取:
// 典型代码模式 if(jQuery("#courseTableType").val()=="std"){ bg.form.addInput(form,"ids","2021218001"); }提取正则表达式:
import re pattern = r'bg\.form\.addInput\(form,"ids","(\d+)"\)' student_id = re.search(pattern, html_content).group(1)
3. 动态内容加载破解方案
树维系统最棘手的问题是课程表数据通过iframe动态加载,传统DOM操作经常失效。经过反复测试,总结出三种可靠的数据获取方案:
3.1 iframe嵌套破解法
// 获取iframe内的document对象 const iframe = document.querySelector('iframe[name="contentFrame"]'); const innerDoc = iframe.contentDocument || iframe.contentWindow.document; // 在iframe上下文中执行查询 const scripts = innerDoc.getElementsByTagName('script'); for(let script of scripts) { if(script.textContent.includes('bg.form.addInput')){ // 提取ids逻辑... } }3.2 直接请求模拟方案
当iframe方案不可行时,可以直接构造POST请求:
import requests session = requests.Session() headers = { "User-Agent": "Mozilla/5.0", "Content-Type": "application/x-www-form-urlencoded" } data = { "semester.id": current_semester_id, "ids": student_id, "weekFlag": "1" } response = session.post( "https://your-school.edu/courseTableForStd!courseTable.action", data=data, headers=headers )3.3 混合式降级方案
针对不同环境的兼容性处理逻辑:
function getStudentId() { try { // 方案1:直接查找 const scripts = document.getElementsByTagName('script'); const id = extractFromScripts(scripts); if(id) return id; // 方案2:iframe查找 const iframe = document.querySelector('iframe[name="contentFrame"]'); if(iframe) { const innerDoc = iframe.contentDocument; return extractFromScripts(innerDoc.getElementsByTagName('script')); } // 方案3:XHR拦截(高级技巧) return await interceptXhrRequest(); } catch(e) { console.error("All methods failed", e); return null; } }4. 课程数据解析与转换
获取到原始HTML后,需要从中提取结构化课程信息。典型数据格式如下:
activity = new TaskActivity( "7357,6484", // 教师ID "罗翔,施鹏鹏", // 教师姓名 "13015(02365)", // 课程代码 "刑法案例分析", // 课程名称 "8866", // 教室ID "明法楼305", // 教室名称 "00000011110000", // 周次标记(1表示上课) null, "", "", "", "" );解析算法关键步骤:
周次计算:
def parse_week_pattern(pattern): weeks = [] for i, char in enumerate(pattern): if char == '1': weeks.append(i + 1) # 转为1-based return weeks时间槽转换:
// 示例:index = 2 * unitCount + 1 function decodeTimeSlot(index, unitsPerDay) { const day = Math.floor(index / unitsPerDay); const slot = index % unitsPerDay; return { day, slot }; }多教师处理:
teachers = [ {"id": 6484, "name": "施鹏鹏"}, {"id": 7357, "name": "罗翔"} ] act_teachers = [7357] # 实际授课教师 def get_teacher_names(all_teachers, active_ids): return [t['name'] for t in all_teachers if t['id'] in active_ids]
5. 小爱课程表数据格式适配
最后需要将解析后的数据转换为小爱课程表API要求的JSON格式:
{ "courseName": "刑法案例分析", "teacher": "罗翔", "weeks": [4, 5, 6, 7], "day": "星期三", "sections": [3, 4], "position": "明法楼305" }完整转换函数示例:
function convertToXiaoAiFormat(course) { const weekDays = ['日', '一', '二', '三', '四', '五', '六']; return { courseName: course.name, teacher: course.teachers.join(','), weeks: course.weeks, day: `星期${weekDays[course.day]}`, sections: [course.startSlot, course.endSlot], position: course.classroom }; }6. 自动化脚本部署方案
为实现学期自动更新,可以编写定时任务脚本:
# 自动化部署方案 import schedule import time def update_semester(): # 自动获取当前学期ID current_semester = get_current_semester() # 更新配置文件 update_config(current_semester['id']) # 触发课程表更新 refresh_timetable() # 每天凌晨3点检查 schedule.every().day.at("03:00").do(update_semester) while True: schedule.run_pending() time.sleep(60)对于技术基础薄弱的同学,也可以使用现成的工具链:
- MITMProxy:自动化抓包分析
- Postman:接口测试集合
- GitHub Actions:定时任务托管
这套方法论已经在三所不同高校的树维系统上验证通过,包括课程表显示异常、数据加载不全、周次计算错误等典型问题。关键在于理解每个学校的参数差异,灵活调整正则表达式和接口参数。
