近期完成了一个名为 YuketangAutoplay 的项目,旨在通过 Python 的网络编程技术,实现了自动观看雨课堂网课的功能。但请注意几点:

  • 敬重学校/平台规则: 该项目仅供参考学习,强烈鼓励大家遵循学校规则和道德准则。遵循平台规章制度,维护平台生态的健康。
  • 仅限学习和探索: 该项目仅用于学术和技术探索目的,并不鼓励将这种自动化技术用于不正当用途,或者侵犯他人的权益。
  • 尊重创作和知识产权:项目不侵犯任何人的创作和知识产权。看过或使用过的第三方资源包括:

雨课堂网课自动播放

https://github.com/EricZhang615/AutoYuketang

https://github.com/f2quantum/AutoYuketangforHIT

我相信通过透明地分享和交流,我们可以共同促进技术的良性发展,并在遵循法律和伦理原则的前提下,充分发挥创造力。谢谢您的理解和支持!

课程地址:https://yjsbjtu.yuketang.cn/pro/lms/AYjcVbrktcA/16902117/studycontent

  • https://协议部分,表明链接使用的是HTTPS安全协议进行通信。
  • yjsbjtu.yuketang.cn域名部分,指向了一个网站或服务的主机名。
  • /pro/lms/AYjcVbrktcA/16902117/studycontent路径部分,指示了网站上的具体位置。可能的含义如下:
    • /pro产品(Product)
    • /lms学习管理系统(Learning Management System),表明这是与学习和课程管理相关的内容。
    • /AYjcVbrktcA:用于身份验证或授权的签名或标识符。
    • /16902117:教室的标识符。
    • /studycontent:学习内容或课程资料的部分。

相关id获取

用户 id

https://yjsbjtu.yuketang.cn/edu_admin/get_user_basic_info/?term=latest&uv_id=3033

学校id

university_id

教室id

classroom_id

视频id && 课程id

https://yjsbjtu.yuketang.cn/mooc-api/v1/lms/learn/course/chapter?cid=16902117&sign=AYjcVbrktcA&term=latest&uv_id=3033

举例

https://yjsbjtu.yuketang.cn/pro/lms/AYjcVbrktcA/16902117/video/36712962

获取资源入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import json

university_id = '3033' # 学校id
classroom_id = '16902117' # 教室id
sign = 'AYjcVbrktcA' # 身份验证或授权的签名或标识符
term = 'latest' # 最近学期

headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'cookie': '', # 自补充
'referer': f'https://yjsbjtu.yuketang.cn/pro/lms/{sign}/{classroom_id}/studycontent',
'university-id': university_id,
'xtbz': 'cloud'
}

base_url = 'https://yjsbjtu.yuketang.cn'

获取用户 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
def get_user_id():
"""
获取用户ID并存储
:return: 用户ID
"""
# https://yjsbjtu.yuketang.cn/edu_admin/get_user_basic_info/?term=latest&uv_id=3033
user_url = f'{base_url}/edu_admin/get_user_basic_info/?term={term}&uv_id={university_id}'
response = requests.get(user_url, headers=headers)
response.encoding = response.apparent_encoding
data = json.loads(response.text)['data']
user_info = data['user_info']
user_id = user_info['user_id']
return user_id

获取视频 IDS 和课程 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_video_ids_and_course_id(classroom_id):
"""
获取需要观看课程 ID 和视频的 ID 集合
:param classroom_id: 教室id
:return: 课程ID,视频 ID 集合
"""
# https://yjsbjtu.yuketang.cn/mooc-api/v1/lms/learn/course/chapter?cid=16902117&sign=AYjcVbrktcA&term=latest&uv_id=3033
chapter_url = f'{base_url}/mooc-api/v1/lms/learn/course/chapter?cid={classroom_id}&sign={sign}&term={term}&uv_id={university_id}'
response = requests.get(chapter_url, headers=headers)
response.encoding = response.apparent_encoding
data = json.loads(response.text)['data']
course_id = data['course_id']
video_ids = [section_leaf['id']
for chapter in data['course_chapter']
for section_leaf in chapter['section_leaf_list']
]
return course_id, video_ids

