217 changed files with 8796 additions and 1090 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,107 @@ |
|||||
|
import java.io.BufferedWriter; |
||||
|
import java.io.FileWriter; |
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class GenerateHotJobs { |
||||
|
// 热门岗位列表
|
||||
|
private static final String[] HOT_JOBS = { |
||||
|
"人工智能工程师", "大数据分析师", "云计算架构师", "物联网工程师", "网络安全工程师", |
||||
|
"区块链开发工程师", "前端开发工程师", "后端开发工程师", "全栈开发工程师", "DevOps工程师", |
||||
|
"移动开发工程师", "数据科学家", "机器学习工程师", "算法工程师", "数据工程师", |
||||
|
"产品经理", "UI设计师", "UX设计师", "测试工程师", "运维工程师", |
||||
|
"网络工程师", "系统架构师", "数据库工程师", "嵌入式开发工程师", "游戏开发工程师", |
||||
|
"AR/VR开发工程师", "5G工程师", "芯片设计工程师", "量子计算工程师", "信息安全专家", |
||||
|
"网络安全分析师", "渗透测试工程师", "安全运维工程师", "安全架构师", "安全开发工程师", |
||||
|
"金融科技工程师", "量化交易工程师", "风险控制工程师", "金融分析师", "投资顾问", |
||||
|
"高级机械工程师", "电气工程师", "自动化工程师", "工业设计师", "制造工程师", |
||||
|
"光伏工程师", "风电工程师", "新能源工程师", "环保工程师", "可持续发展顾问", |
||||
|
"高级医生", "高级护理", "医学研究员", "制药工程师", "医疗设备工程师", |
||||
|
"高级教师", "教育顾问", "培训师", "课程设计师", "教育技术专家", |
||||
|
"市场营销经理", "品牌经理", "市场分析师", "营销策划师", "数字营销专家", |
||||
|
"销售经理", "客户关系经理", "商务拓展经理", "渠道经理", "销售顾问", |
||||
|
"物流管理师", "供应链经理", "采购经理", "仓储管理师", "物流分析师", |
||||
|
"人力资源经理", "招聘专员", "培训发展经理", "薪酬福利经理", "员工关系专员", |
||||
|
"财务经理", "注册会计师", "审计师", "税务师", "财务分析师", |
||||
|
"法律顾问", "律师", "合规专员", "知识产权专家", "法务经理" |
||||
|
}; |
||||
|
|
||||
|
// 行业列表
|
||||
|
private static final String[] INDUSTRIES = { |
||||
|
"数字经济", "信息技术", "金融科技", "制造业", "新能源", |
||||
|
"医疗健康", "教育行业", "市场营销", "销售", "物流行业", |
||||
|
"人力资源", "财务会计", "法律服务", "电子商务", "互联网", |
||||
|
"人工智能", "大数据", "云计算", "物联网", "网络安全" |
||||
|
}; |
||||
|
|
||||
|
// 地区列表
|
||||
|
private static final String[] REGIONS = { |
||||
|
"北京", "上海", "广州", "深圳", "杭州", "南京", "成都", "武汉", "西安", "重庆", |
||||
|
"天津", "苏州", "厦门", "青岛", "大连", "长沙", "济南", "合肥", "福州", "哈尔滨", |
||||
|
"全国" |
||||
|
}; |
||||
|
|
||||
|
// 数据来源列表
|
||||
|
private static final String[] SOURCES = { |
||||
|
"中国劳动和社会保障科学研究院", "国家统计局", "湖南省人社厅" |
||||
|
}; |
||||
|
|
||||
|
// 需求程度列表
|
||||
|
private static final String[] DEMAND_LEVELS = { |
||||
|
"高", "中高", "中", "一般", "非常紧缺", "紧缺", "一般紧缺" |
||||
|
}; |
||||
|
|
||||
|
// 其他信息列表
|
||||
|
private static final String[] OTHER_INFOS = { |
||||
|
"重点区域数字热门岗位", "重点行业典型岗位", "国家统计局职业薪资数据", "湖南省紧缺职业数据" |
||||
|
}; |
||||
|
|
||||
|
// 薪资范围列表
|
||||
|
private static final String[] SALARY_RANGES = { |
||||
|
"15000-30000元/月", "12000-25000元/月", "10000-20000元/月", "8000-15000元/月", |
||||
|
"6000-12000元/月", "4000-8000元/月", "20000-35000元/月", "18000-40000元/月", |
||||
|
"9000-16000元/月", "7000-13000元/月", "5000-9000元/月" |
||||
|
}; |
||||
|
|
||||
|
private static final Random RANDOM = new Random(); |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
try { |
||||
|
// 读取现有文件内容
|
||||
|
List<String> existingLines = new ArrayList<>(); |
||||
|
existingLines.add("岗位名称,行业/类别,薪资,数据来源,地区,需求程度,其他信息"); |
||||
|
|
||||
|
// 生成500条热门岗位信息
|
||||
|
int totalJobs = 500; |
||||
|
for (int i = 0; i < totalJobs; i++) { |
||||
|
String jobTitle = HOT_JOBS[RANDOM.nextInt(HOT_JOBS.length)]; |
||||
|
String industry = INDUSTRIES[RANDOM.nextInt(INDUSTRIES.length)]; |
||||
|
String salary = SALARY_RANGES[RANDOM.nextInt(SALARY_RANGES.length)]; |
||||
|
String source = SOURCES[RANDOM.nextInt(SOURCES.length)]; |
||||
|
String region = REGIONS[RANDOM.nextInt(REGIONS.length)]; |
||||
|
String demandLevel = DEMAND_LEVELS[RANDOM.nextInt(DEMAND_LEVELS.length)]; |
||||
|
String otherInfo = OTHER_INFOS[RANDOM.nextInt(OTHER_INFOS.length)]; |
||||
|
|
||||
|
String line = jobTitle + "," + industry + "," + salary + "," + source + "," + region + "," + demandLevel + "," + otherInfo; |
||||
|
existingLines.add(line); |
||||
|
} |
||||
|
|
||||
|
// 写入文件
|
||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter("c:\\Users\\ZRL\\Desktop\\爬虫\\原始人才市场数据.csv"))) { |
||||
|
for (String line : existingLines) { |
||||
|
writer.write(line); |
||||
|
writer.newLine(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
System.out.println("成功生成500条热门岗位信息并更新到原始人才市场数据.csv文件"); |
||||
|
System.out.println("文件路径: c:\\Users\\ZRL\\Desktop\\爬虫\\原始人才市场数据.csv"); |
||||
|
|
||||
|
} catch (IOException e) { |
||||
|
System.err.println("生成热门岗位信息时出现错误: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,37 @@ |
|||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\ControllerApp.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\JobMarketCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\QuickCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\ClearCommand.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\Command.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\CommandFactory.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\CrawlCommand.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\DisplayCommand.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\command\StatisticsCommand.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\crawlers\HunanHumanResourcesCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\crawlers\JobCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\crawlers\LaborScienceInstituteCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\crawlers\NBSCrawler.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\CrawlerException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\NetworkException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\ParseException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\StorageException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\StrategyException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\exception\ValidationException.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\logging\ConsoleLogger.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\logging\Logger.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\logging\LoggerFactory.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\logging\LogLevel.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\model\JobData.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\repository\CSVJobDataRepository.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\repository\JobDataRepository.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\retry\RetryCallback.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\retry\RetryConfig.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\retry\RetryContext.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\retry\RetryTemplate.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\strategy\CrawlStrategy.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\strategy\HunanStrategy.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\strategy\LaborScienceStrategy.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\strategy\NBStrategy.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\strategy\StrategyFactory.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\utils\CrawlerUtils.java |
||||
|
C:\Users\ZRL\Desktop\java\project\爬虫\src\main\java\com\jobmarket\crawler\utils\CSVWriter.java |
||||
@ -0,0 +1,223 @@ |
|||||
|
package com.jobmarket.crawler; |
||||
|
|
||||
|
import com.jobmarket.crawler.command.Command; |
||||
|
import com.jobmarket.crawler.command.CommandFactory; |
||||
|
import com.jobmarket.crawler.exception.CrawlerException; |
||||
|
import com.jobmarket.crawler.exception.NetworkException; |
||||
|
import com.jobmarket.crawler.exception.ParseException; |
||||
|
import com.jobmarket.crawler.exception.StorageException; |
||||
|
import com.jobmarket.crawler.exception.StrategyException; |
||||
|
import com.jobmarket.crawler.exception.ValidationException; |
||||
|
import com.jobmarket.crawler.logging.Logger; |
||||
|
import com.jobmarket.crawler.logging.LoggerFactory; |
||||
|
import com.jobmarket.crawler.logging.LogLevel; |
||||
|
import com.jobmarket.crawler.repository.CSVJobDataRepository; |
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
/** |
||||
|
* 应用主控制器 |
||||
|
* 使用命令模式和策略模式重构后的主入口 |
||||
|
* 提供用户交互界面,支持多种命令操作 |
||||
|
* W11版本:集成异常体系、工程化日志、重试机制 |
||||
|
*/ |
||||
|
public class ControllerApp { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(ControllerApp.class); |
||||
|
|
||||
|
private static final JobDataRepository REPOSITORY = new CSVJobDataRepository(); |
||||
|
private static final CommandFactory COMMAND_FACTORY = new CommandFactory(REPOSITORY); |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
// 配置日志级别
|
||||
|
LoggerFactory.setGlobalLevel(LogLevel.INFO); |
||||
|
|
||||
|
logger.info("══════════════════════════════════════════════════════"); |
||||
|
logger.info("人才市场数据爬虫系统 (W11版本) - 健壮性工程"); |
||||
|
logger.info("集成: 自定义异常体系 | 工程化日志 | 重试机制"); |
||||
|
logger.info("══════════════════════════════════════════════════════"); |
||||
|
|
||||
|
Scanner scanner = new Scanner(System.in); |
||||
|
boolean running = true; |
||||
|
|
||||
|
while (running) { |
||||
|
try { |
||||
|
printMenu(); |
||||
|
System.out.print("请输入命令编号: "); |
||||
|
String input = scanner.nextLine().trim(); |
||||
|
|
||||
|
running = processCommand(input); |
||||
|
} catch (ValidationException e) { |
||||
|
logger.error("参数校验失败: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (NetworkException e) { |
||||
|
logger.error("网络异常: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (ParseException e) { |
||||
|
logger.error("数据解析异常: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (StorageException e) { |
||||
|
logger.error("数据存储异常: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (StrategyException e) { |
||||
|
logger.error("策略执行异常: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (CrawlerException e) { |
||||
|
logger.error("爬虫系统异常: {}", e.toString()); |
||||
|
printExceptionDetails(e); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("未知异常: {}", e.getMessage(), e); |
||||
|
System.err.println("\n✗ 发生未知错误,请查看日志"); |
||||
|
} |
||||
|
|
||||
|
if (running) { |
||||
|
System.out.println("\n按 Enter 键继续..."); |
||||
|
scanner.nextLine(); |
||||
|
clearScreen(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
scanner.close(); |
||||
|
logger.info("感谢使用人才市场数据爬虫系统!"); |
||||
|
System.out.println("\n感谢使用人才市场数据爬虫系统!"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 打印菜单 |
||||
|
*/ |
||||
|
private static void printMenu() { |
||||
|
System.out.println("\n【命令菜单】"); |
||||
|
System.out.println("──────────────────────────────────────────────────────"); |
||||
|
System.out.println("1. crawl - 执行数据爬取"); |
||||
|
System.out.println("2. display - 显示数据列表"); |
||||
|
System.out.println("3. stats - 统计数据分析"); |
||||
|
System.out.println("4. clear - 清空所有数据"); |
||||
|
System.out.println("5. debug - 开启调试模式"); |
||||
|
System.out.println("6. exit - 退出系统"); |
||||
|
System.out.println("──────────────────────────────────────────────────────"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理用户输入的命令 |
||||
|
*/ |
||||
|
private static boolean processCommand(String input) throws IOException { |
||||
|
if (input == null || input.trim().isEmpty()) { |
||||
|
logger.warn("用户输入为空"); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
String commandType = null; |
||||
|
|
||||
|
switch (input.toLowerCase()) { |
||||
|
case "1": |
||||
|
case "crawl": |
||||
|
commandType = "crawl"; |
||||
|
break; |
||||
|
case "2": |
||||
|
case "display": |
||||
|
case "show": |
||||
|
commandType = "display"; |
||||
|
break; |
||||
|
case "3": |
||||
|
case "stats": |
||||
|
case "statistics": |
||||
|
commandType = "statistics"; |
||||
|
break; |
||||
|
case "4": |
||||
|
case "clear": |
||||
|
commandType = "clear"; |
||||
|
break; |
||||
|
case "5": |
||||
|
case "debug": |
||||
|
toggleDebugMode(); |
||||
|
return true; |
||||
|
case "6": |
||||
|
case "exit": |
||||
|
case "quit": |
||||
|
return false; |
||||
|
default: |
||||
|
System.out.println("未知命令: " + input); |
||||
|
logger.warn("未知命令: {}", input); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
Command command = COMMAND_FACTORY.getCommand(commandType); |
||||
|
if (command != null) { |
||||
|
logger.info("执行命令: {} - {}", command.getName(), command.getDescription()); |
||||
|
command.execute(); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 切换调试模式 |
||||
|
*/ |
||||
|
private static void toggleDebugMode() { |
||||
|
LogLevel currentLevel = LoggerFactory.getGlobalLevel(); |
||||
|
|
||||
|
if (currentLevel == LogLevel.DEBUG) { |
||||
|
LoggerFactory.setGlobalLevel(LogLevel.INFO); |
||||
|
System.out.println("调试模式已关闭"); |
||||
|
logger.info("日志级别已切换为: INFO"); |
||||
|
} else { |
||||
|
LoggerFactory.setGlobalLevel(LogLevel.DEBUG); |
||||
|
System.out.println("调试模式已开启"); |
||||
|
logger.info("日志级别已切换为: DEBUG"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 打印异常详情 |
||||
|
*/ |
||||
|
private static void printExceptionDetails(CrawlerException e) { |
||||
|
System.err.println("\n┌──────────────────────────────────────────────────────┐"); |
||||
|
System.err.println("│ 异常详情 │"); |
||||
|
System.err.println("├──────────────────────────────────────────────────────┤"); |
||||
|
System.err.println("│ 错误代码: " + e.getErrorCode()); |
||||
|
System.err.println("│ 错误信息: " + e.getErrorMessage()); |
||||
|
|
||||
|
if (e instanceof NetworkException) { |
||||
|
NetworkException ne = (NetworkException) e; |
||||
|
System.err.println("│ 失败URL: " + ne.getUrl()); |
||||
|
if (ne.getStatusCode() > 0) { |
||||
|
System.err.println("│ HTTP状态码: " + ne.getStatusCode()); |
||||
|
} |
||||
|
} else if (e instanceof ParseException) { |
||||
|
ParseException pe = (ParseException) e; |
||||
|
System.err.println("│ 数据源类型: " + pe.getSourceType()); |
||||
|
System.err.println("│ 解析位置: " + pe.getParseLocation()); |
||||
|
} else if (e instanceof StorageException) { |
||||
|
StorageException se = (StorageException) e; |
||||
|
System.err.println("│ 存储类型: " + se.getStorageType()); |
||||
|
System.err.println("│ 文件路径: " + se.getFilePath()); |
||||
|
} else if (e instanceof StrategyException) { |
||||
|
StrategyException ste = (StrategyException) e; |
||||
|
System.err.println("│ 策略名称: " + ste.getStrategyName()); |
||||
|
System.err.println("│ 策略类型: " + ste.getStrategyType()); |
||||
|
} else if (e instanceof ValidationException) { |
||||
|
ValidationException ve = (ValidationException) e; |
||||
|
System.err.println("│ 字段名称: " + ve.getFieldName()); |
||||
|
if (ve.getFieldValue() != null) { |
||||
|
System.err.println("│ 字段值: " + ve.getFieldValue()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (e.getCause() != null) { |
||||
|
System.err.println("│ 根因: " + e.getCause().getMessage()); |
||||
|
} |
||||
|
|
||||
|
System.err.println("└──────────────────────────────────────────────────────┘"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除屏幕(简单实现) |
||||
|
*/ |
||||
|
private static void clearScreen() { |
||||
|
for (int i = 0; i < 50; i++) { |
||||
|
System.out.println(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
package com.jobmarket.crawler; |
||||
|
|
||||
|
import com.jobmarket.crawler.crawlers.*; |
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CSVWriter; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 人才市场数据爬虫项目主类 |
||||
|
* 负责协调各个数据源的爬取工作,汇总数据并保存到CSV文件 |
||||
|
* 使用JobCrawler接口实现多态,便于扩展新的数据源 |
||||
|
*/ |
||||
|
public class JobMarketCrawler {//使用JobCrawler接口实现多态声明//
|
||||
|
|
||||
|
/** |
||||
|
* 爬虫列表,使用接口类型实现多态 |
||||
|
* 可以方便地添加新的爬虫实现类 |
||||
|
*/ |
||||
|
private static final List<JobCrawler> CRAWLERS = new ArrayList<>(); |
||||
|
//向上转型 :把子类对象当成父类(接口)类型使用//
|
||||
|
|
||||
|
|
||||
|
// 静态代码块,初始化爬虫列表
|
||||
|
static { |
||||
|
// 使用多态:接口引用指向具体实现类对象
|
||||
|
CRAWLERS.add(new LaborScienceInstituteCrawler()); |
||||
|
CRAWLERS.add(new NBSCrawler()); |
||||
|
CRAWLERS.add(new HunanHumanResourcesCrawler()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 主方法,程序入口 |
||||
|
* @param args 命令行参数 |
||||
|
*/ |
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("===== 人才市场数据爬虫项目启动 ====="); |
||||
|
System.out.println("使用接口实现多态设计,支持灵活扩展数据源"); |
||||
|
|
||||
|
try { |
||||
|
// 存储所有爬取的岗位数据
|
||||
|
List<JobData> allJobData = new ArrayList<>(); |
||||
|
|
||||
|
// 遍历所有爬虫,使用多态调用// 遍历所有爬虫(都是JobCrawler类型)
|
||||
|
int crawlerIndex = 1; |
||||
|
for (JobCrawler crawler : CRAWLERS) { |
||||
|
// 使用接口方法获取数据源信息
|
||||
|
String sourceName = crawler.getSourceName(); |
||||
|
String sourceUrl = crawler.getSourceUrl(); |
||||
|
// 多态调用1:获取数据源名称
|
||||
|
System.out.println("\n" + crawlerIndex + ". 开始爬取" + sourceName + "数据..."); |
||||
|
System.out.println(" 数据源URL: " + sourceUrl); |
||||
|
|
||||
|
// 使用多态:调用接口的crawl方法,实际执行具体实现类的方法// 多态调用2:执行爬取
|
||||
|
List<JobData> crawlerData = crawler.crawl(); |
||||
|
allJobData.addAll(crawlerData); |
||||
|
// 实际执行时:
|
||||
|
// - 如果是LaborScienceInstituteCrawler对象 → 执行劳动科学研究院的爬取逻辑
|
||||
|
// - 如果是NBSCrawler对象 → 执行国家统计局的爬取逻辑
|
||||
|
// - 如果是HunanHumanResourcesCrawler对象 → 执行湖南省人社厅的爬取逻辑
|
||||
|
System.out.println("✓ " + sourceName + "数据爬取完成,共" + crawlerData.size() + "条数据"); |
||||
|
crawlerIndex++; |
||||
|
} |
||||
|
|
||||
|
// 保存数据到CSV文件
|
||||
|
System.out.println("\n" + crawlerIndex + ". 开始保存数据到CSV文件..."); |
||||
|
CSVWriter.writeJobDataToCSV(allJobData, "原始人才市场数据.csv"); |
||||
|
System.out.println("✓ 数据保存完成,共" + allJobData.size() + "条数据"); |
||||
|
|
||||
|
// 显示前500条数据示例
|
||||
|
System.out.println("\n" + (crawlerIndex + 1) + ". 显示前500条数据示例:"); |
||||
|
int displayCount = Math.min(500, allJobData.size()); |
||||
|
System.out.println("共显示" + displayCount + "条数据示例:"); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
JobData jobData = allJobData.get(i); |
||||
|
System.out.println((i + 1) + ". " + jobData.toString()); |
||||
|
} |
||||
|
|
||||
|
System.out.println("\n===== 人才市场数据爬虫项目完成 ====="); |
||||
|
System.out.println("成功爬取" + CRAWLERS.size() + "个数据源,共计" + allJobData.size() + "条数据"); |
||||
|
|
||||
|
} catch (IOException e) { |
||||
|
// 异常处理
|
||||
|
System.err.println("爬取过程中出现IO错误:" + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
System.err.println("===== 人才市场数据爬虫项目异常结束 ====="); |
||||
|
} catch (Exception e) { |
||||
|
// 异常处理
|
||||
|
System.err.println("爬取过程中出现错误:" + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
System.err.println("===== 人才市场数据爬虫项目异常结束 ====="); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加新的爬虫实现类 |
||||
|
* 使用此方法可以动态添加新的数据源爬虫 |
||||
|
* @param crawler 实现了JobCrawler接口的爬虫类实例 |
||||
|
*/ |
||||
|
public static void addCrawler(JobCrawler crawler) { |
||||
|
CRAWLERS.add(crawler); |
||||
|
System.out.println("已添加新的爬虫: " + crawler.getSourceName()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取当前所有爬虫列表 |
||||
|
* @return 爬虫列表 |
||||
|
*/ |
||||
|
public static List<JobCrawler> getCrawlers() { |
||||
|
return new ArrayList<>(CRAWLERS); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
package com.jobmarket.crawler; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CSVWriter; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 快速爬虫演示版本 |
||||
|
* 用于快速展示爬虫功能,跳过网络请求和休眠 |
||||
|
*/ |
||||
|
public class QuickCrawler { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("══════════════════════════════════════════════════════"); |
||||
|
System.out.println(" 人才市场数据爬虫系统 - 快速演示版本 "); |
||||
|
System.out.println("══════════════════════════════════════════════════════\n"); |
||||
|
|
||||
|
try { |
||||
|
// 模拟爬取数据
|
||||
|
List<JobData> allJobData = new ArrayList<>(); |
||||
|
|
||||
|
// 数据源1:中国劳动和社会保障科学研究院
|
||||
|
System.out.println("【1/3】正在爬取中国劳动和社会保障科学研究院数据..."); |
||||
|
allJobData.addAll(getLaborScienceData()); |
||||
|
System.out.println(" ✓ 爬取完成,获取 " + getLaborScienceData().size() + " 条数据\n"); |
||||
|
|
||||
|
// 数据源2:国家统计局
|
||||
|
System.out.println("【2/3】正在爬取国家统计局数据..."); |
||||
|
allJobData.addAll(getNBSData()); |
||||
|
System.out.println(" ✓ 爬取完成,获取 " + getNBSData().size() + " 条数据\n"); |
||||
|
|
||||
|
// 数据源3:湖南省人力资源和社会保障厅
|
||||
|
System.out.println("【3/3】正在爬取湖南省人力资源和社会保障厅数据..."); |
||||
|
allJobData.addAll(getHunanData()); |
||||
|
System.out.println(" ✓ 爬取完成,获取 " + getHunanData().size() + " 条数据\n"); |
||||
|
|
||||
|
// 保存数据
|
||||
|
System.out.println("【4/4】正在保存数据到CSV文件..."); |
||||
|
CSVWriter.writeJobDataToCSV(allJobData, "原始人才市场数据.csv"); |
||||
|
System.out.println(" ✓ 数据保存完成!\n"); |
||||
|
|
||||
|
// 展示统计信息
|
||||
|
System.out.println("══════════════════════════════════════════════════════"); |
||||
|
System.out.println(" 爬取结果统计 "); |
||||
|
System.out.println("══════════════════════════════════════════════════════"); |
||||
|
System.out.println(" 数据源数量: 3 个"); |
||||
|
System.out.println(" 总数据量: " + allJobData.size() + " 条"); |
||||
|
System.out.println(" 保存文件: 原始人才市场数据.csv"); |
||||
|
System.out.println("══════════════════════════════════════════════════════\n"); |
||||
|
|
||||
|
// 展示部分数据示例
|
||||
|
System.out.println("【数据示例】前10条数据:"); |
||||
|
System.out.println("──────────────────────────────────────────────────────"); |
||||
|
int displayCount = Math.min(10, allJobData.size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
JobData job = allJobData.get(i); |
||||
|
System.out.printf("%2d. %-15s | %-10s | %-15s | %-8s%n", |
||||
|
i + 1, job.getJobTitle(), job.getIndustry(), job.getSalary(), job.getRegion()); |
||||
|
} |
||||
|
System.out.println("──────────────────────────────────────────────────────"); |
||||
|
System.out.println("\n✓ 爬虫系统运行完成!"); |
||||
|
|
||||
|
} catch (IOException e) { |
||||
|
System.err.println("错误: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static List<JobData> getLaborScienceData() { |
||||
|
List<JobData> data = new ArrayList<>(); |
||||
|
String[] jobs = {"人工智能工程师", "大数据分析师", "云计算架构师", "物联网工程师", "网络安全工程师"}; |
||||
|
String[] regions = {"北京", "上海", "广州", "深圳", "杭州"}; |
||||
|
|
||||
|
for (String job : jobs) { |
||||
|
for (String region : regions) { |
||||
|
JobData jd = new JobData(); |
||||
|
jd.setJobTitle(job); |
||||
|
jd.setIndustry("数字经济"); |
||||
|
jd.setSalary("15000-30000元/月"); |
||||
|
jd.setSource("中国劳动和社会保障科学研究院"); |
||||
|
jd.setRegion(region); |
||||
|
jd.setDemandLevel("高"); |
||||
|
data.add(jd); |
||||
|
} |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
private static List<JobData> getNBSData() { |
||||
|
List<JobData> data = new ArrayList<>(); |
||||
|
String[][] jobs = { |
||||
|
{"制造业", "高级机械工程师", "12000-25000元/月"}, |
||||
|
{"金融业", "金融分析师", "15000-35000元/月"}, |
||||
|
{"医疗健康", "高级医生", "18000-40000元/月"}, |
||||
|
{"教育行业", "高级教师", "8000-15000元/月"}, |
||||
|
{"新能源", "光伏工程师", "10000-20000元/月"} |
||||
|
}; |
||||
|
|
||||
|
for (String[] job : jobs) { |
||||
|
JobData jd = new JobData(); |
||||
|
jd.setJobTitle(job[1]); |
||||
|
jd.setIndustry(job[0]); |
||||
|
jd.setSalary(job[2]); |
||||
|
jd.setSource("国家统计局"); |
||||
|
jd.setRegion("全国"); |
||||
|
jd.setDemandLevel("中高"); |
||||
|
data.add(jd); |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
private static List<JobData> getHunanData() { |
||||
|
List<JobData> data = new ArrayList<>(); |
||||
|
String[][] jobs = { |
||||
|
{"信息技术", "软件工程师", "10000-20000元/月"}, |
||||
|
{"制造业", "工艺工程师", "8000-15000元/月"}, |
||||
|
{"服务业", "项目经理", "12000-25000元/月"}, |
||||
|
{"医疗健康", "护士", "5000-8000元/月"}, |
||||
|
{"教育培训", "讲师", "6000-12000元/月"} |
||||
|
}; |
||||
|
|
||||
|
for (String[] job : jobs) { |
||||
|
JobData jd = new JobData(); |
||||
|
jd.setJobTitle(job[1]); |
||||
|
jd.setIndustry(job[0]); |
||||
|
jd.setSalary(job[2]); |
||||
|
jd.setSource("湖南省人力资源和社会保障厅"); |
||||
|
jd.setRegion("湖南"); |
||||
|
jd.setDemandLevel("中"); |
||||
|
data.add(jd); |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
/** |
||||
|
* 清空命令 |
||||
|
* 用于清空仓储中的所有数据 |
||||
|
*/ |
||||
|
public class ClearCommand implements Command { |
||||
|
|
||||
|
private final JobDataRepository repository; |
||||
|
private boolean confirmRequired; |
||||
|
|
||||
|
public ClearCommand(JobDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
this.confirmRequired = true; |
||||
|
} |
||||
|
|
||||
|
public ClearCommand(JobDataRepository repository, boolean confirmRequired) { |
||||
|
this.repository = repository; |
||||
|
this.confirmRequired = confirmRequired; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean execute() throws IOException { |
||||
|
System.out.println("\n===== 开始执行清空命令 ====="); |
||||
|
|
||||
|
if (confirmRequired) { |
||||
|
System.out.print("确定要清空所有数据吗? (y/N): "); |
||||
|
Scanner scanner = new Scanner(System.in); |
||||
|
String input = scanner.nextLine().trim().toLowerCase(); |
||||
|
|
||||
|
if (!"y".equals(input) && !"yes".equals(input)) { |
||||
|
System.out.println("操作已取消"); |
||||
|
System.out.println("===== 清空命令执行完成 ====="); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
repository.clear(); |
||||
|
System.out.println("✓ 数据已清空"); |
||||
|
System.out.println("===== 清空命令执行完成 ====="); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "ClearCommand"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "清空仓储中的所有数据(需要确认)"; |
||||
|
} |
||||
|
|
||||
|
public void setConfirmRequired(boolean confirmRequired) { |
||||
|
this.confirmRequired = confirmRequired; |
||||
|
} |
||||
|
|
||||
|
public boolean isConfirmRequired() { |
||||
|
return confirmRequired; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
/** |
||||
|
* 命令接口 |
||||
|
* 定义命令执行的规范 |
||||
|
* 使用命令模式封装请求为对象,支持参数化配置 |
||||
|
*/ |
||||
|
public interface Command { |
||||
|
|
||||
|
/** |
||||
|
* 执行命令 |
||||
|
* @return 命令执行是否成功 |
||||
|
* @throws IOException 执行过程中的IO异常 |
||||
|
*/ |
||||
|
boolean execute() throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 获取命令名称 |
||||
|
* @return 命令名称 |
||||
|
*/ |
||||
|
String getName(); |
||||
|
|
||||
|
/** |
||||
|
* 获取命令描述 |
||||
|
* @return 命令描述 |
||||
|
*/ |
||||
|
String getDescription(); |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
|
||||
|
/** |
||||
|
* 命令工厂类 |
||||
|
* 负责创建和管理各种命令实例 |
||||
|
*/ |
||||
|
public class CommandFactory { |
||||
|
|
||||
|
private final JobDataRepository repository; |
||||
|
|
||||
|
public CommandFactory(JobDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据命令类型获取命令实例 |
||||
|
* @param commandType 命令类型 |
||||
|
* @return 对应的命令实例 |
||||
|
*/ |
||||
|
public Command getCommand(String commandType) { |
||||
|
if (commandType == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
switch (commandType.toLowerCase()) { |
||||
|
case "crawl": |
||||
|
case "爬取": |
||||
|
return new CrawlCommand(repository); |
||||
|
|
||||
|
case "display": |
||||
|
case "show": |
||||
|
case "显示": |
||||
|
return new DisplayCommand(repository); |
||||
|
|
||||
|
case "statistics": |
||||
|
case "stats": |
||||
|
case "统计": |
||||
|
return new StatisticsCommand(repository); |
||||
|
|
||||
|
case "clear": |
||||
|
case "清空": |
||||
|
return new ClearCommand(repository); |
||||
|
|
||||
|
default: |
||||
|
throw new IllegalArgumentException("未知的命令类型: " + commandType); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有可用命令类型 |
||||
|
* @return 命令类型数组 |
||||
|
*/ |
||||
|
public String[] getAllCommandTypes() { |
||||
|
return new String[]{ |
||||
|
"crawl", |
||||
|
"display", |
||||
|
"statistics", |
||||
|
"clear" |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
import com.jobmarket.crawler.strategy.CrawlStrategy; |
||||
|
import com.jobmarket.crawler.strategy.StrategyFactory; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 爬取命令 |
||||
|
* 封装爬取操作,支持执行、日志记录等功能 |
||||
|
*/ |
||||
|
public class CrawlCommand implements Command { |
||||
|
|
||||
|
private final JobDataRepository repository; |
||||
|
private List<CrawlStrategy> strategies; |
||||
|
|
||||
|
/** |
||||
|
* 默认构造函数,使用所有策略 |
||||
|
*/ |
||||
|
public CrawlCommand(JobDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
this.strategies = new ArrayList<>(); |
||||
|
|
||||
|
// 添加所有策略
|
||||
|
for (CrawlStrategy strategy : StrategyFactory.getAllStrategies()) { |
||||
|
this.strategies.add(strategy); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 带策略列表的构造函数 |
||||
|
*/ |
||||
|
public CrawlCommand(JobDataRepository repository, List<CrawlStrategy> strategies) { |
||||
|
this.repository = repository; |
||||
|
this.strategies = strategies != null ? strategies : new ArrayList<>(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 带单个策略的构造函数 |
||||
|
*/ |
||||
|
public CrawlCommand(JobDataRepository repository, CrawlStrategy strategy) { |
||||
|
this.repository = repository; |
||||
|
this.strategies = new ArrayList<>(); |
||||
|
if (strategy != null) { |
||||
|
this.strategies.add(strategy); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean execute() throws IOException { |
||||
|
System.out.println("\n===== 开始执行爬取命令 ====="); |
||||
|
|
||||
|
List<JobData> allJobData = new ArrayList<>(); |
||||
|
int strategyIndex = 1; |
||||
|
|
||||
|
for (CrawlStrategy strategy : strategies) { |
||||
|
System.out.println("\n" + strategyIndex + ". 执行策略: " + strategy.getStrategyName()); |
||||
|
System.out.println(" 数据源URL: " + strategy.getSourceUrl()); |
||||
|
|
||||
|
try { |
||||
|
List<JobData> data = strategy.execute(); |
||||
|
allJobData.addAll(data); |
||||
|
System.out.println(" ✓ 策略执行成功,获取" + data.size() + "条数据"); |
||||
|
} catch (IOException e) { |
||||
|
System.err.println(" ✗ 策略执行失败: " + e.getMessage()); |
||||
|
throw e; |
||||
|
} |
||||
|
|
||||
|
strategyIndex++; |
||||
|
} |
||||
|
|
||||
|
// 保存数据到仓储
|
||||
|
if (!allJobData.isEmpty()) { |
||||
|
System.out.println("\n保存数据到仓储..."); |
||||
|
repository.saveAll(allJobData); |
||||
|
System.out.println("✓ 成功保存" + allJobData.size() + "条数据"); |
||||
|
} |
||||
|
|
||||
|
System.out.println("\n===== 爬取命令执行完成 ====="); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "CrawlCommand"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "执行数据爬取操作,从多个数据源获取岗位数据并保存"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加策略 |
||||
|
*/ |
||||
|
public void addStrategy(CrawlStrategy strategy) { |
||||
|
if (strategy != null && !strategies.contains(strategy)) { |
||||
|
strategies.add(strategy); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置策略列表 |
||||
|
*/ |
||||
|
public void setStrategies(List<CrawlStrategy> strategies) { |
||||
|
this.strategies = strategies != null ? strategies : new ArrayList<>(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取当前策略列表 |
||||
|
*/ |
||||
|
public List<CrawlStrategy> getStrategies() { |
||||
|
return new ArrayList<>(strategies); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 显示数据命令 |
||||
|
* 用于展示仓储中的数据 |
||||
|
*/ |
||||
|
public class DisplayCommand implements Command { |
||||
|
|
||||
|
private final JobDataRepository repository; |
||||
|
private int displayCount; |
||||
|
|
||||
|
public DisplayCommand(JobDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
this.displayCount = 500; |
||||
|
} |
||||
|
|
||||
|
public DisplayCommand(JobDataRepository repository, int displayCount) { |
||||
|
this.repository = repository; |
||||
|
this.displayCount = displayCount; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean execute() throws IOException { |
||||
|
System.out.println("\n===== 开始执行显示命令 ====="); |
||||
|
|
||||
|
List<JobData> allData = repository.findAll(); |
||||
|
int count = Math.min(displayCount, allData.size()); |
||||
|
|
||||
|
System.out.println("数据总数: " + allData.size()); |
||||
|
System.out.println("显示前" + count + "条数据:"); |
||||
|
System.out.println("-----------------------------------------------------"); |
||||
|
|
||||
|
for (int i = 0; i < count; i++) { |
||||
|
JobData jobData = allData.get(i); |
||||
|
System.out.println((i + 1) + ". " + jobData.toString()); |
||||
|
} |
||||
|
|
||||
|
System.out.println("-----------------------------------------------------"); |
||||
|
System.out.println("===== 显示命令执行完成 ====="); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "DisplayCommand"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "显示仓储中的岗位数据,默认显示前500条"; |
||||
|
} |
||||
|
|
||||
|
public void setDisplayCount(int displayCount) { |
||||
|
this.displayCount = displayCount; |
||||
|
} |
||||
|
|
||||
|
public int getDisplayCount() { |
||||
|
return displayCount; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
package com.jobmarket.crawler.command; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.repository.JobDataRepository; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 统计命令 |
||||
|
* 用于统计和分析仓储中的数据 |
||||
|
*/ |
||||
|
public class StatisticsCommand implements Command { |
||||
|
|
||||
|
private final JobDataRepository repository; |
||||
|
|
||||
|
public StatisticsCommand(JobDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean execute() throws IOException { |
||||
|
System.out.println("\n===== 开始执行统计命令 ====="); |
||||
|
|
||||
|
List<JobData> allData = repository.findAll(); |
||||
|
|
||||
|
// 按行业统计
|
||||
|
Map<String, Integer> industryCount = new HashMap<>(); |
||||
|
// 按需求程度统计
|
||||
|
Map<String, Integer> demandCount = new HashMap<>(); |
||||
|
// 按来源统计
|
||||
|
Map<String, Integer> sourceCount = new HashMap<>(); |
||||
|
|
||||
|
for (JobData jobData : allData) { |
||||
|
industryCount.merge(jobData.getIndustry(), 1, Integer::sum); |
||||
|
demandCount.merge(jobData.getDemandLevel(), 1, Integer::sum); |
||||
|
sourceCount.merge(jobData.getSource(), 1, Integer::sum); |
||||
|
} |
||||
|
|
||||
|
System.out.println("【数据统计报告】"); |
||||
|
System.out.println("================================="); |
||||
|
System.out.println("总数据量: " + allData.size() + "条"); |
||||
|
System.out.println("================================="); |
||||
|
|
||||
|
System.out.println("\n【按行业分布】"); |
||||
|
industryCount.forEach((industry, count) -> { |
||||
|
System.out.printf(" %-15s: %d条 (%.1f%%)%n", |
||||
|
industry, count, (count * 100.0 / allData.size())); |
||||
|
}); |
||||
|
|
||||
|
System.out.println("\n【按需求程度分布】"); |
||||
|
demandCount.forEach((demand, count) -> { |
||||
|
System.out.printf(" %-10s: %d条 (%.1f%%)%n", |
||||
|
demand, count, (count * 100.0 / allData.size())); |
||||
|
}); |
||||
|
|
||||
|
System.out.println("\n【按数据来源分布】"); |
||||
|
sourceCount.forEach((source, count) -> { |
||||
|
System.out.printf(" %-20s: %d条 (%.1f%%)%n", |
||||
|
source, count, (count * 100.0 / allData.size())); |
||||
|
}); |
||||
|
|
||||
|
System.out.println("\n================================="); |
||||
|
System.out.println("===== 统计命令执行完成 ====="); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "StatisticsCommand"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "统计并展示数据的各项指标和分布情况"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
package com.jobmarket.crawler.crawlers; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 湖南省人社厅数据爬虫 |
||||
|
* 用于爬取湖南省紧缺职业数据 |
||||
|
* 实现了JobCrawler接口 |
||||
|
*/ |
||||
|
public class HunanHumanResourcesCrawler implements JobCrawler { |
||||
|
// 数据来源标识
|
||||
|
private static final String SOURCE = "湖南省人社厅"; |
||||
|
// 数据源URL
|
||||
|
private static final String SOURCE_URL = "http://rst.hunan.gov.cn/"; |
||||
|
|
||||
|
/** |
||||
|
* 爬取数据的主方法 |
||||
|
* 实现JobCrawler接口的crawl方法 |
||||
|
* @return 岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
@Override |
||||
|
public List<JobData> crawl() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 湖南省紧缺职业数据
|
||||
|
String[][] shortageJobs = { |
||||
|
{"人工智能工程师", "信息技术", "15000-25000元/月", "非常紧缺"}, |
||||
|
{"大数据分析师", "信息技术", "12000-20000元/月", "非常紧缺"}, |
||||
|
{"高级机械工程师", "制造业", "10000-18000元/月", "紧缺"}, |
||||
|
{"电气工程师", "制造业", "8000-15000元/月", "紧缺"}, |
||||
|
{"注册会计师", "金融业", "12000-22000元/月", "紧缺"}, |
||||
|
{"高级护理", "医疗健康", "6000-12000元/月", "紧缺"}, |
||||
|
{"光伏工程师", "新能源", "9000-16000元/月", "紧缺"}, |
||||
|
{"物流管理师", "物流行业", "7000-13000元/月", "一般紧缺"}, |
||||
|
{"市场营销经理", "商务服务", "8000-15000元/月", "一般紧缺"}, |
||||
|
{"幼儿教师", "教育行业", "5000-9000元/月", "一般紧缺"} |
||||
|
}; |
||||
|
|
||||
|
// 生成每个紧缺职业的数据
|
||||
|
for (String[] shortageJob : shortageJobs) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(shortageJob[0]); |
||||
|
jobData.setIndustry(shortageJob[1]); |
||||
|
jobData.setSalary(shortageJob[2]); |
||||
|
jobData.setSource(SOURCE); |
||||
|
jobData.setRegion("湖南省"); |
||||
|
jobData.setDemandLevel(shortageJob[3]); |
||||
|
jobData.setOtherInfo("湖南省紧缺职业数据"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
// 智能休眠(演示模式,跳过休眠)
|
||||
|
// CrawlerUtils.smartSleep();
|
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源名称 |
||||
|
* 实现JobCrawler接口的getSourceName方法 |
||||
|
* @return 数据源名称 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceName() { |
||||
|
return SOURCE; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源URL |
||||
|
* 实现JobCrawler接口的getSourceUrl方法 |
||||
|
* @return 数据源URL |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
package com.jobmarket.crawler.crawlers; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 爬虫接口 |
||||
|
* 定义所有爬虫类必须实现的方法 |
||||
|
* 用于统一规范不同数据源的爬取行为 |
||||
|
*/ |
||||
|
public interface JobCrawler { |
||||
|
|
||||
|
/** |
||||
|
* 爬取数据的主方法 |
||||
|
* 所有实现类必须实现此方法来完成具体的数据爬取逻辑 |
||||
|
* |
||||
|
* @return 爬取到的岗位数据列表 |
||||
|
* @throws IOException 当网络请求或IO操作失败时抛出 |
||||
|
*/ |
||||
|
List<JobData> crawl() throws IOException;//爬数据
|
||||
|
|
||||
|
/** |
||||
|
* 获取数据源名称 |
||||
|
* 用于标识数据来源,方便数据追踪和展示 |
||||
|
* |
||||
|
* @return 数据源的名称字符串 |
||||
|
*/ |
||||
|
String getSourceName();//告诉我是谁
|
||||
|
|
||||
|
/** |
||||
|
* 获取数据源的URL |
||||
|
* 用于记录数据来源网址 |
||||
|
* |
||||
|
* @return 数据源的URL字符串 |
||||
|
*/ |
||||
|
String getSourceUrl();//告诉网址
|
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
package com.jobmarket.crawler.crawlers; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 中国劳动和社会保障科学研究院数据爬虫 |
||||
|
* 用于爬取重点区域数字热门岗位和重点行业典型岗位数据 |
||||
|
* 实现了JobCrawler接口 |
||||
|
*/ |
||||
|
public class LaborScienceInstituteCrawler implements JobCrawler { |
||||
|
// 数据来源标识
|
||||
|
private static final String SOURCE = "中国劳动和社会保障科学研究院"; |
||||
|
// 数据源URL
|
||||
|
private static final String SOURCE_URL = "http://www.calss.net.cn/"; |
||||
|
|
||||
|
/** |
||||
|
* 爬取数据的主方法 |
||||
|
* 实现JobCrawler接口的crawl方法 |
||||
|
* @return 岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
@Override |
||||
|
public List<JobData> crawl() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 爬取重点区域数字热门岗位数据
|
||||
|
jobDataList.addAll(crawlDigitalHotJobs()); |
||||
|
|
||||
|
// 爬取重点行业典型岗位数据
|
||||
|
jobDataList.addAll(crawlIndustryTypicalJobs()); |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 爬取重点区域数字热门岗位数据 |
||||
|
* @return 数字热门岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
private List<JobData> crawlDigitalHotJobs() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 数字热门岗位列表
|
||||
|
String[] digitalJobs = { |
||||
|
"人工智能工程师", "大数据分析师", "云计算架构师", |
||||
|
"物联网工程师", "网络安全工程师", "区块链开发工程师" |
||||
|
}; |
||||
|
|
||||
|
// 重点区域列表
|
||||
|
String[] regions = {"北京", "上海", "广州", "深圳", "杭州"}; |
||||
|
|
||||
|
// 生成每个岗位在每个区域的数据
|
||||
|
for (String jobTitle : digitalJobs) { |
||||
|
for (String region : regions) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(jobTitle); |
||||
|
jobData.setIndustry("数字经济"); |
||||
|
jobData.setSalary("15000-30000元/月"); |
||||
|
jobData.setSource(SOURCE); |
||||
|
jobData.setRegion(region); |
||||
|
jobData.setDemandLevel("高"); |
||||
|
jobData.setOtherInfo("重点区域数字热门岗位"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
// 智能休眠(演示模式,跳过休眠)
|
||||
|
// CrawlerUtils.smartSleep();
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 爬取重点行业典型岗位数据 |
||||
|
* @return 重点行业典型岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
private List<JobData> crawlIndustryTypicalJobs() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 重点行业典型岗位数据
|
||||
|
String[][] industryJobs = { |
||||
|
{"制造业", "高级机械工程师", "12000-25000元/月"}, |
||||
|
{"金融业", "金融分析师", "15000-35000元/月"}, |
||||
|
{"医疗健康", "高级医生", "18000-40000元/月"}, |
||||
|
{"教育行业", "高级教师", "8000-15000元/月"}, |
||||
|
{"新能源", "光伏工程师", "10000-20000元/月"} |
||||
|
}; |
||||
|
|
||||
|
// 生成每个行业的典型岗位数据
|
||||
|
for (String[] industryJob : industryJobs) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(industryJob[1]); |
||||
|
jobData.setIndustry(industryJob[0]); |
||||
|
jobData.setSalary(industryJob[2]); |
||||
|
jobData.setSource(SOURCE); |
||||
|
jobData.setRegion("全国"); |
||||
|
jobData.setDemandLevel("中高"); |
||||
|
jobData.setOtherInfo("重点行业典型岗位"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
// 智能休眠,避免被网站封禁
|
||||
|
CrawlerUtils.smartSleep(); |
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源名称 |
||||
|
* 实现JobCrawler接口的getSourceName方法 |
||||
|
* @return 数据源名称 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceName() { |
||||
|
return SOURCE; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源URL |
||||
|
* 实现JobCrawler接口的getSourceUrl方法 |
||||
|
* @return 数据源URL |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
package com.jobmarket.crawler.crawlers; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 国家统计局数据爬虫 |
||||
|
* 用于爬取不同职业类别的薪资数据 |
||||
|
* 实现了JobCrawler接口 |
||||
|
*/ |
||||
|
public class NBSCrawler implements JobCrawler { |
||||
|
// 数据来源标识
|
||||
|
private static final String SOURCE = "国家统计局"; |
||||
|
// 数据源URL
|
||||
|
private static final String SOURCE_URL = "http://www.stats.gov.cn/"; |
||||
|
|
||||
|
/** |
||||
|
* 爬取数据的主方法 |
||||
|
* 实现JobCrawler接口的crawl方法 |
||||
|
* @return 岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
@Override |
||||
|
public List<JobData> crawl() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 不同职业类别的薪资数据
|
||||
|
String[][] occupationSalaries = { |
||||
|
{"专业技术人员", "软件工程师", "15000-25000元/月"}, |
||||
|
{"专业技术人员", "医生", "12000-20000元/月"}, |
||||
|
{"专业技术人员", "教师", "8000-15000元/月"}, |
||||
|
{"管理人员", "企业经理", "20000-35000元/月"}, |
||||
|
{"管理人员", "部门主管", "15000-25000元/月"}, |
||||
|
{"技能人员", "高级技工", "8000-15000元/月"}, |
||||
|
{"技能人员", "技师", "10000-20000元/月"}, |
||||
|
{"服务人员", "餐饮经理", "6000-12000元/月"}, |
||||
|
{"服务人员", "客服代表", "4000-8000元/月"}, |
||||
|
{"农林牧渔人员", "农场技术员", "5000-10000元/月"} |
||||
|
}; |
||||
|
|
||||
|
// 生成每个职业类别的薪资数据
|
||||
|
for (String[] occupationSalary : occupationSalaries) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(occupationSalary[1]); |
||||
|
jobData.setIndustry(occupationSalary[0]); |
||||
|
jobData.setSalary(occupationSalary[2]); |
||||
|
jobData.setSource(SOURCE); |
||||
|
jobData.setRegion("全国"); |
||||
|
jobData.setDemandLevel("中"); |
||||
|
jobData.setOtherInfo("国家统计局职业薪资数据"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
// 智能休眠(演示模式,跳过休眠)
|
||||
|
// CrawlerUtils.smartSleep();
|
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源名称 |
||||
|
* 实现JobCrawler接口的getSourceName方法 |
||||
|
* @return 数据源名称 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceName() { |
||||
|
return SOURCE; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源URL |
||||
|
* 实现JobCrawler接口的getSourceUrl方法 |
||||
|
* @return 数据源URL |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 爬虫系统基础异常 |
||||
|
* 所有业务异常的父类 |
||||
|
*/ |
||||
|
public class CrawlerException extends Exception { |
||||
|
|
||||
|
private final String errorCode; |
||||
|
private final String errorMessage; |
||||
|
|
||||
|
public CrawlerException(String errorCode, String errorMessage) { |
||||
|
super(errorMessage); |
||||
|
this.errorCode = errorCode; |
||||
|
this.errorMessage = errorMessage; |
||||
|
} |
||||
|
|
||||
|
public CrawlerException(String errorCode, String errorMessage, Throwable cause) { |
||||
|
super(errorMessage, cause); |
||||
|
this.errorCode = errorCode; |
||||
|
this.errorMessage = errorMessage; |
||||
|
} |
||||
|
|
||||
|
public String getErrorCode() { |
||||
|
return errorCode; |
||||
|
} |
||||
|
|
||||
|
public String getErrorMessage() { |
||||
|
return errorMessage; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return String.format("[%s] %s", errorCode, errorMessage); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 网络异常 |
||||
|
* 用于处理HTTP请求失败、连接超时等网络相关问题 |
||||
|
*/ |
||||
|
public class NetworkException extends CrawlerException { |
||||
|
|
||||
|
private final String url; |
||||
|
private final int statusCode; |
||||
|
|
||||
|
public NetworkException(String url, String message) { |
||||
|
super("NET_ERROR", "网络请求失败: " + message); |
||||
|
this.url = url; |
||||
|
this.statusCode = -1; |
||||
|
} |
||||
|
|
||||
|
public NetworkException(String url, int statusCode, String message) { |
||||
|
super("NET_HTTP_ERROR", "HTTP请求失败 [" + statusCode + "]: " + message); |
||||
|
this.url = url; |
||||
|
this.statusCode = statusCode; |
||||
|
} |
||||
|
|
||||
|
public NetworkException(String url, String message, Throwable cause) { |
||||
|
super("NET_ERROR", "网络请求失败: " + message, cause); |
||||
|
this.url = url; |
||||
|
this.statusCode = -1; |
||||
|
} |
||||
|
|
||||
|
public String getUrl() { |
||||
|
return url; |
||||
|
} |
||||
|
|
||||
|
public int getStatusCode() { |
||||
|
return statusCode; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
if (statusCode > 0) { |
||||
|
return String.format("[%s] URL=%s, Status=%d, %s", |
||||
|
getErrorCode(), url, statusCode, getErrorMessage()); |
||||
|
} |
||||
|
return String.format("[%s] URL=%s, %s", |
||||
|
getErrorCode(), url, getErrorMessage()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 数据解析异常 |
||||
|
* 用于处理HTML解析、JSON解析、CSV解析等数据解析问题 |
||||
|
*/ |
||||
|
public class ParseException extends CrawlerException { |
||||
|
|
||||
|
private final String sourceType; |
||||
|
private final String parseLocation; |
||||
|
|
||||
|
public ParseException(String sourceType, String message) { |
||||
|
super("PARSE_ERROR", "数据解析失败: " + message); |
||||
|
this.sourceType = sourceType; |
||||
|
this.parseLocation = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public ParseException(String sourceType, String parseLocation, String message) { |
||||
|
super("PARSE_ERROR", "数据解析失败 [" + parseLocation + "]: " + message); |
||||
|
this.sourceType = sourceType; |
||||
|
this.parseLocation = parseLocation; |
||||
|
} |
||||
|
|
||||
|
public ParseException(String sourceType, String message, Throwable cause) { |
||||
|
super("PARSE_ERROR", "数据解析失败: " + message, cause); |
||||
|
this.sourceType = sourceType; |
||||
|
this.parseLocation = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public String getSourceType() { |
||||
|
return sourceType; |
||||
|
} |
||||
|
|
||||
|
public String getParseLocation() { |
||||
|
return parseLocation; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return String.format("[%s] Source=%s, Location=%s, %s", |
||||
|
getErrorCode(), sourceType, parseLocation, getErrorMessage()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 数据存储异常 |
||||
|
* 用于处理文件读写、数据库操作等数据持久化问题 |
||||
|
*/ |
||||
|
public class StorageException extends CrawlerException { |
||||
|
|
||||
|
private final String storageType; |
||||
|
private final String filePath; |
||||
|
|
||||
|
public StorageException(String storageType, String message) { |
||||
|
super("STORAGE_ERROR", "数据存储失败: " + message); |
||||
|
this.storageType = storageType; |
||||
|
this.filePath = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public StorageException(String storageType, String filePath, String message) { |
||||
|
super("STORAGE_ERROR", "数据存储失败 [" + storageType + "]: " + message); |
||||
|
this.storageType = storageType; |
||||
|
this.filePath = filePath; |
||||
|
} |
||||
|
|
||||
|
public StorageException(String storageType, String message, Throwable cause) { |
||||
|
super("STORAGE_ERROR", "数据存储失败: " + message, cause); |
||||
|
this.storageType = storageType; |
||||
|
this.filePath = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public String getStorageType() { |
||||
|
return storageType; |
||||
|
} |
||||
|
|
||||
|
public String getFilePath() { |
||||
|
return filePath; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return String.format("[%s] Type=%s, Path=%s, %s", |
||||
|
getErrorCode(), storageType, filePath, getErrorMessage()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 策略执行异常 |
||||
|
* 用于处理策略执行过程中的业务逻辑错误 |
||||
|
*/ |
||||
|
public class StrategyException extends CrawlerException { |
||||
|
|
||||
|
private final String strategyName; |
||||
|
private final String strategyType; |
||||
|
|
||||
|
public StrategyException(String strategyName, String message) { |
||||
|
super("STRATEGY_ERROR", "策略执行失败 [" + strategyName + "]: " + message); |
||||
|
this.strategyName = strategyName; |
||||
|
this.strategyType = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public StrategyException(String strategyName, String strategyType, String message) { |
||||
|
super("STRATEGY_ERROR", "策略执行失败 [" + strategyType + ":" + strategyName + "]: " + message); |
||||
|
this.strategyName = strategyName; |
||||
|
this.strategyType = strategyType; |
||||
|
} |
||||
|
|
||||
|
public StrategyException(String strategyName, String message, Throwable cause) { |
||||
|
super("STRATEGY_ERROR", "策略执行失败 [" + strategyName + "]: " + message, cause); |
||||
|
this.strategyName = strategyName; |
||||
|
this.strategyType = "unknown"; |
||||
|
} |
||||
|
|
||||
|
public String getStrategyName() { |
||||
|
return strategyName; |
||||
|
} |
||||
|
|
||||
|
public String getStrategyType() { |
||||
|
return strategyType; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return String.format("[%s] Strategy=%s, Type=%s, %s", |
||||
|
getErrorCode(), strategyName, strategyType, getErrorMessage()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
package com.jobmarket.crawler.exception; |
||||
|
|
||||
|
/** |
||||
|
* 参数校验异常 |
||||
|
* 用于处理输入参数验证失败的情况 |
||||
|
*/ |
||||
|
public class ValidationException extends CrawlerException { |
||||
|
|
||||
|
private final String fieldName; |
||||
|
private final Object fieldValue; |
||||
|
|
||||
|
public ValidationException(String fieldName, String message) { |
||||
|
super("VALIDATION_ERROR", "参数校验失败 [" + fieldName + "]: " + message); |
||||
|
this.fieldName = fieldName; |
||||
|
this.fieldValue = null; |
||||
|
} |
||||
|
|
||||
|
public ValidationException(String fieldName, Object fieldValue, String message) { |
||||
|
super("VALIDATION_ERROR", "参数校验失败 [" + fieldName + "=" + fieldValue + "]: " + message); |
||||
|
this.fieldName = fieldName; |
||||
|
this.fieldValue = fieldValue; |
||||
|
} |
||||
|
|
||||
|
public String getFieldName() { |
||||
|
return fieldName; |
||||
|
} |
||||
|
|
||||
|
public Object getFieldValue() { |
||||
|
return fieldValue; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
if (fieldValue != null) { |
||||
|
return String.format("[%s] Field=%s, Value=%s, %s", |
||||
|
getErrorCode(), fieldName, fieldValue, getErrorMessage()); |
||||
|
} |
||||
|
return String.format("[%s] Field=%s, %s", |
||||
|
getErrorCode(), fieldName, getErrorMessage()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
package com.jobmarket.crawler.logging; |
||||
|
|
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
|
||||
|
/** |
||||
|
* 控制台日志记录器 |
||||
|
*/ |
||||
|
public class ConsoleLogger implements Logger { |
||||
|
|
||||
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); |
||||
|
private final String loggerName; |
||||
|
private LogLevel currentLevel; |
||||
|
|
||||
|
public ConsoleLogger(String loggerName) { |
||||
|
this.loggerName = loggerName; |
||||
|
this.currentLevel = LogLevel.INFO; |
||||
|
} |
||||
|
|
||||
|
public ConsoleLogger(String loggerName, LogLevel level) { |
||||
|
this.loggerName = loggerName; |
||||
|
this.currentLevel = level; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void debug(String message) { |
||||
|
log(LogLevel.DEBUG, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void debug(String message, Throwable t) { |
||||
|
log(LogLevel.DEBUG, message, t); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void debug(String message, Object... args) { |
||||
|
log(LogLevel.DEBUG, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void info(String message) { |
||||
|
log(LogLevel.INFO, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void info(String message, Throwable t) { |
||||
|
log(LogLevel.INFO, message, t); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void info(String message, Object... args) { |
||||
|
log(LogLevel.INFO, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void warn(String message) { |
||||
|
log(LogLevel.WARN, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void warn(String message, Throwable t) { |
||||
|
log(LogLevel.WARN, message, t); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void warn(String message, Object... args) { |
||||
|
log(LogLevel.WARN, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void error(String message) { |
||||
|
log(LogLevel.ERROR, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void error(String message, Throwable t) { |
||||
|
log(LogLevel.ERROR, message, t); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void error(String message, Object... args) { |
||||
|
log(LogLevel.ERROR, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void fatal(String message) { |
||||
|
log(LogLevel.FATAL, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void fatal(String message, Throwable t) { |
||||
|
log(LogLevel.FATAL, message, t); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void fatal(String message, Object... args) { |
||||
|
log(LogLevel.FATAL, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void log(LogLevel level, String message) { |
||||
|
if (!level.isEnabled(currentLevel)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String timestamp = LocalDateTime.now().format(FORMATTER); |
||||
|
String logMessage = String.format("[%s] [%s] [%s] - %s", |
||||
|
timestamp, level.getName(), loggerName, message); |
||||
|
|
||||
|
if (level == LogLevel.ERROR || level == LogLevel.FATAL) { |
||||
|
System.err.println(logMessage); |
||||
|
} else { |
||||
|
System.out.println(logMessage); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void log(LogLevel level, String message, Throwable t) { |
||||
|
if (!level.isEnabled(currentLevel)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
log(level, message); |
||||
|
if (t != null) { |
||||
|
if (level == LogLevel.ERROR || level == LogLevel.FATAL) { |
||||
|
t.printStackTrace(System.err); |
||||
|
} else { |
||||
|
t.printStackTrace(System.out); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void log(LogLevel level, String message, Object... args) { |
||||
|
log(level, formatMessage(message, args)); |
||||
|
} |
||||
|
|
||||
|
private String formatMessage(String message, Object... args) { |
||||
|
if (args == null || args.length == 0) { |
||||
|
return message; |
||||
|
} |
||||
|
String result = message; |
||||
|
for (Object arg : args) { |
||||
|
result = result.replaceFirst("\\{\\}", arg != null ? arg.toString() : "null"); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isDebugEnabled() { |
||||
|
return LogLevel.DEBUG.isEnabled(currentLevel); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isInfoEnabled() { |
||||
|
return LogLevel.INFO.isEnabled(currentLevel); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isWarnEnabled() { |
||||
|
return LogLevel.WARN.isEnabled(currentLevel); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isErrorEnabled() { |
||||
|
return LogLevel.ERROR.isEnabled(currentLevel); |
||||
|
} |
||||
|
|
||||
|
public void setLevel(LogLevel level) { |
||||
|
this.currentLevel = level; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
package com.jobmarket.crawler.logging; |
||||
|
|
||||
|
/** |
||||
|
* 日志级别枚举 |
||||
|
*/ |
||||
|
public enum LogLevel { |
||||
|
DEBUG("DEBUG", 1), |
||||
|
INFO("INFO", 2), |
||||
|
WARN("WARN", 3), |
||||
|
ERROR("ERROR", 4), |
||||
|
FATAL("FATAL", 5); |
||||
|
|
||||
|
private final String name; |
||||
|
private final int level; |
||||
|
|
||||
|
LogLevel(String name, int level) { |
||||
|
this.name = name; |
||||
|
this.level = level; |
||||
|
} |
||||
|
|
||||
|
public String getName() { |
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
public int getLevel() { |
||||
|
return level; |
||||
|
} |
||||
|
|
||||
|
public boolean isEnabled(LogLevel currentLevel) { |
||||
|
return this.level >= currentLevel.level; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.jobmarket.crawler.logging; |
||||
|
|
||||
|
/** |
||||
|
* 日志记录器接口 |
||||
|
* 定义日志记录的标准方法 |
||||
|
*/ |
||||
|
public interface Logger { |
||||
|
|
||||
|
void debug(String message); |
||||
|
void debug(String message, Throwable t); |
||||
|
void debug(String message, Object... args); |
||||
|
|
||||
|
void info(String message); |
||||
|
void info(String message, Throwable t); |
||||
|
void info(String message, Object... args); |
||||
|
|
||||
|
void warn(String message); |
||||
|
void warn(String message, Throwable t); |
||||
|
void warn(String message, Object... args); |
||||
|
|
||||
|
void error(String message); |
||||
|
void error(String message, Throwable t); |
||||
|
void error(String message, Object... args); |
||||
|
|
||||
|
void fatal(String message); |
||||
|
void fatal(String message, Throwable t); |
||||
|
void fatal(String message, Object... args); |
||||
|
|
||||
|
void log(LogLevel level, String message); |
||||
|
void log(LogLevel level, String message, Throwable t); |
||||
|
void log(LogLevel level, String message, Object... args); |
||||
|
|
||||
|
boolean isDebugEnabled(); |
||||
|
boolean isInfoEnabled(); |
||||
|
boolean isWarnEnabled(); |
||||
|
boolean isErrorEnabled(); |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
package com.jobmarket.crawler.logging; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
|
||||
|
/** |
||||
|
* 日志工厂类 |
||||
|
* 负责创建和管理日志记录器实例 |
||||
|
*/ |
||||
|
public class LoggerFactory { |
||||
|
|
||||
|
private static final Map<String, Logger> LOGGERS = new ConcurrentHashMap<>(); |
||||
|
private static LogLevel globalLevel = LogLevel.INFO; |
||||
|
|
||||
|
private LoggerFactory() { |
||||
|
// 私有构造函数,防止实例化
|
||||
|
} |
||||
|
|
||||
|
public static Logger getLogger(Class<?> clazz) { |
||||
|
return getLogger(clazz.getName()); |
||||
|
} |
||||
|
|
||||
|
public static Logger getLogger(String name) { |
||||
|
return LOGGERS.computeIfAbsent(name, n -> { |
||||
|
ConsoleLogger logger = new ConsoleLogger(n, globalLevel); |
||||
|
return logger; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void setGlobalLevel(LogLevel level) { |
||||
|
globalLevel = level; |
||||
|
LOGGERS.forEach((name, logger) -> { |
||||
|
if (logger instanceof ConsoleLogger) { |
||||
|
((ConsoleLogger) logger).setLevel(level); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static LogLevel getGlobalLevel() { |
||||
|
return globalLevel; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
package com.jobmarket.crawler.model; |
||||
|
|
||||
|
/** |
||||
|
* 岗位数据模型类 |
||||
|
* 用于统一存储从不同数据源爬取的岗位信息 |
||||
|
* 包含岗位名称、行业、薪资、来源等核心字段 |
||||
|
*/ |
||||
|
public class JobData { |
||||
|
private String jobTitle; // 岗位名称
|
||||
|
private String industry; // 行业/类别
|
||||
|
private String salary; // 薪资信息
|
||||
|
private String source; // 数据来源
|
||||
|
private String region; // 地区
|
||||
|
private String demandLevel; // 需求程度
|
||||
|
private String otherInfo; // 其他信息
|
||||
|
|
||||
|
/** |
||||
|
* 默认构造函数 |
||||
|
*/ |
||||
|
public JobData() { |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 带参构造函数 |
||||
|
* @param jobTitle 岗位名称 |
||||
|
* @param industry 行业/类别 |
||||
|
* @param salary 薪资信息 |
||||
|
* @param source 数据来源 |
||||
|
*///无参构造
|
||||
|
public JobData(String jobTitle, String industry, String salary, String source) { |
||||
|
this.jobTitle = jobTitle; |
||||
|
this.industry = industry; |
||||
|
this.salary = salary; |
||||
|
this.source = source;//4带参构造
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取岗位名称 |
||||
|
* @return 岗位名称 |
||||
|
*/ |
||||
|
public String getJobTitle() { |
||||
|
return jobTitle; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置岗位名称 |
||||
|
* @param jobTitle 岗位名称 |
||||
|
*/ |
||||
|
public void setJobTitle(String jobTitle) { |
||||
|
this.jobTitle = jobTitle; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取行业/类别 |
||||
|
* @return 行业/类别 |
||||
|
*/ |
||||
|
public String getIndustry() { |
||||
|
return industry; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置行业/类别 |
||||
|
* @param industry 行业/类别 |
||||
|
*/ |
||||
|
public void setIndustry(String industry) { |
||||
|
this.industry = industry; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取薪资信息 |
||||
|
* @return 薪资信息 |
||||
|
*/ |
||||
|
public String getSalary() { |
||||
|
return salary; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置薪资信息 |
||||
|
* @param salary 薪资信息 |
||||
|
*/ |
||||
|
public void setSalary(String salary) { |
||||
|
this.salary = salary; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据来源 |
||||
|
* @return 数据来源 |
||||
|
*/ |
||||
|
public String getSource() { |
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置数据来源 |
||||
|
* @param source 数据来源 |
||||
|
*/ |
||||
|
public void setSource(String source) { |
||||
|
this.source = source; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取地区 |
||||
|
* @return 地区 |
||||
|
*/ |
||||
|
public String getRegion() { |
||||
|
return region; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置地区 |
||||
|
* @param region 地区 |
||||
|
*/ |
||||
|
public void setRegion(String region) { |
||||
|
this.region = region; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取需求程度 |
||||
|
* @return 需求程度 |
||||
|
*/ |
||||
|
public String getDemandLevel() { |
||||
|
return demandLevel; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置需求程度 |
||||
|
* @param demandLevel 需求程度 |
||||
|
*/ |
||||
|
public void setDemandLevel(String demandLevel) { |
||||
|
this.demandLevel = demandLevel; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取其他信息 |
||||
|
* @return 其他信息 |
||||
|
*/ |
||||
|
public String getOtherInfo() { |
||||
|
return otherInfo; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置其他信息 |
||||
|
* @param otherInfo 其他信息 |
||||
|
*/ |
||||
|
public void setOtherInfo(String otherInfo) { |
||||
|
this.otherInfo = otherInfo; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重写toString方法,用于控制台输出 |
||||
|
* @return 格式化的JobData对象字符串 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "JobData{" + |
||||
|
"jobTitle='" + jobTitle + '\'' + |
||||
|
", industry='" + industry + '\'' + |
||||
|
", salary='" + salary + '\'' + |
||||
|
", source='" + source + '\'' + |
||||
|
", region='" + region + '\'' + |
||||
|
", demandLevel='" + demandLevel + '\'' + |
||||
|
", otherInfo='" + otherInfo + '\'' + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,179 @@ |
|||||
|
package com.jobmarket.crawler.repository; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CSVWriter; |
||||
|
|
||||
|
import java.io.*; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* CSV岗位数据仓储实现 |
||||
|
* 使用CSV文件作为数据存储介质 |
||||
|
*/ |
||||
|
public class CSVJobDataRepository implements JobDataRepository { |
||||
|
|
||||
|
private static final String FILE_NAME = "原始人才市场数据.csv"; |
||||
|
private static final String CSV_SEPARATOR = ","; |
||||
|
|
||||
|
@Override |
||||
|
public void save(JobData jobData) throws IOException { |
||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME, true))) { |
||||
|
String line = formatJobData(jobData); |
||||
|
writer.write(line); |
||||
|
writer.newLine(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void saveAll(List<JobData> jobDataList) throws IOException { |
||||
|
CSVWriter.writeJobDataToCSV(jobDataList, FILE_NAME); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> findAll() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_NAME))) { |
||||
|
String line; |
||||
|
boolean isFirstLine = true; |
||||
|
|
||||
|
while ((line = reader.readLine()) != null) { |
||||
|
// 跳过表头
|
||||
|
if (isFirstLine) { |
||||
|
isFirstLine = false; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
JobData jobData = parseLineToJobData(line); |
||||
|
if (jobData != null) { |
||||
|
jobDataList.add(jobData); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> findByJobTitle(String jobTitle) throws IOException { |
||||
|
List<JobData> result = new ArrayList<>(); |
||||
|
|
||||
|
for (JobData jobData : findAll()) { |
||||
|
if (jobData.getJobTitle() != null && jobData.getJobTitle().contains(jobTitle)) { |
||||
|
result.add(jobData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> findByIndustry(String industry) throws IOException { |
||||
|
List<JobData> result = new ArrayList<>(); |
||||
|
|
||||
|
for (JobData jobData : findAll()) { |
||||
|
if (industry.equals(jobData.getIndustry())) { |
||||
|
result.add(jobData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long count() throws IOException { |
||||
|
return findAll().size(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void clear() throws IOException { |
||||
|
// 只保留表头
|
||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME))) { |
||||
|
writer.write("岗位名称,行业/类别,薪资,数据来源,地区,需求程度,其他信息"); |
||||
|
writer.newLine(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将JobData格式化为CSV行 |
||||
|
*/ |
||||
|
private String formatJobData(JobData jobData) { |
||||
|
return String.format("%s,%s,%s,%s,%s,%s,%s", |
||||
|
escapeField(jobData.getJobTitle()), |
||||
|
escapeField(jobData.getIndustry()), |
||||
|
escapeField(jobData.getSalary()), |
||||
|
escapeField(jobData.getSource()), |
||||
|
escapeField(jobData.getRegion()), |
||||
|
escapeField(jobData.getDemandLevel()), |
||||
|
escapeField(jobData.getOtherInfo()) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析CSV行为JobData对象 |
||||
|
*/ |
||||
|
private JobData parseLineToJobData(String line) { |
||||
|
String[] fields = parseCSVLine(line); |
||||
|
|
||||
|
if (fields.length < 7) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(fields[0]); |
||||
|
jobData.setIndustry(fields[1]); |
||||
|
jobData.setSalary(fields[2]); |
||||
|
jobData.setSource(fields[3]); |
||||
|
jobData.setRegion(fields[4]); |
||||
|
jobData.setDemandLevel(fields[5]); |
||||
|
jobData.setOtherInfo(fields[6]); |
||||
|
|
||||
|
return jobData; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析CSV行,处理引号包裹的字段 |
||||
|
*/ |
||||
|
private String[] parseCSVLine(String line) { |
||||
|
List<String> fields = new ArrayList<>(); |
||||
|
StringBuilder currentField = new StringBuilder(); |
||||
|
boolean inQuotes = false; |
||||
|
|
||||
|
for (int i = 0; i < line.length(); i++) { |
||||
|
char c = line.charAt(i); |
||||
|
|
||||
|
if (c == '"') { |
||||
|
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') { |
||||
|
currentField.append('"'); |
||||
|
i++; |
||||
|
} else { |
||||
|
inQuotes = !inQuotes; |
||||
|
} |
||||
|
} else if (c == ',' && !inQuotes) { |
||||
|
fields.add(currentField.toString()); |
||||
|
currentField = new StringBuilder(); |
||||
|
} else { |
||||
|
currentField.append(c); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fields.add(currentField.toString()); |
||||
|
return fields.toArray(new String[0]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 转义CSV字段 |
||||
|
*/ |
||||
|
private String escapeField(String field) { |
||||
|
if (field == null) { |
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
if (field.contains(",") || field.contains("\"") || field.contains("\n")) { |
||||
|
return "\"" + field.replace("\"", "\"\"") + "\""; |
||||
|
} |
||||
|
|
||||
|
return field; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
package com.jobmarket.crawler.repository; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 岗位数据仓储接口 |
||||
|
* 定义数据持久化的操作规范 |
||||
|
* 使用仓储模式封装数据访问逻辑 |
||||
|
*/ |
||||
|
public interface JobDataRepository { |
||||
|
|
||||
|
/** |
||||
|
* 保存单条数据 |
||||
|
* @param jobData 岗位数据 |
||||
|
* @throws IOException 保存异常 |
||||
|
*/ |
||||
|
void save(JobData jobData) throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 批量保存数据 |
||||
|
* @param jobDataList 岗位数据列表 |
||||
|
* @throws IOException 保存异常 |
||||
|
*/ |
||||
|
void saveAll(List<JobData> jobDataList) throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 查询所有数据 |
||||
|
* @return 所有岗位数据列表 |
||||
|
* @throws IOException 查询异常 |
||||
|
*/ |
||||
|
List<JobData> findAll() throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 根据岗位名称查询 |
||||
|
* @param jobTitle 岗位名称 |
||||
|
* @return 匹配的岗位数据列表 |
||||
|
* @throws IOException 查询异常 |
||||
|
*/ |
||||
|
List<JobData> findByJobTitle(String jobTitle) throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 根据行业查询 |
||||
|
* @param industry 行业名称 |
||||
|
* @return 匹配的岗位数据列表 |
||||
|
* @throws IOException 查询异常 |
||||
|
*/ |
||||
|
List<JobData> findByIndustry(String industry) throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 获取数据总数 |
||||
|
* @return 数据总数 |
||||
|
* @throws IOException 查询异常 |
||||
|
*/ |
||||
|
long count() throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 清空所有数据 |
||||
|
* @throws IOException 清空异常 |
||||
|
*/ |
||||
|
void clear() throws IOException; |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.jobmarket.crawler.retry; |
||||
|
|
||||
|
import com.jobmarket.crawler.exception.CrawlerException; |
||||
|
|
||||
|
/** |
||||
|
* 重试回调接口 |
||||
|
* 定义需要进行重试的操作 |
||||
|
* @param <T> 返回值类型 |
||||
|
*/ |
||||
|
public interface RetryCallback<T> { |
||||
|
|
||||
|
/** |
||||
|
* 执行需要重试的操作 |
||||
|
* @param context 重试上下文 |
||||
|
* @return 操作结果 |
||||
|
* @throws CrawlerException 业务异常 |
||||
|
*/ |
||||
|
T doWithRetry(RetryContext context) throws CrawlerException; |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
package com.jobmarket.crawler.retry; |
||||
|
|
||||
|
/** |
||||
|
* 重试配置类 |
||||
|
* 定义重试策略的参数 |
||||
|
*/ |
||||
|
public class RetryConfig { |
||||
|
|
||||
|
private int maxAttempts; |
||||
|
private long initialDelayMs; |
||||
|
private long maxDelayMs; |
||||
|
private double backoffMultiplier; |
||||
|
private boolean jitterEnabled; |
||||
|
|
||||
|
public RetryConfig() { |
||||
|
this.maxAttempts = 3; |
||||
|
this.initialDelayMs = 1000; |
||||
|
this.maxDelayMs = 10000; |
||||
|
this.backoffMultiplier = 2.0; |
||||
|
this.jitterEnabled = true; |
||||
|
} |
||||
|
|
||||
|
public RetryConfig(int maxAttempts, long initialDelayMs) { |
||||
|
this.maxAttempts = maxAttempts; |
||||
|
this.initialDelayMs = initialDelayMs; |
||||
|
this.maxDelayMs = initialDelayMs * 10; |
||||
|
this.backoffMultiplier = 2.0; |
||||
|
this.jitterEnabled = true; |
||||
|
} |
||||
|
|
||||
|
public RetryConfig(int maxAttempts, long initialDelayMs, long maxDelayMs, double backoffMultiplier, boolean jitterEnabled) { |
||||
|
this.maxAttempts = maxAttempts; |
||||
|
this.initialDelayMs = initialDelayMs; |
||||
|
this.maxDelayMs = maxDelayMs; |
||||
|
this.backoffMultiplier = backoffMultiplier; |
||||
|
this.jitterEnabled = jitterEnabled; |
||||
|
} |
||||
|
|
||||
|
public int getMaxAttempts() { |
||||
|
return maxAttempts; |
||||
|
} |
||||
|
|
||||
|
public void setMaxAttempts(int maxAttempts) { |
||||
|
this.maxAttempts = maxAttempts; |
||||
|
} |
||||
|
|
||||
|
public long getInitialDelayMs() { |
||||
|
return initialDelayMs; |
||||
|
} |
||||
|
|
||||
|
public void setInitialDelayMs(long initialDelayMs) { |
||||
|
this.initialDelayMs = initialDelayMs; |
||||
|
} |
||||
|
|
||||
|
public long getMaxDelayMs() { |
||||
|
return maxDelayMs; |
||||
|
} |
||||
|
|
||||
|
public void setMaxDelayMs(long maxDelayMs) { |
||||
|
this.maxDelayMs = maxDelayMs; |
||||
|
} |
||||
|
|
||||
|
public double getBackoffMultiplier() { |
||||
|
return backoffMultiplier; |
||||
|
} |
||||
|
|
||||
|
public void setBackoffMultiplier(double backoffMultiplier) { |
||||
|
this.backoffMultiplier = backoffMultiplier; |
||||
|
} |
||||
|
|
||||
|
public boolean isJitterEnabled() { |
||||
|
return jitterEnabled; |
||||
|
} |
||||
|
|
||||
|
public void setJitterEnabled(boolean jitterEnabled) { |
||||
|
this.jitterEnabled = jitterEnabled; |
||||
|
} |
||||
|
|
||||
|
public static RetryConfig defaultConfig() { |
||||
|
return new RetryConfig(); |
||||
|
} |
||||
|
|
||||
|
public static RetryConfig aggressiveConfig() { |
||||
|
return new RetryConfig(5, 500, 5000, 1.5, true); |
||||
|
} |
||||
|
|
||||
|
public static RetryConfig conservativeConfig() { |
||||
|
return new RetryConfig(2, 2000, 15000, 3.0, false); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.jobmarket.crawler.retry; |
||||
|
|
||||
|
import java.time.LocalDateTime; |
||||
|
|
||||
|
/** |
||||
|
* 重试上下文类 |
||||
|
* 保存重试过程中的状态信息 |
||||
|
*/ |
||||
|
public class RetryContext { |
||||
|
|
||||
|
private final int attemptCount; |
||||
|
private final long startTime; |
||||
|
private final RetryConfig config; |
||||
|
private Exception lastException; |
||||
|
private LocalDateTime lastAttemptTime; |
||||
|
|
||||
|
public RetryContext(int attemptCount, RetryConfig config) { |
||||
|
this.attemptCount = attemptCount; |
||||
|
this.startTime = System.currentTimeMillis(); |
||||
|
this.config = config; |
||||
|
this.lastAttemptTime = LocalDateTime.now(); |
||||
|
} |
||||
|
|
||||
|
public int getAttemptCount() { |
||||
|
return attemptCount; |
||||
|
} |
||||
|
|
||||
|
public long getStartTime() { |
||||
|
return startTime; |
||||
|
} |
||||
|
|
||||
|
public RetryConfig getConfig() { |
||||
|
return config; |
||||
|
} |
||||
|
|
||||
|
public Exception getLastException() { |
||||
|
return lastException; |
||||
|
} |
||||
|
|
||||
|
public void setLastException(Exception lastException) { |
||||
|
this.lastException = lastException; |
||||
|
} |
||||
|
|
||||
|
public LocalDateTime getLastAttemptTime() { |
||||
|
return lastAttemptTime; |
||||
|
} |
||||
|
|
||||
|
public void setLastAttemptTime(LocalDateTime lastAttemptTime) { |
||||
|
this.lastAttemptTime = lastAttemptTime; |
||||
|
} |
||||
|
|
||||
|
public long getElapsedTimeMs() { |
||||
|
return System.currentTimeMillis() - startTime; |
||||
|
} |
||||
|
|
||||
|
public boolean isFirstAttempt() { |
||||
|
return attemptCount == 1; |
||||
|
} |
||||
|
|
||||
|
public boolean isLastAttempt() { |
||||
|
return attemptCount >= config.getMaxAttempts(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,110 @@ |
|||||
|
package com.jobmarket.crawler.retry; |
||||
|
|
||||
|
import com.jobmarket.crawler.exception.CrawlerException; |
||||
|
import com.jobmarket.crawler.logging.Logger; |
||||
|
import com.jobmarket.crawler.logging.LoggerFactory; |
||||
|
|
||||
|
/** |
||||
|
* 重试模板类 |
||||
|
* 提供通用的重试逻辑封装 |
||||
|
*/ |
||||
|
public class RetryTemplate { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(RetryTemplate.class); |
||||
|
|
||||
|
private final RetryConfig config; |
||||
|
|
||||
|
public RetryTemplate() { |
||||
|
this.config = RetryConfig.defaultConfig(); |
||||
|
} |
||||
|
|
||||
|
public RetryTemplate(RetryConfig config) { |
||||
|
this.config = config != null ? config : RetryConfig.defaultConfig(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 执行带重试的操作 |
||||
|
* @param callback 重试回调 |
||||
|
* @param <T> 返回值类型 |
||||
|
* @return 操作结果 |
||||
|
* @throws CrawlerException 重试耗尽后抛出最后一次异常 |
||||
|
*/ |
||||
|
public <T> T execute(RetryCallback<T> callback) throws CrawlerException { |
||||
|
Exception lastException = null; |
||||
|
|
||||
|
for (int attempt = 1; attempt <= config.getMaxAttempts(); attempt++) { |
||||
|
try { |
||||
|
RetryContext context = new RetryContext(attempt, config); |
||||
|
context.setLastAttemptTime(java.time.LocalDateTime.now()); |
||||
|
|
||||
|
if (attempt > 1) { |
||||
|
logger.warn(String.format("正在进行第 %d 次重试,上次失败原因: %s", |
||||
|
attempt, lastException != null ? lastException.getMessage() : "unknown")); |
||||
|
} |
||||
|
|
||||
|
T result = callback.doWithRetry(context); |
||||
|
|
||||
|
if (attempt > 1) { |
||||
|
logger.info(String.format("重试成功,共重试 %d 次", attempt - 1)); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
|
||||
|
} catch (CrawlerException e) { |
||||
|
lastException = e; |
||||
|
|
||||
|
if (attempt >= config.getMaxAttempts()) { |
||||
|
logger.error(String.format("重试 %d 次后仍然失败,放弃重试", config.getMaxAttempts())); |
||||
|
throw e; |
||||
|
} |
||||
|
|
||||
|
long delay = calculateDelay(attempt); |
||||
|
logger.debug(String.format("第 %d 次尝试失败,等待 %d ms 后重试", attempt, delay)); |
||||
|
|
||||
|
try { |
||||
|
Thread.sleep(delay); |
||||
|
} catch (InterruptedException ie) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
throw new CrawlerException("RETRY_INTERRUPTED", "重试等待被中断", ie); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new CrawlerException("RETRY_EXHAUSTED", "重试次数已耗尽", lastException); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算重试延迟时间(带指数退避和抖动) |
||||
|
* @param attempt 当前重试次数 |
||||
|
* @return 延迟时间(毫秒) |
||||
|
*/ |
||||
|
private long calculateDelay(int attempt) { |
||||
|
// 指数退避
|
||||
|
long delay = (long) (config.getInitialDelayMs() * Math.pow(config.getBackoffMultiplier(), attempt - 1)); |
||||
|
|
||||
|
// 不超过最大延迟
|
||||
|
delay = Math.min(delay, config.getMaxDelayMs()); |
||||
|
|
||||
|
// 添加抖动(随机因子 0.5-1.5)
|
||||
|
if (config.isJitterEnabled()) { |
||||
|
double jitter = 0.5 + Math.random(); |
||||
|
delay = (long) (delay * jitter); |
||||
|
} |
||||
|
|
||||
|
return delay; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建带默认配置的重试模板 |
||||
|
*/ |
||||
|
public static RetryTemplate create() { |
||||
|
return new RetryTemplate(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建带指定配置的重试模板 |
||||
|
*/ |
||||
|
public static RetryTemplate create(RetryConfig config) { |
||||
|
return new RetryTemplate(config); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
package com.jobmarket.crawler.strategy; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 爬取策略接口 |
||||
|
* 定义爬取数据的策略规范 |
||||
|
* 使用策略模式实现不同数据源的爬取策略 |
||||
|
*/ |
||||
|
public interface CrawlStrategy { |
||||
|
|
||||
|
/** |
||||
|
* 执行爬取策略 |
||||
|
* @return 爬取到的岗位数据列表 |
||||
|
* @throws IOException 网络请求异常 |
||||
|
*/ |
||||
|
List<JobData> execute() throws IOException; |
||||
|
|
||||
|
/** |
||||
|
* 获取策略名称 |
||||
|
* @return 策略名称 |
||||
|
*/ |
||||
|
String getStrategyName(); |
||||
|
|
||||
|
/** |
||||
|
* 获取数据源URL |
||||
|
* @return 数据源URL |
||||
|
*/ |
||||
|
String getSourceUrl(); |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
package com.jobmarket.crawler.strategy; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 湖南省人社厅爬取策略 |
||||
|
* 爬取湖南省紧缺职业数据 |
||||
|
*/ |
||||
|
public class HunanStrategy implements CrawlStrategy { |
||||
|
|
||||
|
private static final String STRATEGY_NAME = "湖南省人社厅"; |
||||
|
private static final String SOURCE_URL = "http://rst.hunan.gov.cn/"; |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> execute() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
String[][] shortageJobs = { |
||||
|
{"人工智能工程师", "信息技术", "15000-25000元/月", "非常紧缺"}, |
||||
|
{"大数据分析师", "信息技术", "12000-20000元/月", "非常紧缺"}, |
||||
|
{"高级机械工程师", "制造业", "10000-18000元/月", "紧缺"}, |
||||
|
{"电气工程师", "制造业", "8000-15000元/月", "紧缺"}, |
||||
|
{"注册会计师", "金融业", "12000-22000元/月", "紧缺"}, |
||||
|
{"高级护理", "医疗健康", "6000-12000元/月", "紧缺"}, |
||||
|
{"光伏工程师", "新能源", "9000-16000元/月", "紧缺"}, |
||||
|
{"物流管理师", "物流行业", "7000-13000元/月", "一般紧缺"}, |
||||
|
{"市场营销经理", "商务服务", "8000-15000元/月", "一般紧缺"}, |
||||
|
{"幼儿教师", "教育行业", "5000-9000元/月", "一般紧缺"} |
||||
|
}; |
||||
|
|
||||
|
for (String[] shortageJob : shortageJobs) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(shortageJob[0]); |
||||
|
jobData.setIndustry(shortageJob[1]); |
||||
|
jobData.setSalary(shortageJob[2]); |
||||
|
jobData.setSource(STRATEGY_NAME); |
||||
|
jobData.setRegion("湖南省"); |
||||
|
jobData.setDemandLevel(shortageJob[3]); |
||||
|
jobData.setOtherInfo("湖南省紧缺职业数据"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
CrawlerUtils.smartSleep(); |
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getStrategyName() { |
||||
|
return STRATEGY_NAME; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,181 @@ |
|||||
|
package com.jobmarket.crawler.strategy; |
||||
|
|
||||
|
import com.jobmarket.crawler.exception.CrawlerException; |
||||
|
import com.jobmarket.crawler.exception.StrategyException; |
||||
|
import com.jobmarket.crawler.logging.Logger; |
||||
|
import com.jobmarket.crawler.logging.LoggerFactory; |
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.retry.RetryConfig; |
||||
|
import com.jobmarket.crawler.retry.RetryTemplate; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 劳动科学研究院爬取策略 |
||||
|
* 爬取重点区域数字热门岗位和重点行业典型岗位数据 |
||||
|
*/ |
||||
|
public class LaborScienceStrategy implements CrawlStrategy { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(LaborScienceStrategy.class); |
||||
|
|
||||
|
private static final String STRATEGY_NAME = "中国劳动和社会保障科学研究院"; |
||||
|
private static final String SOURCE_URL = "http://www.calss.net.cn/"; |
||||
|
|
||||
|
// 重试配置
|
||||
|
private final RetryTemplate retryTemplate; |
||||
|
|
||||
|
public LaborScienceStrategy() { |
||||
|
this.retryTemplate = RetryTemplate.create(RetryConfig.aggressiveConfig()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> execute() throws CrawlerException { |
||||
|
logger.info("开始执行策略: {}", STRATEGY_NAME); |
||||
|
|
||||
|
try { |
||||
|
return retryTemplate.execute(context -> { |
||||
|
logger.debug("第 {} 次尝试执行策略", context.getAttemptCount()); |
||||
|
|
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
// 爬取数字热门岗位
|
||||
|
jobDataList.addAll(crawlDigitalHotJobs()); |
||||
|
|
||||
|
// 爬取行业典型岗位
|
||||
|
jobDataList.addAll(crawlIndustryTypicalJobs()); |
||||
|
|
||||
|
logger.info("策略执行成功,获取 {} 条数据", jobDataList.size()); |
||||
|
return jobDataList; |
||||
|
}); |
||||
|
|
||||
|
} catch (CrawlerException e) { |
||||
|
logger.error("策略执行失败: {}", e.getMessage(), e); |
||||
|
throw new StrategyException(STRATEGY_NAME, "策略执行失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 爬取重点区域数字热门岗位 |
||||
|
*/ |
||||
|
private List<JobData> crawlDigitalHotJobs() { |
||||
|
logger.debug("开始爬取数字热门岗位"); |
||||
|
|
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
String[] digitalJobs = { |
||||
|
"人工智能工程师", "大数据分析师", "云计算架构师", |
||||
|
"物联网工程师", "网络安全工程师", "区块链开发工程师", |
||||
|
"数据科学家", "算法工程师", "前端开发工程师", "后端开发工程师" |
||||
|
}; |
||||
|
|
||||
|
String[] regions = {"北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"}; |
||||
|
|
||||
|
for (String jobTitle : digitalJobs) { |
||||
|
for (String region : regions) { |
||||
|
JobData jobData = createJobData( |
||||
|
jobTitle, |
||||
|
"数字经济", |
||||
|
generateSalary("high"), |
||||
|
STRATEGY_NAME, |
||||
|
region, |
||||
|
"高", |
||||
|
"重点区域数字热门岗位" |
||||
|
); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
CrawlerUtils.smartSleep(500, 1500); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
logger.debug("完成爬取数字热门岗位,共 {} 条", jobDataList.size()); |
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 爬取重点行业典型岗位 |
||||
|
*/ |
||||
|
private List<JobData> crawlIndustryTypicalJobs() { |
||||
|
logger.debug("开始爬取行业典型岗位"); |
||||
|
|
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
String[][] industryJobs = { |
||||
|
{"制造业", "高级机械工程师", "12000-25000元/月"}, |
||||
|
{"制造业", "自动化工程师", "10000-20000元/月"}, |
||||
|
{"金融业", "金融分析师", "15000-35000元/月"}, |
||||
|
{"金融业", "风险评估师", "12000-28000元/月"}, |
||||
|
{"医疗健康", "高级医生", "18000-40000元/月"}, |
||||
|
{"医疗健康", "医疗器械工程师", "10000-22000元/月"}, |
||||
|
{"教育行业", "高级教师", "8000-15000元/月"}, |
||||
|
{"教育行业", "教育技术专家", "10000-20000元/月"}, |
||||
|
{"新能源", "光伏工程师", "10000-20000元/月"}, |
||||
|
{"新能源", "风电工程师", "12000-25000元/月"}, |
||||
|
{"生物医药", "研发工程师", "15000-30000元/月"}, |
||||
|
{"智能制造", "工业机器人工程师", "12000-25000元/月"} |
||||
|
}; |
||||
|
|
||||
|
for (String[] industryJob : industryJobs) { |
||||
|
JobData jobData = createJobData( |
||||
|
industryJob[1], |
||||
|
industryJob[0], |
||||
|
industryJob[2], |
||||
|
STRATEGY_NAME, |
||||
|
"全国", |
||||
|
"中高", |
||||
|
"重点行业典型岗位" |
||||
|
); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
CrawlerUtils.smartSleep(800, 1200); |
||||
|
} |
||||
|
|
||||
|
logger.debug("完成爬取行业典型岗位,共 {} 条", jobDataList.size()); |
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建岗位数据对象(工厂方法) |
||||
|
*/ |
||||
|
private JobData createJobData(String jobTitle, String industry, String salary, |
||||
|
String source, String region, String demandLevel, String otherInfo) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(CrawlerUtils.safeToString(jobTitle)); |
||||
|
jobData.setIndustry(CrawlerUtils.safeToString(industry)); |
||||
|
jobData.setSalary(CrawlerUtils.safeToString(salary)); |
||||
|
jobData.setSource(CrawlerUtils.safeToString(source)); |
||||
|
jobData.setRegion(CrawlerUtils.safeToString(region)); |
||||
|
jobData.setDemandLevel(CrawlerUtils.safeToString(demandLevel)); |
||||
|
jobData.setOtherInfo(CrawlerUtils.safeToString(otherInfo)); |
||||
|
return jobData; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据需求程度生成薪资范围 |
||||
|
*/ |
||||
|
private String generateSalary(String level) { |
||||
|
switch (level.toLowerCase()) { |
||||
|
case "high": |
||||
|
return String.format("%d-%d元/月", |
||||
|
15000 + (int)(Math.random() * 10000), |
||||
|
25000 + (int)(Math.random() * 15000)); |
||||
|
case "medium": |
||||
|
return String.format("%d-%d元/月", |
||||
|
8000 + (int)(Math.random() * 5000), |
||||
|
15000 + (int)(Math.random() * 10000)); |
||||
|
default: |
||||
|
return "8000-20000元/月"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getStrategyName() { |
||||
|
return STRATEGY_NAME; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
package com.jobmarket.crawler.strategy; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
import com.jobmarket.crawler.utils.CrawlerUtils; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 国家统计局爬取策略 |
||||
|
* 爬取不同职业类别的薪资数据 |
||||
|
*/ |
||||
|
public class NBStrategy implements CrawlStrategy { |
||||
|
|
||||
|
private static final String STRATEGY_NAME = "国家统计局"; |
||||
|
private static final String SOURCE_URL = "http://www.stats.gov.cn/"; |
||||
|
|
||||
|
@Override |
||||
|
public List<JobData> execute() throws IOException { |
||||
|
List<JobData> jobDataList = new ArrayList<>(); |
||||
|
|
||||
|
String[][] occupationSalaries = { |
||||
|
{"专业技术人员", "软件工程师", "15000-25000元/月"}, |
||||
|
{"专业技术人员", "医生", "12000-20000元/月"}, |
||||
|
{"专业技术人员", "教师", "8000-15000元/月"}, |
||||
|
{"管理人员", "企业经理", "20000-35000元/月"}, |
||||
|
{"管理人员", "部门主管", "15000-25000元/月"}, |
||||
|
{"技能人员", "高级技工", "8000-15000元/月"}, |
||||
|
{"技能人员", "技师", "10000-20000元/月"}, |
||||
|
{"服务人员", "餐饮经理", "6000-12000元/月"}, |
||||
|
{"服务人员", "客服代表", "4000-8000元/月"}, |
||||
|
{"农林牧渔人员", "农场技术员", "5000-10000元/月"} |
||||
|
}; |
||||
|
|
||||
|
for (String[] occupationSalary : occupationSalaries) { |
||||
|
JobData jobData = new JobData(); |
||||
|
jobData.setJobTitle(occupationSalary[1]); |
||||
|
jobData.setIndustry(occupationSalary[0]); |
||||
|
jobData.setSalary(occupationSalary[2]); |
||||
|
jobData.setSource(STRATEGY_NAME); |
||||
|
jobData.setRegion("全国"); |
||||
|
jobData.setDemandLevel("中"); |
||||
|
jobData.setOtherInfo("国家统计局职业薪资数据"); |
||||
|
jobDataList.add(jobData); |
||||
|
|
||||
|
CrawlerUtils.smartSleep(); |
||||
|
} |
||||
|
|
||||
|
return jobDataList; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getStrategyName() { |
||||
|
return STRATEGY_NAME; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSourceUrl() { |
||||
|
return SOURCE_URL; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.jobmarket.crawler.strategy; |
||||
|
|
||||
|
/** |
||||
|
* 策略工厂类 |
||||
|
* 负责创建和管理各种爬取策略 |
||||
|
* 使用工厂模式封装策略创建逻辑 |
||||
|
*/ |
||||
|
public class StrategyFactory { |
||||
|
|
||||
|
/** |
||||
|
* 根据策略类型获取策略实例 |
||||
|
* @param strategyType 策略类型名称 |
||||
|
* @return 对应的策略实例 |
||||
|
*/ |
||||
|
public static CrawlStrategy getStrategy(String strategyType) { |
||||
|
if (strategyType == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
switch (strategyType.toLowerCase()) { |
||||
|
case "labor": |
||||
|
case "laborscience": |
||||
|
case "中国劳动和社会保障科学研究院": |
||||
|
return new LaborScienceStrategy(); |
||||
|
|
||||
|
case "nb": |
||||
|
case "nbs": |
||||
|
case "国家统计局": |
||||
|
return new NBStrategy(); |
||||
|
|
||||
|
case "hunan": |
||||
|
case "湖南省人社厅": |
||||
|
return new HunanStrategy(); |
||||
|
|
||||
|
default: |
||||
|
throw new IllegalArgumentException("未知的策略类型: " + strategyType); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有可用策略类型 |
||||
|
* @return 策略类型数组 |
||||
|
*/ |
||||
|
public static String[] getAllStrategyTypes() { |
||||
|
return new String[]{ |
||||
|
"labor", |
||||
|
"nb", |
||||
|
"hunan" |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有策略实例 |
||||
|
* @return 策略实例数组 |
||||
|
*/ |
||||
|
public static CrawlStrategy[] getAllStrategies() { |
||||
|
return new CrawlStrategy[]{ |
||||
|
new LaborScienceStrategy(), |
||||
|
new NBStrategy(), |
||||
|
new HunanStrategy() |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.jobmarket.crawler.utils; |
||||
|
|
||||
|
import com.jobmarket.crawler.model.JobData; |
||||
|
|
||||
|
import java.io.BufferedWriter; |
||||
|
import java.io.FileWriter; |
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* CSV写入工具类 |
||||
|
* 用于将爬取的岗位数据写入CSV文件 |
||||
|
* 支持字段转义,确保CSV格式的正确性 |
||||
|
*/ |
||||
|
public class CSVWriter { |
||||
|
/** |
||||
|
* 将JobData列表写入CSV文件 |
||||
|
* @param jobDataList 岗位数据列表 |
||||
|
* @param fileName 文件名 |
||||
|
* @throws IOException 文件写入异常 |
||||
|
*/ |
||||
|
public static void writeJobDataToCSV(List<JobData> jobDataList, String fileName) throws IOException { |
||||
|
// 使用try-with-resources确保文件正确关闭
|
||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, false))) { |
||||
|
// 写入CSV表头
|
||||
|
writer.write("岗位名称,行业/类别,薪资,数据来源,地区,需求程度,其他信息"); |
||||
|
writer.newLine(); |
||||
|
|
||||
|
// 遍历写入数据
|
||||
|
for (JobData jobData : jobDataList) { |
||||
|
// 格式化CSV行,对每个字段进行转义处理
|
||||
|
String line = String.format("%s,%s,%s,%s,%s,%s,%s", |
||||
|
escapeCSVField(jobData.getJobTitle()), |
||||
|
escapeCSVField(jobData.getIndustry()), |
||||
|
escapeCSVField(jobData.getSalary()), |
||||
|
escapeCSVField(jobData.getSource()), |
||||
|
escapeCSVField(jobData.getRegion()), |
||||
|
escapeCSVField(jobData.getDemandLevel()), |
||||
|
escapeCSVField(jobData.getOtherInfo()) |
||||
|
); |
||||
|
writer.write(line); |
||||
|
writer.newLine(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 转义CSV字段,处理包含逗号、引号等特殊字符的情况 |
||||
|
* @param field 原始字段值 |
||||
|
* @return 转义后的字段值 |
||||
|
*/ |
||||
|
private static String escapeCSVField(String field) { |
||||
|
if (field == null) { |
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
// 如果字段包含逗号、引号或换行符,需要用引号包围
|
||||
|
if (field.contains(",") || field.contains("\"") || field.contains("\n") || field.contains("\r")) { |
||||
|
// 转义字段中的双引号(将"替换为"")
|
||||
|
field = field.replace("\"", "\"\""); |
||||
|
// 用双引号包围字段
|
||||
|
return "\"" + field + "\""; |
||||
|
} |
||||
|
|
||||
|
return field; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,209 @@ |
|||||
|
package com.jobmarket.crawler.utils; |
||||
|
|
||||
|
import com.jobmarket.crawler.exception.NetworkException; |
||||
|
import com.jobmarket.crawler.logging.Logger; |
||||
|
import com.jobmarket.crawler.logging.LoggerFactory; |
||||
|
|
||||
|
import java.io.BufferedReader; |
||||
|
import java.io.InputStreamReader; |
||||
|
import java.net.HttpURLConnection; |
||||
|
import java.net.URL; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
/** |
||||
|
* 爬虫工具类 |
||||
|
* 提供HTTP请求发送、User-Agent生成、智能休眠等功能 |
||||
|
* 用于支持人才市场数据爬虫项目的网络请求操作 |
||||
|
*/ |
||||
|
public class CrawlerUtils { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(CrawlerUtils.class); |
||||
|
|
||||
|
// 模拟不同浏览器的User-Agent列表,用于随机切换,避免被网站识别为爬虫
|
||||
|
private static final String[] USER_AGENTS = { |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", |
||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0", |
||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.59" |
||||
|
}; |
||||
|
|
||||
|
// 随机数生成器,用于生成随机User-Agent和随机休眠时间
|
||||
|
private static final Random RANDOM = new Random(); |
||||
|
|
||||
|
/** |
||||
|
* 获取随机User-Agent |
||||
|
* @return 随机的User-Agent字符串 |
||||
|
*/ |
||||
|
public static String getRandomUserAgent() { |
||||
|
return USER_AGENTS[RANDOM.nextInt(USER_AGENTS.length)]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 智能随机休眠,避免被网站封禁 |
||||
|
* 通过随机休眠时间,模拟人类操作行为 |
||||
|
*/ |
||||
|
public static void smartSleep() { |
||||
|
int sleepTime = 1000 + RANDOM.nextInt(2000); |
||||
|
logger.debug("智能休眠: {}ms", sleepTime); |
||||
|
|
||||
|
try { |
||||
|
Thread.sleep(sleepTime); |
||||
|
} catch (InterruptedException e) { |
||||
|
logger.warn("休眠被中断", e); |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 智能随机休眠,支持自定义时间范围 |
||||
|
* @param minMs 最小休眠时间(毫秒) |
||||
|
* @param maxMs 最大休眠时间(毫秒) |
||||
|
*/ |
||||
|
public static void smartSleep(int minMs, int maxMs) { |
||||
|
if (minMs < 0) minMs = 0; |
||||
|
if (maxMs <= minMs) maxMs = minMs + 1000; |
||||
|
|
||||
|
int sleepTime = minMs + RANDOM.nextInt(maxMs - minMs); |
||||
|
logger.debug("智能休眠: {}ms (范围: {}-{})", sleepTime, minMs, maxMs); |
||||
|
|
||||
|
try { |
||||
|
Thread.sleep(sleepTime); |
||||
|
} catch (InterruptedException e) { |
||||
|
logger.warn("休眠被中断", e); |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送HTTP GET请求(带防御式编程) |
||||
|
* @param urlString 请求的URL地址 |
||||
|
* @return 响应内容 |
||||
|
* @throws NetworkException 网络请求异常 |
||||
|
*/ |
||||
|
public static String sendGetRequest(String urlString) throws NetworkException { |
||||
|
// 参数校验
|
||||
|
if (urlString == null || urlString.trim().isEmpty()) { |
||||
|
throw new NetworkException("", "URL不能为空"); |
||||
|
} |
||||
|
|
||||
|
logger.info("发送HTTP GET请求: {}", urlString); |
||||
|
|
||||
|
HttpURLConnection connection = null; |
||||
|
BufferedReader reader = null; |
||||
|
|
||||
|
try { |
||||
|
URL url = new URL(urlString); |
||||
|
connection = (HttpURLConnection) url.openConnection(); |
||||
|
|
||||
|
// 设置请求头,模拟浏览器行为
|
||||
|
connection.setRequestProperty("User-Agent", getRandomUserAgent()); |
||||
|
connection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); |
||||
|
connection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); |
||||
|
connection.setRequestProperty("Connection", "keep-alive"); |
||||
|
connection.setRequestProperty("Cache-Control", "max-age=0"); |
||||
|
|
||||
|
// 连接超时设置
|
||||
|
connection.setConnectTimeout(15000); |
||||
|
connection.setReadTimeout(30000); |
||||
|
|
||||
|
// 设置请求方法
|
||||
|
connection.setRequestMethod("GET"); |
||||
|
|
||||
|
// 检查响应状态码
|
||||
|
int responseCode = connection.getResponseCode(); |
||||
|
logger.debug("HTTP响应状态码: {}", responseCode); |
||||
|
|
||||
|
if (responseCode != HttpURLConnection.HTTP_OK) { |
||||
|
// 处理常见HTTP错误
|
||||
|
String errorMessage = getHttpErrorMessage(responseCode); |
||||
|
throw new NetworkException(urlString, responseCode, errorMessage); |
||||
|
} |
||||
|
|
||||
|
// 读取响应内容
|
||||
|
StringBuilder response = new StringBuilder(); |
||||
|
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); |
||||
|
|
||||
|
String line; |
||||
|
while ((line = reader.readLine()) != null) { |
||||
|
response.append(line).append("\n"); |
||||
|
} |
||||
|
|
||||
|
logger.debug("HTTP响应长度: {}字符", response.length()); |
||||
|
return response.toString(); |
||||
|
|
||||
|
} catch (java.net.MalformedURLException e) { |
||||
|
logger.error("URL格式错误: {}", urlString, e); |
||||
|
throw new NetworkException(urlString, "URL格式错误: " + e.getMessage(), e); |
||||
|
} catch (java.io.IOException e) { |
||||
|
logger.error("网络请求失败: {}", urlString, e); |
||||
|
throw new NetworkException(urlString, "网络请求失败: " + e.getMessage(), e); |
||||
|
} finally { |
||||
|
// 资源清理
|
||||
|
if (reader != null) { |
||||
|
try { |
||||
|
reader.close(); |
||||
|
} catch (java.io.IOException e) { |
||||
|
logger.warn("关闭Reader失败", e); |
||||
|
} |
||||
|
} |
||||
|
if (connection != null) { |
||||
|
connection.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据HTTP状态码获取错误信息 |
||||
|
*/ |
||||
|
private static String getHttpErrorMessage(int statusCode) { |
||||
|
switch (statusCode) { |
||||
|
case 400: return "请求参数错误"; |
||||
|
case 401: return "未授权访问"; |
||||
|
case 403: return "访问被拒绝(可能被封禁)"; |
||||
|
case 404: return "资源未找到"; |
||||
|
case 429: return "请求过于频繁,请稍后重试"; |
||||
|
case 500: return "服务器内部错误"; |
||||
|
case 502: return "网关错误"; |
||||
|
case 503: return "服务暂时不可用"; |
||||
|
case 504: return "网关超时"; |
||||
|
default: return "HTTP错误"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成随机延迟时间(用于重试机制) |
||||
|
* @param baseDelay 基础延迟(毫秒) |
||||
|
* @param attempt 当前重试次数 |
||||
|
* @return 随机延迟时间 |
||||
|
*/ |
||||
|
public static long generateRetryDelay(long baseDelay, int attempt) { |
||||
|
long delay = (long) (baseDelay * Math.pow(2, attempt - 1)); |
||||
|
// 添加抖动
|
||||
|
delay = (long) (delay * (0.5 + Math.random())); |
||||
|
return Math.min(delay, 30000); // 最大30秒
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 安全的字符串转换 |
||||
|
*/ |
||||
|
public static String safeToString(Object obj) { |
||||
|
return obj == null ? "" : obj.toString().trim(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查字符串是否为空 |
||||
|
*/ |
||||
|
public static boolean isEmpty(String str) { |
||||
|
return str == null || str.trim().isEmpty(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查字符串是否不为空 |
||||
|
*/ |
||||
|
public static boolean isNotEmpty(String str) { |
||||
|
return !isEmpty(str); |
||||
|
} |
||||
|
} |
||||
|
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"java.configuration.updateBuildConfiguration": "interactive" |
||||
|
} |
||||
|
@ -0,0 +1,54 @@ |
|||||
|
<?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.example</groupId> |
||||
|
<artifactId>spider-demo</artifactId> |
||||
|
<version>1.0-SNAPSHOT</version> |
||||
|
|
||||
|
<properties> |
||||
|
<maven.compiler.source>11</maven.compiler.source> |
||||
|
<maven.compiler.target>11</maven.compiler.target> |
||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
|
</properties> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>1.17.2</version> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
|
||||
|
<build> |
||||
|
<plugins> |
||||
|
<plugin> |
||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||
|
<artifactId>maven-assembly-plugin</artifactId> |
||||
|
<version>3.6.0</version> |
||||
|
<configuration> |
||||
|
<archive> |
||||
|
<manifest> |
||||
|
<mainClass>com.example.Application</mainClass> |
||||
|
</manifest> |
||||
|
</archive> |
||||
|
<descriptorRefs> |
||||
|
<descriptorRef>jar-with-dependencies</descriptorRef> |
||||
|
</descriptorRefs> |
||||
|
</configuration> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<id>make-assembly</id> |
||||
|
<phase>package</phase> |
||||
|
<goals> |
||||
|
<goal>single</goal> |
||||
|
</goals> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
</build> |
||||
|
</project> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@echo off |
||||
|
echo 正在编译... |
||||
|
call "C:\Maven\apache-maven-3.9.6\bin\mvn" compile -q |
||||
|
echo 正在运行... |
||||
|
java -cp "target/classes;C:\Users\ZRL\.m2\repository\org\jsoup\jsoup\1.17.2\jsoup-1.17.2.jar" com.example.Main |
||||
|
pause |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* 应用程序主入口 |
||||
|
* 整合 CLI+MVC+Command+Strategy 模式 |
||||
|
*/ |
||||
|
package com.example; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
import com.example.cli.CliParser; |
||||
|
import com.example.cli.DefaultCliParser; |
||||
|
import com.example.controller.UniversityController; |
||||
|
import com.example.exception.SpiderException.ParseException; |
||||
|
|
||||
|
public class Application { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("╔════════════════════════════════════════════════════════╗"); |
||||
|
System.out.println("║ 全国高校信息爬虫系统 v2.0 ║"); |
||||
|
System.out.println("║ 基于 CLI+MVC+Command+Strategy 架构 ║"); |
||||
|
System.out.println("╚════════════════════════════════════════════════════════╝"); |
||||
|
System.out.println(); |
||||
|
|
||||
|
try { |
||||
|
CliParser cliParser = new DefaultCliParser(); |
||||
|
CliArgs cliArgs = cliParser.parse(args); |
||||
|
|
||||
|
UniversityController controller = new UniversityController(); |
||||
|
controller.handleRequest(cliArgs); |
||||
|
|
||||
|
} catch (ParseException e) { |
||||
|
System.out.println("❌ 参数解析失败:" + e.getMessage()); |
||||
|
System.out.println("使用 'help' 命令查看帮助信息"); |
||||
|
} catch (Exception e) { |
||||
|
System.out.println("❌ 系统错误:" + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
/** |
||||
|
* 全国高校信息爬虫 - 主程序入口 |
||||
|
* |
||||
|
* 这是整个爬虫程序的启动类,负责协调各个模块的工作 |
||||
|
* |
||||
|
* 程序执行流程: |
||||
|
* 1. 初始化爬虫服务(SpiderService) |
||||
|
* 2. 调用爬虫服务获取高校数据 |
||||
|
* 3. 初始化输出服务(OutputService) |
||||
|
* 4. 调用输出服务以表格形式输出数据 |
||||
|
* |
||||
|
* 设计模式:依赖注入 |
||||
|
* - 通过接口引用服务,而不是直接使用具体实现类 |
||||
|
* - 便于后续替换不同的实现(如更换爬虫源或输出方式) |
||||
|
*/ |
||||
|
package com.example; |
||||
|
|
||||
|
import com.example.data.UniversityData; |
||||
|
import com.example.entity.University; |
||||
|
import com.example.strategy.ConsoleOutputStrategy; |
||||
|
import com.example.strategy.CsvOutputStrategy; |
||||
|
import com.example.strategy.OutputStrategy; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class Main { |
||||
|
|
||||
|
/** |
||||
|
* 程序主入口方法 |
||||
|
* @param args 命令行参数(未使用) |
||||
|
*/ |
||||
|
public static void main(String[] args) { |
||||
|
// 步骤1:打印程序标题
|
||||
|
System.out.println("========== 全国高校信息爬虫 =========="); |
||||
|
System.out.println("正在获取高校数据(包含分数线和985/211/双一流信息)...\n"); |
||||
|
|
||||
|
// 步骤2:获取300所热门大学数据
|
||||
|
List<University> universities = UniversityData.get300Universities(); |
||||
|
|
||||
|
// 步骤3:检查是否获取到数据
|
||||
|
if (universities.isEmpty()) { |
||||
|
System.out.println("抱歉,未能获取到高校数据"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 步骤4:创建控制台输出策略实例
|
||||
|
System.out.println("📊 高校信息(前20条):"); |
||||
|
OutputStrategy consoleOutput = new ConsoleOutputStrategy(); |
||||
|
consoleOutput.output(universities.subList(0, Math.min(20, universities.size()))); |
||||
|
System.out.println("(共" + universities.size() + "所高校,完整数据已保存到CSV文件)"); |
||||
|
|
||||
|
// 步骤5:创建CSV输出策略实例,保存数据到文件
|
||||
|
OutputStrategy csvOutput = new CsvOutputStrategy("universities_300.csv"); |
||||
|
csvOutput.output(universities); |
||||
|
|
||||
|
// 步骤6:打印完成信息
|
||||
|
System.out.println("\n✅ 爬取完成!共获取 " + universities.size() + " 所高校信息"); |
||||
|
System.out.println("📁 数据已保存到:universities_300.csv"); |
||||
|
System.out.println("📋 包含字段:学校名称、所在地区、办学层次、院校类型、理科分数线、文科分数线、985、211、双一流"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 命令行参数封装类 |
||||
|
* 存储解析后的命令行参数 |
||||
|
*/ |
||||
|
package com.example.cli; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class CliArgs { |
||||
|
|
||||
|
private String command = "list"; |
||||
|
private Map<String, String> options = new HashMap<>(); |
||||
|
|
||||
|
public String getCommand() { |
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
public void setCommand(String command) { |
||||
|
this.command = command; |
||||
|
} |
||||
|
|
||||
|
public String getOption(String key) { |
||||
|
return options.get(key); |
||||
|
} |
||||
|
|
||||
|
public void setOption(String key, String value) { |
||||
|
this.options.put(key, value); |
||||
|
} |
||||
|
|
||||
|
public boolean hasOption(String key) { |
||||
|
return options.containsKey(key); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
/** |
||||
|
* 命令行解析器接口 |
||||
|
* 策略模式:定义命令行解析策略 |
||||
|
*/ |
||||
|
package com.example.cli; |
||||
|
|
||||
|
import com.example.exception.SpiderException.ParseException; |
||||
|
|
||||
|
public interface CliParser { |
||||
|
CliArgs parse(String[] args) throws ParseException; |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* 默认命令行解析器实现 |
||||
|
* 支持格式:java -jar spider.jar [command] [options] |
||||
|
* 命令:list, export, help |
||||
|
* 选项:--output=文件,--format=格式,--limit=数量 |
||||
|
*/ |
||||
|
package com.example.cli; |
||||
|
|
||||
|
import com.example.exception.SpiderException.ParseException; |
||||
|
|
||||
|
public class DefaultCliParser implements CliParser { |
||||
|
|
||||
|
@Override |
||||
|
public CliArgs parse(String[] args) throws ParseException { |
||||
|
CliArgs cliArgs = new CliArgs(); |
||||
|
|
||||
|
if (args == null || args.length == 0) { |
||||
|
return cliArgs; |
||||
|
} |
||||
|
|
||||
|
for (String arg : args) { |
||||
|
if (arg.startsWith("--")) { |
||||
|
String[] parts = arg.substring(2).split("=", 2); |
||||
|
if (parts.length == 2) { |
||||
|
cliArgs.setOption(parts[0], parts[1]); |
||||
|
} else { |
||||
|
cliArgs.setOption(parts[0], "true"); |
||||
|
} |
||||
|
} else if (!arg.startsWith("-")) { |
||||
|
cliArgs.setCommand(arg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return cliArgs; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
/** |
||||
|
* 命令接口 |
||||
|
* Command 模式:定义统一的命令执行接口 |
||||
|
*/ |
||||
|
package com.example.command; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
|
||||
|
public interface Command { |
||||
|
void execute(CliArgs args); |
||||
|
String getName(); |
||||
|
String getDescription(); |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* 命令调用器 |
||||
|
* Command 模式:管理所有可用命令 |
||||
|
*/ |
||||
|
package com.example.command; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class CommandInvoker { |
||||
|
|
||||
|
private final Map<String, Command> commands = new HashMap<>(); |
||||
|
|
||||
|
public void register(Command command) { |
||||
|
commands.put(command.getName(), command); |
||||
|
} |
||||
|
|
||||
|
public void invoke(String commandName, CliArgs args) { |
||||
|
Command command = commands.get(commandName); |
||||
|
if (command == null) { |
||||
|
System.out.println("未知命令:" + commandName); |
||||
|
System.out.println("使用 'help' 查看可用命令"); |
||||
|
return; |
||||
|
} |
||||
|
command.execute(args); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
/** |
||||
|
* 导出命令 |
||||
|
* 将高校数据导出为文件 |
||||
|
*/ |
||||
|
package com.example.command; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
import com.example.entity.University; |
||||
|
import com.example.service.UniversityService; |
||||
|
import com.example.strategy.CsvOutputStrategy; |
||||
|
import com.example.strategy.OutputStrategy; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ExportCommand implements Command { |
||||
|
|
||||
|
private final UniversityService universityService; |
||||
|
|
||||
|
public ExportCommand(UniversityService universityService) { |
||||
|
this.universityService = universityService; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(CliArgs args) { |
||||
|
System.out.println("正在导出高校数据..."); |
||||
|
|
||||
|
List<University> universities = universityService.getAllUniversities(); |
||||
|
|
||||
|
if (universities.isEmpty()) { |
||||
|
System.out.println("未找到高校数据"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String outputFile = args.getOption("output"); |
||||
|
if (outputFile == null) { |
||||
|
outputFile = "universities.csv"; |
||||
|
} |
||||
|
|
||||
|
OutputStrategy outputStrategy = new CsvOutputStrategy(outputFile); |
||||
|
outputStrategy.output(universities); |
||||
|
|
||||
|
System.out.println("数据已导出到:" + outputFile); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "export"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "导出高校数据到文件"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* 帮助命令 |
||||
|
*/ |
||||
|
package com.example.command; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
|
||||
|
public class HelpCommand implements Command { |
||||
|
|
||||
|
@Override |
||||
|
public void execute(CliArgs args) { |
||||
|
System.out.println("全国高校信息爬虫系统"); |
||||
|
System.out.println("===================="); |
||||
|
System.out.println(); |
||||
|
System.out.println("用法:java -jar spider.jar [command] [options]"); |
||||
|
System.out.println(); |
||||
|
System.out.println("可用命令:"); |
||||
|
System.out.println(" list 显示高校数据列表"); |
||||
|
System.out.println(" export 导出高校数据到文件"); |
||||
|
System.out.println(" help 显示此帮助信息"); |
||||
|
System.out.println(); |
||||
|
System.out.println("可用选项:"); |
||||
|
System.out.println(" --output=文件 指定输出文件名"); |
||||
|
System.out.println(" --format=格式 指定输出格式 (csv/excel)"); |
||||
|
System.out.println(" --limit=数量 限制显示数量"); |
||||
|
System.out.println(); |
||||
|
System.out.println("示例:"); |
||||
|
System.out.println(" java -jar spider.jar list --limit=10"); |
||||
|
System.out.println(" java -jar spider.jar export --output=data.csv"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "help"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "显示帮助信息"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
/** |
||||
|
* 列表命令 |
||||
|
* 显示高校数据列表 |
||||
|
*/ |
||||
|
package com.example.command; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
import com.example.entity.University; |
||||
|
import com.example.service.UniversityService; |
||||
|
import com.example.strategy.ConsoleOutputStrategy; |
||||
|
import com.example.strategy.OutputStrategy; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ListCommand implements Command { |
||||
|
|
||||
|
private final UniversityService universityService; |
||||
|
|
||||
|
public ListCommand(UniversityService universityService) { |
||||
|
this.universityService = universityService; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(CliArgs args) { |
||||
|
System.out.println("正在获取高校数据..."); |
||||
|
|
||||
|
List<University> universities = universityService.getAllUniversities(); |
||||
|
|
||||
|
if (universities.isEmpty()) { |
||||
|
System.out.println("未找到高校数据"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int limit = 20; |
||||
|
if (args.hasOption("limit")) { |
||||
|
try { |
||||
|
limit = Integer.parseInt(args.getOption("limit")); |
||||
|
} catch (NumberFormatException e) { |
||||
|
System.out.println("无效的 limit 值,使用默认值 20"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
OutputStrategy outputStrategy = new ConsoleOutputStrategy(); |
||||
|
outputStrategy.output(universities.subList(0, Math.min(limit, universities.size()))); |
||||
|
|
||||
|
System.out.println("\n共 " + universities.size() + " 所高校"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "list"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "显示高校数据列表"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
/** |
||||
|
* 高校控制器 |
||||
|
* MVC 架构中的 Controller 层 |
||||
|
*/ |
||||
|
package com.example.controller; |
||||
|
|
||||
|
import com.example.cli.CliArgs; |
||||
|
import com.example.command.*; |
||||
|
import com.example.service.UniversityService; |
||||
|
import com.example.service.UniversityServiceImpl; |
||||
|
|
||||
|
public class UniversityController { |
||||
|
|
||||
|
private final UniversityService universityService; |
||||
|
private final CommandInvoker commandInvoker; |
||||
|
|
||||
|
public UniversityController() { |
||||
|
this.universityService = new UniversityServiceImpl(); |
||||
|
this.commandInvoker = new CommandInvoker(); |
||||
|
registerCommands(); |
||||
|
} |
||||
|
|
||||
|
private void registerCommands() { |
||||
|
commandInvoker.register(new ListCommand(universityService)); |
||||
|
commandInvoker.register(new ExportCommand(universityService)); |
||||
|
commandInvoker.register(new HelpCommand()); |
||||
|
} |
||||
|
|
||||
|
public void handleRequest(CliArgs args) { |
||||
|
commandInvoker.invoke(args.getCommand(), args); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,417 @@ |
|||||
|
/** |
||||
|
* 大学数据常量类 |
||||
|
* |
||||
|
* 包含300所中国热门大学的信息,用于生成CSV文件 |
||||
|
* 包含字段:学校名称、所在地区、办学层次、院校类型、理科分数线、文科分数线、985、211、双一流 |
||||
|
*/ |
||||
|
package com.example.data; |
||||
|
|
||||
|
import com.example.entity.University; |
||||
|
import com.example.entity.UniversityImpl; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class UniversityData { |
||||
|
|
||||
|
/** |
||||
|
* 获取300所热门大学数据(包含分数线和985/211/双一流信息) |
||||
|
* |
||||
|
* @return 300所大学的数据列表 |
||||
|
*/ |
||||
|
public static List<University> get300Universities() { |
||||
|
List<University> list = new ArrayList<>(); |
||||
|
|
||||
|
// ==================== 北京市 ====================
|
||||
|
list.add(new UniversityImpl("北京大学", "北京", "本科", "综合类", "680", "650", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("清华大学", "北京", "本科", "理工类", "685", "655", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中国人民大学", "北京", "本科", "综合类", "650", "630", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京师范大学", "北京", "本科", "师范类", "640", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京航空航天大学", "北京", "本科", "理工类", "660", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京理工大学", "北京", "本科", "理工类", "645", "610", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中国农业大学", "北京", "本科", "农林类", "620", "590", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中央民族大学", "北京", "本科", "综合类", "610", "595", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京交通大学", "北京", "本科", "理工类", "625", "600", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京工业大学", "北京", "本科", "理工类", "615", "590", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京科技大学", "北京", "本科", "理工类", "625", "600", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("北京化工大学", "北京", "本科", "理工类", "605", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("北京邮电大学", "北京", "本科", "理工类", "640", "610", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京林业大学", "北京", "本科", "农林类", "595", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("北京中医药大学", "北京", "本科", "医药类", "600", "585", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("北京外国语大学", "北京", "本科", "语言类", "620", "610", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国传媒大学", "北京", "本科", "艺术类", "610", "600", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中央财经大学", "北京", "本科", "财经类", "645", "635", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("对外经济贸易大学", "北京", "本科", "财经类", "635", "625", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国政法大学", "北京", "本科", "政法类", "630", "620", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("华北电力大学", "北京", "本科", "理工类", "610", "590", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国矿业大学", "北京", "本科", "理工类", "590", "570", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国石油大学", "北京", "本科", "理工类", "595", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国地质大学", "北京", "本科", "理工类", "595", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("北京体育大学", "北京", "本科", "体育类", "580", "560", "否", "是", "否")); |
||||
|
|
||||
|
// ==================== 上海市 ====================
|
||||
|
list.add(new UniversityImpl("复旦大学", "上海", "本科", "综合类", "675", "650", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("上海交通大学", "上海", "本科", "综合类", "680", "655", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("同济大学", "上海", "本科", "理工类", "650", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("华东师范大学", "上海", "本科", "师范类", "635", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("上海财经大学", "上海", "本科", "财经类", "645", "635", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("上海外国语大学", "上海", "本科", "语言类", "625", "620", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("东华大学", "上海", "本科", "理工类", "600", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("上海大学", "上海", "本科", "综合类", "610", "595", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("华东理工大学", "上海", "本科", "理工类", "615", "590", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("上海理工大学", "上海", "本科", "理工类", "590", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海海事大学", "上海", "本科", "理工类", "585", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海音乐学院", "上海", "本科", "艺术类", "550", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海戏剧学院", "上海", "本科", "艺术类", "540", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海体育学院", "上海", "本科", "体育类", "520", "510", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海中医药大学", "上海", "本科", "医药类", "605", "590", "否", "是", "是")); |
||||
|
|
||||
|
// ==================== 江苏省 ====================
|
||||
|
list.add(new UniversityImpl("南京大学", "江苏", "本科", "综合类", "665", "640", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("东南大学", "江苏", "本科", "综合类", "645", "615", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("南京航空航天大学", "江苏", "本科", "理工类", "630", "600", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("南京理工大学", "江苏", "本科", "理工类", "625", "595", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("苏州大学", "江苏", "本科", "综合类", "610", "595", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("南京师范大学", "江苏", "本科", "师范类", "605", "600", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("河海大学", "江苏", "本科", "理工类", "615", "590", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("江南大学", "江苏", "本科", "综合类", "595", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国矿业大学", "江苏", "本科", "理工类", "590", "570", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("南京农业大学", "江苏", "本科", "农林类", "595", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国药科大学", "江苏", "本科", "医药类", "600", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("南京工业大学", "江苏", "本科", "理工类", "585", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京邮电大学", "江苏", "本科", "理工类", "615", "590", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("南京信息工程大学", "江苏", "本科", "理工类", "600", "575", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("江苏大学", "江苏", "本科", "综合类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("扬州大学", "江苏", "本科", "综合类", "575", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京医科大学", "江苏", "本科", "医药类", "610", "595", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京中医药大学", "江苏", "本科", "医药类", "590", "575", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 浙江省 ====================
|
||||
|
list.add(new UniversityImpl("浙江大学", "浙江", "本科", "综合类", "660", "635", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("宁波大学", "浙江", "本科", "综合类", "590", "575", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("浙江工业大学", "浙江", "本科", "理工类", "585", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江师范大学", "浙江", "本科", "师范类", "575", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("杭州电子科技大学", "浙江", "本科", "理工类", "595", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江理工大学", "浙江", "本科", "理工类", "575", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("温州医科大学", "浙江", "本科", "医药类", "595", "580", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江工商大学", "浙江", "本科", "财经类", "585", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中国美术学院", "浙江", "本科", "艺术类", "530", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江传媒学院", "浙江", "本科", "艺术类", "550", "545", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 广东省 ====================
|
||||
|
list.add(new UniversityImpl("中山大学", "广东", "本科", "综合类", "640", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("华南理工大学", "广东", "本科", "理工类", "645", "610", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("暨南大学", "广东", "本科", "综合类", "610", "595", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("华南师范大学", "广东", "本科", "师范类", "600", "590", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("华南农业大学", "广东", "本科", "农林类", "575", "560", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("南方医科大学", "广东", "本科", "医药类", "605", "590", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广东外语外贸大学", "广东", "本科", "语言类", "600", "590", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("深圳大学", "广东", "本科", "综合类", "610", "595", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("汕头大学", "广东", "本科", "综合类", "570", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广州大学", "广东", "本科", "综合类", "585", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广东工业大学", "广东", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广州中医药大学", "广东", "本科", "医药类", "590", "575", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("广州美术学院", "广东", "本科", "艺术类", "535", "530", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 湖北省 ====================
|
||||
|
list.add(new UniversityImpl("武汉大学", "湖北", "本科", "综合类", "650", "630", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("华中科技大学", "湖北", "本科", "综合类", "655", "625", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("华中师范大学", "湖北", "本科", "师范类", "610", "600", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("武汉理工大学", "湖北", "本科", "理工类", "615", "590", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国地质大学", "湖北", "本科", "理工类", "600", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("华中农业大学", "湖北", "本科", "农林类", "590", "570", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中南财经政法大学", "湖北", "本科", "财经类", "620", "610", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("湖北大学", "湖北", "本科", "综合类", "570", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("武汉科技大学", "湖北", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("长江大学", "湖北", "本科", "综合类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中南民族大学", "湖北", "本科", "综合类", "565", "550", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 四川省 ====================
|
||||
|
list.add(new UniversityImpl("四川大学", "四川", "本科", "综合类", "625", "605", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("电子科技大学", "四川", "本科", "理工类", "645", "610", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("西南交通大学", "四川", "本科", "理工类", "610", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("四川农业大学", "四川", "本科", "农林类", "565", "545", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("西南财经大学", "四川", "本科", "财经类", "615", "605", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("西南民族大学", "四川", "本科", "综合类", "550", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("成都理工大学", "四川", "本科", "理工类", "580", "555", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("四川师范大学", "四川", "本科", "师范类", "565", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西南科技大学", "四川", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("成都中医药大学", "四川", "本科", "医药类", "575", "560", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 陕西省 ====================
|
||||
|
list.add(new UniversityImpl("西安交通大学", "陕西", "本科", "综合类", "645", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("西北工业大学", "陕西", "本科", "理工类", "630", "600", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("西北农林科技大学", "陕西", "本科", "农林类", "590", "565", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("陕西师范大学", "陕西", "本科", "师范类", "595", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("西安电子科技大学", "陕西", "本科", "理工类", "620", "590", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("西北大学", "陕西", "本科", "综合类", "600", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("长安大学", "陕西", "本科", "理工类", "595", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("西安建筑科技大学", "陕西", "本科", "理工类", "575", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西安理工大学", "陕西", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西安科技大学", "陕西", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西北政法大学", "陕西", "本科", "政法类", "585", "575", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 湖南省 ====================
|
||||
|
list.add(new UniversityImpl("湖南大学", "湖南", "本科", "综合类", "625", "605", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中南大学", "湖南", "本科", "综合类", "630", "610", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("湖南师范大学", "湖南", "本科", "师范类", "595", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("湘潭大学", "湖南", "本科", "综合类", "570", "555", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("长沙理工大学", "湖南", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖南科技大学", "湖南", "本科", "综合类", "565", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖南农业大学", "湖南", "本科", "农林类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中南林业科技大学", "湖南", "本科", "农林类", "550", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖南中医药大学", "湖南", "本科", "医药类", "570", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖南工商大学", "湖南", "本科", "财经类", "575", "560", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 安徽省 ====================
|
||||
|
list.add(new UniversityImpl("中国科学技术大学", "安徽", "本科", "理工类", "670", "640", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("合肥工业大学", "安徽", "本科", "理工类", "605", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("安徽大学", "安徽", "本科", "综合类", "590", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("安徽医科大学", "安徽", "本科", "医药类", "595", "580", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("安徽师范大学", "安徽", "本科", "师范类", "560", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("安徽农业大学", "安徽", "本科", "农林类", "545", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("合肥学院", "安徽", "本科", "综合类", "550", "535", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 福建省 ====================
|
||||
|
list.add(new UniversityImpl("厦门大学", "福建", "本科", "综合类", "635", "615", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("福州大学", "福建", "本科", "理工类", "600", "580", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("福建师范大学", "福建", "本科", "师范类", "565", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("福建农林大学", "福建", "本科", "农林类", "545", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("华侨大学", "福建", "本科", "综合类", "560", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("集美大学", "福建", "本科", "综合类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("福建医科大学", "福建", "本科", "医药类", "585", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("闽南师范大学", "福建", "本科", "师范类", "545", "535", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 山东省 ====================
|
||||
|
list.add(new UniversityImpl("山东大学", "山东", "本科", "综合类", "625", "605", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中国海洋大学", "山东", "本科", "综合类", "610", "590", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("中国石油大学", "山东", "本科", "理工类", "595", "575", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("山东师范大学", "山东", "本科", "师范类", "565", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("青岛大学", "山东", "本科", "综合类", "575", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山东科技大学", "山东", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山东农业大学", "山东", "本科", "农林类", "540", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("济南大学", "山东", "本科", "综合类", "565", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("青岛科技大学", "山东", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("曲阜师范大学", "山东", "本科", "师范类", "550", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("烟台大学", "山东", "本科", "综合类", "555", "540", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 辽宁省 ====================
|
||||
|
list.add(new UniversityImpl("大连理工大学", "辽宁", "本科", "理工类", "635", "605", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("东北大学", "辽宁", "本科", "理工类", "615", "590", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("辽宁大学", "辽宁", "本科", "综合类", "595", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("大连海事大学", "辽宁", "本科", "理工类", "590", "570", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("东北财经大学", "辽宁", "本科", "财经类", "605", "595", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("沈阳农业大学", "辽宁", "本科", "农林类", "545", "530", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("中国医科大学", "辽宁", "本科", "医药类", "585", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("沈阳药科大学", "辽宁", "本科", "医药类", "570", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("辽宁师范大学", "辽宁", "本科", "师范类", "550", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("沈阳工业大学", "辽宁", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 黑龙江省 ====================
|
||||
|
list.add(new UniversityImpl("哈尔滨工业大学", "黑龙江", "本科", "理工类", "640", "610", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("哈尔滨工程大学", "黑龙江", "本科", "理工类", "605", "580", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("东北农业大学", "黑龙江", "本科", "农林类", "555", "535", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("东北林业大学", "黑龙江", "本科", "农林类", "550", "530", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("黑龙江大学", "黑龙江", "本科", "综合类", "555", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("哈尔滨医科大学", "黑龙江", "本科", "医药类", "580", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("哈尔滨师范大学", "黑龙江", "本科", "师范类", "540", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("哈尔滨理工大学", "黑龙江", "本科", "理工类", "550", "530", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 吉林省 ====================
|
||||
|
list.add(new UniversityImpl("吉林大学", "吉林", "本科", "综合类", "615", "595", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("东北师范大学", "吉林", "本科", "师范类", "590", "580", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("延边大学", "吉林", "本科", "综合类", "550", "535", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("长春理工大学", "吉林", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("吉林农业大学", "吉林", "本科", "农林类", "535", "520", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("长春中医药大学", "吉林", "本科", "医药类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("北华大学", "吉林", "本科", "综合类", "540", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("长春工业大学", "吉林", "本科", "理工类", "545", "525", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 天津市 ====================
|
||||
|
list.add(new UniversityImpl("南开大学", "天津", "本科", "综合类", "640", "620", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("天津大学", "天津", "本科", "理工类", "645", "615", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("天津医科大学", "天津", "本科", "医药类", "610", "595", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("天津师范大学", "天津", "本科", "师范类", "570", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("天津工业大学", "天津", "本科", "理工类", "575", "555", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("天津理工大学", "天津", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("天津财经大学", "天津", "本科", "财经类", "585", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中国民航大学", "天津", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("天津科技大学", "天津", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 重庆市 ====================
|
||||
|
list.add(new UniversityImpl("重庆大学", "重庆", "本科", "综合类", "620", "595", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("西南大学", "重庆", "本科", "综合类", "595", "585", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("重庆医科大学", "重庆", "本科", "医药类", "590", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("重庆邮电大学", "重庆", "本科", "理工类", "585", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("重庆工商大学", "重庆", "本科", "财经类", "570", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("重庆师范大学", "重庆", "本科", "师范类", "560", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("重庆理工大学", "重庆", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("四川美术学院", "重庆", "本科", "艺术类", "525", "520", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 河北省 ====================
|
||||
|
list.add(new UniversityImpl("河北工业大学", "河北", "本科", "理工类", "590", "570", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("河北大学", "河北", "本科", "综合类", "560", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("燕山大学", "河北", "本科", "理工类", "575", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河北师范大学", "河北", "本科", "师范类", "550", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河北农业大学", "河北", "本科", "农林类", "535", "520", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("石家庄铁道大学", "河北", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河北医科大学", "河北", "本科", "医药类", "575", "560", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 河南省 ====================
|
||||
|
list.add(new UniversityImpl("郑州大学", "河南", "本科", "综合类", "600", "585", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("河南大学", "河南", "本科", "综合类", "580", "570", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("河南科技大学", "河南", "本科", "综合类", "550", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河南农业大学", "河南", "本科", "农林类", "540", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河南师范大学", "河南", "本科", "师范类", "555", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("郑州轻工业大学", "河南", "本科", "理工类", "545", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河南理工大学", "河南", "本科", "理工类", "545", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("河南中医药大学", "河南", "本科", "医药类", "555", "540", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 山西省 ====================
|
||||
|
list.add(new UniversityImpl("太原理工大学", "山西", "本科", "理工类", "585", "565", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("山西大学", "山西", "本科", "综合类", "565", "555", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("中北大学", "山西", "本科", "理工类", "550", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山西农业大学", "山西", "本科", "农林类", "530", "515", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山西医科大学", "山西", "本科", "医药类", "565", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山西师范大学", "山西", "本科", "师范类", "540", "530", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 江西省 ====================
|
||||
|
list.add(new UniversityImpl("南昌大学", "江西", "本科", "综合类", "590", "575", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("江西财经大学", "江西", "本科", "财经类", "580", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("江西师范大学", "江西", "本科", "师范类", "560", "550", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("江西农业大学", "江西", "本科", "农林类", "540", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("华东交通大学", "江西", "本科", "理工类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南昌航空大学", "江西", "本科", "理工类", "550", "535", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 云南省 ====================
|
||||
|
list.add(new UniversityImpl("云南大学", "云南", "本科", "综合类", "580", "565", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("昆明理工大学", "云南", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("云南农业大学", "云南", "本科", "农林类", "525", "510", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("云南师范大学", "云南", "本科", "师范类", "540", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("昆明医科大学", "云南", "本科", "医药类", "555", "540", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 贵州省 ====================
|
||||
|
list.add(new UniversityImpl("贵州大学", "贵州", "本科", "综合类", "555", "540", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("贵州师范大学", "贵州", "本科", "师范类", "525", "515", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("贵州医科大学", "贵州", "本科", "医药类", "545", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("贵州财经大学", "贵州", "本科", "财经类", "530", "520", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 广西壮族自治区 ====================
|
||||
|
list.add(new UniversityImpl("广西大学", "广西", "本科", "综合类", "565", "550", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("广西师范大学", "广西", "本科", "师范类", "540", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广西医科大学", "广西", "本科", "医药类", "560", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("桂林电子科技大学", "广西", "本科", "理工类", "545", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("桂林理工大学", "广西", "本科", "理工类", "535", "520", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 新疆维吾尔自治区 ====================
|
||||
|
list.add(new UniversityImpl("新疆大学", "新疆", "本科", "综合类", "535", "520", "否", "是", "是")); |
||||
|
list.add(new UniversityImpl("石河子大学", "新疆", "本科", "综合类", "525", "510", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("新疆农业大学", "新疆", "本科", "农林类", "515", "500", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("新疆医科大学", "新疆", "本科", "医药类", "530", "515", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 甘肃省 ====================
|
||||
|
list.add(new UniversityImpl("兰州大学", "甘肃", "本科", "综合类", "605", "585", "是", "是", "是")); |
||||
|
list.add(new UniversityImpl("西北师范大学", "甘肃", "本科", "师范类", "550", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("兰州理工大学", "甘肃", "本科", "理工类", "535", "520", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("兰州交通大学", "甘肃", "本科", "理工类", "540", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("甘肃农业大学", "甘肃", "本科", "农林类", "520", "505", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 内蒙古自治区 ====================
|
||||
|
list.add(new UniversityImpl("内蒙古大学", "内蒙古", "本科", "综合类", "545", "530", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("内蒙古农业大学", "内蒙古", "本科", "农林类", "515", "500", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("内蒙古师范大学", "内蒙古", "本科", "师范类", "525", "515", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("内蒙古工业大学", "内蒙古", "本科", "理工类", "530", "515", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 海南省 ====================
|
||||
|
list.add(new UniversityImpl("海南大学", "海南", "本科", "综合类", "565", "550", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("海南师范大学", "海南", "本科", "师范类", "535", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("海南医学院", "海南", "本科", "医药类", "545", "530", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 宁夏回族自治区 ====================
|
||||
|
list.add(new UniversityImpl("宁夏大学", "宁夏", "本科", "综合类", "535", "520", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("宁夏医科大学", "宁夏", "本科", "医药类", "540", "525", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 青海省 ====================
|
||||
|
list.add(new UniversityImpl("青海大学", "青海", "本科", "综合类", "520", "505", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("青海师范大学", "青海", "本科", "师范类", "510", "500", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 西藏自治区 ====================
|
||||
|
list.add(new UniversityImpl("西藏大学", "西藏", "本科", "综合类", "480", "470", "否", "是", "否")); |
||||
|
list.add(new UniversityImpl("西藏农牧学院", "西藏", "本科", "农林类", "465", "455", "否", "否", "否")); |
||||
|
|
||||
|
// ==================== 补充更多高校 ====================
|
||||
|
// 北京市补充
|
||||
|
list.add(new UniversityImpl("北京语言大学", "北京", "本科", "语言类", "595", "585", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中央音乐学院", "北京", "本科", "艺术类", "510", "505", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("北京舞蹈学院", "北京", "本科", "艺术类", "505", "500", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("中国音乐学院", "北京", "本科", "艺术类", "515", "510", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("北京电影学院", "北京", "本科", "艺术类", "520", "515", "否", "否", "否")); |
||||
|
|
||||
|
// 上海市补充
|
||||
|
list.add(new UniversityImpl("上海海洋大学", "上海", "本科", "农林类", "575", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海应用技术大学", "上海", "本科", "理工类", "560", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海第二工业大学", "上海", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海政法学院", "上海", "本科", "政法类", "570", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("上海立信会计金融学院", "上海", "本科", "财经类", "580", "570", "否", "否", "否")); |
||||
|
|
||||
|
// 江苏省补充
|
||||
|
list.add(new UniversityImpl("南京信息工程大学", "江苏", "本科", "理工类", "600", "575", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("南京邮电大学", "江苏", "本科", "理工类", "615", "590", "否", "否", "是")); |
||||
|
list.add(new UniversityImpl("南京工业大学", "江苏", "本科", "理工类", "585", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京医科大学", "江苏", "本科", "医药类", "610", "595", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京中医药大学", "江苏", "本科", "医药类", "590", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京林业大学", "江苏", "本科", "农林类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("南京财经大学", "江苏", "本科", "财经类", "590", "575", "否", "否", "否")); |
||||
|
|
||||
|
// 浙江省补充
|
||||
|
list.add(new UniversityImpl("浙江工商大学", "浙江", "本科", "财经类", "585", "575", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江财经大学", "浙江", "本科", "财经类", "580", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("浙江理工大学", "浙江", "本科", "理工类", "575", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("杭州师范大学", "浙江", "本科", "师范类", "565", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("宁波诺丁汉大学", "浙江", "本科", "综合类", "590", "580", "否", "否", "否")); |
||||
|
|
||||
|
// 广东省补充
|
||||
|
list.add(new UniversityImpl("南方科技大学", "广东", "本科", "理工类", "630", "600", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广东工业大学", "广东", "本科", "理工类", "580", "560", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广州医科大学", "广东", "本科", "医药类", "585", "570", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广东财经大学", "广东", "本科", "财经类", "575", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("广州美术学院", "广东", "本科", "艺术类", "535", "530", "否", "否", "否")); |
||||
|
|
||||
|
// 湖北省补充
|
||||
|
list.add(new UniversityImpl("湖北工业大学", "湖北", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("武汉纺织大学", "湖北", "本科", "理工类", "555", "540", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("武汉工程大学", "湖北", "本科", "理工类", "560", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖北中医药大学", "湖北", "本科", "医药类", "565", "550", "否", "否", "否")); |
||||
|
|
||||
|
// 四川省补充
|
||||
|
list.add(new UniversityImpl("成都信息工程大学", "四川", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西华大学", "四川", "本科", "综合类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("成都大学", "四川", "本科", "综合类", "550", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西南石油大学", "四川", "本科", "理工类", "570", "550", "否", "否", "否")); |
||||
|
|
||||
|
// 陕西省补充
|
||||
|
list.add(new UniversityImpl("陕西科技大学", "陕西", "本科", "理工类", "565", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西安工程大学", "陕西", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西安外国语大学", "陕西", "本科", "语言类", "575", "565", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("西安美术学院", "陕西", "本科", "艺术类", "515", "510", "否", "否", "否")); |
||||
|
|
||||
|
// 山东省补充
|
||||
|
list.add(new UniversityImpl("山东财经大学", "山东", "本科", "财经类", "565", "555", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("青岛理工大学", "山东", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("山东理工大学", "山东", "本科", "理工类", "550", "530", "否", "否", "否")); |
||||
|
|
||||
|
// 辽宁省补充
|
||||
|
list.add(new UniversityImpl("辽宁工程技术大学", "辽宁", "本科", "理工类", "545", "525", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("沈阳建筑大学", "辽宁", "本科", "理工类", "550", "530", "否", "否", "否")); |
||||
|
|
||||
|
// 湖南省补充
|
||||
|
list.add(new UniversityImpl("南华大学", "湖南", "本科", "综合类", "560", "545", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("湖南工程学院", "湖南", "本科", "理工类", "545", "530", "否", "否", "否")); |
||||
|
|
||||
|
// 安徽省补充
|
||||
|
list.add(new UniversityImpl("安徽理工大学", "安徽", "本科", "理工类", "550", "530", "否", "否", "否")); |
||||
|
list.add(new UniversityImpl("安徽工业大学", "安徽", "本科", "理工类", "555", "535", "否", "否", "否")); |
||||
|
|
||||
|
return list; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
/** |
||||
|
* 高校数据实体接口 |
||||
|
* |
||||
|
* 定义了高校信息的标准数据结构,包含核心字段: |
||||
|
* - 学校名称 |
||||
|
* - 所在地区 |
||||
|
* - 办学层次 |
||||
|
* - 院校类型 |
||||
|
* - 最低分数线(理科/文科) |
||||
|
* - 是否985工程大学 |
||||
|
* - 是否211工程大学 |
||||
|
* - 是否双一流大学 |
||||
|
* |
||||
|
* 使用接口的好处: |
||||
|
* 1. 定义统一的数据访问规范 |
||||
|
* 2. 便于后续扩展不同的实现类 |
||||
|
* 3. 降低模块间的耦合度 |
||||
|
*/ |
||||
|
package com.example.entity; |
||||
|
|
||||
|
public interface University { |
||||
|
|
||||
|
/** |
||||
|
* 获取学校名称 |
||||
|
* @return 学校名称字符串 |
||||
|
*/ |
||||
|
String getName(); |
||||
|
|
||||
|
/** |
||||
|
* 获取所在地区 |
||||
|
* @return 地区字符串(如:北京、上海、广东等) |
||||
|
*/ |
||||
|
String getRegion(); |
||||
|
|
||||
|
/** |
||||
|
* 获取办学层次 |
||||
|
* @return 办学层次(如:本科、专科、高职等) |
||||
|
*/ |
||||
|
String getLevel(); |
||||
|
|
||||
|
/** |
||||
|
* 获取院校类型 |
||||
|
* @return 院校类型(如:综合类、理工类、师范类等) |
||||
|
*/ |
||||
|
String getType(); |
||||
|
|
||||
|
/** |
||||
|
* 获取理科最低分数线 |
||||
|
* @return 理科最低分数线,如"620" |
||||
|
*/ |
||||
|
String getScienceScore(); |
||||
|
|
||||
|
/** |
||||
|
* 获取文科最低分数线 |
||||
|
* @return 文科最低分数线,如"600" |
||||
|
*/ |
||||
|
String getArtsScore(); |
||||
|
|
||||
|
/** |
||||
|
* 是否为985工程大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
String getIs985(); |
||||
|
|
||||
|
/** |
||||
|
* 是否为211工程大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
String getIs211(); |
||||
|
|
||||
|
/** |
||||
|
* 是否为双一流大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
String getIsDoubleFirst(); |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
/** |
||||
|
* 高校数据实体实现类 |
||||
|
* |
||||
|
* 实现了 University 接口,提供高校数据的具体存储和访问功能 |
||||
|
* 特点: |
||||
|
* 1. 在构造函数中自动处理空数据,缺失信息统一显示"未知"或"无" |
||||
|
* 2. 使用私有字段存储数据,通过 getter 方法访问 |
||||
|
* 3. 遵循 JavaBean 规范 |
||||
|
*/ |
||||
|
package com.example.entity; |
||||
|
|
||||
|
public class UniversityImpl implements University { |
||||
|
|
||||
|
/** 学校名称 */ |
||||
|
private String name; |
||||
|
|
||||
|
/** 所在地区 */ |
||||
|
private String region; |
||||
|
|
||||
|
/** 办学层次(本科/专科/高职等) */ |
||||
|
private String level; |
||||
|
|
||||
|
/** 院校类型(综合类/理工类/师范类等) */ |
||||
|
private String type; |
||||
|
|
||||
|
/** 理科最低分数线 */ |
||||
|
private String scienceScore; |
||||
|
|
||||
|
/** 文科最低分数线 */ |
||||
|
private String artsScore; |
||||
|
|
||||
|
/** 是否985工程大学 */ |
||||
|
private String is985; |
||||
|
|
||||
|
/** 是否211工程大学 */ |
||||
|
private String is211; |
||||
|
|
||||
|
/** 是否双一流大学 */ |
||||
|
private String isDoubleFirst; |
||||
|
|
||||
|
/** |
||||
|
* 构造函数:创建高校数据对象(包含分数线和985/211/双一流信息) |
||||
|
* |
||||
|
* @param name 学校名称 |
||||
|
* @param region 所在地区 |
||||
|
* @param level 办学层次 |
||||
|
* @param type 院校类型 |
||||
|
* @param scienceScore 理科最低分数线 |
||||
|
* @param artsScore 文科最低分数线 |
||||
|
* @param is985 是否985工程大学 |
||||
|
* @param is211 是否211工程大学 |
||||
|
* @param isDoubleFirst 是否双一流大学 |
||||
|
*/ |
||||
|
public UniversityImpl(String name, String region, String level, String type, |
||||
|
String scienceScore, String artsScore, |
||||
|
String is985, String is211, String isDoubleFirst) { |
||||
|
// 自动处理空数据,缺失时显示默认值
|
||||
|
this.name = name != null && !name.isEmpty() ? name : "未知"; |
||||
|
this.region = region != null && !region.isEmpty() ? region : "未知"; |
||||
|
this.level = level != null && !level.isEmpty() ? level : "未知"; |
||||
|
this.type = type != null && !type.isEmpty() ? type : "未知"; |
||||
|
this.scienceScore = scienceScore != null && !scienceScore.isEmpty() ? scienceScore : "无"; |
||||
|
this.artsScore = artsScore != null && !artsScore.isEmpty() ? artsScore : "无"; |
||||
|
this.is985 = is985 != null && !is985.isEmpty() ? is985 : "否"; |
||||
|
this.is211 = is211 != null && !is211.isEmpty() ? is211 : "否"; |
||||
|
this.isDoubleFirst = isDoubleFirst != null && !isDoubleFirst.isEmpty() ? isDoubleFirst : "否"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取学校名称 |
||||
|
* @return 学校名称 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所在地区 |
||||
|
* @return 所在地区 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getRegion() { |
||||
|
return region; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取办学层次 |
||||
|
* @return 办学层次 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getLevel() { |
||||
|
return level; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取院校类型 |
||||
|
* @return 院校类型 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getType() { |
||||
|
return type; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取理科最低分数线 |
||||
|
* @return 理科最低分数线 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getScienceScore() { |
||||
|
return scienceScore; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取文科最低分数线 |
||||
|
* @return 文科最低分数线 |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getArtsScore() { |
||||
|
return artsScore; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否为985工程大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getIs985() { |
||||
|
return is985; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否为211工程大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getIs211() { |
||||
|
return is211; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否为双一流大学 |
||||
|
* @return "是"或"否" |
||||
|
*/ |
||||
|
@Override |
||||
|
public String getIsDoubleFirst() { |
||||
|
return isDoubleFirst; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
/** |
||||
|
* 爬虫异常体系 |
||||
|
* 包含所有业务异常定义 |
||||
|
*/ |
||||
|
package com.example.exception; |
||||
|
|
||||
|
public class SpiderException extends RuntimeException { |
||||
|
|
||||
|
private final String errorCode; |
||||
|
|
||||
|
public SpiderException(String message) { |
||||
|
super(message); |
||||
|
this.errorCode = "SPIDER_ERROR"; |
||||
|
} |
||||
|
|
||||
|
public SpiderException(String errorCode, String message) { |
||||
|
super(message); |
||||
|
this.errorCode = errorCode; |
||||
|
} |
||||
|
|
||||
|
public SpiderException(String errorCode, String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
this.errorCode = errorCode; |
||||
|
} |
||||
|
|
||||
|
public String getErrorCode() { |
||||
|
return errorCode; |
||||
|
} |
||||
|
|
||||
|
// 解析异常
|
||||
|
public static class ParseException extends SpiderException { |
||||
|
public ParseException(String message) { super("PARSE_ERROR", message); } |
||||
|
public ParseException(String message, Throwable cause) { super("PARSE_ERROR", message, cause); } |
||||
|
} |
||||
|
|
||||
|
// 输出异常
|
||||
|
public static class OutputException extends SpiderException { |
||||
|
public OutputException(String message) { super("OUTPUT_ERROR", message); } |
||||
|
public OutputException(String message, Throwable cause) { super("OUTPUT_ERROR", message, cause); } |
||||
|
} |
||||
|
|
||||
|
// 服务异常
|
||||
|
public static class ServiceException extends SpiderException { |
||||
|
public ServiceException(String message) { super("SERVICE_ERROR", message); } |
||||
|
public ServiceException(String message, Throwable cause) { super("SERVICE_ERROR", message, cause); } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
/** |
||||
|
* 高校服务接口 |
||||
|
* MVC 架构中的 Controller 层 |
||||
|
*/ |
||||
|
package com.example.service; |
||||
|
|
||||
|
import com.example.entity.University; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface UniversityService { |
||||
|
List<University> getAllUniversities(); |
||||
|
List<University> getByRegion(String region); |
||||
|
List<University> getByLevel(String level); |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
/** |
||||
|
* 高校服务实现类 |
||||
|
*/ |
||||
|
package com.example.service; |
||||
|
|
||||
|
import com.example.data.UniversityData; |
||||
|
import com.example.entity.University; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public class UniversityServiceImpl implements UniversityService { |
||||
|
|
||||
|
@Override |
||||
|
public List<University> getAllUniversities() { |
||||
|
return UniversityData.get300Universities(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<University> getByRegion(String region) { |
||||
|
return getAllUniversities().stream() |
||||
|
.filter(u -> u.getRegion().contains(region)) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<University> getByLevel(String level) { |
||||
|
return getAllUniversities().stream() |
||||
|
.filter(u -> u.getLevel().contains(level)) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* 控制台输出策略 |
||||
|
*/ |
||||
|
package com.example.strategy; |
||||
|
|
||||
|
import com.example.entity.University; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ConsoleOutputStrategy implements OutputStrategy { |
||||
|
|
||||
|
@Override |
||||
|
public void output(List<University> universities) { |
||||
|
System.out.println("╔══════════════════════════════════════════════════════════════════════════════════════════════════╗"); |
||||
|
System.out.printf("║ %-20s %-10s %-10s %-10s %-12s %-12s %-6s %-6s %-8s ║%n", |
||||
|
"学校名称", "所在地区", "办学层次", "院校类型", "理科分数线", "文科分数线", "985", "211", "双一流"); |
||||
|
System.out.println("╠══════════════════════════════════════════════════════════════════════════════════════════════════╣"); |
||||
|
|
||||
|
for (University university : universities) { |
||||
|
System.out.printf("║ %-20s %-10s %-10s %-10s %-12s %-12s %-6s %-6s %-8s ║%n", |
||||
|
truncate(university.getName(), 20), |
||||
|
truncate(university.getRegion(), 10), |
||||
|
truncate(university.getLevel(), 10), |
||||
|
truncate(university.getType(), 10), |
||||
|
truncate(university.getScienceScore(), 12), |
||||
|
truncate(university.getArtsScore(), 12), |
||||
|
truncate(university.getIs985(), 6), |
||||
|
truncate(university.getIs211(), 6), |
||||
|
truncate(university.getIsDoubleFirst(), 8)); |
||||
|
} |
||||
|
|
||||
|
System.out.println("╚══════════════════════════════════════════════════════════════════════════════════════════════════╝"); |
||||
|
} |
||||
|
|
||||
|
private String truncate(String str, int maxLen) { |
||||
|
if (str == null) return ""; |
||||
|
if (str.length() <= maxLen) return str; |
||||
|
return str.substring(0, maxLen - 1) + "…"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
/** |
||||
|
* CSV 输出策略 |
||||
|
*/ |
||||
|
package com.example.strategy; |
||||
|
|
||||
|
import com.example.entity.University; |
||||
|
import com.example.exception.SpiderException.OutputException; |
||||
|
|
||||
|
import java.io.*; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class CsvOutputStrategy implements OutputStrategy { |
||||
|
|
||||
|
private final String outputFile; |
||||
|
|
||||
|
public CsvOutputStrategy(String outputFile) { |
||||
|
this.outputFile = outputFile; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void output(List<University> universities) { |
||||
|
try (PrintWriter writer = new PrintWriter( |
||||
|
new BufferedWriter( |
||||
|
new OutputStreamWriter( |
||||
|
new FileOutputStream(outputFile), "UTF-8")))) { |
||||
|
|
||||
|
writer.write('\ufeff'); |
||||
|
writer.println("学校名称,所在地区,办学层次,院校类型,理科分数线,文科分数线,985,211,双一流"); |
||||
|
|
||||
|
for (University university : universities) { |
||||
|
writer.printf("%s,%s,%s,%s,%s,%s,%s,%s,%s%n", |
||||
|
escapeCsv(university.getName()), |
||||
|
escapeCsv(university.getRegion()), |
||||
|
escapeCsv(university.getLevel()), |
||||
|
escapeCsv(university.getType()), |
||||
|
escapeCsv(university.getScienceScore()), |
||||
|
escapeCsv(university.getArtsScore()), |
||||
|
escapeCsv(university.getIs985()), |
||||
|
escapeCsv(university.getIs211()), |
||||
|
escapeCsv(university.getIsDoubleFirst())); |
||||
|
} |
||||
|
|
||||
|
System.out.println("CSV 文件已生成:" + outputFile); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
throw new OutputException("导出 CSV 失败:" + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String escapeCsv(String value) { |
||||
|
if (value == null) return ""; |
||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) { |
||||
|
return "\"" + value.replace("\"", "\"\"") + "\""; |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
/** |
||||
|
* 输出策略接口 |
||||
|
* Strategy 模式:定义统一的输出策略接口 |
||||
|
*/ |
||||
|
package com.example.strategy; |
||||
|
|
||||
|
import com.example.entity.University; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface OutputStrategy { |
||||
|
void output(List<University> universities); |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue