简化版天气爬虫教程

简化版天气爬虫教程

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 联系方式

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


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

相关推荐
2401_841495642 小时前
【LeetCode刷题】打家劫舍
数据结构·python·算法·leetcode·动态规划·数组·传统dp数组
3824278272 小时前
python:Ajax爬取电影详情实战
开发语言·python·ajax
天呐草莓2 小时前
集成学习 (ensemble learning)
人工智能·python·深度学习·算法·机器学习·数据挖掘·集成学习
BBB努力学习程序设计2 小时前
Python多线程与多进程编程实战指南
python
雪落无尘处2 小时前
Anaconda 虚拟环境配置全攻略+Pycharm使用虚拟环境开发:从安装到高效管理
后端·python·pycharm·conda·anaconda
Amelia1111112 小时前
day36
python
不惑_2 小时前
通俗理解多层感知机(MLP)
开发语言·人工智能·python·深度学习
山沐与山3 小时前
【设计模式】Python责任链模式:从入门到实战
python·设计模式·责任链模式
luoluoal3 小时前
基于python的图像的信息隐藏技术研究(源码+文档)
python·mysql·django·毕业设计·源码