视频 URL 列表

1
2
3
4
5
6
7
8
9
10
def get_video_urls(video_ids):
"""
获取视频url列表
:param video_ids: 视频id列表
:return: 视频url列表
"""
# https://yjsbjtu.yuketang.cn/pro/lms/AYjcVbrktcA/16902117/video/
url_head = f'{base_url}/pro/lms/{sign}/{classroom_id}/video/'
video_urls = [f'{url_head}{vid}' for vid in video_ids]
return video_urls

结果打印

1
2
3
4
5
6
7
8
9
10
11
user_id = get_user_id()  # 获取用户 ID

course_id, video_ids = get_video_ids_and_course_id(classroom_id) # 获取视频 IDS 和课程ID

video_urls = get_video_urls(video_ids) # 获取视频 URL列表

print(f'user id:{user_id}')
print(f'course id:{course_id}')
print(f'video ids:{video_ids}')
for i, video_url in enumerate(video_urls):
print(f'video ids[{i + 1}]:{video_url}')

模拟观看

心跳包(Heartbeat)是计算机网络中的一个术语,它是指在通信过程中周期性地发送的小型数据包,用于维持通信连接的状态和活性。心跳包的主要目的是检测通信链路是否仍然可用,以及确保连接没有断开。心跳包通常由一个设备(通常是客户端)定期地向另一个设备(通常是服务器)发送。服务器在接收到心跳包后,可以回复一个确认响应,表示连接是正常的。如果一段时间内没有收到心跳包或确认响应,系统可以判定连接已经断开,并采取相应的处理措施,比如重新连接、重新启动等。

heart_data: 这是一个列表,包含了多个心跳包事件。

单个心跳包,以第三个heartbeat为例:

参数 含义
c 课程ID 4163351
cards_ed 固定设置为0 0
cc 每个视频的特定参数 EB431BB3CA5558EDFC9558351D509E7C
classroomid 教室ID 16902117
cp 当前播放时长 885.3
d 总时长 1197.1
et 心跳包类型 “heartbeat”
fp 视频起始播放位置 0
i 固定设置为5 5
lob 固定设置为”cloud4“ “cloud4”
n 固定设置为”ali-cdn.xuetangx.com ali-cdn.xuetangx.com
p 固定设置为”web“ “web”
pg 视频id_随机字符串 “36712962_ro1f”
skuild SKU ID 7978195
slide 固定设置为0 0
sp 播放速度 1
sq 心跳包序列号 13
t 固定设置为”video“ “video”
tp 上一次看视频的播放位置 857.8
ts 时间戳,标识事件发生的时间戳 “1691804250921”
u 用户ID 68288550
uip 固定设置为”“ “”
v 视频ID 36712962
v_url 固定设置为”“ “”

流程如下:

  1. loadstart --> seeking --> loadeddata --> play --> playing --> heartbeat --> … —> heartbeat (size(heartbeat)=10) [发送]
  2. –> heartbeat --> heartbeat --> … —> heartbeat (size(heartdata)=10) [发送]
  3. –> heartbeat --> heartbeat --> … —> heartbeat (size(heartdata)=10) [发送]
  4. –> heartbeat --> heartbeat --> … —> heartbeat (size(heartdata)=10) [发送]
  5. –> pause --> 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
def get_video_info(video_id):
'''
获取视频部分信息
:param video_id: 视频 ID
:return: 包含视频 SKU ID, ccid 和视频名称的元组
'''
video_info_url = urls.get('video_info_url').format(base_url=base_url,
classroom_id=classroom_id,
video_id=video_id,
sign=sign, term=term,
university_id=university_id)
video_info = requests.get(video_info_url, headers=headers)
video_info.encoding = video_info.apparent_encoding
data = json.loads(video_info.text)['data']
sku_id = data['sku_id']
cc = data['content_info']['media']['ccid']
video_name = data['name']
return sku_id, cc, video_name


