云学堂自动化学习脚本
此脚本的默认行为为打开主页,找到“我的任务”,如果有任务,则点击“开始学习”,在任务中心对每个任务进行遍历,直到所有进度完成。如果任务带有考试,那么打开考试网页并等待回答完毕,且正确率合格后继续进行。
如果没有任务,则打开“课程库”并按照“学习人数”排序,遍历所有分页执对没有完成学习的知识进行学习。
在任何学习过程中,每隔 10s 读取学习进度,如果进度完成则视为学习完成,如果弹框提示“不要走开”或者“你已在学习其他知识”,则自动点击确定跳过并继续学习。
在大部分情况下,不需要任何手动干预即可完成学习。
此脚本需要 Chrome、对应 Chrome 版本的 Chrome WebDriver,Babashka,登录所需凭证在 config.edn
中配置或暴露为环境变量 EDU_PASS
。可配合 clj-runner 实现双击脚本直接运行。
所需的工具除了 Chrome 浏览器外如下所示:
-
chromedriver.exe
Chrome WebDriver 驱动,需要和 Chrome 版本一致,下载
-
bb.exe
Babashka SCI 执行环境,需要将其暴露到 PATH,下载
-
config.edn
内容如下所示,或不提供此文件且直接暴露环境变量 EDU_PASS,格式为 {username}::{password}::{driver-path}。
{:user "" :pass "" :path-driver "chromedriver.exe"}
-
script.clj
内容如下所示,直接
clj-runner script.clj
执行或双击选择使用 clj-runner 打开(Windows)
#!/usr/bin/env bb -Sdeps '{:paths ["."] :deps {etaoin/etaoin {:mvn/version,"1.0.39"}}}'
(ns yxt-learn
"学习自动化,需要暴露变量 EDU_PASS,格式为 {username}::{password}::{driver-path} 或提供 config.edn 文件。"
(:require [clojure.string :as str]
[etaoin.api :as e]
[clojure.edn :as edn]
[clojure.java.io :as io])
(:import (java.time Duration LocalTime)
(java.util Base64)))
(def config (if (.exists (io/file "config.edn"))
(edn/read-string (slurp "config.edn"))
(if-let [pass-env (System/getenv "EDU_PASS")]
(let [pass (.split (String. (.decode (Base64/getDecoder) pass-env)) "::")]
{:user (first pass)
:pass (second pass)
:path-driver (last pass)})
{:user "xxx@xxx.com"
:pass "YOUR_PASS_HERE"
:path-driver (if (= "SCI" (-> *clojure-version* :qualifier))
"../resources/chromedriver.exe"
"resources/chromedriver.exe")})))
(defn wait-seconds [seconds stop]
(let [c (atom seconds)]
(while (and (> @c 0) (not @stop))
(Thread/sleep 1000)
(swap! c dec))))
(defn go-home [driver]
(e/go driver "https://LEARN_WEB_SITE.com")
(e/wait-visible driver {:id :btnLogin2})
(e/fill driver {:id :txtUserName2} (:user config))
(e/fill driver {:id :txtPassword2} (:pass config))
(e/click driver {:id :chkloginpass})
(e/click driver {:id :btnLogin2})
(e/wait-visible driver {:id :panel2})
(e/wait driver 2)
(println "task count: " (e/get-element-text driver {:id :panel2})
", exam count: " (e/get-element-text driver {:id :panel3})))
(defn goto-learn-page [driver]
;随机漫步
(e/click driver {:css "#divContents > div > div.banner-bg > div > div.remind.fr > div.infor-wrap > div > ul > li:nth-child(2) > a > div > div.text-trim.gray3"})
(e/wait-visible driver {:id :StudyPersonCount0})
(e/click driver {:id :StudyPersonCount0}))
(defn learn-doc [driver need-learn-minute block?]
(let [control (atom false)]
(let [need-learn
(fn [] (let [tasks (filterv #(not (str/includes? (or (e/get-element-text-el driver %) "") "已完成"))
(e/query-all driver [{:tag :ul :class "el-kng-img-list clearfix"} {:tag :li}]))]
(if (empty? tasks)
(do
(println "empty tasks this page, go next...")
(e/click driver {:css "a[title=\"下一页\"]"})
(e/wait driver 1)
(recur))
tasks)))
business
(fn [] (let [start-learn (LocalTime/now)]
(while true
(e/switch-window-next driver)
(e/refresh driver)
(let [task (first (need-learn))
passed-minutes (.toMinutes (Duration/between start-learn (LocalTime/now)))]
(cond (> passed-minutes need-learn-minute)
(throw (RuntimeException. "已完成学习"))
@control
(throw (RuntimeException. "停止学习"))
:else
(do (println "starting learning "
(str/replace (e/get-element-text-el driver task) "\n" ""))
(e/click-el driver task)
(println "waiting for new window...")
(e/switch-window-next driver)
(while (and (not= "100%" (e/get-element-text driver {:id :ScheduleText}))
(not @control))
(e/wait driver 1)
(println "waiting need time: " (e/get-element-text driver {:id :spanLeavTimes}))
(when (e/exists? driver {:id :reStartStudy})
(println "skipping hint...")
(e/click driver {:id :reStartStudy}))
(when (e/exists? driver {:css "#dvHeartTip > input:nth-child(5)"})
(println "force learn...")
(e/click driver {:css "#dvHeartTip > input:nth-child(5)"}))
(wait-seconds 10 control))
(e/close-window driver)
(println "stop this task learn...")))))))]
(if block?
(business)
(future (business)))
control)))
(defn week-learn [driver]
(e/click driver {:id "panel2"})
;判断是否有未完成任务,打开新标签学习,学完回来刷新继续判断
(e/wait driver 1)
(when (e/exists? driver {:id :contentitem1})
(e/click driver {:id :contentitem1})
(e/switch-window-next driver)
;学习任务总览页
(loop []
(let [all-task (->> (e/query-tree driver {:class :hand})
(map #(e/get-element-text-el driver %)))
all-unfinished-task-el
(filterv
(fn [ele]
(let [process (e/get-element-text-el driver ele)]
(and (not (nil? process))
(not (.endsWith (.trim process) "100%")))))
(e/query-all driver {:class :hand}))
all-unfinished-task-name
(map #(str/replace (e/get-element-text-el driver %)
"\n" "")
all-unfinished-task-el)]
(if-not (first all-unfinished-task-el)
(e/quit driver) ;如果当前任务已完成,则结束(最终流程)
(let []
;学习任务详情页
(e/wait driver 2)
(e/click-el driver (first all-unfinished-task-el))
(e/wait driver 2)
(e/switch-window-next driver)
(cond (e/exists? driver {:css "#btnStartStudy"})
(e/click driver {:css "#btnStartStudy"})
(e/exists? driver {:css "#btnContinueStudy"})
(e/click driver {:css "#btnContinueStudy"})
:else :done #_(throw (RuntimeException. "没有找到开始学习或继续学习按钮"))
;不一定,此处可能直接进入了单页面学习界面
)
(if (e/exists? driver {:id :btnTest}) ;如果是测试,停止并告知用户,反之开始学习
(do (e/click driver {:id :btnTest})
;如果是考试,等待通过并执行如下代码
(while (empty? (filterv
(fn [ele]
(let [name (e/get-element-text-el driver ele)]
(= (str/trim name) "通过")))
(e/query-all driver {:class "text-center"})))
(Thread/sleep 5000))
(recur))
(let [kill-future (atom false)]
(while (and (not (= "100%" (e/get-element-text driver {:id :ScheduleText})))
(not @kill-future))
(println (format "当前视频 [%s] 进度 %s,预计剩余时间 %s"
(e/get-element-text driver {:id :lblTitle})
(e/get-element-text driver {:id :ScheduleText})
(let [remain (e/get-element-text driver {:id :spanLeavTimes})]
(if (str/blank? remain)
"0 分钟" remain))))
(when (e/exists? driver {:id :reStartStudy})
(println "skipping hint...")
(e/click driver {:id :reStartStudy}))
(when (e/exists? driver {:css "#dvHeartTip > input:nth-child(5)"})
(println "force learn...")
(e/click driver {:css "#dvHeartTip > input:nth-child(5)"}))
(wait-seconds 20 kill-future))
(e/close-window driver)
(e/switch-window-next driver)
(e/reload driver)
(recur)))))))))
(defn -main [_]
(let [learn-minute 60
driver (e/chrome (merge {:path-driver (:path-driver config) :log-level :warn}
(if-let [chrome (:chrome config)] {:path-browser chrome} {})))]
(try
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (try (e/quit driver) (catch Exception _)))))
(go-home driver)
(if-let [res (e/get-element-text driver {:id :panel2})]
(if (= "0" res)
(do (println "task count 0, just walk!")
;2. 随机漫步:在文档学习页进行多次学习
(goto-learn-page driver)
(learn-doc driver learn-minute true))
(do (println "finding task count" res)
;1. 任务学习
(week-learn driver)))
(println "no task found! exit now!"))
(catch Exception e
(.printStackTrace e)
(try (e/quit driver) (catch Exception _))
(Thread/sleep 5000)
(-main nil))
(finally (try (e/quit driver) (catch Exception _))))))
(when (= (-> *clojure-version* :qualifier) "SCI")
(-main nil))