Browse Source

feat: 完成Java爬虫与数据分析项目,已放入project子目录

main
jingjiaying 3 weeks ago
parent
commit
638343f908
  1. 4
      W4/src/Circle.java
  2. 4
      W4/src/Rectangle.java
  3. 4
      W4/src/Shape.java
  4. 4
      W4/src/ShapeUtil.java
  5. 4
      W4/src/Triangle.java
  6. BIN
      project/charts/province_distribution_2022.png
  7. BIN
      project/charts/province_distribution_2023.png
  8. BIN
      project/charts/province_distribution_2024.png
  9. BIN
      project/charts/rank_trend_上海交通大学.png
  10. BIN
      project/charts/rank_trend_北京大学.png
  11. BIN
      project/charts/rank_trend_复旦大学.png
  12. BIN
      project/charts/rank_trend_浙江大学.png
  13. BIN
      project/charts/rank_trend_清华大学.png
  14. BIN
      project/charts/top10_2022.png
  15. BIN
      project/charts/top10_2023.png
  16. BIN
      project/charts/top10_2024.png
  17. 21
      project/data/university_rank_2022.csv
  18. 21
      project/data/university_rank_2023.csv
  19. 21
      project/data/university_rank_2024.csv
  20. 51
      project/dependency-reduced-pom.xml
  21. 98
      project/pom.xml
  22. 359
      project/src/main/java/com/university/Main.java
  23. 250
      project/src/main/java/com/university/analysis/RankAnalyzer.java
  24. 153
      project/src/main/java/com/university/crawler/UniversityRankCrawler.java
  25. 145
      project/src/main/java/com/university/model/RankChange.java
  26. 120
      project/src/main/java/com/university/model/University.java
  27. 171
      project/src/main/java/com/university/model/UniversityComparison.java
  28. 202
      project/src/main/java/com/university/storage/DataStorage.java
  29. 299
      project/src/main/java/com/university/visualization/ChartGenerator.java
  30. 241
      project/src/main/java/com/university/visualization/ConsoleReporter.java
  31. BIN
      project/target/classes/com/university/Main.class
  32. BIN
      project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class
  33. BIN
      project/target/classes/com/university/analysis/RankAnalyzer.class
  34. BIN
      project/target/classes/com/university/crawler/UniversityRankCrawler.class
  35. BIN
      project/target/classes/com/university/model/RankChange.class
  36. BIN
      project/target/classes/com/university/model/University.class
  37. BIN
      project/target/classes/com/university/model/UniversityComparison.class
  38. BIN
      project/target/classes/com/university/storage/DataStorage.class
  39. BIN
      project/target/classes/com/university/visualization/ChartGenerator.class
  40. BIN
      project/target/classes/com/university/visualization/ConsoleReporter.class
  41. 3
      project/target/maven-archiver/pom.properties
  42. 10
      project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  43. 9
      project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
  44. BIN
      project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar
  45. BIN
      project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar
  46. BIN
      project/target/university-rank-crawler-1.0-SNAPSHOT.jar

4
W4/src/Circle.java

@ -0,0 +1,4 @@
package PACKAGE_NAME;
public class Circle {
}

4
W4/src/Rectangle.java

@ -0,0 +1,4 @@
package PACKAGE_NAME;
public class Rectangle {
}

4
W4/src/Shape.java

@ -0,0 +1,4 @@
package PACKAGE_NAME;
public class Shape {
}

4
W4/src/ShapeUtil.java

@ -0,0 +1,4 @@
package PACKAGE_NAME;
public class ShapeUtil {
}

4
W4/src/Triangle.java

@ -0,0 +1,4 @@
package PACKAGE_NAME;
public class Triangle {
}

BIN
project/charts/province_distribution_2022.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
project/charts/province_distribution_2023.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
project/charts/province_distribution_2024.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
project/charts/rank_trend_上海交通大学.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
project/charts/rank_trend_北京大学.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
project/charts/rank_trend_复旦大学.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
project/charts/rank_trend_浙江大学.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
project/charts/rank_trend_清华大学.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
project/charts/top10_2022.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
project/charts/top10_2023.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
project/charts/top10_2024.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

21
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"
1 排名 学校名称 省份 总分 年份
2 1 清华大学 北京 852.5 2022
3 2 北京大学 北京 848.2 2022
4 3 浙江大学 浙江 822.5 2022
5 4 上海交通大学 上海 815.3 2022
6 5 复旦大学 上海 805.1 2022
7 6 南京大学 江苏 785.6 2022
8 7 中国科学技术大学 安徽 782.4 2022
9 8 华中科技大学 湖北 765.8 2022
10 9 武汉大学 湖北 758.2 2022
11 10 西安交通大学 陕西 752.6 2022
12 11 中山大学 广东 745.3 2022
13 12 四川大学 四川 738.9 2022
14 13 哈尔滨工业大学 黑龙江 732.5 2022
15 14 北京航空航天大学 北京 725.8 2022
16 15 东南大学 江苏 718.4 2022
17 16 北京理工大学 北京 712.6 2022
18 17 同济大学 上海 705.3 2022
19 18 中国人民大学 北京 698.5 2022
20 19 北京师范大学 北京 692.1 2022
21 20 南开大学 天津 685.7 2022

