Python实战项目千千万但要说既练爬虫、学数据处理、又能产出漂亮图表的入门项目天气数据爬取数据可视化绝对是我最推荐的那一个。跑通以后拿到的不是一堆看不懂的控制台输出而是清清楚楚的折线图、柱状图甚至可以生成一个交互式HTML图表发给谁都能直接打开看。这个项目适合什么基础的人说实话只要会一点Python语法、装过第三方库哪怕完全没写过爬虫跟着做下来完全没问题。简单说这个项目做三件事程序自动向天气数据接口发起请求拿到指定城市的实时气温、湿度、风向、天气预报把数据清洗成规矩的结构存到CSV最后用matplotlib画出静态图表、用pyecharts生成交互式网页图表。我写这类入门项目有个原则代码不要太长但必须每一步都能解释清楚“为什么这么写”。下面的内容全部来自我实际调试过的方案包括城市ID怎么找、字段怎么清洗、中文乱码怎么处理、接口返回异常怎么办都是一步步踩过坑后沉淀下来的。1. 项目整体设计与技术选型思路1.1 为什么天气数据适合做爬虫实战很多初学者挑实战项目时容易走两个极端。一个极端是去爬那种需要复杂登录、滑块验证、加密参数的网站结果一周时间全耗在反爬对抗上最后连数据都拿不回来另一个极端是只读书上的教程跟着敲完一段代码却完全不知道怎么迁移到别的地方。天气数据恰好卡在中间它足够真实数据来自线上接口包含了真实的HTTP请求、JSON解析、异常处理这些核心环节它又足够简单不需要登录、不需要验证码、不需要处理动态渲染的复杂页面。更关键的是爬完之后的展示效果好——温度柱状图、温湿度散点图、温度变化折线图每一种都能让人直观感受到爬虫到底“爬”到了什么。把天气数据练熟之后再去爬电商商品、爬新闻资讯、爬各类公开数据接口思路是完全可以复用的。用个生活化的类比爬虫就像你雇了一个跑腿小哥你给他一个地址URL、告诉他带什么工具请求头headers、然后他从目的地取回一个快递响应数据你负责拆开快递箱JSON或HTML把有用的东西挑出来。搞清楚这条链路爬虫的骨架就立住了。1.2 技术栈选型为什么不用Scrapy框架这个项目我选了requests、csv、matplotlib、pyecharts这几个核心库刻意没有上Scrapy框架。很多人一上来就追求“工业级方案”结果被框架的并发模型、中间件、Pipeline绕得头晕。对一个小体量学习项目来说requests足够轻一行一个请求出问题能立刻定位Scrapy适合大规模采集、需要调度器和并发管理的场景等你理解单线程请求怎么处理、反爬怎么应对之后再上手Scrapy会顺畅很多。各模块职责如下表技术组件职责选型理由requests发送HTTP请求获取接口数据语法简洁正确处理Cookie、headers、超时设置json解析接口返回的JSON结构Python原生支持快速提取嵌套字段csv存储清洗后的天气数据通用性强Excel直接打开新人友好matplotlib绘制静态图表柱状图、散点图、折线图生态成熟中文资料多定制自由度大pyecharts生成交互式HTML图表图表酷炫支持鼠标悬浮、缩放适合展示整个项目的数据流是请求接口 - 拿到JSON - 解析字段 - 清洗单位与类型 - 写入CSV - 读取CSV - 画图。这样拆开之后爬虫和可视化两个模块可以独立调试后面想换数据源或者换图表类型都不会牵扯到对方。2. 数据采集模块从零开始爬天气数据2.1 先搞清数据从哪来开发者工具实战很多教程一上来直接甩给你一个URL然后说“爬这个”但URL背后怎么来的没人讲。实战里找数据源的方法比爬虫代码本身更重要。拿天气数据举例你可以直接在浏览器打开中国天气网的某个城市页面按F12进入开发者工具切到“网络”Network面板勾选“XHR”或“Fetch/XHR”类型的请求然后刷新页面。这时你会看到页面发起的各类请求很多数据其实是页面JavaScript脚本在加载完成后向后台接口发请求获取的。找到返回内容是JSON、且体积不大的请求右键复制URL用浏览器新标签页打开就能看到接口返回的原始数据结构。我在实际调试中发现天气网站有几个公开接口是非常稳定的。它们没有加密参数、不带签名、返回的就是标准JSON非常适合学习。比如以下两个接口实时天气http://www.weather.com.cn/data/sk/{城市ID}.html 当天预报http://www.weather.com.cn/data/cityinfo/{城市ID}.html其中“城市ID”是城市编码比如北京是101010100、上海是101020100。怎么找到这个ID在浏览器打开对应城市的天气页面URL里那串数字就是城市ID。例如 www.weather.com.cn/weather/101010100.shtml 这个地址里的 101010100就是北京的城市编号。注意接口字段在不同时间可能会有调整这不是问题——只要你会用开发者工具看真实返回字段变了改个名字就能继续跑。2.2 第一个爬虫请求实时天气接口先用最简代码把这个接口跑通。新建一个Python文件比如 weather_spider.py写入下面这段import requests city_id 101010100 # 北京 url fhttp://www.weather.com.cn/data/sk/{city_id}.html 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 } resp requests.get(url, headersheaders, timeout5) data resp.json().get(weatherinfo, {}) print(data)运行后你能看到类似这样的输出{ city: 北京, cityid: 101010100, temp: 24.1, WD: 南风, WS: 2级, SD: 23%, time: 11:00 }里面有实时温度temp、风向WD、风力WS、相对湿度SD。注意这里所有字段都是字符串这是原始数据的常态后面需要自己清洗。那两行headers里的User-Agent是干什么用的简单说服务器会通过这个字段识别客户端类型。很多网站会把默认的python-requests识别为非浏览器请求而直接拒绝加上一个常见浏览器的User-Agent就能规避大部分初级拦截。2.3 批量爬取多城市循环里最不能省的间隔单城市能爬到多城市就是加一个for循环的事。我准备一个城市ID字典覆盖北京、上海、广州、深圳、杭州、成都、武汉、南京八个城市这样后面画对比柱状图时数据更丰富视觉效果也会好很多。CITY_IDS { 北京: 101010100, 上海: 101020100, 广州: 101280101, 深圳: 101280601, 杭州: 101210101, 成都: 101270101, 武汉: 101200101, 南京: 101190101, }批量请求时我建议在每个请求之间主动加 time.sleep(2) 的间隔。这既是礼貌也是保护自己的手段。爬虫说到底就是向服务器发起大量请求如果你一秒内连刷上百次服务端日志里全是你轻则当前IP被临时限制重则触发风控规则影响后续访问。控制频率是爬虫的基本礼仪也是实战中必须养成的习惯。2.4 反爬意识与合规边界我在这里必须说清楚这个项目的数据来源于公开的天气接口接口本身允许匿名访问我把它作为教学案例的前提是“合理频次、个人学习、不涉及商业分发”。做爬虫一定要守住合规底线不爬需要登录才能看的内容、不绕过付费墙、不暴力请求导致对方服务异常、不对爬到的数据做商业化操作。实战中如果遇到接口突然拿不到数据先别怀疑自己代码大概率是触发频率限制或者接口改版了。触发限制后先停一段时间再试不要立刻换IP硬刚。改版了就用开发者工具重新找接口这个方法我在后面第6章还会详细讲。3. 数据清洗与CSV存储拿到原始数据后别急着画图3.1 原始数据长什么样洗净才能用爬虫拿到的JSON字段看起来清晰但离“能直接画图”还有几步。我实际运行后取到的一个完整字段集是这样的{ city: 北京, temp: 24.1, temp_high: 32℃, temp_low: 24℃, humidity: 23%, wind_dir: 南风, wind_level: 2级, weather: 多云转晴 }仔细看会发现问题不少。第一温度是字符串“24.1”画图时需要转成float。第二预报里最高温和最低温带着“℃”单位不处理的话像 int(32℃) 这种代码直接报错。第三湿度“23%”带百分号要转成23这样的整数才能计算。写一个专门的清洗函数是必要的别在主体逻辑里穿插replace和strip代码会乱成一锅粥。def clean_value(value, convert_typefloat): 去掉常见单位并转换类型转换失败返回0 try: cleaned str(value).replace(℃, ).replace(%, ).strip() return convert_type(cleaned) except (ValueError, TypeError): return 0这个函数能同时处理温度和湿度。单独拆出来还有一个好处如果换了数据源字段单位变了只需要改这一处不用动整个逻辑。3.2 用csv模块保存数据Excel直接能打开清洗完的数据我建议立刻落盘保存。原因很实际爬虫和可视化往往是分开调试的你不可能每调一次图标就重新爬一次数据把中间结果存下来能节省大量时间。CSV是我在这个阶段的首选格式Excel直接能打开肉眼检查数据是否合理非常方便。写入时有个容易被忽略的细节文件编码要指定为 utf-8-sig而不是默认的 utf-8。原因在于Excel打开utf-8编码的CSV时会出现中文乱码而utf-8-sig带上了BOM头Excel能正确识别中文。如果你用的是macOS的Numbers两者区别不大但在Windows上这个细节能避免一个看似“灵异”的乱码问题。import csv def save_to_csv(rows, filenameweather_data.csv): fieldnames [city, time, temp, temp_high, temp_low, humidity, wind_dir, wind_level, weather] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(rows)3.3 为什么建议存CSV而不是直接上数据库不少入门者一听到存储就想着MySQL、SQLite仿佛不用数据库就不专业。这个项目的数据量一天也就几百条MySQL完全是大炮打蚊子。CSV的好处是零配置、零依赖、人类可读学完CSV以后真需要上数据库时你会发现无非是把 csv.DictWriter 换成 INSERT 语句核心的数据结构是一样的。当然如果后续想做定时采集一个月的数据、再做历史趋势查询那迁移到SQLite成本也不高。SQLite就一个文件不用安装服务Python自带sqlite3模块属于下一步扩展最平滑的选择。4. 数据可视化让数字变成一眼能懂的图表4.1 中文乱码问题必须先解决再画图matplotlib画中文第一步不是调数据而是设置中文字体。直接跑默认配置画图图上的“北京”“温度”全变成一个个方框这是matplotlib的默认字体里没有中文字符导致的。解决办法是在画图前加两行配置plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False # 解决负号显示成方框的问题SimHei是黑体Windows系统基本都自带Microsoft YaHei是微软雅黑PingFang SC是macOS的字体。把它们罗列在一起程序会自动在系统里找一个可用的。第二行axes.unicode_minus设置为False很重要否则坐标轴上的负数会变成乱码一样的方框。如果你在Linux服务器上运行没有Windows或macOS的中文字体那就需要先把中文字体安装到系统里比如通过fontconfig安装文泉驿微米黑然后把字体名填进sans-serif列表。这个步骤一度是我在服务器上跑脚本时踩过最大的坑。4.2 多城市温度柱状图最直观的效果图清洗和存储都搞定后读取CSV画图。用matplotlib画一个多城市实时温度对比柱状图这应该是整个项目里最快出效果的部分import csv import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False with open(weather_data.csv, r, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) cities [row[city] for row in rows] temps [float(row[temp]) for row in rows] plt.figure(figsize(10, 5)) bars plt.bar(cities, temps, colorskyblue, width0.5) plt.title(多城市实时温度对比, fontsize16) plt.ylabel(温度(℃)) plt.ylim(0, max(temps) 5) for bar, temp in zip(bars, temps): plt.text(bar.get_x() bar.get_width() / 2, bar.get_height() 0.5, f{temp}℃, hacenter, fontsize12) plt.tight_layout() plt.savefig(temperature_bar.png, dpi200) plt.show()运行后你会得到一个柱状图文件每根柱子顶部标了具体温度城市名正常显示中文。保存文件时用 dpi200 是为了保证图片导出来够清晰放进课程报告或博客里都不糊。4.3 进阶玩法用pyecharts生成可交互图表matplotlib的图是静态图片适合打印和嵌入文档。但如果想给别人展示、或者想做出那种鼠标悬浮就显示数值的酷炫效果我强烈建议加一个pyecharts版本。pyecharts生成的图表是一个HTML文件双击就能在浏览器里打开完整保留交互能力。from pyecharts.charts import Bar from pyecharts import options as opts cities [北京, 上海, 广州] temps [24.1, 26.5, 29.0] bar ( Bar() .add_xaxis(cities) .add_yaxis(实时温度(℃), temps) .set_global_opts( title_optsopts.TitleOpts(title多城市实时温度对比), yaxis_optsopts.AxisOpts(name温度(℃)), ) ) bar.render(temperature_interactive.html)pyecharts的语法风格和matplotlib差异很大它采用链式调用每行一个配置项。初看可能不习惯但它的官方文档和示例非常齐全学会一个图之后其余图都是换汤不换药。这里我把折线图、饼图、地图都留给读者自己去扩展——数据和图表逻辑已经搭好换图类型其实就是换一下组件名。4.4 让折线图派上用场连续采集看温度走势细心的你可能会问柱状图和散点图用一次采集的数据就够了折线图怎么画折线图适合展示时间序列比如记录同一个城市每隔10分钟的温度累积成一条曲线能看出昼夜温度变化趋势。做法很简单写一个循环采集函数调用前面写的 get_weather()把每次得到的温度追加到列表然后用 plt.plot() 画折线。我试过跑两小时拿到的曲线能明显看出午后升温、傍晚回落的规律。这一步完全复用前面的采集函数只不过从“批量采集多个城市”变成“单城市多次采集”。在代码结构上因为采集逻辑已经拆成了独立的函数多城市和多时段两种模式只需要调整调用方式不需要重写爬虫这就是模块化的好处。5. 完整代码整合与运行指南5.1 项目文件结构建议把代码拆成两个文件互不干扰。这是我的目录结构weather_project/ ├── weather_spider.py # 采集 清洗 存储 ├── weather_charts.py # 读取CSV 绘图 ├── weather_data.csv # 运行后生成的数据文件 ├── temperature_bar.png # 运行后生成的静态图表 └── temperature_interactive.html # 运行后生成的交互式图表拆分文件的一个额外好处是以后爬另一个站点时weather_charts.py基本不用改只需要让新爬虫输出同样结构的CSV。5.2 采集与存储完整代码下面是采集部分完整代码整合了第2章、第3章的内容加了异常处理和打印日志复制就能用。# -*- coding: utf-8 -*- 天气数据采集 清洗 CSV存储完整示例 数据来源中国天气网公开接口 仅供个人学习使用请控制请求频率 import csv import time from datetime import datetime import requests CITY_IDS { 北京: 101010100, 上海: 101020100, 广州: 101280101, 深圳: 101280601, 杭州: 101210101, 成都: 101270101, 武汉: 101200101, 南京: 101190101, } 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, Referer: http://www.weather.com.cn/, } def clean_number(value, convert_typefloat): 去掉℃、%等单位并转换类型 try: return convert_type(str(value).replace(℃, ).replace(%, ).strip()) except (ValueError, TypeError): return 0 def get_weather(city_name, city_id): 获取单个城市的实时天气和当天预报 sk_url fhttp://www.weather.com.cn/data/sk/{city_id}.html info_url fhttp://www.weather.com.cn/data/cityinfo/{city_id}.html result { city: city_name, time: datetime.now().strftime(%Y-%m-%d %H:%M:%S), } try: sk_resp requests.get(sk_url, headersHEADERS, timeout5) sk_data sk_resp.json().get(weatherinfo, {}) result[temp] clean_number(sk_data.get(temp, 0)) result[humidity] clean_number(sk_data.get(SD, 0%), int) result[wind_dir] sk_data.get(WD, ) result[wind_level] sk_data.get(WS, ) info_resp requests.get(info_url, headersHEADERS, timeout5) info_data info_resp.json().get(weatherinfo, {}) result[temp_high] clean_number(info_data.get(temp1, 0℃), int) result[temp_low] clean_number(info_data.get(temp2, 0℃), int) result[weather] info_data.get(weather, ) except Exception as exc: print(f[警告] 采集 {city_name} 失败: {exc}) return None return result def collect_batch(city_dict, interval2): 批量采集多城市天气数据 rows [] for city, city_id in city_dict.items(): data get_weather(city, city_id) if data: rows.append(data) print(f已采集 {city}: 实时{data[temp]}℃, f最高{data[temp_high]}℃, 天气{data[weather]}) time.sleep(interval) return rows def save_to_csv(rows, filenameweather_data.csv): 保存到CSVutf-8-sig编码防止Excel中文乱码 if not rows: print(没有数据可保存) return fieldnames [city, time, temp, temp_high, temp_low, humidity, wind_dir, wind_level, weather] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(rows) print(f数据已保存到 {filename}) if __name__ __main__: all_data collect_batch(CITY_IDS) save_to_csv(all_data)5.3 可视化完整代码再新建 weather_charts.py用来读取weather_data.csv并画图。# -*- coding: utf-8 -*- 读取天气CSV并绘制图表 包含matplotlib静态图 pyecharts交互图 import csv import matplotlib.pyplot as plt from pyecharts import options as opts from pyecharts.charts import Bar def load_csv(filenameweather_data.csv): 读取CSV数据 with open(filename, r, encodingutf-8-sig) as f: return list(csv.DictReader(f)) def setup_chinese_font(): 设置matplotlib中文字体解决乱码 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False def draw_bar_temp(rows): 多城市实时温度柱状图 setup_chinese_font() cities [row[city] for row in rows] temps [float(row[temp]) for row in rows] plt.figure(figsize(10, 5)) bars plt.bar(cities, temps, colorskyblue, width0.5) plt.title(多城市实时温度对比, fontsize16) plt.ylabel(温度(℃)) plt.ylim(0, max(temps) 5) for bar, temp in zip(bars, temps): plt.text(bar.get_x() bar.get_width() / 2, bar.get_height() 0.5, f{temp}℃, hacenter, fontsize12) plt.tight_layout() plt.savefig(temperature_bar.png, dpi200) plt.show() def draw_scatter(rows): 气温与相对湿度散点图 setup_chinese_font() temps [float(row[temp]) for row in rows] humis [float(row[humidity]) for row in rows] cities [row[city] for row in rows] plt.figure(figsize(10, 5)) plt.scatter(temps, humis, s80, colorcoral, alpha0.7) for i, city in enumerate(cities): plt.annotate(city, (temps[i], humis[i]), textcoordsoffset points, xytext(5, 5), fontsize11) plt.xlabel(气温(℃)) plt.ylabel(相对湿度(%)) plt.title(气温与相对湿度关系散点图, fontsize16) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(temp_humidity_scatter.png, dpi200) plt.show() def draw_interactive_bar(rows): pyecharts交互式柱状图生成HTML cities [row[city] for row in rows] temps [float(row[temp]) for row in rows] bar ( Bar() .add_xaxis(cities) .add_yaxis(实时温度(℃), temps) .set_global_opts( title_optsopts.TitleOpts(title多城市实时温度对比), yaxis_optsopts.AxisOpts(name温度(℃)), ) ) bar.render(temperature_interactive.html) print(交互式图表已生成: temperature_interactive.html) if __name__ __main__: data_rows load_csv() if data_rows: draw_bar_temp(data_rows) draw_scatter(data_rows) draw_interactive_bar(data_rows) else: print(CSV中没有数据请先运行 weather_spider.py)5.4 三步跑通整个项目安装依赖这一步只装三个库就够了其他都是Python标准库pip install requests matplotlib pyecharts然后按顺序运行python weather_spider.py python weather_charts.py第一步会在控制台打印每个城市的采集结果同时生成weather_data.csv。第二步会弹出两个matplotlib窗口或保存PNG图片并生成temperature_interactive.html。整个流程跑下来从请求接口到生成可视化图表完整闭环就打通了。如果电脑同时装了Python 2和Python 3或者走的是conda环境命令可能是 python3 或 conda activate 之后再执行。确保 pip install 装到的环境和你运行脚本的环境一致这个基础问题反而是新手最常见翻车点。6. 常见问题与排查技巧实录6.1 高频报错速查表这个项目在实际运行中最容易遇到的报错我整理成了一张表每一个都是我见过的真实错误报错信息可能原因解决办法requests.exceptions.ConnectionError网络不通或接口拒绝访问检查网络确认URL没写错降低请求频率requests.exceptions.Timeout服务器响应过慢增大timeout参数从5改成10JSONDecodeError接口返回的不是JSON先打印resp.text看内容可能被重定向到验证页KeyError: weatherinfo接口返回结构变化打印完整返回内容重新定位数据所在的字段UnicodeEncodeError / 乱码编码问题写入CSV用utf-8-sig控制台乱码调整终端编码matplotlib中文方框字体里没中文字符设置rcParams中文字体见第4.1节ModuleNotFoundError: pyecharts库没装或装错环境pip install pyecharts确认和运行脚本是同一Python环境6.2 接口返回结构变了怎么办天气接口不是官方商用API你无法要求它保持永远不变。万一某天运行代码发现 all_data 是空的或者报错KeyError第一反应不是改代码而是去验证“现在接口到底返回什么”。我的做法是先写一行最短的请求代码import requests print(requests.get(http://www.weather.com.cn/data/sk/101010100.html, headers{User-Agent: Mozilla/5.0}, timeout5).text)把返回内容打印出来看它是JSON、HTML、还是被风控拦截了。JSON里字段还在只是名字换了改一下字段名就行返回的是HTML且里面提示验证说明触发频率限制了停一晚上再试接口彻底失效就回到第2.1节的方法去开发者工具重新找一个能用的数据接口。这个方法放之四海而皆准——不管爬什么网站排查“数据结构变了”永远是先看真实响应而不是猜。6.3 可以继续扩展的五个方向项目跑通之后扩展就是水到渠成的事。我列几个亲测有效的方向按难度递增排定时采集用time.sleep循环或者系统自带的计划任务每小时跑一次weather_spider.py积累一个月数据后画气温历史曲线能看出季节变化。接入SQLite把CSV换成SQLite存储用sqlite3模块写入支持按日期查询历史数据为以后做分析打基础。换成其他数据源把CITY_IDS和URL字段替换成别的公开接口代码主体不用动。比如有些国外天气API返回的JSON字段是英文清洗函数里多写几个replace就行。做Web展示用Flask写一个页面读取CSV或SQLite数据前端用ECharts动态渲染图表把整个项目变成一个小型天气数据展示系统。预测建模有了历史温度数据叠加湿度、风力字段可以尝试用线性回归或时间序列模型做短期温度预测。爬虫项目到这里就自然延伸到机器学习领域了。我个人在实际操作中的体会是这个项目最大的价值不在于“爬天气”本身而在于帮你建立一套处理真实数据的工作流——从一口一口吃不透的原始数据到清洗、存储、可视化每一步都有明确的产出物。做课程设计也好、写技术博客也好、甚至面试讲项目经历也好这套流程的含金量比单纯背一堆语法高得多。最后再分享一个小技巧调试的时候把时间等字段一起存下来看似多余但以后回看数据时能帮你快速定位“这次采集是不是凌晨脚本跑挂了”之类的问题。数据字段多存一点永远比少存一点更省心。