0%

雨课堂网课自动播放

最近又到了一年一度看《形势与政策》网课的时候了,全国高校,每个专业,无一例外。我校本年度(2020年)选用的是雨课堂的平台。大概有15个视频,每个视频时间20-40分钟不等,观看视频占总分的90%,这些视频全部看一遍,就算开二倍速也要很久,于是自然想到了用自动化脚本自动播放。


网站架构分析

课程地址:https://scut.yuketang.cn/pro/courselist

雨课堂

界面

其中第一排第一列的课程就是本学期的《形势与政策》课程。因为我只有这一门课程需要观看,所以我直接点进去,不需要在这个页面获取其他视频的信息。

点击进入《形势与政策》之后:

网址:https://scut.yuketang.cn/pro/lms/7GmGPsM8ci5/4449749/studycontent

课程内容

课程内容

这里的url很明显,“7GmGPsM8ci5”和“4449749”都是一些比较明显的参数,但是目前还不知道代表着什么。
这时我们进入控制台,试图寻找一些有关每个需要观看的视频的参数和规律等等(其实也没有套路,就是经验,具体要找什么也只有试过才知道)。

经过在Network中加载的页面的寻找,我们很容易的能够发现这些信息:

  • 用户id

    屏幕截图1

    https://scut.yuketang.cn/edu_admin/get_user_basic_info/?term=latest&uv_id=2627

  • 章节id

    在后期模拟播放时没有用到,所以就不展示了。

  • 学校id

    学校id会在用户登陆后清晰的显示在url参数上,用户可以直接获取,所以也不需要抓取。

  • 教室id

    教室id会在用户登陆并选择课程后清晰的显示在url参数上,用户也可以直接获取,故也不需要特意去抓取。

  • 视频id

    屏幕截图2

https://scut.yuketang.cn/mooc-api/v1/lms/learn/course/chapter?cid=4449749&sign=7GmGPsM8ci5&term=latest&uv_id=2627

在上面这个网址保存了所有的视频信息,视频名称,视频编号等,具体如下:

屏幕截图3.png

最主要的信息就是id,我是通过率先点开每个视频观察了url的变化,然后再对比发现的,例如我点开第一个视频,网址变为:
https://scut.yuketang.cn/pro/lms/7GmGPsM8ci5/4449749/video/4061816,这里面的参数为教室id+视频id,这样我们就可以将每个视频的id爬取,为了之后的模拟播放做准备。


视频id和用户id抓取

下面给出爬取视频id、用户id等信息的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import requests
import json
import time
import cv2

classroomid = '4449749' # 教室id
sign = '7GmGPsM8ci5'
term = 'latest'
universityid = '2627' # 学校id

headers1 = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
'cookie': '', # 安全起见我就不给出我的cookie了
'referer': 'https://scut.yuketang.cn/pro/lms/7GmGPsM8ci5/%s/studycontent' % classroomid,
'university-id': universityid,
'xtbz': 'cloud'
}


def get_videoid(crid):
"""
获取需要观看视频的编号
:param crid: 教室id
:return: 储存着需要观看视频编号的列表
"""
chap_url = 'https://scut.yuketang.cn/mooc-api/v1/lms/learn/course/' \
'chapter?cid=%s&sign=%s&term=%s&uv_id=%s' % (crid, sign, term, universityid)
response = requests.get(chap_url, headers=headers1)
response.encoding = response.apparent_encoding
jsdata = json.loads(response.text)
cid = jsdata['data']['course_id']
course_chapters = jsdata['data']['course_chapter']
vid = []
for chap in course_chapters:
for sec in chap['section_leaf_list']:
if sec['name'] != '章节测试题':
vid.append(sec['id'])
return list(map(lambda x: str(x), vid)), cid


def get_videourl(vid):
"""
获取视频url函数
:param vid: 视频id列表
:return: 视频url列表
"""
url_head = 'https://scut.yuketang.cn/pro/lms/%s/%s/video/' % (sign, classroomid)
vurl = list(map(lambda x: url_head+x, vid))
return vurl


def get_userinfo():
user_url = 'https://scut.yuketang.cn/edu_admin/get_user_basic_info/?term=latest&uv_id=%s' % universityid
response = requests.get(user_url, headers=headers1)
print('用户姓名:', json.loads(response.text)['data']['user_info']['name'])
userid = json.loads(response.text)['data']['user_info']['user_id']
return userid


userid = get_userinfo()
courseid = get_videoid(classroomid)[1]
video_id = get_videoid(classroomid)[0]
video_url = get_videourl(video_id)

