├── AES.py
├── LICENSE
├── README.md
├── cookies.txt
├── id.txt
├── 教程.md
└── 知到2.1.py
/AES.py:
--------------------------------------------------------------------------------
1 | from Crypto.Cipher import AES
2 | import base64
3 | import binascii
4 |
5 |
6 | # 数据类
7 | class MData():
8 | def __init__(self, data=b"", characterSet='utf-8'):
9 | # data肯定为bytes
10 | self.data = data
11 | self.characterSet = characterSet
12 |
13 | def saveData(self, FileName):
14 | with open(FileName, 'wb') as f:
15 | f.write(self.data)
16 |
17 | def fromString(self, data):
18 | self.data = data.encode(self.characterSet)
19 | return self.data
20 |
21 | def fromBase64(self, data):
22 | self.data = base64.b64decode(data.encode(self.characterSet))
23 | return self.data
24 |
25 | def fromHexStr(self, data):
26 | self.data = binascii.a2b_hex(data)
27 | return self.data
28 |
29 | def toString(self):
30 | return self.data.decode(self.characterSet)
31 |
32 | def toBase64(self):
33 | return base64.b64encode(self.data).decode()
34 |
35 | def toHexStr(self):
36 | return binascii.b2a_hex(self.data).decode()
37 |
38 | def toBytes(self):
39 | return self.data
40 |
41 | def __str__(self):
42 | try:
43 | return self.toString()
44 | except Exception:
45 | return self.toBase64()
46 |
47 |
48 | ### 封装类
49 | class AEScryptor():
50 | def __init__(self, key, mode, iv='', paddingMode="NoPadding", characterSet="utf-8"):
51 | '''
52 | 构建一个AES对象
53 | key: 秘钥,字节型数据
54 | mode: 使用模式,只提供两种,AES.MODE_CBC, AES.MODE_ECB
55 | iv: iv偏移量,字节型数据
56 | paddingMode: 填充模式,默认为NoPadding, 可选NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
57 | characterSet: 字符集编码
58 | '''
59 | self.key = key
60 | self.mode = mode
61 | self.iv = iv
62 | self.characterSet = characterSet
63 | self.paddingMode = paddingMode
64 | self.data = ""
65 |
66 | def __ZeroPadding(self, data):
67 | data += b'\x00'
68 | while len(data) % 16 != 0:
69 | data += b'\x00'
70 | return data
71 |
72 | def __StripZeroPadding(self, data):
73 | data = data[:-1]
74 | while len(data) % 16 != 0:
75 | data = data.rstrip(b'\x00')
76 | if data[-1] != b"\x00":
77 | break
78 | return data
79 |
80 | def __PKCS5_7Padding(self, data):
81 | needSize = 16 - len(data) % 16
82 | if needSize == 0:
83 | needSize = 16
84 | return data + needSize.to_bytes(1, 'little') * needSize
85 |
86 | def __StripPKCS5_7Padding(self, data):
87 | paddingSize = data[-1]
88 | return data.rstrip(paddingSize.to_bytes(1, 'little'))
89 |
90 | def __paddingData(self, data):
91 | if self.paddingMode == "NoPadding":
92 | if len(data) % 16 == 0:
93 | return data
94 | else:
95 | return self.__ZeroPadding(data)
96 | elif self.paddingMode == "ZeroPadding":
97 | return self.__ZeroPadding(data)
98 | elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
99 | return self.__PKCS5_7Padding(data)
100 | else:
101 | print("不支持Padding")
102 |
103 | def __stripPaddingData(self, data):
104 | if self.paddingMode == "NoPadding":
105 | return self.__StripZeroPadding(data)
106 | elif self.paddingMode == "ZeroPadding":
107 | return self.__StripZeroPadding(data)
108 |
109 | elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
110 | return self.__StripPKCS5_7Padding(data)
111 | else:
112 | print("不支持Padding")
113 |
114 | def setCharacterSet(self, characterSet):
115 | '''
116 | 设置字符集编码
117 | characterSet: 字符集编码
118 | '''
119 | self.characterSet = characterSet
120 |
121 | def setPaddingMode(self, mode):
122 | '''
123 | 设置填充模式
124 | mode: 可选NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
125 | '''
126 | self.paddingMode = mode
127 |
128 | def decryptFromBase64(self, entext):
129 | '''
130 | 从base64编码字符串编码进行AES解密
131 | entext: 数据类型str
132 | '''
133 | mData = MData(characterSet=self.characterSet)
134 | self.data = mData.fromBase64(entext)
135 | return self.__decrypt()
136 |
137 | def decryptFromHexStr(self, entext):
138 | '''
139 | 从hexstr编码字符串编码进行AES解密
140 | entext: 数据类型str
141 | '''
142 | mData = MData(characterSet=self.characterSet)
143 | self.data = mData.fromHexStr(entext)
144 | return self.__decrypt()
145 |
146 | def decryptFromString(self, entext):
147 | '''
148 | 从字符串进行AES解密
149 | entext: 数据类型str
150 | '''
151 | mData = MData(characterSet=self.characterSet)
152 | self.data = mData.fromString(entext)
153 | return self.__decrypt()
154 |
155 | def decryptFromBytes(self, entext):
156 | '''
157 | 从二进制进行AES解密
158 | entext: 数据类型bytes
159 | '''
160 | self.data = entext
161 | return self.__decrypt()
162 |
163 | def encryptFromString(self, data):
164 | '''
165 | 对字符串进行AES加密
166 | data: 待加密字符串,数据类型为str
167 | '''
168 | self.data = data.encode(self.characterSet)
169 | return self.__encrypt()
170 |
171 | def __encrypt(self):
172 | if self.mode == AES.MODE_CBC:
173 | aes = AES.new(self.key, self.mode, self.iv)
174 | elif self.mode == AES.MODE_ECB:
175 | aes = AES.new(self.key, self.mode)
176 | else:
177 | print("不支持这种模式")
178 | return
179 |
180 | data = self.__paddingData(self.data)
181 | enData = aes.encrypt(data)
182 | return MData(enData)
183 |
184 | def __decrypt(self):
185 | if self.mode == AES.MODE_CBC:
186 | aes = AES.new(self.key, self.mode, self.iv)
187 | elif self.mode == AES.MODE_ECB:
188 | aes = AES.new(self.key, self.mode)
189 | else:
190 | print("不支持这种模式")
191 | return
192 | data = aes.decrypt(self.data)
193 | mData = MData(self.__stripPaddingData(data), characterSet=self.characterSet)
194 | return mData
195 |
196 |
197 | if __name__ == '__main__':
198 | key = b"qz632524n86i7fk9"
199 | iv = b"1g3qqdh4jvbskb9x"
200 | aes = AEScryptor(key, AES.MODE_CBC, iv, paddingMode="PKCS7Padding", characterSet='utf-8')
201 |
202 | data = '{"recruitAndCourseId":"4b595d5b415b4859454a585958425f455f","dateFormate":1650106384000}'
203 | data = 'ewuM4ytnBaQ9mbm6UutMMA7hOeM4dnbm3QZKtPcD2hamJOTpxk0/KqzeTJj7NHtPlq/TrPIKEwO71rKhC5TgLt/3i+cYGVwB/G+zx+N+OEw1FUCM47mK9UY8RN10XXNe'
204 | rData = aes.encryptFromString(data)
205 | print("密文:", rData.toBase64())
206 | rData = aes.decryptFromBase64(rData.toBase64())
207 | print("明文:", rData)
208 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 稚小白
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 环境Python3.9
2 | #### 我在这说一下关于Cookie值登录失败的问题,需要取播放界面的Cookie值,最好就取/learning/saveDatabaseIntervalTime这个接口的Cookie值
3 | #### 关于课程secret码的问题 请滑到教程.md最下面 都是有说明的,它其实就在播放界面的地址里面
4 | #### 知到请求参数更新了,参数用了AES加密,且移除了用户id这个参数,将这个参数添加到了cookies里面,所以现在只用session已经登不上去了,需要直接贴cookies
5 | #### 目前版本已知运行失败问题:
6 | 当运行时提示{'code': -12, 'message': '需要弹出滑块验证', 'data': None}时,请登录智慧树在视频播放界面进行验证滑块验证后,即可解决此问题
7 |
8 | ## 更新记录
9 | ### 1.0:
10 | 1.基本实现刷课功能
11 | ### 2.0:由用户SzLeaves更新
12 | 1.修复了XHR异常返回值导致的程序中止
13 |
14 | 2.从文件中读取cookies和secret码,修改了默认选项,可以批量刷课。可以从文件里面读取需要的值,cookies从cookies.txt中读取,secret码从id.txt中读取(一行放一个课程的secret码)
15 | ### 2.1:
16 | 1.修复了{'code': -5, 'message': '提交参数错误', 'data': None}报错
17 |
18 | 2.移除了execjs库
19 |
--------------------------------------------------------------------------------
/cookies.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhixiaobai/Python-zhihuishu/210fa25190d56622d1ad264050a7f9d68b76e288/cookies.txt
--------------------------------------------------------------------------------
/id.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhixiaobai/Python-zhihuishu/210fa25190d56622d1ad264050a7f9d68b76e288/id.txt
--------------------------------------------------------------------------------
/教程.md:
--------------------------------------------------------------------------------
1 | # 前言
2 |
3 | 学校让去知到看网课,我寻思这不是折磨兄弟吗?当即我直接破防,然后决定搞一手,从问题根源来解决问题。
4 |
5 |
6 |
7 | 提示:以下是本篇文章正文内容,下面案例可供参考
8 |
9 | # 一、思路分析
10 | 我们先打开F12开发者模式,播放个视频抓下接口看看嗷。
11 | 找到了很多接口,但是最有用的应该还是这个
12 | 
13 | 为什么我要说它最有用了?
14 | 因为看接口名称我们就应该知道,这个接口和视频时长是相关的,做个测试大家就明白了。
15 | 我们打开一个视频,反复点播放和暂停就能看出来。
16 | 
17 | 好了,现在我们找到了接口,下一步我们就来看一下它提交的参数有什么?
18 | 
19 | 可以看到有6个参数,分别是:
20 | watchPoint:
21 | ev:
22 | learingTokenId:
23 | courseId:
24 | uuid:
25 | dateFormate:
26 | 我们现在就只知道最后一个参数是时间戳,其他的一个都不知道是啥,这咋办?
27 | 最好的方法就是直接复制一个参数名称到top文件夹里去搜索,我就用watchPoint测试。
28 | 
29 | 我们点击去,再在js文件里面搜索watchPoint
30 | 
31 | 我们直接找到了我们刚刚看的接口对应的函数,而且这里面就有着watchPoint、ev、learningTokenId、courseId四个参数了,而且还有个时间戳。
32 | 所以现在还就只有个uuid不知道是啥了。
33 | 我们先来看看这四个参数
34 |
35 | ## 1.watchPoint分析
36 | 我们继续在当前js文件里面搜索剩余的watchPoint,最后在一个叫learningTimeRecord的方法里面找到了watchPoint是如何生成的。
37 |
38 | ```javascript
39 | learningTimeRecord: function() {
40 | var t = parseInt(this.totalStudyTime / 5) + 2
41 | , e = null == this.watchPointPost || "" == this.watchPointPost ? "0,1," : this.watchPointPost + ",";
42 | this.watchPointPost = e + t
43 | },
44 | ```
45 | 这里面出现了一个变量totalStudyTime ,我们在当前js文件里面搜索一下看看。
46 | 最后还是在totalStudyTimeFun方法中找到了这个变量
47 |
48 | ```javascript
49 | totalStudyTimeFun: function() {
50 | this.totalStudyTime += 5 * this.playRate,
51 | this.totalTimeFinish += 5 * this.playRate,
52 | this.playTimes += 5 * this.playRate,
53 | this.computeProgree()
54 | },
55 | ```
56 |
57 | 那这个变量到底是啥意思啊?其实在上面的图片中就有了,是当前学习总时间。而this.playRate又是啥了?是当前播放的倍速。我们可以直接默认为1。所以说this.totalStudyTime += 5,并且在最下面可以看到this.totalStudyTime初始是为0的。
58 | 我们现在再回到learningTimeRecord方法中
59 | 变量t 它是等于parseInt(this.totalStudyTime / 5) + 2;
60 | 变量e 它先做了一个判断当(null == this.watchPointPost || "" == this.watchPointPost)为true时,e就等于"0,1,"如果为false时 e就等于this.watchPointPost + ",";
61 | 最后是this.watchPointPost = e + t。
62 |
63 | 那现在我们就有点迷糊了,就这啊?怎么感觉不对劲啊?我们继续在js文件里面查找,最后找到了个叫startTotalTimer的方法
64 |
65 | ```javascript
66 | startTotalTimer: function() {
67 | this.learningTimeRecordInterval = setInterval(this.learningTimeRecord, 1990),
68 | this.totalStudyTimeInterval = setInterval(this.totalStudyTimeFun, 4990),
69 | this.cacheInterval = setInterval(this.saveCacheIntervalTime, 18e4),
70 | this.databaseInterval = setInterval(this.saveDatabaseIntervalTime, 3e5)
71 | },
72 | ```
73 | 这一下就明了了嗷,这里有4个定时器,而里面有两个方法就是我们目前需要的。我们现在知道了每1990毫秒执行一次learningTimeRecord,每4990毫秒执行一次totalStudyTimeFun。我们用Python代码模拟一下看看
74 |
75 | ```python
76 | # 生成watchPoint
77 | # videoSec为当前视频总长度
78 | def generateWatchPoint(videoSec):
79 | # 初始化为0
80 | tolStudyTime = 0
81 | watchPointPost = ""
82 | # 因为要想一次性提交整个视频的时间的话
83 | # 那么我们用for循环来实现定时器,从0s开始,视频总长度结束
84 | for i in range(0, videoSec):
85 | # 取余来判断当前该执行哪一个
86 | if i % 2 == 0:
87 | t, e = learningTimeRecord(tolStudyTime, watchPointPost)
88 | watchPointPost = e + str(t)
89 | if i % 5 == 0:
90 | tolStudyTime += 5
91 | return watchPointPost
92 |
93 |
94 | # 模拟learningTimeRecord
95 | def learningTimeRecord(tolStudyTime, watchPointPost):
96 | t = int(tolStudyTime / 5) + 2
97 | if watchPointPost is None or watchPointPost == "":
98 | e = "0,1,"
99 | else:
100 | e = watchPointPost + ","
101 | return t, e
102 | ```
103 | 至此watchPoint参数我们算是搞定了嗷!!
104 |
105 | ## 2.ev分析
106 | 我们先把saveDatabaseIntervalTime整过来
107 |
108 | ```javascript
109 | saveDatabaseIntervalTime: function(t, e, n) {
110 | var o = this
111 | , a = this.lessonId
112 | , r = this.smallLessonId
113 | , s = [this.recruitId, a, r, this.lastViewVideoId, this.videoDetail.chapterId, this.data.studyStatus, parseInt(this.playTimes), parseInt(this.totalStudyTime), i.i(p.g)(ablePlayerX("container").getPosition())]
114 | , l = {
115 | watchPoint: this.watchPointPost,
116 | ev: this.D26666.Z(s),
117 | learningTokenId: C.encode(this.preVideoInfo.studiedLessonDto.id),
118 | courseId: this.courseId
119 | };
120 | console.log("提交进度时间:" + this.playTimes),
121 | console.log("观看总时间:" + this.totalStudyTime),
122 | m.a.saveDatabaseIntervalTime(l).then(function(i) {
123 | o.playTimes = 0,
124 | o.watchPointPost = "",
125 | -10 == i.code ? (o.tipsDialog = !0,
126 | o.tipsMsg = "同时播放多个视频,其他页面的学习进度将停止记录哦!",
127 | o.tipsBtn = "我知道了") : 403 == i.code ? setTimeout(function() {
128 | window.location.href = root + "/login/gologin?fromurl=" + encodeURIComponent(window.location.href)
129 | }, 3e3) : 0 != i.code ? o.backDialog = !0 : o.saveDataFilish && t && (o.prelearningNote(t, e, n),
130 | o.saveDataFilish = !1)
131 | })
132 | },
133 | ```
134 | 其实可以看到ev其实就是this.D26666.Z(s),而s是啥了?变量s就是上面的
135 | s = [this.recruitId, a, r, this.lastViewVideoId, this.videoDetail.chapterId, this.data.studyStatus, parseInt(this.playTimes), parseInt(this.totalStudyTime), i.i(p.g)(ablePlayerX("container").getPosition())]
136 | 分析一下数据:
137 | 开头五个参数都在/learning/videolist接口返回的json数据中
138 | this.recruitId: 对应recruitId
139 | a(this.lessonId): 对应lessonId
140 | r(this.smallLessonId): 对应id(这个值有些课程没有 所以需要自行适配)
141 | this.lastViewVideoId: 对应videoId
142 | this.videoDetail.chapterId: 对应chapterId
143 | this.data.studyStatus: 固定值 "0"
144 | parseInt(this.playTimes): 当前播放时长(上一次暂停到这一次暂停的时长)
145 | parseInt(this.totalStudyTime): 该视频播放的总时长
146 | i.i(p.g)(ablePlayerX("container").getPosition()): 当前视频总时间需要转换成hh:mm:ss形式
147 | 该视频总时长也在返回的json数据中 对应的数据是videoSec
148 |
149 | 我们s搞清楚了,再来看看this.D26666.Z这个方法。这个方法直接搜索是找不到的,所以我建议直接打断点,然后直接定位过去
150 |
151 | ```javascript
152 | , , function(t, e, i) {
153 | "use strict";
154 | var n = {
155 | _a: "AgrcepndtslzyohCia0uS@",
156 | _b: "A0ilndhga@usreztoSCpyc",
157 | _c: "d0@yorAtlhzSCeunpcagis",
158 | _d: "zzpttjd",
159 | X: function(t) {
160 | for (var e = "", i = 0; i < t[this._c[8] + this._a[4] + this._c[15] + this._a[1] + this._a[8] + this._b[6]]; i++) {
161 | var n = t[this._a[3] + this._a[14] + this._c[18] + this._a[2] + this._b[18] + this._b[16] + this._c[0] + this._a[4] + this._b[0] + this._b[15]](i) ^ this._d[this._b[21] + this._b[6] + this._a[17] + this._c[5] + this._b[18] + this._c[4] + this._a[7] + this._a[4] + this._a[0] + this._c[7]](i % this._d[this._a[10] + this._b[13] + this._b[4] + this._a[1] + this._c[7] + this._a[14]]);
162 | e += this.Y(n)
163 | }
164 | return e
165 | },
166 | Y: function(t) {
167 | var e = t[this._c[7] + this._a[13] + this._a[20] + this._b[15] + this._a[2] + this._b[2] + this._c[15] + this._c[19]](16);
168 | return e = e[this._b[3] + this._a[4] + this._b[4] + this._a[1] + this._c[7] + this._c[9]] < 2 ? this._b[1] + e : e,
169 | e[this._a[9] + this._b[3] + this._c[20] + this._c[17] + this._c[13]](-4)
170 | },
171 | Z: function(t) {
172 | for (var e = "", i = 0; i < t.length; i++)
173 | e += t[i] + ";";
174 | return e = e.substring(0, e.length - 1),
175 | this.X(e)
176 | }
177 | };
178 | e.a = n
179 | }
180 | ```
181 | 这不就找到了吗?我们这个就不模拟了,直接用Python的execjs去执行js代码就行。
182 |
183 | ## 3.learningTokenId分析
184 | learningTokenId 是在/learning/prelearningNote接口中返回的id,且用Base64编码。
185 |
186 | ## 4.courseId分析
187 | 这个就很简单了就是上面watchPoint里面的courseId
188 |
189 | ## 5.uuid分析
190 | uuid就在/login/getLoginUserInfo接口中,我们直接取出来就行了
191 |
192 | # 二、代码实现
193 |
194 | ```python
195 | import execjs
196 | import requests
197 | import time
198 | import base64
199 |
200 | # recruitId
201 | recruitId = ""
202 | # courseId
203 | courseId = ""
204 | # 存储当前课程所有视频信息
205 | videoInformationList = []
206 | # 时间戳 毫秒级
207 | t = time.time()
208 | dateFormate = int(round(t * 1000) / 1000)
209 | # 课程secret码
210 | recruitAndCourseId = ""
211 | # session
212 | SESSION = input("请输入session登录:")
213 | # 请求头
214 | headers = {
215 | "Cookie": "SESSION=" + SESSION + ";"
216 | }
217 | # 用户id
218 | uuid = ""
219 | # 判断类型
220 | typeNumbers = 0
221 |
222 |
223 | ctx = execjs.compile("""
224 | var _a = "AgrcepndtslzyohCia0uS@",
225 | _b = "A0ilndhga@usreztoSCpyc",
226 | _c = "d0@yorAtlhzSCeunpcagis",
227 | _d = "zzpttjd";
228 |
229 | function X(t) {
230 | for (var e = "", i = 0; i < t[_c[8] + _a[4] + _c[15] + _a[1] + _a[8] + _b[6]]; i++) {
231 | var n = t[_a[3] + _a[14] + _c[18] + _a[2] + _b[18] + _b[16] + _c[0] + _a[4] + _b[0] + _b[15]](i) ^
232 | _d[_b[21] + _b[6] + _a[17] + _c[5] + _b[18] + _c[4] + _a[7] + _a[4] + _a[0] + _c[7]]
233 | (i % _d[_a[10] + _b[13] + _b[4] + _a[1] + _c[7] + _a[14]]);
234 | e += Y(n)
235 | }
236 | return e
237 | }
238 |
239 | function Y(t) {
240 | var e = t[_c[7] + _a[13] + _a[20] + _b[15] + _a[2] + _b[2] + _c[15] + _c[19]](16);
241 | return e = e[_b[3] + _a[4] + _b[4] + _a[1] + _c[7] + _c[9]] < 2 ? _b[1] + e : e,
242 | e[_a[9] + _b[3] + _c[20] + _c[17] + _c[13]](-4)
243 | }
244 |
245 | function Z(t) {
246 | for (var e = "", i = 0; i < t.length; i++) {
247 | e += t[i] + ";";
248 | }
249 | return e = e.substring(0, e.length - 1), X(e);
250 | }
251 | """)
252 |
253 |
254 | def getTolTime(tolTime):
255 | m, s = divmod(tolTime, 60)
256 | h, m = divmod(m, 60)
257 | tolTime = "%02d:%02d:%02d" % (h, m, s)
258 | return tolTime
259 |
260 |
261 | def learningTimeRecord(tolStudyTime, watchPointPost):
262 | t = int(tolStudyTime / 5) + 2
263 | if watchPointPost is None or watchPointPost == "":
264 | e = "0,1,"
265 | else:
266 | e = watchPointPost + ","
267 | return t, e
268 |
269 |
270 | def generateWatchPoint(videoSec):
271 | tolStudyTime = 0
272 | watchPointPost = ""
273 | for i in range(0, videoSec):
274 | if i % 2 == 0:
275 | t, e = learningTimeRecord(tolStudyTime, watchPointPost)
276 | watchPointPost = e + str(t)
277 | if i % 5 == 0:
278 | tolStudyTime += 5
279 | return watchPointPost
280 |
281 |
282 | def submitData(k, studyTotalTime):
283 | resp = requests.post("https://studyservice.zhihuishu.com/learning/prelearningNote", {
284 | "ccCourseId": courseId,
285 | "chapterId": k["chapterId"],
286 | "isApply": 1,
287 | "lessonId": k["id"],
288 | "recruitId": recruitId,
289 | "videoId": k["videoId"],
290 | "uuid": uuid,
291 | "dateFormate": dateFormate
292 | }, headers=headers)
293 | learningTokenId = str(resp.json()["data"]["studiedLessonDto"]["id"])
294 | learningTokenId = base64.encodebytes(learningTokenId.encode("utf8")).decode()
295 |
296 | s = [recruitId, k["id"], 0, k["videoId"], k["chapterId"], "0", k["videoSec"] - studyTotalTime,
297 | k["videoSec"], getTolTime(k["videoSec"])]
298 | resp = requests.post("https://studyservice.zhihuishu.com/learning/saveDatabaseIntervalTime", {
299 | "watchPoint": generateWatchPoint(k["videoSec"]),
300 | "ev": ctx.call("Z", s),
301 | "learningTokenId": learningTokenId,
302 | "courseId": courseId,
303 | "uuid": uuid,
304 | "dateFormate": dateFormate
305 | }, headers=headers)
306 | return resp.json()
307 |
308 |
309 | def submitData2(k, chapterId, studyTotalTime):
310 | resp = requests.post("https://studyservice.zhihuishu.com/learning/prelearningNote", {
311 | "ccCourseId": courseId,
312 | "chapterId": chapterId,
313 | "isApply": 1,
314 | "lessonId": k["lessonId"],
315 | "lessonVideoId": k["id"],
316 | "recruitId": recruitId,
317 | "videoId": k["videoId"],
318 | "uuid": uuid,
319 | "dateFormate": dateFormate
320 | }, headers=headers)
321 | learningTokenId = str(resp.json()["data"]["studiedLessonDto"]["id"])
322 | learningTokenId = base64.encodebytes(learningTokenId.encode("utf8")).decode()
323 |
324 | s = [recruitId, k["lessonId"], k["id"], k["videoId"], chapterId, "0", k["videoSec"] - studyTotalTime,
325 | k["videoSec"], getTolTime(k["videoSec"])]
326 | resp = requests.post("https://studyservice.zhihuishu.com/learning/saveDatabaseIntervalTime", {
327 | "watchPoint": generateWatchPoint(k["videoSec"]),
328 | "ev": ctx.call("Z", s),
329 | "learningTokenId": learningTokenId,
330 | "courseId": courseId,
331 | "uuid": uuid,
332 | "dateFormate": dateFormate
333 | }, headers=headers)
334 | return resp.json()
335 |
336 |
337 | # 登录
338 | resp = requests.get("https://studyservice.zhihuishu.com/login/getLoginUserInfo?dateFormate=" + str(dateFormate) + "000"
339 | , headers=headers)
340 | # 读取返回数据
341 | data = resp.json()
342 | if data["code"] == 200:
343 | # 赋值
344 | uuid = data["data"]["uuid"]
345 | print(" 用户信息 ")
346 | print("realName:" + data["data"]["realName"])
347 | print("uuid:" + data["data"]["uuid"])
348 | print("username:" + data["data"]["username"])
349 | print("登录成功!")
350 | print()
351 | else:
352 | print("登录失败!停止运行")
353 | quit()
354 |
355 | recruitAndCourseId = input("请输入课程secret码:")
356 | if recruitAndCourseId is None or recruitAndCourseId == "":
357 | print("获取失败!停止运行")
358 | quit()
359 | else:
360 | # 获取当前课程所有视频信息
361 | resp = requests.post("https://studyservice.zhihuishu.com/learning/videolist", {
362 | "recruitAndCourseId": recruitAndCourseId,
363 | "uuid": uuid,
364 | "dateFormate": dateFormate
365 | }, headers=headers)
366 | if resp.json()["code"] == 0:
367 | content = resp.json()["data"]
368 | recruitId = content["recruitId"]
369 | courseId = content["courseId"]
370 | videoInformationList = content["videoChapterDtos"]
371 | print("获取课程所有视频信息成功!")
372 | print()
373 | else:
374 | print("获取课程所有视频信息失败!停止运行")
375 | quit()
376 |
377 | print("正在查询当前视频学习情况。。。。")
378 | data = {}
379 | count = 0
380 | for i in videoInformationList:
381 | videoLessons = i["videoLessons"]
382 | for j in videoLessons:
383 | data["lessonIds[" + str(count) + "]"] = j["id"]
384 | count += 1
385 | count = 0
386 | for i in videoInformationList:
387 | videoLessons = i["videoLessons"]
388 | for k in videoLessons:
389 | if len(k) == 9:
390 | videoSmallLessons = k["videoSmallLessons"]
391 | for l in videoSmallLessons:
392 | data["lessonVideoIds[" + str(count) + "]"] = l["id"]
393 | count += 1
394 | data["recruitId"] = recruitId
395 | data["uuid"] = uuid
396 | data["dateFormate"] = dateFormate
397 |
398 | resp = requests.post("https://studyservice.zhihuishu.com/learning/queryStuyInfo", data=data, headers=headers)
399 | if resp.json()["code"] == 0:
400 | print("开始检测。。。。")
401 | print()
402 | queryStuyInfoData = resp.json()["data"]
403 | lessonList = {}
404 | lvList = {}
405 | if len(queryStuyInfoData) == 2:
406 | lessonList = resp.json()["data"]["lesson"]
407 | lvList = resp.json()["data"]["lv"]
408 | else:
409 | lessonList = resp.json()["data"]["lesson"]
410 | for m in videoInformationList:
411 | videoLessons = m["videoLessons"]
412 | for n in videoLessons:
413 | if len(n) == 10:
414 | lessonInformation = lessonList[str(n["id"])]
415 | print("视频名称:" + n["name"])
416 | print("视频总时长:" + getTolTime(n["videoSec"]))
417 | print("学习总时长:" + str(lessonInformation["studyTotalTime"]) + "s")
418 | if n["videoSec"] - lessonInformation["studyTotalTime"] < 50:
419 | print("状态:已完成")
420 | print()
421 | else:
422 | print("状态:未完成")
423 | choose = input("是否刷取该节视频?(Y/N):")
424 | if choose == "Y" or choose == "y":
425 | result = submitData(n, lessonInformation["studyTotalTime"])
426 | if result["code"] == 0:
427 | if result["data"]["submitSuccess"]:
428 | print("提交数据成功!")
429 | print()
430 | else:
431 | print("提交数据失败!")
432 | else:
433 | print("请求失败!停止运行")
434 | else:
435 | print("刷取完成!停止运行")
436 | quit()
437 | else:
438 | chapterId = n["chapterId"]
439 | videoSmallLessons = n["videoSmallLessons"]
440 | for p in videoSmallLessons:
441 | lvInformation = lvList[str(p["id"])]
442 | print("视频名称:" + p["name"])
443 | print("视频总时长:" + getTolTime(p["videoSec"]))
444 | print("学习总时长:" + str(lvInformation["studyTotalTime"]) + "s")
445 | if p["videoSec"] - lvInformation["studyTotalTime"] < 50:
446 | print("状态:已完成")
447 | print()
448 | else:
449 | print("状态:未完成")
450 | choose = input("是否刷取该节视频?(Y/N):")
451 | if choose == "Y" or choose == "y":
452 | result = submitData2(p, chapterId, lvInformation["studyTotalTime"])
453 | if result["code"] == 0:
454 | if result["data"]["submitSuccess"]:
455 | print("提交数据成功!")
456 | print()
457 | else:
458 | print("提交数据失败!")
459 | else:
460 | print("刷取完成!停止运行")
461 | quit()
462 | time.sleep(1)
463 | print("全部刷取完成!感谢使用")
464 |
465 | ```
466 | 补充几点:
467 | 1.第一步需要先去播放界面打开开发者模式随便抓个包,拿到cookies里面SESSION(现在需要整个cookie)
468 | 2.第二步需要复制当前课程的recruitAndCourseId,在播放界面的URL地址里面
469 | 图片放一下,好理解
470 | 
471 | 
472 | 
473 | 
474 |
475 |
476 |
477 | # 总结
478 | 没啥总结的,就/learning/prelearningNote和/learning/videolist的参数需要你们自己去搞定,当然代码给出来了,可以自己看嗷。
479 | 别的不多说,直接跑路!
480 | 打扰了,告辞!!!
481 |
--------------------------------------------------------------------------------
/知到2.1.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import time
3 | import requests
4 | from Crypto.Cipher import AES
5 | from AES import AEScryptor
6 |
7 |
8 | # 读取cookies
9 | with open("cookies.txt", "r") as file:
10 | COOKIES = file.read()
11 |
12 | # 课程secret码列表
13 | with open("id.txt", "r") as file:
14 | recruitAndCourseIdList = []
15 | rid = file.read()
16 | if rid != "":
17 | recruitAndCourseIdList = rid.split("\n")
18 |
19 | # recruitId
20 | recruitId: str = ""
21 |
22 | # courseId
23 | courseId: str = ""
24 |
25 | # 存储当前课程所有视频信息
26 | videoInformationList: list = list()
27 |
28 | # 获取当前时间戳 毫秒级
29 | timestamp: time = time.time()
30 | dateFormate: int = int(round(timestamp * 1000) / 1000)
31 |
32 | # 课程recruitAndCourseId码
33 | recruitAndCourseId: str = ""
34 |
35 | # 设置请求头 带上Cookie
36 | headers: dict = {
37 | "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.39',
38 | "Cookie": COOKIES
39 | }
40 |
41 | # 初始化 AES加密器 key:密钥 iv:偏移量
42 | key: bytes = b"qz632524n86i7fk9"
43 | iv: bytes = b"1g3qqdh4jvbskb9x"
44 | aesCryptor: AEScryptor = AEScryptor(key, AES.MODE_CBC, iv, paddingMode="PKCS7Padding", characterSet='utf-8')
45 |
46 | _a = "AgrcepndtslzyohCia0uS@"
47 | _b = "A0ilndhga@usreztoSCpyc"
48 | _c = "d0@yorAtlhzSCeunpcagis"
49 | _d = "zzpttjd"
50 |
51 |
52 | def X(t):
53 | e = ""
54 | for i in range(0, len(t)):
55 | n = ord(t[i]) ^ ord(_d[i % len(_d)])
56 | e += Y(n)
57 | return e
58 |
59 |
60 | def Y(t):
61 | e = hex(int(t)).replace('0x', '')
62 | if len(e) < 2:
63 | e = _b[1] + e
64 | return e[-4: -1] + e[-1]
65 |
66 |
67 | def Z(t):
68 | e = ""
69 | for i in range(0, len(t)):
70 | e += str(t[i]) + ";"
71 | e = e[0: len(e) - 1]
72 | return X(e)
73 |
74 |
75 | # 获取总时间 转换成H:M:S格式
76 | def getTolTime(tolTime: int) -> str:
77 | minute, second = divmod(tolTime, 60)
78 | hour, minute = divmod(minute, 60)
79 | tolTime: str = "%02d:%02d:%02d" % (hour, minute, second)
80 | return tolTime
81 |
82 |
83 | # learningTimeRecord、generateWatchPoint 通过这两个方法,生成 为/learning/saveDatabaseIntervalTime中的watchPoint参数
84 | # 此参数的作用是验证是否完整观看
85 | def learningTimeRecord(tolStudyTime: int, watchPointPost: str) -> tuple:
86 | t: int = int(tolStudyTime / 5) + 2
87 | if watchPointPost is None or watchPointPost == "":
88 | e: str = "0,1,"
89 | else:
90 | e: str = watchPointPost + ","
91 | return t, e
92 |
93 |
94 | def generateWatchPoint(videoSec: int) -> str:
95 | tolStudyTime: int = 0
96 | watchPointPost: str = ""
97 | for i in range(0, videoSec):
98 | if i % 2 == 0:
99 | t, e = learningTimeRecord(tolStudyTime, watchPointPost)
100 | watchPointPost = e + str(t)
101 | if i % 5 == 0:
102 | tolStudyTime += 5
103 | return watchPointPost
104 |
105 |
106 | # 提交数据 实现刷课
107 | def submitData(lesson_Information: dict, chapterId: str, studyTotalTime: int) -> dict:
108 | params: dict = {
109 | "ccCourseId": courseId,
110 | "isApply": 1,
111 | "recruitId": recruitId,
112 | "videoId": lesson_Information["videoId"],
113 | "dateFormate": dateFormate
114 | }
115 |
116 | if chapterId == "":
117 | params["chapterId"] = lesson_Information["chapterId"]
118 | params["lessonId"] = lesson_Information["id"]
119 |
120 | lessonData: list = [recruitId, lesson_Information["id"], 0, lesson_Information["videoId"],
121 | lesson_Information["chapterId"], "0", lesson_Information["videoSec"] - studyTotalTime,
122 | lesson_Information["videoSec"], getTolTime(lesson_Information["videoSec"])]
123 | else:
124 | params["chapterId"] = chapterId
125 | params["lessonVideoId"] = lesson_Information["id"]
126 | params["lessonId"] = lesson_Information["lessonId"]
127 |
128 | lessonData: list = [recruitId, lesson_Information["lessonId"], lesson_Information["id"],
129 | lesson_Information["videoId"], chapterId, "0", lesson_Information["videoSec"] - studyTotalTime,
130 | lesson_Information["videoSec"], getTolTime(lesson_Information["videoSec"])]
131 |
132 | resp: requests = requests.post("https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/prelearningNote", {
133 | "secretStr": aesCryptor.encryptFromString(str(params))
134 | }, headers=headers)
135 |
136 | learningTokenId: str = str(resp.json()["data"]["studiedLessonDto"]["id"])
137 | learningTokenId: str = base64.encodebytes(learningTokenId.encode("utf8")).decode()
138 |
139 | params: dict = {
140 | "watchPoint": generateWatchPoint(lesson_Information["videoSec"]),
141 | "ev": Z(lessonData),
142 | "learningTokenId": learningTokenId,
143 | "courseId": courseId,
144 | "dateFormate": dateFormate
145 | }
146 |
147 | resp: requests = requests.post("https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/saveDatabaseIntervalTime", {
148 | "secretStr": aesCryptor.encryptFromString(str(params))
149 | }, headers=headers)
150 | return resp.json()
151 |
152 |
153 | if __name__ == "__main__":
154 | if COOKIES == "":
155 | print("请在cookies.txt中填入播放页面的cookies")
156 | exit(1)
157 |
158 | if len(recruitAndCourseIdList) == 0:
159 | print("请在id.txt中填入需要刷的课程id")
160 | exit(1)
161 |
162 | # 登录
163 | resp = requests.get(
164 | "https://studyservice.zhihuishu.com/login/getLoginUserInfo?dateFormate=" + str(dateFormate) + "000"
165 | , headers=headers)
166 | # 读取返回数据
167 | data = resp.json()
168 | if data["code"] == 200:
169 | uuid = data["data"]["uuid"]
170 | print(" 用户信息 ")
171 | print("realName:" + data["data"]["realName"])
172 | print("uuid:" + data["data"]["uuid"])
173 | print("username:" + data["data"]["username"])
174 | print("登录成功!")
175 |
176 | choose = input("是否默认刷未完成的课时(Y/N)? : ")
177 | for recruitAndCourseId in recruitAndCourseIdList:
178 | print("--> 当前id: ", recruitAndCourseId)
179 | time.sleep(3)
180 |
181 | if recruitAndCourseId is None or recruitAndCourseId == "":
182 | print("获取失败!停止运行")
183 | quit()
184 | else:
185 | # 获取当前课程所有视频信息
186 | data = {
187 | "recruitAndCourseId": recruitAndCourseId,
188 | "dateFormate": dateFormate
189 | }
190 | resp = requests.post("https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/videolist", {
191 | "secretStr": aesCryptor.encryptFromString(str(data))
192 | }, headers=headers)
193 | if resp.json()["code"] == 0:
194 | content = resp.json()["data"]
195 | recruitId = content["recruitId"]
196 | courseId = content["courseId"]
197 | videoInformationList = content["videoChapterDtos"]
198 | print("获取课程所有视频信息成功!")
199 | print()
200 | else:
201 | print("获取课程所有视频信息失败!停止运行")
202 | quit()
203 |
204 | print("正在查询当前视频学习情况。。。。")
205 | data = {}
206 | videoLessonsIds = []
207 | for i in videoInformationList:
208 | videoLessons = i["videoLessons"]
209 | for j in videoLessons:
210 | videoLessonsIds.append(j["id"])
211 | data["lessonIds"] = videoLessonsIds
212 |
213 | videoSmallLessonsIds = []
214 | for i in videoInformationList:
215 | videoLessons = i["videoLessons"]
216 | for k in videoLessons:
217 | if len(k) == 9:
218 | videoSmallLessons = k["videoSmallLessons"]
219 | for l in videoSmallLessons:
220 | videoSmallLessonsIds.append(l["id"])
221 | data["lessonVideoIds"] = videoSmallLessonsIds
222 | data["recruitId"] = recruitId
223 | data["dateFormate"] = dateFormate
224 |
225 | resp = requests.post("https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/queryStuyInfo", {
226 | "secretStr": aesCryptor.encryptFromString(str(data))
227 | }, headers=headers)
228 |
229 | if resp.json()["code"] == 0:
230 | print("开始检测。。。。")
231 | print()
232 | queryStuyInfoData = resp.json()["data"]
233 | lessonList = {}
234 | lvList = {}
235 | if len(queryStuyInfoData) == 2:
236 | lessonList = resp.json()["data"]["lesson"]
237 | lvList = resp.json()["data"]["lv"]
238 | else:
239 | lessonList = resp.json()["data"]["lesson"]
240 | for m in videoInformationList:
241 | videoLessons = m["videoLessons"]
242 | for n in videoLessons:
243 | if len(n) == 10:
244 | lessonInformation = lessonList[str(n["id"])]
245 | print("视频名称:" + n["name"])
246 | print("视频总时长:" + getTolTime(n["videoSec"]))
247 | print("学习总时长:" + str(lessonInformation["studyTotalTime"]) + "s")
248 | if n["videoSec"] - lessonInformation["studyTotalTime"] < 50:
249 | print("状态:已完成")
250 | print()
251 | else:
252 | print("状态:未完成")
253 | if choose == "Y" or choose == "y":
254 | result = submitData(n, "", lessonInformation["studyTotalTime"])
255 | print("----submitData1------")
256 | if result["code"] == 0:
257 | if result["data"]["submitSuccess"]:
258 | print("提交数据成功!")
259 | print()
260 | else:
261 | print("提交数据失败!")
262 | else:
263 | print(result)
264 | exit(1)
265 | else:
266 | chapterId = n["chapterId"]
267 | videoSmallLessons = n["videoSmallLessons"]
268 | for p in videoSmallLessons:
269 | lvInformation = lvList[str(p["id"])]
270 | print("视频名称:" + p["name"])
271 | print("视频总时长:" + getTolTime(p["videoSec"]))
272 | print("学习总时长:" + str(lvInformation["studyTotalTime"]) + "s")
273 | if p["videoSec"] - lvInformation["studyTotalTime"] < 50:
274 | print("状态:已完成")
275 | print()
276 | else:
277 | print("状态:未完成")
278 | if choose == "Y" or choose == "y":
279 | result = submitData(p, chapterId, lvInformation["studyTotalTime"])
280 | print("----submitData2------")
281 | if result["code"] == 0:
282 | if result["data"]["submitSuccess"]:
283 | print("提交数据成功!")
284 | print()
285 | else:
286 | print("提交数据失败!")
287 | else:
288 | print(result)
289 | exit(1)
290 | time.sleep(1)
291 |
292 | print("全部刷取完成!感谢使用")
293 | else:
294 | print("登录失败!停止运行")
295 | exit(1)
296 |
--------------------------------------------------------------------------------