每天打开电脑第一件事查天气?翻手机 APP 总觉得不够方便?其实用 PyQt5 写一个桌面版天气预报工具并不难 —— 不需要复杂的框架,几行代码就能实现界面交互,结合爬虫技术还能实时获取天气数据。
哇哇 今天就带大家手把手拆解一个实用的桌面天气预报程序,从界面设计到数据爬取,从线程优化到异常处理,让你看懂每一行代码的作用,轻松入门 PyQt5 开发。
🔥🔥🔥🔥哇哇 提醒:文章结尾附项目完整代码,安装好对应需求库,复制代码即可运行🔥🔥🔥🔥
一、先看效果:这个程序能做什么?
运行程序后,你会看到一个简洁的窗口:顶部是输入框和查询按钮,中间是天气数据表格,底部有状态提示。输入支持的城市名(比如 “北京”“成都”),点击 “查询天气”,表格就会显示未来 7 天的日期、最高温度、最低温度和天气状况,而且会根据天气类型自动变色 —— 雨天是浅蓝色,晴天是浅黄色,多云是浅灰色,直观又好看。
最关键的是,查询过程中界面不会卡顿,网络超时或输入错误时会弹出提示,完全像一个 “正经” 的桌面软件。这个看似简单的工具,其实包含了 PyQt5 界面设计、多线程处理、网络爬虫、数据解析等多个知识点,非常适合作为 PyQt5 入门实战案例。
二、准备工作:3 个核心库搞定开发
开发前先准备好 “工具箱”,这个程序需要三个关键库:
- PyQt5:用于搭建桌面界面,提供窗口、按钮、表格等各种控件,是整个程序的 “骨架”。
- requests:作为网络爬虫的 “跑腿员”,负责从天气网站下载数据。
- BeautifulSoup:解析网页内容的 “过滤器”,能从杂乱的 HTML 中精准提取温度、日期等信息。
如果你的电脑还没安装这些库,打开终端输入三行命令即可:
pip install pyqt5 -i https://mirrors.aliyun.com/pypi/simple/
pip install requests -i https://mirrors.aliyun.com/pypi/simple/
pip install beautifulsoup4 -i https://mirrors.aliyun.com/pypi/simple/
!!!!滴滴:不知道怎么安装第三方库或者想换用国内镜像源加速下载,可以点击链接跳转推荐学习文章↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
极速配置!Anaconda换用国内pip镜像源,一劳永逸解决下载龟速!
安装完成后,就可以开始写代码了。
三、核心代码拆解:从界面到功能的实现逻辑
1. 城市代码字典:给城市编个 “身份证号”
程序开头有一个CITY_CODES
字典,里面存着城市名和对应的数字编码,比如'北京': '101010100'
。这串数字可不是随便编的 ——
中国天气网的每个城市天气页面都有固定 URL:http://www.weather.com.cn/weather/城市编码.shtml
,比如北京的天气页就是http://www.weather.com.cn/weather/101010100.shtml
。
这个字典的作用就像 “城市通讯录”,当用户输入 “北京” 时,程序能快速找到对应的编码,拼出正确的网页地址,为后续爬取数据做准备。
2. WeatherWorker:用线程解决界面卡顿问题
如果你直接在界面按钮点击事件里写网络请求,会发现一个尴尬的问题:查询时窗口会卡住,按钮点不动、窗口拖不动,像 “死机” 一样。这是因为 PyQt5 的界面渲染和用户操作在同一个主线程里,网络请求这种耗时操作会阻塞主线程。
解决办法是用QThread
创建后台线程,让网络请求在 “后台” 运行,不影响界面响应。这就是WeatherWorker
类的作用:
class WeatherWorker(QThread):
finished = pyqtSignal(dict, str) # 成功信号:天气数据和城市名
error = pyqtSignal(str) # 错误信号:错误信息
def run(self):
# 网络请求和数据解析代码
# ...
- 信号(Signal):线程和主线程的 “信使”。查询成功时,
finished
信号会把天气数据传给主线程;出错时,error
信号会发送错误信息。 - run 方法:线程启动后自动执行的函数,里面封装了从网络请求到数据解析的全过程。这样耗时操作被放在后台,界面就能保持流畅。
3. 网络爬取与数据解析:从网页中 “挖” 出天气信息
run
方法里藏着获取天气数据的核心逻辑,主要分三步:
第一步:发送网络请求
用requests.get
访问天气网页,为了避免被网站拒绝,需要设置headers
模拟浏览器:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/86.0.4240.198...'
}
url = f'http://www.weather.com.cn/weather/{citycode}.shtml'
res = requests.get(url, headers=headers, timeout=20)
timeout=20
表示如果 20 秒内没拿到数据,就触发超时错误,避免程序无限等待。
第二步:解析 HTML 内容
拿到网页内容后,用BeautifulSoup
提取需要的信息。打开天气网页面源码会发现,温度、日期、天气状况分别藏在特定标签里:
- 温度在
p class="tem"
标签中 - 日期在
ul class="t clearfix"
下的h1
标签中 - 天气状况在
p class="wea"
标签中
所以代码里用find_all
精准定位这些标签
soup = BeautifulSoup(res.text, 'html.parser')
tem_list = soup.find_all('p', class_='tem') # 温度列表
day_list = soup.find('ul', class_='t clearfix').find_all('h1') # 日期列表
wealist = soup.find_all('p', class_='wea') # 天气状况列表
第三步:处理特殊情况
有时候网页中某一天的最高温度会缺失(比如当天只显示最低温),这时候直接提取会报错。代码用try-except
处理这种情况:
try:
temHigh = tem_list[i].span.string # 尝试获取最高温
except AttributeError:
# 当天没有最高温时,用第二天的代替(最后一天则显示N/A)
temHigh = tem_list[i + 1].span.string if i < 6 else "N/A"
这种 “容错处理” 能让程序更稳定,不会因为数据格式小变化就崩溃。
4. WeatherApp:搭建美观又好用的界面
界面是程序的 “脸面”,WeatherApp
类负责把各种控件(输入框、按钮、表格)组合成一个完整窗口,主要做了三件事:
第一步:设计布局
用 PyQt5 的布局管理器(QVBoxLayout
纵向布局、QHBoxLayout
横向布局)排列控件,避免手动调整位置的麻烦:
- 顶部放标题标签
- 中间用横向布局放输入框和查询按钮
- 下面是状态提示标签
- 核心区域是表格控件
- 底部放数据来源说明
这样无论窗口怎么拉伸,控件都会自动调整位置,保持美观。
第二步:美化界面
通过setStyleSheet
设置 CSS 样式,让界面摆脱默认的 “原生丑陋”:
self.setStyleSheet("""
QPushButton {
background-color: #4a86e8;
color: white;
border-radius: 5px;
}
QPushButton:hover {
background-color: #3a76d8; /* 鼠标悬停时变色 */
}
""")
按钮的圆角、悬停效果,表格的网格线颜色,甚至不同天气的背景色,都是通过样式表实现的,让程序看起来更专业。
第三步:绑定事件与信号
界面控件需要和功能逻辑关联,比如点击 “查询按钮” 要触发查询操作,线程返回数据后要更新表格。这就需要用到 PyQt5 的 “信号与槽” 机制:
# 按钮点击时,调用get_weather方法
self.search_btn.clicked.connect(self.get_weather)
# 线程查询成功时,调用display_weather显示数据
self.worker.finished.connect(self.display_weather)
# 线程出错时,调用show_error显示错误
self.worker.error.connect(self.show_error)
这种 “松耦合” 的设计让界面和逻辑分离,便于后续修改和扩展。
5. 数据展示:让天气信息清晰呈现
display_weather
方法负责把解析好的天气数据填充到表格中:
- 先设置表格行数为 7(对应 7 天数据)
- 循环将日期、最高温、最低温、天气状况分别放入表格单元格
- 根据天气类型设置单元格背景色(雨天蓝色、晴天黄色等)
表格设置为 “不可编辑”(setEditTriggers(QTableWidget.NoEditTriggers)
),避免用户误操作;列宽设置为 “自适应”(QHeaderView.Stretch
),确保在不同屏幕上都能完整显示。
四、细节设计:让程序更 “抗造” 的小技巧
一个好用的程序不仅要能正常工作,还要能应对各种意外情况,这个程序里有几个值得学习的细节:
- 防止重复点击:查询时禁用按钮(
self.search_btn.setEnabled(False)
),避免用户连续点击导致多个线程同时运行,查询完成后再恢复可用。 - 详细的异常处理:网络超时、城市不存在、网页结构变化等情况都有对应的错误提示,用户知道哪里出了问题,而不是一脸懵。
- 状态反馈:查询中显示 “正在获取数据”,成功后显示城市名,出错时显示错误信息,让用户清楚当前状态。
- 输入校验:用户没输入城市或输入不支持的城市时,用
QMessageBox
弹窗提示,避免无效请求。
五、扩展方向:让你的程序更强大
学会这个基础版本后,你可以试着给程序加功能,比如:
- 添加更多城市:在
CITY_CODES
字典中补充更多城市的编码(可以从中国天气网查询)。 - 增加实时天气:爬取当天的实时温度、风力等信息,在表格上方显示。
- 历史数据对比:将每天的天气数据保存到本地文件,生成温度变化曲线图。
- 设置提醒:当预报有雨时,自动弹出 “带伞提醒”。
六、总结:PyQt5 开发不难,动手做才是关键
这个天气预报程序虽然简单,但涵盖了 PyQt5 开发的核心知识点:界面布局、控件使用、信号与槽、多线程、样式美化,还结合了网络爬虫和数据解析的技巧。其实 PyQt5 入门并不难,关键是多动手写代码,遇到问题时逐行调试(比如打印res.text
看网页内容,打印soup
看解析结果)。
试着自己运行一下代码,输入你所在的城市,看看能不能正确显示天气?如果遇到报错,仔细看看错误信息指向哪一行,慢慢排查 —— 编程能力就是在解决问题的过程中一点点提升的。
当你能用代码做出一个每天都能用到的工具时,那种成就感远比背知识点来得强烈。现在就动手试试,让你的电脑桌面多一个自己写的实用工具吧!
附:本项目完整代码
已调试好,安装好依赖库后可以直接运行
当前版本仅支持全国所有省份省会城市以及特定城市的天气查询,若要增添城市,可在
CITY_CODES 定义的字典里按模板添加城市与其对应的代码
import sys # 用于Python解释器相关操作,如命令行参数、退出等
import requests # 用于发送HTTP网络请求
from bs4 import BeautifulSoup # 用于解析和提取HTML页面内容
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QLabel, QMessageBox) # PyQt5界面相关控件
from PyQt5.QtCore import Qt, QThread, pyqtSignal # Qt常用枚举、线程类、信号机制
from PyQt5.QtGui import QFont, QColor, QPalette # 字体、颜色、调色板相关
# 城市代码字典,城市名称 -> weather.com.cn的城市编码
CITY_CODES = {
# 直辖市
'北京': '101010100',
'上海': '101020100',
'天津': '101030100',
'重庆': '101040100',
# 省会
'石家庄': '101090101',
'太原': '101100101',
'沈阳': '101070101',
'长春': '101060101',
'哈尔滨': '101050101',
'南京': '101190101',
'杭州': '101210101',
'合肥': '101220101',
'福州': '101230101',
'南昌': '101240101',
'济南': '101120101',
'郑州': '101180101',
'武汉': '101200101',
'长沙': '101250101',
'广州': '101280101',
'海口': '101310101',
'成都': '101270101',
'贵阳': '101260101',
'昆明': '101290101',
'西安': '101110101',
'兰州': '101160101',
'西宁': '101150101',
'台北': '101340101',
# 自治区首府
'呼和浩特': '101080101',
'南宁': '101300101',
'拉萨': '101140101',
'银川': '101170101',
'乌鲁木齐': '101130101',
# 特别行政区
'香港': '101320101',
'澳门': '101330101',
# 其他重要城市
'桂林': '101300501',
'柳州': '101300301',
}
class WeatherWorker(QThread):
"""后台线程处理天气数据获取,防止阻塞界面主线程"""
finished = pyqtSignal(dict, str) # 查询完成信号,参数:天气数据和城市名
error = pyqtSignal(str) # 查询出错信号,参数:错误信息
def __init__(self, city_name):
super().__init__() # 初始化父类QThread
self.city_name = city_name # 需要查询天气的城市名
def run(self):
"""线程开始运行后执行的函数"""
try:
citycode = CITY_CODES.get(self.city_name) # 获取城市编码
if not citycode:
self.error.emit(f"未找到城市: {self.city_name}") # 没找到城市
return
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/86.0.4240.198 Safari/537.36'
}
url = f'http://www.weather.com.cn/weather/{citycode}.shtml' # 拼接网页URL
# 发送HTTP GET请求
res = requests.get(url, headers=headers, timeout=20)
res.encoding = 'utf-8' # 指定编码
# 检查HTTP响应状态码
if res.status_code != 200:
self.error.emit(f"网络错误: HTTP {res.status_code}")
return
# 用BeautifulSoup解析HTML文档
soup = BeautifulSoup(res.text, 'html.parser')
# 找到包含温度的所有<p class="tem">标签
tem_list = soup.find_all('p', class_='tem')
# 找到包含日期的<ul class="t clearfix">下面的所有<h1>标签
day = soup.find('ul', class_='t clearfix')
day_list = day.find_all('h1')
# 找到包含天气描述的所有<p class="wea">标签
wealist = soup.find_all('p', class_='wea')
# 用于存储7天的天气数据
weather_data = {}
for i in range(7): # 解析7天数据
try:
# 最高温度可能不存在(如当天只有最低温),做特殊处理
try:
temHigh = tem_list[i].span.string # 最高温
except AttributeError:
temHigh = tem_list[i + 1].span.string if i < 6 else "N/A" # 兼容特殊情况
temLow = tem_list[i].i.string # 最低温
wea = wealist[i].string # 天气描述
# 存入字典,日期为key
weather_data[day_list[i].string] = {
'最高温度': temHigh,
'最低温度': temLow,
'天气': wea
}
except Exception as e:
self.error.emit(f"解析错误: {str(e)}") # 解析失败
return
# 查询成功,发出信号
self.finished.emit(weather_data, self.city_name)
except requests.exceptions.Timeout:
self.error.emit("请求超时,请重试") # 超时错误
except requests.exceptions.RequestException as e:
self.error.emit(f"网络请求失败: {str(e)}") # 其他请求错误
except Exception as e:
self.error.emit(f"未知错误: {str(e)}") # 其他未知错误
class WeatherApp(QMainWindow):
"""主应用程序窗口类,继承自QMainWindow"""
def __init__(self):
super().__init__() # 初始化父类
self.initUI() # 初始化界面
self.worker = None # 后台线程初始化为None
def initUI(self):
"""设置主窗口和界面布局"""
self.setWindowTitle("天气预报查询") # 窗口标题
self.setGeometry(300, 300, 800, 600) # 窗口位置和大小
# 设置整体界面样式
self.setStyleSheet("""
QMainWindow {
background-color: #f0f8ff;
}
QPushButton {
background-color: #4a86e8;
color: white;
border-radius: 5px;
padding: 8px 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a76d8;
}
QLineEdit {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
QTableWidget {
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
gridline-color: #e0e0e0;
}
QHeaderView::section {
background-color: #e3f2fd;
padding: 8px;
border: none;
font-weight: bold;
}
""")
# 创建主部件和布局
main_widget = QWidget() # 主体widget
self.setCentralWidget(main_widget) # 设置为中心部件
main_layout = QVBoxLayout() # 纵向布局
main_widget.setLayout(main_layout) # 应用布局
# 标题标签
title = QLabel("7天天气预报查询")
title_font = QFont("Arial", 18, QFont.Bold)
title.setFont(title_font)
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("color: #1a5276; margin: 20px 0;")
main_layout.addWidget(title)
# 输入区域布局
input_layout = QHBoxLayout()
self.city_input = QLineEdit() # 城市输入框
self.city_input.setPlaceholderText("输入城市名称 (如: 北京)")
self.city_input.setMinimumHeight(40)
input_layout.addWidget(self.city_input)
self.search_btn = QPushButton("查询天气") # 查询按钮
self.search_btn.setMinimumHeight(40)
self.search_btn.clicked.connect(self.get_weather) # 按钮点击事件绑定
input_layout.addWidget(self.search_btn)
main_layout.addLayout(input_layout) # 输入区加入主布局
# 状态标签,用于显示提示信息
self.status_label = QLabel("准备查询...")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #7f8c8d; font-style: italic; margin: 10px 0;")
main_layout.addWidget(self.status_label)
# 天气表格
self.weather_table = QTableWidget() # 表格控件
self.weather_table.setColumnCount(4) # 4列:日期、最高温、最低温、天气
self.weather_table.setHorizontalHeaderLabels(["日期", "最高温度", "最低温度", "天气"])
self.weather_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) # 列宽自适应
self.weather_table.setEditTriggers(QTableWidget.NoEditTriggers) # 不可编辑
self.weather_table.verticalHeader().setVisible(False) # 行头不可见
self.weather_table.setStyleSheet("""
QTableWidget::item {
padding: 12px;
}
""")
main_layout.addWidget(self.weather_table)
# 底部信息标签
footer = QLabel("数据来源: 中国天气网")
footer.setAlignment(Qt.AlignCenter)
footer.setStyleSheet("color: #95a5a6; margin-top: 20px;")
main_layout.addWidget(footer)
def get_weather(self):
"""点击查询按钮时启动天气查询流程"""
city_name = self.city_input.text().strip() # 获取输入城市名
if not city_name:
QMessageBox.warning(self, "输入错误", "请输入城市名称") # 未输入城市
return
if city_name not in CITY_CODES:
QMessageBox.warning(self, "城市错误", "该城市不在支持列表中") # 城市不支持
return
# 禁用按钮,防止重复点击
self.search_btn.setEnabled(False)
self.status_label.setText("正在查询天气数据...")
self.status_label.setStyleSheet("color: #3498db;")
# 清空表格历史数据
self.weather_table.setRowCount(0)
# 创建并启动后台线程
self.worker = WeatherWorker(city_name)
self.worker.finished.connect(self.display_weather) # 成功信号连接
self.worker.error.connect(self.show_error) # 错误信号连接
self.worker.start() # 启动线程
def display_weather(self, weather_data, city_name):
"""显示查询到的天气数据到表格"""
self.search_btn.setEnabled(True) # 恢复按钮可用
self.status_label.setText(f"{city_name} 7天天气预报")
self.status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self.weather_table.setRowCount(len(weather_data)) # 设置表行数
# 填充表格
for row, (date, data) in enumerate(weather_data.items()):
# 日期
date_item = QTableWidgetItem(date)
date_item.setTextAlignment(Qt.AlignCenter)
self.weather_table.setItem(row, 0, date_item)
# 最高温度
high_temp_item = QTableWidgetItem(f"{data['最高温度']}℃")
high_temp_item.setTextAlignment(Qt.AlignCenter)
self.weather_table.setItem(row, 1, high_temp_item)
# 最低温度
low_temp_item = QTableWidgetItem(f"{data['最低温度']}℃")
low_temp_item.setTextAlignment(Qt.AlignCenter)
self.weather_table.setItem(row, 2, low_temp_item)
# 天气描述
weather_item = QTableWidgetItem(data['天气'])
weather_item.setTextAlignment(Qt.AlignCenter)
self.weather_table.setItem(row, 3, weather_item)
# 根据天气类型设置单元格背景色
if "雨" in data['天气']:
weather_item.setBackground(QColor(220, 237, 255)) # 蓝色
elif "晴" in data['天气']:
weather_item.setBackground(QColor(255, 245, 220)) # 黄色
elif "云" in data['天气']:
weather_item.setBackground(QColor(240, 240, 240)) # 灰色
def show_error(self, message):
"""显示错误信息到界面和弹窗"""
self.search_btn.setEnabled(True) # 恢复按钮
self.status_label.setText(message) # 状态提示
self.status_label.setStyleSheet("color: #e74c3c;")
QMessageBox.critical(self, "错误", message) # 弹窗显示错误
if __name__ == "__main__":
app = QApplication(sys.argv) # 创建应用对象,传入命令行参数
window = WeatherApp() # 创建主窗口实例
window.show() # 显示主窗口
sys.exit(app.exec_()) # 启动事件循环,主程序入口
从模仿到原创,再到潜能激发
每一个优秀的开发者都是从复制粘贴开始的
勤加练习,终有一天你也能写出改变世界的代码
转载自CSDN-专业IT技术社区