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
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(
|
|
"🎉 系统运行结束"
|
|
);
|
|
}
|
|
}
|