5.7 Python HTTP 请求与 urllib、requests 库

前言:无处不在的HTTP

在当今的互联网世界中,HTTP(HyperText Transfer Protocol) 协议是几乎所有Web通信的基础。无论是浏览网页、使用手机App,还是微服务之间的调用,最终大多都转化为HTTP请求。

  • Web爬虫:需要模拟浏览器向网站服务器发送请求,获取网页内容。
  • API集成:需要调用第三方服务(如天气API、支付API、社交媒体API)提供的HTTP接口。
  • 自动化测试:需要测试Web应用的接口是否正确工作。
  • 数据采集与分析:需要从各种数据源获取数据。

Python提供了多种方式进行HTTP请求,最主要的是标准库中的urllib和第三方库requests。本章将深入探讨这两者,帮助你根据场景做出最佳选择。


一、 HTTP协议快速回顾

在深入代码之前,有必要快速回顾一下HTTP的基本概念:

  1. 请求-响应模型:客户端发起请求,服务器返回响应。
  2. URL(统一资源定位符):标识互联网上资源的地址,如 https://api.github.com/users/octocat
  3. HTTP方法
    • GET:获取资源
    • POST:创建资源
    • PUT:更新资源
    • DELETE:删除资源
    • HEADPATCHOPTIONS
  4. 状态码
    • 2xx:成功(如200 OK)
    • 3xx:重定向
    • 4xx:客户端错误(如404 Not Found)
    • 5xx:服务器错误
  5. 请求头/响应头:包含元数据,如内容类型、认证信息等。
  6. 请求体/响应体:实际传输的数据内容。

二、 标准库:urllib

Python标准库中的urllib包提供了用于HTTP请求的基本功能。它包含多个模块:

  • urllib.request:打开和读取URL
  • urllib.error:包含urllib.request引发的异常
  • urllib.parse:用于解析URL
  • urllib.robotparser:用于解析robots.txt文件
1. 基本GET请求
from urllib import request
import urllib.error

url = 'https://httpbin.org/get'

try:
    # 发送GET请求
    with request.urlopen(url) as response:
        # 获取状态码和响应头
        print(f"状态码: {response.status}")
        print(f"响应头: {dict(response.getheaders())}")
        
        # 读取响应内容
        data = response.read()
        print(f"响应体: {data.decode('utf-8')}")
        
except urllib.error.URLError as e:
    print(f"请求失败: {e.reason}")
2. 添加请求参数

对于GET请求,参数通常附加在URL的查询字符串中:

from urllib import request, parse

base_url = 'https://httpbin.org/get'
params = {
    'name': 'John Doe',
    'age': 30,
    'city': 'New York'
}

# 将参数字典编码为查询字符串
query_string = parse.urlencode(params)
full_url = f"{base_url}?{query_string}"
print(f"完整URL: {full_url}")

with request.urlopen(full_url) as response:
    print(response.read().decode('utf-8'))
3. 添加自定义请求头

有些网站会验证请求头,防止简单的爬虫访问:

from urllib import request

url = 'https://httpbin.org/headers'

# 创建请求对象并添加头部
req = request.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
req.add_header('Accept', 'application/json')
req.add_header('Custom-Header', 'MyValue')

with request.urlopen(req) as response:
    print(response.read().decode('utf-8'))
4. 发送POST请求

POST请求通常用于提交表单数据或JSON数据:

from urllib import request, parse
import json

url = 'https://httpbin.org/post'

# 方式1: 表单数据
form_data = {
    'username': 'testuser',
    'password': 'testpass'
}
encoded_data = parse.urlencode(form_data).encode('utf-8')

# 方式2: JSON数据
json_data = json.dumps({
    'title': 'Test Post',
    'body': 'This is a test post content',
    'userId': 1
}).encode('utf-8')

# 创建请求对象
req = request.Request(url, data=encoded_data, method='POST')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')

# 对于JSON数据
# req = request.Request(url, data=json_data, method='POST')
# req.add_header('Content-Type', 'application/json')

with request.urlopen(req) as response:
    result = response.read().decode('utf-8')
    print(result)
5. 处理异常

urllib提供了丰富的异常类来处理各种错误情况:

from urllib import request, error

test_urls = [
    'https://httpbin.org/status/404',
    'https://httpbin.org/status/500',
    'https://invalid-domain-abcdefg.org',
    'https://httpbin.org/delay/5'  # 可能会超时
]

for url in test_urls:
    try:
        # 设置超时时间为3秒
        with request.urlopen(url, timeout=3) as response:
            print(f"{url} - 成功: {response.status}")
            
    except error.HTTPError as e:
        print(f"{url} - HTTP错误: {e.code} {e.reason}")
    except error.URLError as e:
        print(f"{url} - URL错误: {e.reason}")
    except Exception as e:
        print(f"{url} - 其他错误: {e}")
