Python爬虫 2.urllib发起请求与Response详解


一、为什么要学习 urllib

在 Python 爬虫开发中,urllib 是一个非常重要的基础库。

在标准库中,urllib 并不是单一功能模块,而是一组与 URL 处理相关的工具。入门阶段最常用的两个部分是:

  • urllib.request:负责发送请求
  • urllib.parse:负责处理 URL 和请求参数

补充:urllib 常见模块还包括:

  • urllib.error:处理请求异常
  • urllib.robotparser:解析 robots 协议

本篇主要聚焦最常用的 requestparse

它的特点是:

  • Python 内置,无需安装
  • 可以直接发送 HTTP 请求
  • 能够帮助我们理解“爬虫的底层原理”

虽然在实际开发中,我们更多会使用更高级的库(如 requests),但 urllib 的学习可以帮助我们理解:

浏览器是如何和服务器进行通信的, 爬虫的请求是如何构造的 , 因此,urllib 更适合作为“入门底层原理”的工具。

补充:urllib 在爬虫体系中的位置

在整个爬虫技术栈中,可以把 urllib 理解为:

  • 最底层的请求工具(原生)
  • 帮助理解 HTTP 请求的本质

对比关系可以这样理解:

  • urllib:底层原理学习
  • requests:工程化开发
  • Scrapy:框架化爬虫

学习 urllib 的意义不在于“多用”,而在于“理解”。


二、最简单的请求方式

我们先来看一个最简单的爬虫程序:

# 导入 urllib.request 模块,用于发送 HTTP 请求
import urllib.request

# 定义要访问的目标网址
url = 'http://www.baidu.com'

# 向目标网址发送请求,并获取服务器返回的响应对象
response = urllib.request.urlopen(url)

# 读取响应内容,并按 utf-8 编码解码为字符串
content = response.read().decode('utf-8')

# 输出网页源码内容
print(content)

代码说明:

  • urlopen():向服务器发送请求
  • response:服务器返回的数据对象
  • read():读取返回的数据(字节)
  • decode():将字节转换为字符串

补充说明:

并不是所有网页都使用 utf-8 编码。
如果页面编码不同,直接使用 decode('utf-8') 可能会报错。
入门阶段可以先这样写,后续会学习如何判断网页真实编码。

运行之后,你会看到网页的 HTML 源码。

另外,urlopen() 返回的并不是普通字符串,而是一个 HTTPResponse 对象。后续我们读取状态码、响应头、最终 URL,都是基于这个对象完成的。

注意:这里获取到的是“网页源码”,而不是浏览器渲染后的页面。

补充:请求执行流程(非常重要)

这个简单程序背后,其实发生了完整的 HTTP 请求流程:

  1. 客户端(你的程序)向服务器发送请求
  2. 服务器接收请求并处理
  3. 返回响应(HTML / JSON 等)
  4. 客户端接收并解析数据

可以理解为:

爬虫的核心 = 发送请求 + 解析响应

后续所有技术,都是围绕这两步展开。


三、Response 对象详解

response 是服务器返回的结果对象,我们可以通过它获取很多信息。

常用方法:

response.read()         # 读取全部内容
response.readline()     # 读取一行
response.readlines()    # 读取多行

response.getcode()      # 状态码
response.geturl()       # 当前访问的 URL
response.getheaders()   # 响应头

常见 HTTP 状态码:

  • 200:请求成功
  • 301 / 302:重定向
  • 403:禁止访问(常见反爬)
  • 404:页面不存在
  • 500:服务器错误

在爬虫开发中,最常见的问题就是 403(被网站拒绝)

常见异常处理

在实际开发中,请求经常会失败,因此需要进行异常处理:

# 导入 urllib.request 模块,用于发送请求
import urllib.request
# 导入 urllib.error 模块,用于处理请求异常
import urllib.error

# 定义要访问的网址
url = 'http://www.baidu.com'

# 尝试执行请求代码
try:
    # 发送请求并获取响应对象
    response = urllib.request.urlopen(url)
    # 读取响应内容并解码后输出
    print(response.read().decode('utf-8'))
