@ -1,2 +1,2 @@
|
|||||||
from .ismart import finish
|
from .spider import Spider
|
||||||
from .ismart import export
|
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'])
|