恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
前端转AI:用requests写第一个爬虫,从HTTP请求到429限流应对
首页
资讯中心
/
前端转AI:用requests写第一个爬虫,从HTTP请求到429限流应对
前端转AI:用requests写第一个爬虫,从HTTP请求到429限流应对
发布时间:2026/10/17 15:55:24
1. 前端转AI第7天为什么第一个爬虫项目从requests开始1.1 坦白说前端转AI最容易被卡住的地方在哪里我在第1天定学习计划的时候把Python基础排在前面当时觉得语法刷完就算过了。可真到了写项目的那天发现自己根本不会用Python去干实事——前端这么多年遇到跨域问题第一反应是找后端联调遇到数据需求第一反应是问接口文档。直到第7天我意识到转AI这条路上数据获取能力是绕不开的地基。训练样本从哪来、模型效果怎么验证、API评测怎么做全都依赖能把数据稳定地抓下来这个基本动作。第7天的任务我选的是requests爬虫实战。选它的理由很直接它是Python生态里最基础、最接近前端fetch心智模型的HTTP客户端库没有Scrapy那种框架层面的学习成本也不需要Playwright这种浏览器自动化的重型武器。一个前端开发从fetch切换到requests本质上只是换了个语法外壳底层的HTTP状态码、请求头、JSON序列化都是老朋友。1.2 requests、Scrapy、Playwright第7天该选谁很多教程一上来就推Scrapy理由是工业级高性能但对一个刚接触爬虫的前端来说Scrapy的Spider类、Item Pipeline、中间件机制一套组合拳下来很容易把学习重点从理解HTTP请求带偏到背框架API。我给自己定的原则是第7天只解决一个问题——怎么用代码稳定地拿到一份真实数据。等到理解了请求的完整生命周期再上框架不迟。我当时排了一下三个方案的差异用一张表说清楚方案上手成本典型场景对前端转型者的友好度requests最低一个函数搞定请求调API、抓JSON数据、轻量页面抓取极高和fetch/axios心智模型几乎一一对应Scrapy较高需要理解框架约定大规模整站抓取、增量抓取、分布式任务中等适合第二或第三阶段再学Playwright高但无需理解HTTP细节动态渲染页面、需要登录态、模拟用户操作低等遇到前端渲染拦截再学也不迟热词里有人搜playwright相比于直接解码爬虫的优点这个问题我后来的理解是Playwright适合应对页面被JavaScript动态渲染、接口加密、需要真实浏览器指纹的场景但它绕过了HTTP层面的学习直接跳到模拟用户操作。对一个前端来说先明白requests怎么发请求、怎么读响应、怎么处理限流比直接上Playwright盲目抓动态页面有价值得多——因为你本来就会JS去学Playwright等于用自己不熟的Python重新学一遍DOM操作这不合算。1.3 今天这个项目的学习边界我给Day 7划定的边界是不搞分布式、不搞代理池、不搞验证码识别就写一个能跑、能存、能应对常见报错的单文件脚本。目标有四个——第一用requests成功拿到一个真实API的JSON数据第二理解请求头和响应头的实际作用第三处理一个爬虫入门最常见的坑429限流第四通过异常处理让脚本能在真实网络环境下活下来。这四个目标全部达成之后Scrapy之类的框架想什么时候学都可以requests打下的底子一点不会浪费。2. 从fetch思维切到requests先发通一个真实的API请求2.1 安装与第一个请求环境准备没什么悬念。Python环境装好之后pip install requests一分钟搞定。我没有用虚拟环境因为第7天还没有多项目依赖冲突的烦恼等后面做大模型API调用项目再上venv或conda也不迟。写第一个请求的时候我的心态就是把前端的fetch翻译成Python。下面是当时的第一个版本请求GitHub的用户公开信息import requests url https://api.github.com/users/octocat response requests.get(url) print(response.status_code) print(response.json())这段代码跑通的时候其实并没有太多激动因为它在逻辑上长得太像前端的fetch了。但很快我就发现了第一个差异前端在浏览器里发跨域请求浏览器会自动带上一些请求头比如Origin、User-Agent、Referer还有同源策略帮你挡掉一部分问题而requests发出的请求默认的User-Agent是类似python-requests/2.31.0这样的字符串服务器一眼就能看出你是脚本。2.2 fetch和requests的心智模型对比我记得自己在看到response.json()可以一行解析的时候脑子里对照了一下前端写法// 前端 fetch(https://api.github.com/users/octocat) .then(res res.json()) .then(data console.log(data)) .catch(err console.error(err))# requests import requests response requests.get(https://api.github.com/users/octocat) data response.json() print(data)表面看就是语法差异但requests的响应对象response比fetch的Response多了一层已经帮你处理好的便利。fetch的res.json()是异步的因为浏览器里网络IO不阻塞主线程requests的response.json()是同步的因为Python脚本本身就是单线程阻塞模型。这个差异对一个前端来说反而是友好的——你不用写async/await代码从上到下按顺序执行理解起来更直觉。不过同步模型也意味着如果网络慢或者对方服务器不响应你的脚本会一直卡在那里。这在后面引出了设置timeout的必要性我在第5章会细讲。2.3 响应对象到底有什么我建议每个新手在拿到response之后都打印一下这几个属性眼见为实response.status_code状态码200代表成功404代表资源不存在429代表限流response.headers响应头这里面有Content-Type、Server还有限流相关的X-RateLimit-Remaining等字段response.url最终请求的URL有时候你传了params参数实际请求的地址会被拼接看这个能确认response.request.headers请求头方便确认实际发出去的头长什么样这里有一个前端容易忽略的细节response.status_code本身等于200不代表业务成功。很多API在业务出错时也返回200靠JSON体里的code字段区分状态。比如一些国内的接口规范是{code: 500, message: 参数错误}HTTP层却是200。所以解析JSON后第一个判断应该是业务码而不是状态码。这个坑我第7天没遇到但后来调大模型API时几乎天天见。3. 抓取真实API的全流程找接口、伪装请求头、解析JSON3.1 选一个适合练手的真实API很多教程喜欢用httpbin.org这类测试接口它虽然稳定但抓下来的数据是死的缺少真实业务的感觉。我选GitHub API理由有三第一无需注册就能调匿名配额是每小时60次足够学习第二数据结构规范字段名清晰适合练JSON解析第三GitHub在技术圈里是正当数据源不会有什么合规风险。除了GitHub API当时还研究过另外几个备选都适合练手API是否需要密钥配额情况适合练什么GitHub API匿名可用带token配额更高匿名60次/小时用户信息、仓库列表、JSON解析天气API如Open-Meteo无需密钥较宽松纬度经度参数传递、时间序列数据icanhazdadjoke无需密钥宽松随机文本、请求头定制CoinGecko行情API无需密钥按次数限制数字货币行情、嵌套JSON处理我最终用GitHub API作为主项目因为它的限流机制非常透明响应头里有明确的剩余配额信息非常适合用来理解什么叫被限流。3.2 请求头伪装是爬虫的第一课requests发出去的默认请求头太有辨识度了。我测试了一下不加任何请求头直接请求GitHub API虽然能返回数据但有些做了基础防护的网站直接就会拒绝返回403。于是我给请求加上了浏览器身份的User-Agentimport requests url https://api.github.com/users/octocat 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: application/vnd.githubjson, Accept-Language: zh-CN,zh;q0.9, } response requests.get(url, headersheaders, timeout5) print(response.status_code) print(response.headers.get(X-RateLimit-Limit)) print(response.headers.get(X-RateLimit-Remaining))把User-Agent改成浏览器标识本质上就是告诉服务器我是一个正常的浏览器用户。但这里要说明白这不等于黑客行为HTTP协议本身允许客户端自定义请求头GitHub API官方文档也要求开发者设置一个有辨识度的User-Agent。伪装请求头只是网络请求的常规礼仪它解决的是被当成垃圾流量的问题而不是破解反爬的问题。抓数据的时候如果响应返回的Content-Type是text/html而不是application/json大概率是服务器把你当成普通浏览器返回了一个HTML页面而不是接口数据。可以加一个Accept字段值为application/json来引导服务器返回JSON。3.3 用params传递查询参数真实世界里的API几乎没有不带参数的。GitHub的搜索仓库接口就是典型的例子。我当时的练习是搜索Star数超过1000的Python仓库按Star数排序。import requests base_url https://api.github.com/search/repositories params { q: language:python stars:1000, sort: stars, order: desc, per_page: 5, } headers { User-Agent: Mozilla/5.0, Accept: application/vnd.githubjson, } response requests.get(base_url, headersheaders, paramsparams, timeout10) data response.json() for repo in data.get(items, []): print(repo[full_name], | Stars:, repo[stargazers_count]) print( , repo[description])request.get的params参数会自动做URL编码把字典拼到URL后面。对比前端的axios写法用params传入查询参数也不需要手动拼接问号和符号。这一点requests和axios的体验几乎完全一致唯一的区别就是requests不需要担心编码问题——它默认会用UTF-8处理。3.4 JSON解析与数据清洗拿到真实数据之后解析环节才是前端真正的主场。嵌套字典、数组遍历、None值判断这些在JavaScript里写过无数次的操作换成Python语法没有障碍。但有一个差异值得注意JavaScript的JSON.parse和Python的response.json()在遇到非法JSON时都会抛异常但Python的None判断更直白——直接if item.get(description)就行不会出现前端那种undefined和null混合判断的混乱。我当时写了一段把仓库数据清洗成结构化列表的代码顺便把可能的None值处理掉cleaned [] for repo in data.get(items, []): cleaned.append({ name: repo[full_name], stars: repo[stargazers_count], language: repo.get(language) or unknown, description: (repo.get(description) or ).strip()[:100], url: repo[html_url], }) for item in cleaned: print(item)清洗完的数据可以导出成JSON文件存起来这就完成了抓取-解析-落地的最小闭环。前端转AI的人对这个流程不陌生它有点像前端的请求拦截器加数据状态管理只不过这里没有响应式更新只有最原始的IO。4. 429 Too Many Requests限流拦截的定位与应对4.1 我是怎么触发429的Requests爬虫实战真正开始出状况是在我把请求放进循环之后。当时我的第一版爬虫脚本长这样for page in range(1, 6): params { q: language:python stars:1000, sort: stars, order: desc, per_page: 30, page: page, } response requests.get(base_url, headersheaders, paramsparams, timeout10) data response.json() print(fPage {page}: got {len(data.get(items, []))} repos)这个循环跑得很快快到像闪电一样。第1页正常第2页正常第3页还没打印完终端里蹦出一个刺眼的响应码429。后面的请求全部失败了。这正是热词里很多人搜到的那个典型错误exceeded retry limit, last status: 429 too many requests。我当时第一反应是不科学啊GitHub匿名限流不是60次每小时吗我这才发了10次不到。后来读了响应体才知道GitHub的搜索接口是独立限流的配额远低于普通接口按分钟计算。而且它有一个特殊的响应头Retry-After会明确告诉你需要等多少秒。4.2 读懂响应头里的限流信息触发限流之后官方文档不会帮你在浏览器里弹窗提示但响应头会说。当时我打印了完整的响应头response requests.get(base_url, headersheaders, paramsparams, timeout10) print(status_code:, response.status_code) print(Retry-After:, response.headers.get(Retry-After)) print(X-RateLimit-Remaining:, response.headers.get(X-RateLimit-Remaining)) print(X-RateLimit-Limit:, response.headers.get(X-RateLimit-Limit))X-RateLimit-Limit代表配额上限X-RateLimit-Remaining代表剩余额度Retry-After代表需要等待的秒数。这三个字段组成了限流的完整信号。网络请求里的限流机制本质上是服务端保护自身资源的方式——前端开发时几乎不用关心因为浏览器用户不可能毫秒级连续请求同一个接口但爬虫脚本完全做得到。理解到这一层我意识到429并不是什么神秘错误它就是服务器在对你说你太快了慢一点再来。解决方案也不是去绕开限流而是按照提示等待。这也是爬虫和外挂App之间的一条重要分界线尊重服务端的显式控制信号。4.3 指数退避的实现最简单的应对方式是收到429后等Retry-After指定的秒数再重试。但真实网络环境里Retry-After头不一定每次都返回所以更通用的方案是实现指数退避——第一次失败等1秒第二次等2秒第三次等4秒按指数增长同时设置一个最大等待时间防止无限等下去。我用一个带退避逻辑的重试函数来解决import time import requests def fetch_with_retry(url, headersNone, paramsNone, max_retries5): for attempt in range(max_retries): response requests.get(url, headersheaders, paramsparams, timeout10) if response.status_code 200: return response if response.status_code 429: retry_after int(response.headers.get(Retry-After, 2 ** attempt)) wait_time min(retry_after, 30) print(f429 received, waiting {wait_time}s...) time.sleep(wait_time) continue if response.status_code 500: wait_time min(2 ** attempt, 30) print(fServer error {response.status_code}, retrying in {wait_time}s) time.sleep(wait_time) continue response.raise_for_status() raise RuntimeError(fExceeded retry limit, last status: {response.status_code})这个函数的核心逻辑是200直接返回429按Retry-After或指数退避等待5xx服务端错误也做退避重试4xx其他错误直接抛异常不再重试。为什么4xx客户端错误不重试因为参数写错了重试多少次都一样方向错了再努力也没用。这个判断逻辑让我少做了很多无效请求。4.4 关于重试最重要的一个教训热词里还有一句高频报错exceeded retry limit, last status: 429 too many requests, request id: xxx。我的理解是很多人把重试写成了死循环式的失败就立刻再试结果就是429了还疯狂重试不仅拿不到数据还进一步加重了服务端压力甚至可能触发封IP。正确的处理思路是三条第一重试之间必须有间隔且间隔要逐步拉长第二设定最大重试次数超过次数就放弃并记录日志第三所有重试共享同一个session让服务端看到你是同一个客户端在持续请求而不是不断建立新连接。第二点特别重要——爬虫脚本和正常用户的区别不在于会不会失败而在于失败之后的行为是否符合理性。5. 给爬虫装上安全气囊异常处理、超时与重试5.1 不设timeout的后果我的爬虫脚本不是一开始就健壮的。早期版本里requests.get没写timeout参数结果有一次对方服务器响应超时脚本直接卡住不动了。在浏览器里请求挂起无所谓用户可以刷新页面但脚本一旦挂起整个任务就停摆后面所有请求全部排队等待看起来像死循环。timeout参数是requests里性价比最高的一个参数。它接受一个数字代表秒数超过这个时间就抛出requests.exceptions.Timeout异常。更精细的用法是传一个元组比如timeout(3, 5)意思是连接阶段最多等3秒读取阶段最多等5秒。连接超时解决的是连不上的问题读取超时解决的是连上了但不返回的问题。我建议所有请求都设置timeout宁可偶尔超时重试也不要卡死一次拖垮整个任务。5.2 用try-except捕获哪几类异常Requests库的异常体系我实际跑下来发现值得捕获的主要有五类异常类型触发场景我的处理方式requests.exceptions.Timeout连接或读取超时捕获后退避重试requests.exceptions.ConnectionErrorDNS解析失败、连接被拒、网络断连捕获后重试或记录日志requests.exceptions.HTTPError4xx/5xx且调用了raise_for_status()区分限流与其他状态码requests.exceptions.JSONDecodeError响应不是合法JSON记录响应内容并跳过requests.exceptions.RequestException其他所有请求层异常兜底捕获防止脚本崩溃写异常处理的时候我的一个经验是不要用一个大的try-except包住整个循环。如果循环里某个请求失败整个循环都退出去那你前面的成果全丢了。更合理的结构是try包住单次请求失败后记录日志然后continue继续下一个请求。让异常影响范围最小化而不是最大化。5.3 一个可以直接跑的健壮版本把前面的知识点整合起来我当时最终跑通的项目脚本核心逻辑是这样的import json import time import requests class GitHubRepoFetcher: def __init__(self, tokenNone): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (compatible; MyLearningBot/1.0), Accept: application/vnd.githubjson, }) if token: self.session.headers.update({Authorization: ftoken {token}}) def fetch_with_retry(self, url, paramsNone, max_retries5): for attempt in range(max_retries): try: response self.session.get(url, paramsparams, timeout(3, 10)) if response.status_code 200: return response.json() if response.status_code 429: retry_after int(response.headers.get(Retry-After, 2 ** attempt)) wait_time min(retry_after, 30) print(f[限流] 等待 {wait_time}s 后重试) time.sleep(wait_time) continue if response.status_code 500: wait_time min(2 ** attempt, 30) print(f[服务端错误] {response.status_code}{wait_time}s 后重试) time.sleep(wait_time) continue print(f[客户端错误] {response.status_code}放弃重试) return None except requests.exceptions.Timeout: print(f[超时] 第 {attempt 1} 次尝试超时) time.sleep(2 ** attempt) except requests.exceptions.ConnectionError: print(f[连接失败] 第 {attempt 1} 次尝试连接失败) time.sleep(2 ** attempt) except requests.exceptions.RequestException as e: print(f[未知请求异常] {e}) break print([失败] 超过最大重试次数) return None fetcher GitHubRepoFetcher() all_repos [] for page in range(1, 6): params { q: language:python stars:1000, sort: stars, per_page: 30, page: page, } data fetcher.fetch_with_retry(https://api.github.com/search/repositories, paramsparams) if data and data.get(items): all_repos.extend(data[items]) print(f第 {page} 页成功累计 {len(all_repos)} 条) time.sleep(1) # 主动控制频率避免触发限流 with open(github_repos.json, w, encodingutf-8) as f: json.dump(all_repos, f, ensure_asciiFalse, indent2) print(f共抓取 {len(all_repos)} 个仓库已保存到 github_repos.json)这个脚本有几个细节值得展开说用requests.Session()而不是直接requests.get好处是复用底层TCP连接同时保持Cookie和请求头的一致性。Session在一次会话里维护相同的连接池性能更好也更接近一个客户端的身份语义。主动time.sleep(1)是对服务器的礼貌这个习惯在面对任何不熟悉的网站时都应该保留。GitHub搜索接口限制是10次/分钟30页数据如果每页等1秒既不会触发限流也完全够用。跑完这个脚本之后本地会多出一个github_repos.json文件。文件的落地意味着一个完整的数据获取流程闭环了发请求、处理异常、应对限流、结构化存储。到这一步前端转AI的第一块数据能力拼图就算装上了。6. 爬虫不止是技术频率控制与合规边界6.1 别让你的脚本成为攻击脚本Requests本身没有善恶属性但一个没有频率控制的循环有时确实会给对方服务器带来压力。我的经验是凡是自己开发的爬虫脚本第一步永远是评估目标服务的承受能力。GitHub API有官方限流说明可以直接按文档要求来但对于没有公开API的站点请求间隔至少要保持在1秒以上动态渲染页面更要谨慎。频率控制的本质是把自己想象成一个人类用户。人类浏览页面需要几秒思考时间不会在毫秒级连续点击几百次。我现在的习惯是单机学习用途请求间隔不低于1秒每秒最多一个请求批量任务前先在小范围内测试确认对方没有明显压力后再扩大。这样做既保护自己的IP不被封也避免给目标服务器添麻烦。写作时回想一下很多响应码是429的报错其实都是因为脚本的请求节奏已经远远偏离了人类行为。6.2 robots.txt与数据使用边界前端开发者对浏览器开发者工具非常熟悉看到数据自然会想看它是怎么加载的。但能看到不等于可以随便抓。正规的做法是在抓取一个站点之前先查看它的robots.txt文件确认哪些路径允许抓取。robots.txt是站点方用明文表达的抓取规则虽然它对编程脚本没有强制约束力但它体现了网站运营者对自己数据的边界声明。GitHub的robots.txt就明确规定了不同路径的抓取许可范围。尊重这个文件是爬虫开发者最基本的职业素养。还要提醒一点抓下来的数据怎么用比怎么抓更重要。公开接口返回的数据也存在版权和使用条款。就拿GitHub来说它的API使用条款明确了不允许用抓取的数据做某些竞品分析。我的建议是学习项目里数据只用于个人研究和技能练习不商用、不二次分发、不做任何可能损害原平台的行为。这也是我在前面选GitHub API作为教学数据源的原因——它的使用条款清晰API文档明确鼓励开发者在限流范围内调用属于相对安全的练习对象。6.3 前端转AI视角下的爬虫定位前端转AI的学习路径里爬虫经常被误解为一个独立的黑客技能其实在我眼里它更像数据管道的第一节。后续要学的大模型API调用、数据集整理、RAG应用开发每一步都需要用HTTP请求去获取数据。Day 7学的requests基础直接可以迁移到调用大模型API的场景——只是把GitHub的接口换成大模型服务的接口把JSON响应换成模型返回的文本异常处理和限流应对的逻辑几乎一模一样。我在后续Day 9到Day 12调用大模型API时发现很多报错思路都能回溯到Day 7。比如API返回400错误模型名称不对、429限流需要退避重试、响应体里的JSON结构要容错解析这些在GitHub API练习中全部踩过一遍。前期把requests的坑踩透后期接大模型API时就能把精力专注在提示词和模型参数上而不是被网络请求的基础问题绊住。对我个人来说Day 7最值得记录的并不是写出了多少行代码而是建立了一个认知前端开发时浏览器的请求上下文把很多底层细节藏起来了一旦脱离浏览器HTTP请求的每一个环节——请求头、状态码、限流策略、连接超时——都会袒露到你面前。这不是麻烦这恰好是向前端开发者补足基础的好机会。如果你也在经历前端转AI的阶段我建议今天就把这个练习跑一遍别急着上框架先亲手写完一个requests请求再亲手处理一次429整个过程花不了两个小时但对后续学习会产生长期帮助。