diff --git a/w2/WeatherMain.java b/w2/WeatherMain.java new file mode 100644 index 0000000..bbfe0d2 --- /dev/null +++ b/w2/WeatherMain.java @@ -0,0 +1,529 @@ +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🎉 爬虫平台执行完毕!"); + } +} \ No newline at end of file diff --git a/w2/乌鲁木齐_温度图.png b/w2/乌鲁木齐_温度图.png new file mode 100644 index 0000000..65e1d65 Binary files /dev/null and b/w2/乌鲁木齐_温度图.png differ diff --git a/w2/兰州_温度图.png b/w2/兰州_温度图.png new file mode 100644 index 0000000..f5134a7 Binary files /dev/null and b/w2/兰州_温度图.png differ diff --git a/w2/成都_温度图.png b/w2/成都_温度图.png new file mode 100644 index 0000000..814f96c Binary files /dev/null and b/w2/成都_温度图.png differ diff --git a/w2/西安_温度图.png b/w2/西安_温度图.png new file mode 100644 index 0000000..7f07596 Binary files /dev/null and b/w2/西安_温度图.png differ