写get_videourl()这个函数主要是因为,访问具体的视频页面在请求头部的referer参数需要对应这个视频网页地址的值。

那么我现在就可以模拟访问每一个视频了。


模拟观看

接下来就是重点了,我现在只是模拟地拿到了每个视频的入口,但是我该如何才能使我的程序自动地播放视频呢?其实熟悉爬虫的朋友们一定对selenium不陌生,这是一个实现网络测试自动化的工具,可以轻松地模拟鼠标的点击,那么自然得也就可以模拟点击播放按钮,然后播放视频,如果能做成多线程,然后再模拟点击二倍速播放,那么就可以在20分钟左右看完所有视频(最长视频时长的1/2)。但是,selenium也有一些不足的地方:

  1. 容易被识别,很多网站都针对selenium做了反爬虫措施,所以在使用过程中会出现很多小问题(例如前几年常见的自动清空购物车脚本,自动抢货脚本等,现在基本已经被淘宝京东等网站检测到了)。
  2. 需要安装特定的浏览器驱动,且爬虫速度极慢。
  3. 缺乏技术含量(😜)。

基于最后一点(以上三点),我基本不使用selenium来进行爬虫。

那么我就要仔细思考一下,服务器到底是如何知道浏览器看了多少进度的视频的(计算机网络知识在脑中打转)。基于经验和资料查找(咨询苏森),以及我人工手动播放了一个视频,并进行了一些操作,如暂停,开始,刷新页面,观看结束等等,我大致有了如下的总结:

浏览器通过POST请求给服务器发送心跳包(heartbeat/),心跳包有对应的类型(‘pause’,‘loadstart’,‘heartbeat’,‘videoend’等等),每个心跳又包含了很多参数,其中便有代表着视频已经播放时间的参数,浏览器不断地给服务器发送心跳包,这就对应着播放视频时进度条的移动,那么我可以模仿整个心跳包的发送过程,以此来”欺骗“服务器我是一个普通浏览器,并且真正地看了视频。

那么先来看看心跳包长什么样:

网址:https://scut.yuketang.cn/video-log/heartbeat/

心跳包1

这个需要传给POST请求的data参数的部分是常见的json格式,键为”heart_data“,值为一个列表,列表里面有若干个字典形式的心跳包(上图一个列表里有7个心跳包)。

那么接下来看一下每个心跳包具体有哪些内容:

心跳包2

接下来我来介绍一下经过观察和实验,每个参数的含义:

参数 含义
c 课程id(course id)
cc 每个视频的特定参数,在我之前的get_videoid()函数中返回的第二个值即为每个视频的cc值
classroomid 顾名思义,教室id
cp 视频进度,这是最关键的参数
d 总时长,也是极其关键的参数
et 心跳包类型,指明这个心跳包的作用
fp 不太清楚,固定设为0
i 固定设置为5
lob 固定设置为”cloud4“
n 固定设置为”cc“
p 固定设置为”web“
pg 前半段为视频id,后半段为一个下划线+随机字符串(可以只采用一种)
skuid get_videoinfo()函数返回的第一个值即为skuid,每个视频相同
sp 控制倍速,可选0.5,1, 1.25,1.5,1.75,2等值,不重要,设置为1
sq 心跳包的序号,顺序加1
t 固定设置为”video“
tp 缓存你上一次看的视频进度,不重要,可以每次都设置为0
ts 1000倍的时间戳,用python的time.time()获取然后乘1000即可
u 用户id(user_id)
uip 固定设置为”“
v 视频id(video_id)

粗体的参数是比较重要的,需要我们调整。这样下来,即使没用完全清楚每一个参数的含义(比如为什么要设为固定的某个值),依然不影响我模拟浏览器观看视频时的发包。接下来就要观察观看视频发送心跳包的流程了。

心跳包3

发包流程

这是我观看视频一段时间的Network发包结果,可以看到,名字为heartbeat/的包就是心跳包,而紧接着的请求是用来查询观看进度,服务器的响应包含了观看进度、观看历史、总时长等信息。而经过观察,每隔30s发送一个heartbeat/请求,而且heartbeat的请求类别从点进视频到播放结束分别为:

  1. 空包

  2. loadstart

  3. seeking

  4. loadeddata

  5. play

  6. playing

  7. heartbeat

    这时心跳包里的cp参数开始增加,间隔为5s(即每过5s增加一个heartbeat类型的字典,加进列表中),当列表中的元素达到某个阈值,POST发送请求(阈值没有规律,我这里设置为10)。

  8. pause

  9. videoend

下面是描述发包流程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# 心跳包post请求网址
heartbeat_url = 'https://scut.yuketang.cn/video-log/heartbeat/'

