一、项目背景与痛点分析
在日常生活中,无论是日常出行、户外活动规划、校园通勤还是农业气象参考,天气查询都是不可或缺的需求。然而,当前市面上的主流天气应用和网页工具存在诸多不足,同时传统的爬虫脚本缺乏可视化界面,难以实际落地使用。具体痛点如下:
1.1 第三方平台体验问题
- 广告干扰严重:主流天气网页充斥着弹窗广告、开屏广告,还会推送无关资讯内容,用户只想查看天气却被大量无关信息干扰
- 城市切换繁琐:每次查询不同城市天气都需要重新输入或定位,高频查看的家乡、工作城市无法一键快捷访问
1.2 传统爬虫局限性
- 缺乏可视化界面:绝大多数Python天气爬虫仅能在控制台打印文本数据,没有网页界面和趋势图表,实用性较差
- 数据更新滞后:手动触发爬虫获取天气数据,无法实现后台定时自动刷新,温度、风力、降雨预警数据存在延迟
- 无历史数据回溯:无法留存过往每日温度、湿度数据,不能查看近期气温变化走势,缺乏历史天气数据复盘能力
针对以上痛点,本项目基于Python网络爬虫 + Django4.2 + ECharts可视化技术栈,搭建轻量化天气预报系统。通过调用公开免费天气API结合定向爬虫抓取气象数据,实现城市天气搜索、常用城市收藏、实时天气详情、未来7天预报、气温趋势曲线图、历史天气数据留存、后台定时自动更新等全功能模块,打造纯净无广告的私有化天气查询平台。
二、核心目标与项目定位
2.1 核心目标
搭建无广告私有化网页版天气查询平台,实现完整业务流程:
- 用户城市搜索 → 爬虫拉取实时气象数据
- 天气数据入库持久化 → 当日详情卡片展示
- 7日预报折线图可视化 → 常用城市收藏管理
- Celery定时自动刷新 → 历史天气数据回溯
2.2 项目定位
- 技术栈定位:Django结合Python爬虫的轻量化可视化Web项目
- 架构选择:采用原生MVT架构,无需前后端分离,上手难度极低
- 权限设计:区分普通用户与管理员权限,管理员可后台管理城市列表、手动刷新全站天气数据
- 成本控制:全程使用免费公开气象数据源,零调用成本
- 学习价值:适合爬虫入门+Django网页联动实战学习
2.3 设计理念
- 页面极简干净:去除所有冗余元素,专注核心功能
- 数据实时同步:确保天气信息的时效性和准确性
- 图表直观易懂:通过可视化图表提升数据可读性
- 自动化免手动:减少用户操作步骤,提升使用体验
- 无广告无推送:回归天气查询工具的本质价值
三、整体技术架构设计
3.1 分层架构流程图
#mermaid-svg-QbmOdzy21pUGbXsO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QbmOdzy21pUGbXsO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QbmOdzy21pUGbXsO .error-icon{fill:#552222;}#mermaid-svg-QbmOdzy21pUGbXsO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QbmOdzy21pUGbXsO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QbmOdzy21pUGbXsO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QbmOdzy21pUGbXsO .marker.cross{stroke:#333333;}#mermaid-svg-QbmOdzy21pUGbXsO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QbmOdzy21pUGbXsO p{margin:0;}#mermaid-svg-QbmOdzy21pUGbXsO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QbmOdzy21pUGbXsO .cluster-label text{fill:#333;}#mermaid-svg-QbmOdzy21pUGbXsO .cluster-label span{color:#333;}#mermaid-svg-QbmOdzy21pUGbXsO .cluster-label span p{background-color:transparent;}#mermaid-svg-QbmOdzy21pUGbXsO .label text,#mermaid-svg-QbmOdzy21pUGbXsO span{fill:#333;color:#333;}#mermaid-svg-QbmOdzy21pUGbXsO .node rect,#mermaid-svg-QbmOdzy21pUGbXsO .node circle,#mermaid-svg-QbmOdzy21pUGbXsO .node ellipse,#mermaid-svg-QbmOdzy21pUGbXsO .node polygon,#mermaid-svg-QbmOdzy21pUGbXsO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QbmOdzy21pUGbXsO .rough-node .label text,#mermaid-svg-QbmOdzy21pUGbXsO .node .label text,#mermaid-svg-QbmOdzy21pUGbXsO .image-shape .label,#mermaid-svg-QbmOdzy21pUGbXsO .icon-shape .label{text-anchor:middle;}#mermaid-svg-QbmOdzy21pUGbXsO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QbmOdzy21pUGbXsO .rough-node .label,#mermaid-svg-QbmOdzy21pUGbXsO .node .label,#mermaid-svg-QbmOdzy21pUGbXsO .image-shape .label,#mermaid-svg-QbmOdzy21pUGbXsO .icon-shape .label{text-align:center;}#mermaid-svg-QbmOdzy21pUGbXsO .node.clickable{cursor:pointer;}#mermaid-svg-QbmOdzy21pUGbXsO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QbmOdzy21pUGbXsO .arrowheadPath{fill:#333333;}#mermaid-svg-QbmOdzy21pUGbXsO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QbmOdzy21pUGbXsO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QbmOdzy21pUGbXsO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QbmOdzy21pUGbXsO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QbmOdzy21pUGbXsO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QbmOdzy21pUGbXsO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QbmOdzy21pUGbXsO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QbmOdzy21pUGbXsO .cluster text{fill:#333;}#mermaid-svg-QbmOdzy21pUGbXsO .cluster span{color:#333;}#mermaid-svg-QbmOdzy21pUGbXsO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QbmOdzy21pUGbXsO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QbmOdzy21pUGbXsO rect.text{fill:none;stroke-width:0;}#mermaid-svg-QbmOdzy21pUGbXsO .icon-shape,#mermaid-svg-QbmOdzy21pUGbXsO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QbmOdzy21pUGbXsO .icon-shape p,#mermaid-svg-QbmOdzy21pUGbXsO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QbmOdzy21pUGbXsO .icon-shape .label rect,#mermaid-svg-QbmOdzy21pUGbXsO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QbmOdzy21pUGbXsO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QbmOdzy21pUGbXsO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QbmOdzy21pUGbXsO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 底层技术底座
Web框架: Python3.11 + Django4.2
爬虫核心: requests + BeautifulSoup4
定时任务: Celery+Redis
前端可视化: ECharts
数据缓存: Redis
数据存储: MySQL
安全处理: UA伪装 + 频率限制
用户交互页面层
路由视图处理层
爬虫数据抓取层
数据清洗过滤层
MySQL数据持久层
ECharts可视化层
定时自动刷新层
3.2 技术栈详细清单
| 技术组件 | 具体实现 | 主要作用 |
|---|---|---|
| 后端框架 | Python 3.11、Django 4.2 | 原生MVT架构,开发部署零门槛 |
| 爬虫工具 | requests、bs4、json | 接口请求与结构化数据解析 |
| 定时调度 | Celery 5.2 + Redis | 实现每日自动更新天气数据 |
| 数据可视化 | ECharts | 多维度展示气象变化趋势 |
| 缓存优化 | Redis | 缓存高频访问城市天气,降低接口请求次数 |
| 辅助工具 | time、pytz | 时区校准,避免气象时间偏差 |
| 权限控制 | Django原生登录鉴权 | 仅登录用户可使用收藏功能 |
四、核心功能模块详解
4.1 城市天气爬虫抓取模块
功能特性
- 数据抓取范围:实时温度、体感温度、风力风向、空气湿度、气压、能见度、天气状况
- 预报数据同步:未来7天日间夜间温度、天气类型、风力变化数据
- 反爬机制规避:浏览器UA请求头伪装、请求间隔设置、单IP请求频率限制
- 数据清洗处理:自动过滤无用冗余字段,统一温度、时间格式,适配前端展示
技术实现要点
python
# 请求头伪装示例
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json",
"Accept-Language": "zh-CN,zh;q=0.9"
}
4.2 城市搜索与收藏管理模块
搜索功能
- 模糊搜索支持:支持输入城市全称/简称,智能匹配对应城市气象数据
- 实时搜索反馈:输入过程中实时显示匹配结果,提升搜索效率
收藏管理
- 个人收藏夹:登录用户可添加常用城市至个人收藏,首页一键点击切换查看
- 权限隔离机制:不同用户收藏数据相互隔离,确保数据隐私安全
- 批量操作支持:支持单条删除与批量清空收藏列表两种操作方式
4.3 天气数据可视化图表模块
图表类型与作用
-
7日气温折线图
- 展示日间最高温、夜间最低温变化曲线
- 直观查看降温升温趋势,辅助穿衣决策
-
湿度变化曲线图
- 展示一周空气湿度波动情况
- 辅助判断潮湿、干燥天气,提醒防潮防燥
-
风力风向柱状图
- 可视化展示每日风力等级
- 方便出行防风参考,特别适合户外活动规划
-
天气状况图标系统
- 晴天、雨天、多云、雪天匹配对应图标
- 提升页面视觉体验,信息传达更直观
4.4 Celery定时自动更新模块
定时任务配置
python
# Celery定时任务配置示例
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'refresh-weather-every-day': {
'task': 'weather.tasks.auto_refresh_all_city_weather',
'schedule': crontab(hour=2, minute=0), # 每日凌晨2点执行
},
}
核心功能特性
- 无人值守更新:每日凌晨2点自动爬取全网所有已收录城市最新天气数据
- 缓存智能刷新:同步清理Redis过期天气缓存,保证页面展示最新数据
- 异常重试机制:爬虫请求失败自动重试2次,保障网络波动场景下的数据更新
- 运行日志留存:后台记录每一次自动爬取时间、成功城市数量、失败城市数量
4.5 历史天气数据回溯模块
数据留存机制
- 每日快照存储:系统自动留存当日天气完整快照
- 多维度数据记录:温度、湿度、风力、天气状况等全方位记录
查询功能
- 日期选择查询:支持用户选择过往日期,回看历史气象数据
- 数据对比分析:支持多日期数据对比,查看气温变化趋势
- 月度统计报表:生成月度平均气温、降雨天数统计图表
4.6 后台城市管理模块
管理员功能
- 城市数据管理:自主新增、编辑、删除城市数据
- 手动数据刷新:一键触发全站天气数据刷新
- 运行日志查看:实时查看爬虫运行日志,快速排查问题
- 系统状态监控:监控接口请求成功率、数据完整性等关键指标
五、项目创新价值与核心亮点
5.1 用户体验优化
- 纯净无广告界面:自研网页无任何广告、资讯推送、弹窗干扰,专注天气查询核心功能
- 极速页面加载:Redis缓存优化+前端资源压缩,页面打开速度远快于第三方天气网页
5.2 技术实现创新
- 爬虫与Web深度整合:将爬虫后端与网页可视化有机结合,实现爬虫项目真正网页化落地
- 全自动化数据更新:依托Celery实现全自动无人值守更新,用户无需关心底层爬虫逻辑
- 多维度数据可视化:通过折线图、柱状图等可视化手段,将枯燥文本数据转化为直观图表
5.3 成本与实用性
- 零成本运营:全程使用免费公开气象接口,无需付费调用第三方天气API
- 高实用性设计:从个人使用到团队协作,满足不同场景下的天气查询需求
- 易部署易维护:提供Docker容器化部署方案,一键启动,降低运维门槛
六、应用场景与落地价值
6.1 个人使用场景
- 日常出行助手:本地或内网部署,作为个人专属无广告天气查询页面
- 户外活动规划:结合历史天气数据,科学规划户外活动时间
6.2 团队协作场景
- 校园气象看板:部署在校内服务器,作为公共气象大屏供师生使用
- 企业内网服务:公司内部部署,为员工提供统一的天气查询服务
- 农业气象参考:为农业生产提供历史天气数据分析和趋势预测
6.3 学习与教学价值
- 全栈开发实战:融合爬虫、定时任务、数据可视化三大热门后端技能
- Django进阶学习:涵盖MVT架构、Celery异步任务、Redis缓存等核心知识点
- 简历项目亮点:区别于常规业务管理系统,项目辨识度高,技术栈全面
七、完整代码实现与部署
7.1 项目目录结构
django-weather-crawl/
├── manage.py
├── weather_project/ # 项目全局配置目录
│ ├── settings.py # Celery、Redis缓存、爬虫请求全局配置
│ ├── urls.py # 前端页面与接口路由分发
│ └── celery.py # 每日天气自动更新定时任务配置
├── apps/ # 模块化业务应用拆分
│ ├── user_account/ # 用户登录、个人收藏管理模块
│ ├── city_manage/ # 城市基础信息、后台城市管理模块
│ ├── weather_crawl/ # 爬虫核心、数据清洗、接口请求模块
│ ├── weather_show/ # 天气页面渲染、ECharts图表数据封装模块
│ └── history_weather/ # 历史天气存储、历史数据查询模块
├── core/ # 公共工具类封装
│ ├── crawl_tool.py # 天气爬虫核心请求工具类
│ ├── data_clean.py # 气象数据格式化清洗工具
│ ├── weather_task.py # Celery定时自动爬取任务
│ └── ua_pool.py # 爬虫请求头UA池,提升反爬能力
├── static/ # ECharts图表、天气图标静态资源
├── templates/ # 天气首页、详情页、收藏页前端模板
├── requirements.txt # Python爬虫+Django全套依赖清单
└── docker-compose.yml # 一键容器化部署脚本
7.2 核心代码实现
7.2.1 天气爬虫核心工具类
python
# core/crawl_tool.py
import requests
import json
import time
from typing import Optional, Dict
from core.ua_pool import get_random_ua
# 免费公开天气API地址
WEATHER_API_URL = "https://v0.yiketianqi.com/free/day"
class WeatherCrawlTool:
"""天气数据爬虫工具类 - 负责与第三方天气API交互"""
@staticmethod
def get_city_weather(city_name: str) -> Optional[Dict]:
"""
根据城市名爬取实时天气+7日预报数据
Args:
city_name: 城市名称,支持中文城市名
Returns:
结构化天气字典数据,包含实时天气和7日预报
如果请求失败返回None
"""
headers = {
"User-Agent": get_random_ua(),
"Accept": "application/json",
"Accept-Language": "zh-CN,zh;q=0.9"
}
params = {
"appid": "demo", # 实际使用时替换为申请的appid
"appsecret": "demo", # 实际使用时替换为对应的appsecret
"city": city_name,
"version": "v9"
}
try:
# 添加请求延迟,避免触发反爬机制
time.sleep(0.5)
# 发起接口请求,设置超时时间
response = requests.get(
WEATHER_API_URL,
headers=headers,
params=params,
timeout=10
)
# 状态码检查
if response.status_code != 200:
print(f"【HTTP错误】城市:{city_name},状态码:{response.status_code}")
return None
# 解析JSON响应
res_data = json.loads(response.text)
# 业务状态码检查
if res_data.get("code") != 0:
print(f"【业务错误】城市:{city_name},错误信息:{res_data.get('msg')}")
return None
return res_data
except requests.exceptions.Timeout:
print(f"【请求超时】城市:{city_name},接口响应超时")
return None
except requests.exceptions.ConnectionError:
print(f"【连接错误】城市:{city_name},网络连接异常")
return None
except json.JSONDecodeError:
print(f"【JSON解析错误】城市:{city_name},响应数据格式异常")
return None
except Exception as e:
print(f"【未知错误】城市:{city_name},错误信息:{str(e)}")
return None
7.2.2 天气数据模型设计
python
# apps/weather_crawl/models.py
from django.db import models
from django.utils import timezone
from apps.city_manage.models import City
class RealTimeWeather(models.Model):
"""实时天气数据表 - 存储当前时刻的天气信息"""
city = models.ForeignKey(
City,
on_delete=models.CASCADE,
verbose_name="关联城市",
related_name="realtime_weather"
)
date = models.CharField(max_length=20, verbose_name="查询日期")
week = models.CharField(max_length=10, verbose_name="星期")
wea = models.CharField(max_length=20, verbose_name="天气状况")
tem_now = models.CharField(max_length=10, verbose_name="当前温度")
tem_day = models.CharField(max_length=10, verbose_name="日间最高温")
tem_night = models.CharField(max_length=10, verbose_name="夜间最低温")
win = models.CharField(max_length=30, verbose_name="风力风向")
humidity = models.CharField(max_length=10, verbose_name="空气湿度")
pressure = models.CharField(max_length=10, verbose_name="气压", default="1013")
visibility = models.CharField(max_length=10, verbose_name="能见度", default="10")
update_time = models.DateTimeField(default=timezone.now, verbose_name="数据更新时间")
class Meta:
verbose_name = "实时天气数据"
verbose_name_plural = verbose_name
ordering = ["-update_time"]
indexes = [
models.Index(fields=['city', 'date']),
models.Index(fields=['update_time']),
]
def __str__(self):
return f"{self.city.city_name} - {self.date} - {self.wea}"
class WeekWeather(models.Model):
"""七日天气预报数据表 - 存储未来7天的天气预报"""
city = models.ForeignKey(
City,
on_delete=models.CASCADE,
verbose_name="关联城市",
related_name="week_weather"
)
forecast_date = models.CharField(max_length=20, verbose_name="预报日期")
forecast_wea = models.CharField(max_length=20, verbose_name="预报天气")
forecast_tem_day = models.CharField(max_length=10, verbose_name="日间温度")
forecast_tem_night = models.CharField(max_length=10, verbose_name="夜间温度")
forecast_win = models.CharField(max_length=30, verbose_name="预报风力")
forecast_humidity = models.CharField(max_length=10, verbose_name="预报湿度", default="60")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="数据录入时间")
class Meta:
verbose_name = "七日天气预报"
verbose_name_plural = verbose_name
ordering = ["city", "forecast_date"]
unique_together = ['city', 'forecast_date']
7.2.3 Celery每日凌晨自动更新天气定时任务
python
# apps/weather_crawl/celery_tasks.py
from celery import shared_task
from apps.city_manage.models import City
from core.crawl_tool import WeatherCrawlTool
from core.data_clean import clean_weather_data
@shared_task
def auto_refresh_all_city_weather():
"""每日凌晨定时任务:自动刷新全城市天气数据"""
# 获取系统内所有收录城市
city_list = City.objects.all()
success_count = 0
fail_count = 0
for city in city_list:
# 爬虫获取原始天气数据
raw_data = WeatherCrawlTool.get_city_weather(city.city_name)
if not raw_data:
fail_count += 1
continue
# 清洗并入库天气数据
clean_weather_data(city.id, raw_data)
success_count += 1
# 返回本次定时任务执行结果
return {
"total_city": city_list.count(),
"success_refresh": success_count,
"fail_refresh": fail_count,
"task_status": "finished"
}
八、总结与展望
本篇博客全新切入Python爬虫+Django网页可视化 赛道,弥补了之前专栏缺少爬虫实战项目的空白,依托免费公开天气接口,从零实现自动化天气爬虫、网页展示、图表可视化、定时自动更新全套功能,全程无付费接口、无复杂第三方依赖,新手极易上手。
项目融合爬虫反爬处理、Django模型设计、Celery定时任务、Redis缓存优化、ECharts前端图表渲染、用户收藏权限隔离六大核心知识点,区别于常规业务管理系统,兼具爬虫脚本能力与Web项目落地能力,作为简历项目辨识度极高,同时和专栏过往8个项目场景、代码完全无重复。
后续迭代规划
- 新增天气预警推送功能,暴雨、大风、寒潮恶劣天气自动站内消息提醒;
- 增加生活指数推荐,根据天气自动推送穿衣、洗车、运动、出行建议;
- 接入IP自动定位,用户打开页面自动获取本地城市天气,无需手动搜索;
- 增加月度天气统计报表,生成月度平均气温、降雨天数统计图表。