6. 使用代理

在某些网络环境下,可能需要使用代理服务器:

from urllib import request

url = 'https://httpbin.org/ip'

# 设置代理
proxy_handler = request.ProxyHandler({
    'http': 'http://proxy.example.com:8080',
    'https': 'https://proxy.example.com:8080',
})

# 创建自定义opener
opener = request.build_opener(proxy_handler)

# 安装为默认opener
request.install_opener(opener)

try:
    with request.urlopen(url) as response:
        print(response.read().decode('utf-8'))
except Exception as e:
    print(f"通过代理请求失败: {e}")
7. 处理Cookie

对于需要保持会话的网站,需要处理Cookie:

from urllib import request
from http import cookiejar

# 创建CookieJar来存储cookies
cookie_jar = cookiejar.CookieJar()

# 创建处理器
cookie_handler = request.HTTPCookieProcessor(cookie_jar)

# 创建opener
opener = request.build_opener(cookie_handler)

# 模拟登录
login_url = 'https://httpbin.org/cookies/set/sessioncookie/123456789'
with opener.open(login_url) as response:
    print("设置cookie响应:", response.read().decode('utf-8'))

# 再次访问,cookie会自动带上
test_url = 'https://httpbin.org/cookies'
with opener.open(test_url) as response:
    print("带cookie的响应:", response.read().decode('utf-8'))

虽然urllib功能强大,但它的API相对底层和繁琐。这也是为什么requests库如此受欢迎的原因。


三、 第三方库:requests

requests是一个优雅而简单的HTTP库,为人类设计。它封装了urllib的复杂细节,提供了更友好直观的API。

首先安装它:pip install requests

1. 基本GET请求
import requests

url = 'https://httpbin.org/get'

# 发送GET请求
response = requests.get(url)

# 检查请求是否成功
if response.status_code == 200:
    print("请求成功!")
    print(f"状态码: {response.status_code}")
    print(f"响应头: {dict(response.headers)}")
    print(f"响应内容: {response.text}")  # 文本内容
    print(f"JSON响应: {response.json()}")  # 自动解析JSON
else:
    print(f"请求失败,状态码: {response.status_code}")
2. 传递URL参数
import requests

url = 'https://httpbin.org/get'
params = {
    'key1': 'value1',
    'key2': 'value2',
    'key3': ['value3', 'value4']  # 多个值
}

response = requests.get(url, params=params)
print(f"最终URL: {response.url}")
print(response.json())
3. 自定义请求头
import requests

url = 'https://httpbin.org/headers'
headers = {
    'User-Agent': 'MyPythonScript/1.0',
    'Accept': 'application/json',
    'Custom-Header': 'custom-value'
}

response = requests.get(url, headers=headers)
print(response.json())
4. 发送POST请求
import requests
import json

url = 'https://httpbin.org/post'

# 方式1: 表单数据
form_data = {'key1': 'value1', 'key2': 'value2'}
response = requests.post(url, data=form_data)
print("表单POST响应:")
print(response.json())

# 方式2: JSON数据
json_data = {'name': 'John', 'age': 30}
response = requests.post(url, json=json_data)
print("\nJSON POST响应:")
print(response.json())

# 方式3: 原始数据
raw_data = '简单文本数据'
response = requests.post(url, data=raw_data)
print("\n原始数据POST响应:")
print(response.json())
5. 处理超时和异常
import requests
from requests.exceptions import RequestException

urls = [
    'https://httpbin.org/status/404',
    'https://httpbin.org/status/500',
    'https://httpbin.org/delay/10',  # 10秒延迟
    'https://invalid-domain-xyz.com'
]

for url in urls:
    try:
        response = requests.get(url, timeout=5)  # 5秒超时
        response.raise_for_status()  # 如果状态码不是200,抛出HTTPError
        print(f"{url} - 成功")
        
    except requests.exceptions.Timeout:
        print(f"{url} - 请求超时")
    except requests.exceptions.HTTPError as e:
        print(f"{url} - HTTP错误: {e}")
    except requests.exceptions.ConnectionError:
        print(f"{url} - 连接错误")
    except RequestException as e:
        print(f"{url} - 其他请求错误: {e}")
6. 使用会话保持状态

Session对象可以自动处理cookies,保持会话状态:

import requests

# 创建会话
session = requests.Session()

# 第一次请求,设置cookies
url1 = 'https://httpbin.org/cookies/set/sessionid/12345'
response1 = session.get(url1)
print("第一次请求响应:", response1.json())

# 第二次请求,自动携带cookies
url2 = 'https://httpbin.org/cookies'
response2 = session.get(url2)
print("第二次请求响应:", response2.json())

# 会话结束后关闭
session.close()
7. 高级特性:认证、代理、SSL验证
import requests
from requests.auth import HTTPBasicAuth