length = get_videolen(id)
sku_id = get_videoinfo(id)[0]
cc = get_videoinfo(id)[1]

hb_list = []
# 心跳包模板
template = {
'i': 5,
'et': '',
'p': 'web',
'n': 'cc',
'lob': 'cloud4',
'cp': 0, # 观看进度
'fp': 0,
'tp': 0,
'sp': 1,
'ts': int(time.time() * 1000),
'u': userid, # 用户id
'uip': '',
'c': courseid, # 课程id
'v': id, # 视频id
'skuid': sku_id, # skuid
'classroomid': classroomid, # 教室id
'cc': cc,
'd': length, # 视频时长
'pg': id + '_12bvp',
'sq': 0, # 心跳包序列
't': 'video'
}

# 先发送一次空包
requests.post(heartbeat_url, headers=headers2, data=json.dumps({"heart_data": []}))

# loadstart
ls = template.copy()
ls['et'] = 'loadstart'
ls['d'] = 0
ls['sq'] = 1
hb_list.append(ls)

# seeking
sk = template.copy()
sk['et'] = 'seeking'
sk['sq'] = 2
hb_list.append(sk)

# loadeddata
ld = template.copy()
ld['et'] = 'loadeddata'
ld['sq'] = 3
hb_list.append(ld)

# play
play = template.copy()
play['et'] = 'play'
play['sq'] = 4
hb_list.append(play)

# playing
playing = template.copy()
playing['et'] = 'playing'
playing['sq'] = 5
hb_list.append(playing)

sq = 6
# heartbeat
for i in range(0, length, 5):
hb = template.copy()
hb['et'] = 'heartbeat'
hb['cp'] = i
hb['sq'] = sq
hb_list.append(hb)
sq += 1
if len(hb_list) == 10:
hb_data = {'heart_data': hb_list}
send = requests.post(heartbeat_url, headers=headers2, data=json.dumps(hb_data))
# print(send.status_code)
try:
rate = requests.get('https://scut.yuketang.cn/'
'video-log/get_video_watch'
'_progress/?cid=%s&'
'user_id=%s&classroom_id=%s&'
'video_type=video&vtype=rate&'
'video_id=%s&snapshot=1&term=latest&uv_id=%s'
% (str(courseid), str(userid), classroomid, id, universityid), headers=headers2)
rate = json.loads(rate.text)
print('观看进度:', rate[id]['rate'])
# print(hb_list)
except:
pass
hb_list.clear()
time.sleep(1)

# 再发最后一个heartbeat
hb = template.copy()
sq += 1
hb['et'] = 'heartbeat'
hb['cp'] = length
hb['sq'] = sq
hb_list.append(hb)

# pause
pause = template.copy()
sq += 1
pause['et'] = 'pause'
pause['cp'] = length
pause['sq'] = sq
hb_list.append(pause)

# videoend
ve = template.copy()
sq += 1
ve['et'] = 'videoend'
ve['cp'] = length
ve['sq'] = sq
hb_list.append(ve)

requests.post(heartbeat_url, headers=headers2, data=json.dumps({'heart_data': hb_list}))

rate = requests.get('https://scut.yuketang.cn/video-log/get_video_watch_progress'
'/?cid=%s&user_id=%s&classroom_id=%s&video_typ'
'e=video&vtype=rate&video_id=%s&snapshot=1&term=latest&uv_id=%s'
% (str(courseid), str(userid), classroomid, id, universityid),
headers=headers2)
rate.encoding = rate.apparent_encoding
print('是否完成观看:', json.loads(rate.text)[id]['completed'])

这时发包流程结束,视频看完。


一些问题

  • 发包间隔时间

因为正常观看是30s发送一次heartbeat请求,但是这样也很耗费时间,我尝试着不停歇,不断地发送,会出现一些丢包的现象,但是整体没有问题,可以适当停顿1s-5s,以提高发包的成功率。

  • 视频总时长

