刚接触Python爬虫的同学,最容易卡住的地方不是不会写代码,而是不知道"到底该爬哪个接口"。看到网页上明明有天气数据,代码里却只能拿到一堆HTML标签,或者整页都是动态加载的内容,直接 requests 根本拿不到想要的字段。
这篇文章就用一个非常典型的入门级案例——采集2345天气网的城市天气预报数据,把整套"抓包分析思路+接口定位+数据解析+完整代码"从头到尾走一遍。2345天气网的数据是动态加载的,正好适合练抓包;接口本身又是JSON结构,比去解析HTML清爽太多。如果你是刚学完 requests 基础语法、想找一个真实项目练手的人,这篇应该能直接帮你跑通第一个完整爬虫。
1. 为什么选2345天气网当入门练手项目——先摸清目标再动手
很多人一上来就到处找大站练手,结果第一步就被各种签名、加密参数、登录验证劝退。2345天气网作为入门案例,难度梯度卡得很准:有动态加载接口可以练抓包,但没有复杂的加密逻辑,反爬也比较温和,非常适合用来建立"从网页到数据"的完整链路感。
1.1 做天气爬虫能学到的四件事
我把这个案例拆开看,它其实是四个独立能力的组合,每一项以后做别的爬虫都能复用。第一是抓包定位真实接口,学会在 Network 面板里区分 HTML 文档、JS 文件、XHR 异步请求,找到真正返回天气数据的那个请求;第二是处理 jsonp 数据格式,理解为什么接口返回的不是纯 JSON,而是包了一层函数调用,以及怎么把外壳剥掉;第三是城市代码与 URL 参数构造,搞明白 2345 天气网每个城市对应一串数字编码,怎么把参数拼接成合法的请求;第四是解析 JSON 并落地保存,从嵌套的字典列表里把日期、天气、温度、风力这些字段提取出来,整理成结构化数据或 CSV 文件。
这四点覆盖了日常爬虫开发的大部分核心动作,尤其是抓包和 jsonp 处理,在后面的项目里会反复用到。
1.2 静态页面与动态接口的区别
用浏览器打开 2345 天气网的北京页面,页面上的天气信息看起来是正常 HTML,但只要用 requests 直接去请求这个 URL,就会发现拿回来的代码里根本没有温度、天气描述这些内容。原因是数据是通过 JavaScript 在页面加载后异步请求回来的,浏览器执行完脚本才把数据填进页面,我们直接请求 HTML 文件,自然什么都拿不到。
这就是静态页面和动态接口的核心区别:页面展示层是一套代码,数据源是另一套独立接口。爬虫要做的事情,就是绕过页面展示层,直接找到背后那个数据接口。这个思路通了,很多看起来"爬不到"的网站在你眼里就会变成"只要找到接口就能爬"。
1.3 入门爬虫的三个常见认知误区
第一个误区是"一定要会 selenium 才能爬动态网页"。实际上大部分动态网页的数据都是通过某个 XHR 接口返回的 JSON,只要抓包拿到这个接口,用 requests 一个请求就能解决,完全没必要开浏览器,效率还高得多。第二个误区是"数据一定要从 HTML 里解析"。HTML 解析容易受标签结构变化影响,而 JSON 接口的数据结构通常更稳定、字段更明确,优先找接口才是正路。第三个误区是"所有网站都有很强的反爬"。其实大量正规网站的反爬力度只停留在校验 User-Agent 和请求频率,做好基本的请求头伪装、控制好访问节奏,就不会触发风控。
2. 抓包定位:找到天气数据真正的出处
这个环节是整个爬虫项目里最有价值的部分,建议新手不要跳过直接抄接口地址。因为网站接口随时可能调整,一旦接口失效,不会抓包分析就寸步难行;反过来,只要掌握了抓包定位的方法,任何网站都能在几分钟内重新找到数据源。
2.1 Chrome Network 面板的初筛操作
先打开 Chrome,按 F12 进入开发者工具,切到 Network 面板,勾选 Preserve log 防止页面刷新后请求记录被清空。在地址栏输入 2345 天气网的城市页面,比如北京,按回车加载。页面加载完成后,Network 面板里会出现大量请求,此时做一层筛选:点击面板顶部的 Fetch/XHR 过滤按钮,只看异步接口请求。
在这个列表里找返回内容为 JSON 或 JSONP 格式的请求,通常名字里带 weather 字样的就是关键目标。点开这条请求,可以在 Headers 里看到完整的请求 URL 和请求方式,在 Preview 或 Response 里看到返回的数据结构。我当时的做法是先不看任何教程,强制自己在 15 分钟内只看 Network 面板找到数据接口,这个练习很值得做一次。
2.2 Fiddler 抓包的补充价值
Chrome 开发者工具能完成 90% 的抓包需求,但有些场景下 Fiddler 不可替代,比如想看完整请求链路、修改请求参数后重新发送,或者排查代理层面的问题。Fiddler 抓包的基本用法是:安装后启动,确保菜单栏 Capturing 处于开启状态,它会自动把系统代理设置为 127.0.0.1:8888。这个时候再打开浏览器访问目标页面,Fiddler 的会话列表里就会实时出现所有请求。
定位到天天气接口后,Fiddler 左边点这条会话,右侧切到 Inspectors 面板,上半部分可以看到请求的 Headers、Body,下半部分可以看到服务器返回的原文。右上角的 Replay 按钮可以一键重发这个请求,改参数再重发也很方便,用来验证自己的代码模拟是否符合预期。
2.3 JSONP 跨域历史和它的数据外壳
2345 天气网接口的返回内容大概率不是纯 JSON,而是类似weatherInfo({...})或jsonpCallBack({...})这种形式,这就是经典的 JSONP。JSONP 最早是为了绕开浏览器的跨域限制:代码里没法直接用 XHR 读取别的域名下的数据,但<script>标签没有这个限制,于是开发者通过动态创建 script 标签,让服务器把数据包在一个函数调用里返回。
理解这一点对爬虫的意义在于,requests 请求不受跨域约束,我们拿到的响应文本里会包含这个函数外壳。处理办法很直接:用字符串切片或者正则把最外层函数名剥掉,剩下的内容就是标准 JSON。不要试图在代码里去执行这个 JS 代码,把数据抽取出来就够用了。
2.4 参数含义与城市代码的对应关系
以 2345 天气网为例,抓到的请求往往会带上areaInfo[]=101010100这样的参数,这个看起来像编号的数字其实就是城市代码。在 2345 天气网里,北京是 101010100,上海是 101020100,广州是 101280101。这套编码长期保持稳定,我们可以提前准备好一份城市代码映射表。
构造请求的时候,data 参数直接写成{"areaInfo[]": ["101010100", "101020100"]},requests 会自动把同一个 key 拼成多个同名参数,不需要手动去拼接字符串。这也是新手容易踩坑的地方:以为必须手动拼 URL,结果漏掉了某个附加参数,请求就返回空数据。
3. 数据解析思路:从 JSONP 响应到干净的中文天气字段
拿到响应文本只是第一步,真正繁琐且容易出错的是解析环节。JSON 数据虽然是结构化的,但嵌套层级深、字段名不直观,直接写解析逻辑容易写错,建议先把返回结果完整打印出来,对着真实数据结构写代码。
3.1 剥掉 JSONP 外壳的两种写法
假设接口返回的文本是weatherInfo({"rows":[{"city":"北京","temp":5}]}),第一种剥外壳方式是字符串切片,找到第一个左括号和最后一个右括号,把中间部分取出来。这种方式简单直观,适合返回格式固定的接口。
第二种方式是使用正则表达式,比如re.search(r'\((\{.*\})\)', text, re.S),匹配第一对包含大括号内的完整内容。两种方法各有使用场景,我用得更多的是正则,因为即使回调函数名变了,只要返回的是对象结构,这个正则基本都能命中。
3.2 JSON 嵌套结构的字段提取
用json.loads()把 JSON 字符串转成 Python 里的字典和列表之后,就可以按层级逐层取数了。常见返回结构类似:最外层是状态字段,接着是包含多座城市数据的列表,每个城市字段里又嵌套日期、天气、温度、风力等信息。
我在解析时习惯先用一段临时代码完整打印json.dumps(data, ensure_ascii=False, indent=2),看清楚每个字段的准确名称和嵌套层级,再写解析逻辑。直接猜字段名十有八九会写错,浪费调试时间。提取过程中最需要注意的是部分城市没有某一项数据,比如某些极端天气下风力字段可能为空,代码里要用.get()而不是["字段"]直接取值,避免 KeyError 导致整个程序中断。
3.3 中文乱码问题的根因
明明接口返回的是中文,打印出来却变成一串乱码或者\u5317\u4eac这种转义字符,这个问题的根源在编码和显示两个层面。requests 拿到响应后,会先根据响应头里的 charset 猜测编码方式,如果猜错了,resp.text拿到的就是乱码。
此时最稳妥的做法是明确指定编码,例如resp.encoding = 'utf-8'后再访问resp.text。至于看到\u开头的内容不用慌张,那只是 JSON 字符串在传输过程中的 Unicode 转义,json.loads()之后会自然还原成中文。
4. 完整爬虫代码实现与逐行注释
下面这段代码是针对 2345 天气网接口的完整实现,包含请求构造、JSONP 清洗、JSON 解析、CSV 落地四个部分。代码本身不复杂,难点主要在前面讲的抓包分析和数据结构理解,这两步做完,代码只是把分析结果翻译成 Python 而已。
4.1 环境准备
脚本只依赖 requests 这一个第三方库,安装命令:
pip install requestsCSV 保存用的是 Python 标准库的 csv,不需要额外安装。如果你的机器上连 requests 都还没装,先跑上面这条命令。如果是在公司网络环境,可以用国内镜像源加速,比如pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple。
4.2 完整代码
import requests import json import re import csv import time # 目标接口:抓包分析得到,实际使用时以你自己抓到的地址为准 API_URL = "https://tianqi.2345.com/Pc/GetWeather" # 常用城市及对应的2345城市代码,可自行扩充 CITY_CODES = { "北京": "101010100", "上海": "101020100", "广州": "101280101", "深圳": "101280601", "杭州": "101210101", "成都": "101270101", } # 请求头伪装:带上网址来源和浏览器UA,模拟真实浏览器请求 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": "https://tianqi.2345.com/", "Accept": "application/json, text/javascript, */*; q=0.01", } def get_weather_data(city_codes): """ 请求天气接口,返回解析后的JSON数据 :param city_codes: 城市代码列表 :return: 解析后的列表数据 """ # 构造POST请求的参数,多个同名参数用列表传递 data = {"areaInfo[]": city_codes} # 发送请求,设置超时避免长时间无响应 resp = requests.post(API_URL, data=data, headers=HEADERS, timeout=10) resp.encoding = "utf-8" # 打印原始响应,调试时建议打开看字段结构 print("原始响应示例:", resp.text[:500]) # 提取JSONP外壳中的JSON字符串 match = re.search(r"\((\{.*\})\)", resp.text, re.S) if not match: raise ValueError("未匹配到JSON数据,请检查接口是否发生变化") json_str = match.group(1) # 解析JSON为Python对象 data = json.loads(json_str) # 根据实际结构返回列表;这里以rows字段为例 return data.get("rows", []) def save_to_csv(rows, filename="weather_data.csv"): """ 把天气数据写入CSV文件 :param rows: 解析后的数据列表 :param filename: 输出文件名 """ if not rows: print("没有数据可保存") return # 通过第一行数据的字段名决定CSV列头 fieldnames = list(rows[0].keys()) with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) print(f"数据已保存到 {filename}") def main(): codes = list(CITY_CODES.values()) # 请求数据 rows = get_weather_data(codes) # 在终端里友好显示关键字段 for row in rows: city = row.get("city") date = row.get("date") weather = row.get("weather") temp = row.get("temp") wind = row.get("wind") print(f"{city} | {date} | {weather} | 气温{temp} | {wind}") # 保存到CSV文件 save_to_csv(rows) if __name__ == "__main__": main()4.3 代码里的几个设计细节
上面这段代码里有几个地方是新手容易忽略的,我单独拎出来说明。第一个是HEADERS里的Referer字段,有些接口会校验请求来源,不加它可能直接返回 403。第二个是data = {"areaInfo[]": city_codes},当你把一个列表赋值给字典的值并在 POST 请求中传递时,requests 会展开成多个同名参数,这比手动拼接字符串更可靠。第三个是encoding="utf-8-sig",用它保存的 CSV 文件用 Excel 打开不会出现中文乱码,很多教程里用的是utf-8,打开 Excel 就一团乱麻。
运行脚本后,终端会依次打印每座城市的天气信息,同时将结构化的数据写入 weather_data.csv。看到这个效果,第一个完整爬虫就算跑通了。
5. 反爬虫与请求头伪装的实际细节
2345 天气网目前的反爬策略不算强,但这不代表可以裸奔式访问。入门阶段就要养成好习惯:把请求伪装成真实浏览器,控制访问频率,做好错误处理。这些习惯能避免绝大多数入门项目的"翻车"。
5.1 User-Agent 与 Referer 的作用
User-Agent 标识了客户端的类型,没有它或者用它默认的 Python-requests 字符串,服务器一眼就能识别出这是脚本访问。Referer 则告诉服务器"我是从哪个页面跳过来的",很多接口会校验它,比如 2345 天气网的接口就要求请求来源是它的站点页面,否则可能返回异常。
在实际配置时,直接把一台真实 Chrome 的 UA 字符串复制过来用即可,不需要伪造得特别复杂。Referer 设置成目标网站首页,比如https://tianqi.2345.com/,就能让请求看起来像页面上某个 JS 脚本在正常拉数据。
5.2 请求频率控制的必要性
即便反爬策略很弱,也不建议用循环把所有城市一次性高并发请求完。更稳妥的做法是在每次循环后加随机延时,比如time.sleep(random.uniform(1, 3)),把请求频率控制在一秒一次到三秒一次之间。这样做既不会给目标服务器造成压力,也避免触发访问频率限制。
如果真的要大规模采集全国几千座城市的数据,那就要考虑分批执行、动态调整间隔、失败重试等策略,这些属于进阶内容,入门阶段先把单次请求做对,再谈规模化。
5.3 403、418 错误排查清单
遇到请求被拦截,不要急着怀疑自己的代码写得不对,按顺序排查这几项基本能解决大部分问题。第一看请求头是否完整,UA 和 Referer 都加上了吗;第二看请求参数是否和抓包时完全一致,尤其是那些看起来不起眼的隐藏参数,漏一个可能就校验失败;第三看请求频率,如果是一秒几十次的高频访问,先降速再试;第四看 Cookie,部分接口需要先访问页面种下 Cookie 才能正常请求,可以通过 requests.Session() 保持会话来解决。
这些排查项我在后面的踩坑章节里会再具体展开,初学者只要记住一个原则:模拟得越像真实验证过程,成功率越高。
6. 实际踩坑记录:从"爬不到数据"到"跑通全流程"
这部分我把自己反复调试这个小项目时遇到过的真实问题整理出来,包括现象、原因和解法。这些问题都很典型,你照着做的时候大概率也会碰到其中一个。
6.1 参数编码:中文城市名导致的请求失败
一开始我图省事,直接把城市中文名作为参数传给接口:data = {"city": "北京"}。结果返回的数据是空的,或者提示参数错误。看抓包记录才发现,接口需要的参数是城市代码,而且还要带上方括号形式的字段名areaInfo[]。中文名称必须经过 URL 编码才能安全传输,我用 requests 的 data 参数自动处理了这个过程,而手动拼接 URL 时则要先用urllib.parse.urlencode()处理中文。
这里给新手一个建议:平时写字典传给 requests 的 data 或 params 参数,让它替你处理编码。等哪一天必须手动拼 URL 了,再用 urlencode 显式编码,避免踩坑。
6.2 JSONP 回调名不固定导致的正则失配
刚开始我的正则写得很死,比如jsonpCallBack\((.*)\),结果某一天接口悄悄改了回调函数名,程序直接报错。后来改成匹配\((\{.*\})\),只要返回内容最外层是 JSON 对象,就能稳定提取。
这个教训的核心是:解析代码要写得健壮,不要依赖那些可能变化的外部特征。接口地址、回调名、字段名都可能变,写代码时尽量选择更稳定的锚点,比如用正则匹配大括号结构而不是匹配具体函数名。
6.3 编码设置不当导致中文乱码
第一次跑通代码时,打印出来的城市名全是乱码。问题就出在响应编码判断上,requests 把响应当成了其他编码。强制指定resp.encoding = "utf-8"后,中文恢复如常。另外保存 CSV 时如果不想在 Excel 里看到乱码,建议用utf-8-sig编码,比起标准utf-8,它对 Excel 的支持更友好。
6.4 响应字段为空的容错处理
有些城市在某些时间点会缺少个别字段,比如风力数据为空,或者某一天的预报没有生成。如果解析时用row["wind"]直接取,就可能触发 KeyError,程序中断。后来我统一改成row.get("wind", ""),缺失的字段自动返回空字符串,保证整个采集过程稳定运行。
这个容错习惯适用于所有爬虫项目。只要是外部数据源,就有字段缺失、结构变化的风险,写解析代码时建议全部用.get()取值,并给足默认值。
7. 扩展思考:从单城市到批量采集的进阶方向
跑通单个接口之后,自然会产生"能不能把更多城市的天气全部拿下来"的想法。这个扩展方向值得做,但得提前想清楚几件事:城市代码从哪里来、请求频率如何控制、数据怎么增量更新。
2345 天气网的首页通常会提供一个城市导航页,里面有全省甚至全国城市的链接,城市代码就藏在链接里。可以用一个爬虫去解析这些链接,把城市名和代码建立成映射表,存到本地文件,后续采集直接用这张表。批量采集的时候要加强健壮性设计,比如每个请求包在 try-except 里,失败时记录日志并跳过,而不是让整个任务崩溃。
数据保存这块也可以升级。CSV 适合简单演示,但如果后续要做历史天气趋势分析,建议换成 SQLite,结构化的查询和筛选会更方便。等到数据量大到一定程度,再考虑做数据可视化,比如按城市渲染温度折线图。
我在实际做类似项目时的体会是:爬虫能跑通一次不难,难的是不断添加异常处理、优化效率和保持稳定。这个 2345 天气网案例虽然简单,但每一处细节——从抓包定位到 JSONP 清洗、从请求头伪装到容错解析——都是日常爬虫工作的缩影。把这条链路完整走一遍,比看十篇教程都管用。