url = 'https://httpbin.org/basic-auth/user/passwd'

# 基本认证
response = requests.get(url, auth=HTTPBasicAuth('user', 'passwd'))
print("基本认证响应:", response.json())

# 使用代理
proxies = {
    'http': 'http://10.10.1.10:3128',
    'https': 'http://10.10.1.10:1080',
}
# response = requests.get('https://httpbin.org/ip', proxies=proxies)

# 禁用SSL验证(不推荐在生产环境使用)
response = requests.get('https://httpbin.org/get', verify=False)
print("禁用SSL验证请求:", response.status_code)

# 设置自定义CA证书
# response = requests.get('https://httpbin.org/get', verify='/path/to/certfile')

四、 urllib vs requests:如何选择?
特性urllib (标准库)requests (第三方库)
安装Python内置,无需安装需要pip安装
API友好度底层,繁琐高级,直观易用
功能完整性基础功能完备功能丰富,封装完善
文档和社区官方文档,相对简单文档极佳,社区活跃
性能稍好(更接近底层)稍差(封装开销)
适用场景简单请求,不想引入依赖绝大多数HTTP请求场景

建议

  • 对于大多数应用,优先使用requests,它的开发效率更高。
  • 只有在不能安装第三方库,或者需要极致的性能控制时,才考虑使用urllib。

五、 综合实战:GitHub API客户端

让我们构建一个实用的GitHub API客户端,展示如何在实际项目中使用requests库。

import requests
import json
from datetime import datetime, timedelta

class GitHubAPIClient:
    def __init__(self, token=None):
        self.base_url = 'https://api.github.com'
        self.headers = {
            'Accept': 'application/vnd.github.v3+json',
            'User-Agent': 'Python-GitHub-API-Client'
        }
        if token:
            self.headers['Authorization'] = f'token {token}'
        
        self.session = requests.Session()
        self.session.headers.update(self.headers)

    def get_user_info(self, username):
        """获取用户信息"""
        url = f'{self.base_url}/users/{username}'
        response = self.session.get(url)
        response.raise_for_status()
        return response.json()

    def get_user_repos(self, username, sort='updated', direction='desc'):
        """获取用户仓库列表"""
        url = f'{self.base_url}/users/{username}/repos'
        params = {
            'sort': sort,
            'direction': direction,
            'per_page': 10  # 限制返回数量
        }
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()

    def search_repositories(self, query, language=None, stars='>100', sort='stars'):
        """搜索仓库"""
        url = f'{self.base_url}/search/repositories'
        search_query = query
        if language:
            search_query += f' language:{language}'
        if stars:
            search_query += f' stars:{stars}'
            
        params = {
            'q': search_query,
            'sort': sort,
            'order': 'desc'
        }
        
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()

    def get_trending_repos(self, since='weekly'):
        """获取趋势仓库(通过搜索模拟)"""
        # 计算日期范围
        if since == 'daily':
            date_since = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
        elif since == 'weekly':
            date_since = (datetime.now() - timedelta(weeks=1)).strftime('%Y-%m-%d')
        else:  # monthly
            date_since = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
            
        query = f'created:>{date_since}'
        return self.search_repositories(query, sort='stars')

    def create_gist(self, description, files, public=True):
        """创建Gist(需要认证)"""
        if 'Authorization' not in self.headers:
            raise Exception("需要GitHub token来创建Gist")
            
        url = f'{self.base_url}/gists'
        data = {
            'description': description,
            'public': public,
            'files': files
        }
        
        response = self.session.post(url, json=data)
        response.raise_for_status()
        return response.json()

    def close(self):
        """关闭会话"""
        self.session.close()

def main():
    # 创建客户端实例
    # 如果需要创建Gist,需要提供GitHub personal access token
    # github = GitHubAPIClient('your_github_token_here')
    github = GitHubAPIClient()

    try:
        # 1. 获取用户信息
        print("=== 获取用户信息 ===")
        user_info = github.get_user_info('octocat')
        print(f"用户名: {user_info['login']}")
        print(f"姓名: {user_info.get('name', '未知')}")
        print(f"粉丝数: {user_info['followers']}")
        print(f"仓库数: {user_info['public_repos']}\n")

        # 2. 获取用户仓库
        print("=== 获取用户最新仓库 ===")
        repos = github.get_user_repos('octocat')
        for repo in repos[:3]:  # 只显示前3个
            print(f"{repo['name']}: {repo['description']}")
            print(f"  星标数: {repo['stargazers_count']}, 语言: {repo.get('language', '未知')}\n")

        # 3. 搜索Python仓库
        print("=== 搜索热门Python仓库 ===")
        search_results = github.search_repositories('web framework', 'python', '>1000')
        for repo in search_results['items'][:5]:
            print(f"{repo['name']} - {repo['description']}")
            print(f"  星标: {repo['stargazers_count']},  forks: {repo['forks_count']}")
            print(f"  URL: {repo['html_url']}\n")

        # 4. 获取趋势仓库
        print("=== 本周趋势仓库 ===")
        trending = github.get_trending_repos('weekly')
        for repo in trending['items'][:5]:
            print(f"{repo['name']} - {repo['description']}")
            print(f"  星标: {repo['stargazers_count']}, 创建于: {repo['created_at']}\n")

    except requests.exceptions.HTTPError as e:
        print(f"HTTP错误: {e}")
    except requests.exceptions.RequestException as e:
        print(f"请求错误: {e}")
    finally:
        github.close()