# 捕获 HTTP 状态码异常,例如 403、404
except urllib.error.HTTPError as e:
    # 输出 HTTP 错误状态码
    print('HTTPError:', e.code)
# 捕获 URL 请求异常,例如网络错误、域名错误
except urllib.error.URLError as e:
    # 输出具体的异常原因
    print('URLError:', e.reason)

说明:

  • HTTPError:服务器返回了响应,但状态码异常(如 403、404)
  • URLError:请求未成功发出(如网络问题、域名错误)

补充:响应头中常见信息

可以通过 response.getheaders() 查看完整响应头,例如:

print(response.getheaders())

常见字段包括:

  • Content-Type:返回数据类型(HTML / JSON)
  • Server:服务器类型
  • Set-Cookie:服务器设置的 Cookie

这些信息在后续分析接口和登录状态时非常重要。

实战建议

在真实项目中,建议:

  • 对关键请求增加重试机制
  • 打印日志方便排查问题

例如:

for i in range(3):
    try:
        response = urllib.request.urlopen(url)
        break
    except Exception:
        print('重试中...', i)

四、下载网页、图片和文件

urllib 提供了一个更简单的方法用于下载资源:

# 导入 urllib.request 模块,用于下载网络资源
import urllib.request

# 定义要下载的图片地址
url = 'https://www.baidu.com/img/bd_logo1.png'

# 下载图片并保存为本地文件 logo.png
urllib.request.urlretrieve(url, 'logo.png')

说明:

  • 第一个参数:资源地址
  • 第二个参数:保存的文件名

这个方法不仅可以下载图片,还可以下载:

  • HTML 页面
  • 视频
  • 文件(如 PDF)

注意事项:

  • 有些网站需要请求头,否则下载失败
  • 大文件下载建议使用流式处理(后续会讲)

补充说明:

urlretrieve() 适合入门演示,但在实际项目中,很多开发者更倾向于使用 urlopen()requests 手动读取响应并保存文件,这样更加灵活,也更方便控制请求头和异常处理。

补充:使用 urlopen 手动下载(更灵活)

import urllib.request

url = 'https://www.baidu.com/img/bd_logo1.png'

# 发送请求
response = urllib.request.urlopen(url)

# 读取二进制数据
content = response.read()

# 写入文件
with open('logo.png', 'wb') as f:
    f.write(content)

优点:

  • 可以控制请求头
  • 可以处理异常
  • 更适合真实项目

五、为什么要定制请求对象

在真实爬虫中,直接使用 urlopen() 往往会失败,例如:

返回 403(被拒绝访问)

原因是:

服务器会判断请求是否来自“真实浏览器”。

因此我们需要使用 Request 对象来自定义请求。

# 导入 urllib.request 模块,用于构造请求和发送请求
import urllib.request

# 定义要访问的网址
url = 'http://www.baidu.com'

# 定义请求头字典
headers = {
    # 添加 User-Agent,请求时伪装成浏览器
    'User-Agent': 'Mozilla/5.0'
}

# 根据 URL 和请求头构造 Request 对象
request = urllib.request.Request(url=url, headers=headers)

# 发送自定义请求对象,并获取响应结果
response = urllib.request.urlopen(request)

# 读取响应内容并解码后输出
print(response.read().decode('utf-8'))

核心作用:

自定义请求头(headers)
模拟浏览器访问

补充:

除了 User-Agent,常见的请求头还包括:

  • Referer
  • Cookie
  • Accept
  • Accept-Language

在很多网站中,仅设置 User-Agent 仍可能被识别,后续需要组合多个请求头。

补充:403 的本质原因

403 Forbidden 本质上是:

服务器判断你不是“正常用户”

常见检测方式:

  • User-Agent
  • Referer
  • Cookie
  • 请求频率

因此,仅添加 UA 有时还不够,需要组合策略。


六、User-Agent 基础介绍

什么是 User-Agent?

User-Agent 是 HTTP 请求头的一部分,用于标识客户端信息,例如:

  • 浏览器类型
  • 操作系统
  • 设备信息

例如:

Mozilla/5.0 (Windows NT 10.0; Win64; x64)

为什么网站会检测 UA?

