diff --git a/W4/src/Circle.java b/W4/src/Circle.java
new file mode 100644
index 0000000..8d6bf42
--- /dev/null
+++ b/W4/src/Circle.java
@@ -0,0 +1,4 @@
+package PACKAGE_NAME;
+
+public class Circle {
+}
diff --git a/W4/src/Rectangle.java b/W4/src/Rectangle.java
new file mode 100644
index 0000000..ddd6d25
--- /dev/null
+++ b/W4/src/Rectangle.java
@@ -0,0 +1,4 @@
+package PACKAGE_NAME;
+
+public class Rectangle {
+}
diff --git a/W4/src/Shape.java b/W4/src/Shape.java
new file mode 100644
index 0000000..3a5ad1a
--- /dev/null
+++ b/W4/src/Shape.java
@@ -0,0 +1,4 @@
+package PACKAGE_NAME;
+
+public class Shape {
+}
diff --git a/W4/src/ShapeUtil.java b/W4/src/ShapeUtil.java
new file mode 100644
index 0000000..75fb22a
--- /dev/null
+++ b/W4/src/ShapeUtil.java
@@ -0,0 +1,4 @@
+package PACKAGE_NAME;
+
+public class ShapeUtil {
+}
diff --git a/W4/src/Triangle.java b/W4/src/Triangle.java
new file mode 100644
index 0000000..856df3a
--- /dev/null
+++ b/W4/src/Triangle.java
@@ -0,0 +1,4 @@
+package PACKAGE_NAME;
+
+public class Triangle {
+}
diff --git a/project/charts/province_distribution_2022.png b/project/charts/province_distribution_2022.png
new file mode 100644
index 0000000..21ca053
Binary files /dev/null and b/project/charts/province_distribution_2022.png differ
diff --git a/project/charts/province_distribution_2023.png b/project/charts/province_distribution_2023.png
new file mode 100644
index 0000000..783ca23
Binary files /dev/null and b/project/charts/province_distribution_2023.png differ
diff --git a/project/charts/province_distribution_2024.png b/project/charts/province_distribution_2024.png
new file mode 100644
index 0000000..b68155d
Binary files /dev/null and b/project/charts/province_distribution_2024.png differ
diff --git a/project/charts/rank_trend_上海交通大学.png b/project/charts/rank_trend_上海交通大学.png
new file mode 100644
index 0000000..2bc6b2c
Binary files /dev/null and b/project/charts/rank_trend_上海交通大学.png differ
diff --git a/project/charts/rank_trend_北京大学.png b/project/charts/rank_trend_北京大学.png
new file mode 100644
index 0000000..bdf6fa1
Binary files /dev/null and b/project/charts/rank_trend_北京大学.png differ
diff --git a/project/charts/rank_trend_复旦大学.png b/project/charts/rank_trend_复旦大学.png
new file mode 100644
index 0000000..e25a446
Binary files /dev/null and b/project/charts/rank_trend_复旦大学.png differ
diff --git a/project/charts/rank_trend_浙江大学.png b/project/charts/rank_trend_浙江大学.png
new file mode 100644
index 0000000..ee484af
Binary files /dev/null and b/project/charts/rank_trend_浙江大学.png differ
diff --git a/project/charts/rank_trend_清华大学.png b/project/charts/rank_trend_清华大学.png
new file mode 100644
index 0000000..aae460e
Binary files /dev/null and b/project/charts/rank_trend_清华大学.png differ
diff --git a/project/charts/top10_2022.png b/project/charts/top10_2022.png
new file mode 100644
index 0000000..793e19e
Binary files /dev/null and b/project/charts/top10_2022.png differ
diff --git a/project/charts/top10_2023.png b/project/charts/top10_2023.png
new file mode 100644
index 0000000..1f08206
Binary files /dev/null and b/project/charts/top10_2023.png differ
diff --git a/project/charts/top10_2024.png b/project/charts/top10_2024.png
new file mode 100644
index 0000000..8309920
Binary files /dev/null and b/project/charts/top10_2024.png differ
diff --git a/project/data/university_rank_2022.csv b/project/data/university_rank_2022.csv
new file mode 100644
index 0000000..95b84ec
--- /dev/null
+++ b/project/data/university_rank_2022.csv
@@ -0,0 +1,21 @@
+"排名","学校名称","省份","总分","年份"
+"1","清华大学","北京","852.5","2022"
+"2","北京大学","北京","848.2","2022"
+"3","浙江大学","浙江","822.5","2022"
+"4","上海交通大学","上海","815.3","2022"
+"5","复旦大学","上海","805.1","2022"
+"6","南京大学","江苏","785.6","2022"
+"7","中国科学技术大学","安徽","782.4","2022"
+"8","华中科技大学","湖北","765.8","2022"
+"9","武汉大学","湖北","758.2","2022"
+"10","西安交通大学","陕西","752.6","2022"
+"11","中山大学","广东","745.3","2022"
+"12","四川大学","四川","738.9","2022"
+"13","哈尔滨工业大学","黑龙江","732.5","2022"
+"14","北京航空航天大学","北京","725.8","2022"
+"15","东南大学","江苏","718.4","2022"
+"16","北京理工大学","北京","712.6","2022"
+"17","同济大学","上海","705.3","2022"
+"18","中国人民大学","北京","698.5","2022"
+"19","北京师范大学","北京","692.1","2022"
+"20","南开大学","天津","685.7","2022"
diff --git a/project/data/university_rank_2023.csv b/project/data/university_rank_2023.csv
new file mode 100644
index 0000000..aa9f005
--- /dev/null
+++ b/project/data/university_rank_2023.csv
@@ -0,0 +1,21 @@
+"排名","学校名称","省份","总分","年份"
+"1","清华大学","北京","853.0","2023"
+"2","北京大学","北京","848.7","2023"
+"3","浙江大学","浙江","823.0","2023"
+"4","上海交通大学","上海","815.8","2023"
+"5","复旦大学","上海","805.6","2023"
+"6","南京大学","江苏","786.1","2023"
+"7","中国科学技术大学","安徽","782.9","2023"
+"8","华中科技大学","湖北","766.3","2023"
+"9","武汉大学","湖北","758.7","2023"
+"10","西安交通大学","陕西","753.1","2023"
+"11","中山大学","广东","745.8","2023"
+"12","四川大学","四川","739.4","2023"
+"13","哈尔滨工业大学","黑龙江","733.0","2023"
+"14","北京航空航天大学","北京","726.3","2023"
+"15","东南大学","江苏","718.9","2023"
+"16","北京理工大学","北京","713.1","2023"
+"17","同济大学","上海","705.8","2023"
+"18","中国人民大学","北京","699.0","2023"
+"19","北京师范大学","北京","692.6","2023"
+"20","南开大学","天津","686.2","2023"
diff --git a/project/data/university_rank_2024.csv b/project/data/university_rank_2024.csv
new file mode 100644
index 0000000..266b4a3
--- /dev/null
+++ b/project/data/university_rank_2024.csv
@@ -0,0 +1,21 @@
+"排名","学校名称","省份","总分","年份"
+"1","清华大学","北京","853.5","2024"
+"2","北京大学","北京","849.2","2024"
+"3","浙江大学","浙江","823.5","2024"
+"4","上海交通大学","上海","816.3","2024"
+"5","复旦大学","上海","806.1","2024"
+"6","南京大学","江苏","786.6","2024"
+"7","中国科学技术大学","安徽","783.4","2024"
+"8","华中科技大学","湖北","766.8","2024"
+"9","武汉大学","湖北","759.2","2024"
+"10","西安交通大学","陕西","753.6","2024"
+"11","中山大学","广东","746.3","2024"
+"12","四川大学","四川","739.9","2024"
+"13","哈尔滨工业大学","黑龙江","733.5","2024"
+"14","北京航空航天大学","北京","726.8","2024"
+"15","东南大学","江苏","719.4","2024"
+"16","北京理工大学","北京","713.6","2024"
+"17","同济大学","上海","706.3","2024"
+"18","中国人民大学","北京","699.5","2024"
+"19","北京师范大学","北京","693.1","2024"
+"20","南开大学","天津","686.7","2024"
diff --git a/project/dependency-reduced-pom.xml b/project/dependency-reduced-pom.xml
new file mode 100644
index 0000000..0bbd124
--- /dev/null
+++ b/project/dependency-reduced-pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+ com.university
+ university-rank-crawler
+ 1.0-SNAPSHOT
+
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+ 11
+ 11
+
+
+
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ com.university.Main
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.1
+
+ com.university.Main
+
+
+
+
+
+ 11
+ 11
+ UTF-8
+
+
diff --git a/project/pom.xml b/project/pom.xml
new file mode 100644
index 0000000..5634cbb
--- /dev/null
+++ b/project/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+
+ com.university
+ university-rank-crawler
+ 1.0-SNAPSHOT
+ jar
+
+
+
+ 11
+ 11
+
+ UTF-8
+
+
+
+
+
+ org.jsoup
+ jsoup
+ 1.16.2
+
+
+
+
+ org.jfree
+ jfreechart
+ 1.5.3
+
+
+
+
+ com.opencsv
+ opencsv
+ 5.8
+
+
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.9
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 11
+ 11
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ com.university.Main
+
+
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.1
+
+ com.university.Main
+
+
+
+
+
diff --git a/project/src/main/java/com/university/Main.java b/project/src/main/java/com/university/Main.java
new file mode 100644
index 0000000..4e39ddd
--- /dev/null
+++ b/project/src/main/java/com/university/Main.java
@@ -0,0 +1,359 @@
+package com.university;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Scanner;
+
+import com.university.analysis.RankAnalyzer;
+import com.university.crawler.UniversityRankCrawler;
+import com.university.model.RankChange;
+import com.university.model.University;
+import com.university.model.UniversityComparison;
+import com.university.storage.DataStorage;
+import com.university.visualization.ChartGenerator;
+import com.university.visualization.ConsoleReporter;
+
+/**
+ * 主程序入口
+ * 整合所有模块,提供交互式菜单
+ */
+public class Main {
+
+ // 核心组件
+ private final UniversityRankCrawler crawler;
+ private final DataStorage storage;
+ private final RankAnalyzer analyzer;
+ private final ChartGenerator chartGenerator;
+ private final ConsoleReporter reporter;
+
+ // 数据缓存
+ private Map> dataCache;
+ private Scanner scanner;
+
+ public Main() {
+ this.crawler = new UniversityRankCrawler();
+ this.storage = new DataStorage();
+ this.analyzer = new RankAnalyzer();
+ this.chartGenerator = new ChartGenerator();
+ this.reporter = new ConsoleReporter();
+ this.dataCache = new HashMap<>();
+ this.scanner = new Scanner(System.in);
+ }
+
+ public static void main(String[] args) {
+ Main app = new Main();
+ app.run();
+ }
+
+ /**
+ * 运行主程序
+ */
+ public void run() {
+ // 打印欢迎信息
+ reporter.printWelcome();
+
+ // 初始化数据
+ initializeData();
+
+ // 主循环
+ boolean running = true;
+ while (running) {
+ reporter.printMenu();
+ String choice = scanner.nextLine().trim();
+
+ switch (choice) {
+ case "1":
+ showTopN();
+ break;
+ case "2":
+ showByProvince();
+ break;
+ case "3":
+ searchUniversity();
+ break;
+ case "4":
+ showProvinceStatistics();
+ break;
+ case "5":
+ showScoreStatistics();
+ break;
+ case "6":
+ showRankChanges();
+ break;
+ case "7":
+ compareUniversities();
+ break;
+ case "8":
+ showYearlyTrend();
+ break;
+ case "9":
+ generateAllCharts();
+ break;
+ case "0":
+ running = false;
+ System.out.println("感谢使用,再见!");
+ break;
+ default:
+ System.out.println("无效选择,请重新输入!");
+ }
+ }
+
+ scanner.close();
+ }
+
+ /**
+ * 初始化数据
+ */
+ private void initializeData() {
+ System.out.println("正在初始化数据...");
+
+ // 爬取2022-2024年的数据
+ int[] years = {2022, 2023, 2024};
+
+ for (int year : years) {
+ List data;
+
+ // 先尝试从文件读取
+ if (storage.dataExists(year)) {
+ System.out.println("从文件加载 " + year + " 年数据...");
+ data = storage.readRawData(year);
+ } else {
+ // 文件不存在则爬取
+ System.out.println("爬取 " + year + " 年数据...");
+ data = crawler.crawlRankings(year);
+ // 保存到文件
+ storage.saveRawData(data, year);
+ }
+
+ dataCache.put(year, data);
+ }
+
+ System.out.println("数据初始化完成!\n");
+ }
+
+ /**
+ * 显示Top N
+ */
+ private void showTopN() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ System.out.print("请输入要查看的数量: ");
+ int n = Integer.parseInt(scanner.nextLine().trim());
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ List topN = analyzer.getTopN(data, n);
+ reporter.printUniversityList(topN, year + "年 Top " + n + " 高校");
+
+ // 生成图表
+ chartGenerator.generateTopNBarChart(data, year, n);
+ }
+
+ /**
+ * 按省份查看
+ */
+ private void showByProvince() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ System.out.print("请输入省份名称: ");
+ String province = scanner.nextLine().trim();
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ List result = analyzer.getByProvince(data, province);
+ if (result.isEmpty()) {
+ System.out.println("该省份没有高校数据!");
+ } else {
+ reporter.printUniversityList(result, year + "年 " + province + " 高校");
+ }
+ }
+
+ /**
+ * 搜索高校
+ */
+ private void searchUniversity() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ System.out.print("请输入搜索关键词: ");
+ String keyword = scanner.nextLine().trim();
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ List result = analyzer.searchUniversity(data, keyword);
+ if (result.isEmpty()) {
+ System.out.println("未找到匹配的高校!");
+ } else {
+ reporter.printUniversityList(result, "搜索结果");
+ }
+ }
+
+ /**
+ * 显示省份统计
+ */
+ private void showProvinceStatistics() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ Map provinceCount = analyzer.countByProvince(data);
+ reporter.printProvinceStatistics(provinceCount, year + "年 省份分布统计");
+
+ // 生成图表
+ chartGenerator.generateProvincePieChart(provinceCount, year);
+ }
+
+ /**
+ * 显示分数统计
+ */
+ private void showScoreStatistics() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ RankAnalyzer.ScoreStatistics stats = analyzer.getScoreStatistics(data);
+ reporter.printScoreStatistics(stats, year + "年 分数统计");
+ }
+
+ /**
+ * 显示排名变化
+ */
+ private void showRankChanges() {
+ List changes = analyzer.calculateRankChanges(dataCache);
+
+ // 显示上升最快
+ List rising = analyzer.getFastestRising(changes, 5);
+ reporter.printRankChanges(rising, "排名上升最快 Top 5");
+
+ // 显示下降最快
+ List falling = analyzer.getFastestFalling(changes, 5);
+ reporter.printRankChanges(falling, "排名下降最快 Top 5");
+
+ // 生成图表
+ if (!rising.isEmpty()) {
+ chartGenerator.generateRankChangeChart(rising, "排名上升最快", "rank_rising.png");
+ }
+ if (!falling.isEmpty()) {
+ chartGenerator.generateRankChangeChart(falling, "排名下降最快", "rank_falling.png");
+ }
+ }
+
+ /**
+ * 对比两所高校
+ */
+ private void compareUniversities() {
+ System.out.print("请输入要查看的年份(2022-2024): ");
+ int year = Integer.parseInt(scanner.nextLine().trim());
+
+ System.out.print("请输入第一所高校名称: ");
+ String name1 = scanner.nextLine().trim();
+
+ System.out.print("请输入第二所高校名称: ");
+ String name2 = scanner.nextLine().trim();
+
+ List data = dataCache.get(year);
+ if (data == null) {
+ System.out.println("该年份数据不存在!");
+ return;
+ }
+
+ Optional u1 = data.stream()
+ .filter(u -> u.getName().equals(name1))
+ .findFirst();
+ Optional u2 = data.stream()
+ .filter(u -> u.getName().equals(name2))
+ .findFirst();
+
+ if (u1.isPresent() && u2.isPresent()) {
+ UniversityComparison comparison = analyzer.compareUniversities(u1.get(), u2.get());
+ reporter.printComparison(comparison);
+ } else {
+ System.out.println("未找到指定的高校!");
+ }
+ }
+
+ /**
+ * 显示某高校历年趋势
+ */
+ private void showYearlyTrend() {
+ System.out.print("请输入高校名称: ");
+ String name = scanner.nextLine().trim();
+
+ List history = analyzer.getUniversityHistory(dataCache, name);
+
+ if (history.isEmpty()) {
+ System.out.println("未找到该高校的数据!");
+ } else {
+ reporter.printYearlyTrend(history, name);
+ chartGenerator.generateRankTrendLineChart(history, name);
+ }
+ }
+
+ /**
+ * 生成所有图表
+ */
+ private void generateAllCharts() {
+ System.out.println("正在生成所有图表...");
+
+ for (Map.Entry> entry : dataCache.entrySet()) {
+ int year = entry.getKey();
+ List data = entry.getValue();
+
+ // Top 10 柱状图
+ chartGenerator.generateTopNBarChart(data, year, 10);
+
+ // 省份分布饼图
+ Map provinceCount = analyzer.countByProvince(data);
+ chartGenerator.generateProvincePieChart(provinceCount, year);
+ }
+
+ // 排名变化图
+ List changes = analyzer.calculateRankChanges(dataCache);
+ List rising = analyzer.getFastestRising(changes, 10);
+ List falling = analyzer.getFastestFalling(changes, 10);
+
+ if (!rising.isEmpty()) {
+ chartGenerator.generateRankChangeChart(rising, "排名上升最快", "rank_rising.png");
+ }
+ if (!falling.isEmpty()) {
+ chartGenerator.generateRankChangeChart(falling, "排名下降最快", "rank_falling.png");
+ }
+
+ // 为Top 5高校生成历年趋势折线图
+ List topUniversities = analyzer.getTopN(dataCache.get(2024), 5);
+ for (University u : topUniversities) {
+ List history = analyzer.getUniversityHistory(dataCache, u.getName());
+ if (!history.isEmpty()) {
+ chartGenerator.generateRankTrendLineChart(history, u.getName());
+ }
+ }
+
+ System.out.println("所有图表生成完成!\n");
+ }
+}
diff --git a/project/src/main/java/com/university/analysis/RankAnalyzer.java b/project/src/main/java/com/university/analysis/RankAnalyzer.java
new file mode 100644
index 0000000..3627576
--- /dev/null
+++ b/project/src/main/java/com/university/analysis/RankAnalyzer.java
@@ -0,0 +1,250 @@
+package com.university.analysis;
+
+import com.university.model.RankChange;
+import com.university.model.University;
+import com.university.model.UniversityComparison;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 排名分析类
+ * 提供各种数据分析功能
+ */
+public class RankAnalyzer {
+
+ /**
+ * 获取Top N高校
+ *
+ * @param universities 高校列表
+ * @param n 数量
+ * @return Top N高校列表
+ */
+ public List getTopN(List universities, int n) {
+ return universities.stream()
+ .sorted(Comparator.comparingInt(University::getRank))
+ .limit(n)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 按省份统计高校数量
+ *
+ * @param universities 高校列表
+ * @return 省份-数量映射
+ */
+ public Map countByProvince(List universities) {
+ return universities.stream()
+ .collect(Collectors.groupingBy(
+ University::getProvince,
+ Collectors.counting()
+ ));
+ }
+
+ /**
+ * 按省份统计平均分
+ *
+ * @param universities 高校列表
+ * @return 省份-平均分映射
+ */
+ public Map averageScoreByProvince(List universities) {
+ return universities.stream()
+ .collect(Collectors.groupingBy(
+ University::getProvince,
+ Collectors.averagingDouble(University::getScore)
+ ));
+ }
+
+ /**
+ * 获取指定省份的高校
+ *
+ * @param universities 高校列表
+ * @param province 省份
+ * @return 该省份的高校列表
+ */
+ public List getByProvince(List universities, String province) {
+ return universities.stream()
+ .filter(u -> u.getProvince().equals(province))
+ .sorted(Comparator.comparingInt(University::getRank))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 搜索高校
+ *
+ * @param universities 高校列表
+ * @param keyword 关键词
+ * @return 匹配的高校列表
+ */
+ public List searchUniversity(List universities, String keyword) {
+ return universities.stream()
+ .filter(u -> u.getName().contains(keyword))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 获取分数统计信息
+ *
+ * @param universities 高校列表
+ * @return 统计信息
+ */
+ public ScoreStatistics getScoreStatistics(List universities) {
+ DoubleSummaryStatistics stats = universities.stream()
+ .mapToDouble(University::getScore)
+ .summaryStatistics();
+
+ return new ScoreStatistics(
+ stats.getCount(),
+ stats.getSum(),
+ stats.getAverage(),
+ stats.getMax(),
+ stats.getMin()
+ );
+ }
+
+ /**
+ * 计算历年排名变化
+ *
+ * @param dataMap 多年数据映射(年份->高校列表)
+ * @return 排名变化列表
+ */
+ public List calculateRankChanges(Map> dataMap) {
+ List changes = new ArrayList<>();
+
+ // 获取所有年份并排序
+ List years = new ArrayList<>(dataMap.keySet());
+ Collections.sort(years);
+
+ if (years.size() < 2) {
+ return changes;
+ }
+
+ int startYear = years.get(0);
+ int endYear = years.get(years.size() - 1);
+
+ List startData = dataMap.get(startYear);
+ List endData = dataMap.get(endYear);
+
+ // 创建名称到高校的映射
+ Map startMap = startData.stream()
+ .collect(Collectors.toMap(University::getName, u -> u));
+ Map endMap = endData.stream()
+ .collect(Collectors.toMap(University::getName, u -> u));
+
+ // 计算每所高校的变化
+ for (String name : startMap.keySet()) {
+ if (endMap.containsKey(name)) {
+ University startUni = startMap.get(name);
+ University endUni = endMap.get(name);
+
+ RankChange change = new RankChange(
+ name,
+ startYear,
+ endYear,
+ startUni.getRank(),
+ endUni.getRank(),
+ startUni.getScore(),
+ endUni.getScore()
+ );
+ changes.add(change);
+ }
+ }
+
+ return changes;
+ }
+
+ /**
+ * 获取排名上升最快的高校
+ *
+ * @param changes 排名变化列表
+ * @param n 数量
+ * @return 上升最快的高校列表
+ */
+ public List getFastestRising(List changes, int n) {
+ return changes.stream()
+ .filter(c -> c.getRankChange() > 0) // 只取排名上升的
+ .sorted(Comparator.comparingInt(RankChange::getRankChange).reversed())
+ .limit(n)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 获取排名下降最快的高校
+ *
+ * @param changes 排名变化列表
+ * @param n 数量
+ * @return 下降最快的高校列表
+ */
+ public List getFastestFalling(List changes, int n) {
+ return changes.stream()
+ .filter(c -> c.getRankChange() < 0) // 只取排名下降的
+ .sorted(Comparator.comparingInt(RankChange::getRankChange))
+ .limit(n)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 对比两所高校
+ *
+ * @param u1 高校1
+ * @param u2 高校2
+ * @return 对比结果
+ */
+ public UniversityComparison compareUniversities(University u1, University u2) {
+ return new UniversityComparison(u1, u2);
+ }
+
+ /**
+ * 获取某高校在多年数据中的信息
+ *
+ * @param dataMap 多年数据映射
+ * @param universityName 高校名称
+ * @return 该高校历年的信息列表
+ */
+ public List getUniversityHistory(Map> dataMap,
+ String universityName) {
+ List history = new ArrayList<>();
+
+ for (List yearData : dataMap.values()) {
+ yearData.stream()
+ .filter(u -> u.getName().equals(universityName))
+ .findFirst()
+ .ifPresent(history::add);
+ }
+
+ // 按年份排序
+ history.sort(Comparator.comparingInt(University::getYear));
+ return history;
+ }
+
+ /**
+ * 分数统计信息内部类
+ */
+ public static class ScoreStatistics {
+ private final long count;
+ private final double sum;
+ private final double average;
+ private final double max;
+ private final double min;
+
+ public ScoreStatistics(long count, double sum, double average, double max, double min) {
+ this.count = count;
+ this.sum = sum;
+ this.average = average;
+ this.max = max;
+ this.min = min;
+ }
+
+ public long getCount() { return count; }
+ public double getSum() { return sum; }
+ public double getAverage() { return average; }
+ public double getMax() { return max; }
+ public double getMin() { return min; }
+
+ @Override
+ public String toString() {
+ return String.format("统计信息: 数量=%d, 平均分=%.2f, 最高分=%.2f, 最低分=%.2f",
+ count, average, max, min);
+ }
+ }
+}
diff --git a/project/src/main/java/com/university/crawler/UniversityRankCrawler.java b/project/src/main/java/com/university/crawler/UniversityRankCrawler.java
new file mode 100644
index 0000000..2257f3a
--- /dev/null
+++ b/project/src/main/java/com/university/crawler/UniversityRankCrawler.java
@@ -0,0 +1,153 @@
+package com.university.crawler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.university.model.University;
+
+/**
+ * 高校排名爬虫类
+ * 负责从网页抓取高校排名数据
+ */
+public class UniversityRankCrawler {
+
+ // 请求间隔时间(毫秒),防止请求过快被封
+ private static final int REQUEST_DELAY = 1000;
+
+ /**
+ * 爬取软科中国大学排名数据
+ * 分析软科官网HTML结构,提取真实排名数据
+ *
+ * @param year 年份
+ * @return 高校列表
+ */
+ public List crawlRankings(int year) {
+ List universities = new ArrayList<>();
+
+ try {
+ // 软科排名URL
+ String url = "https://www.shanghairanking.cn/rankings/bcur/" + year;
+
+ System.out.println("正在爬取 " + year + " 年高校排名数据...");
+
+ // 发送HTTP请求获取网页内容
+ Document doc = Jsoup.connect(url)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
+ .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
+ .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
+ .timeout(15000)
+ .get();
+
+ // 分析HTML结构,提取排名数据
+ // 找到排名表格
+ Elements rows = doc.select("table.rk-table tbody tr");
+
+ for (Element row : rows) {
+ Elements cells = row.select("td");
+ if (cells.size() >= 5) {
+ try {
+ // 提取排名
+ String rankText = cells.get(0).text().trim();
+ rankText = rankText.replaceAll("[^0-9]", "");
+ if (rankText.isEmpty()) continue;
+ int rank = Integer.parseInt(rankText);
+
+ // 提取学校名称
+ String name = cells.get(1).text().trim();
+
+ // 提取省份
+ String province = cells.get(2).text().trim();
+
+ // 提取总分
+ String scoreText = cells.get(4).text().trim();
+ scoreText = scoreText.replaceAll("[^0-9.]", "");
+ if (scoreText.isEmpty()) continue;
+ double score = Double.parseDouble(scoreText);
+
+ // 创建高校对象
+ University university = new University(rank, name, province, score, year);
+ universities.add(university);
+
+ // 限制爬取数量,避免请求过多
+ if (universities.size() >= 100) break;
+ } catch (NumberFormatException e) {
+ // 跳过解析失败的行
+ continue;
+ }
+ }
+ }
+
+ // 请求间隔,避免被封
+ Thread.sleep(REQUEST_DELAY);
+
+ } catch (IOException e) {
+ System.err.println("爬取数据失败: " + e.getMessage());
+ System.out.println("将使用模拟数据...");
+ // 如果爬取失败,使用模拟数据
+ universities = generateMockData(year);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ System.out.println("成功获取 " + universities.size() + " 条数据");
+ return universities;
+ }
+
+ /**
+ * 爬取多年数据
+ *
+ * @param startYear 开始年份
+ * @param endYear 结束年份
+ * @return 多年数据集合
+ */
+ public List> crawlMultipleYears(int startYear, int endYear) {
+ List> allData = new ArrayList<>();
+
+ for (int year = startYear; year <= endYear; year++) {
+ List yearData = crawlRankings(year);
+ allData.add(yearData);
+ }
+
+ return allData;
+ }
+
+ /**
+ * 生成模拟数据(用于演示)
+ * 当真实网站无法访问时使用
+ */
+ private List generateMockData(int year) {
+ List mockData = new ArrayList<>();
+
+ // 基础数据,每年的分数略有变化
+ double variation = (year - 2022) * 0.5;
+
+ mockData.add(new University(1, "清华大学", "北京", 852.5 + variation, year));
+ mockData.add(new University(2, "北京大学", "北京", 848.2 + variation, year));
+ mockData.add(new University(3, "浙江大学", "浙江", 822.5 + variation, year));
+ mockData.add(new University(4, "上海交通大学", "上海", 815.3 + variation, year));
+ mockData.add(new University(5, "复旦大学", "上海", 805.1 + variation, year));
+ mockData.add(new University(6, "南京大学", "江苏", 785.6 + variation, year));
+ mockData.add(new University(7, "中国科学技术大学", "安徽", 782.4 + variation, year));
+ mockData.add(new University(8, "华中科技大学", "湖北", 765.8 + variation, year));
+ mockData.add(new University(9, "武汉大学", "湖北", 758.2 + variation, year));
+ mockData.add(new University(10, "西安交通大学", "陕西", 752.6 + variation, year));
+ mockData.add(new University(11, "中山大学", "广东", 745.3 + variation, year));
+ mockData.add(new University(12, "四川大学", "四川", 738.9 + variation, year));
+ mockData.add(new University(13, "哈尔滨工业大学", "黑龙江", 732.5 + variation, year));
+ mockData.add(new University(14, "北京航空航天大学", "北京", 725.8 + variation, year));
+ mockData.add(new University(15, "东南大学", "江苏", 718.4 + variation, year));
+ mockData.add(new University(16, "北京理工大学", "北京", 712.6 + variation, year));
+ mockData.add(new University(17, "同济大学", "上海", 705.3 + variation, year));
+ mockData.add(new University(18, "中国人民大学", "北京", 698.5 + variation, year));
+ mockData.add(new University(19, "北京师范大学", "北京", 692.1 + variation, year));
+ mockData.add(new University(20, "南开大学", "天津", 685.7 + variation, year));
+
+ return mockData;
+ }
+}
diff --git a/project/src/main/java/com/university/model/RankChange.java b/project/src/main/java/com/university/model/RankChange.java
new file mode 100644
index 0000000..f34a83c
--- /dev/null
+++ b/project/src/main/java/com/university/model/RankChange.java
@@ -0,0 +1,145 @@
+package com.university.model;
+
+/**
+ * 排名变化实体类
+ * 用于存储高校历年排名变化信息
+ */
+public class RankChange {
+
+ // 学校名称
+ private String universityName;
+
+ // 起始年份
+ private int startYear;
+
+ // 结束年份
+ private int endYear;
+
+ // 起始排名
+ private int startRank;
+
+ // 结束排名
+ private int endRank;
+
+ // 排名变化(正数表示上升,负数表示下降)
+ private int rankChange;
+
+ // 起始分数
+ private double startScore;
+
+ // 结束分数
+ private double endScore;
+
+ // 分数变化
+ private double scoreChange;
+
+ public RankChange() {
+ }
+
+ public RankChange(String universityName, int startYear, int endYear,
+ int startRank, int endRank, double startScore, double endScore) {
+ this.universityName = universityName;
+ this.startYear = startYear;
+ this.endYear = endYear;
+ this.startRank = startRank;
+ this.endRank = endRank;
+ this.startScore = startScore;
+ this.endScore = endScore;
+
+ // 计算变化
+ this.rankChange = startRank - endRank; // 排名数字变小表示上升
+ this.scoreChange = endScore - startScore;
+ }
+
+ // Getters and Setters
+ public String getUniversityName() {
+ return universityName;
+ }
+
+ public void setUniversityName(String universityName) {
+ this.universityName = universityName;
+ }
+
+ public int getStartYear() {
+ return startYear;
+ }
+
+ public void setStartYear(int startYear) {
+ this.startYear = startYear;
+ }
+
+ public int getEndYear() {
+ return endYear;
+ }
+
+ public void setEndYear(int endYear) {
+ this.endYear = endYear;
+ }
+
+ public int getStartRank() {
+ return startRank;
+ }
+
+ public void setStartRank(int startRank) {
+ this.startRank = startRank;
+ }
+
+ public int getEndRank() {
+ return endRank;
+ }
+
+ public void setEndRank(int endRank) {
+ this.endRank = endRank;
+ }
+
+ public int getRankChange() {
+ return rankChange;
+ }
+
+ public void setRankChange(int rankChange) {
+ this.rankChange = rankChange;
+ }
+
+ public double getStartScore() {
+ return startScore;
+ }
+
+ public void setStartScore(double startScore) {
+ this.startScore = startScore;
+ }
+
+ public double getEndScore() {
+ return endScore;
+ }
+
+ public void setEndScore(double endScore) {
+ this.endScore = endScore;
+ }
+
+ public double getScoreChange() {
+ return scoreChange;
+ }
+
+ public void setScoreChange(double scoreChange) {
+ this.scoreChange = scoreChange;
+ }
+
+ /**
+ * 获取变化趋势描述
+ */
+ public String getTrendDescription() {
+ if (rankChange > 0) {
+ return String.format("上升%d位", rankChange);
+ } else if (rankChange < 0) {
+ return String.format("下降%d位", Math.abs(rankChange));
+ } else {
+ return "排名不变";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s: %d年(第%d名) -> %d年(第%d名), %s",
+ universityName, startYear, startRank, endYear, endRank, getTrendDescription());
+ }
+}
diff --git a/project/src/main/java/com/university/model/University.java b/project/src/main/java/com/university/model/University.java
new file mode 100644
index 0000000..8f6ea64
--- /dev/null
+++ b/project/src/main/java/com/university/model/University.java
@@ -0,0 +1,120 @@
+package com.university.model;
+
+import java.util.Objects;
+
+/**
+ * 高校实体类 (Java Bean)
+ * 用于封装高校排名数据
+ */
+public class University {
+
+ // 排名
+ private int rank;
+
+ // 学校名称
+ private String name;
+
+ // 所在省份
+ private String province;
+
+ // 总分
+ private double score;
+
+ // 年份
+ private int year;
+
+ // 无参构造方法(必须,用于反射创建对象)
+ public University() {
+ }
+
+ // 全参构造方法
+ public University(int rank, String name, String province, double score, int year) {
+ this.rank = rank;
+ this.name = name;
+ this.province = province;
+ this.score = score;
+ this.year = year;
+ }
+
+ // Getter和Setter方法
+ public int getRank() {
+ return rank;
+ }
+
+ public void setRank(int rank) {
+ this.rank = rank;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getProvince() {
+ return province;
+ }
+
+ public void setProvince(String province) {
+ this.province = province;
+ }
+
+ public double getScore() {
+ return score;
+ }
+
+ public void setScore(double score) {
+ this.score = score;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ /**
+ * 计算排名变化
+ * @param previousRank 往年排名
+ * @return 排名变化(正数表示上升,负数表示下降)
+ */
+ public int calculateRankChange(int previousRank) {
+ return previousRank - this.rank;
+ }
+
+ /**
+ * 计算分数变化
+ * @param previousScore 往年分数
+ * @return 分数变化
+ */
+ public double calculateScoreChange(double previousScore) {
+ return this.score - previousScore;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("University{rank=%d, name='%s', province='%s', score=%.2f, year=%d}",
+ rank, name, province, score, year);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ University that = (University) o;
+ return rank == that.rank &&
+ Double.compare(that.score, score) == 0 &&
+ year == that.year &&
+ Objects.equals(name, that.name) &&
+ Objects.equals(province, that.province);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rank, name, province, score, year);
+ }
+}
diff --git a/project/src/main/java/com/university/model/UniversityComparison.java b/project/src/main/java/com/university/model/UniversityComparison.java
new file mode 100644
index 0000000..022c825
--- /dev/null
+++ b/project/src/main/java/com/university/model/UniversityComparison.java
@@ -0,0 +1,171 @@
+package com.university.model;
+
+/**
+ * 高校对比实体类
+ * 用于存储两所高校的对比信息
+ */
+public class UniversityComparison {
+
+ // 第一所高校
+ private String universityName1;
+
+ // 第二所高校
+ private String universityName2;
+
+ // 年份
+ private int year;
+
+ // 高校1排名
+ private int rank1;
+
+ // 高校2排名
+ private int rank2;
+
+ // 高校1分数
+ private double score1;
+
+ // 高校2分数
+ private double score2;
+
+ // 高校1省份
+ private String province1;
+
+ // 高校2省份
+ private String province2;
+
+ // 排名差距
+ private int rankGap;
+
+ // 分数差距
+ private double scoreGap;
+
+ public UniversityComparison() {
+ }
+
+ public UniversityComparison(University u1, University u2) {
+ this.universityName1 = u1.getName();
+ this.universityName2 = u2.getName();
+ this.year = u1.getYear();
+ this.rank1 = u1.getRank();
+ this.rank2 = u2.getRank();
+ this.score1 = u1.getScore();
+ this.score2 = u2.getScore();
+ this.province1 = u1.getProvince();
+ this.province2 = u2.getProvince();
+
+ this.rankGap = Math.abs(rank1 - rank2);
+ this.scoreGap = Math.abs(score1 - score2);
+ }
+
+ // Getters and Setters
+ public String getUniversityName1() {
+ return universityName1;
+ }
+
+ public void setUniversityName1(String universityName1) {
+ this.universityName1 = universityName1;
+ }
+
+ public String getUniversityName2() {
+ return universityName2;
+ }
+
+ public void setUniversityName2(String universityName2) {
+ this.universityName2 = universityName2;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ public int getRank1() {
+ return rank1;
+ }
+
+ public void setRank1(int rank1) {
+ this.rank1 = rank1;
+ }
+
+ public int getRank2() {
+ return rank2;
+ }
+
+ public void setRank2(int rank2) {
+ this.rank2 = rank2;
+ }
+
+ public double getScore1() {
+ return score1;
+ }
+
+ public void setScore1(double score1) {
+ this.score1 = score1;
+ }
+
+ public double getScore2() {
+ return score2;
+ }
+
+ public void setScore2(double score2) {
+ this.score2 = score2;
+ }
+
+ public String getProvince1() {
+ return province1;
+ }
+
+ public void setProvince1(String province1) {
+ this.province1 = province1;
+ }
+
+ public String getProvince2() {
+ return province2;
+ }
+
+ public void setProvince2(String province2) {
+ this.province2 = province2;
+ }
+
+ public int getRankGap() {
+ return rankGap;
+ }
+
+ public void setRankGap(int rankGap) {
+ this.rankGap = rankGap;
+ }
+
+ public double getScoreGap() {
+ return scoreGap;
+ }
+
+ public void setScoreGap(double scoreGap) {
+ this.scoreGap = scoreGap;
+ }
+
+ /**
+ * 获取排名较高的高校名称
+ */
+ public String getHigherRankedUniversity() {
+ return rank1 < rank2 ? universityName1 : universityName2;
+ }
+
+ /**
+ * 获取对比结果描述
+ */
+ public String getComparisonResult() {
+ String higherUni = getHigherRankedUniversity();
+ return String.format("%d年: %s 排名高于 %s %d位,分数相差 %.2f分",
+ year, higherUni,
+ higherUni.equals(universityName1) ? universityName2 : universityName1,
+ rankGap, scoreGap);
+ }
+
+ @Override
+ public String toString() {
+ return getComparisonResult();
+ }
+}
diff --git a/project/src/main/java/com/university/storage/DataStorage.java b/project/src/main/java/com/university/storage/DataStorage.java
new file mode 100644
index 0000000..b7edd2e
--- /dev/null
+++ b/project/src/main/java/com/university/storage/DataStorage.java
@@ -0,0 +1,202 @@
+package com.university.storage;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.opencsv.CSVReader;
+import com.opencsv.CSVWriter;
+import com.opencsv.bean.CsvToBean;
+import com.opencsv.bean.CsvToBeanBuilder;
+import com.opencsv.bean.StatefulBeanToCsv;
+import com.opencsv.bean.StatefulBeanToCsvBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import com.university.model.University;
+
+/**
+ * 数据存储类
+ * 负责数据的持久化存储(CSV格式)
+ */
+public class DataStorage {
+
+ // 数据存储目录
+ private static final String DATA_DIR = "data";
+
+ /**
+ * 构造方法,确保数据目录存在
+ */
+ public DataStorage() {
+ File dir = new File(DATA_DIR);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ }
+
+ /**
+ * 保存高校列表到CSV文件
+ *
+ * @param universities 高校列表
+ * @param year 年份
+ */
+ public void saveToCsv(List universities, int year) {
+ String filename = DATA_DIR + "/university_rank_" + year + ".csv";
+
+ try (Writer writer = new OutputStreamWriter(
+ new FileOutputStream(filename), StandardCharsets.UTF_8)) {
+
+ // 添加BOM,解决Excel中文乱码
+ writer.write('\ufeff');
+
+ // 创建CSV写入器
+ StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer)
+ .withQuotechar('"')
+ .withSeparator(',')
+ .withOrderedResults(true)
+ .build();
+
+ // 写入数据
+ beanToCsv.write(universities);
+ System.out.println("数据已保存到: " + filename);
+
+ } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
+ System.err.println("保存CSV文件失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 从CSV文件读取高校列表
+ *
+ * @param year 年份
+ * @return 高校列表
+ */
+ public List readFromCsv(int year) {
+ String filename = DATA_DIR + "/university_rank_" + year + ".csv";
+ List universities = new ArrayList<>();
+
+ try (Reader reader = new InputStreamReader(
+ new FileInputStream(filename), StandardCharsets.UTF_8)) {
+
+ // 创建CSV读取器
+ CsvToBean csvToBean = new CsvToBeanBuilder(reader)
+ .withType(University.class)
+ .withIgnoreLeadingWhiteSpace(true)
+ .build();
+
+ // 读取数据
+ universities = csvToBean.parse();
+ System.out.println("从 " + filename + " 读取了 " + universities.size() + " 条数据");
+
+ } catch (IOException e) {
+ System.err.println("读取CSV文件失败: " + e.getMessage());
+ }
+
+ return universities;
+ }
+
+ /**
+ * 保存原始数据(手动控制格式)
+ *
+ * @param universities 高校列表
+ * @param year 年份
+ */
+ public void saveRawData(List universities, int year) {
+ String filename = DATA_DIR + "/university_rank_" + year + ".csv";
+
+ try (CSVWriter writer = new CSVWriter(new OutputStreamWriter(
+ new FileOutputStream(filename), StandardCharsets.UTF_8))) {
+
+ // 写入表头
+ String[] header = {"排名", "学校名称", "省份", "总分", "年份"};
+ writer.writeNext(header);
+
+ // 写入数据
+ for (University u : universities) {
+ String[] row = {
+ String.valueOf(u.getRank()),
+ u.getName(),
+ u.getProvince(),
+ String.valueOf(u.getScore()),
+ String.valueOf(u.getYear())
+ };
+ writer.writeNext(row);
+ }
+
+ System.out.println("原始数据已保存到: " + filename);
+
+ } catch (IOException e) {
+ System.err.println("保存原始数据失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 读取原始数据
+ *
+ * @param year 年份
+ * @return 高校列表
+ */
+ public List readRawData(int year) {
+ String filename = DATA_DIR + "/university_rank_" + year + ".csv";
+ List universities = new ArrayList<>();
+
+ try (CSVReader reader = new CSVReader(new InputStreamReader(
+ new FileInputStream(filename), StandardCharsets.UTF_8))) {
+
+ // 跳过表头
+ reader.readNext();
+
+ // 读取数据行
+ String[] row;
+ while ((row = reader.readNext()) != null) {
+ if (row.length >= 5) {
+ University u = new University();
+ u.setRank(Integer.parseInt(row[0].trim()));
+ u.setName(row[1].trim());
+ u.setProvince(row[2].trim());
+ u.setScore(Double.parseDouble(row[3].trim()));
+ u.setYear(Integer.parseInt(row[4].trim()));
+ universities.add(u);
+ }
+ }
+
+ System.out.println("从 " + filename + " 读取了 " + universities.size() + " 条数据");
+
+ } catch (IOException | CsvValidationException e) {
+ System.err.println("读取原始数据失败: " + e.getMessage());
+ }
+
+ return universities;
+ }
+
+ /**
+ * 检查某年份的数据是否存在
+ *
+ * @param year 年份
+ * @return 是否存在
+ */
+ public boolean dataExists(int year) {
+ File file = new File(DATA_DIR + "/university_rank_" + year + ".csv");
+ return file.exists();
+ }
+
+ /**
+ * 删除某年份的数据文件
+ *
+ * @param year 年份
+ */
+ public void deleteData(int year) {
+ File file = new File(DATA_DIR + "/university_rank_" + year + ".csv");
+ if (file.exists() && file.delete()) {
+ System.out.println("已删除 " + year + " 年的数据文件");
+ }
+ }
+}
diff --git a/project/src/main/java/com/university/visualization/ChartGenerator.java b/project/src/main/java/com/university/visualization/ChartGenerator.java
new file mode 100644
index 0000000..439d97f
--- /dev/null
+++ b/project/src/main/java/com/university/visualization/ChartGenerator.java
@@ -0,0 +1,299 @@
+package com.university.visualization;
+
+import com.university.model.RankChange;
+import com.university.model.University;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.BarRenderer;
+import org.jfree.chart.renderer.category.LineAndShapeRenderer;
+import org.jfree.data.category.DefaultCategoryDataset;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 图表生成类
+ * 使用JFreeChart生成各种统计图表
+ */
+public class ChartGenerator {
+
+ // 图表输出目录
+ private static final String CHART_DIR = "charts";
+
+ /**
+ * 构造方法,确保图表目录存在
+ */
+ public ChartGenerator() {
+ File dir = new File(CHART_DIR);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ }
+
+ /**
+ * 生成Top N高校柱状图
+ *
+ * @param universities 高校列表
+ * @param year 年份
+ * @param n 数量
+ */
+ public void generateTopNBarChart(List universities, int year, int n) {
+ // 创建数据集
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+
+ // 取前N名
+ int count = Math.min(n, universities.size());
+ for (int i = 0; i < count; i++) {
+ University u = universities.get(i);
+ dataset.addValue(u.getScore(), "总分", u.getName());
+ }
+
+ // 创建图表
+ JFreeChart chart = ChartFactory.createBarChart(
+ year + "年高校排名Top" + n, // 标题
+ "学校", // X轴标签
+ "总分", // Y轴标签
+ dataset, // 数据集
+ PlotOrientation.VERTICAL, // 方向
+ true, // 显示图例
+ true, // 显示工具提示
+ false // 不生成URL
+ );
+
+ // 美化图表
+ customizeBarChart(chart);
+
+ // 保存图表
+ saveChart(chart, "top" + n + "_" + year + ".png");
+ }
+
+ /**
+ * 生成省份分布饼图
+ *
+ * @param provinceCount 省份统计
+ * @param year 年份
+ */
+ public void generateProvincePieChart(Map provinceCount, int year) {
+ // 创建饼图数据集
+ org.jfree.data.general.DefaultPieDataset dataset =
+ new org.jfree.data.general.DefaultPieDataset<>();
+
+ // 添加数据
+ provinceCount.forEach(dataset::setValue);
+
+ // 创建饼图
+ JFreeChart chart = ChartFactory.createPieChart(
+ year + "年高校省份分布", // 标题
+ dataset, // 数据集
+ true, // 显示图例
+ true, // 显示工具提示
+ false // 不生成URL
+ );
+
+ // 获取饼图plot并设置标签
+ org.jfree.chart.plot.PiePlot plot = (org.jfree.chart.plot.PiePlot) chart.getPlot();
+
+ // 设置标签格式:省份名称 + 数量 + 百分比
+ plot.setLabelGenerator(new org.jfree.chart.labels.StandardPieSectionLabelGenerator(
+ "{0}: {1}所 ({2})",
+ java.text.NumberFormat.getIntegerInstance(),
+ java.text.NumberFormat.getPercentInstance()
+ ));
+
+ // 设置标签字体
+ plot.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12));
+
+ // 设置标签颜色
+ plot.setLabelPaint(Color.BLACK);
+
+ // 设置标签背景
+ plot.setLabelBackgroundPaint(new Color(255, 255, 255, 200));
+
+ // 设置标题字体
+ chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16));
+
+ // 保存图表
+ saveChart(chart, "province_distribution_" + year + ".png");
+ }
+
+ /**
+ * 生成历年排名变化折线图
+ *
+ * @param universityHistory 某高校历年数据
+ * @param universityName 高校名称
+ */
+ public void generateRankTrendLineChart(List universityHistory,
+ String universityName) {
+ // 创建数据集
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+
+ // 添加数据(注意:排名越小越好,所以取负值让折线图向上表示进步)
+ for (University u : universityHistory) {
+ dataset.addValue(u.getRank(), "排名", String.valueOf(u.getYear()));
+ }
+
+ // 创建图表
+ JFreeChart chart = ChartFactory.createLineChart(
+ universityName + " 历年排名变化", // 标题
+ "年份", // X轴标签
+ "排名", // Y轴标签
+ dataset, // 数据集
+ PlotOrientation.VERTICAL, // 方向
+ true, // 显示图例
+ true, // 显示工具提示
+ false // 不生成URL
+ );
+
+ // 美化折线图
+ customizeLineChart(chart);
+
+ // 保存图表
+ saveChart(chart, "rank_trend_" + universityName + ".png");
+ }
+
+ /**
+ * 生成排名变化对比图
+ *
+ * @param changes 排名变化列表
+ * @param title 图表标题
+ * @param filename 文件名
+ */
+ public void generateRankChangeChart(List changes, String title, String filename) {
+ // 创建数据集
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+
+ // 添加数据
+ for (RankChange change : changes) {
+ dataset.addValue(change.getRankChange(), "排名变化", change.getUniversityName());
+ }
+
+ // 创建图表
+ JFreeChart chart = ChartFactory.createBarChart(
+ title,
+ "学校",
+ "排名变化(位)",
+ dataset,
+ PlotOrientation.HORIZONTAL,
+ true,
+ true,
+ false
+ );
+
+ // 美化
+ customizeBarChart(chart);
+
+ // 保存
+ saveChart(chart, filename);
+ }
+
+ /**
+ * 生成多高校对比图
+ *
+ * @param universities 高校列表
+ * @param year 年份
+ */
+ public void generateComparisonChart(List universities, int year) {
+ // 创建数据集
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+
+ // 添加分数数据
+ for (University u : universities) {
+ dataset.addValue(u.getScore(), "总分", u.getName());
+ }
+
+ // 创建图表
+ JFreeChart chart = ChartFactory.createBarChart(
+ year + "年高校分数对比",
+ "学校",
+ "总分",
+ dataset,
+ PlotOrientation.VERTICAL,
+ true,
+ true,
+ false
+ );
+
+ customizeBarChart(chart);
+ saveChart(chart, "comparison_" + year + ".png");
+ }
+
+ /**
+ * 美化柱状图
+ */
+ private void customizeBarChart(JFreeChart chart) {
+ CategoryPlot plot = chart.getCategoryPlot();
+
+ // 设置背景色
+ plot.setBackgroundPaint(Color.WHITE);
+ plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+
+ // 设置柱状图颜色
+ BarRenderer renderer = (BarRenderer) plot.getRenderer();
+ renderer.setSeriesPaint(0, new Color(79, 129, 189));
+
+ // 设置字体
+ CategoryAxis domainAxis = plot.getDomainAxis();
+ domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+ domainAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12));
+
+ NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
+ rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+ rangeAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12));
+
+ // 设置标题字体
+ chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16));
+ }
+
+ /**
+ * 美化折线图
+ */
+ private void customizeLineChart(JFreeChart chart) {
+ CategoryPlot plot = chart.getCategoryPlot();
+
+ // 设置背景色
+ plot.setBackgroundPaint(Color.WHITE);
+ plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+
+ // 设置折线样式
+ LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
+ renderer.setSeriesPaint(0, new Color(79, 129, 189));
+ renderer.setSeriesStroke(0, new BasicStroke(2.0f));
+ renderer.setSeriesShapesVisible(0, true);
+
+ // 设置字体
+ CategoryAxis domainAxis = plot.getDomainAxis();
+ domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+ domainAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12));
+
+ NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
+ rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+ rangeAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12));
+
+ // 设置标题字体
+ chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16));
+ }
+
+ /**
+ * 保存图表到文件
+ *
+ * @param chart 图表对象
+ * @param filename 文件名
+ */
+ private void saveChart(JFreeChart chart, String filename) {
+ try {
+ File file = new File(CHART_DIR + "/" + filename);
+ ChartUtils.saveChartAsPNG(file, chart, 800, 600);
+ System.out.println("图表已保存: " + file.getAbsolutePath());
+ } catch (IOException e) {
+ System.err.println("保存图表失败: " + e.getMessage());
+ }
+ }
+}
diff --git a/project/src/main/java/com/university/visualization/ConsoleReporter.java b/project/src/main/java/com/university/visualization/ConsoleReporter.java
new file mode 100644
index 0000000..d35a9af
--- /dev/null
+++ b/project/src/main/java/com/university/visualization/ConsoleReporter.java
@@ -0,0 +1,241 @@
+package com.university.visualization;
+
+import com.university.analysis.RankAnalyzer;
+import com.university.model.RankChange;
+import com.university.model.University;
+import com.university.model.UniversityComparison;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 控制台报表类
+ * 格式化输出各种统计结果到控制台
+ */
+public class ConsoleReporter {
+
+ /**
+ * 打印分隔线
+ */
+ private void printSeparator() {
+ System.out.println("=".repeat(80));
+ }
+
+ /**
+ * 打印高校列表
+ *
+ * @param universities 高校列表
+ * @param title 标题
+ */
+ public void printUniversityList(List universities, String title) {
+ printSeparator();
+ System.out.println("【" + title + "】");
+ printSeparator();
+
+ // 表头
+ System.out.printf("%-6s %-20s %-10s %-10s %-6s%n",
+ "排名", "学校名称", "省份", "总分", "年份");
+ System.out.println("-".repeat(80));
+
+ // 数据行
+ for (University u : universities) {
+ System.out.printf("%-6d %-20s %-10s %-10.2f %-6d%n",
+ u.getRank(),
+ truncate(u.getName(), 20),
+ u.getProvince(),
+ u.getScore(),
+ u.getYear());
+ }
+
+ System.out.println();
+ }
+
+ /**
+ * 打印省份统计
+ *
+ * @param provinceCount 省份统计
+ * @param title 标题
+ */
+ public void printProvinceStatistics(Map provinceCount, String title) {
+ printSeparator();
+ System.out.println("【" + title + "】");
+ printSeparator();
+
+ System.out.printf("%-15s %-10s%n", "省份", "高校数量");
+ System.out.println("-".repeat(30));
+
+ // 按数量降序排序
+ provinceCount.entrySet().stream()
+ .sorted(Map.Entry.comparingByValue().reversed())
+ .forEach(entry -> System.out.printf("%-15s %-10d%n",
+ entry.getKey(), entry.getValue()));
+
+ System.out.println();
+ }
+
+ /**
+ * 打印分数统计
+ *
+ * @param statistics 统计信息
+ * @param title 标题
+ */
+ public void printScoreStatistics(RankAnalyzer.ScoreStatistics statistics, String title) {
+ printSeparator();
+ System.out.println("【" + title + "】");
+ printSeparator();
+
+ System.out.printf("高校数量: %d%n", statistics.getCount());
+ System.out.printf("平均分数: %.2f%n", statistics.getAverage());
+ System.out.printf("最高分数: %.2f%n", statistics.getMax());
+ System.out.printf("最低分数: %.2f%n", statistics.getMin());
+ System.out.println();
+ }
+
+ /**
+ * 打印排名变化
+ *
+ * @param changes 排名变化列表
+ * @param title 标题
+ */
+ public void printRankChanges(List changes, String title) {
+ printSeparator();
+ System.out.println("【" + title + "】");
+ printSeparator();
+
+ System.out.printf("%-20s %-8s %-8s %-12s %-12s%n",
+ "学校名称", "起始年", "结束年", "排名变化", "分数变化");
+ System.out.println("-".repeat(80));
+
+ for (RankChange change : changes) {
+ String rankChangeStr = change.getRankChange() > 0 ?
+ "↑" + change.getRankChange() :
+ (change.getRankChange() < 0 ?
+ "↓" + Math.abs(change.getRankChange()) :
+ "-");
+
+ System.out.printf("%-20s %-8d %-8d %-12s %+.2f%n",
+ truncate(change.getUniversityName(), 20),
+ change.getStartYear(),
+ change.getEndYear(),
+ rankChangeStr,
+ change.getScoreChange());
+ }
+
+ System.out.println();
+ }
+
+ /**
+ * 打印高校对比结果
+ *
+ * @param comparison 对比结果
+ */
+ public void printComparison(UniversityComparison comparison) {
+ printSeparator();
+ System.out.println("【高校对比分析】");
+ printSeparator();
+
+ System.out.printf("对比年份: %d年%n%n", comparison.getYear());
+
+ System.out.println("学校信息:");
+ System.out.println("-".repeat(50));
+ System.out.printf("%-20s %-10s %-10s%n", "学校", "排名", "分数");
+ System.out.printf("%-20s %-10d %-10.2f%n",
+ comparison.getUniversityName1(),
+ comparison.getRank1(),
+ comparison.getScore1());
+ System.out.printf("%-20s %-10d %-10.2f%n",
+ comparison.getUniversityName2(),
+ comparison.getRank2(),
+ comparison.getScore2());
+
+ System.out.println();
+ System.out.println("对比结果:");
+ System.out.println("-".repeat(50));
+ System.out.printf("排名领先: %s (领先%d位)%n",
+ comparison.getHigherRankedUniversity(),
+ comparison.getRankGap());
+ System.out.printf("分数差距: %.2f分%n", comparison.getScoreGap());
+ System.out.println();
+ }
+
+ /**
+ * 打印历年趋势
+ *
+ * @param history 历年数据
+ * @param name 学校名称
+ */
+ public void printYearlyTrend(List history, String name) {
+ printSeparator();
+ System.out.println("【" + name + " 历年排名趋势】");
+ printSeparator();
+
+ System.out.printf("%-8s %-8s %-10s%n", "年份", "排名", "分数");
+ System.out.println("-".repeat(30));
+
+ University previous = null;
+ for (University u : history) {
+ String trend = "";
+ if (previous != null) {
+ int change = previous.getRank() - u.getRank();
+ if (change > 0) {
+ trend = "↑" + change;
+ } else if (change < 0) {
+ trend = "↓" + Math.abs(change);
+ } else {
+ trend = "-";
+ }
+ }
+
+ System.out.printf("%-8d %-8d %-10.2f %s%n",
+ u.getYear(), u.getRank(), u.getScore(), trend);
+ previous = u;
+ }
+
+ System.out.println();
+ }
+
+ /**
+ * 打印菜单
+ */
+ public void printMenu() {
+ printSeparator();
+ System.out.println("【高校排名分析系统】");
+ printSeparator();
+ System.out.println("1. 查看Top N高校排名");
+ System.out.println("2. 按省份查看高校");
+ System.out.println("3. 搜索高校");
+ System.out.println("4. 查看省份分布统计");
+ System.out.println("5. 查看分数统计");
+ System.out.println("6. 查看历年排名变化");
+ System.out.println("7. 对比两所高校");
+ System.out.println("8. 查看某高校历年趋势");
+ System.out.println("9. 生成所有图表");
+ System.out.println("0. 退出系统");
+ printSeparator();
+ System.out.print("请选择功能(0-9): ");
+ }
+
+ /**
+ * 打印欢迎信息
+ */
+ public void printWelcome() {
+ printSeparator();
+ System.out.println(" 欢迎使用高校排名分析系统");
+ System.out.println(" 本系统提供高校排名数据爬取、分析和可视化功能");
+ printSeparator();
+ System.out.println();
+ }
+
+ /**
+ * 截断字符串
+ *
+ * @param str 原字符串
+ * @param length 最大长度
+ * @return 截断后的字符串
+ */
+ private String truncate(String str, int length) {
+ if (str == null) return "";
+ if (str.length() <= length) return str;
+ return str.substring(0, length - 3) + "...";
+ }
+}
diff --git a/project/target/classes/com/university/Main.class b/project/target/classes/com/university/Main.class
new file mode 100644
index 0000000..71a146c
Binary files /dev/null and b/project/target/classes/com/university/Main.class differ
diff --git a/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class b/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class
new file mode 100644
index 0000000..cb7600e
Binary files /dev/null and b/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class differ
diff --git a/project/target/classes/com/university/analysis/RankAnalyzer.class b/project/target/classes/com/university/analysis/RankAnalyzer.class
new file mode 100644
index 0000000..735ad1b
Binary files /dev/null and b/project/target/classes/com/university/analysis/RankAnalyzer.class differ
diff --git a/project/target/classes/com/university/crawler/UniversityRankCrawler.class b/project/target/classes/com/university/crawler/UniversityRankCrawler.class
new file mode 100644
index 0000000..9828189
Binary files /dev/null and b/project/target/classes/com/university/crawler/UniversityRankCrawler.class differ
diff --git a/project/target/classes/com/university/model/RankChange.class b/project/target/classes/com/university/model/RankChange.class
new file mode 100644
index 0000000..b69584d
Binary files /dev/null and b/project/target/classes/com/university/model/RankChange.class differ
diff --git a/project/target/classes/com/university/model/University.class b/project/target/classes/com/university/model/University.class
new file mode 100644
index 0000000..4e29518
Binary files /dev/null and b/project/target/classes/com/university/model/University.class differ
diff --git a/project/target/classes/com/university/model/UniversityComparison.class b/project/target/classes/com/university/model/UniversityComparison.class
new file mode 100644
index 0000000..3dd0bba
Binary files /dev/null and b/project/target/classes/com/university/model/UniversityComparison.class differ
diff --git a/project/target/classes/com/university/storage/DataStorage.class b/project/target/classes/com/university/storage/DataStorage.class
new file mode 100644
index 0000000..26e7b4d
Binary files /dev/null and b/project/target/classes/com/university/storage/DataStorage.class differ
diff --git a/project/target/classes/com/university/visualization/ChartGenerator.class b/project/target/classes/com/university/visualization/ChartGenerator.class
new file mode 100644
index 0000000..82ea901
Binary files /dev/null and b/project/target/classes/com/university/visualization/ChartGenerator.class differ
diff --git a/project/target/classes/com/university/visualization/ConsoleReporter.class b/project/target/classes/com/university/visualization/ConsoleReporter.class
new file mode 100644
index 0000000..1b55633
Binary files /dev/null and b/project/target/classes/com/university/visualization/ConsoleReporter.class differ
diff --git a/project/target/maven-archiver/pom.properties b/project/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..bec0a4e
--- /dev/null
+++ b/project/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=university-rank-crawler
+groupId=com.university
+version=1.0-SNAPSHOT
diff --git a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..1b4a93f
--- /dev/null
+++ b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,10 @@
+com\university\analysis\RankAnalyzer.class
+com\university\model\University.class
+com\university\storage\DataStorage.class
+com\university\Main.class
+com\university\model\UniversityComparison.class
+com\university\analysis\RankAnalyzer$ScoreStatistics.class
+com\university\visualization\ChartGenerator.class
+com\university\crawler\UniversityRankCrawler.class
+com\university\model\RankChange.class
+com\university\visualization\ConsoleReporter.class
diff --git a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..4b2be80
--- /dev/null
+++ b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,9 @@
+D:\javatrae\src\main\java\com\university\analysis\RankAnalyzer.java
+D:\javatrae\src\main\java\com\university\model\RankChange.java
+D:\javatrae\src\main\java\com\university\Main.java
+D:\javatrae\src\main\java\com\university\model\UniversityComparison.java
+D:\javatrae\src\main\java\com\university\visualization\ConsoleReporter.java
+D:\javatrae\src\main\java\com\university\storage\DataStorage.java
+D:\javatrae\src\main\java\com\university\model\University.java
+D:\javatrae\src\main\java\com\university\visualization\ChartGenerator.java
+D:\javatrae\src\main\java\com\university\crawler\UniversityRankCrawler.java
diff --git a/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar b/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..cbf9a18
Binary files /dev/null and b/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar differ
diff --git a/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar b/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar
new file mode 100644
index 0000000..5829a6c
Binary files /dev/null and b/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar differ
diff --git a/project/target/university-rank-crawler-1.0-SNAPSHOT.jar b/project/target/university-rank-crawler-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..5829a6c
Binary files /dev/null and b/project/target/university-rank-crawler-1.0-SNAPSHOT.jar differ