Skip to content

Commit

Permalink
重构项目 #1
Browse files Browse the repository at this point in the history
新增了命令行参数和选项的支持
项目结构基本完善
  • Loading branch information
Mufanc committed Oct 5, 2021
1 parent 1db550b commit d2adaeb
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 99 deletions.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## iSmart 课程自动姬 v1.0.0
## iSmart 课程自动姬 v1.0.2

> <div align="center"><b>「不止于自动化,追求极致效率」</b></div><br/>
>
Expand Down Expand Up @@ -39,14 +39,38 @@
### 使用方法

&emsp;&emsp;修改 `configs.yml` 中的账号和密码,保证与 iSmart 客户端中登录的账号一致,然后修改 iSmart 的启动快捷方式,增加参数 `--remote-debugging-port=9222`
&emsp;&emsp;修改 iSmart 的启动快捷方式,增加参数 `--remote-debugging-port=9222`

![](images/edit-lnk.png)

&emsp;&emsp;此时运行 main.py,启动 iSmart 客户端,进入某本书籍的教材学习页(如下图),脚本会自动提交成绩。
&emsp;&emsp;修改 `configs.yml` 中的账号和密码,保证与 iSmart 客户端中登录的账号一致,然后根据需要调整下方参数。在终端中执行 `py main.py -h` 可以查看更多帮助信息,这里列举几个常用命令

* 列出所有课程和书籍的详细信息

```shell
py main.py list -d
```

* 根据书籍 id 执行刷课

```shell
py main.py flash -i 51627#7B6911511DB6B33638F6C58531D8FBD3
```

* 根据当前打开的页面执行刷课

```shell
py main.py flash -c
```

&emsp;&emsp;注意如果打开的是「教材学习」页(如下图),只会刷打开的这一本书籍的任务

![](images/booklearn.png)

### 写在最后
&emsp;&emsp;而如果是在课程详情页面,则会对该课程下的所有书籍执行刷课:

![](images/current_course.png)

### 过滤器语法

&emsp;&emsp;该项目尚处于起步阶段,项目结构还没有完全确定下来,所以后续可能会经历多次重构。目前很多功能虽然存在于源码中,但还不完善或者未经测试,可能造成意料之外的结果,所以在使用前还请三思
* 待完善
2 changes: 0 additions & 2 deletions automaton/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from .spider import Spider
from .devtools import Browser
70 changes: 27 additions & 43 deletions automaton/devtools.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
import asyncio
import ctypes
import json
import re
import urllib.parse as parser

import httpx
import websockets
from loguru import logger

from configs import configs
from .utils import ainput

_default_port = configs['browser']['port']
_executable = configs['browser']['executable']
_args = configs['browser']['args']


class Browser(object):
@classmethod
def connect(cls):
return cls(_default_port)

@classmethod
def launch(cls):
ctypes.windll.shell32.ShellExecuteW(
None, 'runas', _executable,
' '.join([f'--remote-debugging-port={_default_port}', *_args]),
None, 1
)
return cls(_default_port)
async def connect(cls):
browser = cls(configs['browser']['port'])
if configs['browser']['verify'] and not await browser._verify():
return None
return browser

def __init__(self, dev_port):
self.port = dev_port

