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.
 
 

1383 lines
34 KiB

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* ==========================================================
*
* 功能:
* 1. 天气爬虫
* 2. 空气质量爬虫
* 3. 日出日落爬虫
* 4. 湿度爬虫
* 5. 风速风向爬虫
*
* 技术:
* √ Java面向对象
* √ 抽象类
* √ 泛型
* √ 多态
* √ MVC思想
* √ Command模式
* √ Strategy模式
* √ 数据可视化
* √ 文件持久化
* ==========================================================
*/
/* ==========================================================
1. 自定义异常
========================================================== */
class CrawlerException extends Exception {
public CrawlerException(String message) {
super(message);
}
public CrawlerException(String message, Throwable cause) {
super(message, cause);
}
}
/* ==========================================================
2. 泛型接口
========================================================== */
interface DataCrawler<T, K> {
T crawlData(K key) throws CrawlerException;
Map<String, Object> parseData(T rawData);
void saveData(K key, Map<String, Object> data)
throws CrawlerException;
}
interface DataVisualizer<T> {
void generateVisualization(String title, T data);
}
/* ==========================================================
3. 保存策略
========================================================== */
interface SaveStrategy {
void save(String fileName, String content)
throws IOException;
}
class TxtSaveStrategy implements SaveStrategy {
@Override
public void save(String fileName, String content)
throws IOException {
File folder = new File("data");
if (!folder.exists()) {
folder.mkdirs();
}
File file = new File(folder, fileName);
try (BufferedWriter writer =
new BufferedWriter(
new FileWriter(file)
)) {
writer.write(content);
}
}
}
/* ==========================================================
4. 抽象爬虫父类
========================================================== */
abstract class AbstractDataCrawler<T, K>
implements DataCrawler<T, K> {
protected Map<K, T> rawDataMap =
new HashMap<>();
protected Map<K, Map<String, Object>>
parsedDataMap =
new LinkedHashMap<>();
protected SaveStrategy saveStrategy =
new TxtSaveStrategy();
protected String doHttpGet(String url)
throws CrawlerException {
try {
HttpClient client =
HttpClient.newBuilder()
.connectTimeout(
Duration.ofSeconds(10)
)
.build();
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 CrawlerException(
"HTTP请求失败:"
+ response.statusCode()
);
}
return response.body();
} catch (Exception e) {
throw new CrawlerException(
"网络请求异常",
e
);
}
}
protected abstract String getApiUrl(K key);
}
/* ==========================================================
5. 城市坐标工具类
========================================================== */
class CityCoordinateUtil {
public static final Map<String, String[]>
CITY_MAP =
new LinkedHashMap<>();
static {
CITY_MAP.put(
"西安",
new String[]{"34.2644", "108.9497"}
);
CITY_MAP.put(
"成都",
new String[]{"30.5728", "104.0668"}
);
CITY_MAP.put(
"兰州",
new String[]{"36.0611", "103.8343"}
);
CITY_MAP.put(
"乌鲁木齐",
new String[]{"43.8256", "87.6168"}
);
}
}
/* ==========================================================
6. 天气爬虫
========================================================== */
class WeatherCrawler
extends AbstractDataCrawler<String, String>
implements DataVisualizer<Map<String, List<?>>> {
@Override
public String crawlData(String city)
throws CrawlerException {
System.out.println(
"🌤️ 正在爬取天气:" + city
);
String rawData =
doHttpGet(getApiUrl(city));
rawDataMap.put(city, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(
String rawData
) {
Map<String, Object> result =
new HashMap<>();
List<Double> temps =
new ArrayList<>();
Matcher matcher =
Pattern.compile(
"\"temperature_2m\":\\[([^\\]]+)]"
).matcher(rawData);
if (matcher.find()) {
String[] arr =
matcher.group(1).split(",");
for (String s : arr) {
try {
temps.add(
Double.parseDouble(
s.trim()
)
);
} catch (Exception ignored) {
}
}
}
result.put("temps", temps);
return result;
}
@Override
public void saveData(
String city,
Map<String, Object> data
) throws CrawlerException {
parsedDataMap.put(city, data);
try {
saveStrategy.save(
city + "_weather.txt",
data.toString()
);
} catch (IOException e) {
throw new CrawlerException(
"天气保存失败",
e
);
}
}
@Override
protected String getApiUrl(String city) {
String[] latLon =
CityCoordinateUtil.CITY_MAP.get(city);
return String.format(
"https://api.open-meteo.com/v1/forecast?"
+ "latitude=%s&longitude=%s"
+ "&hourly=temperature_2m",
latLon[0],
latLon[1]
);
}
@Override
public void generateVisualization(
String city,
Map<String, List<?>> data
) {
ChartUtil.drawLineChart(
city + " 温度折线图",
(List<Double>) data.get("temps"),
"data/" + city + "_weather.png",
Color.RED
);
}
}
/* ==========================================================
7. 空气质量爬虫
========================================================== */
class AirQualityCrawler
extends AbstractDataCrawler<String, String>
implements DataVisualizer<Map<String, List<?>>> {
@Override
public String crawlData(String city)
throws CrawlerException {
System.out.println(
"🌫️ 正在爬取AQI:" + city
);
String rawData =
doHttpGet(getApiUrl(city));
rawDataMap.put(city, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(
String rawData
) {
Map<String, Object> result =
new HashMap<>();
Matcher matcher =
Pattern.compile("\"aqi\":(\\d+)")
.matcher(rawData);
int aqi =
matcher.find()
? Integer.parseInt(
matcher.group(1)
)
: -1;
List<Double> list =
new ArrayList<>();
list.add((double) aqi);
result.put("aqi", list);
return result;
}
@Override
public void saveData(
String city,
Map<String, Object> data
) throws CrawlerException {
parsedDataMap.put(city, data);
try {
saveStrategy.save(
city + "_aqi.txt",
data.toString()
);
} catch (IOException e) {
throw new CrawlerException(
"AQI保存失败",
e
);
}
}
@Override
protected String getApiUrl(String city) {
String[] latLon =
CityCoordinateUtil.CITY_MAP.get(city);
return "https://api.waqi.info/feed/geo:"
+ latLon[0]
+ ";"
+ latLon[1]
+ "/?token=demo";
}
@Override
public void generateVisualization(
String city,
Map<String, List<?>> data
) {
ChartUtil.drawBarChart(
city + " AQI柱状图",
(List<Double>) data.get("aqi"),
"data/" + city + "_aqi.png",
Color.BLUE
);
}
}
/* ==========================================================
8. 日出日落爬虫
========================================================== */
class SunriseCrawler
extends AbstractDataCrawler<String, String>
implements DataVisualizer<Map<String, List<?>>> {
@Override
public String crawlData(String city)
throws CrawlerException {
System.out.println(
"🌅 正在爬取日出日落:" + city
);
String rawData =
doHttpGet(getApiUrl(city));
rawDataMap.put(city, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(
String rawData
) {
Map<String, Object> result =
new HashMap<>();
Matcher sunriseMatcher =
Pattern.compile(
"\"sunrise\":\"([^\"]+)\""
).matcher(rawData);
Matcher sunsetMatcher =
Pattern.compile(
"\"sunset\":\"([^\"]+)\""
).matcher(rawData);
String sunrise =
sunriseMatcher.find()
? sunriseMatcher.group(1)
: "";
String sunset =
sunsetMatcher.find()
? sunsetMatcher.group(1)
: "";
double duration =
calculateHours(
sunrise,
sunset
);
List<Double> list =
new ArrayList<>();
list.add(duration);
result.put("duration", list);
return result;
}
private double calculateHours(
String sunrise,
String sunset
) {
try {
int sunriseHour =
Integer.parseInt(
sunrise.substring(11, 13)
);
int sunsetHour =
Integer.parseInt(
sunset.substring(11, 13)
);
return sunsetHour - sunriseHour;
} catch (Exception e) {
return 0;
}
}
@Override
public void saveData(
String city,
Map<String, Object> data
) throws CrawlerException {
parsedDataMap.put(city, data);
try {
saveStrategy.save(
city + "_sunrise.txt",
data.toString()
);
} catch (IOException e) {
throw new CrawlerException(
"日出日落保存失败",
e
);
}
}
@Override
protected String getApiUrl(String city) {
String[] latLon =
CityCoordinateUtil.CITY_MAP.get(city);
return "https://api.sunrise-sunset.org/json?"
+ "lat="
+ latLon[0]
+ "&lng="
+ latLon[1]
+ "&formatted=0";
}
@Override
public void generateVisualization(
String city,
Map<String, List<?>> data
) {
ChartUtil.drawBarChart(
city + " 日照时长图",
(List<Double>) data.get("duration"),
"data/" + city + "_sunrise.png",
Color.ORANGE
);
}
}
/* ==========================================================
9. 湿度爬虫
========================================================== */
class HumidityCrawler
extends AbstractDataCrawler<String, String>
implements DataVisualizer<Map<String, List<?>>> {
@Override
public String crawlData(String city)
throws CrawlerException {
System.out.println(
"💧 正在爬取湿度:" + city
);
String rawData =
doHttpGet(getApiUrl(city));
rawDataMap.put(city, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(
String rawData
) {
Map<String, Object> result =
new HashMap<>();
List<Double> humidityList =
new ArrayList<>();
Matcher matcher =
Pattern.compile(
"\"relativehumidity_2m\":\\[([^\\]]+)]"
).matcher(rawData);
if (matcher.find()) {
String[] arr =
matcher.group(1).split(",");
for (String s : arr) {
try {
humidityList.add(
Double.parseDouble(
s.trim()
)
);
} catch (Exception ignored) {
}
}
}
result.put("humidity", humidityList);
return result;
}
@Override
public void saveData(
String city,
Map<String, Object> data
) throws CrawlerException {
parsedDataMap.put(city, data);
try {
saveStrategy.save(
city + "_humidity.txt",
data.toString()
);
} catch (IOException e) {
throw new CrawlerException(
"湿度保存失败",
e
);
}
}
@Override
protected String getApiUrl(String city) {
String[] latLon =
CityCoordinateUtil.CITY_MAP.get(city);
return String.format(
"https://api.open-meteo.com/v1/forecast?"
+ "latitude=%s&longitude=%s"
+ "&hourly=relativehumidity_2m",
latLon[0],
latLon[1]
);
}
@Override
public void generateVisualization(
String city,
Map<String, List<?>> data
) {
ChartUtil.drawLineChart(
city + " 湿度折线图",
(List<Double>) data.get("humidity"),
"data/" + city + "_humidity.png",
Color.CYAN
);
}
}
/* ==========================================================
10. 风速风向爬虫
========================================================== */
class WindCrawler
extends AbstractDataCrawler<String, String>
implements DataVisualizer<Map<String, List<?>>> {
@Override
public String crawlData(String city)
throws CrawlerException {
System.out.println(
"💨 正在爬取风速风向:" + city
);
String rawData =
doHttpGet(getApiUrl(city));
rawDataMap.put(city, rawData);
return rawData;
}
@Override
public Map<String, Object> parseData(
String rawData
) {
Map<String, Object> result =
new HashMap<>();
List<Double> windList =
new ArrayList<>();
Matcher matcher =
Pattern.compile(
"\"windspeed_10m\":\\[([^\\]]+)]"
).matcher(rawData);
if (matcher.find()) {
String[] arr =
matcher.group(1).split(",");
for (String s : arr) {
try {
windList.add(
Double.parseDouble(
s.trim()
)
);
} catch (Exception ignored) {
}
}
}
result.put("wind", windList);
return result;
}
@Override
public void saveData(
String city,
Map<String, Object> data
) throws CrawlerException {
parsedDataMap.put(city, data);
try {
saveStrategy.save(
city + "_wind.txt",
data.toString()
);
} catch (IOException e) {
throw new CrawlerException(
"风速保存失败",
e
);
}
}
@Override
protected String getApiUrl(String city) {
String[] latLon =
CityCoordinateUtil.CITY_MAP.get(city);
return String.format(
"https://api.open-meteo.com/v1/forecast?"
+ "latitude=%s&longitude=%s"
+ "&hourly=windspeed_10m",
latLon[0],
latLon[1]
);
}
@Override
public void generateVisualization(
String city,
Map<String, List<?>> data
) {
ChartUtil.drawLineChart(
city + " 风速折线图",
(List<Double>) data.get("wind"),
"data/" + city + "_wind.png",
Color.MAGENTA
);
}
}
/* ==========================================================
11. 图表工具类(核心修改部分)
========================================================== */
class ChartUtil {
// 坐标轴字体样式
private static final Font AXIS_FONT = new Font("微软雅黑", Font.PLAIN, 12);
// 刻度间隔(控制标注密度)
private static final int X_AXIS_TICK_INTERVAL = 5;
private static final int Y_AXIS_TICK_COUNT = 6;
public static void drawLineChart(
String title,
List<Double> data,
String path,
Color color
) {
drawChart(
title,
data,
path,
color,
true
);
}
public static void drawBarChart(
String title,
List<Double> data,
String path,
Color color
) {
drawChart(
title,
data,
path,
color,
false
);
}
private static void drawChart(
String title,
List<Double> data,
String path,
Color color,
boolean lineMode
) {
try {
int width = 1000;
int height = 500;
BufferedImage image =
new BufferedImage(
width,
height,
BufferedImage.TYPE_INT_RGB
);
Graphics2D g =
image.createGraphics();
// 开启抗锯齿,让文字和线条更清晰
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 1. 绘制背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 2. 绘制标题
g.setColor(Color.BLACK);
g.setFont(new Font("微软雅黑", Font.BOLD, 24));
g.drawString(title, 320, 40);
// 3. 绘制坐标轴
g.drawLine(80, 400, 900, 400); // X轴
g.drawLine(80, 80, 80, 400); // Y轴
// 4. 计算数据范围
double maxValue = data.stream().mapToDouble(Double::doubleValue).max().orElse(1);
double minValue = data.stream().mapToDouble(Double::doubleValue).min().orElse(0);
// 保证Y轴从0开始(更符合常规图表逻辑)
minValue = Math.min(minValue, 0);
double valueRange = maxValue - minValue;
// 避免除零
if (valueRange == 0) valueRange = 1;
// 5. 绘制坐标轴标注(核心新增逻辑)
drawAxisLabels(g, data, minValue, maxValue, valueRange);
// 6. 绘制数据图形(折线/柱状图)
g.setColor(color);
int prevX = 0;
int prevY = 0;
for (int i = 0; i < data.size(); i++) {
int x = 100 + i * 30;
// 修正Y轴计算逻辑(适配minValue)
int y = (int) (400 - ((data.get(i) - minValue) / valueRange * 250));
if (lineMode) {
// 折线图:绘制数据点和连线
g.fillOval(x - 3, y - 3, 6, 6);
if (i > 0) {
g.drawLine(prevX, prevY, x, y);
}
prevX = x;
prevY = y;
} else {
// 柱状图:绘制柱子
g.fillRect(x - 10, y, 20, 400 - y);
}
}
// 7. 释放资源并保存图片
g.dispose();
ImageIO.write(image, "png", new File(path));
System.out.println("📈 图表生成成功:" + path);
} catch (Exception e) {
System.err.println("❌ 图表生成失败:" + e.getMessage());
}
}
/**
* 绘制坐标轴标注(面向对象封装:单一职责)
* @param g 绘图上下文
* @param data 原始数据
* @param minValue 数据最小值
* @param maxValue 数据最大值
* @param valueRange 数据范围(max - min)
*/
private static void drawAxisLabels(Graphics2D g, List<Double> data, double minValue, double maxValue, double valueRange) {
// 设置坐标轴字体
g.setFont(AXIS_FONT);
g.setColor(Color.DARK_GRAY);
// ===== 绘制Y轴标注(数值刻度)=====
double yTickStep = valueRange / (Y_AXIS_TICK_COUNT - 1);
for (int i = 0; i < Y_AXIS_TICK_COUNT; i++) {
// 计算刻度值
double tickValue = minValue + (yTickStep * i);
// 计算刻度在图片中的Y坐标
int yPos = (int) (400 - (i * 250 / (Y_AXIS_TICK_COUNT - 1)));
// 绘制刻度线
g.drawLine(75, yPos, 80, yPos);
// 绘制刻度文字(保留1位小数,保证可读性)
String label = String.format("%.1f", tickValue);
// 文字左对齐,居中显示
FontMetrics fm = g.getFontMetrics();
int textX = 70 - fm.stringWidth(label);
int textY = yPos + (fm.getAscent() / 2);
g.drawString(label, textX, textY);
}
// ===== 绘制X轴标注(索引刻度)=====
for (int i = 0; i < data.size(); i += X_AXIS_TICK_INTERVAL) {
// 计算刻度在图片中的X坐标
int xPos = 100 + i * 30;
// 绘制刻度线
g.drawLine(xPos, 400, xPos, 405);
// 绘制刻度文字(数据索引)
String label = String.valueOf(i);
FontMetrics fm = g.getFontMetrics();
int textX = xPos - (fm.stringWidth(label) / 2);
int textY = 400 + fm.getAscent() + 5;
g.drawString(label, textX, textY);
}
// ===== 绘制坐标轴名称 =====
g.setFont(new Font("微软雅黑", Font.PLAIN, 14));
// X轴名称(数据点索引)
g.drawString("数据点索引", 450, 450);
// Y轴名称(数值)
// 旋转文字:Y轴名称垂直显示
g.rotate(Math.toRadians(-90), 40, 240);
g.drawString("数值", 40, 240);
// 恢复旋转角度
g.rotate(Math.toRadians(90), 40, 240);
}
}
/* ==========================================================
12. Command模式
========================================================== */
interface Command {
void execute();
}
abstract class AbstractCommand
implements Command {
protected CrawlerPlatform platform;
protected String city;
public AbstractCommand(
CrawlerPlatform platform,
String city
) {
this.platform = platform;
this.city = city;
}
}
class WeatherCommand extends AbstractCommand {
public WeatherCommand(
CrawlerPlatform platform,
String city
) {
super(platform, city);
}
@Override
public void execute() {
platform.runCrawler("weather", city);
}
}
class AQICommand extends AbstractCommand {
public AQICommand(
CrawlerPlatform platform,
String city
) {
super(platform, city);
}
@Override
public void execute() {
platform.runCrawler("aqi", city);
}
}
class SunriseCommand extends AbstractCommand {
public SunriseCommand(
CrawlerPlatform platform,
String city
) {
super(platform, city);
}
@Override
public void execute() {
platform.runCrawler("sunrise", city);
}
}
class HumidityCommand extends AbstractCommand {
public HumidityCommand(
CrawlerPlatform platform,
String city
) {
super(platform, city);
}
@Override
public void execute() {
platform.runCrawler("humidity", city);
}
}
class WindCommand extends AbstractCommand {
public WindCommand(
CrawlerPlatform platform,
String city
) {
super(platform, city);
}
@Override
public void execute() {
platform.runCrawler("wind", city);
}
}
/* ==========================================================
13. 平台控制器
========================================================== */
class CrawlerPlatform {
private final Map<String,
DataCrawler<?, ?>>
crawlerMap =
new HashMap<>();
public <T, K> void registerCrawler(
String type,
DataCrawler<T, K> crawler
) {
crawlerMap.put(type, crawler);
System.out.println(
"✅ 注册爬虫:" + type
);
}
@SuppressWarnings("unchecked")
public <T, K> void runCrawler(
String type,
K key
) {
try {
DataCrawler<T, K> crawler =
(DataCrawler<T, K>)
crawlerMap.get(type);
T rawData =
crawler.crawlData(key);
Map<String, Object> parsedData =
crawler.parseData(rawData);
crawler.saveData(
key,
parsedData
);
if (crawler instanceof DataVisualizer) {
((DataVisualizer<Map<String, List<?>>>)
crawler)
.generateVisualization(
key.toString(),
convertMap(parsedData)
);
}
} catch (Exception e) {
System.err.println(
"❌ 爬虫执行失败:"
+ e.getMessage()
);
}
}
private Map<String, List<?>> convertMap(
Map<String, Object> map
) {
Map<String, List<?>> result =
new HashMap<>();
for (String key : map.keySet()) {
if (map.get(key) instanceof List<?>) {
result.put(
key,
(List<?>) map.get(key)
);
}
}
return result;
}
}
/* ==========================================================
14. CLI视图
========================================================== */
class CLIView {
public void showMenu() {
System.out.println("\n==============================");
System.out.println(" 城市环境综合数据爬虫平台");
System.out.println("==============================");
System.out.println("1. 天气爬虫");
System.out.println("2. 空气质量爬虫");
System.out.println("3. 日出日落爬虫");
System.out.println("4. 湿度爬虫");
System.out.println("5. 风速风向爬虫");
System.out.println("6. 执行全部功能");
System.out.println("0. 退出系统");
System.out.println("==============================");
}
}
/* ==========================================================
15. 主程序
========================================================== */
public class WeatherMain {
public static void main(String[] args) {
System.out.println(
"🚀 系统启动时间:"
+ LocalDateTime.now()
.format(
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss"
)
)
);
Scanner scanner =
new Scanner(System.in);
CLIView view =
new CLIView();
CrawlerPlatform platform =
new CrawlerPlatform();
platform.registerCrawler(
"weather",
new WeatherCrawler()
);
platform.registerCrawler(
"aqi",
new AirQualityCrawler()
);
platform.registerCrawler(
"sunrise",
new SunriseCrawler()
);
platform.registerCrawler(
"humidity",
new HumidityCrawler()
);
platform.registerCrawler(
"wind",
new WindCrawler()
);
List<String> cities =
new ArrayList<>(
CityCoordinateUtil.CITY_MAP.keySet()
);
while (true) {
view.showMenu();
System.out.print("请输入操作:");
int choice =
scanner.nextInt();
if (choice == 0) {
System.out.println(
"👋 系统退出"
);
break;
}
switch (choice) {
case 1:
for (String city : cities) {
new WeatherCommand(
platform,
city
).execute();
}
break;
case 2:
for (String city : cities) {
new AQICommand(
platform,
city
).execute();
}
break;
case 3:
for (String city : cities) {
new SunriseCommand(
platform,
city
).execute();
}
break;
case 4:
for (String city : cities) {
new HumidityCommand(
platform,
city
).execute();
}
break;
case 5:
for (String city : cities) {
new WindCommand(
platform,
city
).execute();
}
break;
case 6:
for (String city : cities) {
new WeatherCommand(
platform,
city
).execute();
new AQICommand(
platform,
city
).execute();
new SunriseCommand(
platform,
city
).execute();
new HumidityCommand(
platform,
city
).execute();
new WindCommand(
platform,
city
).execute();
}
break;
default:
System.out.println(
"⚠️ 输入错误"
);
}
}
scanner.close();
System.out.println(
"🎉 系统运行结束"
);
}
}