├── 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 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/ded7fca99de0467a92d641b51ae1ad10.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 13 | 为什么我要说它最有用了? 14 | 因为看接口名称我们就应该知道,这个接口和视频时长是相关的,做个测试大家就明白了。 15 | 我们打开一个视频,反复点播放和暂停就能看出来。 16 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/54f8a087b6ab4fa2b446cba6e79ed669.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 17 | 好了,现在我们找到了接口,下一步我们就来看一下它提交的参数有什么? 18 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/67bb5c0687f24128809117a4688a26af.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 19 | 可以看到有6个参数,分别是: 20 | watchPoint: 21 | ev: 22 | learingTokenId: 23 | courseId: 24 | uuid: 25 | dateFormate: 26 | 我们现在就只知道最后一个参数是时间戳,其他的一个都不知道是啥,这咋办? 27 | 最好的方法就是直接复制一个参数名称到top文件夹里去搜索,我就用watchPoint测试。 28 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/88623e7a1a694523b5511e890e501a2e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 29 | 我们点击去,再在js文件里面搜索watchPoint 30 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/7f1c71fa38a44e13b0700c54f3b90172.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 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 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/f33f3bc02dea490894ee79cafe6a8d18.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 471 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/a85ad176777847c785bccb811e5da182.png#pic_center) 472 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/03834d5136ab4aa0857b74513bcaab5f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 473 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/88a5a5ddbf5b475ab54fc32e83a8e6e6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA56ia5bCP55m9,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) 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 | --------------------------------------------------------------------------------