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