def get_video_len(video_id):
'''
获取视频长度
:param video_id: 视频 ID
:return: 视频时长(以秒为单位)
'''
video_play_url = urls.get('video_play_url').format(base_url=base_url,
_date=str(int(time.time() * 1000)),
video_id=get_video_info(video_id)[1])
video_play = requests.get(video_play_url, headers=headers)
video_play.encoding = video_play.apparent_encoding
try:
video_play = json.loads(video_play.text)['data']['playurl']['sources']['quality10'][0]
except:
video_play = json.loads(video_play.text)['data']['playurl']['sources']['quality20'][0]
cap = cv2.VideoCapture(video_play)
if cap.isOpened():
fps = cap.get(5)
framenum = cap.get(7)
duration = int(framenum / fps)
return duration
else:
return 0

心跳包

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
# 心跳包模板,包含通用的信息
template = {
'c': course_id, # 课程ID
'cards_ed': 0, # 固定设置为0
'cc': cc, # 每个视频的特定参数
'classroomid': classroom_id, # 教室ID
'cp': 0, # 当前播放时长
'd': d, # 总时长
'et': '', # 心跳包类型
'fp': 0, # 视频起始播放位置
'i': 5, # 固定设置为5
'lob': 'cloud4', # 固定设置为”cloud4“
'n': 'ali-cdn.xuetangx.com', # 固定设置为”ali-cdn.xuetangx.com“
'p': 'web', # 固定设置为”web“
'pg': video_id + '_ro1f', # 视频id_随机字符串
'skuid': sku_id, # SKU ID
'slide': 0, # 固定设置为0
'sp': 1, # 播放速度
'sq': 0, # 心跳包序列号
't': 'video', # 固定设置为”video“
'tp': 0, # 上一次看视频的播放位置
'ts': int(time.time() * 1000), # 时间戳,标识事件发生的时间戳
'u': user_id, # 用户ID
'uip': '', # 固定设置为”“
'v': video_id, # 视频ID
'v_url': '' # 固定设置为”“
}

过程

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
rate_url = urls.get('rate_url').format(base_url=base_url,
course_id=course_id,
user_id=user_id,
classroom_id=classroom_id,
video_id=video_id,
university_id=university_id)
while True:
rate = requests.get(rate_url, headers=headers)
rate.encoding = rate.apparent_encoding
rate = json.loads(rate.text)[video_id]['completed']
if rate == 1:
print(f'视频<{video_name}>已看完')
break
else:
# 1. 发送空的心跳包
requests.post(heartbeat_url, headers=headers, data=json.dumps({"heart_data": []}))

# 2. 构建新的心跳包
heart_data = []
# 2.1 添加初始心跳包数据
for etype, sq in [('loadstart', 1), ('seeking', 2), ('loadeddata', 3), ('play', 4), ('playing', 5)]:
data = template.copy()
data['et'] = etype
data['sq'] = sq
heart_data.append(data)
sq = 6
# 2.2 生成心跳包序列并发送
for i in range(0, d, 5):
# 2.2.1 迭代生成心跳包
hb = template.copy()
hb['et'] = 'heartbeat'
hb['cp'] = i
hb['sq'] = sq
heart_data.append(hb)
sq += 1
if len(heart_data) == 10: # 累积了一定数量的心跳包(10)
# 2.2.2 发送心跳包
send = requests.post(heartbeat_url, headers=headers, data=json.dumps({'heart_data': heart_data}))
try:
rate = requests.get(rate_url, headers=headers)
rate = json.loads(rate.text)
percentage = float(rate[video_id]['rate']) * 100
print('{} 观看进度:{.2f}%'.format(video_name, percentage))
except:
pass
# 2.2.3 清空心跳包列表
heart_data.clear()
time.sleep(1)
# 3. 添加结束心跳包数据
for etype in ['heartbeat', 'pause', 'videoend']:
data = template.copy()
data['et'] = etype
data['cp'] = d
data['sq'] = sq
heart_data.append(data)
requests.post(heartbeat_url, headers=headers, data=json.dumps({'heart_data': heart_data}))

结果

已经运行完的结果,后面还是重新看了一遍~😁(温故而知新)

源代码地址

👉项目地址