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
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🎉 爬虫平台执行完毕!");
|
|
}
|
|
}
|