网站通过 UA 判断:

  • 是否是浏览器访问
  • 是否是爬虫程序

如果没有 UA,很多网站会直接返回:

403 Forbidden


如何伪装浏览器?

我们只需要在请求头中加入 User-Agent:

# 定义请求头字典
headers = {
    # 在请求头中加入 User-Agent,伪装成浏览器
    'User-Agent': 'Mozilla/5.0'
}

扩展:

后续我们还会学习:

  • Cookie(保持登录状态)
  • Proxy(代理 IP)
  • Headers 伪装策略

这些都是“反爬对抗”的核心技术。

常见浏览器 UA 示例

Chrome:
Mozilla/5.0 (Windows NT 10.0; Win64; x64)

Safari:
Mozilla/5.0 (Macintosh; Intel Mac OS X)

Mobile:
Mozilla/5.0 (iPhone; CPU iPhone OS)

可以根据目标网站选择更合适的 UA。


七、补充:GET 与 POST 参数的基本处理

在使用 urllib 发送请求时,除了最基本的 urlopen()Request(),还需要理解参数是如何传递的。

1. GET 请求参数

GET 请求通常会把参数直接拼接到 URL 后面。

例如,我们在搜索引擎中搜索关键词时,常见的 URL 形式如下:

https://www.baidu.com/s?wd=python

如果参数中包含中文或特殊字符,就需要先进行编码。

方式一:使用 quote() 处理单个参数

# 导入 urllib.parse 模块,用于处理 URL 编码
import urllib.parse

# 定义搜索关键词
keyword = '周杰伦'

# 对中文关键词进行编码,并拼接成完整 URL
url = 'https://www.baidu.com/s?wd=' + urllib.parse.quote(keyword)

# 输出编码后的 URL
print(url)

这种方式适合参数较少、结构比较简单的情况。

方式二:使用 urlencode() 处理多个参数

# 导入 urllib.parse 模块,用于处理查询参数
import urllib.parse

# 定义多个查询参数
params = {
    # 搜索关键词参数
    'wd': '周杰伦',
    # 示例参数:性别
    'sex': '男',
    # 示例参数:地区
    'location': '中国台湾省'
}

# 将字典参数编码为查询字符串
query_string = urllib.parse.urlencode(params)

# 将查询字符串拼接到 URL 后面
url = 'https://www.baidu.com/s?' + query_string

# 输出最终生成的 URL
print(url)

实际开发中,更推荐使用 urlencode(),因为它更适合处理多个参数,也更接近真实项目中的请求写法。

2. POST 请求参数

POST 请求和 GET 的主要区别在于:

  • GET:参数拼接在 URL 后面
  • POST:参数放在请求体中

在 urllib 中,POST 参数除了要进行 urlencode() 处理,还必须进一步转换为字节数据。

# 导入 urllib.request 模块,用于构造和发送请求
import urllib.request
# 导入 urllib.parse 模块,用于对 POST 参数进行编码
import urllib.parse

# 定义目标接口地址
url = 'https://fanyi.baidu.com/sug'

# 定义请求头字典
headers = {
    # 添加 User-Agent,请求时伪装成浏览器
    'User-Agent': 'Mozilla/5.0'
}

# 定义要提交的 POST 表单数据
data = {
    # 提交的关键词参数
    'kw': 'spider'
}

# 先将字典编码为查询字符串,再转换为字节数据
encoded_data = urllib.parse.urlencode(data).encode('utf-8')

# 构造携带 POST 数据的请求对象
request = urllib.request.Request(url=url, headers=headers, data=encoded_data)

# 发送 POST 请求并获取响应对象
response = urllib.request.urlopen(request)

# 读取响应内容并按 utf-8 解码为字符串
content = response.read().decode('utf-8')

# 输出服务器返回的内容
print(content)

这里需要重点记住两点:

  • GET 参数通常不需要再调用 encode(),因为它们最终是 URL 字符串的一部分
  • POST 参数在放入 Request()data 参数前,必须先转成字节类型

3. GET 和 POST 的简单对比

可以先用一句话记住它们的区别:

  • GET 更常用于获取数据
  • POST 更常用于提交数据

