空气质量数据背后的秘密:教你用Python爬取PM2.5历史数据并可视化分析

最近几年,每当秋冬季节来临,朋友圈总会被各种"晒蓝天"和"吐槽雾霾"的内容刷屏。作为一名数据爱好者,我常常好奇:我们城市的空气质量到底是在变好还是变坏?每天的PM2.5波动有什么规律?不同季节的污染特征有何不同?为了回答这些问题,我决定自己动手,用Python构建一个空气质量数据分析工具。

本文将带你从零开始,通过爬取权威空气质量数据,建立本地数据库,并进行多维度的可视化分析。不同于简单的数据展示,我们会重点关注如何从原始数据中挖掘有价值的洞见。整个过程只需要基础的Python知识,但最终你将获得一个可以持续监测和分析空气质量变化的实用工具。

1. 数据获取:构建稳定的爬虫系统

1.1 选择可靠的数据源

在开始编码之前,首先要确定数据来源。经过对比测试,我发现中国环境监测总站提供的实时发布系统数据最为权威和稳定。这个平台不仅提供实时数据,还包含历史数据查询功能,非常适合我们的需求。

import requests
from bs4 import BeautifulSoup

def get_city_list():
    url = "http://www.cnemc.cn/sssj/"
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    city_options = soup.select('select[name="city"] option')
    return {opt.text: opt['value'] for opt in city_options if opt['value']}

提示:在实际项目中,建议设置合理的请求间隔(如5-10秒一次),避免给服务器造成过大压力。

1.2 解析网页结构获取关键数据

空气质量数据通常以表格形式呈现,我们需要准确识别其中的关键字段:

字段名称 说明 数据类型
AQI 空气质量指数 整数
PM2.5 细颗粒物浓度 整数(μg/m³)
PM10 可吸入颗粒物浓度 整数(μg/m³)
SO2 二氧化硫浓度 整数(μg/m³)
NO2 二氧化氮浓度 整数(μg/m³)
CO 一氧化碳浓度 浮点(mg/m³)
O3 臭氧浓度 整数(μg/m³)
def parse_air_data(html):
    soup = BeautifulSoup(html, 'html.parser')
    table = soup.find('table', {'class': 'report-table'})
    
    data = {}
    for row in table.find_all('tr'):
        cells = row.find_all('td')
        if len(cells) == 2:
            key = cells[0].text.strip()
            value = cells[1].text.strip()
            data[key] = float(value) if '.' in value else int(value)
    
    return data

1.3 构建完整的历史数据爬取流程

为了获取长期数据,我们需要设计一个可以自动翻页、处理异常的重试机制:

import time
from datetime import datetime, timedelta

def fetch_history_data(city_code, start_date, end_date):
    base_url = "http://www.cnemc.cn/sssj/history.aspx"
    session = requests.Session()
    all_data = []
    
    current_date = start_date
    while current_date <= end_date:
        try:
            params = {
                'city': city_code,
                'date': current_date.strftime('%Y-%m-%d')
            }
            response = session.get(base_url, params=params)
            day_data = parse_air_data(response.text)
            day_data['date'] = current_date.strftime('%Y-%m-%d')
            all_data.append(day_data)
            
            print(f"成功获取 {current_date} 数据")
            time.sleep(8)  # 礼貌性延迟
            current_date += timedelta(days=1)
            
        except Exception as e:
            print(f"获取 {current_date} 数据失败: {str(e)}")
            time.sleep(30)  # 失败后延长等待时间
    
    return all_data

2. 数据存储与管理:构建本地空气质量数据库

2.1 设计合理的数据存储结构

获取到的原始数据需要经过清洗和结构化处理才能用于分析。我推荐使用SQLite作为本地存储方案,它轻量且无需额外配置:

import sqlite3
from contextlib import closing

