一、白嫖EC和GFS预报数据
EC的openData部分公开了一部分预报数据,作为普通用户只能访问这些免费预报数据,具体位置在这
可以发现,由于是Open Data,我们只能获得临近四天的预报结果,虽然时间较短,但是我们可以通过每天都执行脚本储存在本地 ,日积月累就可以存储完整的EC预报数据
同样的,NCEP的GFS数据也是只提供近几天的数据 ,因此也是需要一直爬取才能获取连续的预报数据
注意:该网站的gfs数据是包含41层数据,如果想要选择变量和高度层的话请访问这里
1.1 脚本核心思路
- 本质上是使用Python的request库通过拼接好的url发送get请求,然后每天执行一次,就可以爬取最新的数据保存下来
- 我们点进去下载目录中点击一个文件下载 ,按F12调试中可以从"网络"中抓包下载时候请求的url
- 详细看一下这个url,可以发现是由几个变量拼接起来的,包括:预报时长(forecast_hour), 起报时间(start_hour),基本url(base_url),分辨率(resolution)当然这几个名字是我自己取的,跟后面代码中的相对应
- 这样只需要我们在脚本中指定好这几个参数,之后按一定规律拼接url,并发送get请求,就可以相应下载
1.2 脚本编写中的面向对象思路(模板方法设计模式)
- 在编写过程中发现两种数据下载的逻辑完全相同,只有使用的base_url和文件名拼接规则不同
- 将核心功能和公共功能封装为基类
- 两个继承了基类 的具体的实现类只需要根据不同的数据下载需求将filename和url的拼接规则函数重写即可
- 启示:在python中用子类不实现该方法就会NotImplementedError 的方式来实现接口,将不同的逻辑用接口的形式让子类实现,实现多态
- 后来想起来好像无意中运用了一个之前见过的设计模式,模板方法设计模式,基类规定好算法骨架,部分会随着实现类变化的部分的实现方式延迟到子类中实现
1.3 基类ForecastDownloader类
ForecastDownloader作为基类,其核心方法是start和download,是控制整个下载流程的函数,同时有两个抽象方法需要子类实现,这样做的目的是面对不同的下载需求,其实只有一小部分在变化,将拼接方法延迟到子类中实现就好了
python
import requests
import os
from datetime import datetime, timedelta
from urllib.parse import urljoin
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../utils')))
from utils.logger import Logger
class ForecastDownloader:
"""
下载天气预报数据的基类。
这个类定义了天气预报数据下载器的基础功能,包括文件的下载和保存。
子类应该实现 `construct_filename` 和 `construct_download_url` 方法来提供具体的文件名构造和下载 URL 构造。
属性:
forecast_hour (list): 预报小时列表,例如 [0, 6, 12, 18]。
start_hour (str): 预报开始小时,例如 '00'。
save_dir (str): 文件保存目录。
base_url (str): 基础下载 URL。
previous_days (int): 需要下载的前几天数据。
log_savepath (str): 日志记录器路径。
方法:
start(): 开始下载数据,遍历指定的日期范围。
download(time): 根据时间下载数据。
construct_filename(time, hour): 构造文件名。子类需实现。
construct_download_url(time, filename): 构造下载 URL。子类需实现。
"""
def __init__(self, forecast_hour, start_hour, save_dir, base_url, previous_days, log_savepath):
self.save_dir = save_dir
self.forecast_hour = forecast_hour
self.start_hour = start_hour
self.base_url = base_url
self.previous_days = previous_days
self.logger = Logger.setup_logger(log_savepath)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
def start(self):
"""
开始下载数据,遍历指定的日期范围。
这个方法会从当前日期向过去的日期循环,并调用 `download` 方法来下载数据。
"""
today = datetime.now().date()
for i in range(self.previous_days):
date = today - timedelta(days=i)
self.logger.info(str(date))
time_str = f'{date.year}{date.month:02}{date.day:02}'
self.download(time_str)
def download(self, time):
"""
根据时间下载数据。
这个方法会下载指定时间的数据,并保存到指定目录。
参数:
time (str): 数据的时间字符串,例如 '20240721'。
"""
for hour in self.forecast_hour:
filename = self.construct_filename(time, hour)
if not os.path.exists(os.path.join(self.save_dir, time, filename)):
url = self.construct_download_url(time, filename)
try:
response = requests.get(url, verify=False)
if response.status_code == 200:
self.logger.info(f"Downloading {url}")
day_dir = os.path.join(self.save_dir, time)
if not os.path.exists(day_dir):
os.makedirs(day_dir)
save_path = os.path.join(self.save_dir, time, filename)
with open(save_path, 'wb') as f:
f.write(response.content)
self.logger.info(f"Finished downloading {url}")
else:
self.logger.error(f"Failed to download {url}: Status code {response.status_code}")
except Exception as e:
self.logger.error(f"Error downloading {url}: {e}")
else:
self.logger.info('data exists')
def construct_filename(self, time, hour):
"""
构造文件名。
这个方法应该在子类中实现,用于构造特定格式的文件名。
参数:
time (str): 数据的时间字符串。
hour (int): 预报小时。
返回:
str: 构造出的文件名。
抛出:
NotImplementedError: 子类必须实现此方法。
"""
raise NotImplementedError("This method should be overridden by subclasses")
def construct_download_url(self, time, filename):
"""
构造下载 URL。
这个方法应该在子类中实现,用于构造特定格式的下载 URL。
参数:
time (str): 数据的时间字符串。
filename (str): 文件名。
返回:
str: 构造出的下载 URL。
抛出:
NotImplementedError: 子类必须实现此方法。
"""
raise NotImplementedError("This method should be overridden by subclasses")
1.4 子类EcForecastDownloader类
- 继承自ForecastDownloader类 ,只需要把baserl改一下和重写两个拼接文件名和url的方法,其他逻辑与基类完全一致,这样就轻松实现了代码复用
python
class EcForecastDownloader(ForecastDownloader):
def __init__(self, forecast_hour, start_hour, save_dir, previous_days, log_savepath='../logs/ecLog.log'):
base_url = "https://data.ecmwf.int/forecasts/"
super().__init__(forecast_hour, start_hour, save_dir, base_url, previous_days, log_savepath)
def construct_filename(self, time, forecast_hour):
return f'{time}{self.start_hour}0000-{forecast_hour}h-oper-fc.grib2'
def construct_download_url(self, time, filename):
return urljoin(self.base_url, f"{time}/{self.start_hour}z/ifs/0p25/oper/{filename}")
1.5 子类NecpForecastDownloader类
python
class NecpForecastDownloader(ForecastDownloader):
def __init__(self, forecast_hour, start_hour, save_dir, previous_days, log_savepath='../logs/ncepLog.log'):
base_url = "https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/"
super().__init__(forecast_hour, start_hour, save_dir, base_url, previous_days, log_savepath)
def construct_filename(self, time, forecast_hour):
return f'gfs.t{self.start_hour}z.pgrb2.0p25.f0{forecast_hour}'
def construct_download_url(self, time, filename):
return f'{self.base_url}/gfs.{time}/{self.start_hour}/atmos/{filename}'
1.6 日志类
python
import logging
import os
class Logger:
@staticmethod
def setup_logger(log_file):
if not os.path.exists('../logs'):
os.makedirs('../logs')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# 文件处理器
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.INFO)
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
return logger
1.7 控制部分
将下载类和具体的使用逻辑分离,只要指定好构造函数的几个参量,之后间隔一定的小时数之后一直执行下载代码,这样就可以实现不间断更新数据
python
from forecast_downloader import EcForecastDownloader, NecpForecastDownloader
import time
def download_ec(interval_hour):
save_dir = '../download_res/ec'
D = EcForecastDownloader([24, 48, 72], '00', save_dir, 4)
while True:
D.start()
time.sleep(interval_hour * 60 * 60)
def download_ncep(interval_hour):
save_dir = '../download_res/ncep'
D = NecpForecastDownloader([24, 48, 72], '00', save_dir, 4)
while True:
D.start()
time.sleep(interval_hour * 60 * 60)
if __name__ == '__main__':
download_ec(24)
1.8 使用时的目录结构
- 三个Downloader类都放在了forecasr_downloader.py中
- 控制部分放在start_downloading.py中
- 日志类放在utils下面
1.9 挂服务器上后台运行
- 传入服务器后,直接后台运行,只要线程不被杀,就可以一直将最新的数据自动下载
bash
nohup python3 start_downloading.py &
二、小节
- 上述脚本适用于一切需要定时执行拼接url来自动下载最新数据的数据爬取
- 如果再来一个数据下载需求,只需要创建一个类,继承基类后重写拼接规则以及base url即可,代码的可拓展性和复用性较强
- 一开始也是直接写,不断抽象为各个方法,类,类与类之间再抽象出基类,通过合理的抽象和设计模式的运用使代码优雅,复用性强WMF