一、requests 库简介
requests 库是一个第三方HTTP客户端库,是对Python内置的urllib库的封装。遵循 Apache2.0 开源协议,支持 HTTP/1.1 和 HTTP/2,用于发送各类HTTP请求,核心优势是 “让 HTTP 请求变得更简单”。
无需手动构造复杂的请求头、处理URL编码、管理cookie等,开发者可用极少的代码实现各类HTTP交互场景,相比Python内置的urllib库,它更简洁、易用、功能更丰富,被广泛应用于接口测试、网络爬虫、数据采集等场景。
# 安装requests库
python3 -m pip install requests
二、requests 库核心特点
1、语法简洁直观,实现相同功能的代码量少于urllib
2、原生支持HTTP/1.1,涵盖GET、POST、PUT、DELETE 等所有HTTP标准请求方法
3、自动处理URL编码、Cookie持久化、响应内容解析(JSON/文本/二进制)
4、支持请求头自定义、文件上传、超时设置、代理配置等高级功能
5、内置异常处理机制,便于捕获和处理网络请求中的各类错误
6、支持会话保持(Session),可维持跨请求的上下文状态
三、requests库的方法和参数
# 通用参数:
* method: 请求方法
- 作用: 指定HTTP请求的方式,对应HTTP协议的请求方法
- 取值: 字符串类型,支持'GET'、'POST'、'PUT'、'DELETE'、'HEAD'、'OPTIONS'、'PATCH'
- 在request()中需'显式指定',而get()、post()等方法已默认绑定对应method,无需手动传入
* url: 请求地址
- 作用: 指定要访问的目标资源的URL地址,是所有请求的必选参数
- 取值: 字符串类型,要请求的HTTP/HTTPS协议的目标URL地址(如https://www.baidu.com)
* params: URL查询参数,适用于GET请求传递参数
- 作用: 用于自动拼接在URL末尾的查询字符串(键值对形式),自动进行URL编码
- 取值: 字典、列表、元组或字节类型,默认None
* data: 请求体表单数据,适用于POST/PUT等
- 作用: 传递表单格式(非JSON格式)的请求体数据,对应HTML表单提交的数据格式
- Content-Type:application/x-www-form-urlencoded
- 取值: 字典、列表、元组、字节或文件对象,默认 None
* json: JSON格式请求体,适用于POST/PUT等
- 作用: 传递JSON格式的请求体数据,requests会"自动将字典序列化为JSON字符串",并设置请求头Content-Type: application/json
- 取值: 可序列化的Python字典(或其他JSON可序列化对象),默认None
- 说明: 优先级高于data,若同时传入data和json,json会覆盖data的作用
* headers: 请求头信息
- 作用: 自定义HTTP请求头,用于模拟浏览器、传递认证信息、指定数据格式等
- 取值: 字典类型,默认None
- 常用字段: User-Agent(模拟浏览器)、Authorization(身份令牌)、Referer(来源页)等
* auth: HTTP身份认证信息
- 作用: 用于HTTP基础认证(Basic Auth)或摘要认证(Digest Auth),自动处理认证信息的编码和传递
- 取值: 元组(用户名, 密码)或requests.auth模块下的认证对象(如HTTPBasicAuth),默认None
* cookies: Cookies传递
- 作用: 向服务器传递Cookies信息,用于维持会话状态(如登录后保持登录状态)
- 取值: 字典类型或 requests.cookies.RequestsCookieJar对象,默认None
* timeout: 请求超时时间
- 作用: 指定请求的最大等待时间,包括连接时间和读取响应时间
- 超时后抛出requests.exceptions.Timeout异常,可通过try/except捕获,便于程序容错处理
- 取值:
- 浮点数/整数: 表示总超时时间(秒),如3.5表示3.5秒内未完成连接或读取则超时
- 元组: (连接超时时间, 读取超时时间),如(3,10)表示3秒内未连接成功超时,10秒内未读取到响应超时
- 默认: None,无限等待,不推荐在生产环境使用
* allow_redirects: 是否允许自动重定向
- 作用: 控制是否自动跟随HTTP重定向响应(状态码: 3xx,如301、302)
- 取值: True/False,默认True,允许自动重定向
* proxies: 代理配置
- 作用: 指定HTTP/HTTPS/SOCKS代理服务器,用于隐藏客户端IP、突破访问限制等
- 取值: dict 类型,格式{'http': '代理地址', 'https': '代理地址'},默认None
* verify: SSL证书验证
- 作用: 控制是否验证目标服务器的SSL证书有效性,针对HTTPS请求
- 取值:
- 字符串: 指定本地SSL证书文件的路径(如.pem格式文件),用于自定义证书验证
- 布尔类型:
- True: 默认,严格验证SSL证书,证书无效则抛出SSLError异常
- False: 跳过SSL证书验证,不推荐生产环境使用
* cert: 客户端SSL证书
- 作用: 当服务器要求客户端提供SSL证书进行身份验证时,指定客户端证书文件
- 取值:
- 字符串: 客户端证书文件(.pem 格式)的路径
- 元组: (证书文件路径, 私钥文件路径),用于分离证书和私钥的场景
- 默认: None
* files: 文件上传
- 作用: 用于向服务器上传文件,支持单个或多个文件上传
- Content-Type: multipart/form-data
- 取值: 字典类型,键为表单字段名,值为文件元组,默认None
- {"表单字段名": (文件名, 文件对象, 文件类型, 额外头信息)},后两个参数可选
- {"file": ("test.txt", open("test.txt", "rb"), "text/plain")}
* stream: 流式响应
- 作用: 控制是否以流式方式接收响应内容,适用于下载大文件(避免一次性加载全部内容到内存)
- 取值: 布尔类型
- False: 默认,一次性将响应内容加载到内存
- True:需通过response.iter_content()逐块读取内容
* hooks: 请求/响应钩子
- 作用: 在请求发送前或响应接收后执行自定义函数,用于日志记录、响应预处理等场景
- 取值: 字典类型,常用格式为{'response': callback_func},值为自定义函数列表,默认None
3.1、GET请求-无参数
url = "https://httpbin.org/get"
resp = requests.get(url)
# 查看响应状态码
print("响应状态码:", resp.status_code)
# 查看文本响应内容(字符串格式,适用于网页、接口返回文本)
print("响应文本:", resp.text)
# 查看响应JSON内容(自动解析JSON格式,返回字典/列表,适用于接口返回 JSON)
# 仅当响应内容是合法JSON时可用,否则会抛出JSONDecodeError
print("响应 JSON 解析:", resp.json())
# 查看响应二进制内容(适用于下载图片、视频、文件等二进制资源)
print("响应二进制内容长度:", len(resp.content))
3.2、GET请求-带参数
通过params参数传递键值对,requests 会自动将其拼接为 URL 查询字符串(无需手动处理 URL 编码)
# 定义URL参数
params = {
"page": 1,
"limit": 10,
"keyword": "python requests"
}
# 发送带参数的GET请求
response = requests.get(
url="https://example.com/api/data",
params=params # 自动拼接为: ?page=1&limit=10&keyword=python+requests
)
# 打印最终请求的URL
print("最终请求URL:", response.url)
3.3、POST请求-提交表单数据
使用data参数传递普通键值对,对应HTML表单提交form-data格式,
自动设置请求头:Content-Type: application/x-www-form-urlencoded
# 定义表单数据
form_data = {
"username": "test_user",
"password": "test_pass123"
}
# 发送POST请求(表单提交)
response = requests.post(
url="https://example.com/api/login",
data=form_data
)
print("登录响应:", response.text)
3.4、POST请求-提交JSON数据
使用json参数传递字典,requests会自动将字典序列化为JSON字符串,并设置请求头Content-Type: application/json、
# 定义JSON数据
json_data = {
'title': '测试文章',
'content': '这是测试文章'
}
# 发送POST请求
response = requests.post(
url="https://httpbin.org/post",
json=json_data, # 自动序列化+设置Content-Type
headers={'User-Agent': 'Python-Requests'},
timeout=5
)
print("文章提交响应:", response.json()) # 直接解析JSON响应
3.5、设置请求头
网站/接口会验证请求头,如User-Agent模拟浏览器、Authorization身份验证等,通过headers参数传递字典格式的请求头
# 定义自定义请求头
headers = {
# User-Agent:模拟Chrome浏览器
"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",
# Authorization:Token 认证(部分接口需要登录后携带 Token 访问)
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# 声明请求数据格式为 JSON
"Content-Type": "application/json"
}
# 发送请求时传入 headers 参数
response = requests.get("https://httpbin.org/headers", headers=headers)
# 查看响应,验证请求头是否被正确携带
print("响应 JSON:", response.json())
3.6、Cookie 管理
Cookie用于维持用户会话状态(如登录态),requests 支持两种 Cookie 处理方式:手动传入 Cookie、自动保持 Cookie(Session)
# 两个关键概念的差异:
1. 传入的cookies参数: 是客户端(你)发给服务器的Cookie,相当于"你告诉服务器你的身份凭证"
2. response.cookies: 是服务器返回给客户端的 Cookie,相当于"服务器回应你的凭证(可能新增/修改Cookie)"
# 在对接真实业务接口时:
1. 无需打印response.cookies,只要登录请求返回200/201等成功状态码,session 就会自动保存Cookie;
2. 若后续请求提示 “未登录”,优先检查:
是否用了同一个session发送所有请求
登录接口的响应头是否有Set-Cookie,可通过print(login_resp.headers)查看
Cookie的Domain/Path是否匹配(比如xxx.com 的Cookie不能带到yyy.com)。
# 总结:
1. login_resp.cookies为空是解析层面的小问题,不代表session没保存 Cookie,核心看session.cookies;
2. 真实开发中,session.cookies才是决定后续请求是否携带Cookie的关键,无需关注单次响应的response.cookies;
3. 只要用同一个session发送登录和后续请求,即使login_resp.cookies为空,Cookie也会被正确携带
3.6.1、携带Cookie发送请求
# 定义 Cookie 数据(字典格式)
cookies = {
"session_id": "123456abcdef",
"user_id": "789"
}
# 传入 cookies 参数发送请求
response = requests.get("https://httpbin.org/cookies", cookies=cookies)
"""
# 遇到的报错(大概率是KeyError: 'user_id'),本质是:
1. https://httpbin.org/cookies接口只会返回你发送的Cookie内容,在响应体里
2. 但不会主动把这些Cookie再回写给客户端(即不会在响应头里设置 Cookie);
因此response.cookies是空的,自然取不到 user_id 这个键
"""
# 查看响应,验证 Cookie 是否被正确携带
print("响应 JSON:", response.json())
print(response.cookies)
print(response.cookies['session_id'])
3.6.2、获取响应中的Cookie
response = requests.get("https://example.com/login")
# 获取Cookie字典
cookie_dict = requests.utils.dict_from_cookiejar(response.cookies)
print("Session ID:", cookie_dict.get("session_id"))
3.7、超时设置
为了避免请求无限等待(如服务器无响应),可通过timeout参数设置超时时间(单位:秒),超时会抛出Timeout异常
try:
# 设置超时时间:总超时 5 秒
# response = requests.get("https://httpbin.org/get", timeout=5)
# 连接超时1秒,读取超时3秒(总超时4秒)
response = requests.get(
url="https://example.com/api/data",
timeout=(1, 3)
)
except requests.exceptions.Timeout:
print("请求超时,请稍后重试")
3.8、Session会话保持
当需要连续发送多个请求,如登录后访问其他接口,使用requests.Session()可以保持会话状态(自动携带 Cookie、请求头等),无需手动传递

| 方法 | 作用 | 示例 |
|---|---|---|
| session.cookies.get(key) | 从 Cookie中获取指定键的 Cookie 值 | session.cookies.get(“token”) |
| session.cookies.update(dict) | 手动添加/更新 Cookie 到 Cookie | session.cookies.update({“token”: “zbc”}) |
| session.cookies.clear() | 清空 Cookie(退出登录) | session.cookies.clear() |
| response.cookies | 单次请求返回的 Cookie(临时) | login_resp.cookies.get(“token”) |
# 创建会话
session = requests.Session()
# 设置默认参数
session.headers.update({'User-Agent': 'my-app/1.0.0'})
session.auth = ('username', 'password')
session.proxies.update({'http': 'proxy.example.com:8080'})
# 会话保持 cookies 和其他参数
session.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
response = session.get('https://httpbin.org/cookies') # 包含之前的 cookies
# 会话适配器(自定义连接池等)
from requests.adapters import HTTPAdapter
adapter = HTTPAdapter(pool_connections=100, pool_maxsize=100)
session.mount('https://', adapter)
import requests
# 1. 初始化 Session
session = requests.Session()
print("初始 Cookie:", dict(session.cookies)) # 输出:{}
# 场景1:手动注入 Cookie 到 Session(模拟已有登录凭证)
manual_cookies = {
"user_id": "123456",
"token": "abc123xyz"
}
session.cookies.update(manual_cookies) # 把 Cookie 放进session里
print("注入后Cookie:", dict(session.cookies)) # 输出:{'user_id': '123456', 'token': 'abc123xyz'}
# 场景2:发送请求,Session 自动携带 Cookie
# httpbin.org/cookies 会返回请求携带的 Cookie,验证是否携带成功
resp1 = session.get("https://httpbin.org/cookies")
print("请求携带的Cookie:", resp1.json()) # 输出:{'cookies': {'user_id': '123456', 'token': 'abc123xyz'}}
# 场景3:服务器返回新Cookie,Session自动保存
# httpbin.org/cookies/set 会给客户端设置新 Cookie(overwrite_token=789)
resp2 = session.get("https://httpbin.org/cookies/set?overwrite_token=789")
print("服务器返回新 Cookie 后,Cookie:", dict(session.cookies))
# 输出:{'user_id': '123456', 'token': 'abc123xyz', 'overwrite_token': '789'}
# 场景4:清空 Cookie(模拟退出登录)
session.cookies.clear()
print("清空后 Cookie:", dict(session.cookies)) # 输出:{}
resp3 = session.get("https://httpbin.org/cookies")
print("清空后请求携带的 Cookie:", resp3.json()) # 输出:{'cookies': {}}
session = requests.Session()
# 1. 发送登录请求
login_resp = session.get("https://httpbin.org/cookies/set?token=real_token_123")
# 2. 如果 session 中没有 Cookie,手动注入(仅测试用,真实接口无需)
if not session.cookies.get("token"):
session.cookies.update({"token": "real_token_123"})
print("session 最终的 Cookie:", dict(session.cookies))
# 3. 后续请求
profile_resp = session.get("https://httpbin.org/cookies")
print("后续请求携带的 Cookie:", profile_resp.json())
3.8.1、重试机制
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
response = session.get("https://api.example.com")
3.8.2、实际应用示例
import requests
import json
class APIClient:
def __init__(self, base_url, token=None):
self.base_url = base_url
self.session = requests.Session()
if token:
self.session.headers.update({'Authorization': f'Bearer {token}'})
def get_users(self):
response = self.session.get(f'{self.base_url}/users')
response.raise_for_status()
return response.json()
def create_user(self, user_data):
response = self.session.post(
f'{self.base_url}/users',
json=user_data
)
response.raise_for_status()
return response.json()
def upload_file(self, file_path):
with open(file_path, 'rb') as f:
files = {'file': (file_path, f)}
response = self.session.post(
f'{self.base_url}/upload',
files=files
)
response.raise_for_status()
return response.json()
# 使用示例
client = APIClient('https://api.example.com', 'your-token-here')
users = client.get_users()
3.9、忽略SSL证书验证
访问HTTPS网站时,若证书无效(如自签名证书),会抛出SSLError异常,可通过verify=False忽略证书验证
# 忽略SSL证书验证
response = requests.get(
url="https://self-signed.example.com",
verify=False
)
# 屏蔽证书警告
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
3.10、文件上传
使用 files 参数实现文件上传,支持单个/多个文件上传,requests 会自动设置请求头 Content-Type: multipart/form-data
# 定义文件上传参数(字典格式)
# 格式:{"文件字段名": (文件名, 文件对象, 文件类型)}
# 若不指定文件名和文件类型,可简化为 {"文件字段名": 文件对象}
files = {
# 单个文件上传
"avatar": (
"my_avatar.jpg", # 服务器接收到的文件名
open("my_avatar.jpg", "rb"), # 以二进制只读模式打开文件
"image/jpeg" # 文件MIME类型
),
# 可同时上传多个文件
"attachment": ("document.txt", open("document.txt", "rb"), "text/plain")
}
# 传入 files 参数发送 POST 请求
response = requests.post("https://httpbin.org/post", files=files)
# 查看响应,验证文件是否被正确上传
print("响应 JSON:", response.json())
# 注意:文件对象使用后需关闭,或使用 with 语句自动关闭
# 推荐写法(自动关闭文件):
with open("my_avatar.jpg", "rb") as f1, open("document.txt", "rb") as f2:
files = {
"avatar": ("my_avatar.jpg", f1, "image/jpeg"),
"attachment": ("document.txt", f2, "text/plain")
}
response = requests.post("https://httpbin.org/post", files=files)
# 多文件上传第二种
files = [
('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))
]
response = requests.post('https://httpbin.org/post', files=files)
3.11、代理设置和客户端证书
proxies = {
"http": "http://127.0.0.1:8080", # http 协议代理
"https": "https://127.0.0.1:8081", # https 协议代理
"socks5": "socks5://127.0.0.1:1080" # socks5 代理(需安装 requests[socks] 依赖)
}
response = requests.get(url="https://api.example.com", proxies=proxies)
# 单个证书文件
response = requests.get(url="https://cert-auth.example.com", cert="/path/to/client.pem")
# 证书+私钥分离
response = requests.get(url="https://cert-auth.example.com", cert=("/path/to/cert.pem", "/path/to/key.pem"))
3.12、请求/响应钩子
# 自定义响应钩子:打印响应状态码
def print_status(response, *args, **kwargs):
print(f"响应状态码:{response.status_code}")
response = requests.get(url="https://api.example.com", hooks={"response": [print_status]})
def print_url(r, *args, **kwargs):
print(r.url)
def check_status(r, *args, **kwargs):
r.raise_for_status()
hooks = {'response': [print_url, check_status]}
response = requests.get('https://api.example.com', hooks=hooks)
四、response 对象
发送请求后,会返回一个Response对象,该对象包含了服务器返回的所有信息
# Response对象:
* status_code: HTTP响应状态码
- 作用: 获取服务器返回的HTTP状态码,用于判断请求的执行结果(成功/失败/重定向等)
- 常用状态码:
- 200: 请求成功--OK
- 301: 永久重定向
- 302: 临时重定向
- 400: 请求参数错误--Bad Request
- 401: 未授权--Unauthorized
- 403: 禁止访问--Forbidden
- 404: 资源不存在--Not Found
- 500: 服务器内部错误--Internal Server Error
* reason: 响应状态描述
- 作用: 获取与HTTP状态码对应的文字描述信息
* ok: 请求是否成功标识
- 作用: 快速判断请求是否成功,本质是判断状态码是否在200~299范围内
- 取值:
- True: 状态码200-299,请求成功
- False: 状态码非200-299,请求失败或重定向
* headers: 响应头字典
- 作用: 获取服务器返回的HTTP响应头信息,包含内容类型、服务器信息、缓存策略等
- 常用响应头字段:
- Content-Type: 响应内容的类型(如text/html; charset=utf-8、application/json)
- Server: 服务器软件名称
- Content-Length: 响应内容的长度(字节数)
* url: 最终请求的URL
- 作用: 获取请求最终实际访问的URL,若存在自动重定向,该URL与原始请求URL不一致
* encoding: 响应内容编码格式
- 作用: 获取或设置响应内容的编码格式,用于正确解析响应文本
- 手动设置: 可直接给encoding赋值,修改解析编码(解决乱码核心方案)
* elapsed: 请求耗时
- 作用: 获取从请求发送到接收完响应头的总耗时,不包括读取响应内容的时间
- 取值: datetime.timedelta类型,可通过total_seconds()转换为秒数
* cookies: 响应中的Cookie信息
- 作用: 获取服务器返回的Cookie信息,封装为RequestsCookieJar对象
- 类似字典,支持字典的常用操作,用于维持会话状态。如: 后续请求携带该Cookie保持登录
* text: 文本格式响应内容
- 作用: 以字符串"str"格式获取响应内容,适用于文本类数据。如: HTML页面、JSON字符串、普通文本等
- 底层逻辑: requests会根据encoding属性的编码格式,将响应的二进制数据(bytes)解码为字符串,若编码设置错误会导致中文乱码
* content: 二进制格式响应内容
- 作用: 以二进制字节流(bytes)格式获取响应内容,适用于非文本类数据。如: 图片、音频、视频、压缩包等二进制文件
- 特点: 不经过编码转换,直接返回服务器返回的原始数据,是下载文件的核心属性
* json(): JSON格式响应内容
- 作用: 将JSON格式的响应内容自动反序列化为Python对象(字典/列表),适用于接口返回的JSON数据
- 底层逻辑: 先读取content二进制数据,再使用json模块进行反序列化,自动处理编码问题
- 异常: 若响应内容不是合法的JSON格式,调用该方法会抛出requests.exceptions.JSONDecodeError 异常
* iter_content(): 逐块读取二进制内容
- 作用: 以"迭代器"形式"逐块读取"响应的二进制内容,适用于下载大文件
- 避免一次性将全部内容加载到内存,导致内存溢出
- 配合使用: 需在requests.get()中指定"stream=True"(开启流式响应)
- 参数: chunk_size(每次读取的字节数,默认1),建议设置为1024的整数倍(如: 1024*1024=1MB)
* iter_lines(): 逐行读取文本内容
- 作用: 以"迭代器"形式"逐行读取"响应的文本内容,适用于处理大文本文件(如日志文件、CSV 文件等)
- 配合使用: 需指定"stream=True",自动按行分割内容,无需一次性加载全部文本
* raise_for_status(): 异常抛出方法
- 作用: 若请求失败(状态码非200-299),自动抛出requests.exceptions.HTTPError异常,便于统一捕获请求错误
- 场景: 替代手动判断status_code或ok,简化异常处理逻辑
4.1、使用 Response 对象
response = requests.get("https://api.github.com/users/octocat")
# 1. 状态码判断
if response.status_code == 200:
print("请求成功")
else:
print(f"请求失败,状态码:{response.status_code}")
# 2. 解析JSON响应(接口返回常用)
user_info = response.json()
print("用户名:", user_info["login"])
print("用户头像:", user_info["avatar_url"])
# 3. 二进制内容(下载图片示例)
avatar_response = requests.get(user_info["avatar_url"])
if avatar_response.status_code == 200:
with open("octocat_avatar.jpg", "wb") as f:
f.write(avatar_response.content) # 二进制写入文件
print("头像下载完成")
# 4. 异常处理(raise_for_status)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP请求错误:{e}")
4.2、逐块读取二进制内容
# 开启流式响应,避免内存溢出
large_file_response = requests.get("https://example.com/large_file.zip", stream=True)
if large_file_response.ok:
with open("large_file.zip", "wb") as f:
# 每次读取1MB内容
for chunk in large_file_response.iter_content(chunk_size=1024*1024):
if chunk: # 过滤空块
f.write(chunk)
print("大文件下载完成!")
4.3、逐行读取文本内容
log_response = requests.get("https://example.com/server.log", stream=True)
if log_response.ok:
# 逐行读取日志内容
for line_num, line in enumerate(log_response.iter_lines(), 1):
if line: # 过滤空行
line_text = line.decode("utf-8") # 字节转字符串
print(f"第{line_num}行:{line_text}")
五、异常处理
requests 库定义了多种异常类型,便于捕获不同场景的错误,核心异常如下:
| 异常类型 | 说明 |
|---|---|
| requests.exceptions.RequestException | 所有 requests 异常的基类,可捕获所有 requests 相关错误 |
| requests.exceptions.HTTPError | HTTP 错误(状态码 4xx/5xx),由raise_for_status()触发 |
| requests.exceptions.Timeout | 请求超时异常 |
| requests.exceptions.ConnectionError | 连接错误(如网络断开、服务器不可达) |
| requests.exceptions.SSLError | SSL 证书验证失败异常 |
| requests.exceptions.JSONDecodeError | JSON 解析错误(响应内容非合法 JSON) |
5.1、完整异常处理
try:
response = requests.get(
url="https://example.com/api/data",
params={"page": 1},
timeout=(1, 3),
headers={"User-Agent": "Python-Requests/2.31.0"}
)
response.raise_for_status() # 触发HTTP错误异常
data = response.json()
print("请求数据成功:", data)
except requests.exceptions.HTTPError as e:
print(f"HTTP错误:{e}")
except requests.exceptions.Timeout as e:
print(f"请求超时:{e}")
except requests.exceptions.ConnectionError as e:
print(f"连接错误:{e}")
except requests.exceptions.JSONDecodeError as e:
print(f"JSON解析失败:{e}")
except requests.exceptions.RequestException as e:
print(f"通用请求错误:{e}")
六、爬虫爬取豆瓣案例
使用正则表达式分析网页数据
编码选择:
写入文件时用 encoding=“utf-8-sig”,而非 utf-8—— 这样 Excel 打开CSV时不会出现中文乱码
6.1、使用requests库
import os
import random
import time
import requests
import re
CSV_TITLE = ["排名, 电影名称, 英文名称, 其他名称, 评分, 评价人数, 导演, 演员, 年份, 地区, 类型, 经典台词\n"]
def request(film_path, start):
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\
(KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"
}
# 随机休眠(分散请求压力,避免被封)
time.sleep(random.uniform(0.5, 1.0))
response = requests.get(f"{film_path}?start={start}", headers=headers)
if response.status_code != 200:
raise RuntimeError(f"Request ailed with status code {response.status_code}")
return response.text
movie_list = []
def get_movie_info(context):
"""
获取每部电影对应的信息
:param context: 页面html信息
:return:
"""
# 1. 第一步:先匹配所有电影的<li>标签,(缩小匹配范围,降低出错概率)
# 匹配每部电影的<li>标签,从<li>开始到</li>结束,非贪婪匹配,匹配最近的</li>
li_pattern = re.compile(r'<li>(.*?)</li>', re.S)
movie_li_list = re.findall(li_pattern, context)
# 2. 第二步:遍历每个<li>容器,提取每部电影信息
for li_html in movie_li_list:
# print(li_html)
movie_info = {}
# 提取排名信息(<em>标签内的数字)
rank_match = re.search(r'<em>(\d+)</em>', li_html)
movie_info["排名"] = rank_match.group(1) if rank_match else ""
# 提取电影名称(第一个<span class="title">)
main_title_match = re.search(r'<span class="title">(.*?)</span>', li_html)
movie_info["电影名称"] = main_title_match.group(1) if main_title_match else ""
# 提取英文名称(第二个<span class="title">)
english_title_match = re.search(r'<span class="title"> / (.*?)</span>', li_html)
movie_info["英文名称"] = english_title_match.group(1) if english_title_match else ""
# 提取其他名称(第一个<span class="other">)
other_title_match = re.search(r'<span class="other"> / (.*?)</span>', li_html)
movie_info["其他名称"] = other_title_match.group(1) if other_title_match else ""
# 提取评分(<span class="rating_num">)
# rating_match = re.search(r'<span class="rating_num" property="v:average">(\d.\d)</span>', li_html)
rating_match = re.search(r'<span class="rating_num" property="v:average">(.*?)</span>', li_html)
# movie_info["评分"] = float(rating_match.group(1)) if rating_match else ""
movie_info["评分"] = rating_match.group(1) if rating_match else ""
# 提取评价人数(匹配"XXX人评价"中的数字)
evaluate_match = re.search(r'<span>(\d+)人评价</span>', li_html)
# movie_info["评价人数"] = int(evaluate_match.group(1)) if evaluate_match else ""
movie_info["评价人数"] = evaluate_match.group(1) if evaluate_match else ""
# 提取导演、主演、年份、地区、类型(最复杂的部分,做容错)
# 先匹配p标签内的所有文本(去除HTML标签)
p_text_match = re.search(r'<p>(.*?)</p>', li_html, re.S)
if p_text_match:
p_text = p_text_match.group(1)
# 清理p文本:去除所有HTML标签、多余空格和换行
p_text = re.sub(r'<.*?>', '', p_text)
p_text = re.sub(r'\s+', ' ', p_text).strip()
# 提取导演(匹配"导演: xxx")
director_match = re.search(r'导演: (.*?) ', p_text)
movie_info["导演"] = director_match.group(1).strip() if director_match else ""
# 提取主演(匹配"主演: xxx",可选)
actor_match = re.search(r'主演: (.*?)(?: \d{4}|$)', p_text)
movie_info["演员"] = actor_match.group(1).strip() if actor_match else ""
# 提取年份(4位数字)
year_match = re.search(r'(\d{4})', p_text)
movie_info["年份"] = year_match.group(1) if year_match else ""
# 提取地区和类型(年份后按"/"拆分)
# 再去除 便于分隔地区和年份
p_text = re.sub(r' ', '', p_text)
area_type_match = re.search(r'\d{4}\s*/\s*(.*?)\s*/\s*(.*?)$', p_text)
if area_type_match:
movie_info["地区"] = area_type_match.group(1).strip() if area_type_match else ""
movie_info["类型"] = area_type_match.group(2).strip() if area_type_match else ""
else:
movie_info["地区"] = ""
movie_info["类型"] = ""
# 提取经典台词(<span class="quote">)
quote_match = re.search(r'<span>(\D+)</span>', li_html, re.S)
movie_info["经典台词"] = quote_match.group(1) if quote_match else ""
movie_list.append(movie_info)
def analysis(info_list):
print("Start Analysing Movie Info...")
every_movie_list = []
if not info_list:
print("movie list is empty, please check!.")
for index, every_movie in enumerate(info_list):
# print(every_movie)
print("正在处理第{index}部电影,电影名称是: {every_movie}".format(index=index + 1, every_movie=every_movie["电影名称"]))
# 1. 新增:每部电影单独用一个临时列表存字段值
temp_movie_list = []
for key, value in every_movie.items():
if isinstance(value, str):
if "其他名称" in key:
# value = value.strip().replace(" ", "")
value = re.sub(r'\s+', '', value)
value = value.strip()
else:
value = str(value)
# 处理值中包含逗号的情况(CSV中逗号会分隔字段,需用双引号包裹)
if "," in value:
value = f'"{value}"'
temp_movie_list.append(value)
# 2. 新增:单部电影的字段值拼接成一行,末尾加换行符
movie_line = ",".join(temp_movie_list) + "\n"
# print(movie_line)
every_movie_list.append(movie_line) # 把带换行的行加入总列表
CSV_TITLE.extend(every_movie_list)
def write_file(csv_path):
with open(csv_path, 'w', encoding='utf-8-sig') as file:
file.writelines(CSV_TITLE)
def main(film_url, csv_path):
for num in range(0, 250, 25):
html = request(film_url, num)
get_movie_info(html)
# 数据处理,并存放excel表格
analysis(movie_list)
write_file(csv_path)
if __name__ == '__main__':
print("Start Downloading douban Movies...")
url = "https://movie.douban.com/top250"
current_path = os.path.dirname(os.path.abspath(__file__))
csv_path = os.path.join(current_path, "movie.csv")
start_time = time.time()
main(url, csv_path)
end_time = time.time()
print(f"抓取电影信息总耗时: {end_time - start_time:.2f}秒")
6.2、使用aiohttp和asyncio库异步IO并发
优化点一:
1、复用 ClientSession:将每次请求时创建session,改成一次创建重复调用
async def request(session, film_path, start):
async with aiohttp.ClientSession() as session:
async with session.get(f"{film_path}?start={start}", headers=headers) as response:
if response.status != 200:
raise RuntimeError(f"Request failed with status {response.status}")
return await response.text()
优化为:
async def main(film_url, csv_path):
# 1. 创建会话(复用连接池,提升性能)
async with aiohttp.ClientSession() as session:
# 2. 创建所有请求任务(并发执行)
tasks = []
for start in range(0, 250, 25):
task = asyncio.create_task(request(session, film_url, start))
tasks.append(task)
2. 异步请求被串行化执行
main函数中逻辑,整个循环是一个请求完成→解析完成→休眠→下一个请求,完全串行,没有并发
for start in range(0, 250, 25):
# 会阻塞当前协程,直到这个请求返回结果,和同步的requests.get()一样
html = await request(film_url, start)
movie_info = asyncio.create_task(get_movie_info(html))
# 会阻塞当前协程,直到这个请求返回结果,和同步的requests.get()一样
await movie_info # 等待当前解析完成
await asyncio.sleep(random.randint(1, 2)) # 等待休眠完成
优化成:
先批量创建所有请求任务(tasks = [create_task(...) for ...])
用asyncio.gather(*tasks)并发执行所有请求,等待全部完成
3. 休眠时间占比高
# 核心:
同时发起多个请求,在等待请求响应的空闲时间里处理其他任务
批量创建任务→并发执行→统一等待结果,能真正体现异步优势
import os
import random
import time
import aiohttp
import asyncio
import re
import aiofiles
timeout= aiohttp.ClientTimeout(total=10)
CSV_TITLE = ["排名, 电影名称, 英文名称, 其他名称, 评分, 评价人数, 导演, 演员, 年份, 地区, 类型, 经典台词\n"]
async def request(session, film_path, start):
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\
(KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"
}
# 随机休眠(分散请求压力,避免被封)
await asyncio.sleep(random.uniform(0.5, 1.0))
async with session.get(f"{film_path}?start={start}", headers=headers) as response:
if response.status != 200:
raise RuntimeError(f"Request failed with status {response.status}")
return await response.text()
movie_info_list = []
async def get_movie_info(context):
# 1. 第一步:先匹配所有电影的<li>标签(缩小匹配范围,降低出错概率)
# 匹配每部电影的<li>标签,从<li>开始到</li>结束,非贪婪匹配,匹配最近的</li>
li_pattern = re.compile(r'<li>(.*?)</li>', re.S)
movie_li_list = re.findall(li_pattern, context)
for li_html in movie_li_list:
movie_info = {}
# 提取排名信息(<em>标签内的数字)
rank_match = re.search(r'<em>(.*?)</em>', li_html)
movie_info["排名"] = rank_match.group(1) if rank_match else ""
# 提取电影名称(第一个<span class="title">)
main_title_match = re.search(r'<span class="title">(.*?)</span>', li_html)
movie_info["电影名称"] = main_title_match.group(1) if main_title_match else ""
# 提取英文名称(第二个<span class="title">)
english_title_match = re.search(r'<span class="title"> / (.*?)</span>', li_html)
movie_info["英文名称"] = english_title_match.group(1) if english_title_match else ""
# 提取其他名称(第一个<span class="other">)
other_title_match = re.search(r'<span class="other"> / (.*?)</span>', li_html)
movie_info["其他名称"] = other_title_match.group(1) if other_title_match else ""
# 提取评分(<span class="rating_num">)
# rating_match = re.search(r'<span class="rating_num" property="v:average">(\d.\d)</span>', li_html)
rating_match = re.search(r'<span class="rating_num" property="v:average">(.*?)</span>', li_html)
# movie_info["评分"] = float(rating_match.group(1)) if rating_match else ""
movie_info["评分"] = rating_match.group(1) if rating_match else ""
# 提取评价人数(匹配"XXX人评价"中的数字)
evaluate_match = re.search(r'<span>(\d+)人评价</span>', li_html)
# movie_info["评价人数"] = int(evaluate_match.group(1)) if evaluate_match else ""
movie_info["评价人数"] = evaluate_match.group(1) if evaluate_match else ""
# 提取导演、主演、年份、地区、类型(最复杂的部分,做容错)
# 先匹配p标签内的所有文本(去除HTML标签)
p_text_match = re.search(r'<p>(.*?)</p>', li_html, re.S)
if p_text_match:
p_text = p_text_match.group(1)
# 清理p文本:去除所有HTML标签、多余空格和换行
p_text = re.sub(r'<.*?>', "", p_text)
p_text = re.sub(r'\s+', ' ', p_text).strip()
# 提取导演(匹配"导演: xxx")
director_match = re.search(r'导演: (.*?) ', p_text)
movie_info["导演"] = director_match.group(1).strip() if director_match else ""
# 提取主演(匹配"主演: xxx",可选)
actor_match = re.search(r'主演: (.*?)(?: \d{4}|$)', p_text)
movie_info["演员"] = actor_match.group(1).strip() if actor_match else ""
# 提取年份(4位数字)
year_match = re.search(r'(\d{4})', p_text)
movie_info["年份"] = year_match.group(1) if year_match else ""
# 提取地区和类型(年份后按"/"拆分)
# 再去除 便于分隔地区和年份
p_text = re.sub(r' ', '', p_text)
area_type_match = re.search(r'\d{4}\s*/\s*(.*?)\s*/\s*(.*?)$', p_text)
if area_type_match:
movie_info["地区"] = area_type_match.group(1).strip() if area_type_match else ""
movie_info["类型"] = area_type_match.group(2).strip() if area_type_match else ""
else:
movie_info["地区"] = ""
movie_info["类型"] = ""
# 提取经典台词(<span class="quote">)
quote_match = re.search(r'<span>(\D+)</span>', li_html, re.S)
movie_info["经典台词"] = quote_match.group(1) if quote_match else ""
movie_info_list.append(movie_info)
async def analysis(info_list):
"""批量解析电影信息为CSV行"""
print("Start Async Analysing Movie Info...")
every_movie_list = []
if not info_list:
print("movie list is empty, please check!.")
for index, every_movie in enumerate(info_list):
print("正在处理第{index}部电影,电影名称是: {every_movie}".format(index=index + 1, every_movie=every_movie["电影名称"]))
temp_movie_list = []
for key, value in every_movie.items():
if isinstance(value, str):
if "其他名称" in key:
# value = value.strip().replace(" ", "")
value = re.sub(r'\s+', '', value)
value = value.strip()
else:
value = str(value)
# 处理值中包含逗号的情况(CSV中逗号会分隔字段,需用双引号包裹)
if "," in value:
value = f'"{value}"'
temp_movie_list.append(value)
# 2. 新增:单部电影的字段值拼接成一行,末尾加换行符
movie_line = ",".join(temp_movie_list) + "\n"
# print(movie_line)
every_movie_list.append(movie_line) # 把带换行的行加入总列表
CSV_TITLE.extend(every_movie_list)
async def write_file(csv_path):
"""异步写入文件"""
async with aiofiles.open(csv_path, 'w', encoding='utf-8-sig') as file:
await file.writelines(CSV_TITLE)
async def main(film_url, csv_path):
# 1. 创建会话(复用连接池,提升性能)
async with aiohttp.ClientSession(timeout=timeout) as session:
# 2. 创建所有请求任务(并发执行)
tasks = []
for start in range(0, 250, 25):
task = asyncio.create_task(request(session, film_url, start))
tasks.append(task)
# 3. 并发执行所有请求,等待全部完成
print("开始并发请求所有页面...")
html_list = await asyncio.gather(*tasks) # 核心:并发执行
# 4. 解析所有页面的电影信息(串行,CPU密集型,无io,异步无法加速,和同步类似)
all_movie_info = []
for html in html_list:
await get_movie_info(html)
await analysis(movie_info_list)
await write_file(csv_path)
if __name__ == '__main__':
print("Start Async Downloading douban Movies...")
current_path = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(current_path, "movie1.csv")
url = "https://movie.douban.com/top250"
# 第一种启动方式
# loop = asyncio.new_event_loop()
# loop.run_until_complete(main(url))
# 第二种启动方式
start_time = time.time()
asyncio.run(main(url, file_path))
end_time = time.time()
print(f"抓取电影信息总耗时: {end_time - start_time:.2f}秒")
6.3、怎么生成csv文件?
目的就是把要写入csv文件的数据先按行处理,先处理每一行,每一行存放到一个临时列表,再用逗号连接。最后再将连接后的内容存入另一个列表。
单部电影解析完成后拼接成一行字符串,再将所有行用换行符连接
movie_info_list = [
{'排名': '1', '电影名称': '肖申克的救赎', '英文名称': 'The Shawshank Redemption', '其他名称': '月黑高飞(港) / 刺激1995(台)', '评分': '9.7', '评价人数': '3247709', '导演': '弗兰克·德拉邦特 Frank Darabont', '演员': '蒂姆·罗宾斯 Tim Robbins /...', '年份': '1994', '地区': '美国', '类型': '犯罪 剧情', '经典台词': '希望让人自由。'}
]
# 1、创建表头
CSV_TITLE = ["排名, 电影名称, 英文名称, 其他名称, 评分, 评价人数, 导演, 演员, 年份, 地区, 类型, 经典台词\n"]
def get_movie_info_list():
# 2、创建存放所有电影信息的列表
movie_info = []
for index, every_movie in enumerate(movie_info_list):
# 3、创建临时列表,目的是将每一个行单独处理
temp_movie_list = []
for key, value in every_movie.items():
# 数据处理
if "," in value:
value = f"{str(value)}"
# 3、将一部电影的信息存入临时列表
# 如 ['1', '肖申克的救赎', 'The Shawshank Redemption'.. , '希望让人自由。']
temp_movie_list.append(value)
# 4、先将每部电影用逗号连接,注意换行符
# 如:1, 肖申克的救赎,The Shawshank Redemption... , 希望让人自由。
msg = ",".join(temp_movie_list) + "\n"
# 5、再将连接后的内容,存入存放所有电影信息的列表
# 如:["1, 肖申克的救赎,The Shawshank Redemption... , 希望让人自由", "2, 肖申克的救赎,The Shawshank Redemption... , 希望让人自由"]
movie_info.append(msg)
# 6、将所有信息依次加入大列表
CSV_TITLE.extend(movie_info)
with open("movie_info.csv", "w+", encoding="utf-8") as f:
f.writelines(CSV_TITLE)
if __name__ == '__main__':
get_movie_info_list()
6.5、大批量数据,逐行写入
如果解析 250 部电影,建议逐行写入文件(而非先存列表再一次性写入),减少内存占用
def analysis(info_list, file_path):
with open(file_path, "w", encoding="utf-8-sig") as f:
# 先写标题行
f.write(",".join(CSV_TITLE) + "\n")
for every_movie in info_list:
movie_row = []
for key in CSV_TITLE:
value = every_movie.get(key, "")
if isinstance(value, str):
value = value.strip()
else:
value = str(value)
if "," in value:
value = f'"{value}"'
movie_row.append(value)
# 逐行写入,自动换行
f.write(",".join(movie_row) + "\n")
451

被折叠的 条评论
为什么被折叠?



