写在前面:别怕,这其实很简单
我第一次写代码去抓取网页数据的时候,满屏的红字报错差点让我把键盘砸了。ConnectionError、Timeout、HTTPError……这一堆英文单词像天书一样。但后来我发现,HTTP请求其实就像你去隔壁邻居家借糖:你得先找到他家(建立连接),敲敲门(发送请求),等主人开门回应(接收响应),最后拿到糖(解析数据)或者被拒绝(报错)。
今天我们就用大白话,配合真实代码,把这个过程讲清楚。不管你是刚学编程的小白,还是想回顾基础的老手,这篇文章都会让你豁然开朗。
一、核心工具:requests库 —— Python里的“万能钥匙”
虽然Python标准库里有urllib,但说实话,用它写代码就像用手搓洗衣粉洗衣服——能洗干净,但累得半死。业界公认的首选是第三方库requests。它优雅、简单,把复杂的HTTP细节都藏起来了。
先安装(如果你还没装的话):
pip install requests
二、GET请求:最基础的“问路”
GET请求是你从服务器“拿”数据。比如查天气、搜新闻,都不需要提交敏感信息,用GET最合适。
1. 最简单的GET请求
import requests
# 目标网址:使用httpbin.org做测试,它专门用来演示HTTP行为
url = 'https://httpbin.org/get'
# 发起请求
response = requests.get(url)
# 查看状态码(200表示成功)
print(f'状态码: {response.status_code}')
# 查看返回的JSON数据
print(response.json())
运行结果可能长这样:
状态码: 200
{'args': {}, 'headers': {'Host': 'httpbin.org', ...}, 'origin': '123.45.67.89', 'url': 'https://httpbin.org/get'}
你看,httpbin.org/get返回了你请求的完整信息,包括你的IP地址、请求头等等。这对调试超级有用!
2. 带参数的GET请求
现实中,你搜索“Python教程”时,网址里会有?keyword=Python这样的参数。在requests里,我们用params字典来传:
import requests
# 模拟百度搜索
url = 'https://httpbin.org/get'
params = {
'keyword': 'Python HTTP requests',
'page': 1,
'limit': 10
}
response = requests.get(url, params=params)
# 注意:requests会自动把params拼接到URL里
print(f'实际请求的URL: {response.url}')
# 输出: 实际请求的URL: https://httpbin.org/get?keyword=Python+HTTP+requests&page=1&limit=10
print(response.json()['args'])
# 输出: {'keyword': 'Python HTTP requests', 'page': '1', 'limit': '10'}
重点来了:很多人会手动拼接URL字符串,比如url + '?keyword=' + keyword,这样很容易出错(比如忘记编码特殊字符)。用params参数,requests会自动帮你做URL编码,安全又省事。
三、POST请求:往服务器“送东西”
POST用于提交数据,比如登录、发评论、上传文件。数据不会出现在URL里,更安全。
1. 提交表单数据
import requests
url = 'https://httpbin.org/post'
# 表单数据
data = {
'username': 'zhangsan',
'password': '123456',
'remember_me': True
}
response = requests.post(url, data=data)
print(f'状态码: {response.status_code}')
result = response.json()
print(f'收到的数据: {result["json"]}')
运行结果:
状态码: 200
收到的数据: {'username': 'zhangsan', 'password': '123456', 'remember_me': true}
你看,httpbin.org/post把我们发的数据原样返回了,还包在json字段里。这在调试时非常方便——你能清楚看到服务器收到了什么。
2. 提交JSON数据(现代API的主流)
现在大多数API都用JSON格式传输数据。用json参数,requests会自动序列化成JSON并设置正确的Content-Type头:
import requests
url = 'https://httpbin.org/post'
# JSON数据(注意:直接传字典,requests会自动处理)
json_data = {
'title': '如何优雅地学习Python',
'author': '小明',
'tags': ['Python', 'HTTP', '入门'],
'views': 10000
}
response = requests.post(url, json=json_data)
print(f'状态码: {response.status_code}')
result = response.json()
# 查看服务器收到的原始JSON
print(f'服务器收到的JSON: {result["json"]}')
# 对比:看看Content-Type是否正确设置
print(f'请求头Content-Type: {result["headers"]["Content-Type"]}')
输出:
状态码: 200
服务器收到的JSON: {'title': '如何优雅地学习Python', 'author': '小明', 'tags': ['Python', 'HTTP', '入门'], 'views': 10000}
请求头Content-Type: application/json
为什么用json=而不是data=?
data=发送的是表单编码(application/x-www-form-urlencoded),适合传统表单json=发送的是JSON编码(application/json),适合现代REST API- 用错会导致服务器解析失败,比如有些API严格要求JSON格式,你传了表单数据它就报400错误
3. 上传文件
import requests
url = 'https://httpbin.org/post'
# 上传一个图片文件
with open('photo.jpg', 'rb') as f:
files = {'file': ('photo.jpg', f, 'image/jpeg')}
response = requests.post(url, files=files)
result = response.json()
print(f'文件名: {result["files"]["file"][0]}')
print(f'文件类型: {result["headers"]["Content-Type"]}')
这里有个小细节:files字典的值是一个三元组(filename, file_object, content_type)。你可以只传文件名和文件对象,但显式指定content_type更规范。
四、常见报错详解:遇到红字别慌,逐一击破
这是我最想重点讲的部分。报错不是敌人,它是你的朋友——它在告诉你哪里出了问题。
报错1:ConnectionError —— 连都连不上
import requests
try:
response = requests.get('https://this-domain-definitely-does-not-exist-12345.com', timeout=5)
except requests.exceptions.ConnectionError as e:
print(f'连接失败: {e}')
可能原因:
- 网址写错了:拼写错误、多了斜杠、协议写错(http写成https)
- 网络不通:你家断网了,或者目标服务器挂了
- DNS解析失败:域名无法解析成IP地址
解决方案:
import requests
# 方案A:检查网址
url = 'https://httpbin.org/get'
print(url) # 手动核对一下
# 方案B:给请求加超时,避免一直卡死
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # 如果状态码是4xx或5xx,抛出异常
except requests.exceptions.ConnectionError:
print('网络连接失败,请检查网络或网址')
except requests.exceptions.Timeout:
print('请求超时,服务器响应太慢')
except requests.exceptions.HTTPError as e:
print(f'HTTP错误: {e}')
实战技巧:遇到连接失败,先用浏览器打开网址试试。如果浏览器能打开,说明是代码或网络问题;如果浏览器也打不开,那是服务器或你的网络问题。
报错2:Timeout —— 请求太慢了
import requests
try:
# 故意连接一个很慢的服务器
response = requests.get('http://httpbin.org/delay/10', timeout=3)
except requests.exceptions.Timeout:
print('请求超时!服务器3秒内没响应')
为什么需要超时? 想象一下,你给一个朋友打电话,对方一直不接。你不会一直举着电话等一整天吧?你等30秒不接就挂断了。HTTP请求也一样,设置超时防止程序永远卡住。
最佳实践:合理设置超时
# 推荐写法:同时设置connect_timeout和read_timeout
response = requests.get(
'https://api.github.com/users/python',
timeout=(3.05, 10) # 3秒建立连接,10秒等待响应
)
- 第一个值(3.05):建立连接的超时时间
- 第二个值(10):等待响应的超时时间
经验之谈:我见过很多人只设一个超时值,比如timeout=10。这在简单场景没问题,但如果网络很慢,连接可能要5秒,剩下只有5秒收数据,很容易误判超时。分开设置更安全。
报错3:HTTPError —— 服务器拒绝了请求
import requests
try:
# 404表示页面不存在
response = requests.get('https://httpbin.org/status/404')
response.raise_for_status() # 这行会抛出HTTPError
except requests.exceptions.HTTPError as e:
print(f'HTTP错误: {e}')
# 输出: HTTP错误: 404 Client Error: Not Found for url: https://httpbin.org/status/404
raise_for_status()是什么?
默认情况下,即使服务器返回404或500,requests也不会报错,它只会把状态码存起来。调用raise_for_status()才会主动抛出异常。
常见状态码及含义:
# 2xx:成功
200 OK - 一切正常
201 Created - 资源创建成功(POST后常用)
# 3xx:重定向
301 Moved Permanently - 永久重定向
302 Found - 临时重定向
# 4xx:客户端错误(你错了)
400 Bad Request - 请求格式错误
401 Unauthorized - 未授权(需要登录)
403 Forbidden - 禁止访问(权限不足)
404 Not Found - 资源不存在
429 Too Many Requests - 请求太频繁(被限流了)
# 5xx:服务器错误(服务器错了)
500 Internal Server Error - 服务器内部错误
502 Bad Gateway - 网关错误
503 Service Unavailable - 服务暂时不可用
实战:处理429限流
import requests
import time
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
response = requests.get(url)
if response.status_code == 429:
# 服务器说“慢点”,我们等一会再试
wait_time = int(response.headers.get('Retry-After', 5))
print(f'请求太频繁,等待{wait_time}秒...')
time.sleep(wait_time)
continue
response.raise_for_status()
return response
except requests.exceptions.HTTPError:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # 指数退避:1秒、2秒、4秒...
return None
这个模式叫“指数退避”,是处理限流的经典做法。第一次等1秒,第二次等2秒,第三次等4秒。比傻等更智能。
报错4:TooManyRedirects —— 重定向死循环
import requests
try:
# 故意制造重定向循环
response = requests.get('http://httpbin.org/redirect/5', allow_redirects=False)
except requests.exceptions.TooManyRedirects:
print('重定向次数过多,可能陷入了死循环')
发生了什么?
服务器A说“去B那里”,B说“去C那里”……结果它们互相指来指去,永无止境。requests默认最多重定向20次,超过就报这个错。
解决方案:
import requests
# 方案1:限制重定向次数
response = requests.get(url, allow_redirects=False)
# 手动处理重定向
# 方案2:检查重定向是否合理
response = requests.get(url, max_redirects=5) # 限制为5次
五、实战案例:做一个简单的网页爬虫
理论讲完了,我们来写个真正有用的东西。假设我们要从豆瓣电影Top250抓取数据:
import requests
from bs4 import BeautifulSoup
import time
def fetch_douban_top250():
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'
}
all_movies = []
for page in range(1, 11): # 共250部,每页25部,共10页
start = (page - 1) * 25
url = f'https://movie.douban.com/top250?start={start}'
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
items = soup.find_all('div', class_='item')
for item in items:
rank = item.find('em').text
title = item.find('span', class_='title').text
rating = item.find('span', class_='rating_num').text
movie = {
'rank': rank,
'title': title,
'rating': rating
}
all_movies.append(movie)
print(f'已抓取第{page}页,共{len(all_movies)}部电影')
time.sleep(1) # 礼貌地等1秒,避免给服务器太大压力
except requests.exceptions.RequestException as e:
print(f'抓取第{page}页失败: {e}')
continue
return all_movies
# 运行爬虫
movies = fetch_douban_top250()
print(f'\n抓取完成!共{len(movies)}部电影')
print('前5部:')
for m in movies[:5]:
print(f" {m['rank']}. {m['title']} - 评分: {m['rating']}")
关键点解析:
- 设置User-Agent:很多网站会拦截没有User-Agent的请求,假装你是浏览器
- 异常处理:某页失败不影响其他页,用
continue跳过 - 礼貌爬取:
time.sleep(1)避免请求太快被封IP - BeautifulSoup解析:专门用来从HTML中提取数据的库,比正则表达式更强大
六、进阶技巧:Session —— 保持登录状态
想象一下,你去超市购物,每次拿一件商品都要重新排队验证会员身份,是不是很烦?requests.Session就像你的会员卡,一次性验证,全程有效。
import requests
# 创建会话对象
session = requests.Session()
# 设置公共请求头(所有请求都会带这个头)
session.headers.update({
'User-Agent': 'MyCoolApp/1.0',
'Accept-Language': 'zh-CN,zh;q=0.9'
})
# 第一步:登录
login_url = 'https://httpbin.org/post'
login_data = {'username': 'user', 'password': 'pass'}
session.post(login_url, data=login_data)
# 第二步:访问需要登录的页面(session会自动带上cookie)
protected_url = 'https://httpbin.org/headers'
response = session.get(protected_url)
print(response.json())
什么时候用Session?
- 需要登录的网站(自动管理cookie)
- 多次请求同一个服务器(TCP连接复用,更快)
- 需要共享请求头或参数
七、异步请求:当速度成为关键
如果你的项目需要同时请求100个网址,串行执行会很慢。用aiohttp可以并行处理:
import asyncio
import aiohttp
async def fetch_url(session, url):
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 200:
data = await resp.json()
return {'url': url, 'status': 200, 'data': data}
else:
return {'url': url, 'status': resp.status}
except Exception as e:
return {'url': url, 'error': str(e)}
async def main():
urls = [
'https://httpbin.org/json',
'https://httpbin.org/uuid',
'https://httpbin.org/get',
'https://httpbin.org/delay/1',
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for r in results:
print(r)
# 运行异步爬虫
asyncio.run(main())
对比:
- 同步:100个请求,每个
