feat: 对细节进行了优化 添加部分异常处理

master
B1ue1nWh1te 3 years ago
parent 0a569f872c
commit 1d23ffb27f

2
.gitignore vendored

@ -1,2 +1,4 @@
/.cache/* /.cache/*
/venv/ /venv/
/.idea/
/.vscode/

8
.idea/.gitignore vendored

@ -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/

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/.cache" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (iSmartAuto2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -1,105 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="8">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="tr" />
<item index="7" class="java.lang.String" itemvalue="td" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://csee.hnu.edu.cn" />
</list>
</option>
</inspection_tool>
<inspection_tool class="JSJQueryEfficiency" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyDefaultArgumentInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyMandatoryEncodingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="tqdm" />
<item index="1" class="java.lang.String" itemvalue="scipy" />
<item index="2" class="java.lang.String" itemvalue="colour" />
<item index="3" class="java.lang.String" itemvalue="progressbar" />
<item index="4" class="java.lang.String" itemvalue="pydub" />
<item index="5" class="java.lang.String" itemvalue="argparse" />
<item index="6" class="java.lang.String" itemvalue="win32gui" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E402" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PySimplifyBooleanCheckInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoreComparisonToZero" value="false" />
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="_importlib_modulespec.ModuleType.main" />
<option value="requests.models.Response.html" />
<option value="tuple.spines" />
<option value="ctypes.c_long.*" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (iSmartAuto2)" project-jdk-type="Python SDK" />
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/iSmartAuto2.iml" filepath="$PROJECT_DIR$/.idea/iSmartAuto2.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -1,16 +1,14 @@
## iSmart 课程自动姬 v1.0.2 ## iSmart 课程自动姬
### 效果展示 ### 效果展示
* 拥有更好的题型适应性,理论上适配所有客观题种类 - 拥有更好的题型适应性,理论上适配所有客观题种类
- 提升稳定性,中途宕机概率大大降低
* 提升稳定性,中途宕机概率大大降低 - 采用全新思路,相较 [自动化方案](https://github.com/Mufanc/iSmartAuto) ,效率提升超过 1000%
* 采用全新思路,相较 [自动化方案](https://github.com/Mufanc/iSmartAuto) ,效率提升超过 1000%
![](images/demo.png) ![](images/demo.png)
### 工作原理 ### 工作原理
&emsp;&emsp;使用抓包工具分析客户端流量,可以得知 iSmart 客户端采用的判题方式为本地评判。也就是说会首先将题目和答案一同下载下来,完成答题后使用用户的计算机完成判分,最后将分数回传。这样一来就为爬取答案提供了可能,脚本会根据提供的用户名和密码完成登录,然后将习题的答案下载下来,为进一步地自动答题做好准备。 &emsp;&emsp;使用抓包工具分析客户端流量,可以得知 iSmart 客户端采用的判题方式为本地评判。也就是说会首先将题目和答案一同下载下来,完成答题后使用用户的计算机完成判分,最后将分数回传。这样一来就为爬取答案提供了可能,脚本会根据提供的用户名和密码完成登录,然后将习题的答案下载下来,为进一步地自动答题做好准备。
@ -23,13 +21,13 @@
#### Q&A #### Q&A
* **Q** 既然是回传分数,那为何不用 Python 直接将分数上报,反而要走 cdp - **Q** 既然是回传分数,那为何不用 Python 直接将分数上报,反而要走 cdp
> &emsp;&emsp;上报分数的请求中有疑似 Hash 的字段 `ut`,且生成 `ut` 的方法 native无法通过分析 JavaScript 得到(有木有大佬会 ollydbg 的来交个 PR > &emsp;&emsp;上报分数的请求中有疑似 Hash 的字段 `ut`,且生成 `ut` 的方法 native无法通过分析 JavaScript 得到(有木有大佬会 ollydbg 的来交个 PR
<br/> <br/>
* **Q** 使用这个脚本,会不会被检测到作弊? - **Q** 使用这个脚本,会不会被检测到作弊?
> &emsp;&emsp;不排除这样的可能性,相较自动化而言,目前的方式提交的数据尚不完整(但成绩和学习时长会被记录),若是仔细比对,有可能会发现数据异常 > &emsp;&emsp;不排除这样的可能性,相较自动化而言,目前的方式提交的数据尚不完整(但成绩和学习时长会被记录),若是仔细比对,有可能会发现数据异常
@ -66,7 +64,7 @@ pip install -r requirements.txt
&emsp;&emsp;修改 `configs.yml` 中的账号和密码,保证与 iSmart 客户端中登录的账号一致,然后根据需要调整下方参数。在终端中执行 `py main.py -h` 可以查看更多帮助信息,这里列举几个常用命令 &emsp;&emsp;修改 `configs.yml` 中的账号和密码,保证与 iSmart 客户端中登录的账号一致,然后根据需要调整下方参数。在终端中执行 `py main.py -h` 可以查看更多帮助信息,这里列举几个常用命令
* 列出所有课程和书籍的详细信息 - 列出所有课程和书籍的详细信息
```shell ```shell
py main.py list -d py main.py list -d
@ -74,7 +72,7 @@ py main.py list -d
<br/> <br/>
* 根据书籍 id 执行刷课 - 根据书籍 id 执行刷课
```shell ```shell
py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3 py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3
@ -82,7 +80,7 @@ py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3
<br/> <br/>
* 根据当前打开的页面执行刷课 - 根据当前打开的页面执行刷课
```shell ```shell
py main.py flash -c py main.py flash -c
@ -98,22 +96,22 @@ py main.py flash -c
### 过滤器语法 ### 过滤器语法
* 待完善 - 待完善
### 常见问题 ### 常见问题
* 无法刷课? - 无法刷课?
&emsp;&emsp;**除非明确要求用户输入,本项目中的所有 Warning 都不会阻塞**,如果你莫名其妙卡住了,控制台又没有要求你输入,那么大概率是 未适配你的课程/环境没配置好/网络问题 &emsp;&emsp;**除非明确要求用户输入,本项目中的所有 Warning 都不会阻塞**,如果你莫名其妙卡住了,控制台又没有要求你输入,那么大概率是 未适配你的课程/环境没配置好/网络问题
* 父节点不存在? - 父节点不存在?
&emsp;&emsp;不会因为报这个 Warning 而卡住,如果脚本运行完之后你发现所有任务点确实都刷到了,那么完全不用管这个 Warning &emsp;&emsp;不会因为报这个 Warning 而卡住,如果脚本运行完之后你发现所有任务点确实都刷到了,那么完全不用管这个 Warning
* 账号密码与登录不符? - 账号密码与登录不符?
&emsp;&emsp;脚本判断的逻辑比较粗糙,弹警告只是提醒你务必确保 iSmart 客户端和 `configs.yml` 中是同一个账号,确定无误后大胆使用即可 &emsp;&emsp;脚本判断的逻辑比较粗糙,弹警告只是提醒你务必确保 iSmart 客户端和 `configs.yml` 中是同一个账号,确定无误后大胆使用即可
* **提 issue 前请看这个 ↓↓↓** - **提 issue 前请看这个 ↓↓↓**
&emsp;&emsp;如果脚本出现异常,请检查你的课程和我学的是不是同一门(见上图),不同课程的参数可能会有细微差异,导致获取不到任务或者别的一些诡异现象。如果你有一定的爬虫开发经验,相信你自己可以通过该修改源代码很快解决这些问题;如果你只是想刷课,那么非常抱歉我并没有时间和经历去对每一本教材都做适配 &emsp;&emsp;如果脚本出现异常,请检查你的课程和我学的是不是同一门(见上图),不同课程的参数可能会有细微差异,导致获取不到任务或者别的一些诡异现象。如果你有一定的爬虫开发经验,相信你自己可以通过该修改源代码很快解决这些问题;如果你只是想刷课,那么非常抱歉我并没有时间和精力去对每一本教材都做适配

@ -1,12 +1,11 @@
import asyncio
import json
import re import re
import urllib.parse as parser import sys
import json
import httpx import httpx
import asyncio
import websockets import websockets
import urllib.parse as parser
from loguru import logger from loguru import logger
from configs import configs from configs import configs
@ -22,9 +21,10 @@ class Browser(object):
self.port = dev_port self.port = dev_port
async def _verify(self): # 校验客户端和配置文件中的用户是否相同 async def _verify(self): # 校验客户端和配置文件中的用户是否相同
logger.info('正在校验账号...') try:
logger.info('[账号校验] | 正在校验账号...')
page = await self.wait_for_page(r'https?://.*') page = await self.wait_for_page(r'https?://.*')
user_info = json.loads((await page.eval(''' user_info = (await page.eval('''
(function () { (function () {
var xhr = new XMLHttpRequest() var xhr = new XMLHttpRequest()
xhr.open('POST', 'https://school.ismartlearning.cn/client/user/student-info', false) xhr.open('POST', 'https://school.ismartlearning.cn/client/user/student-info', false)
@ -32,23 +32,25 @@ class Browser(object):
xhr.send(null) xhr.send(null)
return xhr.responseText return xhr.responseText
})() })()
'''))['result']['value'])['data'] '''))['result']['value']['data']
spider_user = configs['user']['username'] spider_user = configs['user']['username']
logger.debug(f'spider: {spider_user}') logger.debug(f'[账号校验] | 配置文件用户: {spider_user}')
logger.debug(f'iSmart client: {json.dumps(user_info, indent=4)}') logger.debug(f'[账号校验] | 客户端用户: {json.dumps(user_info, indent=4)}')
if str(spider_user) != user_info['mobile'] and str(spider_user) != user_info['username']: if str(spider_user) != user_info['mobile'] and str(spider_user) != user_info['username']:
logger.warning('检测到 iSmart 客户端中登录的账号与配置文件中账号不符') logger.warning('[账号校验] | 客户端中登录的账号与配置文件中账号不符')
choice = input('继续使用可能会出现意料之外的问题,是否继续?[y/N]') choice = input('[账号校验] | 继续使用可能会出现意料之外的问题,是否继续?[y/N]')
if choice.lower() != 'y': if choice.lower() != 'y':
exit() exit()
else: else:
logger.info('校验通过') logger.info('[账号校验] | 校验通过')
return True return True
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[账号校验] | 账号校验出错:{exceptionInformation}')
async def wait_for_page(self, regexp): # 等待符合条件的页面出现 async def wait_for_page(self, regexp): # 等待符合条件的页面出现
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
while True: while True:
logger.info('等待可用页面...')
try: try:
pages = (await client.get(f'http://127.0.0.1:{self.port}/json')).json() pages = (await client.get(f'http://127.0.0.1:{self.port}/json')).json()
for page in pages: for page in pages:

@ -1,7 +1,7 @@
from os import path import sys
import cv2 import cv2
import numpy as np import numpy as np
from os import path
from loguru import logger from loguru import logger
from numpy import average, dot, linalg from numpy import average, dot, linalg
@ -9,6 +9,7 @@ base_path = path.join(path.split(__file__)[0], 'models')
def similarity(img_1, img_2): def similarity(img_1, img_2):
try:
images = [img_1, img_2] images = [img_1, img_2]
vectors = [] vectors = []
norms = [] norms = []
@ -19,9 +20,13 @@ def similarity(img_1, img_2):
a, b = vectors a, b = vectors
a_norm, b_norm = norms a_norm, b_norm = norms
return dot(a / a_norm, b / b_norm) return dot(a / a_norm, b / b_norm)
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[验证码识别] | 运算出错:{exceptionInformation}')
def recognize(img_content: bytes): def recognize(img_content: bytes):
try:
img = cv2.imdecode(np.asarray(bytearray(img_content), dtype=np.uint8), cv2.IMREAD_COLOR) img = cv2.imdecode(np.asarray(bytearray(img_content), dtype=np.uint8), cv2.IMREAD_COLOR)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)[1] img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)[1]
@ -30,9 +35,10 @@ def recognize(img_content: bytes):
for i in range(4): for i in range(4):
code += sorted( code += sorted(
[(f'{j}', similarity(img[4:24, 9 + i * 15:24 + i * 15], std)) for j, std in enumerate(models)], [(f'{j}', similarity(img[4:24, 9 + i * 15:24 + i * 15], std)) for j, std in enumerate(models)],
key=lambda x: x[1] key=lambda x: x[1], reverse=True
)[-1][0] )[0][0]
logger.info(f'识别结果:{code}') logger.info(f'[验证码识别] | 识别结果:{code}')
if len(code) != 4:
logger.warning('验证码长度不是 4 位')
return code return code
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[验证码识别] | 识别出错:{exceptionInformation}')

@ -1,10 +1,8 @@
import sys
import json import json
from hashlib import md5
from random import random
import httpx import httpx
from hashlib import md5
from loguru import logger from loguru import logger
from .captcha import recognize from .captcha import recognize
@ -14,11 +12,15 @@ class Tree: # 任务树
self.child = [] self.child = []
def sort(self): def sort(self):
try:
self.child.sort( self.child.sort(
key=lambda node: node.task['displayOrder'] key=lambda node: node.task['displayOrder']
) )
for ch in self.child: for ch in self.child:
ch.sort() ch.sort()
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[构建任务树] | 排序出错:{exceptionInformation}')
class Spider(httpx.AsyncClient): class Spider(httpx.AsyncClient):
@ -27,19 +29,21 @@ class Spider(httpx.AsyncClient):
self.is_login = False self.is_login = False
async def login(self, username, password): async def login(self, username, password):
try:
if self.is_login: if self.is_login:
return {} return {}
self.cookies.clear() # 重置 cookies self.cookies.clear() # 重置 cookies
logger.info('正在获取验证码...') logger.info('[登录] | 正在获取验证码...')
result = await self.get(f'http://sso.ismartlearning.cn/captcha.html?{random()}') result = await self.get('http://sso.ismartlearning.cn/captcha.html')
code = recognize(result.content) code = recognize(result.content)
token = md5(password.encode()).hexdigest() password = md5(md5(password.encode()).hexdigest().encode() + b'fa&s*l%$k!fq$k!ld@fjlk').hexdigest()
logger.info('[登录] | 正在登录...')
info = (await self.post( info = (await self.post(
'http://sso.ismartlearning.cn/v2/tickets-v2', 'http://sso.ismartlearning.cn/v2/tickets-v2',
data={ data={
'username': username, 'username': username,
'password': md5(token.encode() + b'fa&s*l%$k!fq$k!ld@fjlk').hexdigest(), # 啥时候炸了就写成动态获取的 'password': password,
'captcha': code 'captcha': code
}, },
headers={ headers={
@ -47,31 +51,41 @@ class Spider(httpx.AsyncClient):
'Origin': 'http://me.ismartlearning.cn', 'Origin': 'http://me.ismartlearning.cn',
'Referer': 'http://me.ismartlearning.cn/' 'Referer': 'http://me.ismartlearning.cn/'
} }
)).json() )).json()['result']
logger.debug(info['result']) logger.debug(f"[登录] | {info}")
assert info['code'] == -26 # 断言登录结果
assert info['result']['code'] == -26 # 断言登录结果
self.is_login = True self.is_login = True
return info['result'] logger.success('[登录] | 登录成功')
return info
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[登录] | 登录出错:{exceptionInformation}')
async def get_courses(self): # 获取课程列表 async def get_courses(self): # 获取课程列表
logger.info('正在获取课程列表...') try:
logger.info('[获取课程列表] | 正在获取课程列表...')
courses = (await self.post( courses = (await self.post(
'https://school.ismartlearning.cn/client/course/list-of-student?status=1', 'https://school.ismartlearning.cn/client/course/list-of-student?status=1',
data={ data={
'pager.currentPage': 1, 'pager.currentPage': 1,
'pager.pageSize': 32767 'pager.pageSize': 100
} }
)).json()['data'] )).json()['data']['list']
return courses['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): # 获取某课程的书籍列表 async def get_books(self, course_id): # 获取某课程的书籍列表
logger.info('正在获取书籍列表...') try:
logger.info('[获取书籍列表] | 正在获取书籍列表...')
await self.post( # 必须有这个请求,否则后面会报错 await self.post( # 必须有这个请求,否则后面会报错
'http://school.ismartlearning.cn/client/course/list-of-student?status=1', 'http://school.ismartlearning.cn/client/course/list-of-student?status=1',
data={ data={
'pager.currentPage': 1, 'pager.currentPage': 1,
'pager.pageSize': 32767 'pager.pageSize': 100
} }
) )
books = (await self.post( books = (await self.post(
@ -80,10 +94,15 @@ class Spider(httpx.AsyncClient):
'courseId': course_id 'courseId': course_id
} }
)).json()['data'] )).json()['data']
logger.success('[获取书籍列表] | 获取书籍列表成功')
return books return books
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[获取书籍列表] | 获取书籍列表出错:{exceptionInformation}')
async def get_tasks(self, book_id, book_type, course_id): # 获取某书籍的任务树 async def get_tasks(self, book_id, book_type, course_id): # 获取某书籍的任务树
logger.info('正在获取任务列表...') try:
logger.info('[获取任务列表] | 正在获取任务列表...')
await self.post('http://school.ismartlearning.cn/client/course/textbook/chapters') await self.post('http://school.ismartlearning.cn/client/course/textbook/chapters')
tasks = (await self.post( tasks = (await self.post(
'http://school.ismartlearning.cn/client/course/textbook/chapters', 'http://school.ismartlearning.cn/client/course/textbook/chapters',
@ -100,29 +119,40 @@ class Spider(httpx.AsyncClient):
'unitStudyPercent': 0, 'unitStudyPercent': 0,
'name': book_name 'name': book_name
}) })
logger.info('[构建任务树] | 正在构建任务树...')
for task_id in id_record: for task_id in id_record:
node = id_record[task_id] node = id_record[task_id]
node_name = (f'{node.task["name"]} ' if 'name' in node.task else '') + f'[id:{node.task["id"]}]' node_name = f'{node.task.get("name","")}[id:{node.task["id"]}]'
if 'parent_id' in node.task: if 'parent_id' in node.task:
if (parent_id := node.task['parent_id']) in id_record: if node.task['parent_id'] in id_record:
id_record[parent_id].child.append(node) id_record[node.task['parent_id']].child.append(node)
else: else:
logger.warning(f'父节点不存在:{node_name}') logger.warning(f'[构建任务树] | {node_name} 父节点不存在')
else: else:
root.child.append(node) root.child.append(node)
root.sort() root.sort()
logger.success('[构建任务树] | 构建任务树完成')
logger.success('[获取任务列表] | 获取任务列表完成')
return root return root
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[获取任务列表] | 获取任务列表出错:{exceptionInformation}')
async def get_paper(self, paper_id): # 获取任务点信息(包括题目和答案) async def get_paper(self, paper_id): # 获取任务点信息(包括题目和答案)
try:
logger.info('[获取任务点] | 正在获取任务点信息...')
ticket = (await self.post( ticket = (await self.post(
'http://sso.ismartlearning.cn/v1/serviceTicket', 'http://sso.ismartlearning.cn/v1/serviceTicket',
data={ data={
'service': 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo' 'service': 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo'
} }
)).json()['data']['serverTicket'] )).json()['data']['serverTicket']
logger.debug(f'Ticket: {ticket}') logger.debug(f'[获取任务点] | {ticket}')
paper_info = (await self.post( paper_info = (await self.post(
'http://xot-api.ismartlearning.cn/client/textbook/paperinfo', 'http://xot-api.ismartlearning.cn/client/textbook/paperinfo',
params={
'ticket': ticket
},
data={ data={
'paperId': paper_id 'paperId': paper_id
}, },
@ -131,44 +161,54 @@ class Spider(httpx.AsyncClient):
'Referer': 'http://me.ismartlearning.cn/', 'Referer': 'http://me.ismartlearning.cn/',
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
'Accept-Encoding': 'gzip, deflate' 'Accept-Encoding': 'gzip, deflate'
},
params={
'ticket': ticket
} }
)).json() )).json()['data']
logger.debug(f'paper_info: {json.dumps(paper_info, indent=4)}') logger.debug(f'[获取任务点] | {json.dumps(paper_info, indent=4)}')
return paper_info['data'] logger.success('[获取任务点] | 获取任务点信息完成')
return paper_info
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[获取任务点] | 获取任务点出错:{exceptionInformation}')
async def user_info(self): async def user_info(self):
logger.info('正在获取用户信息...') try:
return (await self.post( logger.info('[获取用户信息] | 正在获取用户信息...')
'https://school.ismartlearning.cn/client/user/student-info') info = await self.post('https://school.ismartlearning.cn/client/user/student-info').json()
).json() logger.success('[获取用户信息] | 获取用户信息完成')
return info
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[获取用户信息] | 获取用户信息出错:{exceptionInformation}')
async def book_info(self, book_id): async def book_info(self, book_id):
logger.info('正在获取书籍信息...') try:
logger.info('[获取书籍信息] | 正在获取书籍信息...')
ticket = (await self.post( ticket = (await self.post(
'http://sso.ismartlearning.cn/v1/serviceTicket', 'http://sso.ismartlearning.cn/v1/serviceTicket',
data={ data={
'service': 'http://book-api.ismartlearning.cn/client/v2/book/info' 'service': 'http://book-api.ismartlearning.cn/client/v2/book/info'
} }
)).json()['data']['serverTicket'] )).json()['data']['serverTicket']
logger.debug(f'Ticket: {ticket}') logger.debug(f'[获取书籍信息] | {ticket}')
book_info = (await self.post( book_info = (await self.post(
'http://book-api.ismartlearning.cn/client/v2/book/info', '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={ params={
'ticket': ticket 'ticket': ticket
}, },
data={ data={
'bookId': book_id, 'bookId': book_id,
'bookType': 0 'bookType': 0
},
headers={
'Origin': 'http://me.ismartlearning.cn',
'Referer': 'http://me.ismartlearning.cn/',
'X-Requested-With': 'XMLHttpRequest',
'Accept-Encoding': 'gzip, deflate'
} }
)).json() )).json()['data']
logger.debug(f'book_info: {json.dumps(book_info, indent=4)}') logger.debug(f'[获取书籍信息] | {json.dumps(book_info, indent=4)}')
return book_info['data'] logger.success('[获取书籍信息] | 获取书籍信息完成')
return book_info
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[获取书籍信息] | 获取书籍信息出错:{exceptionInformation}')

@ -1,38 +1,42 @@
from random import uniform, randint import sys
from bs4 import BeautifulSoup
from loguru import logger from loguru import logger
from bs4 import BeautifulSoup
from random import uniform, randint
from configs import configs from configs import configs
from .devtools import Browser from .devtools import Browser
from .spider import Spider from .spider import Spider
PLACEHOLDER = '. ' PLACEHOLDER = ". "
_paper_config = configs['paper'] _paper_config = configs['paper']
async def list_books(detail): async def list_books(detail):
try:
async with Spider() as spider: async with Spider() as spider:
await spider.login(**configs['user']) await spider.login(**configs['user'])
courses = await spider.get_courses() courses = await spider.get_courses()
for cr in courses: for cr in courses:
if detail: if detail:
hint = f'{cr["courseName"]} ({cr["teacherName"]})' hint = f'{cr["courseName"]}({cr["teacherName"]})'
else: else:
hint = cr['courseName'] hint = cr['courseName']
print(hint) logger.info(f'[课程信息] | {hint}')
books = await spider.get_books(cr["courseId"]) books = await spider.get_books(cr["courseId"])
for book in books: for book in books:
if detail: if detail:
hint = f'> [{cr["courseId"]}#{book["bookId"]}] {book["bookName"]} ({book["percent"]}%)' hint = f'[{cr["courseId"]}-{book["bookId"]}] {book["bookName"]}({book["percent"]}%)'
else: else:
hint = f'> {book["bookName"]}' hint = f'{book["bookName"]}'
print(PLACEHOLDER + hint) logger.info(f'[书籍信息] | {hint}')
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[读取信息] | 读取信息出错:{exceptionInformation}')
async def _random(spider, paper_id): # 随机的分数和学习时长 async def _random(spider, paper_id): # 随机的分数和学习时长
try:
paper = BeautifulSoup((await spider.get_paper(paper_id))['paperData'], 'lxml-xml') paper = BeautifulSoup((await spider.get_paper(paper_id))['paperData'], 'lxml-xml')
questions = paper.select('element[knowledge]:has(> question_type)') questions = paper.select('element[knowledge]:has(> question_type)')
if not questions: if not questions:
@ -46,29 +50,34 @@ async def _random(spider, paper_id): # 随机的分数和学习时长
total += q_score total += q_score
ranges = _paper_config['random-score'] ranges = _paper_config['random-score']
if q_type <= len(ranges) and (r := ranges[q_type-1]) is not None: if q_type <= len(ranges) and ranges[q_type - 1] is not None:
if not isinstance(ranges[q_type-1], list): r = ranges[q_type - 1]
if not isinstance(r, list):
r = [r, r] r = [r, r]
score += q_score * uniform(*r) score += q_score * uniform(*r)
else: else:
q_no = q.select_one('question_no').text q_no = q.select_one('question_no').text
logger.warning(f'T{q_no}:未知的题型!') logger.warning(f'[任务点信息] | 题[{q_no}] 未知题型')
if (r := _paper_config['defaults']) == 'pause': if _paper_config['defaults'] == 'pause':
score += float(input('请手动输入该题得分 [0-1]')) score += float(input('[任务点信息] | 请手动输入该题得分 [0-1]:'))
else: # defaults else: # defaults
score += q_score * uniform(*r) 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 _flash(course_id, book_id, spider):
try:
async def dfs(node, depth=0): async def dfs(node, depth=0):
task = node.task task = node.task
print(PLACEHOLDER * depth + task['name']) logger.info(f"[刷任务点] | {PLACEHOLDER * depth} {task['name']}")
if _paper_config['skip-finished'] and task['unitStudyPercent'] == 100: if _paper_config['skip-finished'] and task['unitStudyPercent'] == 100:
print(PLACEHOLDER * depth + '# Skipped') logger.info(f"[刷任务点] | {PLACEHOLDER * depth} 跳过")
return return
if 'paperId' in task: # 如果有任务则提交 if 'paperId' in task: # 如果有任务则提交
chapter_id = task['chapterId'] chapter_id = task['chapterId']
@ -76,7 +85,7 @@ async def _flash(course_id, book_id, spider):
score, time = await _random(spider, task['paperId']) score, time = await _random(spider, task['paperId'])
result = await page.submit(book_id, chapter_id, task_id, score, time, 100, user_id) result = await page.submit(book_id, chapter_id, task_id, score, time, 100, user_id)
if result['wasThrown'] or not result['result']['value']: if result['wasThrown'] or not result['result']['value']:
logger.warning(f'任务 {task["name"]} 可能提交失败,请留意最终结果!') logger.warning(f'[刷任务点] | 任务[{task["name"]}]可能提交失败,请留意最终结果')
for ch in node.child: for ch in node.child:
await dfs(ch, depth + 1) await dfs(ch, depth + 1)
@ -86,10 +95,12 @@ async def _flash(course_id, book_id, spider):
# With Spider # With Spider
await spider.login(**configs['user']) await spider.login(**configs['user'])
user_id = (await spider.user_info())['data']['uid'] user_id = (await spider.user_info())['data']['uid']
book_type = (await spider.book_info(book_id))['bookType'] book_type = (await spider.book_info(book_id))['bookType']
root = await spider.get_tasks(book_id, book_type, course_id) root = await spider.get_tasks(book_id, book_type, course_id)
await dfs(root) await dfs(root)
except Exception:
exceptionInformation = sys.exc_info()
logger.warning(f'[刷任务点] | 刷任务点出错:{exceptionInformation}')
async def flash_by_id(identity): async def flash_by_id(identity):

@ -2,7 +2,6 @@ browser:
port: 9222 port: 9222
verify: true verify: true
# 用户配置(务必保持账号密码与 iSmart 中已登录的相同) # 用户配置(务必保持账号密码与 iSmart 中已登录的相同)
user: user:
username: <用户名> # 用户名/手机号 username: <用户名> # 用户名/手机号
@ -17,16 +16,16 @@ paper:
- 1 # 1.单选 - 1 # 1.单选
- 1 # 2.多选 - 1 # 2.多选
- 1 # 3.判断 - 1 # 3.判断
- [ 0.9, 1 ] # 4.填空 - [0.9, 1] # 4.填空
- -
- [ 0.9, 1 ] # 6.连线 - [0.9, 1] # 6.连线
- -
- 1 # 8.匹配 - 1 # 8.匹配
- [ 0.7, 1 ] # 9.口语跟读 - [0.7, 1] # 9.口语跟读
- [ 0.7, 1 ] # 10.短文改错 - [0.7, 1] # 10.短文改错
- 1 # 11.选词填空 - 1 # 11.选词填空
- -
defaults: pause # 未知题型的处理方式(暂停或使用默认得分) defaults: pause # 未知题型的处理方式(暂停或使用默认得分)
# defaults: [ 1, 1 ] # defaults: [ 1, 1 ]
random-time: [ 60, 120 ] # 每道题的随机用时(秒) random-time: [60, 120] # 每道题的随机用时(秒)

@ -1,32 +1,30 @@
import asyncio
import sys import sys
from argparse import ArgumentParser import asyncio
from loguru import logger from loguru import logger
from automaton import utils from automaton import utils
from argparse import ArgumentParser
async def main(): async def main():
parser = ArgumentParser('main.py') 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='模式选择') subparsers = parser.add_subparsers(dest='method', help='模式选择')
method_list = subparsers.add_parser('list', help='列出所有课程和书籍') method_list = subparsers.add_parser('list', help='列出所有课程和书籍')
method_list.add_argument('-d', '--detail', action='store_true', 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 = method_flash.add_mutually_exclusive_group()
target.add_argument('-i', '--id', help='直接指定书籍 id') target.add_argument('-i', '--id', help='指定书籍ID')
target.add_argument('-c', '--current', action='store_true', help='限定当前课程或书籍') target.add_argument('-c', '--current', action='store_true', help='限定当前课程或书籍')
target.add_argument('-a', '--all', action='store_true', help='选择全部') target.add_argument('-a', '--all', action='store_true', help='选择全部(慎用 除非你知道自己在进行什么操作)')
method_flash.add_argument('-f', '--filter', help='任务过滤器,设置后只刷匹配的任务(尚未实现)') # Todo: 实现这个 # method_flash.add_argument('-f', '--filter', help='任务过滤器,设置后只刷匹配的任务(尚未实现)') # Todo: 实现这个
args = parser.parse_args() args = parser.parse_args()
logger.remove() 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': if args.method == 'list':
await utils.list_books(detail=args.detail) await utils.list_books(detail=args.detail)
@ -46,4 +44,3 @@ if __name__ == '__main__':
loop.run_until_complete(main()) loop.run_until_complete(main())
finally: finally:
loop.close() loop.close()

@ -1,8 +1,9 @@
loguru~=0.5.3 loguru
opencv-python~=4.5.3.56 opencv-python
numpy~=1.21.2 numpy
httpx~=0.19.0 httpx
beautifulsoup4~=4.10.0 beautifulsoup4
websockets==8.1 PyYAML
PyYAML~=5.4.1 lxml
lxml~=4.6.3 argparse
websockets
Loading…
Cancel
Save