在后续的接口请求、登录请求、翻译请求中,POST 会非常常见。

补充:如何判断请求类型

可以通过浏览器开发者工具判断:

  1. 打开 Network 面板
  2. 点击请求
  3. 查看 Method 字段

如果是:

  • GET → 参数在 URL
  • POST → 参数在 Request Payload

八、Ajax 请求、Handler 与代理补充

前面介绍的是最基础的页面请求。实际抓取时,经常会遇到接口请求、Cookie 请求、代理请求等情况。

1. Ajax 请求

很多网站页面中的数据并不是直接写在 HTML 中,而是通过接口异步加载。浏览器中看到的数据,可能来自 Network 面板中的 XHR 或 Fetch 请求。

这类请求一般先在浏览器中找到接口地址,然后用 urllib 直接请求接口。

from urllib import request, parse

base_url = 'https://example.com/api/list?'
params = {
    'page': 1,
    'pageSize': 10
}

url = base_url + parse.urlencode(params)

headers = {
    'User-Agent': 'Mozilla/5.0',
    'Referer': 'https://example.com/list'
}

req = request.Request(url=url, headers=headers)
response = request.urlopen(req, timeout=10)
content = response.read().decode('utf-8')

print(content)

Ajax 接口通常返回 JSON 数据,后面可以继续使用 json.loads() 转成 Python 字典。

2. Handler 的作用

urlopen() 适合简单请求,如果需要统一处理 Cookie、代理、认证等逻辑,就需要使用 Handler。

可以把 Handler 理解为 urllib 的请求处理器。

from urllib import request

handler = request.HTTPHandler()
opener = request.build_opener(handler)

req = request.Request(
    url='https://www.baidu.com',
    headers={'User-Agent': 'Mozilla/5.0'}
)

response = opener.open(req)
print(response.read().decode('utf-8'))

后续如果要加代理、Cookie,也是在 build_opener() 中组合不同 Handler。

3. 使用代理

代理的作用是让请求通过另一台服务器转发。测试代理是否可用时,不要一开始就访问复杂网站,可以先访问能返回 IP 的测试接口。

from urllib import request

proxy_handler = request.ProxyHandler({
    'http': 'http://127.0.0.1:7890',
    'https': 'http://127.0.0.1:7890'
})

opener = request.build_opener(proxy_handler)

req = request.Request(
    url='https://httpbin.org/ip',
    headers={'User-Agent': 'Mozilla/5.0'}
)

response = opener.open(req, timeout=10)
print(response.read().decode('utf-8'))

如果要使用多个代理,可以先把代理地址放到列表中,再通过循环或随机选择的方式取出。

import random
from urllib import request

proxy_list = [
    'http://127.0.0.1:7890',
    'http://127.0.0.1:7891'
]

proxy = random.choice(proxy_list)

proxy_handler = request.ProxyHandler({
    'http': proxy,
    'https': proxy
})

实际使用代理时要注意两点:

  1. 代理不可用是很常见的,需要加超时和异常处理。
  2. 不要因为使用了代理就高频请求,频率过高仍然可能影响目标网站。

九、调试建议

如果代码运行失败,可以优先检查以下问题:

  • URL 是否可以在浏览器中正常打开
  • 是否因为缺少请求头被网站拒绝(如 403)
  • 是否是编码问题导致解码失败
  • 是否是网络问题或代理/VPN影响了请求

建议刚开始先使用结构简单、限制较少的网站进行测试,例如:

http://httpbin.org/get

十、本篇总结

本篇我们系统学习了 urllib 的核心内容:

  • 如何发送请求(urlopen / Request)
  • Response 对象的结构
  • 如何处理异常
  • 如何下载资源
  • 如何添加请求头
  • GET / POST 参数处理
  • Ajax 接口请求、Handler 与代理的基本使用

同时理解了一个非常重要的概念:

爬虫 = 请求 + 解析

urllib 虽然使用起来不如 requests 简洁,但它帮助我们理解了:

  • HTTP 请求的本质
  • 浏览器与服务器的交互方式

为后续学习 requests、Selenium、Scrapy 打下基础。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录