简化版天气爬虫教程

简化版天气爬虫教程

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 数据流向

  1. 发送HTTP请求获取网页内容
  2. 使用BeautifulSoup解析HTML
  3. 提取所需的天气数据
  4. 将数据保存到CSV文件

4. 网页结构分析(重点)

4.1 目标网站

我们的目标网站是天气网,具体页面为乌兰察布天气

4.2 使用浏览器开发者工具分析网页

在开始编写爬虫之前,我们需要先了解目标网站的HTML结构,确定数据所在的位置。您可以使用浏览器的开发者工具来分析网页:

  1. 打开目标网站
  2. 右键点击页面,选择"检查"或"审查元素"
  3. 使用"选择元素"工具(通常是左上角的箭头图标)点击您想要分析的数据
  4. 在开发者工具的"元素"标签中查看对应的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天预报数据分布在多个相邻元素中,这是我们需要重点分析的部分:

  1. 日期和星期 :位于最后一个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>
  2. 天气状况 :位于紧邻日期星期元素的下一个class="txt txt2"的ul元素中

    html 复制代码
    <ul class="txt txt2">
        <li>多云</li>
        <li>晴转多云</li>
        <!-- 其他天气状况 -->
    </ul>
  3. 温度数据 :位于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小时预报数据分布在两个不同的容器中:

  1. 时间数据 :位于class="txt canvas_hour"的ul元素中

    html 复制代码
    <ul class="txt canvas_hour">
        <li>00:00</li>
        <li>02:00</li>
        <!-- 其他时间 -->
    </ul>
  2. 温度数据 :位于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()

注意事项:

  1. 中文乱码问题:务必添加中文字体设置,否则图表中的中文可能会显示为方块
  2. 数据类型转换:原始数据中的温度通常是字符串格式(如"-4℃"),需要转换为数值类型才能绘制图表
  3. 环境兼容性plt.show()在某些非图形界面环境(如服务器)中可能无法正常显示,建议优先使用plt.savefig()保存图表
  4. 数据完整性检查:在绘制图表前,务必检查数据是否包含所需的列,避免因数据格式变化导致程序崩溃
  5. 图表美观性:适当调整图表样式(如标题、坐标轴标签、网格线、图例等),可以提高图表的可读性
  6. 高分辨率保存 :使用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开发一个简化版的天气数据爬虫,重点讲解了网页结构分析的方法和技巧。通过本教程,您应该掌握了以下内容:

  1. 环境搭建:如何安装Python和所需的依赖库,创建虚拟环境
  2. 网页分析:如何使用浏览器开发者工具分析网页结构,确定数据所在的位置
  3. 爬虫开发:如何使用httpx发送HTTP请求,使用BeautifulSoup解析HTML,提取所需数据
  4. 数据处理:如何使用pandas将数据保存到CSV文件
  5. 面向对象设计:如何设计合理的类结构,封装爬虫功能
  6. 异常处理:如何处理异常情况,提高爬虫的健壮性
  7. 扩展优化:如何扩展爬虫功能,优化爬虫性能

8.1 项目回顾

我们开发的简化版天气爬虫具有以下特点:

  • 结构清晰:采用面向对象设计,代码结构清晰,易于维护和扩展
  • 功能完整:能够爬取当前天气、未来7天预报和24小时预报数据
  • 健壮性强:添加了异常处理机制,能够处理各种异常情况
  • 易于使用:提供了简单的API,只需创建对象并调用run方法即可执行爬取

8.2 学习建议

  1. 多实践:尝试爬取其他网站的天气数据,加深对爬虫开发的理解
  2. 学习正则表达式:掌握正则表达式,能够更灵活地提取数据
  3. 学习XPath:了解XPath选择器,与CSS选择器配合使用,提高数据提取能力
  4. 学习异步爬虫:了解异步编程,提高爬虫的爬取效率
  5. 学习反爬策略:了解常见的反爬策略和应对方法
  6. 学习数据可视化:掌握数据可视化技术,能够将爬取的数据转换为直观的图表

8.3 注意事项

  1. 遵守网站规则:在爬取网站数据时,遵守网站的robots.txt规则,不要过度爬取
  2. 保护隐私:不要爬取和传播他人的隐私数据
  3. 尊重版权:爬取的数据仅用于学习和研究,不要用于商业用途
  4. 定期更新:网站结构可能会变化,需要定期检查和更新爬虫代码

希望本教程对您有所帮助,祝您在爬虫开发的道路上越走越远!

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 参考资源

  1. Python官方文档https://docs.python.org/3/
  2. httpx文档https://www.python-httpx.org/
  3. BeautifulSoup文档https://www.crummy.com/software/BeautifulSoup/bs4/doc/
  4. pandas文档https://pandas.pydata.org/docs/
  5. CSS选择器参考https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Selectors
  6. 浏览器开发者工具教程https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_are_browser_developer_tools

9.3 联系方式

如果您在学习过程中遇到问题,或者有任何建议和意见,欢迎与我联系。


本教程完,祝您学习愉快!

相关推荐
Dxy123931021615 分钟前
Python基于BERT的上下文纠错详解
开发语言·python·bert
SiYuanFeng1 小时前
Colab复现 NanoChat:从 Tokenizer(CPU)、Base Train(CPU) 到 SFT(GPU) 的完整踩坑实录
python·colab
炸炸鱼.2 小时前
Python 操作 MySQL 数据库
android·数据库·python·adb
_深海凉_3 小时前
LeetCode热题100-颜色分类
python·算法·leetcode
AC赳赳老秦3 小时前
OpenClaw email技能:批量发送邮件、自动回复,高效处理工作邮件
运维·人工智能·python·django·自动化·deepseek·openclaw
zhaoshuzhaoshu3 小时前
Python 语法之数据结构详细解析
python
AI问答工程师4 小时前
Meta Muse Spark 的"思维压缩"到底是什么?我用 Python 复现了核心思路(附代码)
人工智能·python
zfan5205 小时前
python对Excel数据处理(1)
python·excel·pandas
小饕5 小时前
我从零搭建 RAG 学到的 10 件事
python
老歌老听老掉牙5 小时前
PyQt5+Qt Designer实战:可视化设计智能参数配置界面,告别手动布局时代!
python·qt