if __name__ == "__main__":
    main()

这个实战项目展示了:

  1. API调用:多种类型的GitHub API请求
  2. 参数处理:查询参数、认证头部的使用
  3. 错误处理:完善的异常处理机制
  4. 会话管理:使用Session保持连接和头部信息
  5. 数据处理:解析和利用JSON响应

六、 高级话题与最佳实践
1. 重试机制

对于生产环境,应该实现重试机制来处理临时性网络问题:

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

def create_session_with_retries(retries=3, backoff_factor=0.3):
    """创建带重试机制的会话"""
    session = requests.Session()
    
    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,  # 重试之间的等待时间:{backoff_factor} * (2 ** (重试次数 - 1))
        status_forcelist=[429, 500, 502, 503, 504],  # 遇到这些状态码时重试
        allowed_methods=["GET", "POST"]  # 只对这些方法重试
    )
    
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    
    return session

# 使用示例
session = create_session_with_retries()
try:
    response = session.get('https://httpbin.org/status/500', timeout=5)
    print(response.status_code)
except requests.exceptions.RetryError:
    print("重试多次后仍然失败")
2. 速率限制处理

许多API都有速率限制,需要合理控制请求频率:

import requests
import time
from collections import deque

class RateLimitedSession(requests.Session):
    """带速率限制的会话"""
    def __init__(self, requests_per_second=1):
        super().__init__()
        self.requests_per_second = requests_per_second
        self.request_times = deque()
    
    def request(self, *args, **kwargs):
        # 确保不超过速率限制
        now = time.time()
        
        # 移除1秒前的请求记录
        while self.request_times and now - self.request_times[0] >= 1:
            self.request_times.popleft()
            
        # 如果达到限制,等待
        if len(self.request_times) >= self.requests_per_second:
            sleep_time = 1 - (now - self.request_times[0])
            if sleep_time > 0:
                time.sleep(sleep_time)
                now = time.time()  # 更新当前时间
                
        # 记录本次请求时间
        self.request_times.append(now)
        
        return super().request(*args, **kwargs)

# 使用示例
session = RateLimitedSession(requests_per_second=2)  # 每秒最多2个请求
for i in range(5):
    response = session.get('https://httpbin.org/get')
    print(f"请求 {i+1}: {response.status_code}")
3. 流式处理大响应

对于大文件或流式响应,应该使用流式处理:

import requests

url = 'https://httpbin.org/stream/20'  # 流式API

# 使用流式处理
response = requests.get(url, stream=True)

try:
    for line in response.iter_lines():
        if line:  # 过滤keep-alive空行
            print(f"收到数据: {line.decode('utf-8')}")
finally:
    response.close()
4. 超时设置的最佳实践

总是设置合理的超时时间,避免请求永远挂起:

import requests

# 连接超时和读取超时分开设置
timeouts = (3.05, 27)  # (连接超时, 读取超时)

try:
    response = requests.get('https://httpbin.org/delay/5', timeout=timeouts)
    print("请求成功")
except requests.exceptions.ConnectTimeout:
    print("连接超时")
except requests.exceptions.ReadTimeout:
    print("读取超时")

七、 总结

通过本章的学习,你应该已经掌握了:

  1. HTTP协议基础:理解了HTTP请求-响应模型、方法、状态码等核心概念。
  2. urllib库:学会了使用Python标准库进行基本的HTTP请求。
  3. requests库:掌握了更现代、更友好的HTTP客户端库的使用。
  4. 实战应用:通过GitHub API客户端项目,了解了如何在实际项目中进行API集成。
  5. 高级特性:了解了重试机制、速率限制、流式处理等生产环境需要的功能。

选择建议

  • 对于大多数项目,直接使用requests库,它的开发体验更好。
  • 只有在不能有外部依赖,或者需要极细粒度控制时,才考虑使用urllib

HTTP请求是现代编程的基础技能,无论是Web开发、数据分析还是自动化运维,都离不开与HTTP API的交互。掌握好这个技能,将为你的Python编程之路打开无数可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

达文汐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值