科举小抄

科举小抄 - 阿里云大学“科考”辅助工具

  1. // ==UserScript==
  2. // @name 科举小抄
  3. // @namespace https://github.com/fuckKeju/fuckKeju
  4. // @version 0.0.5
  5. // @description 科举小抄 - 阿里云大学“科考”辅助工具
  6. // @author fuckKeju
  7. // @match *.developer.aliyun.com/*
  8. // @run-at document-start
  9. // @grant unsafeWindow
  10. // ==/UserScript==
  11.  
  12. /* 题库数据 */
  13. var customQuestionsDatabase = []
  14. var useCustomQuestionsDatabase = false
  15.  
  16. async function getPageWindow () {
  17. return new Promise(function (resolve, reject) {
  18. if (window._pageWindow) {
  19. return resolve(window._pageWindow)
  20. }
  21.  
  22. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event']
  23.  
  24. function getWin (event) {
  25. window._pageWindow = this
  26. // debug.log('getPageWindow succeed', event)
  27. listenEventList.forEach(eventType => {
  28. window.removeEventListener(eventType, getWin, true)
  29. })
  30. resolve(window._pageWindow)
  31. }
  32.  
  33. listenEventList.forEach(eventType => {
  34. window.addEventListener(eventType, getWin, true)
  35. })
  36.  
  37. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  38. window.dispatchEvent(new window.Event('get-page-window-event'))
  39. })
  40. }
  41. getPageWindow()
  42.  
  43. /* 修正标题字符串 */
  44. function trimTitle (title, removeSerialNumber) {
  45. title = title || ''
  46. title = title.replace(/\s+/gi, ' ').replace(/\?{2,}/gi, ' ')
  47. if (removeSerialNumber) {
  48. title = title.replace(/^\d+\./, '')
  49. }
  50. return title
  51. }
  52.  
  53. /* 提取答案字符串 */
  54. function fixAnswer (answer) {
  55. answer = answer || ''
  56. return answer.replace(/^[A-Za-z]\.\s/, '').replace(/\s+--checked/, '')
  57. }
  58.  
  59. /**
  60. * 判断两条题目是否为同一条题目
  61. * @param questionA
  62. * @param questionB
  63. */
  64. function isTheSameQuestion (questionA, questionB) {
  65. let isSame = true
  66. const titleA = trimTitle(questionA.title, true)
  67. const titleB = trimTitle(questionB.title, true)
  68.  
  69. if (titleA === titleB) {
  70. for (let i = 0; i < questionA.answerList.length; i++) {
  71. const answerA = fixAnswer(questionA.answerList[i])
  72. let hasSameAnswer = false
  73. for (let j = 0; j < questionB.answerList.length; j++) {
  74. const answerB = fixAnswer(questionB.answerList[j])
  75. if (answerA === answerB) {
  76. hasSameAnswer = true
  77. break
  78. }
  79. }
  80.  
  81. if (!hasSameAnswer) {
  82. isSame = false
  83. break
  84. }
  85. }
  86. } else {
  87. isSame = false
  88. }
  89.  
  90. // isSame && console.log(titleA, titleB, isSame)
  91. return isSame
  92. }
  93.  
  94. /* 因为收集了部分异常数据,为了排查异常数据的干扰,所以需要进行是否异常的判断 */
  95. function isNormalQuestion (question) {
  96. return /\s+--checked/.test(JSON.stringify(question.answerList))
  97. }
  98.  
  99. function eachQuestionsDatabase (questionsDatabase, callback) {
  100. questionsDatabase.forEach((items, index) => {
  101. if (Array.isArray(items)) {
  102. items.forEach(curQuestion => {
  103. callback(curQuestion, index)
  104. })
  105. } else {
  106. callback(items, index)
  107. }
  108. })
  109. }
  110.  
  111. function getQuestionsDatabase () {
  112. const subjectEl = document.querySelector('.yq-main-examination .top-info h2.title-content')
  113. let questionsDatabase = []
  114. try {
  115. if (subjectEl) {
  116. questionsDatabase = JSON.parse(localStorage.getItem(subjectEl.innerText) || '[]')
  117. } else {
  118. questionsDatabase = customQuestionsDatabase
  119. }
  120. } catch (e) {
  121. questionsDatabase = []
  122. }
  123. return questionsDatabase
  124. }
  125.  
  126. /* 从混乱的题库集里提取整理后的题库 */
  127. function extractQuestionList (questionsDatabase) {
  128. const questionList = []
  129. let addCount = 0
  130.  
  131. function addToQuestionList (question) {
  132. addCount++
  133. // console.log(question, addCount)
  134. if (!question || !question.title || !Array.isArray(question.answerList)) {
  135. return false
  136. }
  137.  
  138. let hasSameQuestion = false
  139. for (let i = 0; i < questionList.length; i++) {
  140. const questionB = questionList[i]
  141. if (isTheSameQuestion(question, questionB)) {
  142. hasSameQuestion = true
  143. if (isNormalQuestion(question) && question.rightAnswer === '答案正确') {
  144. questionList[i] = question
  145. } else {
  146. questionList[i].relatedQuestions = questionList[i].relatedQuestions || []
  147. questionList[i].relatedQuestions.push(question)
  148. }
  149. break
  150. }
  151. }
  152.  
  153. if (!hasSameQuestion) {
  154. questionList.push(question)
  155. }
  156. }
  157.  
  158. eachQuestionsDatabase(questionsDatabase, (question, index) => {
  159. addToQuestionList(question, index)
  160. })
  161.  
  162. return questionList
  163. }
  164.  
  165. // console.log(extractQuestionList(customQuestionsDatabase))
  166.  
  167. /**
  168. * 从某个题库数据集里查找是否存在相关的题目
  169. * @param questionsDatabase
  170. * @param questions
  171. */
  172. function searchRelatedQuestions (questionsDatabase, questions) {
  173. let relatedQuestions = []
  174. eachQuestionsDatabase(questionsDatabase, (questionsA) => {
  175. if (isTheSameQuestion(questionsA, questions)) {
  176. relatedQuestions.push(questionsA)
  177. }
  178. })
  179.  
  180. /* 查找是否存在答对的历史记录,优先显示答对的数据 */
  181. if (relatedQuestions.length > 1) {
  182. const rightAnswerArr = []
  183. const wrongAnswerArr = []
  184. relatedQuestions.forEach(question => {
  185. if (question.rightAnswer === '答案正确' && isNormalQuestion(question)) {
  186. rightAnswerArr.push(question)
  187. } else {
  188. wrongAnswerArr.push(question)
  189. }
  190. })
  191. relatedQuestions = rightAnswerArr.concat(wrongAnswerArr)
  192. }
  193.  
  194. return relatedQuestions
  195. }
  196.  
  197. /**
  198. * 判断某条题目的相关问答库里是否包含一样的答案记录
  199. * @param questions
  200. * @param relatedQuestions
  201. */
  202. function hasTheSameQuestionsInRelatedQuestions (questions, relatedQuestions) {
  203. let hasSame = false
  204. relatedQuestions = relatedQuestions || []
  205.  
  206. for (let i = 0; i < relatedQuestions.length; i++) {
  207. const relatedQuestion = relatedQuestions[i]
  208. let isSame = true
  209. for (let j = 0; j < relatedQuestion.answerList.length; j++) {
  210. const answer = relatedQuestion.answerList[j]
  211. const relatedQuestionChecked = /\s+--checked/.test(answer)
  212. const questionsChecked = /\s+--checked/.test(questions.answerList[j])
  213. if (relatedQuestionChecked !== questionsChecked) {
  214. isSame = false
  215. break
  216. }
  217. }
  218.  
  219. if (isSame) {
  220. hasSame = true
  221. break
  222. }
  223. }
  224.  
  225. return hasSame
  226. }
  227.  
  228. /**
  229. * 遍历页面上的题目并进行回调,该方法必须在试题页面才能运行
  230. * @param callback
  231. * @returns {[]}
  232. */
  233. function eachQuestionItem (callback) {
  234. const result = []
  235. const isExamMode = document.querySelector('.yq-main-examination .time-info')
  236. const items = document.querySelectorAll('.question-panel .question-item')
  237. if (items) {
  238. items.forEach(questionItemEl => {
  239. const type = questionItemEl.querySelector('.q-title .q-tag').innerText.trim()
  240. const title = trimTitle(questionItemEl.querySelector('.q-title .q-t-text').innerText.trim())
  241. const answerList = []
  242. const answerListEl = questionItemEl.querySelectorAll('.q-option .answer-text')
  243. answerListEl.forEach(answerEl => {
  244. let answer = answerEl.innerText.trim()
  245. const checkedEl = answerEl.parentNode.querySelector('input')
  246. if (checkedEl && checkedEl.checked) {
  247. answer += ' --checked'
  248. }
  249. answerList.push(answer)
  250. })
  251.  
  252. const questionObj = {
  253. title,
  254. type,
  255. answerList
  256. }
  257.  
  258. const pointEl = questionItemEl.querySelector('.e-point .p-detail')
  259. if (pointEl) {
  260. questionObj.point = '相关知识点:' + pointEl.innerText.trim()
  261. } else {
  262. questionObj.point = '未匹配到任何相关知识点'
  263. }
  264.  
  265. const rightAnswerEl = questionItemEl.querySelector('.right-answer')
  266. if (rightAnswerEl) {
  267. questionObj.rightAnswer = rightAnswerEl.innerText.trim() || '答案正确'
  268. } else {
  269. if (isExamMode) {
  270. questionObj.rightAnswer = '答案未知'
  271. } else {
  272. questionObj.rightAnswer = '答案正确'
  273. }
  274. }
  275. result.push(questionObj)
  276.  
  277. if (callback instanceof Function) {
  278. try {
  279. callback(questionObj, questionItemEl)
  280. } catch (err) {
  281. console.error('eachQuestionItem error:', err, questionObj, questionItemEl)
  282. }
  283. }
  284. })
  285. }
  286. return result
  287. }
  288.  
  289. /* 添加相关题目内容到题目面板下面,并且添加显示隐藏事件 */
  290. function addRelatedQuestionsDom (questionItemEl, relatedQuestions) {
  291. const dd = document.createElement('dd')
  292. dd.setAttribute('class', 'relatedQuestions')
  293. dd.style.marginTop = '30px'
  294. dd.style.display = 'none'
  295. dd.style.border = '1px solid #ccc'
  296. dd.style.borderRadius = '5px'
  297. // dd.style.padding = '10px'
  298. // dd.style.backgroundColor = '#f9f9f9'
  299.  
  300. if (questionItemEl.querySelector('.relatedQuestions')) {
  301. questionItemEl.removeChild(questionItemEl.querySelector('.relatedQuestions'))
  302. }
  303.  
  304. if (relatedQuestions.length) {
  305. const codeEl = document.createElement('pre')
  306. codeEl.style.border = 'none'
  307. codeEl.innerHTML = JSON.stringify(relatedQuestions, null, 2)
  308. dd.appendChild(codeEl)
  309. questionItemEl.appendChild(dd)
  310. } else {
  311. dd.innerText = '暂无相关题目信息,先考几遍,然后查看考试结果再试试吧'
  312. questionItemEl.appendChild(dd)
  313. }
  314.  
  315. questionItemEl.ondblclick = function (event) {
  316. const relatedQuestions = questionItemEl.querySelector('.relatedQuestions')
  317. if (relatedQuestions) {
  318. if (relatedQuestions.style.display === 'none') {
  319. relatedQuestions.style.display = 'block'
  320. relatedQuestions.style.opacity = 0.4
  321. relatedQuestions.style.overflow = 'auto'
  322. relatedQuestions.style.maxHeight = '200px'
  323. } else {
  324. relatedQuestions.style.display = 'none'
  325. }
  326. }
  327. }
  328. }
  329.  
  330. /**
  331. * 自动匹配题目并尝试自动填充对应答案
  332. * @param questionsDatabase
  333. */
  334. function autoMatchQuestionAndCheckedAnswer (questionsDatabase) {
  335. eachQuestionItem((questions, questionItemEl) => {
  336. const relatedQuestions = searchRelatedQuestions(questionsDatabase, questions)
  337. if (relatedQuestions.length) {
  338. const relatedQuestion = relatedQuestions[0]
  339. if (isNormalQuestion(relatedQuestion) && relatedQuestion.rightAnswer === '答案正确') {
  340. relatedQuestion.answerList.forEach((answer, index) => {
  341. if (/\s+--checked/.test(answer)) {
  342. const answerLabel = questionItemEl.querySelectorAll('label.option-label')
  343. if (answerLabel[index]) {
  344. answerLabel[index].click()
  345. }
  346. }
  347. })
  348. }
  349. } else {
  350. console.log('以下题目无法匹配答案:', questions, questionItemEl, relatedQuestions)
  351. }
  352. })
  353. }
  354.  
  355. /* 隐藏相关题目面板 */
  356. function hideRelatedQuestions () {
  357. const relatedQuestionsEls = document.querySelectorAll('.relatedQuestions')
  358. relatedQuestionsEls.forEach(item => {
  359. item.style.display = 'none'
  360. })
  361. }
  362.  
  363. let hasInit = false
  364. async function fuckKeju () {
  365. if (hasInit) { return false }
  366. console.log('科举小抄 init suc')
  367. hasInit = true
  368.  
  369. const subjectTitle = document.querySelector('.yq-main-examination .top-info h2.title-content').innerText
  370. const isExamMode = document.querySelector('.yq-main-examination .time-info')
  371.  
  372. let questionsDatabase = getQuestionsDatabase()
  373.  
  374. /* 使用预置数据,而非定义的数据 */
  375. if (useCustomQuestionsDatabase) {
  376. questionsDatabase = customQuestionsDatabase
  377. }
  378.  
  379. let findNewQuestion = false
  380. const curQuestionsList = eachQuestionItem((questions, questionItemEl) => {
  381. const relatedQuestions = searchRelatedQuestions(questionsDatabase, questions)
  382. addRelatedQuestionsDom(questionItemEl, relatedQuestions)
  383.  
  384. /* 收集新题目数据 */
  385. if (!isExamMode && !hasTheSameQuestionsInRelatedQuestions(questions, relatedQuestions)) {
  386. findNewQuestion = true
  387. questionsDatabase.push(questions)
  388.  
  389. if (findNewQuestion) {
  390. console.log('发现新的题目,或新的答案记录:', questions)
  391. }
  392. }
  393. })
  394.  
  395. /* 提示到控制面板,用于手动收集题目数据 */
  396. console.log(JSON.stringify(curQuestionsList, null, 2))
  397.  
  398. /* 重新写入收集到的题目数据 */
  399. if (findNewQuestion) {
  400. // localStorage.setItem(subjectTitle, JSON.stringify(questionsDatabase))
  401. }
  402. localStorage.setItem(subjectTitle, JSON.stringify(questionsDatabase))
  403.  
  404. /* 考试模式下双击标题尝试自填充答案 */
  405. const subjectEl = document.querySelector('.yq-main-examination .top-info h2.title-content')
  406. subjectEl.ondblclick = function () {
  407. if (isExamMode) {
  408. autoMatchQuestionAndCheckedAnswer(questionsDatabase)
  409. }
  410. }
  411.  
  412. /* 切换题目时候,隐藏小抄 */
  413. const switchDoms = document.querySelectorAll('.question-num span.item')
  414. const switchDoms02 = document.querySelectorAll('.e-opt-panel a')
  415.  
  416. switchDoms.forEach(el => {
  417. el.onmouseenter = hideRelatedQuestions
  418. })
  419. switchDoms02.forEach(el => {
  420. el.onclick = hideRelatedQuestions
  421. })
  422.  
  423. /* 通过控制面板提取题库 */
  424. const pageWindow = await getPageWindow()
  425. pageWindow.extractQuestionList = function (print) {
  426. const questionsDatabase = getQuestionsDatabase()
  427. const questionList = extractQuestionList(questionsDatabase)
  428. if (print) {
  429. console.log(JSON.stringify(questionList, null, 2))
  430. }
  431. return questionList
  432. }
  433. }
  434.  
  435. function ready (selector, fn, shadowRoot) {
  436. const listeners = []
  437. const win = window
  438. const doc = shadowRoot || win.document
  439. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver
  440. let observer
  441.  
  442. function $ready (selector, fn) {
  443. // 储存选择器和回调函数
  444. listeners.push({
  445. selector: selector,
  446. fn: fn
  447. })
  448. if (!observer) {
  449. // 监听document变化
  450. observer = new MutationObserver(check)
  451. observer.observe(shadowRoot || doc.documentElement, {
  452. childList: true,
  453. subtree: true
  454. })
  455. }
  456. // 检查该节点是否已经在DOM中
  457. check()
  458. }
  459.  
  460. function check () {
  461. for (let i = 0; i < listeners.length; i++) {
  462. var listener = listeners[i]
  463. var elements = doc.querySelectorAll(listener.selector)
  464. for (let j = 0; j < elements.length; j++) {
  465. var element = elements[j]
  466. if (!element._isMutationReady_) {
  467. element._isMutationReady_ = true
  468. listener.fn.call(element, element)
  469. }
  470. }
  471. }
  472. }
  473.  
  474. $ready(selector, fn)
  475. }
  476.  
  477. ready('.question-panel .question-item', () => {
  478. /**
  479. * 此处必须延迟执行,题目渲染和选中渲染是异步操作
  480. * 需要延时等待选中的渲染成功才执行初始化逻辑
  481. */
  482. console.log('检查到进入了试题页面,即将为你初始化小抄逻辑')
  483. setTimeout(function () {
  484. fuckKeju()
  485. }, 1000 * 3)
  486. })

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址