21
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"
1 排名 学校名称 省份 总分 年份
2 1 清华大学 北京 853.0 2023
3 2 北京大学 北京 848.7 2023
4 3 浙江大学 浙江 823.0 2023
5 4 上海交通大学 上海 815.8 2023
6 5 复旦大学 上海 805.6 2023
7 6 南京大学 江苏 786.1 2023
8 7 中国科学技术大学 安徽 782.9 2023
9 8 华中科技大学 湖北 766.3 2023
10 9 武汉大学 湖北 758.7 2023
11 10 西安交通大学 陕西 753.1 2023
12 11 中山大学 广东 745.8 2023
13 12 四川大学 四川 739.4 2023
14 13 哈尔滨工业大学 黑龙江 733.0 2023
15 14 北京航空航天大学 北京 726.3 2023
16 15 东南大学 江苏 718.9 2023
17 16 北京理工大学 北京 713.1 2023
18 17 同济大学 上海 705.8 2023
19 18 中国人民大学 北京 699.0 2023
20 19 北京师范大学 北京 692.6 2023
21 20 南开大学 天津 686.2 2023

21
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"
1 排名 学校名称 省份 总分 年份
2 1 清华大学 北京 853.5 2024
3 2 北京大学 北京 849.2 2024
4 3 浙江大学 浙江 823.5 2024
5 4 上海交通大学 上海 816.3 2024
6 5 复旦大学 上海 806.1 2024
7 6 南京大学 江苏 786.6 2024
8 7 中国科学技术大学 安徽 783.4 2024
9 8 华中科技大学 湖北 766.8 2024
10 9 武汉大学 湖北 759.2 2024
11 10 西安交通大学 陕西 753.6 2024
12 11 中山大学 广东 746.3 2024
13 12 四川大学 四川 739.9 2024
14 13 哈尔滨工业大学 黑龙江 733.5 2024
15 14 北京航空航天大学 北京 726.8 2024
16 15 东南大学 江苏 719.4 2024
17 16 北京理工大学 北京 713.6 2024
18 17 同济大学 上海 706.3 2024
19 18 中国人民大学 北京 699.5 2024
20 19 北京师范大学 北京 693.1 2024
21 20 南开大学 天津 686.7 2024

51
project/dependency-reduced-pom.xml

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.university</groupId>
<artifactId>university-rank-crawler</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.university.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<mainClass>com.university.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

98
project/pom.xml

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 项目基本信息 -->
<groupId>com.university</groupId>
<artifactId>university-rank-crawler</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<!-- 设置Java版本为11 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<!-- 设置编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Jsoup: 用于发送HTTP请求和解析HTML -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.2</version>
</dependency>
<!-- JFreeChart: 用于生成图表 -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
<!-- OpenCSV: 用于读写CSV文件 -->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.8</version>
</dependency>
<!-- SLF4J: 日志接口 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<!-- Maven打包插件,包含依赖 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.university.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<!-- Exec插件,用于运行程序 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<mainClass>com.university.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

359
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<Integer, List<University>> 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<University> 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<University> data = dataCache.get(year);
if (data == null) {
System.out.println("该年份数据不存在!");
return;
}
List<University> 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<University> data = dataCache.get(year);
if (data == null) {
System.out.println("该年份数据不存在!");
return;
}
List<University> 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<University> data = dataCache.get(year);
if (data == null) {
System.out.println("该年份数据不存在!");
return;
}
List<University> 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<University> data = dataCache.get(year);
if (data == null) {
System.out.println("该年份数据不存在!");
return;
}
Map<String, Long> 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<University> 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<RankChange> changes = analyzer.calculateRankChanges(dataCache);
// 显示上升最快
List<RankChange> rising = analyzer.getFastestRising(changes, 5);
reporter.printRankChanges(rising, "排名上升最快 Top 5");
// 显示下降最快
List<RankChange> 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<University> data = dataCache.get(year);
if (data == null) {
System.out.println("该年份数据不存在!");
return;
}
Optional<University> u1 = data.stream()
.filter(u -> u.getName().equals(name1))
.findFirst();
Optional<University> 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<University> 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<Integer, List<University>> entry : dataCache.entrySet()) {
int year = entry.getKey();
List<University> data = entry.getValue();
// Top 10 柱状图
chartGenerator.generateTopNBarChart(data, year, 10);
// 省份分布饼图
Map<String, Long> provinceCount = analyzer.countByProvince(data);
chartGenerator.generateProvincePieChart(provinceCount, year);
}
// 排名变化图
List<RankChange> changes = analyzer.calculateRankChanges(dataCache);
List<RankChange> rising = analyzer.getFastestRising(changes, 10);
List<RankChange> 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<University> topUniversities = analyzer.getTopN(dataCache.get(2024), 5);
for (University u : topUniversities) {
List<University> history = analyzer.getUniversityHistory(dataCache, u.getName());
if (!history.isEmpty()) {
chartGenerator.generateRankTrendLineChart(history, u.getName());
}
}
System.out.println("所有图表生成完成!\n");
}
}

