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 crawlData(K key) throws Exception; // 解析数据 Map parseData(T rawData); // 保存数据 void saveData(K key, Map data); } /** * 通用数据可视化接口(泛型:T-可视化数据类型) */ interface DataVisualizer { void generateVisualization(String title, T data); } // ========== 2. 抽象泛型父类:爬虫基类 ========== abstract class AbstractDataCrawler implements DataCrawler { // 泛型集合:存储爬取的原始数据(K-标识,T-原始数据) protected Map rawDataMap = new HashMap<>(); // 泛型集合:存储解析后的结构化数据(K-标识,Map-结构化数据) protected Map> parsedDataMap = new LinkedHashMap<>(); // 泛型集合:存储爬取失败的标识 protected List 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 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 getParsedValue(K key, String field, Class 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 implements DataVisualizer>> { // 城市经纬度映射(泛型集合) private Map 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 parseData(String rawData) { Map parsedData = new HashMap<>(); try { List times = parseTimes(rawData); List 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 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 parseTimes(String json) { List 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 parseTemperatures(String json) { List 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> data) { List times = (List) data.get("times"); List temps = (List) 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, String> { // 模拟城市特色数据(实际可替换为真实爬虫逻辑) private Map> cityInfoSource = new HashMap<>(); public CityInfoCrawler() { // 初始化城市特色数据 Map xiAnInfo = new HashMap<>(); xiAnInfo.put("别名", "十三朝古都"); xiAnInfo.put("地标", "兵马俑、大雁塔"); xiAnInfo.put("美食", "肉夹馍、泡馍"); xiAnInfo.put("经纬度", "34.2644, 108.9497"); cityInfoSource.put("西安", xiAnInfo); Map chengDuInfo = new HashMap<>(); chengDuInfo.put("别名", "天府之国"); chengDuInfo.put("地标", "大熊猫基地、宽窄巷子"); chengDuInfo.put("美食", "火锅、串串"); chengDuInfo.put("经纬度", "30.5728, 104.0668"); cityInfoSource.put("成都", chengDuInfo); Map lanZhouInfo = new HashMap<>(); lanZhouInfo.put("别名", "黄河之都"); lanZhouInfo.put("地标", "黄河铁桥、白塔山"); lanZhouInfo.put("美食", "牛肉面、甜醅子"); lanZhouInfo.put("经纬度", "36.0611, 103.8343"); cityInfoSource.put("兰州", lanZhouInfo); Map wuLuMuQiInfo = new HashMap<>(); wuLuMuQiInfo.put("别名", "亚心之都"); wuLuMuQiInfo.put("地标", "天山、国际大巴扎"); wuLuMuQiInfo.put("美食", "羊肉串、手抓饭"); wuLuMuQiInfo.put("经纬度", "43.8256, 87.6168"); cityInfoSource.put("乌鲁木齐", wuLuMuQiInfo); } @Override public Map crawlData(String cityName) throws Exception { if (!cityInfoSource.containsKey(cityName)) { throw new IllegalArgumentException("未找到" + cityName + "的特色数据"); } System.out.println("🌐 正在爬取" + cityName + "城市特色数据"); Map rawData = cityInfoSource.get(cityName); rawDataMap.put(cityName, rawData); return rawData; } @Override public Map parseData(Map rawData) { // 转换为通用Map结构(可扩展解析逻辑) Map 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 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 info = parsedDataMap.get(cityName); if (info == null) { System.out.println("⚠️ 未找到" + cityName + "的特色数据"); return; } System.out.println("\n🏙️ 【" + cityName + "】城市特色"); for (Map.Entry entry : info.entrySet()) { System.out.println(" " + entry.getKey() + ":" + entry.getValue()); } } } // ========== 5. 爬虫平台类(统一管理多类型爬虫) ========== class CrawlerPlatform { // 泛型集合:管理所有爬虫(K-爬虫类型标识,V-爬虫实例) private Map> crawlerMap = new HashMap<>(); // 泛型集合:管理所有可视化器 private Map> visualizerMap = new HashMap<>(); // 注册爬虫 public void registerCrawler(String crawlerType, DataCrawler crawler) { crawlerMap.put(crawlerType, crawler); System.out.println("✅ 注册爬虫成功:" + crawlerType); } // 注册可视化器 public void registerVisualizer(String visualizerType, DataVisualizer visualizer) { visualizerMap.put(visualizerType, visualizer); System.out.println("✅ 注册可视化器成功:" + visualizerType); } // 执行爬虫(泛型方法) @SuppressWarnings("unchecked") public void runCrawler(String crawlerType, K key) { DataCrawler crawler = (DataCrawler) crawlerMap.get(crawlerType); if (crawler == null) { System.err.println("❌ 未找到爬虫:" + crawlerType); return; } try { // 爬取 -> 解析 -> 保存 T rawData = crawler.crawlData(key); Map parsedData = crawler.parseData(rawData); crawler.saveData(key, parsedData); } catch (Exception e) { ((AbstractDataCrawler) crawler).recordFailure(key, e); } } // 执行可视化 @SuppressWarnings("unchecked") public void runVisualization(String visualizerType, String title, T data) { DataVisualizer visualizer = (DataVisualizer) visualizerMap.get(visualizerType); if (visualizer == null) { System.err.println("❌ 未找到可视化器:" + visualizerType); return; } visualizer.generateVisualization(title, data); } // 获取爬虫实例(泛型方法) @SuppressWarnings("unchecked") public DataCrawler getCrawler(String crawlerType) { return (DataCrawler) crawlerMap.get(crawlerType); } // 获取可视化器实例 @SuppressWarnings("unchecked") public DataVisualizer getVisualizer(String visualizerType) { return (DataVisualizer) visualizerMap.get(visualizerType); } // 打印平台统计信息 public void printPlatformStats() { System.out.println("\n📊 爬虫平台统计信息"); System.out.println(" 已注册爬虫数:" + crawlerMap.size()); System.out.println(" 已注册可视化器数:" + visualizerMap.size()); // 遍历爬虫统计数据 for (Map.Entry> 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 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 weatherData = weatherCrawler.parsedDataMap.get(city); if (weatherData != null) { Map> visualData = new HashMap<>(); visualData.put("times", (List) weatherData.get("times")); visualData.put("temps", (List) 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🎉 爬虫平台执行完毕!"); } }