最近总有读者问我,Python基础语法学完之后该做点什么来练手。我的回答基本都很一致:去做一个爬虫加数据可视化的组合项目。原因很简单,爬虫解决的是"数据从哪来",可视化解决的是"数据怎么被看见",两条腿缺一不可。你把这两个环节串起来跑通一次,实际上就走完了一遍完整的数据处理流程,里面的网络请求、页面解析、数据清洗、字段转换、图表渲染,全是日常工作里最高频的技能点。
这次就拿天气数据来拆这个项目。天气数据是练手爬虫最理想的目标之一:结构规范、字段丰富、公开可访问、不需要登录,请求门槛和反爬压力都比较温和。整个项目做下来,你能拿到一套完整的代码——从指定城市抓取近30天的最高气温、最低气温、天气现象和风力风向,清洗成干净的表格,再输出一张能配合鼠标悬停查看详情的交互式温度趋势图。这套代码逻辑清晰,适合刚学完Python基础、想找一个"够真实但不过度复杂"的项目来练手的开发者。
1. 选天气数据练手不是偷懒:项目边界与学习价值
很多人一想到Python实战项目,第一时间会奔着电商评论、短视频数据分析这些"听起来很唬人"的选题去。我不太建议新手一上来就碰这类目标,原因在于它们的页面结构复杂,动态渲染严重,反爬策略激进,你还没体会到"数据到手"的成就感,光是定位加密参数就够消耗掉全部热情了。天气数据恰恰相反,它被低估了,但对打基础来说价值很高。
1.1 一座城市一个月的温差,就是最直观的项目目标
做项目最怕目标太模糊。学完基础语法之后,很多人打开IDE就开始发呆,不知道第一步该写什么。这里我们把目标定义得尽量具体:抓取某座城市最近30天的最高气温、最低气温、天气现象、风力风向,生成一张温度趋势折线图和一张天气现象占比饼图,最终输出一个浏览器直接打开的HTML报告文件。
目标一旦具体,技术选型就跟着清晰了。采集环节用requests发请求,配合必要的请求头伪装。解析环节直接用pandas的read_html读表格,省掉大量手写正则的麻烦。清洗环节处理缺测值、格式不统一等问题,因为不洗数据,后面图表一定出问题。绘图环节用pyecharts生成交互式页面,比matplotlib更适合快速呈现爬虫结果。整套代码控制在200行上下,一个周末就能跑通,不会因为项目体量太大而搁浅在半路上。
1.2 技术栈全景:一条龙走完采集、清洗、呈现
这套技术栈并不新奇,但每一环都是以后做任何数据项目的底座。我用一张表把各环节用到的东西和"为什么选它"列出来,方便你对照着理解。
| 环节 | 工具/库 | 选择理由 |
|---|---|---|
| 网络请求 | requests | 接口简洁,设置请求头、超时、重试都很直观 |
| 页面解析 | pandas.read_html | 目标页面是HTML表格,一行代码直接转DataFrame |
| 备用解析 | BeautifulSoup | 页面结构变化或需要抓非表格信息时兜底 |
| 数据清洗 | pandas | 列名处理、字符串分割、类型转换、缺失值删除都很顺手 |
| 数据存储 | CSV / SQLite | 一次性分析用CSV,定时增量更新用SQLite |
| 图表可视化 | pyecharts | 基于ECharts,交互体验好,直接生成HTML |
这个组合最大的特征是"够用且稳定"。每一样工具的学习曲线都不陡峭,组合起来却覆盖了一个数据项目从获取到呈现的所有关键节点。对我来说,练手项目最怕的不是代码量大,而是过程中缺少真实问题——真实项目中你会遇到编码乱码、请求超时、字段格式脏、x轴标签旋转角度不好看这一堆细节,而这些细节恰恰是你在纯语法练习里永远遇不到的。
2. 爬虫层设计:先从目标页面的HTML结构说起
爬虫能不能跑通,一半取决于你对目标页面的理解程度。写代码之前,先在浏览器里打开目标页面,按下F12看Network面板和Elements面板,弄清楚页面是服务端直接渲染HTML,还是需要额外请求JSON接口,这一步决定了后续所有代码的写法。
2.1 合理选择数据源:优先选服务端渲染的静态表格页
这个项目里我选的示例数据源是天气后报网的历史天气页面,URL格式类似城市拼音加上年月。这类页面的特点是服务端直接把表格渲染成HTML,字段齐全,包含日期、天气现象、气温、风力风向等列。用pandas.read_html就能解析,不需要处理接口鉴权、异步加载、加密参数这些复杂度,对新手非常友好。
当然,页面结构可能会随着时间调整,我也建议你先在浏览器里访问一下对应URL,确认页面还能正常打开、表格里确实有数据,再开始动手写代码。如果这个源不可用了,也可以找其他公开天气站点,只要页面里是静态的HTML表格,这套解析逻辑基本都能复用,只需要改URL和字段名。我把目标站点选择标准总结为三条:页面稳定、字段齐全、请求简单。三个条件同时满足,就是合适的练手目标。
2.2 请求阶段最容易被忽略的三个细节
请求代码非常简单,但细节决定成败。
第一是请求头。很多新手用requests.get(url)裸请求也偶尔能拿到页面,但部分站点会检测User-Agent,如果发现是非浏览器请求,会返回一段没有实际数据的错误页。所以构造headers的时候,User-Agent必须补上,最好连Accept、Accept-Language也带上,让请求看起来像从一个真实浏览器发出的。
第二是页面编码。中文天气站点很多还在用gbk或gb2312编码,如果不处理编码,拿到的字符串就是一堆乱码,read_html也没法正确解析。实操中我一般这样处理:先resp.encoding = resp.apparent_encoding自动探测一下,如果发现列名不对再手动指定encoding="gbk"。不同站点编码情况不一样,所以调试的第一步永远是打印前500个字符看一眼,而不是闷头往下写。
第三是超时和重试。网络状况不稳定,单次请求失败太常见了。程序里必须设置timeout,比如10秒,同时捕获requests.RequestException异常做重试,最多重试三次,每次间隔1秒。这样即使网络抖动,程序也不会中断在中间导致前功尽弃。
2.3 表格解析:pandas.read_html与BeautifulSoup的取舍
对天气历史这种HTML表格,最省力的方案就是pandas.read_html。它能直接把页面里的table标签解析成DataFrame,不用自己写循环遍历tr和td。代码可以这样写:
import requests import pandas as pd def fetch_page(city: str, year_month: str): url = f"http://www.tianqihoubao.com/lishi/{city}/month/{year_month}.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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8" } resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text html_text = fetch_page("chengdu", "202411") tables = pd.read_html(html_text) df = tables[0] print(df.head())read_html返回的是页面中所有表格的列表,如果目标页面只有一张主表,直接取[0]就行。如果有多张表格,可以按位置取,也可以用attrs参数根据table标签的class属性筛选。这个函数的底层依赖lxml或html5lib,首次使用前需要把解析库装好,否则会报缺失解析器的错误。
BeautifulSoup并不比read_html高级,它们适用场景不同。read_html适合"目标就是表格"的情况,BeautififulSoup适合"目标是自由HTML块"的情况,比如抓取页面里的标题、列表、摘要信息。我对选型的判断标准很简单:结构规整的表格用read_html,页面结构复杂或需要精准定位某一个DOM节点的时候再上BeautifulSoup。
3. 数据清洗:别让脏数据毁掉整张图
很多人爬完数据就急着画图,发现图表要么报错要么显示得莫名其妙。问题几乎都出在数据没清洗。这一步看起来不起眼,却决定了可视化阶段能不能顺利进行。我自己的习惯是:爬虫拿到数据之后,先花二十分钟把数据整理干净,再开始画图,后者的调试时间会节省一大半。
3.1 常见脏数据长什么样
用head()看原始DataFrame,很快就会发现几个典型问题。首先是列名带空格,比如"最高气温 "这类的,看起来没区别,实际在按列访问时容易踩坑。其次是气温字段长得像"11℃ / 3℃",温度和单位混在一起,中间还有斜杠,如果不拆开,整列就是字符串而非数值,画图时y轴完全没法用。第三是缺测值,有的行可能是空字符串,也可能是"-",还有可能是无效的"0℃ / 0℃"。
我处理这类问题习惯于分成两类。格式型污染靠字符串替换和分割就能解决,比如去掉单位、按斜杠切分、压缩多余空格。缺失型污染要先判断数据量,某一个单行缺失对整体趋势影响不大,直接丢弃是合理的;但如果缺失比例很高,就要回头检查是不是请求阶段出了问题,而不是清洗阶段硬扛。
3.2 清洗流程与规则实现:从字符串到数值列的转换
常用的清洗流程可以拆成四步。第一步统一列名,把空格去掉;第二步拆分温度列,把"最高温"和"最低温"从"气温"字段里拆成两个数值列;第三步把风力风向里的多余空格压缩掉;第四步处理缺测值。代码这样写比较清楚:
import pandas as pd # 第一步:清洗列名 df.columns = [col.strip() for col in df.columns] # 第二步:拆分温度字段 def clean_temp(val): val = str(val).replace("℃", "").strip() parts = val.split("/") high = float(parts[0].strip()) low = float(parts[1].strip()) if len(parts) > 1 else high return pd.Series([high, low]) df[["最高温", "最低温"]] = df["气温"].apply(clean_temp) # 第三步:压缩风力风向列的多余空格 df["风力风向"] = df["风力风向"].astype(str).str.replace(r"\s+", "", regex=True) # 第四步:处理缺测 df = df.dropna(subset=["最高温", "最低温"]) df = df[df["日期"] != ""].reset_index(drop=True)这一步有个常见的坑:当温度是0℃的时候,字符串替换后得到"0",float("0")本身没问题,但如果字符串里带着不可见空格或者全角符号,就会报错。所以碰到类型转换ValueError时,我建议先把目标列的代表性值打印出来看一遍,很多时候问题就出在一个肉眼看不到的空格上。另外某些表格解析后可能出现MultiIndex列名,这时候先用df.columns.get_level_values(-1)取最后一级,能省下很多麻烦。
3.3 数据存储:DataFrame、CSV、SQLite怎么选
清洗干净的DataFrame可以直接用于绘图,但如果希望保留一份可复用的中间结果,我建议顺手存一个副本。一次性分析项目存CSV就够了,一行to_csv搞定。如果后续要持续更新数据,SQLite是更合适的载体,它不需要独立安装数据库服务,Python自带的sqlite3模块就能用,数据量在几百MB以内表现都很稳定。存取示例如下:
import sqlite3 conn = sqlite3.connect("weather.db") df.to_sql("daily_weather", conn, if_exists="replace", index=False) # 之后按条件读回 query_df = pd.read_sql("SELECT * FROM daily_weather ORDER BY 日期", conn) conn.close()我的个人习惯是"一次分析存CSV,定时任务存SQLite"。原因在于SQLite天然支持条件查询和增量去重,后面做定时更新时,不需要把整个文件读进内存再比对去重,直接在SQL层面对比最新日期即可,效率高出不少。
4. 可视化实现:把数据变成一张会说话的温度曲线
数据清洗完成后,可视化阶段就轻松多了。但即便是画折线图,也存在工具选型的问题。选择pyecharts而不是matplotlib,核心原因是天气数据天生适合交互展示。
4.1 为什么要用pyecharts而不是matplotlib
matplotlib的静态图片不是不能用,但当你面对30个日期的温度数据时,交互能力的差异就体现出来了。用pyecharts生成图表之后,鼠标移到任意数据点,会弹出一个提示框显示当天详细数据;可以通过拖拽缩放工具查看某个时间段的细节;图例还能点击开关,单独看最高温或最低温。这些交互操作不需要写任何前端代码,pyecharts直接生成一个自包含的HTML文件,浏览器打开就能用。对爬虫项目而言,这几乎是成本最低的"数据报告"实现方式。
环境准备方面,执行下面的命令安装依赖即可:requests、pandas、beautifulsoup4、lxml、pyecharts。其中lxml是read_html的解析器依赖,pyecharts用于生成图表,这几个库都比较轻量,不会引入复杂的环境问题。
4.2 温度趋势图的完整配置:不只是画一条线
先看一个最简单的温度折线图实现:
from pyecharts.charts import Line from pyecharts import options as opts line = Line() line.add_xaxis(df["日期"].tolist()) line.add_yaxis("最高气温", df["最高温"].tolist(), is_smooth=True) line.add_yaxis("最低气温", df["最低温"].tolist(), is_smooth=True) line.set_global_opts( title_opts=opts.TitleOpts(title="成都近30天温度变化"), tooltip_opts=opts.TooltipOpts(trigger="axis"), legend_opts=opts.LegendOpts(pos_top="5%"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45, interval=5)), yaxis_opts=opts.AxisOpts(min_=0, max_=40), ) line.render("temperature.html")这段代码里有两个细节值得展开讲。第一个是x轴的axislabel_opts,日期有30个,全部铺开字会重叠,设置rotate=45让标签旋转45度,再用interval=5让日期隔几个显示一个,图表就干净很多。第二个是y轴范围,例如min_=0、max_=40,如果不显式指定,pyecharts会自动根据数据范围缩放,这本身没错,但有时候会放大局部波动,让读者觉得温差特别夸张,显式的范围设定能减少视觉误导。
tooltip的trigger参数也值得注意。设成"axis"之后,鼠标在图表任意位置横向移动时,提示框会同时展示该日期下的最高温、最低温两条数据,非常适合对比两条曲线的走势;默认的"item"模式则只会显示离鼠标最近的那个点。对于这种多序列折线图,我基本都推荐trigger="axis"。
4.3 不只画温度:天气现象与风力的多图表组合
同一个DataFrame里还有天气现象、风力风向这些分类字段,用饼图展示占比很直观。比如统计过去30天晴、多云、雨各占多少天,可以这样写:
from pyecharts.charts import Pie from collections import Counter counter = Counter(df["天气现象"]) data = [{"name": key, "value": val} for key, val in counter.items()] pie = Pie() pie.add("", data, radius=["30%", "60%"]) pie.set_global_opts(title_opts=opts.TitleOpts(title="成都近30天天气现象分布")) pie.render("weather_pie.html")如果要在一个页面里同时展示多张图表,pyecharts的Page组件可以把折线图、饼图、柱状图按顺序整合成一个长页面。用法是page = Page(),然后page.add(line).add(pie),最后page.render("weather_report.html")。这种输出方式很适合做成一个"城市天气周报"形式的自动报告,每次抓完数据重新渲染一次,就是一个可以直接发给别人看的HTML文件。
5. 完整代码走读:从启动到浏览器打开一张图
前面几个章节把采集、清洗、可视化分别讲透了,这一节把完整链路串起来。代码层面我倾向于把功能拆成几个独立函数,fetch_page负责请求页面,parse_data负责解析和清洗,build_charts负责绘图,最后main函数负责串联。这样拆的好处是后续换城市、加图表、改存储方式都只需改动对应函数,不会牵一发动全身。
5.1 主流程合并成一条龙
把前面的代码整合到一起,主流程可以这样组织:
def main(city: str, year_month: str): html_text = fetch_page(city, year_month) df = parse_data(html_text) build_charts(df, city) df.to_csv(f"{city}_{year_month}.csv", index=False, encoding="utf-8-sig") print(f"完成: {city} {year_month}") if __name__ == "__main__": main("chengdu", "202411")parse_data函数里包含了列名清洗、温度拆分、缺测值处理这一整套逻辑。build_charts函数生成折线图和饼图。CSV文件名里带上城市和月份,可以避免覆盖历史结果。整体代码量不大,但每个函数都有明确职责。项目跑通之后,你想扩展任何功能,都能在对应函数里找到修改位置。
5.2 支持多城市批量抓取的最小设计
单城市跑通后,很多人会自然想抓多个城市。批量抓取不需要引入复杂框架,只需要把城市列表抽出来循环处理。城市参数用拼音,形如chengdu、beijing、shanghai。运行的时候注意控制频率,每次请求之间最好sleep(1)左右,给服务器留出响应时间,也降低被临时限流的风险。代码结构大致如下:
import time city_list = ["chengdu", "beijing", "shanghai"] for city in city_list: try: main(city, "202411") except Exception as e: print(f"{city} 抓取失败: {e}") time.sleep(1)批量跑起来之后我建议加一个简单的日志输出,记录每座城市抓取成功还是失败,以及失败原因。这个习惯看起来不起眼,但一旦某个城市页面结构不同或者网络超时,它能帮你快速定位问题,而不用逐个人工查看输出。
5.3 运行结果与常见报错排查
运行这个项目可能遇到几个高频报错,我整理成一张排查表,对标解决会比翻报错堆栈快很多。
| 报错现象 | 大概率原因 | 排查方向 |
|---|---|---|
| HTTP 403 Forbidden | 请求头不完整或触发基础反爬 | 补全User-Agent、Accept、Referer |
| read_html解析出空表 | URL参数错误或页面结构变了 | 打印resp.text[:500]确认返回内容 |
| astype(float)报ValueError | 字符串里有空格或非数字符号 | 打印原始值,先strip再类型转换 |
| 中文列名乱码 | 页面编码处理不对 | 确认目标页面的charset,手动指定encoding |
| 请求超时 | 网络不稳或请求频率过高 | 设置timeout,捕获异常并重试,加sleep间隔 |
排查的核心思路只有一条:不要盯着最终报错瞎猜,逐步打印中间结果。resp.text是原始网页,df.head()是解析结果,清洗前后的dtypes变化,每一步都打出来看一眼,问题基本都能缩小到具体环节。
6. 项目跑通之后:给数据源留点余地,也给自己留点提升空间
跑通不是终点。所有爬虫项目都有一个共同的不确定性——目标页面可能随时改版。所以项目完成后,我建议不要只是看一眼图表就结束,而是再想几步,让这套代码具备更长久的可用性。
6.1 如果目标站改版了怎么办
目标页面改版是爬虫领域再正常不过的事。有些改版是URL变化,有些是参数加密,有些是表格结构调整。我的应对经验是:永远不要试图把代码写死,把解析逻辑独立成函数,就是为改版留的后路。一旦发现页面结构变了,只需要打开开发者工具查看新结构,更新parse_data里的解析逻辑,其他代码基本不用动。
另外,新版页面很多会把数据改成JSON接口返回。这未必是坏事,JSON接口通常结构更清晰,不需要解析HTML表格,直接用requests请求接口再加一个解析函数即可。只要一开始的代码模块化做得好,从"解析HTML表格"换到"解析JSON响应"的工作量并不大。
6.2 定时更新与增量采集:从一个偶然的数据快照到持续观察
天气数据是持续产生的,非常适合做成定时任务。我的设计思路是利用SQLite做增量采集:先查询本地数据库里已有的最大日期,然后从下一天开始往后抓,避免每次都抓全量数据。Windows下可以用计划任务定期执行脚本,Linux下用crontab,间隔设为每天一次即可。
增量采集的好处不只是省流量,更在于数据可以积累出长期趋势。等积累了一整年的温度数据,再画年温度曲线、对比换季时间点、计算温差极值,都变成轻而易举的事。这也是练手项目走向"真实数据产品"的最短路径。
6.3 给数据源留点礼貌,也给自己的代码留点余地
最后回到一个容易被忽略的点。个人学习和练手场景下,请求频率控制在每秒一次以内通常足够,不要并发猛打目标服务器,更不要用多线程疯狂加速。爬虫的价值在于获取数据后怎么处理和分析,而不是以多快的速度把对方站点拉垮。尊重数据源访问规则、保持合理频率、设置异常重试和退出机制,这些习惯比任何具体代码技巧都重要。项目本身能长期稳定跑下去,才是你技术能力最好的证明。
这个项目完整跑下来之后,我最大的体会是:练手项目不需要宏大,但一定要把数据从采集到呈现的链路完整走一遍。哪怕只是一座城市30天的温度,函数拆分、容错重试、清洗先行、模块化绘图,这些经验都不是从教科书里背来的,而是踩坑踩出来的。如果你刚学完Python基础,别纠结项目是否"够酷",先把这一条数据链路跑通。跑通之后,再考虑接上定时任务、扩展更多城市、增加一个简单的Web展示层,每一步都是往项目化方向走,而这条路一旦开始,数据分析和Python开发的能力就会一起跟着涨。