简化版天气爬虫教程
1. 项目介绍
本教程将详细介绍如何使用Python开发一个简化版的天气数据爬虫,用于爬取乌兰察布的天气信息,包括当前天气、未来7天预报和24小时预报数据。
1.1 功能说明
- 爬取当前天气数据(温度、天气状况、湿度、风向等)
- 爬取未来7天天气预报数据(日期、星期、天气状况、温度范围等)
- 爬取24小时天气预报数据(时间、温度等)
- 将爬取的数据保存到CSV文件中
1.2 技术栈
- Python 3.12+:开发语言
- httpx:发送HTTP请求,获取网页内容
- BeautifulSoup4:解析HTML,提取数据
- pandas:数据处理和保存
2. 开发环境准备
2.1 安装Python
确保您的系统已安装Python 3.12或更高版本。您可以从Python官网下载并安装。
2.2 安装依赖库
bash
# 创建虚拟环境(可选但推荐)
python -m venv .venv
# 激活虚拟环境
# Windows: .venv\Scripts\activate
# Linux/Mac: source .venv/bin/activate
# 安装依赖库
pip install httpx beautifulsoup4 pandas
2.3 项目结构
weather/
├── simplified_spiders/
│ ├── simple_weather_spider.py # 简化版天气爬虫
│ └── weather_spider_tutorial.md # 本教程
└── data/
└── spider_data/ # 数据保存目录
3. 核心功能设计
3.1 类设计
我们将采用面向对象的设计方式,创建一个WeatherSpider类来封装所有爬取功能:
| 方法名 | 功能描述 |
|---|---|
__init__ |
初始化爬虫对象 |
fetch_main_page |
获取主页面内容 |
fetch_forecast_page |
获取7天预报页面内容(实际未使用) |
parse_current_weather |
解析当前天气数据 |
parse_forecast_7days |
解析未来7天预报数据 |
parse_hourly_24h |
解析24小时预报数据 |
save_to_csv |
保存数据到CSV文件 |
run |
执行完整的爬取流程 |
3.2 数据流向
- 发送HTTP请求获取网页内容
- 使用BeautifulSoup解析HTML
- 提取所需的天气数据
- 将数据保存到CSV文件
4. 网页结构分析(重点)
4.1 目标网站
4.2 使用浏览器开发者工具分析网页
在开始编写爬虫之前,我们需要先了解目标网站的HTML结构,确定数据所在的位置。您可以使用浏览器的开发者工具来分析网页:
- 打开目标网站
- 右键点击页面,选择"检查"或"审查元素"
- 使用"选择元素"工具(通常是左上角的箭头图标)点击您想要分析的数据
- 在开发者工具的"元素"标签中查看对应的HTML结构
4.3 当前天气数据结构
当前天气数据位于class="weather_info"的div容器内:
html
<div class="weather_info">
<dd class="weather">
<span class="now">
<b>当前温度</b>
</span>
<span>天气状况-温度范围</span>
</dd>
<!-- 其他天气信息 -->
</div>
- 当前温度 :位于
class="now b"的标签中 - 天气状况 :位于
class="weather"的dd标签内的span标签中 - 湿度和风向 :包含在整个
weather_info容器的文本内容中
4.4 7天预报数据结构
7天预报数据分布在多个相邻元素中,这是我们需要重点分析的部分:
-
日期和星期 :位于最后一个
class="week"的ul元素中html<ul class="week"> <li><b>12-26</b><span>星期五</span><img src="..."/></li> <li><b>12-27</b><span>星期六</span><img src="..."/></li> <!-- 其他日期 --> </ul> -
天气状况 :位于紧邻日期星期元素的下一个
class="txt txt2"的ul元素中html<ul class="txt txt2"> <li>多云</li> <li>晴转多云</li> <!-- 其他天气状况 --> </ul> -
温度数据 :位于
class="zxt_shuju"的div容器内的ul元素中html<div class="zxt_shuju" style="display: none;"> <ul> <li><span>-3</span><b>-13</b></li> <!-- 最高温度/最低温度 --> <li><span>-2</span><b>-14</b></li> <!-- 其他温度数据 --> </ul> </div>注意:这个div元素可能设置了
display: none,但它仍然包含完整的数据。
4.5 24小时预报数据结构
24小时预报数据分布在两个不同的容器中:
-
时间数据 :位于
class="txt canvas_hour"的ul元素中html<ul class="txt canvas_hour"> <li>00:00</li> <li>02:00</li> <!-- 其他时间 --> </ul> -
温度数据 :位于
class="zxt_shuju1"的div容器内的ul元素中html<div class="zxt_shuju1"> <ul> <li><span>-12℃</span></li> <li><span>-13℃</span></li> <!-- 其他温度 --> </ul> </div>
5. 代码实现
5.1 导入所需库
python
import httpx # 用于发送HTTP请求
from bs4 import BeautifulSoup # 用于解析HTML
import pandas as pd # 用于数据处理和保存
from datetime import datetime # 用于处理日期和时间
import os # 用于文件和目录操作
5.2 配置常量
python
# 配置常量 - 可根据需要修改
CITY = "乌兰察布" # 城市名称
CITY_CODE = "wulanchabu" # 城市代码,用于构建URL
BASE_URL = "https://www.tianqi.com" # 网站基础URL
MAIN_URL = f"{BASE_URL}/{CITY_CODE}/" # 主页面URL
FORECAST_URL = f"{BASE_URL}/{CITY_CODE}/7/" # 7天预报页面URL(存在403限制,实际未使用)
# 请求头配置 - 模拟浏览器请求,避免被反爬
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Referer": "https://www.tianqi.com/" # 来源页,增加请求可信度
}
# 数据保存目录 - 确保目录存在
DATA_DIR = "data/spider_data"
os.makedirs(DATA_DIR, exist_ok=True) # 若目录不存在则创建
# 当前日期 - 用于数据标识
CURRENT_DATE = datetime.now().strftime("%Y-%m-%d") # 格式:年-月-日
5.3 WeatherSpider类实现
python
class WeatherSpider:
"""简化版天气爬虫类 - 封装所有爬取功能"""
def __init__(self):
"""初始化爬虫对象"""
self.main_soup = None # 主页面的BeautifulSoup对象
self.forecast_soup = None # 7天预报页面的BeautifulSoup对象(实际未使用)
def fetch_main_page(self):
"""获取主页面内容"""
try:
# 发送HTTP GET请求,获取主页面HTML
response = httpx.get(MAIN_URL, headers=HEADERS, follow_redirects=True, timeout=30)
response.raise_for_status() # 检查HTTP响应状态码
# 将HTML转换为BeautifulSoup对象,便于解析
self.main_soup = BeautifulSoup(response.text, "html.parser")
return True # 获取成功
except Exception as e:
print(f"获取主页面失败: {e}") # 打印错误信息
return False # 获取失败
def parse_current_weather(self):
"""解析当前天气数据 - 从主页面提取"""
if not self.main_soup: # 检查主页面是否已获取
return None
try:
# 初始化当前天气数据字典
current = {
"城市": CITY, # 城市名称
"日期": CURRENT_DATE, # 当前日期
"时间": datetime.now().strftime("%H:%M") # 当前时间
}
# 1. 查找主要天气容器 - class="weather_info"的div
weather_info = self.main_soup.select_one(".weather_info")
if not weather_info: # 如果未找到该容器,返回None
return None
# 2. 查找天气详情容器 - class="weather"的dd
weather_dd = weather_info.select_one(".weather")
if weather_dd: # 如果找到该容器
# 解析当前温度 - class="now b"的标签
temp_b = weather_dd.select_one(".now b")
current["温度"] = f"{temp_b.text.strip()}℃" if temp_b else "未知"
# 解析天气状况 - 查找span标签
weather_span = weather_dd.select_one("span")
if weather_span: # 如果找到span标签
weather_text = weather_span.text.strip() # 示例:"多云-13 ~ -3℃"
# 提取天气状况(如"多云-13 ~ -3℃" -> "多云")
weather_status = weather_text.split("-")[0].strip()
current["天气状况"] = weather_status # 天气状况
current["温度范围"] = weather_text # 完整温度范围
# 3. 解析其他天气信息(湿度、风向等)
all_text = weather_info.text.strip() # 获取整个天气容器的文本内容
# 提取湿度 - 从文本中查找"湿度:"关键字
if "湿度:" in all_text:
# 分割文本,获取湿度值
humidity = all_text.split("湿度:")[1].split()[0]
current["湿度"] = f"湿度:{humidity}"
# 提取风向 - 查找风向关键字
wind_keywords = ["西风", "东风", "南风", "北风", "西南风", "西北风", "东南风", "东北风"]
for keyword in wind_keywords:
if keyword in all_text: # 如果找到风向关键字
current["风向"] = f"风向:{keyword}"
break
return current # 返回当前天气数据
except Exception as e: # 捕获所有异常
print(f"解析当前天气失败: {e}") # 打印错误信息
return None # 返回None表示解析失败
def parse_forecast_7days(self):
"""解析未来7天天气预报 - 从主页面提取"""
try:
forecast_list = [] # 初始化7天预报数据列表
# 仅从主页面获取7天预报数据,因为7天预报页面有403限制
if self.main_soup: # 检查主页面是否已获取
# 1. 获取日期和星期信息
week_uls = self.main_soup.find_all("ul", class_="week")
if not week_uls: # 如果未找到,返回空列表
return forecast_list
# 获取最后一个class="week"的ul元素,它包含7天的日期和星期
week_ul = week_uls[-1]
# 2. 获取天气状况信息
# 获取week_ul的下一个兄弟元素,它应该是包含天气状况的ul
txt_ul = week_ul.find_next_sibling()
# 检查下一个兄弟元素是否为ul,否则设为None
if txt_ul and txt_ul.name != "ul":
txt_ul = None
# 3. 获取温度数据
# 查找class="zxt_shuju"的div,它包含7天的温度数据
zxt_shuju_div = self.main_soup.find("div", class_="zxt_shuju")
# 获取div内的ul元素,它包含具体的温度数据
temp_ul = zxt_shuju_div.find("ul") if zxt_shuju_div else None
# 如果找到了所有必要的元素,开始解析数据
if week_ul and txt_ul and temp_ul:
# 获取所有的日期和星期li标签
week_lis = week_ul.find_all("li")
# 获取所有的天气状况li标签
txt_lis = txt_ul.find_all("li")
# 获取所有的温度li标签
temp_lis = temp_ul.find_all("li")
# 确保所有列表的长度一致,避免索引越界
min_length = min(len(week_lis), len(txt_lis), len(temp_lis))
# 遍历所有列表,解析每天的预报数据
for i in range(min_length):
try:
week_li = week_lis[i] # 日期和星期li
txt_li = txt_lis[i] # 天气状况li
temp_li = temp_lis[i] # 温度li
# 解析日期和星期
date_b = week_li.find("b") # 日期在b标签中
week_span = week_li.find("span") # 星期在span标签中
# 初始化单日预报数据字典
forecast = {
"城市": CITY, # 城市名称
"日期": date_b.text.strip() if date_b else "未知", # 日期(如"12-26")
"星期": week_span.text.strip() if week_span else "未知", # 星期(如"星期五")
"天气状况": txt_li.text.strip() if txt_li else "未知" # 天气状况(如"多云")
}
# 解析温度数据
max_temp_span = temp_li.find("span") # 最高温度在span标签中
min_temp_b = temp_li.find("b") # 最低温度在b标签中
if max_temp_span and min_temp_b: # 如果找到温度标签
# 提取并转换温度值为整数
max_temp = int(max_temp_span.text.strip())
min_temp = int(min_temp_b.text.strip())
forecast["最高温度"] = max_temp # 最高温度
forecast["最低温度"] = min_temp # 最低温度
forecast["温度范围"] = f"{min_temp}~{max_temp}℃" # 温度范围
else: # 如果未找到温度标签,使用默认值
forecast["最高温度"] = 0
forecast["最低温度"] = 0
forecast["温度范围"] = "未知"
# 风向和风力信息在主页面中未直接显示,暂时设为未知
forecast["风向"] = "未知"
forecast["风力"] = "未知"
forecast_list.append(forecast) # 将单日预报添加到列表
except Exception as e: # 捕获单天解析异常
print(f"解析单天预报失败: {e}") # 打印错误信息
continue # 继续解析下一天
return forecast_list # 返回7天预报数据列表
except Exception as e: # 捕获所有异常
print(f"解析7天预报失败: {e}") # 打印错误信息
return [] # 返回空列表表示解析失败
def parse_hourly_24h(self):
"""解析24小时天气预报 - 从主页面提取"""
if not self.main_soup: # 检查主页面是否已获取
return []
try:
hourly_list = [] # 初始化24小时预报数据列表
# 1. 查找温度数据容器 - class="zxt_shuju1"的div
zxt_shuju1_div = self.main_soup.find("div", class_="zxt_shuju1")
if not zxt_shuju1_div: # 如果未找到该容器,返回空列表
return hourly_list
# 2. 查找时间数据容器 - class="txt canvas_hour"的ul
canvas_hour_ul = self.main_soup.find("ul", class_="txt canvas_hour")
if not canvas_hour_ul: # 如果未找到该容器,返回空列表
return hourly_list
# 3. 提取温度和时间数据
# 获取温度ul内的所有li标签
temp_lis = zxt_shuju1_div.find("ul").find_all("li") if zxt_shuju1_div.find("ul") else []
# 获取时间ul内的所有li标签
time_lis = canvas_hour_ul.find_all("li")
# 确保时间和温度数据数量匹配,避免索引越界
min_length = min(len(time_lis), len(temp_lis))
# 遍历所有li标签,解析每小时的预报数据
for i in range(min_length):
# 提取时间文本(如"00:00")
time_text = time_lis[i].get_text(strip=True)
# 获取温度li标签
temp_li = temp_lis[i]
# 提取温度文本 - 优先从span标签获取,否则获取整个li的文本
temp_span = temp_li.find('span')
temp_text = temp_span.get_text(strip=True) if temp_span else temp_li.get_text(strip=True)
# 初始化小时预报数据字典
hourly = {
"城市": CITY, # 城市名称
"日期": CURRENT_DATE, # 当前日期
"时间": time_text, # 小时时间
"温度": f"{temp_text}℃", # 温度
"天气状况": "未知", # 天气状况(主页面未直接显示)
"风向": "未知" # 风向(主页面未直接显示)
}
hourly_list.append(hourly) # 将小时预报添加到列表
return hourly_list # 返回24小时预报数据列表
except Exception as e: # 捕获所有异常
print(f"解析24小时预报失败: {e}") # 打印错误信息
return [] # 返回空列表表示解析失败
def save_to_csv(self, data, filename):
"""保存数据到CSV文件"""
try:
# 将数据转换为DataFrame对象
# 如果是单个字典,转换为包含一个字典的列表
df = pd.DataFrame(data if isinstance(data, list) else [data])
# 构建完整的文件路径
file_path = os.path.join(DATA_DIR, filename)
# 将DataFrame保存为CSV文件
# index=False:不保存行索引
# encoding="utf-8-sig":使用UTF-8编码,支持中文,sig表示添加BOM标记
df.to_csv(file_path, index=False, encoding="utf-8-sig")
print(f"数据保存成功: {file_path}") # 打印保存成功信息
except Exception as e: # 捕获所有异常
print(f"保存数据失败: {e}") # 打印错误信息
def run(self):
"""执行完整的爬取流程"""
print(f"开始爬取{CITY}天气数据...") # 打印开始信息
# 1. 获取主页面
if not self.fetch_main_page():
return
# 2. 获取7天预报页面
# 注意:该页面存在403限制,实际未使用该页面数据
# 即使获取失败,也会继续执行后续操作
self.fetch_forecast_page()
# 3. 解析各类天气数据
current_weather = self.parse_current_weather() # 解析当前天气
forecast_7days = self.parse_forecast_7days() # 解析7天预报
hourly_24h = self.parse_hourly_24h() # 解析24小时预报
# 4. 保存数据到CSV文件
# 只有当数据不为空时,才保存到文件
if current_weather:
self.save_to_csv(current_weather, "current_weather.csv") # 保存当前天气
if forecast_7days:
self.save_to_csv(forecast_7days, "forecast_7days.csv") # 保存7天预报
if hourly_24h:
self.save_to_csv(hourly_24h, "hourly_24h.csv") # 保存24小时预报
print(f"{CITY}天气数据爬取完成!") # 打印完成信息
5.4 主函数调用
python
if __name__ == "__main__":
"""主函数 - 程序入口"""
spider = WeatherSpider() # 创建WeatherSpider对象
spider.run() # 执行爬取流程
6. 运行和测试
6.1 运行爬虫
在项目根目录下运行以下命令:
bash
python simplified_spiders/simple_weather_spider.py
6.2 预期输出
开始爬取乌兰察布天气数据...
数据保存成功: data/spider_data\current_weather.csv
数据保存成功: data/spider_data\forecast_7days.csv
数据保存成功: data/spider_data\hourly_24h.csv
乌兰察布天气数据爬取完成!
6.3 查看数据
爬取的数据将保存在data/spider_data目录下,包含三个CSV文件:
current_weather.csv:当前天气数据forecast_7days.csv:未来7天预报数据hourly_24h.csv:24小时预报数据
您可以使用Excel、CSV编辑器或pandas库查看这些文件。例如,使用pandas查看数据:
python
import pandas as pd
# 查看当前天气数据
current_df = pd.read_csv("data/spider_data/current_weather.csv")
print("当前天气数据:")
print(current_df)
# 查看7天预报数据
forecast_df = pd.read_csv("data/spider_data/forecast_7days.csv")
print("\n7天预报数据:")
print(forecast_df)
# 查看24小时预报数据
hourly_df = pd.read_csv("data/spider_data/hourly_24h.csv")
print("\n24小时预报数据:")
print(hourly_df)
6.4 常见问题及解决方案
6.4.1 HTTP 403 Forbidden错误
问题:运行爬虫时出现"获取7天预报页面失败: Client error '403 Forbidden' for url..."错误。
解决方案:这是因为目标网站的反爬机制阻止了我们的请求。我们已经在代码中处理了这个问题,改为从主页面提取7天预报数据,所以这个错误不会影响爬虫的正常运行。
6.4.2 数据解析失败
问题:运行爬虫时出现"解析当前天气失败: ..."或其他解析错误。
解决方案:这通常是因为网站结构发生了变化,导致我们的选择器失效。您需要重新分析网页结构,更新代码中的选择器。
6.4.3 数据保存失败
问题:运行爬虫时出现"保存数据失败: ..."错误。
解决方案 :这可能是因为数据目录不存在或没有写入权限。请确保data/spider_data目录存在,并且您有写入权限。
7. 扩展和优化
7.1 扩展功能
7.1.1 添加日志记录
为爬虫添加日志记录,便于调试和监控:
python
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('weather_spider.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 在代码中使用日志
logger.info("开始爬取天气数据")
logger.error(f"获取页面失败: {e}")
7.1.2 支持多个城市
修改代码,支持爬取多个城市的天气数据:
python
class WeatherSpider:
def __init__(self, city, city_code):
self.city = city
self.city_code = city_code
self.base_url = "https://www.tianqi.com"
self.main_url = f"{self.base_url}/{self.city_code}/"
# 其他初始化代码...
# 使用示例
cities = [
("乌兰察布", "wulanchabu"),
("北京", "beijing"),
("上海", "shanghai")
]
for city, city_code in cities:
spider = WeatherSpider(city, city_code)
spider.run()
7.1.3 添加数据可视化
使用matplotlib或seaborn库添加数据可视化功能,以下是经过验证的完整实现:
python
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体,避免中文乱码
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 绘制24小时温度变化图表
try:
# 读取24小时预报数据
hourly_df = pd.read_csv("data/spider_data/hourly_24h.csv")
print("24小时预报数据读取成功!")
# 检查数据是否包含"温度"列
if "温度" in hourly_df.columns:
# 处理温度数据,将字符串转换为数值类型(移除℃符号)
hourly_df["温度数值"] = hourly_df["温度"].str.replace("℃", "").astype(float)
# 创建图表
plt.figure(figsize=(12, 6))
# 绘制折线图,添加标记点,调整线宽
sns.lineplot(x="时间", y="温度数值", data=hourly_df, marker="o", linewidth=2.5)
plt.title("24小时温度变化", fontsize=16, fontweight='bold')
plt.xlabel("时间", fontsize=12)
plt.ylabel("温度 (℃)", fontsize=12)
plt.xticks(rotation=45, fontsize=10) # 旋转时间标签,避免重叠
plt.yticks(fontsize=10)
plt.grid(True, alpha=0.3) # 添加网格线,提高可读性
plt.tight_layout() # 自动调整布局
# 保存图表
output_path = "data/spider_data/temperature_trend.png"
plt.savefig(output_path, dpi=300, bbox_inches='tight') # 高分辨率保存
print(f"24小时温度变化图表保存成功: {output_path}")
# 显示图表(可选,在某些环境中可能无法显示)
# plt.show()
else:
print("错误:24小时预报数据中不包含'温度'列")
except Exception as e:
print(f"绘制24小时温度变化图表失败: {e}")
import traceback
traceback.print_exc()
# 2. 绘制7天温度变化图表
try:
# 读取7天预报数据
forecast_df = pd.read_csv("data/spider_data/forecast_7days.csv")
print("7天预报数据读取成功!")
# 检查数据是否包含"最高温度"和"最低温度"列
if "最高温度" in forecast_df.columns and "最低温度" in forecast_df.columns:
# 创建图表
plt.figure(figsize=(12, 6))
# 绘制最高温度折线
sns.lineplot(x="日期", y="最高温度", data=forecast_df, marker="o", linewidth=2.5, label="最高温度")
# 绘制最低温度折线
sns.lineplot(x="日期", y="最低温度", data=forecast_df, marker="s", linewidth=2.5, label="最低温度")
plt.title("7天温度变化趋势", fontsize=16, fontweight='bold')
plt.xlabel("日期", fontsize=12)
plt.ylabel("温度 (℃)", fontsize=12)
plt.xticks(fontsize=10)
plt.yticks(fontsize=10)
plt.legend(fontsize=12) # 添加图例
plt.grid(True, alpha=0.3) # 添加网格线
plt.tight_layout() # 自动调整布局
# 保存图表
output_path = "data/spider_data/forecast_temperature_trend.png"
plt.savefig(output_path, dpi=300, bbox_inches='tight') # 高分辨率保存
print(f"7天温度变化趋势图表保存成功: {output_path}")
# 显示图表(可选)
# plt.show()
else:
print("错误:7天预报数据中不包含'最高温度'或'最低温度'列")
except Exception as e:
print(f"绘制7天温度变化图表失败: {e}")
import traceback
traceback.print_exc()
注意事项:
- 中文乱码问题:务必添加中文字体设置,否则图表中的中文可能会显示为方块
- 数据类型转换:原始数据中的温度通常是字符串格式(如"-4℃"),需要转换为数值类型才能绘制图表
- 环境兼容性 :
plt.show()在某些非图形界面环境(如服务器)中可能无法正常显示,建议优先使用plt.savefig()保存图表 - 数据完整性检查:在绘制图表前,务必检查数据是否包含所需的列,避免因数据格式变化导致程序崩溃
- 图表美观性:适当调整图表样式(如标题、坐标轴标签、网格线、图例等),可以提高图表的可读性
- 高分辨率保存 :使用
dpi=300参数保存高分辨率图表,适合用于报告或演示
扩展建议:
- 可以添加更多类型的图表,如柱状图、散点图等
- 可以将图表集成到Web应用中,实现数据可视化的实时展示
- 可以添加交互功能,允许用户通过鼠标悬停查看具体数据值
- 可以使用更高级的可视化库,如Plotly或Bokeh,实现更丰富的交互效果
7.1.4 实现定时爬取
使用apscheduler库实现定时爬取,自动更新数据:
python
from apscheduler.schedulers.background import BackgroundScheduler
# 创建调度器
scheduler = BackgroundScheduler()
# 定义爬取函数
def crawl_weather():
spider = WeatherSpider()
spider.run()
# 每天8:00和18:00自动爬取
scheduler.add_job(crawl_weather, 'cron', hour=[8, 18])
# 启动调度器
scheduler.start()
# 保持程序运行
while True:
time.sleep(1)
7.2 优化建议
7.2.1 添加更完善的异常处理
为不同类型的异常添加不同的处理逻辑,提高爬虫的健壮性:
python
try:
response = httpx.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
except httpx.TimeoutException:
print(f"请求超时: {url}")
except httpx.HTTPStatusError as e:
print(f"HTTP错误,状态码: {e.response.status_code}, URL: {url}")
except httpx.RequestError as e:
print(f"请求失败: {url}, 错误: {e}")
except Exception as e:
print(f"发生未知错误: {e}")
7.2.2 实现代理IP池
使用代理IP池,避免IP被封:
python
def get_proxy():
# 从代理IP池中获取一个代理
proxies = [
"http://proxy1:port",
"http://proxy2:port",
# 更多代理...
]
return random.choice(proxies)
# 使用代理发送请求
proxy = get_proxy()
response = httpx.get(url, headers=HEADERS, proxies=proxy, timeout=30)
7.2.3 添加重试机制
为失败的请求添加重试机制,提高爬取成功率:
python
def fetch_with_retry(url, max_retries=3):
for i in range(max_retries):
try:
response = httpx.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
return response
except Exception as e:
print(f"请求失败,第{i+1}次重试: {e}")
time.sleep(1)
return None
7.2.4 优化选择器
使用更高效的选择器,提高解析效率:
python
# 优化前
weather_info = self.main_soup.select_one(".weather_info")
# 优化后
weather_info = self.main_soup.find("div", class_="weather_info")
7.2.5 实现数据去重
实现数据去重,避免重复数据:
python
def remove_duplicates(data, key):
"""根据指定键去重数据"""
seen = set()
result = []
for item in data:
if item[key] not in seen:
seen.add(item[key])
result.append(item)
return result
# 使用示例
forecast_7days = remove_duplicates(forecast_7days, "日期")
8. 总结
本教程详细介绍了如何使用Python开发一个简化版的天气数据爬虫,重点讲解了网页结构分析的方法和技巧。通过本教程,您应该掌握了以下内容:
- 环境搭建:如何安装Python和所需的依赖库,创建虚拟环境
- 网页分析:如何使用浏览器开发者工具分析网页结构,确定数据所在的位置
- 爬虫开发:如何使用httpx发送HTTP请求,使用BeautifulSoup解析HTML,提取所需数据
- 数据处理:如何使用pandas将数据保存到CSV文件
- 面向对象设计:如何设计合理的类结构,封装爬虫功能
- 异常处理:如何处理异常情况,提高爬虫的健壮性
- 扩展优化:如何扩展爬虫功能,优化爬虫性能
8.1 项目回顾
我们开发的简化版天气爬虫具有以下特点:
- 结构清晰:采用面向对象设计,代码结构清晰,易于维护和扩展
- 功能完整:能够爬取当前天气、未来7天预报和24小时预报数据
- 健壮性强:添加了异常处理机制,能够处理各种异常情况
- 易于使用:提供了简单的API,只需创建对象并调用run方法即可执行爬取
8.2 学习建议
- 多实践:尝试爬取其他网站的天气数据,加深对爬虫开发的理解
- 学习正则表达式:掌握正则表达式,能够更灵活地提取数据
- 学习XPath:了解XPath选择器,与CSS选择器配合使用,提高数据提取能力
- 学习异步爬虫:了解异步编程,提高爬虫的爬取效率
- 学习反爬策略:了解常见的反爬策略和应对方法
- 学习数据可视化:掌握数据可视化技术,能够将爬取的数据转换为直观的图表
8.3 注意事项
- 遵守网站规则:在爬取网站数据时,遵守网站的robots.txt规则,不要过度爬取
- 保护隐私:不要爬取和传播他人的隐私数据
- 尊重版权:爬取的数据仅用于学习和研究,不要用于商业用途
- 定期更新:网站结构可能会变化,需要定期检查和更新爬虫代码
希望本教程对您有所帮助,祝您在爬虫开发的道路上越走越远!
9. 附录
9.1 完整代码
完整的simple_weather_spider.py代码如下:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简化版天气数据爬虫
功能:爬取乌兰察布当前天气、未来7天预报和24小时预报数据
【开发思路】
1. 采用面向对象设计,将爬虫功能封装到WeatherSpider类中,提高代码复用性和可维护性
2. 分阶段获取数据:先获取页面HTML,再解析数据,最后保存到CSV文件
3. 采用分层解析策略:针对不同类型的天气数据(当前天气、7天预报、24小时预报)分别实现解析方法
4. 容错设计:添加异常处理和备选解析方案,提高爬虫的健壮性
5. 适应网站结构变化:使用多种选择器尝试获取数据,确保在网站结构变化时仍能正常工作
【开发过程】
1. 需求分析:确定需要爬取的天气数据类型(当前天气、7天预报、24小时预报)
2. 网站分析:研究目标网站的HTML结构,确定数据所在的标签和选择器
3. 代码设计:设计类结构和方法,规划数据流向
4. 实现获取页面功能:使用httpx库发送HTTP请求,获取页面HTML
5. 实现解析功能:使用BeautifulSoup库解析HTML,提取所需数据
6. 实现保存功能:使用pandas库将数据保存到CSV文件
7. 测试和优化:测试爬虫功能,修复bug,优化解析逻辑
【网页标签分析】
1. 当前天气数据:位于class="weather_info"的div容器内,包含温度、天气状况、湿度、风向等信息
2. 7天预报数据:主页面已包含完整数据,分布在多个相邻元素中
- 日期和星期:最后一个class="week"的ul元素
- 天气状况:紧邻的class="txt txt2"的ul元素
- 温度数据:class="zxt_shuju"的div容器内的ul元素
3. 24小时预报数据:
- 温度数据:class="zxt_shuju1"的div容器内的ul元素
- 时间数据:class="txt canvas_hour"的ul元素
【注意事项】
1. 7天预报页面(https://www.tianqi.com/wulanchabu/7/)存在403 Forbidden限制,因此改为从主页面提取7天预报数据
2. 网站结构可能会变化,需要定期检查和更新选择器
3. 爬取频率不宜过高,避免给目标网站带来过大压力
"""
# 导入所需库
import httpx # 用于发送HTTP请求
from bs4 import BeautifulSoup # 用于解析HTML
import pandas as pd # 用于数据处理和保存
from datetime import datetime # 用于处理日期和时间
import os # 用于文件和目录操作
# 配置常量 - 可根据需要修改
CITY = "乌兰察布" # 城市名称
CITY_CODE = "wulanchabu" # 城市代码,用于构建URL
BASE_URL = "https://www.tianqi.com" # 网站基础URL
MAIN_URL = f"{BASE_URL}/{CITY_CODE}/" # 主页面URL
FORECAST_URL = f"{BASE_URL}/{CITY_CODE}/7/" # 7天预报页面URL(存在403限制,实际未使用)
# 请求头配置 - 模拟浏览器请求,避免被反爬
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Referer": "https://www.tianqi.com/" # 来源页,增加请求可信度
}
# 数据保存目录 - 确保目录存在
DATA_DIR = "data/spider_data"
os.makedirs(DATA_DIR, exist_ok=True) # 若目录不存在则创建
# 当前日期 - 用于数据标识
CURRENT_DATE = datetime.now().strftime("%Y-%m-%d") # 格式:年-月-日
class WeatherSpider:
"""简化版天气爬虫类 - 封装所有爬取功能"""
def __init__(self):
"""初始化爬虫对象"""
self.main_soup = None # 主页面的BeautifulSoup对象
self.forecast_soup = None # 7天预报页面的BeautifulSoup对象(实际未使用)
def fetch_main_page(self):
"""获取主页面内容"""
try:
# 发送HTTP GET请求,获取主页面HTML
response = httpx.get(MAIN_URL, headers=HEADERS, follow_redirects=True, timeout=30)
response.raise_for_status() # 检查HTTP响应状态码
# 将HTML转换为BeautifulSoup对象,便于解析
self.main_soup = BeautifulSoup(response.text, "html.parser")
return True # 获取成功
except Exception as e:
print(f"获取主页面失败: {e}") # 打印错误信息
return False # 获取失败
def fetch_forecast_page(self):
"""获取7天预报页面内容"""
try:
# 发送HTTP GET请求,获取7天预报页面HTML
# 注意:该页面存在403 Forbidden限制,实际未使用该页面数据
response = httpx.get(FORECAST_URL, headers=HEADERS, follow_redirects=True, timeout=30)
response.raise_for_status() # 检查HTTP响应状态码
# 将HTML转换为BeautifulSoup对象(实际未使用)
self.forecast_soup = BeautifulSoup(response.text, "html.parser")
return True # 获取成功
except Exception as e:
print(f"获取7天预报页面失败: {e}") # 打印错误信息
return False # 获取失败
def parse_current_weather(self):
"""解析当前天气数据 - 从主页面提取"""
if not self.main_soup: # 检查主页面是否已获取
return None
try:
# 初始化当前天气数据字典
current = {
"城市": CITY, # 城市名称
"日期": CURRENT_DATE, # 当前日期
"时间": datetime.now().strftime("%H:%M") # 当前时间
}
# 1. 查找主要天气容器 - class="weather_info"的div
# 这是当前天气信息的主要容器,包含温度、天气状况等数据
weather_info = self.main_soup.select_one(".weather_info")
if not weather_info: # 如果未找到该容器,返回None
return None
# 2. 查找天气详情容器 - class="weather"的dd
# 该容器包含当前温度和天气状况
weather_dd = weather_info.select_one(".weather")
if weather_dd: # 如果找到该容器
# 解析当前温度 - class="now b"的标签
temp_b = weather_dd.select_one(".now b")
current["温度"] = f"{temp_b.text.strip()}℃" if temp_b else "未知"
# 解析天气状况 - 查找span标签
weather_span = weather_dd.select_one("span")
if weather_span: # 如果找到span标签
weather_text = weather_span.text.strip() # 示例:"多云-13 ~ -3℃"
# 提取天气状况(如"多云-13 ~ -3℃" -> "多云")
weather_status = weather_text.split("-")[0].strip()
current["天气状况"] = weather_status # 天气状况
current["温度范围"] = weather_text # 完整温度范围
# 3. 解析其他天气信息(湿度、风向等)
all_text = weather_info.text.strip() # 获取整个天气容器的文本内容
# 提取湿度 - 从文本中查找"湿度:"关键字
if "湿度:" in all_text:
# 分割文本,获取湿度值
humidity = all_text.split("湿度:")[1].split()[0]
current["湿度"] = f"湿度:{humidity}"
# 提取风向 - 查找风向关键字
wind_keywords = ["西风", "东风", "南风", "北风", "西南风", "西北风", "东南风", "东北风"]
for keyword in wind_keywords:
if keyword in all_text: # 如果找到风向关键字
current["风向"] = f"风向:{keyword}"
break
return current # 返回当前天气数据
except Exception as e: # 捕获所有异常
print(f"解析当前天气失败: {e}") # 打印错误信息
return None # 返回None表示解析失败
def parse_forecast_7days(self):
"""解析未来7天天气预报 - 从主页面提取"""
try:
forecast_list = [] # 初始化7天预报数据列表
# 仅从主页面获取7天预报数据,因为7天预报页面有403限制
if self.main_soup: # 检查主页面是否已获取
# 1. 获取日期和星期信息
# 查找所有class="week"的ul元素,主页面包含多个week ul
week_uls = self.main_soup.find_all("ul", class_="week")
if not week_uls: # 如果未找到,返回空列表
return forecast_list
# 获取最后一个class="week"的ul元素,它包含7天的日期和星期
week_ul = week_uls[-1]
# 2. 获取天气状况信息
# 获取week_ul的下一个兄弟元素,它应该是包含天气状况的ul
txt_ul = week_ul.find_next_sibling()
# 检查下一个兄弟元素是否为ul,否则设为None
if txt_ul and txt_ul.name != "ul":
txt_ul = None
# 3. 获取温度数据
# 查找class="zxt_shuju"的div,它包含7天的温度数据
# 注意:该div可能设置了display: none,但仍包含完整数据
zxt_shuju_div = self.main_soup.find("div", class_="zxt_shuju")
# 获取div内的ul元素,它包含具体的温度数据
temp_ul = zxt_shuju_div.find("ul") if zxt_shuju_div else None
# 如果找到了所有必要的元素,开始解析数据
if week_ul and txt_ul and temp_ul:
# 获取所有的日期和星期li标签
week_lis = week_ul.find_all("li")
# 获取所有的天气状况li标签
txt_lis = txt_ul.find_all("li")
# 获取所有的温度li标签
temp_lis = temp_ul.find_all("li")
# 确保所有列表的长度一致,避免索引越界
min_length = min(len(week_lis), len(txt_lis), len(temp_lis))
# 遍历所有列表,解析每天的预报数据
for i in range(min_length):
try:
week_li = week_lis[i] # 日期和星期li
txt_li = txt_lis[i] # 天气状况li
temp_li = temp_lis[i] # 温度li
# 解析日期和星期
date_b = week_li.find("b") # 日期在b标签中
week_span = week_li.find("span") # 星期在span标签中
# 初始化单日预报数据字典
forecast = {
"城市": CITY, # 城市名称
"日期": date_b.text.strip() if date_b else "未知", # 日期(如"12-26")
"星期": week_span.text.strip() if week_span else "未知", # 星期(如"星期五")
"天气状况": txt_li.text.strip() if txt_li else "未知" # 天气状况(如"多云")
}
# 解析温度数据
max_temp_span = temp_li.find("span") # 最高温度在span标签中
min_temp_b = temp_li.find("b") # 最低温度在b标签中
if max_temp_span and min_temp_b: # 如果找到温度标签
# 提取并转换温度值为整数
max_temp = int(max_temp_span.text.strip())
min_temp = int(min_temp_b.text.strip())
forecast["最高温度"] = max_temp # 最高温度
forecast["最低温度"] = min_temp # 最低温度
forecast["温度范围"] = f"{min_temp}~{max_temp}℃" # 温度范围
else: # 如果未找到温度标签,使用默认值
forecast["最高温度"] = 0
forecast["最低温度"] = 0
forecast["温度范围"] = "未知"
# 风向和风力信息在主页面中未直接显示,暂时设为未知
forecast["风向"] = "未知"
forecast["风力"] = "未知"
forecast_list.append(forecast) # 将单日预报添加到列表
except Exception as e: # 捕获单天解析异常
print(f"解析单天预报失败: {e}") # 打印错误信息
continue # 继续解析下一天
return forecast_list # 返回7天预报数据列表
except Exception as e: # 捕获所有异常
print(f"解析7天预报失败: {e}") # 打印错误信息
return [] # 返回空列表表示解析失败
def parse_hourly_24h(self):
"""解析24小时天气预报 - 从主页面提取"""
if not self.main_soup: # 检查主页面是否已获取
return []
try:
hourly_list = [] # 初始化24小时预报数据列表
# 1. 查找温度数据容器 - class="zxt_shuju1"的div
# 该div包含24小时温度数据
zxt_shuju1_div = self.main_soup.find("div", class_="zxt_shuju1")
if not zxt_shuju1_div: # 如果未找到该容器,返回空列表
return hourly_list
# 2. 查找时间数据容器 - class="txt canvas_hour"的ul
# 该ul包含24小时时间数据
canvas_hour_ul = self.main_soup.find("ul", class_="txt canvas_hour")
if not canvas_hour_ul: # 如果未找到该容器,返回空列表
return hourly_list
# 3. 提取温度和时间数据
# 获取温度ul内的所有li标签
temp_lis = zxt_shuju1_div.find("ul").find_all("li") if zxt_shuju1_div.find("ul") else []
# 获取时间ul内的所有li标签
time_lis = canvas_hour_ul.find_all("li")
# 确保时间和温度数据数量匹配,避免索引越界
min_length = min(len(time_lis), len(temp_lis))
# 遍历所有li标签,解析每小时的预报数据
for i in range(min_length):
# 提取时间文本(如"00:00")
time_text = time_lis[i].get_text(strip=True)
# 获取温度li标签
temp_li = temp_lis[i]
# 提取温度文本 - 优先从span标签获取,否则获取整个li的文本
temp_span = temp_li.find('span')
temp_text = temp_span.get_text(strip=True) if temp_span else temp_li.get_text(strip=True)
# 初始化小时预报数据字典
hourly = {
"城市": CITY, # 城市名称
"日期": CURRENT_DATE, # 当前日期
"时间": time_text, # 小时时间
"温度": f"{temp_text}℃", # 温度
"天气状况": "未知", # 天气状况(主页面未直接显示)
"风向": "未知" # 风向(主页面未直接显示)
}
hourly_list.append(hourly) # 将小时预报添加到列表
return hourly_list # 返回24小时预报数据列表
except Exception as e: # 捕获所有异常
print(f"解析24小时预报失败: {e}") # 打印错误信息
return [] # 返回空列表表示解析失败
def save_to_csv(self, data, filename):
"""保存数据到CSV文件"""
try:
# 将数据转换为DataFrame对象
# 如果是单个字典,转换为包含一个字典的列表
df = pd.DataFrame(data if isinstance(data, list) else [data])
# 构建完整的文件路径
file_path = os.path.join(DATA_DIR, filename)
# 将DataFrame保存为CSV文件
# index=False:不保存行索引
# encoding="utf-8-sig":使用UTF-8编码,支持中文,sig表示添加BOM标记
df.to_csv(file_path, index=False, encoding="utf-8-sig")
print(f"数据保存成功: {file_path}") # 打印保存成功信息
except Exception as e: # 捕获所有异常
print(f"保存数据失败: {e}") # 打印错误信息
def run(self):
"""执行完整的爬取流程"""
print(f"开始爬取{CITY}天气数据...") # 打印开始信息
# 1. 获取主页面
if not self.fetch_main_page():
return
# 2. 获取7天预报页面
# 注意:该页面存在403限制,实际未使用该页面数据
# 即使获取失败,也会继续执行后续操作
self.fetch_forecast_page()
# 3. 解析各类天气数据
current_weather = self.parse_current_weather() # 解析当前天气
forecast_7days = self.parse_forecast_7days() # 解析7天预报
hourly_24h = self.parse_hourly_24h() # 解析24小时预报
# 4. 保存数据到CSV文件
# 只有当数据不为空时,才保存到文件
if current_weather:
self.save_to_csv(current_weather, "current_weather.csv") # 保存当前天气
if forecast_7days:
self.save_to_csv(forecast_7days, "forecast_7days.csv") # 保存7天预报
if hourly_24h:
self.save_to_csv(hourly_24h, "hourly_24h.csv") # 保存24小时预报
print(f"{CITY}天气数据爬取完成!") # 打印完成信息
if __name__ == "__main__":
"""主函数 - 程序入口"""
spider = WeatherSpider() # 创建WeatherSpider对象
spider.run() # 执行爬取流程
9.2 参考资源
- Python官方文档:https://docs.python.org/3/
- httpx文档:https://www.python-httpx.org/
- BeautifulSoup文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc/
- pandas文档:https://pandas.pydata.org/docs/
- CSS选择器参考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Selectors
- 浏览器开发者工具教程:https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_are_browser_developer_tools
9.3 联系方式
如果您在学习过程中遇到问题,或者有任何建议和意见,欢迎与我联系。
- 邮箱:example@example.com
- GitHub:https://github.com/example
- 博客:https://example.com/blog
本教程完,祝您学习愉快!