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