Python爬虫实战:中国城市PM2.5数据采集与可视化分析

发布时间:2026/9/23 12:28:56
Python爬虫实战:中国城市PM2.5数据采集与可视化分析 简介一份基于Python爬虫的中国城市PM2.5值数据可视化分析项目面向高校Python数据分析、爬虫及可视化相关课程设计与期末大作业场景。整套方案覆盖从数据采集、清洗、存储到分析展示的完整流程资源包含Python主程序、多城市CSV格式历史数据、13个HTML分析图表页面以及Word实验报告和数据说明文档共21个文件压缩包大小约6.13MB。代码注释详细、结构清晰简单部署即可运行尤其适合新手学习爬虫请求、Pandas数据处理与ECharts可视化等关键知识点也可直接作为高分课设或大作业的参考模板。预览中可以看到北京、上海、广州、成都、沈阳等多城市PM2.5数据的可视化结果实验报告对项目背景、实现过程和结果分析均有论述目前已有352人学习下载具有较高的实用价值。1. 期末周最稳的一条数据链路爬虫 PM2.5 数据可视化期末周最怕的不是写不出代码而是交上去的作业代码跑不动、报告凑不够页数、答辩被问两句就接不上话。这门基于 Python 爬虫的中国城市 PM2.5 值数据可视化分析恰好是很多学校期末大作业的经典命题既有网络爬虫原理的实战环节又要有数据清洗和可视化展示产出物还得是一份像样的实验报告。一个反直觉的结论是真正拿高分的作业往往不是爬虫代码写得最炫的那个而是数据链路完整、图表能解释数据、报告逻辑闭环的那个。这篇文章就从 requests 爬虫采集、数据清洗、matplotlib 与 echarts 可视化一路写到满分实验报告的结构和答辩话术帮你把整条产线一次走通。适合正在准备期末大作业、课程设计或毕设起步阶段的学生也适合刚入门 Python 想找一个完整练手项目的读者。2. 采集层设计用 requests 抓取城市 PM2.5 历史数据2.1 为什么选 requests BeautifulSoup而不是 Scrapy做爬虫项目第一件事是选工具。很多教程一上来就推 Scrapy但对期末大作业来说Scrapy 引入的组件太多——引擎、管道、中间件、Item Loader写起来体面答辩时却很难讲清楚“每一行代码在干什么”。常见做法是用 requests 发送 HTTP 请求配合 BeautifulSoup 解析 HTML 表格整套代码不超过 200 行逻辑一目了然。requests 的学习成本低出错也好排查这一点在答辩现场非常重要。老师问“你这条数据是怎么拿到的”你可以直接说“requests 请求页面BeautifulSoup 定位表格行取每行的单元格文本”。另外这套组合对环境几乎没有额外要求只要 Python 装好pip install requests beautifulsoup4 两步就完事不涉及类似“python安装”时的版本坑也不需要在嵌入式或系统层面折腾依赖。用 Scrapy 写分布式爬虫确实更能体现工程能力但那个方向更适合毕设或实习项目。期末作业的评分逻辑是爬虫能稳定跑完、数据量足够、代码能现场演示。requests 在这三个维度上都是最稳的选择。它的网络爬虫原理非常直白构造请求头、发送请求、解析响应、提取数据每一步都可以在报告里对应一个小节评分老师看得到你的实现细节。2.2 目标数据源与采集策略城市列表、月度归档、逐日数据PM2.5 历史数据的公开来源很多常见做法是找那些把数据按“城市 月份归档”组织的空气质量历史网站。这类页面的结构一般是先有一个城市导航页列出所有城市及其链接点进城市后能看到按月份排列的归档链接再点进某个月份页面里就有一张包含每日 PM2.5、PM10、AQI、空气质量等级的表格。采集策略就按这三层设计每一层写一个函数返回结构化数据下一层消费上一层的结果。这样做的另一个好处是报告里好画流程图导航页解析、归档链接提取、每日数据解析、结果落盘四个模块层层递进。import requests from bs4 import BeautifulSoup import pandas as pd import time from random import uniform HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def get_city_links(city_index_url): 解析城市导航页返回 {城市名: 城市详情页URL} 字典 resp requests.get(city_index_url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) city_links {} for a in soup.select(a[href*city]): # 选择器按实际页面结构调整 name a.text.strip() href a.get(href) if name and href and href.startswith(http): city_links[name] href return city_links这段代码的核心逻辑是先给请求加一个常规的 User-Agent避免部分站点对裸 requests 请求直接返回 403通过 BeautifulSoup 的选择器定位所有指向城市页的链接。timeout10 表示超过 10 秒没响应就抛异常防止某个城市页面卡死整个循环resp.encoding utf-8 是强制指定编码比依赖 requests 自动推断要可靠尤其当页面没有明确声明 charset 时会翻车。城市导航页的链接选择器需要你打开浏览器检查一下实际页面结构不同的数据站点类名和路径不一样。我一般会先用浏览器开发者工具 CtrlShiftC 点一下城市名称看它在 HTML 里被包在什么标签里再决定用 select 还是 find_all这一步没有任何黑匣子但能省下大量调试时间。2.3 请求间隔、UA伪装与失败重试的合理参数抓取单个城市一年的数据大概会发出 12 次月份页请求如果扩展到 50 个城市就是 600 次请求。在没有请求间隔的情况下这个量级很容易触发站点的频率限制出现 403 或者被临时封 IP。解决方式很简单把请求间隔控制在 13 秒之间随机波动并给每个请求轮换不同的 User-Agent。import random UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36, ] def fetch_html(url, retries3, base_delay1.5): 带重试的HTML抓取请求失败后指数退避 for attempt in range(retries): try: headers {User-Agent: random.choice(UA_POOL)} resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text elif resp.status_code 403: print(f[403] {url}等待后重试) else: print(f[{resp.status_code}] {url}) except requests.RequestException as e: print(f[请求异常] {url}: {e}) time.sleep(base_delay * (2 ** attempt) uniform(0, 1)) return None关键参数有三个retries3 表示最多重试 3 次base_delay1.5 是初次重试的等待秒数第二次约 3 秒第三次约 6 秒指数退避的好处是给服务端留出恢复时间也避免连续快速重试加重封禁风险timeout10 必须设否则某个请求卡住时整个脚本会一直停在原地。每完成一次请求后主循环里加一句 time.sleep(uniform(1, 3))让请求节奏看起来更像真实用户操作。这套参数不是玄学而是大量实践里验证过的稳妥区间。间隔小于 0.5 秒容易被识别为脚本行为大于 5 秒又会让 600 个城市的采集时间变得不可接受。13 秒随机波动是血泪经验之后的折中值。采集过程中把每页的数据打印一条日志方便你看到进度也方便中断后续传。3. 数据清洗与存储把脏数据变成可分析的 DataFrame3.1 脏数据长什么样负一占位、空行、异常值爬下来的 PM2.5 数据永远不会是干干净净的表格。最常见的坑是数据源本身用 -1 表示“当天无监测数据”有些月份整行都是空的还有些日期只有 PM10 没有 PM2.5。如果你不做预处理直接拿去画图折线图上会出现一个扎眼的断崖条形图的排名也会被异常值带偏。出现 -1 的历史原因是部分监测站点早期设备故障或数据审核未通过数据源没有删行而选择用占位符填充。这个在设计爬虫时就要想到解析阶段把 -1 原样保存不要转成 0因为 0 在后续分析里会被当成有效浓度参与均值计算-1 起码能让你在清洗阶段一眼认出。异常值则是另一类问题。北方城市春季偶尔会有沙尘天气导致 PM2.5 瞬时超过 500 μg/m³这种数据不是错误是真实环境事件。要不要删取决于你的分析目标如果做年度趋势对比建议以 95 分位数做截尾处理而不是直接删行保留数据的整体分布形态如果做“是否达标”的统计就只保留国标定义的日均值 75 μg/m³ 判定逻辑不额外干预数值。3.2 清洗流水线的代码实现类型转换、缺失值处理、截尾清洗环节我用 pandas 一个工具走完解析日期、统一数值列类型、把 -1 替换成 NaN再按分位数做截尾。import pandas as pd import numpy as np def clean_pm25_data(df): 清洗PM2.5原始DataFrame返回可用于分析的标准数据 # 1. 标准列名避免不同页面字段顺序不一致 df.columns [city, date, pm25, pm10, aqi, level] # 2. 日期改成datetime类型解析失败的行直接丢弃 df[date] pd.to_datetime(df[date], errorscoerce, format%Y-%m-%d) df df.dropna(subset[date]) # 3. 数值列里的 -1 / 空字符串统一替换为 NaN numeric_cols [pm25, pm10, aqi] for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce) df.loc[df[col] -1, col] np.nan # 4. 对PM2.5做95分位数截尾处理极端值 upper_limit df[pm25].quantile(0.95) df[pm25_winsorized] df[pm25].clip(upperupper_limit) # 5. 生成月份字段后面可视化分组要用 df[month] df[date].dt.month df[year] df[date].dt.year return df.dropna(subset[city])第 2 步的 format%Y-%m-%d 是按常见日期格式解析如果你的数据源是 2024/1/1 这种斜杠格式format 参数要改成 %Y/%m/%d否则所有日期都会变成 NaT一整行数据就没了。这一步是全程最容易翻车的地方建议清洗后先打印 df[date].head() 肉眼确认一遍再继续。第 4 步的截尾逻辑我单独讲一下clip(upperupper_limit) 不是删数据而是把超过 95 分位的值压到 95 分位的位置这样既保留了数据点数量又不会让个别极端值把整个图表的 Y 轴拉爆。如果你做的是全年均值分析用截尾后的字段算均值会更稳。做“超标天数统计”时不建议截尾因为超标本身就是分析目标。3.3 存储选型CSV 够用SQLite 更体面数据清洗完成后首先要落一份 CSV这是交作业时必须有的中间产物证明你的爬虫真的跑出了数据。CSV 用一行 df.to_csv(pm25_all_cities.csv, indexFalse) 就能搞定字段顺序就是 DataFrame 的列顺序文件体积在 50 个城市单个年度规模下通常只有几兆打开方便老师也能直接看到。但 CSV 有两个短板一是重复运行脚本时会反复写整个文件不好增量追加二是如果要按城市、日期范围做条件查询每次都读全量文件再过滤效率低且代码难看。这里我推荐再加一层 SQLitePython 内置 sqlite3不需要额外起服务适合做一个“能查、能追加、能验证”的存储层。如果数据量大到 CSV 打不开也可以用 SQLAlchemy 统一管理入库逻辑SQLAlchemy 不仅支持 SQLite后续想切 MySQL 或 PostgreSQL 时不用改业务代码。import sqlite3 from sqlalchemy import create_engine # 方式A先用csv方式存一份原始结果 df_cleaned.to_csv(output/pm25_clean.csv, indexFalse, encodingutf-8-sig) # 方式B同时写入SQLite方便按城市/日期查询 engine create_engine(sqlite:///pm25.db) df_cleaned.to_sql(pm25_daily, conengine, if_existsappend, indexFalse) # 验证查北京市2024年1月的平均PM2.5 query SELECT city, date, pm25_winsorized FROM pm25_daily WHERE city 北京 AND date BETWEEN 2024-01-01 AND 2024-01-31 with sqlite3.connect(pm25.db) as conn: beijing_jan pd.read_sql_query(query, conn) print(beijing_jan.head())to_sql 的两个参数是经验值if_existsappend 表示每次运行脚本都追加而非覆盖这样跑多次采集任务不会丢历史数据但要注意重复运行同一时间段会产生重复记录所以我在实际项目里会先按 citydate 去重再入库indexFalse 别漏否则 pandas 会把行索引也写成表的一列后续查询时多出一个用不到的自增字段表结构看起来不专业。编码用 utf-8-sig 是为了让 Excel 直接打开 CSV 不乱码普通 utf-8 在 Windows 上双击打开时会把中文显示成乱码这个细节在交作业时很加分。顺带说明存 SQLite 后写报告还能多一段“数据库设计说明”把表结构、索引字段和查询语句写进去报告深度立刻上一个台阶。4. 可视化层从 matplotlib 基础图到 echarts 交互大屏4.1 必画三张图趋势折线、城市排名条形、季节分布箱线期末作业可视化部分至少要覆盖三个维度时间趋势、空间对比、分布形态。针对 PM2.5 数据我建议画三张图全国或重点城市的年度日均值折线图、城市年平均浓度横向条形图、按季节分组的箱线图。这三张图分别回答“污染是变好还是变差”“哪座城市最严重”“一年中哪个季节污染最集中”这三个问题。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] matplotlib.rcParams[axes.unicode_minus] False def plot_annual_trend(df): 按年份聚合全国PM2.5均值画趋势线 yearly df.groupby(year)[pm25_winsorized].mean().reset_index() fig, ax plt.subplots(figsize(10, 5)) ax.plot(yearly[year], yearly[pm25_winsorized], markero, linewidth2, color#d62728) ax.set_title(全国城市PM2.5年均浓度变化趋势, fontsize14) ax.set_xlabel(年份) ax.set_ylabel(PM2.5 年均浓度 (μg/m³)) ax.grid(True, linestyle--, alpha0.4) plt.tight_layout() plt.savefig(figures/annual_trend.png, dpi200) plt.show()两个环境相关的参数是血泪经验rcParams[font.sans-serif] 必须在导入后立即设置Windows 上优先用 SimHeimacOS 上 PingFang SC 更稳Linux 服务器上没有这两种字体时还得先安装中文字体包否则图表里的中文直接变成方框axes.unicode_minus 也必须设为 False否则坐标轴负号会显示成乱码方块。grid 的 alpha0.4 是为了让网格线不喧宾夺主dpi200 保证图片放大到论文里依旧清晰。这张趋势图的解读方式也有讲究如果折线整体向下走就在实验报告里结合国家大气污染防治行动计划的实施时间点做对照分析如果数据源覆盖年份不长就换“逐月对比”的逻辑比如对比 1 月和 7 月浓度体现出冬季采暖期对北方城市的影响。会解读数据比会画图更重要报告里每张图下面都应该有 34 行的分析文字。4.2 城市对比排名用条形图讲清“谁最严重”城市排名用横向条形图比纵向条形图更合适城市名较长时横向排布不会互相挤压。排序逻辑按年均浓度从高到低取前 15 名显示即可排名靠后的城市在图里占不到多少视觉权重。def plot_city_ranking(df, top_n15): 按城市年均PM2.5排序画横向条形图 city_avg df.groupby(city)[pm25_winsorized].mean().sort_values(ascendingFalse) city_avg city_avg.head(top_n)[::-1] # 翻转成从下往上增长 fig, ax plt.subplots(figsize(10, 8)) ax.barh(city_avg.index, city_avg.values, color#1f77b4) ax.set_title(f中国主要城市PM2.5年均浓度 TOP {top_n}, fontsize14) ax.set_xlabel(PM2.5 年均浓度 (μg/m³)) for i, v in enumerate(city_avg.values): ax.text(v 1, i, f{v:.1f}, vacenter, fontsize9) plt.tight_layout() plt.savefig(figures/city_ranking.png, dpi200) plt.show()head(top_n)[::-1] 这行是坑点也是亮点sort_values 默认从小到大排head 取前 15 名时拿到的是排位最高的 15 个但条形图需要最低值在底部最高值在顶部所以加一次反转。barh 的数值标签位置也调试过text(v1, i) 是在每个条形的右端多偏移 1 个单位显示数值v1 的 1 是根据浓度值量级定的如果你画的是其他指标记得按量级调整偏移量。这张图在报告里的角色是“结论型图表”适合放在实验报告的结果分析章节配一段文字说明排名前五的城市集中在哪个地理区域。这一步我一般会调取采集数据里城市所属省份字段做二次确认如果前线城市全部集中在华北平原这个结论就非常有说服力。4.3 用 pyecharts 做可交互的 echarts 数据可视化页面matplotlib 画完静态图后建议再用 pyecharts 做一张可交互的图表直接渲染成 HTML 文件。一方面是因为期末答辩现场如果只给老师看图片说服力远不如现场打开一个能缩放、能悬浮显示具体数值的页面另一方面毕业设计或课程设计报告里通常有“系统展示”这个章节嵌入一张 echarts 数据可视化作品会让整份报告显得更完整。from pyecharts.charts import Bar from pyecharts import options as opts def make_interactive_bar(city_avg_df): 生成城市年均浓度交互条形图渲染为HTML cities city_avg_df[city].tolist() values [round(v, 1) for v in city_avg_df[pm25_winsorized].tolist()] bar Bar() bar.add_xaxis(cities) bar.add_yaxis(PM2.5年均浓度, values, category_gap30%) bar.set_global_opts( title_optsopts.TitleOpts(title中国主要城市PM2.5年均浓度对比), yaxis_optsopts.AxisOpts(namePM2.5 (μg/m³)), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), ) bar.set_series_opts(label_optsopts.LabelOpts(is_showFalse)) bar.render(output/city_pm25_bar.html)注意这行代码基于 pyecharts 1.x 系列的 API。pyecharts 的版本迭代踩过很大的坑0.5.x 和 1.x 的引用路径完全不同网上大量旧教程用的是 from pyecharts import Bar在 1.x 里已经改成了 from pyecharts.charts import Bar。装的时候直接 pip install pyecharts 装的是最新版如果参照旧教程写代码第一行导入就会报错。这类因版本差异导致的 API 不兼容问题非常普遍后面避坑章节会展开细说。angular gtag js 无关忽略。Y 轴和 X 轴的属性名在 pyecharts 里也有讲究1.x 里所有配置统一走 opts 模块设计得相对规范。render() 默认在项目根目录输出 HTML 文件如果文件是空的先检查 Python 环境和浏览器是否正常再确认数据集是否为空——空数据配空图表是常事。5. 常见问题与避坑环境、反爬、版本冲突与报告截图5.1 爬虫跑一半被拒403 与 IP 临时封禁的处理现象脚本前几十个城市正常抓取跑到某个城市后连续返回 403最后所有请求都失败。原因请求频率虽然做了随机延迟但 50 个城市连续采集的累计请求量仍然触发站点的访问频率限制站点对当前 IP 做了临时封禁。解决停掉脚本等待 1015 分钟再继续同时把 base_delay 从 1.5 调到 3.0将每个城市内部的请求间隔也提高到 24 秒区间并启用 UA_POOL 轮换。如果数据源城市数量大可以考虑拆成多个时间段分段采集而不是一股脑跑完。5.2 图表中文全部变成方框matplotlib 字体配置缺失现象生成的折线图和条形图标题、坐标轴标签全部显示为空心方框数字正常。原因matplotlib 默认字体是 DejaVu Sans不包含中文字形。解决在 plt 绘图前加上两个 rcParams 配置把字体切换为系统已有的中文字体。Windows 用 SimHeimacOS 用 PingFang SCLinux 需要先通过 fc-list :langzh 查看系统是否有中文字体没有就先安装 fonts-wqy-microhei。此问题排查时优先确认系统中文字体是否存在再排查 rcParams 配置是否写在了 plt.figure 之后——配置必须在绘制任何图形之前完成。5.3 pyecharts 版本升级后 API 全部对不上现象安装 pyecharts 后运行 from pyecharts import Bar 报 ImportError。原因pyecharts 0.5.x 和 1.x 的模块结构完全不兼容网上大量教程基于 0.5.x 编写。解决先确认自己安装的版本在命令行执行 pip show pyecharts如果是 1.x 系列所有导入要从 pyecharts.charts 下手。文件末尾再注明环境信息requests 2.28.x、BeautifulSoup4 4.12.x、pandas 2.0.x、pyecharts 1.9.x。这段环境说明还有额外价值实验报告的“开发环境”章节可以直接照搬老师看到你的依赖版本清晰可复现印象分不低。5.4 报告里的运行截图与代码逻辑不一致现象实验报告里贴的截图显示 50 个城市数据但代码清单里只写了 10 个城市或者图表是 2019—2024 年的代码注释却写着 2015-2020。很多同学交作业前临时改了参数重新截图忘记同步更新报告描述导致答辩时被追问漏洞。解决报告里所有截图、数字、图表数据必须来自同一次完整的运行记录。我通常的做法是一次性完整跑完全流程然后把运行日志、生成的 CSV 行数、每张图表的数据源范围记录到一个单独的“运行记录.txt”文件里写报告时严格对照这个文件填写。最后预演答辩时随机抽查报告中三个数字在代码里找到出处确保对得上。5.5 数据总量与图表量级对不上聚合后忘记保存中间结果现象爬虫跑了 5000 条数据清洗后只剩 3000 条画出来的排名图和趋势图都没有问题但报告里写的数据量还是 5000导致图表和文字矛盾。原因清洗阶段丢弃了含空值和异常值的行但实验报告记录了原始采集量而不是清洗后有效数据量。解决在清洗脚本里打印统计日志分别输出“采集总条数”“清洗后有效条数”“各城市缺失率”写报告时把这两个数字分开写。数据质量统计本身也是评分亮点放在实验报告的“数据预处理”章节里能体现专业性。6. 把实验报告写到能拿满分结构、图表编号与答辩准备6.1 期末作业报告五段式结构满分实验报告不需要花哨但要经得起逐条对照评分标准。我见过的高分报告通常都是五段式课题背景与目标、系统方案设计、爬虫与数据预处理、可视化分析与结论、附录。课题背景里写清楚为什么关注 PM2.5引用 23 个环境公报数据就够了不用堆砌。方案设计里放整体架构图和选型对比表一张表格对比 requests 和 Scrapy说明为什么在期末场景下选 requests。核心章节围绕爬虫采集、数据清洗、可视化三个模块展开每个模块包含核心代码、运行截图、输出结果表。结论处放三张图的分析文字对应“趋势、排名、季节”三个维度每张图写 34 行解读即可。附录放完整源码和运行环境说明。6.2 答辩前的查漏清单与现场演示技巧答辩前检查三件事第一代码能否从零开始跑通环境依赖是否有遗漏优先创建独立的虚拟环境验证第二准备好一张“失败截图”作为备答素材如果老师问反爬相关的问题你直接展示调试过程中遇到的 403 截图并说明解决思路效果比纯口述好得多第三把数据库里的查询语句准备好比如“北京市 2024 年 1 月的平均 PM2.5 是多少”现场用 SQLite 查一次给老师看这个动作能让整个项目从“会爬数据”升华到“会用数据”。如果报告中嵌入了 echarts 可视化 HTML答辩时记得展示页面交互效果这是加分项。做这类期末作业我的习惯是永远保留一份“能跑通的最小版本”。调参和美化可以放在后面慢慢做但只要最小版本的数据链路通着心里就不慌。把希望寄托在最后一晚通宵调试环境是这门课最不值得冒的风险。希望这个完整的实操方案能帮你把爬虫、数据清洗、可视化、实验报告一次走通交出一份自己心里有底的满分作业。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询