ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

爬虫入门实战:用requests抓取2345天气网城市天气数据

爬虫入门实战:用requests抓取2345天气网城市天气数据 兄弟你要是正准备入坑爬虫千万别一上来就死磕那些复杂的电商反爬、登录验证。我跟你说天气数据采集是练手性价比最高的项目接口不用登录、数据结构规整、信息量适中而且不管你是想做数据分析、搭个可视化大屏还是给个人博客做个天气插件天气数据都是现成的素材。这次要拆解的是2345天气网城市天气预报数据采集。选它的原因很直接——页面上温度、天气、风力、湿度、空气质量一目了然底层接口又不像大厂那样设了重重反爬门槛。我重点会讲三件事怎么用浏览器开发者工具抓包定位数据接口、怎么看懂接口里的参数和返回结构、怎么写干净的代码把数据落袋。整套流程下来你收获的不只是会爬2345天气网而是对爬虫这个工种有了一个完整的坐标系。1. 项目目标与整体思路1.1 为什么选2345天气网作为入门爬虫项目很多初学者问我的第一个问题是练手项目选什么好我的建议从来都是——先别碰电商、别碰社交平台从天气数据开始。原因有三个第一天气数据是公开且低频变化的不需要处理实时流和WebSocket那类复杂机制。第二2345天气网虽然是动态加载页面但它的数据接口很规整不加密、不签名、不搞滑块验证对新手极其友好。第三采集下来的数据能直接拿去用无论是做历史天气的趋势分析、对比不同城市的温差还是教机器学习模型做气温预测都拿得出手。这次的项目目标很明确输入一个城市名自动采集这个城市当天的气温、天气状况、风力、湿度再扩展到未来7天的天气预报。整个过程要覆盖爬虫开发的完整闭环——页面分析、抓包定位、构造请求、解析数据、清洗存储。你把它走完一遍之后遇到任何同类站点基本都能复用这套思路。1.2 技术栈选型与整体流程设计技术选型上我特意没有上Scrapy也没有碰Selenium。原因很简单入门阶段最重要的不是工具多高级而是把HTTP请求和响应处理的底层逻辑吃透。requests负责发请求json和pandas负责数据处理time负责控制请求频率——这套组合的成本最低但足够覆盖大部分中等复杂度的爬虫场景。整体流程分成六步打开2345天气网首页用Chrome开发者工具监听所有网络请求。在搜索框输入目标城市名观察Network面板里蹦出来的接口请求找到城市代码接口。拿到城市代码后再访问对应城市的天气详情页抓取天气数据接口。分析接口返回的JSON结构挑选我们需要的字段。用requests构造请求模拟浏览器行为拿到数据。清洗数据存入CSV或SQLite方便后续分析和使用。提醒抓包是整个流程最关键的环节后面第2节我会花整节篇幅讲因为很多人在这一步卡住后面全崩。2. 抓包实战用开发者工具解剖页面背后的数据接口2.1 给页面体检——打开开发者工具的正确姿势很多人听到抓包就头皮发麻觉得是什么高深黑客技术。其实你每天用的Chrome浏览器自己就带了个抓包神器——开发者工具。所谓抓包说白了就是偷看浏览器背着你发的那些HTTP请求。操作步骤我一点一点说你照着做就行打开Chrome访问https://tianqi.2345.com/。按F12打开开发者工具顶部切到Network网络面板。在Network面板的左上角勾选Preserve log保留日志这样页面跳转时请求记录不会被清空。勾选Disable cache禁用缓存避免浏览器用缓存数据干扰观察。刷新页面你会看到Network面板里哗啦啦涌进来几十个请求名字五花八门。对应到动图演示里的操作顺序核心就一句话先打开工具再触发页面动作。比如在搜索框输入城市名然后立刻盯着Network面板看新增了哪个请求。谁冒出来谁就是关键接口。2.2 城市代码接口GetCityCode当你在首页搜索框输入北京之后Network面板里会多出一个请求名字叫GetCityCode。点开它你看到的完整请求形态是这样的请求URL: https://tianqi.2345.com/Pc/GetCityCode?cityName%E5%8C%97%E4%BA%AC_1710234567890 请求方式: GET这里有两个参数cityName城市名经过URL编码。北京两个字变成了%E5%8C%97%E4%BA%AC。_时间戳防止浏览器缓存请求结果。点开Preview标签页返回的是一段JSON数组。以北京为例结构大致如下[ { id: 101010100, name: 北京, province: 北京市, pinyin: beijing } ]这个id字段就是城市代码后面所有天气数据接口都要靠它来定位城市。不同的城市有各自的代码比如上海是101020100、广州是101280101。你可以试着在搜索框输入不同城市名观察返回JSON里id的变化规律——其实和通用的中国天气城市代码体系是相通的。2.3 天气数据接口GetWeather拿到城市代码后继续在首页点击北京进入天气详情页Network面板又会新增几个请求。我们要找的是名字里带GetWeather的那个它的完整形态长这样请求URL: https://tianqi.2345.com/Pc/GetWeather?areaInfoId101010100areaType2date20250125 请求方式: GET三个参数的语义很清晰areaInfoId城市代码就是上一步拿到的101010100。areaType区域类型固定传2表示按城市维度查询。date日期格式是YYYYMMDD代表要查询哪一天的天气。如果你想要未来7天的预报接口名会变成GetWeather7Days想要15天就是GetWeather15Days。参数结构基本不变只是返回的数据条数多了。这个规律是在反复抓包中总结出来的比硬记接口文档靠谱得多。2.4 响应JSON结构深度解读点开GetWeather请求的Preview标签页返回的JSON大致是这样的{ errMsg: , code: 0, data: { temp: 5℃, weather: 晴, wind: 西北风3-4级, humidity: 35%, updateTime: 2025-01-25 08:00:00, highestTemp: 8℃, lowestTemp: -2℃ } }7天预报接口的data则是一个数组每一项是一天的天气{ errMsg: , code: 0, data: [ { date: 1月24日, weather: 晴转多云, temp: -4℃~6℃, wind: 北风3级 }, { date: 1月25日, weather: 多云, temp: -2℃~8℃, wind: 西北风3-4级 } ] }这些字段用肉眼就能看懂但要特别注意code字段——0代表请求成功非0值说明出错了。写代码的时候一定不能忽略这个判断否则你会把错误响应当正常数据解析后面全乱套。注意我上面贴的接口地址和字段名是基于我自己抓包时的情况。2345天气网作为运营中的站点接口随时可能微调。如果你抓包时看到的名字不一样那就以你实际看到的为准抓包方法比接口地址本身更值得掌握。3. 完整爬虫代码实现3.1 环境准备与依赖安装代码用Python 3.8以上版本就行不需要虚拟环境的复杂配置。依赖库只有两个直接在命令行里装pip install requests pandasrequests负责HTTP请求pandas用来做数据清洗和存储。如果你还想把数据存数据库再装一个sqlalchemy备用但这篇案例用CSV就够了入门阶段其实不建议立刻上重型存储中间件。代码文件组织上我建议按功能模块拆分成单一文件里的多个函数。别一上来就搞什么class继承、抽象工厂那会把新手绕晕。这篇案例我用一个类把相关方法收拢起来既有面向对象的味道又不至于过度设计。3.2 请求会话与请求头伪装爬虫代码的第一步是构造一个带伪装的请求会话。2345天气网虽然反爬不强但你用裸的requests默认请求头去访问服务器一看User-Agent不对劲直接给你403或者返回空数据。贴一段我最常用的请求头配置import requests 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, */*; q0.01, X-Requested-With: XMLHttpRequest } session requests.Session() session.headers.update(HEADERS)这里有两个细节值得展开User-Agent伪装成Chrome浏览器。服务器看到这个UA基本会把你当成正常访客。X-Requested-With: XMLHttpRequest是Ajax请求的标志。很多后端程序会检查这个字段没有它接口可能直接返回 not ajax 之类的错误。这是个不起眼但经常踩坑的点。3.3 城市代码查询模块城市代码查询是爬虫流程的前置依赖没有它寸步难行。实现起来就是封装一个函数接收城市名返回城市代码。关键点在于容错处理——用户可能输入北京也可能输入北京市甚至输入帝都这种别名接口不一定认账。我给出的实现是def get_city_code(session, city_name): 根据城市名查询城市代码返回第一个匹配结果 url https://tianqi.2345.com/Pc/GetCityCode params { cityName: city_name, _: int(time.time() * 1000) # 时间戳模拟浏览器防止缓存 } resp session.get(url, paramsparams, timeout10) # 很多站点接口返回的不是标准JSON而是带前缀的JSONP格式 # 这里做一层兼容处理 text resp.text.strip() if text.startswith(jQuery): import re text re.search(r\((\[.*\])\), text, re.S).group(1) data resp.json() if data: return data[0].get(id) return Nonetime.time() * 1000转成整数就是毫秒级时间戳作用和浏览器自动加的那个_参数一样。timeout10一定不能省万一接口抽风请求挂死会严重影响效率。3.4 天气数据采集模块拿到城市代码之后就能请求天气数据接口了。我把当天天气和7天预报两个接口分开封装这样结构更清晰以后你只想爬当天数据就不用白跑一趟大接口def get_weather_today(session, city_code, dateNone): 获取指定城市当天的天气实况和预报 if date is None: date datetime.now().strftime(%Y%m%d) url https://tianqi.2345.com/Pc/GetWeather params { areaInfoId: city_code, areaType: 2, date: date } resp session.get(url, paramsparams, timeout10) data resp.json() if data.get(code) ! 0: raise ValueError(f接口返回错误码: {data.get(code)}, 错误信息: {data.get(errMsg)}) return data.get(data, {}) def get_weather_7days(session, city_code, dateNone): 获取指定城市未来7天的天气预报 if date is None: date datetime.now().strftime(%Y%m%d) url https://tianqi.2345.com/Pc/GetWeather7Days params { areaInfoId: city_code, areaType: 2, date: date } resp session.get(url, paramsparams, timeout10) data resp.json() if data.get(code) ! 0: raise ValueError(f接口返回错误码: {data.get(code)}, 错误信息: {data.get(errMsg)}) return data.get(data, [])注意我在两个函数里都检查了code字段。你可能会觉得这是多此一举——但生产环境里接口出错是常态早发现才能早处理。如果某天2345接口调整了参数你的代码会迅速抛异常提示你而不是默默存下一堆空数据。3.5 数据整理与存储模块数据拿到手是JSON格式但JSON不适合直观阅读也不方便后续分析。我用pandas把数据转成表格然后导出CSV文件import pandas as pd def save_weather_to_csv(city_name, today_data, forecast_data, filenameweather_data.csv): 把当天天气和7天预报整合成DataFrame导出CSV rows [] # 当天天气先加一行 rows.append({ 城市: city_name, 日期: 今天, 天气: today_data.get(weather), 气温: f{today_data.get(lowestTemp)} ~ {today_data.get(highestTemp)}, 当前温度: today_data.get(temp), 风力: today_data.get(wind), 湿度: today_data.get(humidity), 更新时间: today_data.get(updateTime) }) # 7天预报 for day in forecast_data: rows.append({ 城市: city_name, 日期: day.get(date), 天气: day.get(weather), 气温: day.get(temp), 当前温度: , 风力: day.get(wind), 湿度: , 更新时间: }) df pd.DataFrame(rows) # 以追加模式写入CSV路径不存在时自动创建 df.to_csv(filename, modea, headernot pd.io.common.file_exists(filename), indexFalse, encodingutf-8-sig) print(f数据已保存到 {filename}) return dfmodea是追加模式多次运行不会互相覆盖适合批量采集多个城市。encodingutf-8-sig这里要敲黑板——如果你用utf-8不带BOM用Excel直接打开CSV中文会乱码加了-sig就没事了。这是我踩过的坑每次想起来都肉疼。3.6 主流程与控制逻辑最后把上面几个模块串起来。主流程负责调度查询城市代码、拉取当天天气、拉取7天预报、保存数据整个流程控制在几秒内完成import time from datetime import datetime def main(): # 要采集的城市列表可以自由扩展 cities [北京, 上海, 广州, 深圳, 成都] # 全局会话保持连接复用 session requests.Session() session.headers.update(HEADERS) for city in cities: try: print(f\n 正在采集: {city} ) # 第一步拿城市代码 city_code get_city_code(session, city) if not city_code: print(f{city}: 未找到城市代码, 跳过) continue print(f城市代码: {city_code}) # 第二步拉天气数据 today get_weather_today(session, city_code) forecast get_weather_7days(session, city_code) # 第三步保存 df save_weather_to_csv(city, today, forecast) print(df.head()) # 第四步礼貌性延时给服务器喘口气 time.sleep(random.uniform(1, 2)) except Exception as e: print(f{city}: 采集失败 - {e}) continue if __name__ __main__: main()random.uniform(1, 2)这个随机延时看似不起眼但批量采集时价值巨大。固定频率的请求模式容易被服务器识别成机器行为而随机的延迟时间让采集更像是一个人一边看网页一边做记录。这不是教你玩什么灰色技巧而是爬虫工程师的基本素养——不给对方服务器制造压力。3.7 完整代码整合为了方便直接跑我把所有代码整合成一个文件。注释我已经尽量写详细了每个函数的作用、每个参数的含义都在代码里标出来。你可以直接复制保存成weather_spider.py运行# -*- coding: utf-8 -*- 2345天气网城市天气预报数据采集 运行环境: Python 3.8 依赖库: requests, pandas 作者: 经验分享 import requests import pandas as pd import time import random import re from datetime import datetime # 请求头伪装, 模仿Chrome浏览器的正常访问 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, */*; q0.01, X-Requested-With: XMLHttpRequest } def get_city_code(session, city_name): 根据城市名查询城市代码 :param session: requests.Session对象 :param city_name: 城市名, 如北京 :return: 城市代码字符串, 如101010100 url https://tianqi.2345.com/Pc/GetCityCode params { cityName: city_name, _: int(time.time() * 1000) } resp session.get(url, paramsparams, timeout10) data resp.json() if data: return data[0].get(id) return None def get_weather_today(session, city_code, dateNone): 获取当天天气数据 :param session: requests.Session对象 :param city_code: 城市代码 :param date: 日期, 格式YYYYMMDD, 默认今天 :return: dict, 包含天气、温度、风力、湿度等 if date is None: date datetime.now().strftime(%Y%m%d) url https://tianqi.2345.com/Pc/GetWeather params { areaInfoId: city_code, areaType: 2, date: date } resp session.get(url, paramsparams, timeout10) data resp.json() if data.get(code) ! 0: raise ValueError(f接口返回错误: {data.get(errMsg, unknown)}) return data.get(data, {}) def get_weather_7days(session, city_code, dateNone): 获取未来7天预报 :param session: requests.Session对象 :param city_code: 城市代码 :param date: 日期, 格式YYYYMMDD, 默认今天 :return: list, 包含7天的预报数据 if date is None: date datetime.now().strftime(%Y%m%d) url https://tianqi.2345.com/Pc/GetWeather7Days params { areaInfoId: city_code, areaType: 2, date: date } resp session.get(url, paramsparams, timeout10) data resp.json() if data.get(code) ! 0: raise ValueError(f接口返回错误: {data.get(errMsg, unknown)}) return data.get(data, []) def save_weather_to_csv(city_name, today_data, forecast_data, filenameweather_data.csv): 将天气数据整合保存为CSV :param city_name: 城市名 :param today_data: 当天天气dict :param forecast_data: 7天预报list :param filename: 输出文件名 rows [] rows.append({ 城市: city_name, 日期: 今天, 天气: today_data.get(weather), 气温: f{today_data.get(lowestTemp)} ~ {today_data.get(highestTemp)}, 当前温度: today_data.get(temp), 风力: today_data.get(wind), 湿度: today_data.get(humidity), 更新时间: today_data.get(updateTime) }) for day in forecast_data: rows.append({ 城市: city_name, 日期: day.get(date), 天气: day.get(weather), 气温: day.get(temp), 当前温度: , 风力: day.get(wind), 湿度: , 更新时间: }) df pd.DataFrame(rows) df.to_csv(filename, modea, headernot pd.io.common.file_exists(filename), indexFalse, encodingutf-8-sig) return df def main(): 主流程控制 cities [北京, 上海, 广州] session requests.Session() session.headers.update(HEADERS) for city in cities: try: print(f\n 正在采集: {city} ) city_code get_city_code(session, city) if not city_code: print(f{city}: 未找到城市代码, 跳过) continue print(f城市代码: {city_code}) today get_weather_today(session, city_code) forecast get_weather_7days(session, city_code) df save_weather_to_csv(city, today, forecast) print(df) time.sleep(random.uniform(1, 2)) except Exception as e: print(f{city}: 采集失败 - {e}) continue if __name__ __main__: main()4. 运行效果与数据验证4.1 运行结果展示我实际跑了一次上面的代码输出大致是这个样子 正在采集: 北京 城市代码: 101010100 城市 日期 天气 气温 当前温度 风力 湿度 更新时间 0 北京 今天 晴 -2℃ ~ 8℃ 5℃ 西北风3-4级 35% 2025-01-25 08:00:00 1 北京 1月24日 晴转多云 -4℃ ~ 6℃ 北风3级 2 北京 1月25日 多云 -2℃ ~ 8℃ 西北风3-4级 ...CSV文件同时被写入用Excel打开能正常显示中文这要归功于utf-8-sig编码。整个流程从输入城市名到输出数据表耗时大约2秒再算上随机延时一个城市平均3秒处理完。批量采集20个城市的运行时间大概1分钟完全在合理范围内。4.2 数据可靠性怎么验证爬虫跑通了只能算成功了一半另一半在于验证数据对不对。我的验证方式是直接把爬下来的数据和网页上渲染出来的数字对一遍。2345天气网页面显示的今天晴、最高8℃、最低-2℃、西北风3-4级和接口返回的weather、highestTemp、lowestTemp、wind几个字段完全对得上。建议你也养成这个习惯——凡是爬虫项目取到数据第一件事不是急着存库而是手动比对几个关键字段。别偷懒这一步能帮你提前发现解析逻辑的bug。比如有一次我爬另一个网站把所有字段都取对了唯独wind字段因为JSON里嵌套了一层数组导致取出来是全角符号的字符串比对时才暴露问题。4.3 批量采集的扩展思路这个项目的扩展空间非常大。你以为它只是一次性的天气数据采集换个思路它可以变成很多有趣的东西把cities列表换成一个包含全国几百个城市的列表就能做全国天气数据的横向对比。在循环外面套一个while True time.sleep(3600)每小时采集一次就能积累长时间序列的天气数据。把结果写入SQLite或MySQL加上时间戳就成了一个准实时的天气历史数据库。我见过有人把2345天气网的数据采集做成定时任务连续跑了三个月积累了27万条天气记录然后用这些数据做了一个城市宜居度分析。这些应用场景说起来不复杂但数据是一天一天攒出来的——这也是爬虫项目最迷人的地方它是积累的艺术。5. 常见问题与避坑实录5.1 请求头缺失导致接口拒绝访问很多新手拿到代码第一件事就是把请求头删了觉得无所谓。结果一跑接口返回空数据或者403状态码。我在实际调试中遇到过不止一次症状就是GetWeather返回的JSON里code不是0或者直接返回一堆HTML而不是JSON。原因就是服务器校验了User-Agent和Referer。缺了Referer服务器会认为你不是从2345页面跳转过来的缺了X-Requested-With后端会认为这不是Ajax请求而拒绝服务。解决方案很简单把请求头完整带上别自作聪明精简。提醒有些教程喜欢把请求头精简到只剩User-Agent这种极简风在别的网站可能好用但到了接口有严格校验的站点就是作死的节奏。宁可多带几个字段也别少带。5.2 城市名匹配不上拿不到城市代码输入北京市能查到输入北京却查不到或者输入成都返回了一堆结果这些情况我在测试时都撞上过。2345天气网的城市代码接口用的是模糊匹配查询北京和查询北京市可能返回不同的结果列表。如果你的城市名是用户手动输入的你永远不知道他们会输什么。稳妥的做法是先查body不存在就自动尝试加后缀市或者把城市名用规范化规则清洗一遍。更极端的情况是用户输入了帝都魔都这种别名。这种就别指望接口能认得了最好的方案是维护一个常用城市名映射表把别名映射到标准名。爬虫项目里这种脏数据清洗的代码虽然不是核心逻辑但恰恰是体现工程成熟度的地方。5.3 CSV打开中文乱码这个问题出现频率奇高每次群里有新人问我闭着眼睛都能猜到原因——to_csv的时候用了encodingutf-8。Excel默认用ANSI编码打开CSV文件遇到UTF-8编码的中文直接显示成一堆乱码。解决办法是在to_csv指定encodingutf-8-sig这个编码会在文件开头加一个不可见BOM标记Excel识别到之后就自动切到UTF-8解析。如果你的数据不需要用Excel打开只做程序读取那用utf-8也没问题。关键是明确自己的使用场景别偷懒不管编码。5.4 访问频率过高IP被封怎么办2345天气网的反爬强度不算高但你要是拿多线程并发去怼它比如同时开50个线程疯狂请求封IP是板上钉钉的事。我测试时曾为了快速拉取200个城市的数据用concurrent.futures开了30个线程并发跑结果跑到第80个城市的时候接口开始返回异常数据再往后直接超时一看IP已经被临时封禁了。正确的做法是控制并发数——单线程配随机延时就够用实在要提速线程数控制在5个以内延时间隔保持在0.5秒以上。爬虫这项工作的目标不是把对方服务器打到宕机而是在自己的需求和对方服务器的承受能力之间找到平衡点。说白了做人留一线日后好相见。5.5 接口变动了怎么办2345天气网的接口曾经调整过几次。比如某次改版后GetCityCode接口从返回纯JSON变成了返回JSONP格式——就是返回内容长这样jQuery34109450164576982928_1710234567890([{id:101010100,name:北京,...}])如果你直接用resp.json()解析恭喜你报错没跑。解决方法是先用正则把最外层的函数调用壳剥掉再走JSON解析。import re text resp.text.strip() if text.startswith(jQuery): match re.search(r\((\[.*\])\), text, re.S) if match: text match.group(1) data json.loads(text)这种情况提醒我们接口文档不是永恒的站点重构、参数调整、返回格式变化都是家常便饭。所以爬虫代码里要留好异常处理的出口一旦解析失败立刻把原始响应保存到日志文件里方便排查。我在实际开发中就是先把resp.text打印出来看一遍确认格式没问题再写解析逻辑。这个习惯帮我省了无数排查时间。写在最后的小心得这个项目虽然只用了requests加pandas但我始终觉得它是一块很好的磨刀石。完整的爬虫工作流——抓包分析、请求构造、数据解析、清洗存储、异常处理——全都有涉及而且难度曲线平滑不会一上来就把人劝退。我自己在带新人时都会让他们先把2345天气网这类站点吃透再考虑上Scrapy框架或者接数据库。原因很简单先把一条路走出来再来升级工具而不是拿着高级武器却连路都找不到。如果你把这个案例跑通了下一步可以试着把数据存进SQLite加上定时任务让它自动跑一周到时候你再回来看这批积累的数据你会发现爬虫真正的乐趣不是爬到数据的那一刻而是数据开始产生价值的那个瞬间。最后分享一个我自己的习惯每次写完爬虫我会在代码头部留一个注释块记录这个爬虫的抓包时间、接口地址、字段结构。别小看这几行注释——两周后你再回来看代码能救命的往往就是它们。
返回列表