commit 89413b0f2ab1b509f1fb76c05f39d0f484aefca2 Author: 范馨遥 <3603458499qq.com> Date: Mon Mar 30 22:00:08 2026 +0800 添加湖南大学选课系统分析项目到w4文件夹 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2028cc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Maven构建产物 +target/ + +# IDE相关文件 +.idea/ +.vscode/ + +# 数据库文件 +*.db + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志文件 +*.log + +# 临时文件 +*.tmp +*.temp + +# 批处理文件 +run.bat diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ef984db --- /dev/null +++ b/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + com.example + hnu-course-analysis + 1.0-SNAPSHOT + + 湖南大学选课系统分析 + 湖南大学选课系统数据爬取、分析与可视化 + + + 17 + 3.2.4 + 4.18.1 + 1.17.2 + 3.45.3.0 + + + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + + + org.jsoup + jsoup + ${jsoup.version} + + + org.xerial + sqlite-jdbc + ${sqlite.version} + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/src/main/java/com/example/HnuCourseAnalysisApplication.java b/src/main/java/com/example/HnuCourseAnalysisApplication.java new file mode 100644 index 0000000..83cbfaa --- /dev/null +++ b/src/main/java/com/example/HnuCourseAnalysisApplication.java @@ -0,0 +1,11 @@ +package com.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HnuCourseAnalysisApplication { + public static void main(String[] args) { + SpringApplication.run(HnuCourseAnalysisApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/analyzer/CourseAnalyzer.java b/src/main/java/com/example/analyzer/CourseAnalyzer.java new file mode 100644 index 0000000..e55e651 --- /dev/null +++ b/src/main/java/com/example/analyzer/CourseAnalyzer.java @@ -0,0 +1,88 @@ +package com.example.analyzer; + +import com.example.entity.Course; +import com.example.repository.CourseRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class CourseAnalyzer { + @Autowired + private CourseRepository courseRepository; + + // 获取整体统计信息 + public Map getOverallStatistics() { + Map statistics = new HashMap<>(); + try { + List allCourses = courseRepository.findAll(); + + // 总课程数 + statistics.put("totalCourses", allCourses.size()); + + // 总学分 + double totalCredits = allCourses.stream() + .mapToDouble(Course::getCredit) + .sum(); + statistics.put("totalCredits", totalCredits); + + // 平均学分 + double avgCredit = allCourses.isEmpty() ? 0 : totalCredits / allCourses.size(); + statistics.put("averageCredit", avgCredit); + + // 课程类型统计 + List> courseTypeStats = courseRepository.getCourseTypeStatistics(); + statistics.put("courseTypeStatistics", courseTypeStats); + + // 院系统计 + List> departmentStats = courseRepository.getDepartmentStatistics(); + statistics.put("departmentStatistics", departmentStats); + + // 选课情况统计 + List> enrollmentStats = courseRepository.getCourseEnrollmentStatistics(); + statistics.put("enrollmentStatistics", enrollmentStats); + } catch (Exception e) { + System.err.println("获取整体统计信息失败:" + e.getMessage()); + // 返回默认值 + statistics.put("totalCourses", 0); + statistics.put("totalCredits", 0); + statistics.put("averageCredit", 0); + statistics.put("courseTypeStatistics", List.of()); + statistics.put("departmentStatistics", List.of()); + statistics.put("enrollmentStatistics", List.of()); + } + return statistics; + } + + // 获取院系统计信息 + public List> getDepartmentStatistics() { + try { + return courseRepository.getDepartmentStatistics(); + } catch (Exception e) { + System.err.println("获取院系统计信息失败:" + e.getMessage()); + return List.of(); + } + } + + // 获取课程类型统计信息 + public List> getCourseTypeStatistics() { + try { + return courseRepository.getCourseTypeStatistics(); + } catch (Exception e) { + System.err.println("获取课程类型统计信息失败:" + e.getMessage()); + return List.of(); + } + } + + // 获取选课情况统计信息 + public List> getEnrollmentStatistics() { + try { + return courseRepository.getCourseEnrollmentStatistics(); + } catch (Exception e) { + System.err.println("获取选课情况统计信息失败:" + e.getMessage()); + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/config/WebConfig.java b/src/main/java/com/example/config/WebConfig.java new file mode 100644 index 0000000..758d25e --- /dev/null +++ b/src/main/java/com/example/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + // 可以在这里添加Web配置 +} \ No newline at end of file diff --git a/src/main/java/com/example/controller/CourseController.java b/src/main/java/com/example/controller/CourseController.java new file mode 100644 index 0000000..5fd0b45 --- /dev/null +++ b/src/main/java/com/example/controller/CourseController.java @@ -0,0 +1,74 @@ +package com.example.controller; + +import com.example.analyzer.CourseAnalyzer; +import com.example.crawler.CourseCrawler; +import com.example.entity.Course; +import com.example.repository.CourseRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Controller +public class CourseController { + @Autowired + private CourseCrawler courseCrawler; + + @Autowired + private CourseAnalyzer courseAnalyzer; + + @Autowired + private CourseRepository courseRepository; + + // 首页 + @GetMapping("/") + public String index() { + return "index"; + } + + // 爬取课程数据 + @PostMapping("/crawl") + @ResponseBody + public Map crawlCourses() { + List courses = courseCrawler.crawlCourses(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "成功爬取 " + courses.size() + " 门课程"); + response.put("data", courses); + return response; + } + + // 查看课程列表 + @GetMapping("/courses") + public String courseList(Model model) { + List courses = courseRepository.findAll(); + model.addAttribute("courses", courses); + return "course-list"; + } + + // 查看数据分析 + @GetMapping("/analysis") + public String analysis(Model model) { + Map statistics = courseAnalyzer.getOverallStatistics(); + model.addAttribute("statistics", statistics); + return "analysis"; + } + + // 获取分析数据(用于前端图表) + @GetMapping("/api/analysis") + @ResponseBody + public Map getAnalysisData() { + return courseAnalyzer.getOverallStatistics(); + } + + // 错误处理 + @GetMapping("/error") + public String error() { + return "error"; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/crawler/CourseCrawler.java b/src/main/java/com/example/crawler/CourseCrawler.java new file mode 100644 index 0000000..8654c0d --- /dev/null +++ b/src/main/java/com/example/crawler/CourseCrawler.java @@ -0,0 +1,95 @@ +package com.example.crawler; + +import com.example.entity.Course; +import com.example.repository.CourseRepository; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Component +public class CourseCrawler { + @Autowired + private LoginHandler loginHandler; + + @Autowired + private CourseRepository courseRepository; + + @Value("${crawler.hnu.course.list.url}") + private String courseListUrl; + + public List crawlCourses() { + WebDriver driver = null; + List courses = new ArrayList<>(); + + try { + // 登录 + driver = loginHandler.login(); + if (driver == null) { + System.err.println("登录失败,无法爬取课程数据"); + return courses; + } + + // 访问课程列表页面 + driver.get(courseListUrl); + + // 等待页面加载完成 + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + wait.until(ExpectedConditions.presenceOfElementLocated(By.className("gridtable"))); + + // 获取页面源码 + String pageSource = driver.getPageSource(); + + // 使用Jsoup解析页面 + Document doc = Jsoup.parse(pageSource); + Elements courseRows = doc.select(".gridtable tr:not(:first-child)"); + + // 解析课程数据 + for (Element row : courseRows) { + Elements cells = row.select("td"); + if (cells.size() >= 10) { + Course course = new Course(); + course.setCourseCode(cells.get(0).text().trim()); + course.setCourseName(cells.get(1).text().trim()); + course.setCredit(Double.parseDouble(cells.get(2).text().trim())); + course.setTeacher(cells.get(3).text().trim()); + course.setDepartment(cells.get(4).text().trim()); + course.setCapacity(Integer.parseInt(cells.get(5).text().trim())); + course.setEnrolled(Integer.parseInt(cells.get(6).text().trim())); + course.setClassTime(cells.get(7).text().trim()); + course.setClassRoom(cells.get(8).text().trim()); + course.setCourseType(cells.get(9).text().trim()); + course.setSemester("2024-2025-2"); + course.setCreateTime(LocalDateTime.now()); + + courses.add(course); + } + } + + // 保存到数据库 + courseRepository.saveAll(courses); + System.out.println("成功爬取并保存 " + courses.size() + " 门课程"); + + } catch (Exception e) { + System.err.println("爬取课程数据失败:" + e.getMessage()); + e.printStackTrace(); + } finally { + if (driver != null) { + loginHandler.logout(); + } + } + + return courses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/crawler/LoginHandler.java b/src/main/java/com/example/crawler/LoginHandler.java new file mode 100644 index 0000000..cffba4e --- /dev/null +++ b/src/main/java/com/example/crawler/LoginHandler.java @@ -0,0 +1,80 @@ +package com.example.crawler; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.time.Duration; + +@Component +public class LoginHandler { + @Value("${crawler.hnu.login.url}") + private String loginUrl; + + @Value("${crawler.hnu.username}") + private String username; + + @Value("${crawler.hnu.password}") + private String password; + + private WebDriver driver; + + public WebDriver login() { + setupDriver(); + try { + driver.get(loginUrl); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + + // 等待用户名输入框出现 + WebElement usernameElement = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("yhm"))); + usernameElement.sendKeys(username); + + // 输入密码 + WebElement passwordElement = driver.findElement(By.id("mm")); + passwordElement.sendKeys(password); + + // 点击登录按钮 + WebElement loginButton = driver.findElement(By.id("dl")); + loginButton.click(); + + // 等待登录成功 + wait.until(ExpectedConditions.titleContains("湖南大学")); + System.out.println("登录成功"); + return driver; + } catch (Exception e) { + System.err.println("登录失败:" + e.getMessage()); + e.printStackTrace(); + if (driver != null) { + driver.quit(); + } + return null; + } + } + + public void logout() { + if (driver != null) { + try { + // 这里可以添加退出登录的逻辑 + System.out.println("退出登录"); + } finally { + driver.quit(); + driver = null; + } + } + } + + private void setupDriver() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless"); // 无头模式 + options.addArguments("--disable-gpu"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + + driver = new ChromeDriver(options); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/entity/Course.java b/src/main/java/com/example/entity/Course.java new file mode 100644 index 0000000..168a03c --- /dev/null +++ b/src/main/java/com/example/entity/Course.java @@ -0,0 +1,157 @@ +package com.example.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Column; +import java.time.LocalDateTime; + +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "course_code") + private String courseCode; + + @Column(name = "course_name") + private String courseName; + + @Column(name = "credit") + private double credit; + + @Column(name = "teacher") + private String teacher; + + @Column(name = "department") + private String department; + + @Column(name = "capacity") + private int capacity; + + @Column(name = "enrolled") + private int enrolled; + + @Column(name = "class_time") + private String classTime; + + @Column(name = "class_room") + private String classRoom; + + @Column(name = "course_type") + private String courseType; + + @Column(name = "semester") + private String semester; + + @Column(name = "create_time") + private LocalDateTime createTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCourseCode() { + return courseCode; + } + + public void setCourseCode(String courseCode) { + this.courseCode = courseCode; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public double getCredit() { + return credit; + } + + public void setCredit(double credit) { + this.credit = credit; + } + + public String getTeacher() { + return teacher; + } + + public void setTeacher(String teacher) { + this.teacher = teacher; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + public int getCapacity() { + return capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + public int getEnrolled() { + return enrolled; + } + + public void setEnrolled(int enrolled) { + this.enrolled = enrolled; + } + + public String getClassTime() { + return classTime; + } + + public void setClassTime(String classTime) { + this.classTime = classTime; + } + + public String getClassRoom() { + return classRoom; + } + + public void setClassRoom(String classRoom) { + this.classRoom = classRoom; + } + + public String getCourseType() { + return courseType; + } + + public void setCourseType(String courseType) { + this.courseType = courseType; + } + + public String getSemester() { + return semester; + } + + public void setSemester(String semester) { + this.semester = semester; + } + + public LocalDateTime getCreateTime() { + return createTime; + } + + public void setCreateTime(LocalDateTime createTime) { + this.createTime = createTime; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/repository/CourseRepository.java b/src/main/java/com/example/repository/CourseRepository.java new file mode 100644 index 0000000..1c06f67 --- /dev/null +++ b/src/main/java/com/example/repository/CourseRepository.java @@ -0,0 +1,25 @@ +package com.example.repository; + +import com.example.entity.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; +import java.util.Map; + +@Repository +public interface CourseRepository extends JpaRepository { + // 根据课程类型查询 + List findByCourseType(String courseType); + + // 获取所有课程类型统计 + @Query(value = "SELECT course_type, COUNT(*) as count FROM courses GROUP BY course_type", nativeQuery = true) + List> getCourseTypeStatistics(); + + // 获取各院系课程数量统计 + @Query(value = "SELECT department, COUNT(*) as count FROM courses GROUP BY department", nativeQuery = true) + List> getDepartmentStatistics(); + + // 获取课程容量与已选人数统计 + @Query(value = "SELECT course_name, capacity, enrolled FROM courses", nativeQuery = true) + List> getCourseEnrollmentStatistics(); +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..06443bd --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,18 @@ +# 服务器配置 +server.port=8084 + +# SQLite数据库配置 +spring.datasource.url=jdbc:sqlite:course.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +spring.jpa.hibernate.ddl-auto=none +spring.jpa.open-in-view=false + +# 爬虫配置 +crawler.hnu.login.url=https://ids.hnu.edu.cn/authserver/login +crawler.hnu.course.list.url=https://jxgl.hnu.edu.cn/courseselect/welcome.do +crawler.hnu.username=test +crawler.hnu.password=test + +# Thymeleaf配置 +spring.thymeleaf.cache=false diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..38e275f --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS courses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + course_code TEXT, + course_name TEXT, + credit REAL, + teacher TEXT, + department TEXT, + capacity INTEGER, + enrolled INTEGER, + class_time TEXT, + class_room TEXT, + course_type TEXT, + semester TEXT, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 插入测试数据 +INSERT INTO courses (course_code, course_name, credit, teacher, department, capacity, enrolled, class_time, class_room, course_type, semester) +VALUES +('CS101', '计算机基础', 3.0, '张老师', '计算机学院', 100, 85, '周一 1-2节', '科技楼101', '必修课', '2024-2025-2'), +('MA201', '高等数学', 4.0, '李老师', '数学学院', 120, 110, '周二 3-4节', '教学楼201', '必修课', '2024-2025-2'), +('EN301', '英语口语', 2.0, '王老师', '外国语学院', 60, 55, '周三 5-6节', '外语楼301', '选修课', '2024-2025-2'), +('PH101', '大学物理', 3.0, '刘老师', '物理学院', 90, 80, '周四 1-2节', '物理楼101', '必修课', '2024-2025-2'), +('HI201', '中国历史', 2.0, '陈老师', '历史学院', 80, 75, '周五 3-4节', '文科楼201', '选修课', '2024-2025-2'); diff --git a/src/main/resources/templates/analysis.html b/src/main/resources/templates/analysis.html new file mode 100644 index 0000000..38b9188 --- /dev/null +++ b/src/main/resources/templates/analysis.html @@ -0,0 +1,171 @@ + + + + + + 数据分析 + + + + + + + 湖南大学选课系统分析 + + + + 首页 + + + 课程列表 + + + 数据分析 + + + + + + + + 数据分析 + + + + + + 总课程数 + + + + + + + + 总学分 + + + + + + + + 平均学分 + + + + + + + + + + + 课程类型分布 + + + + + + + + 院系统计 + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/course-list.html b/src/main/resources/templates/course-list.html new file mode 100644 index 0000000..2c169c3 --- /dev/null +++ b/src/main/resources/templates/course-list.html @@ -0,0 +1,63 @@ + + + + + + 课程列表 + + + + + + 湖南大学选课系统分析 + + + + 首页 + + + 课程列表 + + + 数据分析 + + + + + + + + 课程列表 + + + + 课程代码 + 课程名称 + 学分 + 教师 + 院系 + 容量 + 已选 + 上课时间 + 上课地点 + 课程类型 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..cf86f23 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,24 @@ + + + + + + 错误页面 + + + + + + + + + 500 - 服务器内部错误 + 服务器遇到了一个内部错误,请稍后再试。 + 返回首页 + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..e9347e7 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,76 @@ + + + + + + 湖南大学选课系统分析 + + + + + + + 湖南大学选课系统分析 + + + + 首页 + + + 课程列表 + + + 数据分析 + + + + + + + + + 湖南大学选课系统分析 + 本系统用于爬取、分析和可视化湖南大学选课系统的数据 + + 点击下方按钮开始爬取课程数据 + 爬取课程数据 + + + + + 爬取成功! + + + 查看课程列表 + 查看数据分析 + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/example/HnuCourseAnalysisApplicationTests.java b/src/test/java/com/example/HnuCourseAnalysisApplicationTests.java new file mode 100644 index 0000000..640998a --- /dev/null +++ b/src/test/java/com/example/HnuCourseAnalysisApplicationTests.java @@ -0,0 +1,13 @@ +package com.example; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HnuCourseAnalysisApplicationTests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file
服务器遇到了一个内部错误,请稍后再试。
本系统用于爬取、分析和可视化湖南大学选课系统的数据
点击下方按钮开始爬取课程数据