Posted on 

解放双手——尝试用Python刷超星学习通作业(上)

前言

众所周知,世人苦学习通久矣。虽然如今油猴脚本早已可以满足各类需求,不过出于两个原因:

  1. 油猴脚本有几率被学习通检测到。
  2. 油猴脚本依然需要人工使用支持脚本的设备(主要为电脑)来干预。

有没有一种可能,可以更加轻松地解决掉这些烦人的作业呢?尤其是对于一些可以全部找到精确匹配的答案无需人工干预的作业,不如直接让它自动提交,无需自己再去打开浏览器苦苦挂机了。

开搞

说干就干。毫无疑问,Python作为一门三岁小孩都会的语言,自然是一个不错的选择。

登录

打开浏览器控制台,登录,然后在茫茫人海中寻找登录时发出的请求,最后定位到了这个请求:

通过分析这个请求发出的数据,可以发现数据的形式是这样的:

1
2
3
4
5
6
7
8
{
fid: -1
uname: 账号
password: base64编码后的密码
t: true
forbidotherlogin: 0
validate:
}

堂堂学习通的登录接口居然如此简单粗暴,倒是让我没想到的。

2022-5-29更新

登录接口已经修改,密码改为DES加密,详见https://blog.imoasislee.com/programming/chaoxing-pwd/

保存登录信息

上述登录请求是用 aiohttp 实现的,基本如下:

1
2
3
4
5
6
7
session = aiohttp.ClientSession()
url = "https://passport2.chaoxing.com/fanyalogin"
data = ...
headers = ...
async with session.post(url, data=data, headers=self.headers) as resp:
response = await resp.text()
// ...

aiohttp.ClientSession().cookie_jar 可以被用于其他session

1
session = aiohttp.ClientSession(cookie_jar=cookie_jar)

也可以被保存到本地,之后就无需再次登录:

1
session.cookie_jar.save("cookies")

获取作业信息

接下来就是冲进作业页面,把作业都翻出来了,不过这个过程比我想得稍微曲折了点。

首先打开作业页面,能看到作业界面的URL构成:

1
https://mooc1.chaoxing.com/work/getAllWork?classId=xxx&courseId=xxx&isdisplaytable=2&mooc=1&ut=s&enc=xxx&cpi=xxx&openc=xxx

也就是由 classId courseId isdisplaytable mooc ut enc cpi openc 这么多参数构成。

  1. classId courseId 显然是固定的,获取也不难。
  2. isdisplaytable mooc ut 也不知道是啥 就让它保持不变吧。
  3. enc cpi openc 这三个值很可能并不固定 且由 JavaScript 生成。

于是我返回课程主页,想看看是怎么算出这些参数的。结果…

1
<a onclick="rmWorkDot('53143077','work',this);" href="javascript:;" data="/work/getAllWork?classId=xxx&courseId=xxx&isdisplaytable=2&amp;mooc=1&amp;ut=s&amp;enc=xxx&cpi=xxx&openc=xxx" title="作业">作业  </a>

没错,各个参数赫然写在 data 里了。

那就很容易了,用 lxml 解析课程主页, 再用xpath 提取这个节点中的data 属性,直接拼接到 https://mooc1.chaoxing.com 后面就好了。

大致代码如下:

1
2
3
4
5
6
7
8
from lxml import etree

session = aiohttp.ClientSession(cookie_jar=self.cookiejar)
async with session.get("https://fycourse.fanya.chaoxing.com/courselist/opencourse", headers=..., allow_redirects=False, params={...}) as resp:
html = await resp.text()
// await session.close()
tree = etree.HTML(html)
homework = "https://mooc1.chaoxing.com" + tree.xpath(...)[0].attrib["data"]

进入答题页面

获取必需参数

答题页面的URL更是精彩:

1
https://mooc1.chaoxing.com/work/doHomeWorkNew?courseId=xxx&classId=xxx&workId=xxx&workAnswerId=xxx&isdisplaytable=2&mooc=1&enc=xxx&workSystem=0&cpi=xxx&openc=xxx&standardEnc=xxx

简直眼花缭乱。更要命的是,这里的enc 和之前的enc 的值并不同(和时间无关,短时间内同一个enc 依然可以在之前的页面生效。)

不过有了前车之鉴,我决定去上级页面的源码找找答案。果然不让我失望,这次答案藏在直接用<script> 引用的JS脚本中:


从图中可以并验证得出以下信息:

  1. 需要的workIdworkAnswerId 在上下文中可以找到,且属性为 datadata2


2. enc 依然是写死的。

这样就不难了。首先,还是用 lxml ,提出脚本部分:

1
scripts = tree.xpath("/html/body/script[2]")[0].text

然后使用正则表达式re 找到 enc openc cpi 这三个参数:

1
2
3
enc = re.compile("(?:\?|&)enc=(.+?)&").findall(scripts)[0]
cpi = re.compile("(?:\?|&)cpi=(.+?)&").findall(scripts)[0]
openc = re.compile("(?:\?|&)openc=(.+?)&").findall(scripts)[0]

遍历作业

通过观察源码发现,每个作业对应一个节点,通过用xpath 尝试解析这些节点,直到 解析结果 长度为0, 则表示已没有更多作业。而对于找到的节点,通过 beautifulsoup 可以更方便的通过 className来确定作业状态是否为待做。(classNamept5, 但 不仅仅作业状态使用了这个class, 通过观察, 第n个节点对应的pt5 索引应为 3n-1

(TODO:还涉及翻页的问题)

这里代码太渣了,就不放了。丢脸

一切正常的话,这里就能得到作业节点了。通过attrib 获取到 workIdworkAnswerId

standardEnc

除了enc , 还有个 standardEnc 的参数。这个参数来自于 https://mooc1.chaoxing.com/work/isExpire 这个请求,就不多bb了:

1
2
3
4
5
6
7
async with session.get("https://mooc1.chaoxing.com/work/isExpire", headers=self.headers, params={
"classId": self.classid,
"workRelationId": workrealtionid,
"cpi": cpi,
"courseId": self.courseid
}) as resp:
standardenc = json.loads(await resp.text())["standardEnc"]

验证结果

终于集齐了各种参数,是时候召唤神龙了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async with session.get("https://mooc1.chaoxing.com/work/doHomeWorkNew", headers=self.headers, params={
"courseId": self.courseid,
"classId": self.classid,
"workId": workrealtionid,
"workAnswerId": workanswerid,
"isdisplaytable": 2,
"mooc": 1,
"enc": enc,
"workSystem": 0,
"cpi": cpi,
"openc": openc,
"standardEnc": standardenc
}) as resp:
result = await resp.text()

如果一切正常,接下来的故事就是解析作业页面了。碍于篇幅限制,我要咕咕了,这事儿下回再聊。

开往-友链接力
A member of 开往-友链接力

This site was deployed by @OasisLee using Stellar.

本站由Vercel提供托管与Serverless支持 | PlanetScale提供数据库支持