@ -1,2 +1,2 @@
|
||||
from .ismart import finish
|
||||
from .ismart import export
|
||||
from .spider import Spider
|
||||
from .devtools import Browser
|
||||
|
@ -1,122 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import urllib.parse as parser
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from loguru import logger
|
||||
from random import random, randint
|
||||
|
||||
from configs import configs
|
||||
from .devtools import Browser
|
||||
from .markdown import generate_md
|
||||
from .spider import Spider
|
||||
|
||||
random_args = { # 不同题型对应的随机时长和分数范围
|
||||
'1': { # 单选题
|
||||
'time': (20, 60), # 完成时长 / 秒
|
||||
'score': 1 # 得分 (归一化, 向上随机取至满分)
|
||||
},
|
||||
'2': { # 多选题
|
||||
'time': (40, 120),
|
||||
'score': 0.9
|
||||
},
|
||||
'3': { # 判断题
|
||||
'time': (20, 50),
|
||||
'score': 1
|
||||
},
|
||||
'4': { # 填空题
|
||||
'time': (60, 180),
|
||||
'score': 1
|
||||
},
|
||||
'6': { # 连线题
|
||||
'time': (60, 180),
|
||||
'score': 0.8
|
||||
},
|
||||
'8': { # 匹配题
|
||||
'time': (30, 90),
|
||||
'score': 1
|
||||
},
|
||||
'9': { # 口语跟读
|
||||
'time': (15, 30),
|
||||
'score': 0.8
|
||||
},
|
||||
'10': { # 短文改错
|
||||
'time': (120, 180),
|
||||
'score': 0.7
|
||||
},
|
||||
'11': { # 选词填空
|
||||
'time': (30, 90),
|
||||
'score': 0.9
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _random_progress(paper):
|
||||
paper = BeautifulSoup(paper, 'lxml-xml')
|
||||
questions = paper.select('element[knowledge]:has(> question_type)')
|
||||
if questions:
|
||||
total_score = 0
|
||||
my_score, my_time = 0, 0
|
||||
for que in questions:
|
||||
qt_type = que.select_one('question_type').text
|
||||
qt_score = int(que.select_one('question_score').text)
|
||||
total_score += qt_score
|
||||
|
||||
rate = 1 - (1 - random_args[qt_type]['score']) * random()
|
||||
my_score += qt_score * rate
|
||||
my_time += randint(*random_args[qt_type]['time'])
|
||||
return int(100 * my_score / total_score), my_time
|
||||
return 100, 5
|
||||
|
||||
|
||||
async def export(): # 导出某书籍的答案
|
||||
browser = Browser.connect()
|
||||
page = await browser.wait_for_book()
|
||||
params = dict(parser.parse_qsl(parser.urlsplit(page.url).query))
|
||||
# noinspection PyTypeChecker
|
||||
book_id, course_id = params['bookId'], params['courseId']
|
||||
if not os.path.exists(f'.cache/books/{book_id}'):
|
||||
async with Spider() as spider:
|
||||
await spider.login(**configs['user'])
|
||||
book = await spider.book_info(book_id)
|
||||
book['courseId'] = course_id
|
||||
tasks = await spider.get_tasks(book, tree=True)
|
||||
await spider.download_tree(tasks)
|
||||
with open(f'.cache/books/{book_id}/Tree.pck', 'rb') as fp:
|
||||
generate_md(pickle.load(fp))
|
||||
|
||||
|
||||
async def finish(): # 直接完成某书籍的任务
|
||||
browser = Browser.connect()
|
||||
page = await browser.wait_for_book()
|
||||
params = dict(parser.parse_qsl(parser.urlsplit(page.url).query))
|
||||
# noinspection PyTypeChecker
|
||||
book_id, course_id = params['bookId'], params['courseId']
|
||||
async with Spider() as spider:
|
||||
await spider.login(**configs['user'])
|
||||
if not os.path.exists(f'.cache/books/{book_id}'):
|
||||
book = await spider.book_info(book_id)
|
||||
book['courseId'] = course_id
|
||||
tasks = await spider.get_tasks(book, tree=True)
|
||||
await spider.download_tree(tasks)
|
||||
user_id = (await spider.get_user())['data']['uid']
|
||||
logger.info('正在提交任务...')
|
||||
for file in os.listdir(f'.cache/books/{book_id}'):
|
||||
paper_id, ext = os.path.splitext(file)
|
||||
if ext != '.json':
|
||||
continue
|
||||
|
||||
with open(f'.cache/books/{book_id}/{file}') as fp:
|
||||
data = json.load(fp)
|
||||
task = data['task']
|
||||
paper = data['paperData']
|
||||
score, time = _random_progress(paper)
|
||||
result = await page.submit(book_id, task['chapterId'], task['id'], score, time, 100, user_id)
|
||||
if result['wasThrown'] or not result['result']['value']:
|
||||
logger.warning(f'任务 {task["name"]} [paperId: {paper_id}] 可能提交失败,请留意最终结果!')
|
||||
logger.info('全部提交完成!')
|
||||
|
||||
|
||||
async def finish_all(): # Todo: 全刷了?
|
||||
pass
|
@ -1 +0,0 @@
|
||||
from .md import generate_md
|
@ -1,34 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
class Formatter:
|
||||
@staticmethod
|
||||
def fix_img(text): # 处理 <img/> 标签
|
||||
return re.sub('<img.+?>', '「暂不支持图片显示澳」', text)
|
||||
|
||||
@staticmethod
|
||||
def rm_lgt(text): # 处理括号对
|
||||
return re.sub('<.+?>', '', text)
|
||||
|
||||
@staticmethod
|
||||
def fix_uline(text): # 处理下划线
|
||||
return re.sub('_{3,}', lambda mch: '\\_' * len(mch.group()), text)
|
||||
|
||||
@staticmethod
|
||||
def rm_head(text): # 处理数字标号
|
||||
return re.sub(r'^(?:\d+(?:\.| +\b))+\d+ ', '', text)
|
||||
|
||||
@staticmethod
|
||||
def fix_lf(text): # 处理换行
|
||||
text = re.sub('<br/?>', '\n\n', text)
|
||||
return re.sub('<p>(.+?)</p>', lambda mch: mch.group(1) + '\n\n', text)
|
||||
|
||||
@staticmethod
|
||||
def fix_space(text):
|
||||
return re.sub('(?: )+', ' ', text)
|
||||
|
||||
|
||||
def fix(text, func_ptrs):
|
||||
for func in func_ptrs:
|
||||
text = getattr(Formatter, func)(text)
|
||||
return text
|
@ -1,133 +0,0 @@
|
||||
"""
|
||||
不同 question type 对应的解析方法
|
||||
传入两个参数 ( question, answer, output ), 将输出行依次 append 到 output 队列中
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from .formatter import fix
|
||||
|
||||
|
||||
class Generators:
|
||||
@staticmethod
|
||||
def type_1(que, ans, output): # 单选题
|
||||
# 提取题目内容
|
||||
question = que.select_one("question_text").text
|
||||
question = fix(question, ('rm_lgt', 'fix_uline', 'fix_space'))
|
||||
output.append(f'* **{question}**\n')
|
||||
# 提取答案
|
||||
ans_id = que.attrs['id']
|
||||
corrects = set(ans.select_one(f'[id="{ans_id}"] > answers').text)
|
||||
# 生成对应 Markdown
|
||||
options = que.select('options > *')
|
||||
for opt in options:
|
||||
opt_id = opt.attrs['id']
|
||||
answer_text = fix(opt.text, ('rm_lgt', 'fix_space'))
|
||||
if opt_id in corrects: # 高亮正确答案
|
||||
output.append(f'<p><font color="#2ed573">  <b>{opt_id}.</b> {answer_text}</font></p>\n')
|
||||
else:
|
||||
output.append(f'  <b>{opt_id}.</b> {answer_text}\n')
|
||||
|
||||
@staticmethod
|
||||
def type_2(*args): # 多选题
|
||||
return Generators.type_1(*args)
|
||||
|
||||
@staticmethod
|
||||
def type_3(que, ans, output): # 判断题
|
||||
question = que.select_one("question_text").text
|
||||
question = fix(question, ('rm_lgt', 'fix_uline', 'fix_space'))
|
||||
output.append(f'* **{question}**\n')
|
||||
# 提取答案
|
||||
ans_id = que.attrs['id']
|
||||
correct = ans.select_one(f'[id="{ans_id}"] > answers').text
|
||||
# 生成对应 Markdown
|
||||
output.append(f'* 答案:「**{correct}**」\n')
|
||||
|
||||
@staticmethod
|
||||
def type_4(que, ans, output): # 填空题
|
||||
# 提取题目内容
|
||||
question = que.select_one('question_text').text
|
||||
question = re.sub('<br/?>', '\n', question)
|
||||
question = fix(question, ('rm_lgt', 'fix_uline', 'fix_space'))
|
||||
# 提取答案
|
||||
ans_id = que.attrs['id']
|
||||
corrects = ans.select(f'[id="{ans_id}"] answers > answer')
|
||||
# 执行替换
|
||||
for ans in corrects:
|
||||
question = question.replace(
|
||||
'{{' + ans.attrs['id'] + '}}',
|
||||
f' <font color="#2ed573"><b>[{ans.text}]</b></font> '
|
||||
)
|
||||
output.append(question + '\n')
|
||||
|
||||
@staticmethod
|
||||
def type_6(que, ans, output): # 连线题
|
||||
# 提取题目内容
|
||||
question = que.select_one('question_text').text
|
||||
question = fix(question, ('rm_lgt', 'fix_uline', 'fix_space'))
|
||||
output.append(f'* **{question}**\n')
|
||||
# 提取答案
|
||||
options = que.select('options > *')
|
||||
pairs = {}
|
||||
for opt in options:
|
||||
opt_id = opt.attrs['id']
|
||||
if opt_id not in pairs:
|
||||
pairs[opt_id] = [0, 0]
|
||||
flag = int(opt.attrs['flag'])
|
||||
pairs[opt_id][flag - 1] = opt.text
|
||||
output.append('| Part-A | Part-B |')
|
||||
output.append('| :- | :- |')
|
||||
for gp_id in pairs:
|
||||
left = fix(pairs[gp_id][0], ('fix_img', 'rm_lgt', 'fix_uline', 'fix_space')).replace('|', '\\|')
|
||||
right = fix(pairs[gp_id][1], ('fix_img', 'rm_lgt', 'fix_uline', 'fix_space')).replace('|', '\\|')
|
||||
output.append(f'| {left} | {right} |')
|
||||
output.append('')
|
||||
|
||||
@staticmethod
|
||||
def type_8(que, ans, output): # 匹配题
|
||||
# 提取题目内容
|
||||
question = que.select_one('question_text').text
|
||||
question = fix(question, ('rm_lgt', 'fix_uline'))
|
||||
# 提取答案
|
||||
ans_id = que.attrs['id']
|
||||
corrects = ans.select(f'[id="{ans_id}"] answers > answer')
|
||||
# 执行替换
|
||||
question = fix(question, ('fix_lf', 'rm_lgt', 'fix_space'))
|
||||
for ans in corrects:
|
||||
question = question.replace(
|
||||
'{{' + ans.attrs['id'] + '}}',
|
||||
f' <font color="#2ed573"><b>{ans.text}</b></font> '
|
||||
)
|
||||
output.append(question + '\n')
|
||||
|
||||
@staticmethod
|
||||
def type_9(que, ans, output): # 口语跟读
|
||||
output.append('「口语跟读」\n')
|
||||
|
||||
@staticmethod
|
||||
def type_10(que, ans, output): # 短文改错
|
||||
output.append('* **短文改错**')
|
||||
ans_id = que.attrs['id']
|
||||
corrects = ans.select(f'[id="{ans_id}"] answers > answer')
|
||||
for i, ans in enumerate(corrects):
|
||||
desc = re.sub('(?<=[A-Za-z0-9])(?=[\u4e00-\u9fa5])', ' ', ans.attrs['desc'])
|
||||
desc = re.sub('(?<=[\u4e00-\u9fa5])(?=[A-Za-z0-9])', ' ', desc)
|
||||
output.append(f'{i + 1}. {desc}\n')
|
||||
output.append('')
|
||||
|
||||
@staticmethod
|
||||
def type_11(que, ans, output): # 选词填空
|
||||
# 提取题目内容
|
||||
question = que.select_one('question_text').text
|
||||
question = fix(question, ('fix_uline', 'fix_lf', 'rm_lgt', 'fix_space'))
|
||||
options = {opt.attrs['id']: opt.text for opt in que.select('options > option[flag="2"]')}
|
||||
# 提取答案
|
||||
ans_id = que.attrs['id']
|
||||
corrects = ans.select(f'[id="{ans_id}"] answers > answer')
|
||||
# 执行替换
|
||||
for ans in corrects:
|
||||
question = question.replace(
|
||||
'{{' + ans.attrs['id'] + '}}',
|
||||
f' <font color="#2ed573"><b>{options[ans.text]}</b></font> '
|
||||
)
|
||||
output.append(question + '\n')
|
@ -1,59 +0,0 @@
|
||||
import json
|
||||
from collections import deque
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from loguru import logger
|
||||
|
||||
from .formatter import fix
|
||||
from .generator import Generators
|
||||
|
||||
_output = deque()
|
||||
|
||||
|
||||
# 解码题目与答案 xml
|
||||
def decode(que, ans, qt_type):
|
||||
getattr(Generators, f'type_{qt_type}')(que, ans, _output)
|
||||
|
||||
|
||||
# 生成每个 paper 的答案
|
||||
def unescape(node, book_id):
|
||||
paper_id = node.task['paperId']
|
||||
with open(f'.cache/books/{book_id}/{paper_id}.json', 'r') as fp:
|
||||
task = json.load(fp)
|
||||
paper = BeautifulSoup(task['paperData'], 'lxml-xml')
|
||||
answer = BeautifulSoup(task['answerData'], 'lxml-xml')
|
||||
questions = paper.select('element[knowledge]:has(> question_type)')
|
||||
if questions:
|
||||
for que in questions:
|
||||
qt_type = int(que.select_one('question_type').text)
|
||||
decode(que, answer, qt_type)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# 深搜创建目录树
|
||||
def dfs(node, book_id, depth=2):
|
||||
if title := node.task['name']:
|
||||
logger.info(f'{". " * (depth - 1)}{title}')
|
||||
title = fix(title, ('rm_head',))
|
||||
_output.append(f'{"#" * depth} {title}\n')
|
||||
flag = False
|
||||
if 'paperId' in node.task:
|
||||
flag = unescape(node, book_id)
|
||||
for ch in node.children:
|
||||
if dfs(ch, book_id, depth + 1):
|
||||
flag = True
|
||||
if not flag:
|
||||
_output.pop()
|
||||
return flag
|
||||
|
||||
|
||||
def generate_md(root): # 生成答案
|
||||
book_id = root.task['book_id']
|
||||
for ch in root.children:
|
||||
dfs(ch, book_id)
|
||||
with open('.cache/answer.md', 'w', encoding='utf-8') as file:
|
||||
while len(_output):
|
||||
line = _output.popleft()
|
||||
file.write(line + '\n')
|
||||
logger.info('Done.')
|
@ -0,0 +1 @@
|
||||
from .spider import *
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 410 B After Width: | Height: | Size: 410 B |
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 429 B |
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 404 B |
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 395 B |
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 403 B |
Before Width: | Height: | Size: 414 B After Width: | Height: | Size: 414 B |
Before Width: | Height: | Size: 417 B After Width: | Height: | Size: 417 B |
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
@ -0,0 +1,33 @@
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import urllib.parse as parser
|
||||
|
||||
from configs import configs
|
||||
from .devtools import Browser
|
||||
from .spider import Spider
|
||||
|
||||
|
||||
class Tree: # 任务树
|
||||
def __init__(self, task):
|
||||
self.task = task
|
||||
self.child = []
|
||||
|
||||
|
||||
async def ainput(prompt: str = ''):
|
||||
with ThreadPoolExecutor(1, 'ainput') as executor:
|
||||
return (
|
||||
await asyncio.get_event_loop().run_in_executor(executor, input, prompt)
|
||||
).rstrip()
|
||||
|
||||
|
||||
async def flash_recent(): # 对当前书籍执行刷课
|
||||
if configs['browser']['mode'] == 'launch':
|
||||
browser = Browser.launch()
|
||||
else:
|
||||
browser = Browser.connect()
|
||||
page = await browser.wait_for_book()
|
||||
params = dict(parser.parse_qsl(parser.urlsplit(page.url).query))
|
||||
# noinspection PyTypeChecker
|
||||
book_id, course_id = params['bookId'], params['courseId']
|
||||
async with Spider() as spider:
|
||||
await spider.login(**configs['user'])
|