ARTICLE DETAIL

资讯详情

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

HTTP协议与Requests库实战:爬虫与API调用避坑指南

HTTP协议与Requests库实战:爬虫与API调用避坑指南 兄弟们99天精通Python系列走到第27天今天这块内容可以说是整个系列的转折点。HTTP协议和Requests库一个是通信规则一个是Python里最趁手的工具学会这两样你就能写出人生第一个爬虫也能正儿八经去调别人的API接口。我见过太多新手卡在这一步网上找了一圈爬虫教程一看例子全是requests.get(url)然后print(response.text)跟着敲完发现要么乱码要么直接报错要么被服务器拒绝访问。问题出在哪出在大部分人只学会了怎么调没搞懂为什么这么调。HTTP协议是爬虫和API调用的共同地基Requests库是你手里的工具地基不牢工具再好也白搭。这篇文章我会把HTTP协议里跟爬虫、API强相关的部分拆开讲透然后带你把Requests库的常用功能全部过一遍。全程都是我做爬虫和调API过程中沉淀下来的经验怎么构造请求、怎么处理返回、踩过哪些坑你照着敲一遍基本就能上手。1. HTTP协议到底在干什么爬虫和API绕不开的底层逻辑很多人一听协议两个字就头大觉得是那种几百页的RFC文档其实不用想那么复杂。HTTP协议的本质就是客户端和服务端之间的一套点餐流程你打开浏览器访问网页相当于走进餐厅坐下浏览器帮你把我想吃什么写在菜单上递给服务员服务员把菜单交给后厨后厨做好菜再端到你桌上。菜单是请求Request端上来的菜是响应Response。1.1 一条HTTP请求由什么组成在你眼里一个网址可能就是一行URL但在HTTP世界里一次请求由四部分构成请求方法、请求URL、请求头Headers、请求体Body。先看URL。一条标准的URL长这样https://api.github.com/users/octocat/repos。拆开来看https是协议api.github.com是域名/users/octocat/repos是路径如果后面还跟着?page2per_page10这种那一串就叫查询参数Query Parameters用分隔多个键值对。爬虫里翻页、筛选条件基本都是靠这部分传递的。再看请求方法。最常用的是GET和POST。GET的意思是我想拿东西比如打开一个新闻页面POST的意思是我送点东西给你你再给我结果比如登录时提交用户名密码。PUT是整体更新资源PATCH是局部更新DELETE就是删资源。做爬虫初期你只需要把GET和POST玩明白就够了API开发后面再补其余三种也来得及。请求头里藏着大量关键信息。User-Agent标识你是什么客户端浏览器、爬虫脚本还是手机AppCookie携带你的登录凭证Content-Type告诉服务器你发的请求体是什么格式。很多网站的反爬策略第一道防线就是检查User-Agent你用Requests默认的python-requests/2.x去访问人家一眼就识破你不是真人浏览器。1.2 HTTP状态码服务器给你的体检报告每次服务端返回响应时都会附带一个三位数的状态码告诉我们这次请求到底成没成。做爬虫调试第一件事就是看这个码。我把常用的码给你分个类状态码含义爬虫/API实战中怎么处理200请求成功正常解析返回内容即可301/302重定向可能是网址迁移或登录跳转Requests默认会自动跟随401未认证没带Token或API Key带上再试403禁止访问大概率被反爬拦截了检查请求头和频率404资源不存在URL路径写错或者数据已被删除429请求太频繁触发限流必须放慢速度或增加等待时间500/502/503服务器内部错误优先重试也可能是你请求参数把对方接口逼崩了记住一个原则状态码是4开头的问题多半出在你这边的请求上状态码是5开头的问题多半出在服务器那边。这两个方向排查起来完全不同。1.3 HTTP无状态与Session为什么爬虫需要保持登录HTTP协议是无状态的意思是每次请求之间互相独立服务器默认不记得你是谁。你登录网站后服务器为了记住你的身份会在响应里给你发一个Cookie你后续每次请求都带着这个Cookie它就知道哦是刚才登录的那个用户。Requests库里的Session对象做的就是这件事它会自动帮你保存和携带Cookie让你的多次请求共享同一个会话状态。做爬虫时如果用同一个Session去访问登录后的页面就不会出现明明登录成功却依然跳转到登录页的情况。这个细节后面实操部分我会再重点演示。2. Requests库上手安装、GET与POST实战Requests的定位就是让HTTP请求变成人类能看懂的样子。它把底层的urllib封装得极其顺手官方文档第一句话就是给人类用的HTTP库这句话一点不夸张。你不需要关心socket连接、报文组装这些细节只需要告诉它请求哪个URL、带什么参数、用什么方法剩下的它全包了。2.1 安装与版本确认先检查你的Python环境里装没装。打开终端或命令行输入pip install requests装完之后验证一下版本确保能用python -c import requests; print(requests.__version__)如果你发现是旧版本升级命令也不复杂pip install --upgrade requests这里有个小提醒如果你电脑上同时装了Python 2和Python 3Windows下可能要用pip3或者python -m pip才能装到正确的环境里。别嫌我啰嗦我见过太多人装完了import报错结果发现装到另一个Python版本里去了。用python -m pip install requests是最稳的写法。2.2 GET请求从URL到响应文本最基础的GET请求三行代码搞定import requests resp requests.get(https://api.github.com/users/octocat/repos) print(resp.status_code) # 200 print(resp.text) # 响应内容字符串形式但实际开发中URL往往需要动态拼接参数千万别自己手动去拼?page2per_page10这种字符串又丑又容易出错。Requests提供了params参数你只管传字典进去它帮你完成URL编码import requests params { page: 2, per_page: 10, type: public } resp requests.get(https://api.github.com/users/octocat/repos, paramsparams) print(resp.url) # 输出完整URL方便核对打印resp.url这一步特别适合调试——你可以看到Requests到底把参数拼成了什么样如果服务器返回的参数跟你预期不一致先看这里。响应对象上还有几个常用属性。resp.text拿到的是字符串格式的响应体resp.content拿到的是字节格式适合下载图片、文件这类二进制数据resp.json()则会把响应体解析成Python字典或列表这是调用JSON接口时最常用的方法。要注意resp.json()只有在响应确实是合法JSON时才不会报错否则会抛出JSONDecodeError后面我会讲怎么预防。2.3 POST请求data与json的微妙区别POST请求比GET多了一个请求体也就是你要提交给服务器的数据。提交格式不同服务器解析方式也不同。如果你提交的是表单格式用data参数import requests form_data { username: test_user, password: 123456 } resp requests.post(https://httpbin.org/post, dataform_data) print(resp.text)如果你提交的是JSON格式现在绝大多数API接口都是这种用json参数import requests json_data { name: Tom, age: 18 } resp requests.post(https://httpbin.org/post, jsonjson_data) print(resp.text)这两个参数最大的区别在于data发送的是application/x-www-form-urlencoded格式像是把表单字段编码进请求体里json发送的是application/json格式并且Requests会自动帮你把字典序列化成JSON字符串。如果你拿data去传一个被期望接收JSON的接口大概率收到400或415错误。先去读接口文档确认它要什么格式再决定用哪个参数这是我在调API时踩过最多坑的地方。还有一类特殊情况有些服务端要求请求体是纯字符串。你可以直接传dataraw string但要注意加上对应的Content-Type请求头。3. 爬虫与API实战中最容易忽略的四个细节做爬虫和调API你很快会发现一个真理80%的问题不是出在不会发请求而是出在请求发得不够像真人。服务器是人在维护的反爬策略也是人在设计的你越像真实的浏览器访问越不容易被拦。3.1 请求头伪装User-Agent只是第一层先看一个最简单的UA设置import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } resp requests.get(https://httpbin.org/headers, headersheaders) print(resp.text)很多网站的反爬只查UA你把这一行加上去就能过了。但量级稍微大一点的网站会继续检查Referer你从哪个页面跳过来的、Accept-Language浏览器语言甚至会校验请求头顺序。我的经验是打开浏览器开发者工具F12切到Network面板随便点开一个请求把里面的请求头照抄下来做成字典用。这套操作比你在网上抄一段别人写好的headers要靠谱得多因为不同网站的校验重点不一样。3.2 Session对象让多个请求共享状态接着上面说的保持登录问题。很多人用多个requests.get去抓同一个网站时发现第二次请求拿不到登录后的数据原因就是每次请求都是独立的会话Cookie没有带上。正确写法是先登录然后用同一个Session去请求后续页面import requests session requests.Session() login_data { username: your_account, password: your_password } session.post(https://httpbin.org/post, datalogin_data) # 后续所有请求都用这个sessionCookie自动携带 resp session.get(https://httpbin.org/cookies) print(resp.text)如果你手头已经有一个Cookie字符串比如从浏览器F12里复制出来的也可以直接塞进Sessionsession requests.Session() session.cookies.update({session_id: xxxxxx})Session还有一个好处是可以统一设置公共请求头比如所有请求都要带Token你只需要session.headers.update({Authorization: Bearer xxx})后面的请求自动带上不用每次写。3.3 超时、重试与SSL稳定性的三件套爬虫跑着跑着突然卡死是最让人抓狂的情况。原因通常是目标服务器没响应而你没有设置超时时间程序就一直等下去。所以强烈建议每个请求都加上timeoutresp requests.get(https://httpbin.org/delay/5, timeout3)这里有个细节timeout3在Requests里既代表连接超时也代表读取超时但有些版本的库支持传元组(connect_timeout, read_timeout)更精确resp requests.get(https://httpbin.org/delay/5, timeout(3, 5))超时触发了会抛requests.exceptions.ConnectTimeout或ReadTimeout你需要在代码里捕获import requests from requests.exceptions import RequestException try: resp requests.get(https://httpbin.org/delay/5, timeout3) except RequestException as e: print(f请求失败: {e})遇到SSL证书验证失败的情况也常见尤其是一些证书配置不规范的小网站。最简单的处理是跳过验证书但会弹警告import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) resp requests.get(https://some-weird-site.com, verifyFalse)注意verifyFalse只应该用于你自己能把握风险的环境。如果你在做的项目要长期稳定跑遇到证书问题还是先排查证书本身别一上来就跳过验证。3.4 中文乱码编码问题的根因与解法很多人第一次爬取中文网页时都会遇到乱码然后怀疑是Requests的锅。其实Requests不背这个锅它确实帮你自动猜了编码但猜得不准时就会乱码。resp.text的编码推断逻辑是这样的它先看响应头里的Content-Type字段有没有charset有就按它来没有就根据响应内容去推测。一旦推测错了中文显示就会变成天书。最稳的解法是自己指定编码resp requests.get(https://www.baidu.com) resp.encoding utf-8 print(resp.text)或者先让Requests猜再取反馈值resp.encoding resp.apparent_encodingapparent_encoding是Requests基于响应内容做字节级分析得出的编码比默认的猜测要准。如果遇到GBK编码的旧式页面把resp.encoding gbk试一下就好。4. 常见问题与排查技巧实录说实话这部分的含金量比前面所有代码示例加起来都高。因为代码示例你看一遍就懂但实战中遇到的问题千奇百怪没有一套排查思路很容易卡死。4.1 常见状态码速查表与处理方案状态码常见场景我的处理方案401 UnauthorizedAPI Key错误、Token过期检查请求头里的Authorization重新生成API Key确认Key没被误加空格403 Forbidden反爬拦截、IP被封先换UA再检查Cookie最后考虑换IP但不要以绕过封禁为目的404 Not FoundURL或路径写错打开浏览器手动访问一下确认资源还存在429 Too Many Requests请求频率过快增加time.sleep()配合指数退避策略重试500 Internal Server Error服务器程序异常先等待几分钟重试大概率是对方临时故障502 Bad Gateway服务器网关错误同上重试是主要手段有个细节很多人不知道用resp.raise_for_status()可以把非200的状态码直接变成异常抛出来这样你写代码时就不用每个请求都手写if resp.status_code 200了try: resp requests.get(https://httpbin.org/status/404) resp.raise_for_status() except requests.exceptions.HTTPError as e: print(fHTTP错误: {e})4.2 排查请求被拒绝的通用思路遇到一个网站怎么请求都返回403或者说访问异常我建议按下面这个顺序排查第一步用浏览器打开目标URL确认是真人能打开的页面不是已经失效或需要特殊权限的。第二步在浏览器F12的Network里找到这个真实请求完整复制请求头改用这个headers再试。第三步看看页面数据是直接返回在HTML里还是通过XHR异步加载的。如果是异步加载你直接用Requests请求页面URL是拿不到数据的得去找真正返回数据那个接口的URL。另外强烈建议你学会浏览器里的一项操作在Network面板里右键某个请求选择Copy as cURL然后把复制出来的内容粘贴到命令行里跑一下能成功说明请求构造无误。你甚至可以用一些在线工具把cURL命令直接转成Python Requests代码作为初始模板再改成符合项目需求的脚本。4.3 数据返回为空或格式变化的处理我在调第三方API时碰到过一个非常典型的坑接口文档里写返回字段是data今天跑得好好的明天突然变成result了程序直接KeyError崩溃。API方更新文档甚至改变字段名称不说100%会发生但概率绝对不低。所以处理返回数据时我习惯用字典的get而不是直接取下标data resp.json() items data.get(data, []) # 取不到就返回默认值不直接data[data]还有一点判断一个请求返回是不是正常不能只靠状态码。有些接口即使返回200code字段也可能是错误码业务逻辑层面得看这个字段json_data resp.json() if json_data.get(code) 0: # 业务成功继续处理 else: # 业务失败打印错误信息4.4 别忘了合规爬虫与API调用的边界意识工具本身没有对错但用在哪里是有边界的。我自己做爬虫项目前至少会确认三件事目标网站是否明令禁止爬取、数据是否涉及个人隐私或版权保护、我的请求频率是否会影响对方服务器的正常运行。很多平台现在都提供了官方API比如各类开放平台能走官方API就走官方API既稳定又省心。遇到验证码、滑块等反爬机制时我的建议不是研究怎么绕过而是停下来想一想这个数据是不是有更合规的获取渠道。这些机制的背后本质上是在提醒你这个数据所有者不希望被程序批量采集。5. 个人实操体会与下一步拓展方向写了这么多最后聊几句真心话。Requests库本身非常容易上手但它最大的价值不是让你学会发请求而是帮你建立起网络请求的直觉——看到任何网页或者接口你能快速判断它是怎么构造的、数据从哪来的、有没有分页、返回结构是怎样的。这套直觉在你后面学Scrapy、异步请求、API封装时全部用得上。一个小习惯分享给你在本地建一个纯文件把常用的请求模板放进去每次接到新的爬虫或API任务直接复制再改不用每次从零写。我的模板长这样import requests import time from requests.exceptions import RequestException session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ..., Accept: application/json }) def get_json(url, paramsNone, retries3): for i in range(retries): try: resp session.get(url, paramsparams, timeout(3, 5)) resp.raise_for_status() return resp.json() except RequestException as e: print(f第{i1}次请求失败: {e}) time.sleep(1.5 * (i 1)) return None你不需要写得多花哨能用、能复用、能快速在项目里调起来就够了。Requests是敲门砖敲开门之后的路越走越宽。
返回列表