@ -0,0 +1,4 @@ |
|||||
|
package PACKAGE_NAME; |
||||
|
|
||||
|
public class Circle { |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
package PACKAGE_NAME; |
||||
|
|
||||
|
public class Rectangle { |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
package PACKAGE_NAME; |
||||
|
|
||||
|
public class Shape { |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
package PACKAGE_NAME; |
||||
|
|
||||
|
public class ShapeUtil { |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
package PACKAGE_NAME; |
||||
|
|
||||
|
public class Triangle { |
||||
|
} |
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
|
|
@ -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> |
||||
@ -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> |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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 + " 年的数据文件"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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) + "..."; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
artifactId=university-rank-crawler |
||||
|
groupId=com.university |
||||
|
version=1.0-SNAPSHOT |
||||
@ -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 |
||||
@ -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 |
||||