前面提到过,在每个heartbeat/请求后回立刻有一个请求进度的GET请求,这个请求的响应中服务器会返回观看的进度和视频的总时长等信息,一开始我是直接发送这个请求来获得每个视频的时长,但是发现必须是已经有观看记录的视频才可以请求成功,若是未观看的视频则无法请求,除此之外,并没有包含着视频时长的信息。所以这里只能借助cv2模块中的VideoCapture类中的get函数的提取帧速率和帧数的功能,通过请求视频的原始地址结合length = FrameNumber / FramePerSecond来计算视频的时长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 获取视频时长
def get_videolen(vid):
ask_video_url = 'https://scut.yuketang.cn/api/open/audiovideo/' \
'playurl?_date=%s&term=latest&video_id=%s&' \
'provider=cc&file_type=1&' \
'is_single=0' % (str(int(time.time()*1000)), get_videoinfo(vid)[1])
origin_url = requests.get(ask_video_url, headers=headers2)
origin_url.encoding = origin_url.apparent_encoding
try:
origin_url = json.loads(origin_url.text)['data']['playurl']['sources']['quality10'][0]
except:
origin_url = json.loads(origin_url.text)['data']['playurl']['sources']['quality20'][0]
# 计算时长
cap = cv2.VideoCapture(origin_url)
if cap.isOpened():
fps = cap.get(5)
framenum = cap.get(7)
duration = int(framenum / fps)
return duration
else:
return 0
  • 多线程

本次并未采用多线程,之后会相应的做一些优化。

  • 心跳包的请求头

一开始心跳包的POST请求不起作用,经过反复debug,检查到返回的请求状态码为400多,故猜想是请求头的问题,果然是需要在请求头部加上'content-type': 'application/json‘的键值对。

  • 答题

刚刚说了,90%的成绩由播放视频产生,还有剩下的10%是由答题产生的,其实和播放视频同理,只要有标准答案,答题也可以以相同的方式发送请求,完成自动答题,这也是以后要完善的一个部分。


写在最后

其实github上有一个郑州大学的师兄写了有关雨课堂的视频自动播放,但是用的是selenium模块,在网上和github上还未找见直接用requests模拟发送请求的自动观看雨课堂的项目。所以这个项目做出来还算有一些成就,然后也给几个同学用了(当然主要是帮美丽可爱的小姐姐😋锻炼自己)。希望自己可以继续完善,做成体系。


代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import requests
import json
import time
import cv2


classroomid = '4449749'
sign = '7GmGPsM8ci5'
term = 'latest'
universityid = '2627'


headers1 = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.'
'0.4240.111 Safari/537.36',
'cookie': '',
'referer': 'https://scut.yuketang.cn/pro/lms/7GmGPsM8ci5/%s/studycontent' % classroomid,
'university-id': universityid,
'xtbz': 'cloud'
}


def get_videoid(crid):
"""
获取需要观看视频的编号
:param crid: 教室id
:return: 储存着需要观看视频编号的列表
"""
chap_url = 'https://scut.yuketang.cn/mooc-api/v1/lms/learn/course/' \
'chapter?cid=%s&sign=%s&term=%s&uv_id=%s' % (crid, sign, term, universityid)
response = requests.get(chap_url, headers=headers1)
response.encoding = response.apparent_encoding
jsdata = json.loads(response.text)
cid = jsdata['data']['course_id']
course_chapters = jsdata['data']['course_chapter']
vid = []
for chap in course_chapters:
for sec in chap['section_leaf_list']:
if sec['name'] != '章节测试题':
vid.append(sec['id'])
return list(map(lambda x: str(x), vid)), cid


def get_videourl(vid):
"""
获取视频url函数
:param vid: 视频id列表
:return: 视频url列表
"""
url_head = 'https://scut.yuketang.cn/pro/lms/%s/%s/video/' % (sign, classroomid)
vurl = list(map(lambda x: url_head+x, vid))
return vurl


def get_userinfo():
user_url = 'https://scut.yuketang.cn/edu_admin/get_user_basic_info/?term=latest&uv_id=%s' % universityid
response = requests.get(user_url, headers=headers1)
print('用户姓名:', json.loads(response.text)['data']['user_info']['name'])
userid = json.loads(response.text)['data']['user_info']['user_id']
return userid


userid = get_userinfo()
courseid = get_videoid(classroomid)[1]
video_id = get_videoid(classroomid)[0]
video_url = get_videourl(video_id)

for id in video_id:
print('正在观看视频id:', id)
headers2 = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
'cookie': '',
'origin': 'https://scut.yuketang.cn',
'referer': video_url[video_id.index(id)],
'xtbz': 'cloud',
'content-type': 'application/json'
}

# 获取视频时长
def get_videolen(vid):
ask_video_url = 'https://scut.yuketang.cn/api/open/audiovideo/' \
'playurl?_date=%s&term=latest&video_id=%s&' \
'provider=cc&file_type=1&' \
'is_single=0' % (str(int(time.time()*1000)), get_videoinfo(vid)[1])
origin_url = requests.get(ask_video_url, headers=headers2)
origin_url.encoding = origin_url.apparent_encoding
try:
origin_url = json.loads(origin_url.text)['data']['playurl']['sources']['quality10'][0]
except:
origin_url = json.loads(origin_url.text)['data']['playurl']['sources']['quality20'][0]
# 计算时长
cap = cv2.VideoCapture(origin_url)
if cap.isOpened():
fps = cap.get(5)
framenum = cap.get(7)
duration = int(framenum / fps)
return duration
else:
return 0

