You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

529 lines
22 KiB

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// ========== 1. 泛型接口:定义通用数据处理行为 ==========
/**
* 通用数据爬虫接口(泛型:T-爬取数据类型,K-数据唯一标识类型)
*/
interface DataCrawler<T, K> {
// 爬取数据
T crawlData(K key) throws Exception;
// 解析数据
Map<String, Object> parseData(T rawData);
// 保存数据
void saveData(K key, Map<String, Object> data);
}
/**
* 通用数据可视化接口(泛型:T-可视化数据类型)
*/
interface DataVisualizer<T> {
void generateVisualization(String title, T data);
}
// ========== 2. 抽象泛型父类:爬虫基类 ==========
abstract class AbstractDataCrawler<T, K> implements DataCrawler<T, K> {
// 泛型集合:存储爬取的原始数据(K-标识,T-原始数据)
protected Map<K, T> rawDataMap = new HashMap<>();
// 泛型集合:存储解析后的结构化数据(K-标识,Map-结构化数据)
protected Map<K, Map<String, Object>> parsedDataMap = new LinkedHashMap<>();
// 泛型集合:存储爬取失败的标识
protected List<K> failedKeys = new LinkedList<>();
// 通用HTTP请求方法(泛型返回值)
protected String doHttpGet(String url) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("HTTP请求失败,状态码:" + response.statusCode());
}
return response.body();
}
// 通用失败记录方法
protected void recordFailure(K key, Exception e) {
failedKeys.add(key);
System.err.println("❌ 爬取" + key + "失败:" + e.getMessage());
}
// 泛型方法:获取解析后的数据
public <V> V getParsedValue(K key, String field, Class<V> type) {
if (parsedDataMap.containsKey(key) && parsedDataMap.get(key).containsKey(field)) {
Object value = parsedDataMap.get(key).get(field);
if (type.isInstance(value)) {
return type.cast(value);
}
}
return null;
}
// 抽象方法:获取API地址(子类实现)
protected abstract String getApiUrl(K key);
}
// ========== 3. 天气爬虫子类(泛型实现) ==========
class WeatherCrawler extends AbstractDataCrawler<String, String> implements DataVisualizer<Map<String, List<?>>> {
// 城市经纬度映射(泛型集合)
private Map<String, String[]> cityLatLonMap = new HashMap<>();
// 初始化城市数据
public WeatherCrawler() {
cityLatLonMap.put("西安", new String[]{"34.2644", "108.9497"});
cityLatLonMap.put("成都", new String[]{"30.5728", "104.0668"});
cityLatLonMap.put("兰州", new String[]{"36.0611", "103.8343"});
cityLatLonMap.put("乌鲁木齐", new String[]{"43.8256", "87.6168"});
}
@Override
public String crawlData(String cityName) throws Exception {
if (!cityLatLonMap.containsKey(cityName)) {
throw new IllegalArgumentException("未配置城市:" + cityName + "的经纬度");
}
String url = getApiUrl(cityName);
System.out.println("🌐 正在爬取" + cityName + "天气数据:" + url);
String rawData = doHttpGet(url);
rawDataMap.put(cityName, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(String rawData) {
Map<String, Object> parsedData = new HashMap<>();
try {
List<String> times = parseTimes(rawData);
List<Double> temps = parseTemperatures(rawData);
parsedData.put("times", times);
parsedData.put("temps", temps);
parsedData.put("minTemp", temps.stream().mapToDouble(Double::doubleValue).min().orElse(0));
parsedData.put("maxTemp", temps.stream().mapToDouble(Double::doubleValue).max().orElse(0));
} catch (Exception e) {
System.err.println("❌ 解析天气数据失败:" + e.getMessage());
}
return parsedData;
}
@Override
public void saveData(String cityName, Map<String, Object> data) {
parsedDataMap.put(cityName, data);
System.out.println("💾 " + cityName + "天气数据已保存,解析字段数:" + data.size());
}
@Override
protected String getApiUrl(String cityName) {
String[] latLon = cityLatLonMap.get(cityName);
return String.format(
"https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s&hourly=temperature_2m&past_days=1&forecast_days=3",
latLon[0], latLon[1]
);
}
// 解析时间(复用原有逻辑)
private List<String> parseTimes(String json) {
List<String> times = new ArrayList<>();
try {
Pattern pattern = Pattern.compile("\"time\":\\[([^\\]]+)\\]");
Matcher matcher = pattern.matcher(json);
if (matcher.find()) {
String timeStr = matcher.group(1);
String[] timeArray = timeStr.split(",");
for (String t : timeArray) {
t = t.trim().replace("\"", "");
if (!t.isEmpty()) {
if (t.contains("T")) {
String date = t.substring(5, 10);
String time = t.substring(11, 16);
times.add(date + "\n" + time);
} else {
times.add(t);
}
}
}
}
} catch (Exception e) {
System.err.println("❌ 解析时间失败:" + e.getMessage());
}
return times;
}
// 解析温度(复用原有逻辑)
private List<Double> parseTemperatures(String json) {
List<Double> temps = new ArrayList<>();
try {
Pattern pattern = Pattern.compile("\"temperature_2m\":\\[([^\\]]+)\\]");
Matcher matcher = pattern.matcher(json);
if (matcher.find()) {
String tempStr = matcher.group(1);
String[] tempArray = tempStr.split(",");
for (String t : tempArray) {
t = t.trim();
if (!t.isEmpty()) {
try {
temps.add(Double.parseDouble(t));
} catch (NumberFormatException e) {
System.err.println("⚠️ 无法解析温度值: " + t);
}
}
}
}
} catch (Exception e) {
System.err.println("❌ 解析温度失败:" + e.getMessage());
}
return temps;
}
// 天气数据可视化(泛型实现)
@Override
public void generateVisualization(String cityName, Map<String, List<?>> data) {
List<String> times = (List<String>) data.get("times");
List<Double> temps = (List<Double>) data.get("temps");
if (times == null || temps == null || times.isEmpty() || temps.isEmpty()) {
System.out.println("⚠️ " + cityName + " 数据无效,跳过绘图");
return;
}
try {
int width = 1400;
int height = 700;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, width, height);
int marginLeft = 120;
int marginRight = 60;
int marginTop = 80;
int marginBottom = 120;
int chartWidth = width - marginLeft - marginRight;
int chartHeight = height - marginTop - marginBottom;
double minTemp = temps.stream().mapToDouble(Double::doubleValue).min().orElse(0);
double maxTemp = temps.stream().mapToDouble(Double::doubleValue).max().orElse(0);
double tempRange = maxTemp - minTemp;
// 画网格
g2d.setColor(Color.LIGHT_GRAY);
g2d.setStroke(new BasicStroke(1));
int numYLines = 10;
for (int i = 0; i <= numYLines; i++) {
int y = marginTop + (chartHeight * i / numYLines);
g2d.drawLine(marginLeft, y, marginLeft + chartWidth, y);
double temp = maxTemp - (tempRange * i / numYLines);
String label = String.format("%.1f°C", temp);
g2d.setColor(Color.BLACK);
g2d.setFont(new Font("Arial", Font.PLAIN, 12));
g2d.drawString(label, marginLeft - 50, y + 4);
g2d.setColor(Color.LIGHT_GRAY);
}
// 画坐标轴
g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(2));
g2d.drawLine(marginLeft, marginTop, marginLeft, marginTop + chartHeight);
g2d.drawLine(marginLeft, marginTop + chartHeight, marginLeft + chartWidth, marginTop + chartHeight);
// 标题
g2d.setFont(new Font("Arial", Font.BOLD, 20));
String title = cityName + " 逐小时温度变化(过去1天+未来3天)";
g2d.drawString(title, width / 2 - 250, 45);
// 轴标签
g2d.setFont(new Font("Arial", Font.PLAIN, 14));
g2d.drawString("时间", width / 2 - 20, height - 40);
Graphics2D g2dRotated = (Graphics2D) g2d.create();
g2dRotated.rotate(-Math.PI / 2);
g2dRotated.drawString("温度 (°C)", -height / 2, 35);
g2dRotated.dispose();
// 画数据
if (temps.size() > 1) {
int[] xPoints = new int[temps.size()];
int[] yPoints = new int[temps.size()];
for (int i = 0; i < temps.size(); i++) {
int x = marginLeft + (chartWidth * i / (temps.size() - 1));
int y = marginTop + chartHeight - (int) ((temps.get(i) - minTemp) * chartHeight / tempRange);
xPoints[i] = x;
yPoints[i] = y;
}
g2d.setColor(new Color(255, 0, 0, 180));
g2d.setStroke(new BasicStroke(2.5f));
for (int i = 0; i < temps.size() - 1; i++) {
g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]);
}
g2d.setColor(Color.RED);
for (int i = 0; i < temps.size(); i++) {
g2d.fillOval(xPoints[i] - 3, yPoints[i] - 3, 6, 6);
if (i % 12 == 0) {
g2d.setColor(Color.BLUE);
g2d.setFont(new Font("Arial", Font.PLAIN, 10));
g2d.drawString(String.format("%.1f", temps.get(i)), xPoints[i] + 5, yPoints[i] - 5);
g2d.setColor(Color.RED);
}
}
}
// X轴标签
g2d.setColor(Color.BLACK);
g2d.setFont(new Font("Arial", Font.PLAIN, 9));
int step = Math.max(1, times.size() / 15);
for (int i = 0; i < times.size(); i += step) {
int x = marginLeft + (chartWidth * i / (times.size() - 1));
int y = marginTop + chartHeight + 15;
String label = times.get(i);
if (label.contains("\n")) {
String[] lines = label.split("\n");
g2d.drawString(lines[0], x - 20, y);
g2d.drawString(lines[1], x - 20, y + 12);
} else if (label.length() > 10) {
g2d.drawString(label.substring(0, 10), x - 20, y + 5);
} else {
g2d.drawString(label, x - 15, y + 5);
}
}
g2d.dispose();
String fileName = cityName + "_温度图.png";
File outputFile = new File(fileName);
ImageIO.write(image, "PNG", outputFile);
System.out.println("💾 " + cityName + "图表已保存为:" + outputFile.getAbsolutePath());
} catch (Exception e) {
System.err.println("❌ 生成" + cityName + "图表失败:" + e.getMessage());
e.printStackTrace();
}
}
}
// ========== 4. 城市特色爬虫子类(扩展新类型爬虫) ==========
class CityInfoCrawler extends AbstractDataCrawler<Map<String, String>, String> {
// 模拟城市特色数据(实际可替换为真实爬虫逻辑)
private Map<String, Map<String, String>> cityInfoSource = new HashMap<>();
public CityInfoCrawler() {
// 初始化城市特色数据
Map<String, String> xiAnInfo = new HashMap<>();
xiAnInfo.put("别名", "十三朝古都");
xiAnInfo.put("地标", "兵马俑、大雁塔");
xiAnInfo.put("美食", "肉夹馍、泡馍");
xiAnInfo.put("经纬度", "34.2644, 108.9497");
cityInfoSource.put("西安", xiAnInfo);
Map<String, String> chengDuInfo = new HashMap<>();
chengDuInfo.put("别名", "天府之国");
chengDuInfo.put("地标", "大熊猫基地、宽窄巷子");
chengDuInfo.put("美食", "火锅、串串");
chengDuInfo.put("经纬度", "30.5728, 104.0668");
cityInfoSource.put("成都", chengDuInfo);
Map<String, String> lanZhouInfo = new HashMap<>();
lanZhouInfo.put("别名", "黄河之都");
lanZhouInfo.put("地标", "黄河铁桥、白塔山");
lanZhouInfo.put("美食", "牛肉面、甜醅子");
lanZhouInfo.put("经纬度", "36.0611, 103.8343");
cityInfoSource.put("兰州", lanZhouInfo);
Map<String, String> wuLuMuQiInfo = new HashMap<>();
wuLuMuQiInfo.put("别名", "亚心之都");
wuLuMuQiInfo.put("地标", "天山、国际大巴扎");
wuLuMuQiInfo.put("美食", "羊肉串、手抓饭");
wuLuMuQiInfo.put("经纬度", "43.8256, 87.6168");
cityInfoSource.put("乌鲁木齐", wuLuMuQiInfo);
}
@Override
public Map<String, String> crawlData(String cityName) throws Exception {
if (!cityInfoSource.containsKey(cityName)) {
throw new IllegalArgumentException("未找到" + cityName + "的特色数据");
}
System.out.println("🌐 正在爬取" + cityName + "城市特色数据");
Map<String, String> rawData = cityInfoSource.get(cityName);
rawDataMap.put(cityName, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(Map<String, String> rawData) {
// 转换为通用Map结构(可扩展解析逻辑)
Map<String, Object> parsedData = new HashMap<>(rawData);
// 新增解析字段:经纬度拆分
String latLon = (String) rawData.get("经纬度");
if (latLon != null) {
String[] latLonArr = latLon.split(",");
parsedData.put("纬度", Double.parseDouble(latLonArr[0].trim()));
parsedData.put("经度", Double.parseDouble(latLonArr[1].trim()));
}
return parsedData;
}
@Override
public void saveData(String cityName, Map<String, Object> data) {
parsedDataMap.put(cityName, data);
System.out.println("💾 " + cityName + "城市特色数据已保存,解析字段数:" + data.size());
}
@Override
protected String getApiUrl(String key) {
// 模拟API地址(实际可替换为真实城市信息API)
return "https://api.example.com/cityinfo?name=" + key;
}
// 扩展方法:打印城市特色
public void printCityInfo(String cityName) {
Map<String, Object> info = parsedDataMap.get(cityName);
if (info == null) {
System.out.println("⚠️ 未找到" + cityName + "的特色数据");
return;
}
System.out.println("\n🏙️ 【" + cityName + "】城市特色");
for (Map.Entry<String, Object> entry : info.entrySet()) {
System.out.println(" " + entry.getKey() + ":" + entry.getValue());
}
}
}
// ========== 5. 爬虫平台类(统一管理多类型爬虫) ==========
class CrawlerPlatform {
// 泛型集合:管理所有爬虫(K-爬虫类型标识,V-爬虫实例)
private Map<String, DataCrawler<?, ?>> crawlerMap = new HashMap<>();
// 泛型集合:管理所有可视化器
private Map<String, DataVisualizer<?>> visualizerMap = new HashMap<>();
// 注册爬虫
public <T, K> void registerCrawler(String crawlerType, DataCrawler<T, K> crawler) {
crawlerMap.put(crawlerType, crawler);
System.out.println("✅ 注册爬虫成功:" + crawlerType);
}
// 注册可视化器
public <T> void registerVisualizer(String visualizerType, DataVisualizer<T> visualizer) {
visualizerMap.put(visualizerType, visualizer);
System.out.println("✅ 注册可视化器成功:" + visualizerType);
}
// 执行爬虫(泛型方法)
@SuppressWarnings("unchecked")
public <T, K> void runCrawler(String crawlerType, K key) {
DataCrawler<T, K> crawler = (DataCrawler<T, K>) crawlerMap.get(crawlerType);
if (crawler == null) {
System.err.println("❌ 未找到爬虫:" + crawlerType);
return;
}
try {
// 爬取 -> 解析 -> 保存
T rawData = crawler.crawlData(key);
Map<String, Object> parsedData = crawler.parseData(rawData);
crawler.saveData(key, parsedData);
} catch (Exception e) {
((AbstractDataCrawler<T, K>) crawler).recordFailure(key, e);
}
}
// 执行可视化
@SuppressWarnings("unchecked")
public <T> void runVisualization(String visualizerType, String title, T data) {
DataVisualizer<T> visualizer = (DataVisualizer<T>) visualizerMap.get(visualizerType);
if (visualizer == null) {
System.err.println("❌ 未找到可视化器:" + visualizerType);
return;
}
visualizer.generateVisualization(title, data);
}
// 获取爬虫实例(泛型方法)
@SuppressWarnings("unchecked")
public <T, K> DataCrawler<T, K> getCrawler(String crawlerType) {
return (DataCrawler<T, K>) crawlerMap.get(crawlerType);
}
// 获取可视化器实例
@SuppressWarnings("unchecked")
public <T> DataVisualizer<T> getVisualizer(String visualizerType) {
return (DataVisualizer<T>) visualizerMap.get(visualizerType);
}
// 打印平台统计信息
public void printPlatformStats() {
System.out.println("\n📊 爬虫平台统计信息");
System.out.println(" 已注册爬虫数:" + crawlerMap.size());
System.out.println(" 已注册可视化器数:" + visualizerMap.size());
// 遍历爬虫统计数据
for (Map.Entry<String, DataCrawler<?, ?>> entry : crawlerMap.entrySet()) {
AbstractDataCrawler<?, ?> crawler = (AbstractDataCrawler<?, ?>) entry.getValue();
System.out.println(" " + entry.getKey() + ":爬取成功数=" + crawler.rawDataMap.size() + ",失败数=" + crawler.failedKeys.size());
}
}
}
// ========== 6. 主程序(平台入口) ==========
public class WeatherMain {
public static void main(String[] args) {
System.out.println("🚀 启动通用爬虫平台...\n");
// 1. 初始化平台
CrawlerPlatform platform = new CrawlerPlatform();
// 2. 注册爬虫和可视化器
WeatherCrawler weatherCrawler = new WeatherCrawler();
CityInfoCrawler cityInfoCrawler = new CityInfoCrawler();
platform.registerCrawler("weather", weatherCrawler);
platform.registerCrawler("cityInfo", cityInfoCrawler);
platform.registerVisualizer("weatherChart", weatherCrawler);
// 3. 定义待爬取的城市列表(泛型集合)
List<String> cities = new ArrayList<>(Arrays.asList("西安", "成都", "兰州", "乌鲁木齐"));
// 4. 执行城市特色爬虫
System.out.println("\n========== 执行城市特色爬虫 ==========");
for (String city : cities) {
platform.runCrawler("cityInfo", city);
cityInfoCrawler.printCityInfo(city);
}
// 5. 执行天气爬虫 + 可视化
System.out.println("\n========== 执行天气爬虫 + 可视化 ==========");
for (String city : cities) {
platform.runCrawler("weather", city);
// 获取解析后的天气数据并可视化
Map<String, Object> weatherData = weatherCrawler.parsedDataMap.get(city);
if (weatherData != null) {
Map<String, List<?>> visualData = new HashMap<>();
visualData.put("times", (List<String>) weatherData.get("times"));
visualData.put("temps", (List<Double>) weatherData.get("temps"));
platform.runVisualization("weatherChart", city, visualData);
}
}
// 6. 平台统计
platform.printPlatformStats();
// 7. 泛型方法演示:获取指定类型的解析值
System.out.println("\n========== 泛型方法演示 ==========");
Double xiAnMaxTemp = weatherCrawler.getParsedValue("西安", "maxTemp", Double.class);
String chengDuFood = cityInfoCrawler.getParsedValue("成都", "美食", String.class);
System.out.println(" 西安最高温度:" + xiAnMaxTemp + "°C");
System.out.println(" 成都特色美食:" + chengDuFood);
System.out.println("\n🎉 爬虫平台执行完毕!");
}
}