def init_database(db_path='air_quality.db'):
    with closing(sqlite3.connect(db_path)) as conn:
        cursor = conn.cursor()
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS air_quality (
            date TEXT PRIMARY KEY,
            aqi INTEGER,
            pm25 INTEGER,
            pm10 INTEGER,
            so2 INTEGER,
            no2 INTEGER,
            co REAL,
            o3 INTEGER,
            primary_pollutant TEXT,
            quality_level TEXT
        )
        ''')
        conn.commit()

2.2 实现高效的数据更新机制

为了避免重复爬取已经存在的数据,我们需要实现增量更新功能:

def update_database(new_data, db_path='air_quality.db'):
    with closing(sqlite3.connect(db_path)) as conn:
        cursor = conn.cursor()
        
        # 获取已有日期列表
        cursor.execute("SELECT date FROM air_quality")
        existing_dates = {row[0] for row in cursor.fetchall()}
        
        # 只插入新数据
        inserted = 0
        for record in new_data:
            if record['date'] not in existing_dates:
                cursor.execute('''
                INSERT INTO air_quality VALUES (
                    :date, :aqi, :pm25, :pm10, :so2, 
                    :no2, :co, :o3, :primary_pollutant, :quality_level
                )
                ''', record)
                inserted += 1
        
        conn.commit()
        print(f"成功插入 {inserted} 条新记录")

2.3 数据质量控制与异常处理

原始数据中可能存在缺失值或异常值,我们需要建立自动化的数据清洗流程:

def clean_air_data(raw_data):
    cleaned = []
    for record in raw_data:
        # 处理缺失值
        clean_record = {
            'date': record.get('date', ''),
            'aqi': record.get('AQI', -1),
            'pm25': record.get('PM2.5', -1),
            'pm10': record.get('PM10', -1),
            'so2': record.get('SO2', -1),
            'no2': record.get('NO2', -1),
            'co': record.get('CO', -1.0),
            'o3': record.get('O3', -1),
            'primary_pollutant': record.get('首要污染物', ''),
            'quality_level': record.get('空气质量级别', '')
        }
        
        # 验证数据有效性
        if clean_record['date'] and clean_record['aqi'] > 0:
            cleaned.append(clean_record)
    
    return cleaned

3. 数据分析:挖掘空气质量变化规律

3.1 基础统计分析:了解数据全貌

使用pandas可以快速计算各种统计指标:

import pandas as pd

def basic_analysis(db_path='air_quality.db'):
    with closing(sqlite3.connect(db_path)) as conn:
        df = pd.read_sql("SELECT * FROM air_quality", conn)
    
    # 计算各污染物年度平均值
    annual_avg = df.groupby(pd.to_datetime(df['date']).dt.year).mean()
    
    # 计算月度变化
    df['month'] = pd.to_datetime(df['date']).dt.month
    monthly_avg = df.groupby('month').mean()
    
    return {
        'annual_trend': annual_avg,
        'monthly_pattern': monthly_avg
    }

3.2 可视化分析:直观展示数据特征

matplotlib和seaborn组合可以创建丰富的可视化图表:

import matplotlib.pyplot as plt
import seaborn as sns

def plot_pm25_trend(df):
    plt.figure(figsize=(12, 6))
    df['date'] = pd.to_datetime(df['date'])
    df = df.set_index('date').sort_index()
    
    # 使用移动平均平滑数据
    df['pm25_30d'] = df['pm25'].rolling(30).mean()
    
    ax = sns.lineplot(data=df, x=df.index, y='pm25_30d', 
                     color='red', linewidth=2)
    ax.set(title='PM2.5浓度30日移动平均趋势', 
          ylabel='PM2.5浓度 (μg/m³)',
          xlabel='日期')
    plt.xticks(rotation=45)
    plt.tight_layout()
    plt.show()

3.3 高级分析:污染物相关性研究

不同污染物之间往往存在关联,我们可以通过热力图展示它们的关系:

def plot_pollutant_correlation(df):
    pollutants = ['pm25', 'pm10', 'so2', 'no2', 'co', 'o3']
    corr_matrix = df[pollutants].corr()
    
    plt.figure(figsize=(10, 8))
    sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', 
               vmin=-1, vmax=1, center=0)
    plt.title('主要空气污染物相关性分析')
    plt.tight_layout()
    plt.show()

4. 应用扩展:构建实用的空气质量监测系统

4.1 自动化数据更新与预警

我们可以将爬虫部署到服务器,实现每日自动更新:

import schedule
import time

def daily_update_job():
    today = datetime.now().strftime('%Y-%m-%d')
    data = fetch_history_data('101010100', today, today)  # 北京城市代码
    cleaned = clean_air_data(data)
    update_database(cleaned)

# 每天上午10点执行
schedule.every().day.at("10:00").do(daily_update_job)

while True:
    schedule.run_pending()
    time.sleep(60)

4.2 开发简单的Web展示界面

使用Flask可以快速构建一个数据展示页面:

from flask import Flask, render_template
import pandas as pd

app = Flask(__name__)

@app.route('/')
def dashboard():
    with closing(sqlite3.connect('air_quality.db')) as conn:
        df = pd.read_sql("""
        SELECT date, aqi, pm25, quality_level 
        FROM air_quality 
        ORDER BY date DESC 
        LIMIT 30
        """, conn)
    
    return render_template('dashboard.html', 
                         data=df.to_dict('records'))

if __name__ == '__main__':
    app.run(debug=True)

4.3 空气质量预测模型初探

基于历史数据,我们可以尝试构建简单的预测模型:

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

def build_prediction_model(df):
    # 特征工程
    df['date'] = pd.to_datetime(df['date'])
    df['day_of_year'] = df['date'].dt.dayofyear
    df['month'] = df['date'].dt.month
    df['year'] = df['date'].dt.year
    
    # 准备特征和目标变量
    features = ['day_of_year', 'month', 'year', 
               'pm10', 'so2', 'no2', 'co', 'o3']
    X = df[features]
    y = df['pm25']
    
    # 划分训练测试集
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42)
    
    # 训练模型
    model = RandomForestRegressor(n_estimators=100, random_state=42)
    model.fit(X_train, y_train)
    
    return model, X_test, y_test

在实际项目中,我发现PM2.5数据有明显的季节性特征,冬季浓度通常比夏季高2-3倍。通过持续监测,可以明显看到近几年空气质量改善的趋势,特别是在实施严格环保政策的时期,数据变化尤为显著。这个系统不仅帮助我了解了空气质量的宏观变化,还让我养成了关注环境数据的习惯。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