diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..eaf27d6 --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + work.slhaf + CxstudyAuto + 1.0 + + + + maven-jar-plugin + + + + work.slhaf.Main + + + + + + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + 1.18.36 + + + + + + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + work.slhaf.Main + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + junit + junit + 4.13.2 + test + + + hamcrest-core + org.hamcrest + + + + + + 21 + 21 + UTF-8 + + diff --git a/pom.xml b/pom.xml index 3837555..8b6823b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ work.slhaf CxstudyAuto - 1.0-SNAPSHOT + 1.0 @@ -43,7 +43,7 @@ org.projectlombok lombok 1.18.36 - provided + compile junit @@ -57,16 +57,6 @@ 4.13.2 test - - @@ -75,4 +65,71 @@ UTF-8 + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + work.slhaf.Main + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + 1.18.36 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + work.slhaf.Main + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/work/slhaf/Main.java b/src/main/java/work/slhaf/Main.java index 842b2d3..cdcce1e 100644 --- a/src/main/java/work/slhaf/Main.java +++ b/src/main/java/work/slhaf/Main.java @@ -3,23 +3,48 @@ package work.slhaf; import lombok.extern.slf4j.Slf4j; import work.slhaf.config.Config; import work.slhaf.task.CxstudyTask; +import work.slhaf.task.ZhsTask; import work.slhaf.util.AiUtil; import work.slhaf.util.OcrUtil; import java.io.IOException; +import java.util.Scanner; @Slf4j public class Main { public static void main(String[] args) throws InterruptedException, IOException { Config config = Config.load(); - if (config.isAiEnable()){ + if (config.isAiEnable()) { AiUtil.load(config.getAiConfig()); OcrUtil.load(config.getOcrConfig()); } + Scanner scanner = new Scanner(System.in); + int target; + String notice = """ + 1. 学习通 + 2. 智慧树 + """; + System.out.println(notice); + while (true) { + System.out.print("选择目标平台(输入序号): "); + target = scanner.nextInt(); + switch (target) { + case 1: + CxstudyTask cxstudyTask = CxstudyTask.initialize(config); + cxstudyTask.run(); + break; + case 2: + ZhsTask zhsTask = ZhsTask.initialize(config); + zhsTask.run(); + break; + default: + log.info("输入目标平台对应的序号!"); + continue; + } + break; + } - CxstudyTask cxstudyTask = CxstudyTask.initialize(config); - cxstudyTask.run(); } } \ No newline at end of file diff --git a/src/main/java/work/slhaf/config/Config.java b/src/main/java/work/slhaf/config/Config.java index da54264..19113e0 100644 --- a/src/main/java/work/slhaf/config/Config.java +++ b/src/main/java/work/slhaf/config/Config.java @@ -18,9 +18,10 @@ public class Config { private String browser; private boolean aiEnable; private boolean headless; - private CxstudyConfig cxstudyConfig; private OcrConfig ocrConfig; private AiConfig aiConfig; + private CxstudyConfig cxstudyConfig; + private ZhsConfig zhsConfig; private Config() { } @@ -61,7 +62,7 @@ public class Config { } System.out.println("\r\n---------------\r\n"); - System.out.println("是否显示GUI界面?第一次运行建议选择显示,测试没有问题后,再修改配置文件为隐藏GUI界面"); + System.out.println("是否启用无头模式(不显示GUI界面)?第一次运行建议选择禁用,测试没有问题后,再修改配置文件为启用无头模式"); System.out.print("输入 y (启用) 或 n (禁用) : "); String headlessStr = ""; boolean headless; @@ -118,10 +119,17 @@ public class Config { System.out.print("目标课程: "); studyConfig.setTarget(sc.nextLine()); + System.out.println("\r\n---------------\r\n"); + ZhsConfig zhsConfig = new ZhsConfig(); + System.out.println("记录智慧树配置(智慧树账号需绑定qq, 将通过qq登录): "); + System.out.print("目标课程: "); + zhsConfig.setTarget(sc.nextLine()); + System.out.println("\r\n---------------\r\n"); config.cxstudyConfig = studyConfig; config.ocrConfig = ocrConfig; config.aiConfig = aiConfig; + config.zhsConfig = zhsConfig; String configStr = JSONUtil.toJsonPrettyStr(config); System.out.println("配置文件如下: \r\n" + configStr); System.out.println("配置文件将位于: "+file.getAbsolutePath()); diff --git a/src/main/java/work/slhaf/config/ZhsConfig.java b/src/main/java/work/slhaf/config/ZhsConfig.java new file mode 100644 index 0000000..6a2725a --- /dev/null +++ b/src/main/java/work/slhaf/config/ZhsConfig.java @@ -0,0 +1,18 @@ +package work.slhaf.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ZhsConfig { + private String target = ""; + + @Override + public String toString() { + return """ + ZshConfig: + target: %s + """.formatted(target); + } +} diff --git a/src/main/java/work/slhaf/exception/TargetClassNotFoundException.java b/src/main/java/work/slhaf/exception/TargetClassNotFoundException.java new file mode 100644 index 0000000..59a8bb5 --- /dev/null +++ b/src/main/java/work/slhaf/exception/TargetClassNotFoundException.java @@ -0,0 +1,7 @@ +package work.slhaf.exception; + +public class TargetClassNotFoundException extends RuntimeException { + public TargetClassNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/work/slhaf/exception/UnknownTaskPointException.java b/src/main/java/work/slhaf/exception/UnknownTaskPointException.java new file mode 100644 index 0000000..65ba8df --- /dev/null +++ b/src/main/java/work/slhaf/exception/UnknownTaskPointException.java @@ -0,0 +1,7 @@ +package work.slhaf.exception; + +public class UnknownTaskPointException extends RuntimeException { + public UnknownTaskPointException(String message) { + super(message); + } +} diff --git a/src/main/java/work/slhaf/task/CxstudyTask.java b/src/main/java/work/slhaf/task/CxstudyTask.java index 17b41b7..2e2466a 100644 --- a/src/main/java/work/slhaf/task/CxstudyTask.java +++ b/src/main/java/work/slhaf/task/CxstudyTask.java @@ -1,17 +1,10 @@ package work.slhaf.task; import cn.hutool.json.JSONUtil; -import io.github.bonigarcia.wdm.WebDriverManager; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.*; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.edge.EdgeDriver; -import org.openqa.selenium.edge.EdgeOptions; -import org.openqa.selenium.firefox.FirefoxDriver; -import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; @@ -19,6 +12,7 @@ import work.slhaf.config.Config; import work.slhaf.exception.AiRequestException; import work.slhaf.exception.AiResponseFormatException; import work.slhaf.exception.OcrRequestException; +import work.slhaf.exception.TargetClassNotFoundException; import work.slhaf.pojo.answer.Answer; import work.slhaf.pojo.answer.MultiChoiceAnswer; import work.slhaf.pojo.answer.SingleChoiceAnswer; @@ -29,6 +23,7 @@ import work.slhaf.pojo.question.SingleChoiceQuestion; import work.slhaf.pojo.question.TextQuestion; import work.slhaf.util.AiUtil; import work.slhaf.util.OcrUtil; +import work.slhaf.util.TaskUtil; import java.io.File; import java.io.FileNotFoundException; @@ -42,7 +37,7 @@ import java.util.regex.Pattern; @Slf4j @Getter @Setter -public class CxstudyTask { +public class CxstudyTask { private WebDriver driver; private Actions actions; @@ -93,14 +88,14 @@ public class CxstudyTask { toBeFinished = getTodoTasks(); } - log.info("作答完毕!"); + log.info("任务完成!"); driver.quit(); break; - }catch (AiRequestException | OcrRequestException e) { + } catch (AiRequestException | OcrRequestException e) { log.error(e.getMessage()); driver.quit(); break; - }catch (WebDriverException e) { + } catch (WebDriverException e) { log.error("\r\n---------------\r\n"); log.error(e.getLocalizedMessage()); log.error("\r\n---------------\r\n"); @@ -108,24 +103,22 @@ public class CxstudyTask { driver.get("https://i.chaoxing.com/"); Thread.sleep(5000); switchPage(); - } } } - private void handleDocTask(int i) throws InterruptedException { try { driver.switchTo().frame(i).switchTo().frame(2); log.info("开始处理文档浏览..."); - long lastHeight = (long) executor.executeScript("return document.body.scrollHeight;"); - long currentHeight = 0; + double lastHeight = (double) executor.executeScript("return document.body.scrollHeight;"); + double currentHeight = 0; int step = 2000; while (currentHeight < lastHeight) { executor.executeScript("window.scrollBy(0, arguments[0]);", step); Thread.sleep(600); - currentHeight = (long) executor.executeScript("return window.scrollY + window.innerHeight;"); - lastHeight = (long) executor.executeScript("return document.body.scrollHeight;"); + currentHeight = (double) executor.executeScript("return document.body.scrollHeight;"); + lastHeight = (double) executor.executeScript("return document.body.scrollHeight;"); } log.info("文档浏览任务完成!"); } catch (WebDriverException ignored) { @@ -314,6 +307,7 @@ public class CxstudyTask { wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@class='prev_video_left']"))); log.info("开始处理视频播放..."); WebElement play = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//button[@class='vjs-big-play-button']"))); + executor.executeScript("arguments[0].scrollIntoView({behavior: 'auto', block: 'center'})", play); Thread.sleep(2000); play.click(); WebElement video = driver.findElement(By.id("video_html5_api")); @@ -342,13 +336,13 @@ public class CxstudyTask { int offsetY = (int) (Math.random() * step) - step / 2; // 随机上下移动 try { actions.moveByOffset(offsetX, offsetY).perform(); - }catch (Exception e) { + } catch (Exception e) { actions.moveToElement(video, startX, startY).perform(); } Thread.sleep(200); } String currentTime = driver.findElement(By.xpath(".//span[@class='vjs-current-time-display']")).getText(); - if (currentTime.isEmpty()){ + if (currentTime.isEmpty()) { actions.moveToElement(video, startX, startY).perform(); currentTime = driver.findElement(By.xpath(".//span[@class='vjs-current-time-display']")).getText(); } @@ -407,12 +401,17 @@ public class CxstudyTask { Thread.sleep(5); try { wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@name='课程']"))).click(); - }catch (TimeoutException e){ + } catch (TimeoutException e) { driver.findElement(By.xpath("//div[@name='课程']")).click(); } wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(0)); String xpath = ".//span[@title='" + targetClassName + "']"; - String targetClassHref = wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))).findElement(By.xpath("..")).getDomAttribute("href"); + String targetClassHref; + try { + targetClassHref = wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))).findElement(By.xpath("..")).getDomAttribute("href"); + }catch (Exception e){ + throw new TargetClassNotFoundException("未找到课程: "+targetClassName); + } driver.get(targetClassHref); driver.findElement(By.xpath("//a[@title='章节']")).click(); @@ -423,13 +422,7 @@ public class CxstudyTask { public static CxstudyTask initialize(Config config) { log.info("任务创建中..."); - WebDriverManager.firefoxdriver().clearDriverCache().setup(); - WebDriver driver = switch (config.getBrowser()) { - case "firefox" -> config.isHeadless() ? new FirefoxDriver(new FirefoxOptions().addArguments("--headless")) : new FirefoxDriver(); - case "edge" ->config.isHeadless() ? new EdgeDriver(new EdgeOptions().addArguments("--headless")) : new EdgeDriver(); - case "chrome" ->config.isHeadless() ? new ChromeDriver(new ChromeOptions().addArguments("--headless")) : new ChromeDriver(); - default -> throw new IllegalStateException("不支持的浏览器驱动!: " + config.getBrowser()); - }; + WebDriver driver = TaskUtil.initDriver(config); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); Actions actions = new Actions(driver); JavascriptExecutor executor = (JavascriptExecutor) driver; diff --git a/src/main/java/work/slhaf/task/ZhsTask.java b/src/main/java/work/slhaf/task/ZhsTask.java new file mode 100644 index 0000000..d39614d --- /dev/null +++ b/src/main/java/work/slhaf/task/ZhsTask.java @@ -0,0 +1,236 @@ +package work.slhaf.task; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.*; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import work.slhaf.config.Config; +import work.slhaf.exception.TargetClassNotFoundException; +import work.slhaf.exception.UnknownTaskPointException; +import work.slhaf.util.TaskUtil; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@Data +public class ZhsTask { + + private WebDriver driver; + private Actions actions; + private WebDriverWait wait; + private JavascriptExecutor executor; + private String targetClassName; + private boolean aiEnable; + + + public void run() throws InterruptedException { + login(); + while (true) { + try { + switchPage(); + List todoTasks = getTodoTasks(); + while (!todoTasks.isEmpty()) { + todoTasks.getFirst().click(); + System.out.println(); + try { + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='resources-list']"))); + } catch (Exception e) { + driver.get(driver.getCurrentUrl()); + } + HashMap todoTaskPoints = getTodoTaskPoints(); + log.info("发现{}个任务点", todoTaskPoints.size()); + AtomicInteger count = new AtomicInteger(0); + for (Map.Entry entry : todoTaskPoints.entrySet()) { + WebElement k = entry.getKey(); + String v = entry.getValue(); + count.incrementAndGet(); + log.info("处理第{}个任务点", count.get()); + switch (v) { + case "icon-box video" -> handleVideoTask(k); + case "icon-box baidu" -> handleWebTask(k); + case "icon-box book", "icon-box other", "icon-box img" -> handleDocTask(k); + default -> throw new UnknownTaskPointException("未设置处理策略的任务点类型!"); + } + } + driver.findElement(By.xpath("//img[@alt='back']")).click(); + todoTasks = getTodoTasks(); + } + log.info("任务完成!"); + driver.quit(); + break; + }catch (WebDriverException e) { + log.error("\r\n---------------\r\n"); + log.error(e.getLocalizedMessage()); + log.error("\r\n---------------\r\n"); + log.error("Something went wrong, will retry later.."); + driver.get("https://onlineweb.zhihuishu.com/onlinestuh5"); + Thread.sleep(5000); + } + } + } + + private void handleWebTask(WebElement task) throws InterruptedException { + //记录当前句柄 + String primaryHandle = driver.getWindowHandle(); + task.click(); + Thread.sleep(1000); + Set handles = driver.getWindowHandles(); + handles.forEach(s -> { + if (!s.equals(primaryHandle)) { + driver.switchTo().window(s); + driver.close(); + } + }); + driver.switchTo().window(primaryHandle); + } + + private void handleDocTask(WebElement task) throws InterruptedException { + task.click(); + Thread.sleep(2000); + } + + private void handleVideoTask(WebElement videoTask) throws InterruptedException { + videoTask.click(); + log.info("处理视频播放任务..."); + Thread.sleep(2000); + //模拟移动鼠标使元素显现 + actions.moveToElement(driver.findElement(By.xpath("//div[@class='video-progress']"))).perform(); + actions.moveByOffset(-10, -10).perform(); + + //开启倍速 + try { + driver.findElement(By.xpath("//div[@class='speedTab speedTab15']")).click(); + }catch (Exception ignored) {} + String currentTime = "00:00:00"; + String duration = "00:00:00"; + while (true) { + Thread.sleep(1000); + actions.moveToElement(driver.findElement(By.xpath("//div[@class='video-progress']"))).perform(); + actions.moveByOffset(-10, -10).perform(); + + try { + currentTime = driver.findElement(By.xpath(".//span[@class='currentTime']")).getText(); + duration = driver.findElement(By.xpath(".//span[@class='duration']")).getText(); + }catch (Exception ignored) {} + if (currentTime.equals("00:00:00")) { + continue; + }else { + log.info("{}/{}", currentTime, duration); + } + if (currentTime.equals(duration)) { + break; + } + } + log.info("视频播放任务完成"); + + + } + + private HashMap getTodoTaskPoints() { + HashMap todoTaskPoints = new HashMap<>(); + WebElement taskPointList = driver.findElement(By.xpath("//div[@class='resources-list']")); + List totalTaskPoints = taskPointList.findElements(By.xpath(".//div[@class='resources-item']")); + for (WebElement taskPoint : totalTaskPoints) { + try { + //如果找到完成标签,则直接进行下次循环 + taskPoint.findElement(By.xpath(".//div[@class='finished-icon']")); + continue; + } catch (Exception ignored) { + } + //根据div标签属于icon-box video还是icon-box baidu来确定处理策略,先放在hashmap中 + String classAttribute = taskPoint.findElement(By.xpath(".//div[contains(@class, 'icon-box')]")).getDomAttribute("class"); + todoTaskPoints.put(taskPoint, classAttribute); + } + return todoTaskPoints; + } + + private List getTodoTasks() { + List totalTasks; + List todoTasks = new ArrayList<>(); + boolean pct = false; + try { + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='item-content']"))); + totalTasks = driver.findElements(By.xpath("//div[@class='item-content']")); + } catch (TimeoutException ignored) { + try { + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='knowledge-point']"))); + } catch (TimeoutException e) { + throw new TimeoutException(); + } + pct = true; + totalTasks = driver.findElements(By.xpath("//div[@class='knowledge-point']")); + } + for (WebElement task : totalTasks) { + String progress; + if (pct) { + progress = task.findElement(By.xpath(".//div[@class='pct-text']")).getText(); + } else { + progress = task.findElement(By.xpath(".//span[@class='progress-num']")).getText(); + } + if (progress.equals("100%")) { + continue; + } + todoTasks.add(task); + } + log.info("获取到{}个任务", todoTasks.size()); + return todoTasks; + } + + private void switchPage() { + log.info("跳转中..."); + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//a[@class='courseName']"))); + List classes = driver.findElements(By.xpath("//a[@class='courseName']")); + boolean flag = false; + WebElement targetClass = null; + for (WebElement aClass : classes) { + if (aClass.getText().equals(targetClassName)) { + targetClass = aClass; + flag = true; + break; + } + } + if (!flag) { + throw new TargetClassNotFoundException("未找到课程: " + targetClassName); + } + targetClass.click(); + System.out.println(); + } + + private void login() { + log.info("登录中..."); + driver.get("https://onlineweb.zhihuishu.com/onlinestuh5"); + WebElement qqlogin = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//a[@class='wall-icon wall-qq']"))); + qqlogin.click(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("ptlogin_iframe")); + driver.findElement(By.xpath("//a[@class='face']")).click(); + log.info("登录成功!"); + } + + public static ZhsTask initialize(Config config) { + log.info("任务创建中..."); + WebDriver driver = TaskUtil.initDriver(config); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(8)); + Actions actions = new Actions(driver); + JavascriptExecutor executor = (JavascriptExecutor) driver; + + ZhsTask zhsTask = getZhsTask(config, driver, wait, actions, executor); + return zhsTask; + } + + private static ZhsTask getZhsTask(Config config, WebDriver driver, WebDriverWait wait, Actions actions, JavascriptExecutor executor) { + ZhsTask zhsTask = new ZhsTask(); + zhsTask.setDriver(driver); + zhsTask.setWait(wait); + zhsTask.setActions(actions); + zhsTask.setExecutor(executor); + + zhsTask.setTargetClassName(config.getZhsConfig().getTarget()); + + zhsTask.setAiEnable(config.isAiEnable()); + return zhsTask; + } +} diff --git a/src/main/java/work/slhaf/util/TaskUtil.java b/src/main/java/work/slhaf/util/TaskUtil.java new file mode 100644 index 0000000..3191e90 --- /dev/null +++ b/src/main/java/work/slhaf/util/TaskUtil.java @@ -0,0 +1,33 @@ +package work.slhaf.util; + +import io.github.bonigarcia.wdm.WebDriverManager; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.edge.EdgeDriver; +import org.openqa.selenium.edge.EdgeOptions; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import work.slhaf.config.Config; + +public class TaskUtil { + private TaskUtil() {} + + public static WebDriver initDriver(Config config) { + return switch (config.getBrowser()) { + case "firefox" -> { + WebDriverManager.firefoxdriver().clearDriverCache().setup(); + yield config.isHeadless() ? new FirefoxDriver(new FirefoxOptions().addArguments("--headless")) : new FirefoxDriver(); + } + case "edge" -> { + WebDriverManager.edgedriver().clearDriverCache().setup(); + yield config.isHeadless() ? new EdgeDriver(new EdgeOptions().addArguments("--headless")) : new EdgeDriver(); + } + case "chrome" -> { + WebDriverManager.chromedriver().clearDriverCache().setup(); + yield config.isHeadless() ? new ChromeDriver(new ChromeOptions().addArguments("--headless")) : new ChromeDriver(); + } + default -> throw new IllegalStateException("不支持的浏览器驱动!: " + config.getBrowser()); + }; + } +} diff --git a/src/test/java/ProjectTest.java b/src/test/java/ProjectTest.java index 8be733d..11aa6fa 100644 --- a/src/test/java/ProjectTest.java +++ b/src/test/java/ProjectTest.java @@ -5,7 +5,7 @@ import java.io.IOException; public class ProjectTest { - @Test +// @Test public void configTest() throws IOException { Config config = Config.load(); System.out.println(config.toString());