async def verify(self): # 校验客户端和配置文件中的用户是否相同
async def _verify(self): # 校验客户端和配置文件中的用户是否相同
logger.info('正在校验账号...')
page = await self._any_http_page()
page = await self.wait_for_page(r'https?://.*')
user_info = json.loads((await page.eval('''
(function () {
var xhr = new XMLHttpRequest()
Expand All @@ -47,47 +36,35 @@ async def verify(self): # 校验客户端和配置文件中的用户是否相
spider_user = configs['user']['username']
if spider_user != user_info['mobile'] and spider_user != user_info['username']:
logger.warning('检测到 iSmart 客户端中登录的账号与配置文件中账号不符!')
choice = await ainput('继续使用可能会出现意料之外的问题,是否继续?[y/N]')
choice = input('继续使用可能会出现意料之外的问题,是否继续?[y/N]')
if choice.lower() != 'y':
return False
else:
logger.info('校验通过!')
return True

async def wait_for_book(self): # 等待「教材学习」页面
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:
if re.match(r'.*me.ismartlearning.cn/center/student/course/bookLearn.html.*', page['url']) and \
'webSocketDebuggerUrl' in page:
return Page(page['url'], page['webSocketDebuggerUrl'])
except httpx.ConnectError:
pass
await asyncio.sleep(2)

async def _any_http_page(self):
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:
if re.match(r'https?://.*', page['url']) and 'webSocketDebuggerUrl' in page:
if re.fullmatch(regexp, page['url']) and 'webSocketDebuggerUrl' in page:
return Page(page['url'], page['webSocketDebuggerUrl'])
except httpx.ConnectError:
pass
await asyncio.sleep(2)

async def submit(self, book_id, chapter_id, task_id, score, seconds, percent, user_id): # 提交任务点的得分
page = await self._any_http_page()
model = 'NetBrowser.submitTask("%s", "%s", "%s", 0, "%d", %d, %d, "%s");'
result = f'%7B%22studentid%22:{user_id},%22testInfo%22:%7B%22answerdata%22:%22%22,%22markdatao%22:%22%22%7D%7D'
return await page.eval(
model % (book_id, chapter_id, task_id, score, seconds, percent, result)
)
# noinspection PyTypeChecker
async def get_current(self):
async with httpx.AsyncClient() as client:
pages = (await client.get(f'http://127.0.0.1:{self.port}/json')).json()
for page in pages:
params = dict(parser.parse_qsl(parser.urlsplit(page['url']).query))
if 'bookId' in params:
return params['courseId'], params['bookId']
return params['courseId'], None


class Page(object):
Expand All @@ -113,3 +90,10 @@ async def eval(self, script):
}
)
return result['result']

async def submit(self, book_id, chapter_id, task_id, score, seconds, percent, user_id): # 提交任务点的得分
model = 'NetBrowser.submitTask("%s", "%s", "%s", 0, "%d", %d, %d, "%s");'
result = f'%7B%22studentid%22:{user_id},%22testInfo%22:%7B%22answerdata%22:%22%22,%22markdatao%22:%22%22%7D%7D'
return await self.eval(
model % (book_id, chapter_id, task_id, score, seconds, percent, result)
)
63 changes: 59 additions & 4 deletions automaton/spider/spider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@
from loguru import logger

from .captcha import recognize
from ..utils import Tree


class Tree: # 任务树
def __init__(self, task):
self.task = task
self.child = []

def sort(self):
self.child.sort(
key=lambda node: node.task['displayOrder']
)
for ch in self.child:
ch.sort()


class Spider(httpx.AsyncClient):
def __init__(self):
super().__init__()
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()}')
Expand All @@ -34,6 +50,7 @@ async def login(self, username, password):
logger.debug(info['result'])

assert info['result']['code'] == -26 # 断言登录结果
self.is_login = True
return info['result']

async def get_courses(self): # 获取课程列表
Expand All @@ -49,7 +66,13 @@ async def get_courses(self): # 获取课程列表

async def get_books(self, course_id): # 获取某课程的书籍列表
logger.info('正在获取书籍列表...')
await self.get_courses() # 必须有这个请求,否则后面会报错
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={
Expand All @@ -60,6 +83,7 @@ async def get_books(self, course_id): # 获取某课程的书籍列表

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={
Expand All @@ -69,9 +93,11 @@ async def get_tasks(self, book_id, book_type, 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
'unitStudyPercent': 0,
'name': book_name
})
for task_id in id_record:
node = id_record[task_id]
Expand All @@ -80,9 +106,10 @@ async def get_tasks(self, book_id, book_type, course_id): # 获取某书籍的
if (parent_id := node.task['parent_id']) in id_record:
id_record[parent_id].child.append(node)
else:
logger.warning(f'任务已忽略(父节点不存在{node_name}')
logger.warning(f'父节点不存在:{node_name}')
else:
root.child.append(node)
root.sort()
return root

async def get_paper(self, paper_id): # 获取任务点信息(包括题目和答案)
Expand All @@ -92,6 +119,7 @@ async def get_paper(self, paper_id): # 获取任务点信息(包括题目和
'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={
Expand All @@ -114,3 +142,30 @@ async def user_info(self):
return (await self.post(
'https://school.ismartlearning.cn/client/user/student-info')
).json()

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()
return book_info['data']
Loading

0 comments on commit d2adaeb

Please sign in to comment.