From 1d23ffb27fd3752349d1dd54407004feca7b1026 Mon Sep 17 00:00:00 2001 From: B1ue1nWh1te <708968728@qq.com> Date: Thu, 30 Dec 2021 00:05:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E7=BB=86=E8=8A=82=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=BA=86=E4=BC=98=E5=8C=96=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .idea/.gitignore | 8 - .idea/iSmartAuto2.iml | 11 - .idea/inspectionProfiles/Project_Default.xml | 105 ------ .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 4 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - README.md | 32 +- automaton/devtools.py | 58 ++-- automaton/spider/captcha/captcha.py | 58 ++-- automaton/spider/spider.py | 322 ++++++++++-------- automaton/utils.py | 153 +++++---- configs.yml | 31 +- main.py | 21 +- requirements.txt | 17 +- 16 files changed, 375 insertions(+), 467 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/iSmartAuto2.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 5fcb02f..74d72ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.cache/* /venv/ +/.idea/ +/.vscode/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e0..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/iSmartAuto2.iml b/.idea/iSmartAuto2.iml deleted file mode 100644 index f61c21b..0000000 --- a/.idea/iSmartAuto2.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index a52339a..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index c76173c..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9c51603..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 3f9d390..746f6b4 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ -## iSmart 课程自动姬 v1.0.2 +## iSmart 课程自动姬 ### 效果展示 -* 拥有更好的题型适应性,理论上适配所有客观题种类 - -* 提升稳定性,中途宕机概率大大降低 +- 拥有更好的题型适应性,理论上适配所有客观题种类 +- 提升稳定性,中途宕机概率大大降低 -* 采用全新思路,相较 [自动化方案](https://github.com/Mufanc/iSmartAuto) ,效率提升超过 1000% +- 采用全新思路,相较 [自动化方案](https://github.com/Mufanc/iSmartAuto) ,效率提升超过 1000% ![](images/demo.png) - ### 工作原理 @@ -23,13 +21,13 @@ #### Q&A -* **Q:** 既然是回传分数,那为何不用 Python 直接将分数上报,反而要走 cdp? +- **Q:** 既然是回传分数,那为何不用 Python 直接将分数上报,反而要走 cdp? >   上报分数的请求中有疑似 Hash 的字段 `ut`,且生成 `ut` 的方法 native,无法通过分析 JavaScript 得到(有木有大佬会 ollydbg 的来交个 PR)
-* **Q:** 使用这个脚本,会不会被检测到作弊? +- **Q:** 使用这个脚本,会不会被检测到作弊? >   不排除这样的可能性,相较自动化而言,目前的方式提交的数据尚不完整(但成绩和学习时长会被记录),若是仔细比对,有可能会发现数据异常 @@ -66,7 +64,7 @@ pip install -r requirements.txt   修改 `configs.yml` 中的账号和密码,保证与 iSmart 客户端中登录的账号一致,然后根据需要调整下方参数。在终端中执行 `py main.py -h` 可以查看更多帮助信息,这里列举几个常用命令 -* 列出所有课程和书籍的详细信息 +- 列出所有课程和书籍的详细信息 ```shell py main.py list -d @@ -74,7 +72,7 @@ py main.py list -d
-* 根据书籍 id 执行刷课 +- 根据书籍 id 执行刷课 ```shell py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3 @@ -82,7 +80,7 @@ py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3
-* 根据当前打开的页面执行刷课 +- 根据当前打开的页面执行刷课 ```shell py main.py flash -c @@ -98,22 +96,22 @@ py main.py flash -c ### 过滤器语法 -* 待完善 +- 待完善 ### 常见问题 -* 无法刷课? +- 无法刷课?   **除非明确要求用户输入,本项目中的所有 Warning 都不会阻塞**,如果你莫名其妙卡住了,控制台又没有要求你输入,那么大概率是 未适配你的课程/环境没配置好/网络问题 -* 父节点不存在? +- 父节点不存在?   不会因为报这个 Warning 而卡住,如果脚本运行完之后你发现所有任务点确实都刷到了,那么完全不用管这个 Warning -* 账号密码与登录不符? +- 账号密码与登录不符?   脚本判断的逻辑比较粗糙,弹警告只是提醒你务必确保 iSmart 客户端和 `configs.yml` 中是同一个账号,确定无误后大胆使用即可 -* **提 issue 前请看这个 ↓↓↓** +- **提 issue 前请看这个 ↓↓↓** -  如果脚本出现异常,请检查你的课程和我学的是不是同一门(见上图),不同课程的参数可能会有细微差异,导致获取不到任务或者别的一些诡异现象。如果你有一定的爬虫开发经验,相信你自己可以通过该修改源代码很快解决这些问题;如果你只是想刷课,那么非常抱歉我并没有时间和经历去对每一本教材都做适配 +  如果脚本出现异常,请检查你的课程和我学的是不是同一门(见上图),不同课程的参数可能会有细微差异,导致获取不到任务或者别的一些诡异现象。如果你有一定的爬虫开发经验,相信你自己可以通过该修改源代码很快解决这些问题;如果你只是想刷课,那么非常抱歉我并没有时间和精力去对每一本教材都做适配 diff --git a/automaton/devtools.py b/automaton/devtools.py index 86fe222..bbf2247 100644 --- a/automaton/devtools.py +++ b/automaton/devtools.py @@ -1,12 +1,11 @@ -import asyncio -import json import re -import urllib.parse as parser - +import sys +import json import httpx +import asyncio import websockets +import urllib.parse as parser from loguru import logger - from configs import configs @@ -22,33 +21,36 @@ class Browser(object): self.port = dev_port async def _verify(self): # 校验客户端和配置文件中的用户是否相同 - logger.info('正在校验账号...') - page = await self.wait_for_page(r'https?://.*') - user_info = json.loads((await page.eval(''' - (function () { - var xhr = new XMLHttpRequest() - xhr.open('POST', 'https://school.ismartlearning.cn/client/user/student-info', false) - xhr.withCredentials = true - xhr.send(null) - return xhr.responseText - })() - '''))['result']['value'])['data'] - spider_user = configs['user']['username'] - logger.debug(f'spider: {spider_user}') - logger.debug(f'iSmart client: {json.dumps(user_info, indent=4)}') - if str(spider_user) != user_info['mobile'] and str(spider_user) != user_info['username']: - logger.warning('检测到 iSmart 客户端中登录的账号与配置文件中账号不符!') - choice = input('继续使用可能会出现意料之外的问题,是否继续?[y/N]') - if choice.lower() != 'y': - exit() - else: - logger.info('校验通过!') - return True + try: + logger.info('[账号校验] | 正在校验账号...') + page = await self.wait_for_page(r'https?://.*') + user_info = (await page.eval(''' + (function () { + var xhr = new XMLHttpRequest() + xhr.open('POST', 'https://school.ismartlearning.cn/client/user/student-info', false) + xhr.withCredentials = true + xhr.send(null) + return xhr.responseText + })() + '''))['result']['value']['data'] + spider_user = configs['user']['username'] + logger.debug(f'[账号校验] | 配置文件用户: {spider_user}') + logger.debug(f'[账号校验] | 客户端用户: {json.dumps(user_info, indent=4)}') + if str(spider_user) != user_info['mobile'] and str(spider_user) != user_info['username']: + logger.warning('[账号校验] | 客户端中登录的账号与配置文件中账号不符') + choice = input('[账号校验] | 继续使用可能会出现意料之外的问题,是否继续?[y/N]') + if choice.lower() != 'y': + exit() + else: + logger.info('[账号校验] | 校验通过') + return True + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[账号校验] | 账号校验出错:{exceptionInformation}') async def wait_for_page(self, regexp): # 等待符合条件的页面出现 async with httpx.AsyncClient() as client: while True: - logger.info('等待可用页面...') try: pages = (await client.get(f'http://127.0.0.1:{self.port}/json')).json() for page in pages: diff --git a/automaton/spider/captcha/captcha.py b/automaton/spider/captcha/captcha.py index 65c65a1..ad73cc4 100644 --- a/automaton/spider/captcha/captcha.py +++ b/automaton/spider/captcha/captcha.py @@ -1,7 +1,7 @@ -from os import path - +import sys import cv2 import numpy as np +from os import path from loguru import logger from numpy import average, dot, linalg @@ -9,30 +9,36 @@ base_path = path.join(path.split(__file__)[0], 'models') def similarity(img_1, img_2): - images = [img_1, img_2] - vectors = [] - norms = [] - for image in images: - vector = [average(pixels) for pixels in image] - vectors.append(vector) - norms.append(linalg.norm(vector, 2)) - a, b = vectors - a_norm, b_norm = norms - return dot(a / a_norm, b / b_norm) + try: + images = [img_1, img_2] + vectors = [] + norms = [] + for image in images: + vector = [average(pixels) for pixels in image] + vectors.append(vector) + norms.append(linalg.norm(vector, 2)) + a, b = vectors + a_norm, b_norm = norms + return dot(a / a_norm, b / b_norm) + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[验证码识别] | 运算出错:{exceptionInformation}') def recognize(img_content: bytes): - img = cv2.imdecode(np.asarray(bytearray(img_content), dtype=np.uint8), cv2.IMREAD_COLOR) - img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)[1] - models = [cv2.imread(path.join(base_path, f'{i}.png')) for i in range(10)] - code = '' - for i in range(4): - code += sorted( - [(f'{j}', similarity(img[4:24, 9 + i * 15:24 + i * 15], std)) for j, std in enumerate(models)], - key=lambda x: x[1] - )[-1][0] - logger.info(f'识别结果:{code}') - if len(code) != 4: - logger.warning('验证码长度不是 4 位') - return code + try: + img = cv2.imdecode(np.asarray(bytearray(img_content), dtype=np.uint8), cv2.IMREAD_COLOR) + img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)[1] + models = [cv2.imread(path.join(base_path, f'{i}.png')) for i in range(10)] + code = '' + for i in range(4): + code += sorted( + [(f'{j}', similarity(img[4:24, 9 + i * 15:24 + i * 15], std)) for j, std in enumerate(models)], + key=lambda x: x[1], reverse=True + )[0][0] + logger.info(f'[验证码识别] | 识别结果:{code}') + return code + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[验证码识别] | 识别出错:{exceptionInformation}') diff --git a/automaton/spider/spider.py b/automaton/spider/spider.py index d1b52c9..7efebb7 100644 --- a/automaton/spider/spider.py +++ b/automaton/spider/spider.py @@ -1,10 +1,8 @@ +import sys import json -from hashlib import md5 -from random import random - import httpx +from hashlib import md5 from loguru import logger - from .captcha import recognize @@ -14,11 +12,15 @@ class Tree: # 任务树 self.child = [] def sort(self): - self.child.sort( - key=lambda node: node.task['displayOrder'] - ) - for ch in self.child: - ch.sort() + try: + self.child.sort( + key=lambda node: node.task['displayOrder'] + ) + for ch in self.child: + ch.sort() + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[构建任务树] | 排序出错:{exceptionInformation}') class Spider(httpx.AsyncClient): @@ -27,148 +29,186 @@ class Spider(httpx.AsyncClient): self.is_login = False async def login(self, username, password): - if self.is_login: - return {} - - self.cookies.clear() # 重置 cookies - logger.info('正在获取验证码...') - result = await self.get(f'http://sso.ismartlearning.cn/captcha.html?{random()}') - code = recognize(result.content) - token = md5(password.encode()).hexdigest() - info = (await self.post( - 'http://sso.ismartlearning.cn/v2/tickets-v2', - data={ - 'username': username, - 'password': md5(token.encode() + b'fa&s*l%$k!fq$k!ld@fjlk').hexdigest(), # 啥时候炸了就写成动态获取的 - 'captcha': code - }, - headers={ - 'X-Requested-With': 'XMLHttpRequest', - 'Origin': 'http://me.ismartlearning.cn', - 'Referer': 'http://me.ismartlearning.cn/' - } - )).json() - logger.debug(info['result']) - - assert info['result']['code'] == -26 # 断言登录结果 - self.is_login = True - return info['result'] + try: + if self.is_login: + return {} + + self.cookies.clear() # 重置 cookies + logger.info('[登录] | 正在获取验证码...') + result = await self.get('http://sso.ismartlearning.cn/captcha.html') + code = recognize(result.content) + password = md5(md5(password.encode()).hexdigest().encode() + b'fa&s*l%$k!fq$k!ld@fjlk').hexdigest() + logger.info('[登录] | 正在登录...') + info = (await self.post( + 'http://sso.ismartlearning.cn/v2/tickets-v2', + data={ + 'username': username, + 'password': password, + 'captcha': code + }, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'Origin': 'http://me.ismartlearning.cn', + 'Referer': 'http://me.ismartlearning.cn/' + } + )).json()['result'] + logger.debug(f"[登录] | {info}") + assert info['code'] == -26 # 断言登录结果 + self.is_login = True + logger.success('[登录] | 登录成功') + return info + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[登录] | 登录出错:{exceptionInformation}') async def get_courses(self): # 获取课程列表 - logger.info('正在获取课程列表...') - courses = (await self.post( - 'https://school.ismartlearning.cn/client/course/list-of-student?status=1', - data={ - 'pager.currentPage': 1, - 'pager.pageSize': 32767 - } - )).json()['data'] - return courses['list'] + try: + logger.info('[获取课程列表] | 正在获取课程列表...') + courses = (await self.post( + 'https://school.ismartlearning.cn/client/course/list-of-student?status=1', + data={ + 'pager.currentPage': 1, + 'pager.pageSize': 100 + } + )).json()['data']['list'] + logger.debug(f"[获取课程列表] | {courses}") + logger.success('[获取课程列表] | 获取课程列表成功') + return courses + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取课程列表] | 获取课程列表出错:{exceptionInformation}') async def get_books(self, course_id): # 获取某课程的书籍列表 - logger.info('正在获取书籍列表...') - await self.post( # 必须有这个请求,否则后面会报错 - 'http://school.ismartlearning.cn/client/course/list-of-student?status=1', - data={ - 'pager.currentPage': 1, - 'pager.pageSize': 32767 - } - ) - books = (await self.post( - 'http://school.ismartlearning.cn/client/course/textbook/list-of-student', - data={ - 'courseId': course_id - } - )).json()['data'] - return books + try: + logger.info('[获取书籍列表] | 正在获取书籍列表...') + await self.post( # 必须有这个请求,否则后面会报错 + 'http://school.ismartlearning.cn/client/course/list-of-student?status=1', + data={ + 'pager.currentPage': 1, + 'pager.pageSize': 100 + } + ) + books = (await self.post( + 'http://school.ismartlearning.cn/client/course/textbook/list-of-student', + data={ + 'courseId': course_id + } + )).json()['data'] + logger.success('[获取书籍列表] | 获取书籍列表成功') + return books + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取书籍列表] | 获取书籍列表出错:{exceptionInformation}') async def get_tasks(self, book_id, book_type, course_id): # 获取某书籍的任务树 - logger.info('正在获取任务列表...') - await self.post('http://school.ismartlearning.cn/client/course/textbook/chapters') - tasks = (await self.post( - 'http://school.ismartlearning.cn/client/course/textbook/chapters', - data={ - 'bookId': book_id, - 'bookType': book_type, - 'courseId': course_id - } - )).json()['data'] - id_record = {task['id']: Tree(task) for task in tasks} - book_name = (await self.book_info(book_id))['bookName'] - root = Tree({ - 'book_id': tasks[0]['book_id'], - 'unitStudyPercent': 0, - 'name': book_name - }) - for task_id in id_record: - node = id_record[task_id] - node_name = (f'{node.task["name"]} ' if 'name' in node.task else '') + f'[id:{node.task["id"]}]' - if 'parent_id' in node.task: - if (parent_id := node.task['parent_id']) in id_record: - id_record[parent_id].child.append(node) + try: + logger.info('[获取任务列表] | 正在获取任务列表...') + await self.post('http://school.ismartlearning.cn/client/course/textbook/chapters') + tasks = (await self.post( + 'http://school.ismartlearning.cn/client/course/textbook/chapters', + data={ + 'bookId': book_id, + 'bookType': book_type, + 'courseId': course_id + } + )).json()['data'] + id_record = {task['id']: Tree(task) for task in tasks} + book_name = (await self.book_info(book_id))['bookName'] + root = Tree({ + 'book_id': tasks[0]['book_id'], + 'unitStudyPercent': 0, + 'name': book_name + }) + logger.info('[构建任务树] | 正在构建任务树...') + for task_id in id_record: + node = id_record[task_id] + node_name = f'{node.task.get("name","")}[id:{node.task["id"]}]' + if 'parent_id' in node.task: + if node.task['parent_id'] in id_record: + id_record[node.task['parent_id']].child.append(node) + else: + logger.warning(f'[构建任务树] | {node_name} 父节点不存在') else: - logger.warning(f'父节点不存在:{node_name}') - else: - root.child.append(node) - root.sort() - return root + root.child.append(node) + root.sort() + logger.success('[构建任务树] | 构建任务树完成') + logger.success('[获取任务列表] | 获取任务列表完成') + return root + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取任务列表] | 获取任务列表出错:{exceptionInformation}') async def get_paper(self, paper_id): # 获取任务点信息(包括题目和答案) - ticket = (await self.post( - 'http://sso.ismartlearning.cn/v1/serviceTicket', - data={ - 'service': 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo' - } - )).json()['data']['serverTicket'] - logger.debug(f'Ticket: {ticket}') - paper_info = (await self.post( - 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo', - data={ - 'paperId': paper_id - }, - headers={ - 'Origin': 'http://me.ismartlearning.cn', - 'Referer': 'http://me.ismartlearning.cn/', - 'X-Requested-With': 'XMLHttpRequest', - 'Accept-Encoding': 'gzip, deflate' - }, - params={ - 'ticket': ticket - } - )).json() - logger.debug(f'paper_info: {json.dumps(paper_info, indent=4)}') - return paper_info['data'] + try: + logger.info('[获取任务点] | 正在获取任务点信息...') + ticket = (await self.post( + 'http://sso.ismartlearning.cn/v1/serviceTicket', + data={ + 'service': 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo' + } + )).json()['data']['serverTicket'] + logger.debug(f'[获取任务点] | {ticket}') + paper_info = (await self.post( + 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo', + params={ + 'ticket': ticket + }, + data={ + 'paperId': paper_id + }, + headers={ + 'Origin': 'http://me.ismartlearning.cn', + 'Referer': 'http://me.ismartlearning.cn/', + 'X-Requested-With': 'XMLHttpRequest', + 'Accept-Encoding': 'gzip, deflate' + } + )).json()['data'] + logger.debug(f'[获取任务点] | {json.dumps(paper_info, indent=4)}') + logger.success('[获取任务点] | 获取任务点信息完成') + return paper_info + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取任务点] | 获取任务点出错:{exceptionInformation}') async def user_info(self): - logger.info('正在获取用户信息...') - return (await self.post( - 'https://school.ismartlearning.cn/client/user/student-info') - ).json() + try: + logger.info('[获取用户信息] | 正在获取用户信息...') + info = await self.post('https://school.ismartlearning.cn/client/user/student-info').json() + logger.success('[获取用户信息] | 获取用户信息完成') + return info + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取用户信息] | 获取用户信息出错:{exceptionInformation}') async def book_info(self, book_id): - logger.info('正在获取书籍信息...') - ticket = (await self.post( - 'http://sso.ismartlearning.cn/v1/serviceTicket', - data={ - 'service': 'http://book-api.ismartlearning.cn/client/v2/book/info' - } - )).json()['data']['serverTicket'] - logger.debug(f'Ticket: {ticket}') - book_info = (await self.post( - 'http://book-api.ismartlearning.cn/client/v2/book/info', - headers={ - 'Origin': 'http://me.ismartlearning.cn', - 'Referer': 'http://me.ismartlearning.cn/', - 'X-Requested-With': 'XMLHttpRequest', - 'Accept-Encoding': 'gzip, deflate' - }, - params={ - 'ticket': ticket - }, - data={ - 'bookId': book_id, - 'bookType': 0 - } - )).json() - logger.debug(f'book_info: {json.dumps(book_info, indent=4)}') - return book_info['data'] + try: + logger.info('[获取书籍信息] | 正在获取书籍信息...') + ticket = (await self.post( + 'http://sso.ismartlearning.cn/v1/serviceTicket', + data={ + 'service': 'http://book-api.ismartlearning.cn/client/v2/book/info' + } + )).json()['data']['serverTicket'] + logger.debug(f'[获取书籍信息] | {ticket}') + book_info = (await self.post( + 'http://book-api.ismartlearning.cn/client/v2/book/info', + params={ + 'ticket': ticket + }, + data={ + 'bookId': book_id, + 'bookType': 0 + }, + headers={ + 'Origin': 'http://me.ismartlearning.cn', + 'Referer': 'http://me.ismartlearning.cn/', + 'X-Requested-With': 'XMLHttpRequest', + 'Accept-Encoding': 'gzip, deflate' + } + )).json()['data'] + logger.debug(f'[获取书籍信息] | {json.dumps(book_info, indent=4)}') + logger.success('[获取书籍信息] | 获取书籍信息完成') + return book_info + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[获取书籍信息] | 获取书籍信息出错:{exceptionInformation}') diff --git a/automaton/utils.py b/automaton/utils.py index b9d4c7f..2ca9468 100644 --- a/automaton/utils.py +++ b/automaton/utils.py @@ -1,95 +1,106 @@ -from random import uniform, randint - -from bs4 import BeautifulSoup +import sys from loguru import logger - +from bs4 import BeautifulSoup +from random import uniform, randint from configs import configs from .devtools import Browser from .spider import Spider -PLACEHOLDER = '. ' +PLACEHOLDER = ". " _paper_config = configs['paper'] async def list_books(detail): - async with Spider() as spider: - await spider.login(**configs['user']) - courses = await spider.get_courses() - - for cr in courses: - if detail: - hint = f'{cr["courseName"]} ({cr["teacherName"]})' - else: - hint = cr['courseName'] - print(hint) + try: + async with Spider() as spider: + await spider.login(**configs['user']) + courses = await spider.get_courses() - books = await spider.get_books(cr["courseId"]) - for book in books: + for cr in courses: if detail: - hint = f'> [{cr["courseId"]}#{book["bookId"]}] {book["bookName"]} ({book["percent"]}%)' + hint = f'{cr["courseName"]}({cr["teacherName"]})' else: - hint = f'> {book["bookName"]}' - print(PLACEHOLDER + hint) + hint = cr['courseName'] + logger.info(f'[课程信息] | {hint}') + + books = await spider.get_books(cr["courseId"]) + for book in books: + if detail: + hint = f'[{cr["courseId"]}-{book["bookId"]}] {book["bookName"]}({book["percent"]}%)' + else: + hint = f'{book["bookName"]}' + logger.info(f'[书籍信息] | {hint}') + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[读取信息] | 读取信息出错:{exceptionInformation}') async def _random(spider, paper_id): # 随机的分数和学习时长 - paper = BeautifulSoup((await spider.get_paper(paper_id))['paperData'], 'lxml-xml') - questions = paper.select('element[knowledge]:has(> question_type)') - if not questions: - return 100, 5 - - total = 0 - score, time = 0, 0 - for q in questions: - q_type = int(q.select_one('question_type').text) - q_score = float(q.select_one('question_score').text) - total += q_score - - ranges = _paper_config['random-score'] - if q_type <= len(ranges) and (r := ranges[q_type-1]) is not None: - if not isinstance(ranges[q_type-1], list): - r = [r, r] - score += q_score * uniform(*r) - else: - q_no = q.select_one('question_no').text - logger.warning(f'T{q_no}:未知的题型!') - if (r := _paper_config['defaults']) == 'pause': - score += float(input('请手动输入该题得分 [0-1]:')) - else: # defaults + try: + paper = BeautifulSoup((await spider.get_paper(paper_id))['paperData'], 'lxml-xml') + questions = paper.select('element[knowledge]:has(> question_type)') + if not questions: + return 100, 5 + + total = 0 + score, time = 0, 0 + for q in questions: + q_type = int(q.select_one('question_type').text) + q_score = float(q.select_one('question_score').text) + total += q_score + + ranges = _paper_config['random-score'] + if q_type <= len(ranges) and ranges[q_type - 1] is not None: + r = ranges[q_type - 1] + if not isinstance(r, list): + r = [r, r] score += q_score * uniform(*r) + else: + q_no = q.select_one('question_no').text + logger.warning(f'[任务点信息] | 题[{q_no}] 未知题型') + if _paper_config['defaults'] == 'pause': + score += float(input('[任务点信息] | 请手动输入该题得分 [0-1]:')) + else: # defaults + score += q_score * uniform(*_paper_config['defaults']) - time += randint(*_paper_config['random-time']) + time += randint(*_paper_config['random-time']) - return int(100 * score / total), time + return int(100 * score / total), time + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[处理随机数据] | 处理随机数据出错:{exceptionInformation}') async def _flash(course_id, book_id, spider): - async def dfs(node, depth=0): - task = node.task - print(PLACEHOLDER * depth + task['name']) - if _paper_config['skip-finished'] and task['unitStudyPercent'] == 100: - print(PLACEHOLDER * depth + '# Skipped') - return - if 'paperId' in task: # 如果有任务则提交 - chapter_id = task['chapterId'] - task_id = task['id'] - score, time = await _random(spider, task['paperId']) - result = await page.submit(book_id, chapter_id, task_id, score, time, 100, user_id) - if result['wasThrown'] or not result['result']['value']: - logger.warning(f'任务 {task["name"]} 可能提交失败,请留意最终结果!') - for ch in node.child: - await dfs(ch, depth + 1) - - browser = await Browser.connect() - page = await browser.wait_for_page(r'https?://pc\.ismartin\.com.*') - - # With Spider - await spider.login(**configs['user']) - user_id = (await spider.user_info())['data']['uid'] - - book_type = (await spider.book_info(book_id))['bookType'] - root = await spider.get_tasks(book_id, book_type, course_id) - await dfs(root) + try: + async def dfs(node, depth=0): + task = node.task + logger.info(f"[刷任务点] | {PLACEHOLDER * depth} {task['name']}") + if _paper_config['skip-finished'] and task['unitStudyPercent'] == 100: + logger.info(f"[刷任务点] | {PLACEHOLDER * depth} 跳过") + return + if 'paperId' in task: # 如果有任务则提交 + chapter_id = task['chapterId'] + task_id = task['id'] + score, time = await _random(spider, task['paperId']) + result = await page.submit(book_id, chapter_id, task_id, score, time, 100, user_id) + if result['wasThrown'] or not result['result']['value']: + logger.warning(f'[刷任务点] | 任务[{task["name"]}]可能提交失败,请留意最终结果') + for ch in node.child: + await dfs(ch, depth + 1) + + browser = await Browser.connect() + page = await browser.wait_for_page(r'https?://pc\.ismartin\.com.*') + + # With Spider + await spider.login(**configs['user']) + user_id = (await spider.user_info())['data']['uid'] + book_type = (await spider.book_info(book_id))['bookType'] + root = await spider.get_tasks(book_id, book_type, course_id) + await dfs(root) + except Exception: + exceptionInformation = sys.exc_info() + logger.warning(f'[刷任务点] | 刷任务点出错:{exceptionInformation}') async def flash_by_id(identity): diff --git a/configs.yml b/configs.yml index 8137c02..8fd01e7 100644 --- a/configs.yml +++ b/configs.yml @@ -2,31 +2,30 @@ browser: port: 9222 verify: true - # 用户配置(务必保持账号密码与 iSmart 中已登录的相同) user: - username: <用户名> # 用户名/手机号 - password: <密码> # 密码 + username: <用户名> # 用户名/手机号 + password: <密码> # 密码 # 答题配置 paper: - skip-finished: true # 跳过已完成任务 + skip-finished: true # 跳过已完成任务 # 不同题目类型的随机得分 random-score: - - 1 # 1.单选 - - 1 # 2.多选 - - 1 # 3.判断 - - [ 0.9, 1 ] # 4.填空 + - 1 # 1.单选 + - 1 # 2.多选 + - 1 # 3.判断 + - [0.9, 1] # 4.填空 - - - [ 0.9, 1 ] # 6.连线 + - [0.9, 1] # 6.连线 - - - 1 # 8.匹配 - - [ 0.7, 1 ] # 9.口语跟读 - - [ 0.7, 1 ] # 10.短文改错 - - 1 # 11.选词填空 + - 1 # 8.匹配 + - [0.7, 1] # 9.口语跟读 + - [0.7, 1] # 10.短文改错 + - 1 # 11.选词填空 - - defaults: pause # 未知题型的处理方式(暂停或使用默认得分) -# defaults: [ 1, 1 ] + defaults: pause # 未知题型的处理方式(暂停或使用默认得分) + # defaults: [ 1, 1 ] - random-time: [ 60, 120 ] # 每道题的随机用时(秒) + random-time: [60, 120] # 每道题的随机用时(秒) diff --git a/main.py b/main.py index f4809d7..51e6ebf 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,30 @@ -import asyncio import sys -from argparse import ArgumentParser - +import asyncio from loguru import logger - from automaton import utils +from argparse import ArgumentParser async def main(): parser = ArgumentParser('main.py') - parser.add_argument('-v', dest='level', action='count', help='日志过滤等级,依次为 warning, info, debug') + parser.add_argument('-v', dest='level', action='count', help='日志过滤等级,依次为 warning, info, debug, success') subparsers = parser.add_subparsers(dest='method', help='模式选择') method_list = subparsers.add_parser('list', help='列出所有课程和书籍') method_list.add_argument('-d', '--detail', action='store_true', help='显示详细信息') - method_flash = subparsers.add_parser('flash', help='对选定的一个或几个课程执行刷课') + method_flash = subparsers.add_parser('flash', help='对选定的课程执行刷课') target = method_flash.add_mutually_exclusive_group() - target.add_argument('-i', '--id', help='直接指定书籍 id') - target.add_argument('-c', '--current', action='store_true', help='限定当前课程或书籍') - target.add_argument('-a', '--all', action='store_true', help='选择全部') - method_flash.add_argument('-f', '--filter', help='任务过滤器,设置后只刷匹配的任务(尚未实现)') # Todo: 实现这个 + target.add_argument('-i', '--id', help='指定书籍ID') + target.add_argument('-c', '--current', action='store_true', help='限定为当前课程或书籍') + target.add_argument('-a', '--all', action='store_true', help='选择全部(慎用 除非你知道自己在进行什么操作)') + # method_flash.add_argument('-f', '--filter', help='任务过滤器,设置后只刷匹配的任务(尚未实现)') # Todo: 实现这个 args = parser.parse_args() logger.remove() - logger.add(sys.stdout, level=['WARNING', 'INFO', 'DEBUG'][args.level or 0]) + logger.add(sys.stdout, level=['WARNING', 'INFO', 'DEBUG', 'SUCCESS'][args.level or 0]) if args.method == 'list': await utils.list_books(detail=args.detail) @@ -46,4 +44,3 @@ if __name__ == '__main__': loop.run_until_complete(main()) finally: loop.close() - diff --git a/requirements.txt b/requirements.txt index 0049e73..e6ca984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ -loguru~=0.5.3 -opencv-python~=4.5.3.56 -numpy~=1.21.2 -httpx~=0.19.0 -beautifulsoup4~=4.10.0 -websockets==8.1 -PyYAML~=5.4.1 -lxml~=4.6.3 +loguru +opencv-python +numpy +httpx +beautifulsoup4 +PyYAML +lxml +argparse +websockets \ No newline at end of file