250
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<University> getTopN(List<University> universities, int n) {
return universities.stream()
.sorted(Comparator.comparingInt(University::getRank))
.limit(n)
.collect(Collectors.toList());
}
/**
* 按省份统计高校数量
*
* @param universities 高校列表
* @return 省份-数量映射
*/
public Map<String, Long> countByProvince(List<University> universities) {
return universities.stream()
.collect(Collectors.groupingBy(
University::getProvince,
Collectors.counting()
));
}
/**
* 按省份统计平均分
*
* @param universities 高校列表
* @return 省份-平均分映射
*/
public Map<String, Double> averageScoreByProvince(List<University> universities) {
return universities.stream()
.collect(Collectors.groupingBy(
University::getProvince,
Collectors.averagingDouble(University::getScore)
));
}
/**
* 获取指定省份的高校
*
* @param universities 高校列表
* @param province 省份
* @return 该省份的高校列表
*/
public List<University> getByProvince(List<University> 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<University> searchUniversity(List<University> universities, String keyword) {
return universities.stream()
.filter(u -> u.getName().contains(keyword))
.collect(Collectors.toList());
}
/**
* 获取分数统计信息
*
* @param universities 高校列表
* @return 统计信息
*/
public ScoreStatistics getScoreStatistics(List<University> 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<RankChange> calculateRankChanges(Map<Integer, List<University>> dataMap) {
List<RankChange> changes = new ArrayList<>();
// 获取所有年份并排序
List<Integer> 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<University> startData = dataMap.get(startYear);
List<University> endData = dataMap.get(endYear);
// 创建名称到高校的映射
Map<String, University> startMap = startData.stream()
.collect(Collectors.toMap(University::getName, u -> u));
Map<String, University> 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<RankChange> getFastestRising(List<RankChange> 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<RankChange> getFastestFalling(List<RankChange> 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<University> getUniversityHistory(Map<Integer, List<University>> dataMap,
String universityName) {
List<University> history = new ArrayList<>();
for (List<University> 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);
}
}
}

153
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<University> crawlRankings(int year) {
List<University> 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<List<University>> crawlMultipleYears(int startYear, int endYear) {
List<List<University>> allData = new ArrayList<>();
for (int year = startYear; year <= endYear; year++) {
List<University> yearData = crawlRankings(year);
allData.add(yearData);
}
return allData;
}
/**
* 生成模拟数据(用于演示)
* 当真实网站无法访问时使用
*/
private List<University> generateMockData(int year) {
List<University> 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;
}
}

145
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());
}
}

120
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);
}
}

171
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();
}
}

202
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<University> 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<University> beanToCsv = new StatefulBeanToCsvBuilder<University>(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<University> readFromCsv(int year) {
String filename = DATA_DIR + "/university_rank_" + year + ".csv";
List<University> universities = new ArrayList<>();
try (Reader reader = new InputStreamReader(
new FileInputStream(filename), StandardCharsets.UTF_8)) {
// 创建CSV读取器
CsvToBean<University> csvToBean = new CsvToBeanBuilder<University>(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<University> 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<University> readRawData(int year) {
String filename = DATA_DIR + "/university_rank_" + year + ".csv";
List<University> 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 + " 年的数据文件");
}
}
}

299
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<University> 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<String, Long> provinceCount, int year) {
// 创建饼图数据集
org.jfree.data.general.DefaultPieDataset<String> 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<University> 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<RankChange> 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<University> 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());
}
}
}

241
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<University> 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<String, Long> 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.<String, Long>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<RankChange> 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<University> 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) + "...";
}
}

BIN
project/target/classes/com/university/Main.class

Binary file not shown.

BIN
project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class

Binary file not shown.

BIN
project/target/classes/com/university/analysis/RankAnalyzer.class

Binary file not shown.

BIN
project/target/classes/com/university/crawler/UniversityRankCrawler.class

Binary file not shown.

BIN
project/target/classes/com/university/model/RankChange.class

Binary file not shown.

BIN
project/target/classes/com/university/model/University.class

Binary file not shown.

BIN
project/target/classes/com/university/model/UniversityComparison.class

Binary file not shown.

BIN
project/target/classes/com/university/storage/DataStorage.class

Binary file not shown.

BIN
project/target/classes/com/university/visualization/ChartGenerator.class

Binary file not shown.

BIN
project/target/classes/com/university/visualization/ConsoleReporter.class

Binary file not shown.

3
project/target/maven-archiver/pom.properties

@ -0,0 +1,3 @@
artifactId=university-rank-crawler
groupId=com.university
version=1.0-SNAPSHOT

10
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

9
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

BIN
project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar

Binary file not shown.

BIN
project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar

Binary file not shown.

BIN
project/target/university-rank-crawler-1.0-SNAPSHOT.jar

Binary file not shown.
Loading…
Cancel
Save