简介面向高校数据科学与大数据专业学生的Python爬虫与可视化课程设计资源以中国城市PM2.5值为分析对象完整覆盖数据采集、清洗、分析到可视化展示流程配有注释清晰的源码与满分实验报告可直接作为期末大作业或课程设计高分参考。包体共21个文件以13个HTML可视化页面为主体配合5个CSV城市监测数据、2个DOCX报告文档及1个主程序PY文件压缩包整体仅6.13MB部署轻量、结构规整适合新手按模块理解并二次修改。目前已有352人学习使用资源在数据指标解读、多城市对比呈现及项目文档撰写方面均有较好示范性兼顾代码实用性与报告规范度是快速完成同类型实训任务的高性价比选择。1. 期末大作业选 Python 爬虫 PM2.5 可视化图的不是新鲜是可复现期末大作业选「基于 Python 爬虫的中国城市 PM2.5 值数据可视化分析」最怕的不是爬不到数据而是爬完数据发现画不出能讲出结论的图。这个题目的价值在于它完整覆盖了数据采集、清洗、存储、聚合、可视化的全链路难度适中又天然自带「空气质量」这个有讨论度的话题答辩时容易讲出东西。适合 Python 基础薄弱但想拿高分、或者第一次接触爬虫和数据可视化、需要一个能一口气跑通的项目练手的同学。整条链路里真正决定分数的环节只有两个能不能稳定抓到全量数据以及图表能不能支撑起一句明确的结论。本篇按「选型 → 采集 → 清洗 → 出图 → 排错 → 写报告」的顺序把我自己做这个题的做法完整讲一遍代码可以直接抄坑提前告诉你。2. 抓全国城市 PM2.5 数据requests XPath 的采集实现与反爬边界2.1 先选数据源静态表格页、更新频率、字段丰度怎么权衡做爬虫类大作业第一步不是写代码是选数据源。PM2.5 数据在国内有几个常见来源中国环境监测总站、空气质量在线监测分析平台、青悦开放数据、和蔚蓝地图 API。我的建议是选静态 HTML 表格页原因是期末大作业的网络环境不稳定API 可能要求 token也可能限频而静态页只要 requests 能拿到 HTML 就能解析调试成本最低。我一般选「空气质量在线监测分析平台」的全国城市实时数据页它的核心优势有三个一是无需登录直接 GET 就能返回完整表格二是数据更新频率达到小时级足够支撑「最近24小时趋势」这类分析三是字段覆盖城市、日期、AQI、PM2.5、PM10、SO₂、NO₂、CO、O₃一份数据表能画出多种图。反爬方面它的页面没有验证码也没有高频 IP 封禁策略用正常浏览器 UA 就能访问算是期末场景里性价比最高的目标。选站点时要避开两类一类是大量使用 JavaScript 异步渲染的页面数据藏在 XHR 接口里需要额外抓接口复杂度直接翻倍另一类是 PDF 或图片报表解析成本高且容易翻车。静态表格页是爬虫大作业的舒适区先跑通链路再考虑加难度。2.2 用 requests 建立会话并伪装请求头拿到页面先保存成 HTMLimport requests # 建立会话自动管理 cookies session requests.Session() # 伪装成浏览器避免被服务端识别为脚本请求 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } url https://www.aqistudy.cn/html/city_realtime.php # 示例城市实时数据页 # 防止请求超时导致整个程序中断设置 timeout resp session.get(url, headersheaders, timeout10) # 判断响应状态200 才继续 if resp.status_code 200: # 先确认页面编码中文站常见 utf-8 或 gb2312 print(页面编码:, resp.encoding) # 把原始 HTML 存下来方便后续离线调试解析逻辑 with open(pm25_page.html, w, encodingutf-8) as f: f.write(resp.text) print(页面保存成功) else: print(请求失败状态码:, resp.status_code)这段代码有几个关键参数需要注意timeout必须设否则某个城市页面卡住时整个程序会挂在那里不动resp.encoding是中文网页最常见的一个隐患点如果打印出来是ISO-8859-1说明 requests 猜错了编码需要手动从响应头或页面 meta 标签里找真实编码否则后续 XPath 解析出来全是乱码。另外我习惯把 HTML 先存成本地文件再解析不是多此一举——反爬策略通常在多次请求后才触发本地留存一份原始页面排查「到底是请求被拦截了还是解析写错了」时会省大量时间。2.3 XPath 解析表格text() 与 normalize-space 配合清洗拿到 HTML 后用 lxml 的 XPath 解析比正则表达式稳定得多。表格结构通常是table里套tr和td用 XPath 可以直接按行提取。这里有一个非常容易翻车的写法误区直接用//td/text()提取单元格文本时如果单元格内部有span或a标签text() 只能拿到直接子文本拿不到嵌套标签里的内容导致字段残缺。from lxml import html # 读取本地保存的页面避免重复请求 with open(pm25_page.html, r, encodingutf-8) as f: page_content f.read() # 解析 HTML 为 Element 对象 tree html.fromstring(page_content) # 定位表格里的所有行 rows tree.xpath(//table//tr) data [] for row in rows: # 用 string() 提取整行所有文本再压缩连续空白 cells row.xpath(.//td) if len(cells) 5: continue # 跳过表头或残缺行 line [] for cell in cells: # normalize-space 去除首尾空白并把内部连续空格压缩为单个 text .join(cell.xpath(.//text())).strip() text .join(text.split()) line.append(text) # 城市、日期、AQI、PM2.5 是后续分析必备字段 if len(line) 5 and 城市 not in line[0]: data.append(line) print(解析到数据行数:, len(data)) print(第一条示例:, data[0] if data else 无数据)这里的核心技巧是 .join(cell.xpath(.//text()))它等价于把单元格内所有层级的文本拼接起来再用split()清理空白弥补了text()拿不到嵌套文本的缺陷。string()函数也能做类似的事但它在处理复杂嵌套时会把多余空白一起带进来反而要多做一步清洗。解析完先打印行数和第一条数据确认字段对齐后再往下走不要在数据还没验证的情况下直接写 CSV。2.4 清洗字段并落 CSVpandas 去重与时间字段标准化采集到的原始数据不能直接进可视化因为同一个城市在页面上可能出现两次不同监测站点时间字段也可能是「2025-06-01 12:00」这类字符串需要先统一成标准格式。我用 pandas 做这一步代码短且后续聚合方便。import pandas as pd # 把解析结果转成 DataFrame按页面实际列顺序命名 df pd.DataFrame(data, columns[ 城市, 时间, AQI, PM2.5, PM10, SO2, NO2, CO, O3 ]) # 转数值类型遇到无法转换的置为 NaN后续统一处理 for col in [AQI, PM2.5, PM10, SO2, NO2, CO, O3]: df[col] pd.to_numeric(df[col], errorscoerce) # 按城市时间去重保留第一次出现的记录 df df.drop_duplicates(subset[城市, 时间], keepfirst) # 时间字段标准化为 pandas 的 datetime 类型 df[时间] pd.to_datetime(df[时间]) # 按城市排序方便肉眼抽查 df df.sort_values([城市, 时间]).reset_index(dropTrue) # 输出 CSV 用 utf-8-sig否则 Excel 打开中文会乱码 df.to_csv(pm25_cities.csv, indexFalse, encodingutf-8-sig) print(清洗后数据条数:, len(df)) print(df.head())注意to_numeric(errorscoerce)会在字段里混入「暂无数据」这类占位文本时把它转成 NaN而不是让整个程序崩溃。drop_duplicates的subset参数指定了去重依据不写的话遇到两行完全相同的记录才会去重达不到按城市时间去重的效果。encodingutf-8-sig是 CSV 落地时最容易忽略的细节不写的话 Windows 上双击 CSV 文件会出现中文乱码影响后续检查数据质量。这一步结束数据链路就已经完整了接下来的所有分析都不需要再碰网络请求。3. 把 CSV 画成能讲故事的图pandas 聚合 pyecharts 出图3.1 预处理阶段缺失值填充与 AQI 等级字段构造读完 CSV 先别急着画图先处理缺失值和构造分级字段。PM2.5 数据经常出现个别城市某小时缺失的情况直接画折线图会出现断点影响观感。我的做法是如果缺失比例低于 5%用前后均值填充如果某个城市缺失超过 30%直接剔除不硬填。import pandas as pd df pd.read_csv(pm25_cities.csv, encodingutf-8-sig, parse_dates[时间]) # 查看缺失情况决定填充策略 print(df.isnull().sum()) # 按城市分组用相邻时间点的均值填充 PM2.5 缺失值 df[PM2.5] df.groupby(城市)[PM2.5].transform( lambda x: x.fillna(x.interpolate()) ) # 剔除 PM2.5 仍然为空的记录 df df.dropna(subset[PM2.5]) # 构造 AQI 等级字段用于后续分组统计 def aqi_level(aqi): if aqi 50: return 优 elif aqi 100: return 良 elif aqi 150: return 轻度污染 elif aqi 200: return 中度污染 else: return 重度污染 df[AQI等级] df[AQI].apply(aqi_level) print(df[AQI等级].value_counts())groupby transform的组合是这里的关键它按城市分组后对每个组内部执行插值不会出现用北京的数据去填充上海缺失值这种跨城市污染。interpolate()默认线性插值对小时级的 PM2.5 变化曲线来说足够平滑。AQI 分级字段的构造要跟国家 HJ 633-2012 标准对齐这个字段在答辩时可以拿来回答「AQI 和 PM2.5 浓度有什么关系」。3.2 城市排行与时间趋势Bar Line 的配置要点pyecharts 是期末大作业出图的首选因为它的图是 HTML 交互式图表浏览器打开后可以悬停看数据答辩演示效果好。注意 pyecharts 的版本v1.x 和 v0.5.x 的 API 差异很大现在安装默认是 1.x代码按 1.x 写法来。from pyecharts.charts import Bar, Line from pyecharts import options as opts # 取每个城市当天所有小时的平均值作为日均代表 city_daily df.groupby(城市)[PM2.5].mean().round(1).sort_values(ascendingFalse) city_names city_daily.index.tolist() city_values city_daily.values.tolist() # 横向柱状图展示污染最重的 15 个城市 bar ( Bar() .add_xaxis(city_names[:15]) .add_yaxis(PM2.5 日均值, city_values[:15]) .reversal_axis() # 横向布局城市名显示更完整 .set_series_opts(label_optsopts.LabelOpts(positionright)) .set_global_opts( title_optsopts.TitleOpts(title中国城市 PM2.5 日均值 TOP15), yaxis_optsopts.AxisOpts(name城市), xaxis_optsopts.AxisOpts(namePM2.5 浓度μg/m³), ) ) bar.render(pm25_top15.html)排名图能回答「哪个城市最脏」的问题但时间趋势才能回答「空气质量在变好还是变坏」的问题。取一个重点城市比如石家庄画全天趋势线one_city df[df[城市] 石家庄].sort_values(时间) line ( Line() .add_xaxis(one_city[时间].dt.strftime(%H:%M).tolist()) .add_yaxis( PM2.5 浓度, one_city[PM2.5].tolist(), is_smoothTrue, linestyle_optsopts.LineStyleOpts(width2), ) .set_global_opts( title_optsopts.TitleOpts(title石家庄当日 PM2.5 小时变化), yaxis_optsopts.AxisOpts(name浓度μg/m³), tooltip_optsopts.TooltipOpts(triggeraxis), ) ) line.render(pm25_trend.html)两个图的关键差异在label_opts和tooltip_opts。横向柱状图必须把 label 放在柱子右侧不然图表里数值被柱子挡住答辩一眼看不清折线图的triggeraxis让鼠标悬停时同时显示横轴附近所有数据点比默认的item触发更直观。3.3 地理分布Map 的地图注册与分级配色地理分布是 PM2.5 可视化里最出效果的一张图但也是配置最繁琐的一张。pyecharts 1.x 的地图数据包和主库分开直接渲染中国地图大概率是空白页要先安装地图扩展包。pip install pyecharts pip install echarts-countries-pypkg pip install echarts-china-provinces-pypkg pip install echarts-china-cities-pypkgfrom pyecharts.charts import Map from pyecharts import options as opts # 城市名需要带省前缀否则地图匹配不上 province_map { 北京: 北京市, 上海: 上海市, 广州: 广东省, 深圳: 广东省, 石家庄: 河北省, 太原: 山西省, } city_avg df.groupby(城市)[PM2.5].mean().round(1) map_data [] for city, value in city_avg.items(): full_name province_map.get(city, city) map_data.append((full_name, value)) m ( Map() .add( PM2.5 浓度, map_data, maptypechina, ) .set_global_opts( title_optsopts.TitleOpts(title全国城市 PM2.5 分布), visualmap_optsopts.VisualMapOpts( min_0, max_150, is_piecewiseTrue, pieces[ {min: 0, max: 35, label: 优}, {min: 36, max: 75, label: 良}, {min: 76, max: 115, label: 轻度污染}, {min: 116, max: 150, label: 中度污染}, ], ), ) ) m.render(pm25_map.html)地图的两个关键参数is_piecewiseTrue让颜色分成离散区间而不是连续渐变色因为不同空气质量等级的色差对答辩观感影响很大pieces的分段要跟前面构造的 AQI 等级保持一致否则图和正文结论对应不上是答辩时最容易被老师挑出来的硬伤。省前缀的映射表也是必要的pyecharts 内置地图只认「北京市」「广东省」这样的完整行政区名不认「北京」这种简称不做映射地图上会大片空白。3.4 一个容易被扣分的点图和结论对不上很多拿到及格分而不是满分的大作业问题出在最后图都画出来了但每一张图旁边没有写它说明了什么。柱状图讲了「石家庄最脏」结论里却写「华北地区污染严重」——这个推断需要至少三天以上的数据支撑单日数据得不出区域结论老师一问就露馅。正确做法是每张图配一句「这张图证明了什么」而且要跟你的分析口径一致。比如折线图写「石家庄 14 时出现 PM2.5 峰值 148μg/m³属于中度污染」这就是能站得住的具体结论而不是「空气质量需要关注」这种正确的废话。4. 期末大作业排错记录这 5 个坑让爬虫和出图双双翻车4.1 页面编码识别错误解析出来全是乱码现象print(resp.encoding)返回ISO-8859-1把文本写入文件后中文全变成â开头的乱码。原因requests 从响应头猜测编码时优先看charset部分站点没在响应头里声明编码requests 的默认猜测机制就猜错了。中文网页实际用的是 UTF-8但 HTML 里只在meta charset标注了requests 不会主动去解析 meta 标签。解决不要信任resp.encoding用resp.apparent_encoding做二次确认或者直接根据页面已知信息手动指定resp.encoding utf-8。我一般写完代码先打印前 200 个字符确认中文正常再继续。4.2 XPath 用了//td/text()嵌套标签内容丢失现象单元格里明明有「石家庄 2025-06-01 12:00」两段内容解析出来只剩「石家庄」时间字段集体消失。原因text()在 XPath 里只取当前节点的直接文本子节点如果文本被span、a、font包住这些内容不属于td的直接文本节点。解决用 .join(cell.xpath(.//text()))取全部后代文本再拼接。这个写法的好处是无论嵌套多少层都能拿全坏处是可能带回多余空白所以后面必须接split()压缩。这两个操作必须连用少一个都会在数据清洗阶段出问题。4.3 pyecharts 出图空白但 HTML 文件存在现象render()正常执行浏览器打开 HTML 后图表区域是空白只有标题文字。原因大概率是地图相关依赖没装。pyecharts 1.x 把地图数据拆成了单独的 pypkg 包主库安装不会自动带地图而且class Map的引入路径在不同版本里有差异。解决先确认版本pip show pyecharts看是 1.x 还是 0.5.x。1.x 需要安装echarts-china-provinces-pypkg等扩展包0.5.x 用的是map.save_chart那套旧 API两套代码不兼容。检查顺序是先看控制台有没有报错再看import pyecharts是否正常最后查扩展包是否安装。4.4 CSV 用 Excel 打开乱码但 pandas 读取正常现象pd.to_csv写完文件pandas 读回来没事但用 Excel 打开直接乱码。原因pandas 默认的to_csv使用utf-8编码而 Excel 在 Windows 简体中文环境下默认用 ANSI 编码打开 CSV读不了 UTF-8 的 BOM 标记时就会按乱码显示。解决写文件时指定encodingutf-8-sig这个编码会在文件头部写入 BOM 标记Excel 识别到标记后就会自动用 UTF-8 解码。这个坑不影响程序运行但影响数据检查效率而且期末交作业时老师双击打开 CSV 看到乱码第一印象就打折了。4.5 城市名映射不完整地图上大片空白区域现象地图渲染出来了但只有北京、上海等几个城市有颜色其余地区全是灰色。原因pyecharts 的china地图使用完整的行政区划名但爬虫抓下来的表格里城市列用的是「北京」「深圳」这种简称而且爬虫不会自动补全城市所属省份需要自己构建映射关系。部分城市名如「阿坝」与地图内置名称不一致时也会空白。解决先打印df[城市].unique()拿到全部城市名逐一检查哪些能匹配地图坐标系、哪些不能。偷懒的做法是只展示能匹配的城市并在图上标注严谨的做法是构建完整映射表。大作业里建议选 30 个左右省会级城市做地图城市少且名字规范匹配率高展示效果也够。5. 把实验报告写成满分样子的几个落地技巧实验报告是这个大作业里占比 40% 的一部分很多学生代码写完了报告却只写三四页得分直接掉档。满分报告和普通报告的核心差别在于「有没有证据链」你的每个结论旁边必须有一张对应的图每张图必须有对应的数据支撑说明。我的习惯是按五个模块组织报告内容分别在「需求分析」里写清楚数据来源选型理由在「采集模块」里附上核心爬虫代码和本地保存的原始 HTML 截图在「数据处理」里放清洗前后的对比表在「可视化分析」里每张图配一段 3〜4 行的解读在「总结」里写一条明确的结论和一条明确的不足。代码不要全文贴选 requests 会话、XPath 解析、pyecharts 地图注册这三段最能体现技术含量的贴就行其他只写函数名和作用。报告里的截图不要一张图拍到底要按「终端输出 → 图表整体 → 图表局部细节」三步截。第一张证明程序跑通第二张证明图表完整第三张放大关键区域证明数据确实在图里可见。网站 HTML 表格也截一张标注出你抓的是哪个区域证明数据来源真实可溯。结尾可以写一句「本作业在数据获取环节受限于城市数量未覆盖全部县级市后续可通过增加采集频次和城市列表扩展分析维度」。这句是加分的它表明你知道这个方案的边界在哪里而不是以为它完美无缺。这也是我做完这类项目后最深的感受——一个能讲清楚边界的大作业往往比功能堆得更满但不谈局限的作业拿到更高的分。希望帮到你。本文还有配套的精品资源点击获取