# 获取视频信息
def get_videoinfo(vid):
"""
获取视频信息函数
:param vid: 视频id
:return: 第一个参数为sku_id(type->int),第二个参数为cc(type->str)
"""
info_url = 'https://scut.yuketang.cn/mooc-api/v1/lms/learn/leaf_info/' \
'%s/%s/?sign=7GmGPsM8ci5&term=latest&uv_id=%s' % (classroomid, vid, universityid)
info = requests.get(info_url, headers=headers2)
info.encoding = info.apparent_encoding
info = json.loads(info.text)['data']
return info['sku_id'], info['content_info']['media']['ccid']

# 心跳包post请求网址
heartbeat_url = 'https://scut.yuketang.cn/video-log/heartbeat/'

length = get_videolen(id)
sku_id = get_videoinfo(id)[0]
cc = get_videoinfo(id)[1]

hb_list = []
# 心跳包模板
template = {
'i': 5,
'et': '',
'p': 'web',
'n': 'cc',
'lob': 'cloud4',
'cp': 0, # 观看进度
'fp': 0,
'tp': 0,
'sp': 1,
'ts': int(time.time() * 1000),
'u': userid, # 用户id
'uip': '',
'c': courseid, # 课程id
'v': id, # 视频id
'skuid': sku_id, # skuid
'classroomid': classroomid, # 教室id
'cc': cc,
'd': length, # 视频时长
'pg': id + '_12bvp',
'sq': 0, # 心跳包序列
't': 'video'
}

# 先发送一次空包
requests.post(heartbeat_url, headers=headers2, data=json.dumps({"heart_data": []}))

# loadstart
ls = template.copy()
ls['et'] = 'loadstart'
ls['d'] = 0
ls['sq'] = 1
hb_list.append(ls)

# seeking
sk = template.copy()
sk['et'] = 'seeking'
sk['sq'] = 2
hb_list.append(sk)

# loadeddata
ld = template.copy()
ld['et'] = 'loadeddata'
ld['sq'] = 3
hb_list.append(ld)

# play
play = template.copy()
play['et'] = 'play'
play['sq'] = 4
hb_list.append(play)

# playing
playing = template.copy()
playing['et'] = 'playing'
playing['sq'] = 5
hb_list.append(playing)

sq = 6
# heartbeat
for i in range(0, length, 5):
hb = template.copy()
hb['et'] = 'heartbeat'
hb['cp'] = i
hb['sq'] = sq
hb_list.append(hb)
sq += 1
if len(hb_list) == 10:
hb_data = {'heart_data': hb_list}
send = requests.post(heartbeat_url, headers=headers2, data=json.dumps(hb_data))
# print(send.status_code)
try:
rate = requests.get('https://scut.yuketang.cn/'
'video-log/get_video_watch'
'_progress/?cid=%s&'
'user_id=%s&classroom_id=%s&'
'video_type=video&vtype=rate&'
'video_id=%s&snapshot=1&term=latest&uv_id=%s'
% (str(courseid), str(userid), classroomid, id, universityid), headers=headers2)
rate = json.loads(rate.text)
print('观看进度:', rate[id]['rate'])
# print(hb_list)
except:
pass
hb_list.clear()
time.sleep(1)

# 再发最后一个heartbeat
hb = template.copy()
sq += 1
hb['et'] = 'heartbeat'
hb['cp'] = length
hb['sq'] = sq
hb_list.append(hb)

# pause
pause = template.copy()
sq += 1
pause['et'] = 'pause'
pause['cp'] = length
pause['sq'] = sq
hb_list.append(pause)

# videoend
ve = template.copy()
sq += 1
ve['et'] = 'videoend'
ve['cp'] = length
ve['sq'] = sq
hb_list.append(ve)

requests.post(heartbeat_url, headers=headers2, data=json.dumps({'heart_data': hb_list}))

rate = requests.get('https://scut.yuketang.cn/video-log/get_video_watch_progress'
'/?cid=%s&user_id=%s&classroom_id=%s&video_typ'
'e=video&vtype=rate&video_id=%s&snapshot=1&term=latest&uv_id=%s'
% (str(courseid), str(userid), classroomid, id, universityid),
headers=headers2)
rate.encoding = rate.apparent_encoding
print('是否完成观看:', json.loads(rate.text)[id]['completed'])