python3从入门到精通(十一): requests模块

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

一、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,如301302- 取值: 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 到 Cookiesession.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.HTTPErrorHTTP 错误(状态码 4xx/5xx),由raise_for_status()触发
requests.exceptions.Timeout请求超时异常
requests.exceptions.ConnectionError连接错误(如网络断开、服务器不可达)
requests.exceptions.SSLErrorSSL 证书验证失败异常
requests.exceptions.JSONDecodeErrorJSON 解析错误(响应内容非合法 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">&nbsp;/&nbsp;(.*?)</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">&nbsp;/&nbsp;(.*?)</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'导演: (.*?)&nbsp', 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 ""

            # 提取地区和类型(年份后按"/"拆分)
            # 再去除&nbsp;便于分隔地区和年份
            p_text = re.sub(r'&nbsp;', '', 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">&nbsp;/&nbsp;(.*?)</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">&nbsp;/&nbsp;(.*?)</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'导演: (.*?)&nbsp;', 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 ""

            # 提取地区和类型(年份后按"/"拆分)
            # 再去除&nbsp;便于分隔地区和年份
            p_text = re.sub(r'&nbsp;', '', 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")

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位不知名民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值