Posted on 

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

前言

上篇文章 中,我已经通过 粗制滥造的 代码实现了获取课程中未完成的作业页面,接下来要做的就是:

  1. 解析这个页面,获取题目。
  2. 查询答案,并匹配选项。
  3. 填写答案,提交。

开搞

解析作业页面

首先简单观察一下HTML的结构:


ZyBottom 这个节点包含了整个题目区。再往里面看:



每个classTiMudiv 包含了独立的一道题。

因此,还是用bs4来解析:

1
2
3
soup = BeautifulSoup(result, 'html.parser')
zybottom = soup.find(class_="ZyBottom")
works = zybottom.findChildren(class_="TiMu") # 作业子节点


然后找到题目的节点,通过 .text 获取其内容,最好再.strip() 去除不必要的空格:

1
2
// ... 遍历works子节点 ...
works = work[index].findChildren(class_="clearfix", style="line-height: 35px; font-size: 14px;padding-right:15px;")[0].text.strip()

查询并填入答案

市面上的查题API很多,随便打开一个油猴脚本里面也能找到不少好的API,主要任务在解析答案并匹配选项。

这里就涉及到一个问题:题目的类型。以最常见的单选题与多选题为例,我使用的某API对单选题直接在data中返回答案内容,而多选题返回用#分割开的数个选项。

好在,判断题目类型并不难:单选题和多选题的选择框不同,只需在题目子节点中寻找选择框,就能推断出当前题目的类型:(不排除复杂情况)

1
2
3
4
5
if len(works[i].findChildren(type="radio")) != 0:
// 单选题
elif len(works[i].findChildren(type="checkbox")) != 0:
// 多选题
else: // ...

匹配答案

Python 内置了 difflib ,可以方便的比较两个字符串的相似度。将题目选项和API返回的答案作比较即可。

1
2
3
4
5
6
7
// ...
from difflib import SequenceMatcher
// ...

seq = SequenceMatcher()
seq.set_seqs(s1, s2)
seq.ratio() // 相似度

单选题很简单,一一遍历即可。多选题稍微有些复杂,我写成了函数,并且没有用什么复杂的语法,看起来清晰一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def compare_answers(self, s1, s2):
"""
比较多选题答案
"""
_s2nums = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
result = ""
_s1 = []
for i in s1:
_s1.append(i.strip())
_s2 = []
for i in s2:
_s2.append(i.strip())
for option in _s2:
_bestratio = 0
for i in _s1:
self.seq.set_seqs(i, option)
if self.seq.ratio() > _bestratio:
_bestratio = self.seq.ratio()
if _bestratio > 0.8: result += _s2nums[_s2.index(option)] // 0.8是自己设定的阈值 假定超过这个匹配度就是正确的
return result

后面会提到,最终提交数据的格式是answer123=A,answer456=BCD…因此会有这样的返回值。

提交作业数据

学习通还是没让我失望。尽管提交数据看起来密密麻麻一堆,但是无一例外都可以直接在静态页面中找到。


通过以下这个函数,传入soup, 就可以找到大多数参数的值。

1
2
def getvalue(self, soup, id):
return soup.find(id=id).get_attribute_list("value")[0]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
postdata = {
"pyFlag": "",
"isdisplaytable": 2,
"mooc": 1,
"enc": self.getvalue(soup, 'enc'),
"enc_work": self.getvalue(soup, 'enc_work'),
"cpi": self.cpi,
"courseId": self.courseid,
"classId": self.classid,
"workAnswerId": self.getvalue(soup, 'workAnswerId'),
"totalQuestionNum": self.getvalue(soup, 'totalQuestionNum'),
"fullScore": self.getvalue(soup, 'fullScore'),
"knowledgeid": self.getvalue(soup, 'knowledgeid'),
"oldWorkId": self.getvalue(soup, 'oldWorkId'),
"jobid": self.getvalue(soup, 'jobid'),
"workRelationId": self.getvalue(soup, 'workRelationId'),
"blankobj": self.getvalue(soup, 'blankobj'),
"workLibraryId": self.getvalue(soup, 'workLibraryId'),
"standardEnc": self.getvalue(soup, 'standardEnc'),
"workTimesEnc": self.getvalue(soup, 'workTimesEnc'),
"openc": self.getvalue(soup, 'openc')
}

题目答案

接下来还需要提交题目答案,并合并到postdata 中。此外,最后的 answerwqbid包含了所有题号。这里依然只是着重处理单,多选题,并不是完善的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
answerwqbid = []
for answer in answers:
if len(answers[answer]) == 1:
# 单选题
postdata['answer' + answer] = answers[answer]
postdata['answertype' + answer] = 0
answerwqbid.append(answer)
else:
_sumlength = 0
for a in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']:
_sumlength += answers[answer].count(a)
if _sumlength == len(answers[answer]):
# 多选题
postdata['answer' + answer] = answers[answer]
postdata['answertype' + answer] = 1
answerwqbid.append(answer)
else:
// ...

answerwqbid.append("")
postdata["answerwqbid"] = ",".join(answerwqbid)

多选题实际上还会有一个answercheck的字段,但是它是重复出现的,python的字典是不支持重复键的,只有json支持,而不post这个字段也不影响提交,所以暂时忽略。

最后提交请求即可。注意 Content-Type 应该为 application/json,否则无法识别。

成功示例:

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

This site was deployed by @OasisLee using Stellar.

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