☰
urllib核心使用:urlopen、Request与User-Agent实战解析
2026/10/13 1:25:41 网站建设 项目流程

今天这篇笔记,是爬虫学习路线里一个非常关键的节点:urllib模块的基本使用。标题列出的urlopen、Request、User-Agent、Header这几个关键词,几乎就等于一次真实HTTP请求从发起到成功返回的完整链路。如果你已经跟到day09,你会发现前面几节一直在提"参考阅读",今天的内容就是把这些散落的点集中成一个完整的实践闭环。对于刚开始学爬虫的朋友,我建议别急着跳去用第三方库,先把urllib跑通。它虽然写起来啰嗦,但每一个请求细节都暴露在你面前,出了错你能直接看到是哪一步的问题。这篇文章会从模块结构、函数用法、Header构造,一直写到一个能跑的完整案例,最后附上我实际踩过的坑和排查顺序。

1. urllib模块到底是什么:先搞清它的位置再动手

1.1 它在Python网络请求生态里的定位

Python里发起HTTP请求的库很多,日常项目里最常用的是一个第三方库,写起来极其优雅。但我要说的是,那个库本质上就是从urllib封装出来的,底层干活的还是urllib。之所以学习阶段强烈建议先从urllib开始,是因为它能让你看清请求的"原始面貌":URL怎么构造、Header怎么传递、响应怎么读取、异常怎么抛出。用第三方库舒服是舒服,可一旦遇到403、UA校验、Header顺序不对这些问题,封装层把细节藏得太深,你很难定位。

urllib是Python标准库自带的,不需要pip安装任何额外包,只要机器上有Python解释器就能跑。这一点对入门阶段非常友好。有些公司内网环境装不了第三方包,或者机器离线,你想练爬虫,urllib几乎是唯一的选择。我最初就是在这样一台干净环境里,靠urllib把请求、响应、Header这些概念一个个啃下来的。

而且urllib还有一个特点:它是"标准库里的一个包",里面拆了若干子模块,各自负责不同职责。很多人只会用urllib.request,忽略了其他部分,结果后面碰到URL编码、异常处理时手忙脚乱。我下面把这几个子模块的职责讲清楚,你会发现它们各自都有固定使用场景。

1.2 四个核心子模块:别只盯着request

urllib包里包含四个子模块,我整理成一张表,方便对照记忆:

子模块职责实际使用场景
urllib.request发起请求、读取响应打开URL、下载页面、处理Request对象
urllib.error异常类型定义捕获URLError、HTTPError并做分支处理
urllib.parseURL解析与编码参数拼接、urlencode、quote中文
urllib.robotparserrobots.txt解析判断目标路径是否允许爬取

新手最容易忽略urllib.parse。我举个实际例子:你想请求一个搜索接口,query参数里带中文,比如"关键词=爬虫",直接把中文拼进URL里,服务端大概率识别不了,返回空数据或者直接报错。正确的做法是用urllib.parse.urlencode把参数编码成ASCII安全的格式,再拼到URL里。这一块在标题里虽然没直接出现,但它和urlopen、Request是配套使用的,后面实操案例里我会演示。

urllib.error也值得提前熟悉。它的核心是两个异常类:URLError和HTTPError。HTTPError其实是URLError的子类,专门对应服务端返回了4xx、5xx状态码的情况。学爬虫的过程里,这两个异常你会反复碰到,尤其是HTTPError里的code属性,能直接告诉我403、404、502这些具体状态码,排查问题时比笼统的URLError有用得多。

2. urlopen最简单但最基础:请求入口一步步拆开

2.1 三行代码发起第一次请求

urlopen是urllib.request里最基础、用得最多的函数。它的作用是给定一个URL,自动完成一次HTTP请求,然后返回一个响应对象。最简用法长这样:

from urllib.request import urlopen resp = urlopen("https://www.example.com") print(resp.status) # 200 page = resp.read() # bytes类型 print(page[:200])

我第一次跑这段代码时,觉得这也太简单了,比想象中的爬虫容易不少。但要注意,urlopen不是类,它是函数;返回的对象是http.client.HTTPResponse实例,这个对象上挂着status、headers、read()、getheaders()等方法和属性,后面解析内容全靠它。

urlopen默认发起的是GET请求,大多数练习场景够用了。但它能做的远不止GET,只是需要你额外传入参数。下面我逐个说明这些参数的作用和坑。

2.2 data传错,请求就变味了

urlopen的完整签名是:

urlopen(url, data=None, timeout=..., cafile=None, capath=None, cadefault=False, context=None)

重点看data和timeout。

data参数决定请求方式是GET还是POST:不传data,默认GET;传了data,就变成POST。但这里有个非常容易踩的坑:data必须是bytes类型,不能是字符串。很多新手直接写data="name=张三&age=18",结果报TypeError。正确做法是先encode成字节:

from urllib.request import urlopen post_data = "name=张三&age=18".encode("utf-8") resp = urlopen("https://httpbin.org/post", data=post_data)

timeout参数设置超时时间,单位是秒。不设的话,在某些网络环境里请求可能一直挂住不返回,程序就像死了一样。我建议所有urlopen调用都显式传timeout,哪怕设个10秒,也比无限等待强。这个参数在排查"卡住不动"的问题时起着决定性作用。

2.3 响应对象常用方法速查

urlopen返回的响应对象,我把它常用成员列成表:

方法/属性作用注意事项
resp.read()读取响应体全部内容返回bytes,需要decode
resp.read(n)分块读取n字节适合大文件下载
resp.statusHTTP状态码200正常,403被拒
resp.getheaders()获取所有响应头返回[(key, value), ...]列表
resp.getheader(name)获取单个响应头不存在返回None
resp.url最终请求的URL重定向后与原始URL不同

实际爬取时的标准操作链是:read()拿内容 -> decode()成字符串 -> 再交给正则或解析库提取数据。有一点要提醒:对于超大响应体,read()一次性读全部会占用大量内存。如果目标接口返回几十MB的JSON,建议用resp.read(1024 * 64)循环读,边读边处理或直接写文件。

3. Request与User-Agent:身份伪装从这一步开始

3.1 为什么裸请求会被403拒绝

如果你只写urlopen("https://某个网站"),有很大概率会收到403 Forbidden。原因其实不复杂:很多网站的安全策略会检查User-Agent字段,这个字段用来标识"你的客户端是谁"。浏览器的UA是一个长长的字符串,而urllib默认的UA非常简单直接:

User-Agent: Python-urllib/3.12

网站看到这个UA,基本就能判断你不是真实用户,而是脚本在访问。尤其是一些有反爬意识的站点,遇到Python-urllib这个标识,直接拒绝都是轻的,严重的还会临时封锁IP。解决思路很简单:把UA改成浏览器模样。但改UA不能随便改,需要借助Request对象,在请求发出之前把Header构造好。

3.2 Request对象的正确用法

urlopen负责"发",Request负责"怎么发"。基本套路是:先构造Request,再把它交给urlopen,不能反过来。示例:

from urllib.request import Request, urlopen url = "https://www.example.com" 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" } req = Request(url, headers=headers) resp = urlopen(req, timeout=10)

Request构造函数里除了url和headers,还有几个参数值得提前摸清:

  • data:POST数据,规则和urlopen一致,传了就会变成POST请求。
  • method:显式指定请求方法,比如method="PUT"、method="DELETE"。
  • origin_req_host:请求方主机名,日常几乎用不到。

还有一个细节:headers字典里的值必须是字符串。如果你写"User-Agent": 123,或者某个值是布尔值True,Python会直接报错。这个坑我见过好几个朋友踩过,把数字改成字符串就好了。

3.3 UA选择是有讲究的

UA并不是随便复制一个浏览器串就完事,不同场景有不同选择。我按实战经验整理了几个方向:

  • 桌面Chrome UA:最通用,适合绝大多数业务网站的默认校验。
  • 移动端UA:如果目标网站会自动跳转到手机版页面,或者数据接口是给App用的,用移动UA更合适。
  • 搜索引擎爬虫UA:比如某些搜索引擎的爬虫标识,在部分站点上权限很高,但滥用容易被拉黑,不建议练习时使用。
  • 留空UA:极少数接口不校验,但留空反而可能触发异常检测,不建议主动这样干。

我的习惯是:练习阶段统一用最新的桌面Chrome UA。不用急着堆一大堆Header,先保证UA这关过了再说。Header加得越多越像真人这个想法,在简单网站上是没问题的,但遇到高级风控,反而会因为Header组合不自然被识别。

4. 添加更多Header信息:请求头的完整拆解

4.1 常见请求头到底各自管什么

Header就是请求的"属性标签",服务器靠它判断你是谁、你想干什么、你能接受什么格式。接触爬虫后,你会遇到这些常见字段:

Header名作用常见问题
User-Agent客户端身份标识不设会暴露脚本身份
Referer来源页面缺少时详情页可能403
Cookie会话凭证登录态失效会跳回登录页
Accept可接受的内容类型不设可能返回非预期格式
Accept-Language可接受的语言缺省会返回默认语言
Content-TypePOST请求体格式不对会报参数错误
X-Requested-With是否为Ajax请求部分XHR接口校验

很多新手加了一个UA还是被拒,于是开始怀疑IP被封。实际上,更常见的原因是Referer或Cookie缺失。举个例子:某个详情页会校验Referer必须来自站内列表页,如果你直接访问详情页地址,就会返回403;把Referer加上之后,马上就正常了。所以遇到403,先按顺序排查Header,别一上来就背锅到IP上。

4.2 两种添加Header的方式与使用时机

添加Header有两种写法,都建议掌握。

第一种:构造Request时直接传headers字典。适合一次性固定Header。

req = Request(url, headers=headers)

第二种:先创建Request对象,再用add_header()逐个添加。适合需要动态构造请求头的情况,比如上一次请求的响应Cookie,要作为下一次请求的Header使用。

from urllib.request import Request req = Request(url) req.add_header("User-Agent", "Mozilla/5.0 ...") req.add_header("Referer", "https://www.example.com/") req.add_header("Cookie", "session=abcdef123456")

调试时还能配合get_header(name)和has_header(name)来确认请求头是否生效。我在排查问题的时候,习惯把最终请求的Header打印出来,逐项和浏览器里的请求对比,往往一眼就能看出差别在哪。

4.3 三个必须加Header的实际场景

第一个场景:模拟登录状态。很多数据需要登录后才能看。你可以先用浏览器手动登录,再从F12的Network面板里复制Cookie字符串,放到请求头里,拿到的数据就是登录态下的数据。这个方法练习阶段特别好用,缺点是Cookie有过期时间,过期后要重新复制。

第二个场景:模拟Ajax请求。现在很多网页的数据不是直接写在HTML里的,而是通过异步接口加载。直接抓HTML只能拿到页面壳子,拿不到数据。这时候要去找XHR接口,这些接口通常会对X-Requested-With字段做校验,加上值为XMLHttpRequest的头,成功率会明显提升。

第三个场景:POST请求指定Content-Type。POST提交表单时,Content-Type决定了服务器怎么解析你的请求体。普通表单用application/x-www-form-urlencoded,JSON接口用application/json。我遇到过不少朋友参数明明传得很完整,却一直报"参数错误",其实就是Content-Type没对。这种问题从报错里很难看出来,得靠逐项排查。

5. 实操过程:一个完整的练习案例带你跑通全流程

5.1 案例目标与预先准备

为了把上面这些点串起来,我准备了一个完整练习案例:抓取某公开信息页面的列表数据。目标页面的URL我统一用https://www.example.com/list代替。这个例子不涉及复杂的验证码、JS渲染,但完整覆盖了urllib的核心流程:构造带Header的Request -> 发起请求 -> 处理异常 -> 读取解码 -> 提取数据 -> 保存文件。

做这个练习前,你只需要准备一个Python环境,不需要安装任何第三方库。如果机器上有Python 3.8以上版本,直接运行即可。

5.2 完整代码与逐步讲解

下面是一份可以直接改URL后运行的代码:

import time import re from urllib.request import Request, urlopen from urllib.error import URLError url = "https://www.example.com/list" 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", "Referer": "https://www.example.com/", "Accept-Language": "zh-CN,zh;q=0.9", } req = Request(url, headers=headers) try: resp = urlopen(req, timeout=10) print("状态码:", resp.status) # 查看响应头里是否有charset content_type = resp.getheader("Content-Type") print("Content-Type:", content_type) page_bytes = resp.read() # 先用utf-8解码,失败时忽略非法字节 content = page_bytes.decode("utf-8", errors="ignore") print("页面长度:", len(content)) # 提取所有<h2>标题,仅用于演示 titles = re.findall(r"<h2>(.*?)</h2>", content) print("标题数量:", len(titles)) for t in titles[:10]: print("-", t.strip()) # 保存原始页面,方便离线分析 with open("page.html", "w", encoding="utf-8") as f: f.write(content) except URLError as e: print("请求失败:", e.reason)

代码里几个细节需要单独说明:

  • decode时用errors="ignore"是保底策略,防止页面混入非法字节时整个解码崩溃。
  • 正则提取

    只是为了演示"拿到内容之后怎么处理"。真实项目中我会用专门的HTML解析库,但不影响理解流程。

  • 请求同一个站点时,循环抓取务必加上时间间隔,别一股脑高频请求。这是爬虫练习里最基本的礼貌,也是保护自己的方式。

5.3 编码问题排查思路

中文网站的页面编码,最常见的是utf-8和gbk/gb2312两种。解码方式不对,页面会出现典型的乱码。排查编码的思路如下:

第一步,看响应头里的Content-Type是否带着charset:

content_type = resp.getheader("Content-Type") print(content_type)

如果带了charset就直接用。没有的话,进入到第二步:看HTML源码里的meta标签声明,例如 。第三步,如果前两步都找不到,我才会用chardet这类检测库。

关于chardet,它不一定默认安装,而且检测大文件时有一定耗时。我的经验是,先手动查响应头和meta标签,能解决80%的编码问题。实在不行再上检测工具。

6. 常见问题与排查技巧:爬虫练习阶段必看避坑清单

6.1 403问题的排查顺序

遇到403,先别急着堆Header。我推荐的排查顺序是:

  1. 用浏览器打开目标URL,确认页面确实能正常访问。
  2. 按F12进入Network面板,找到该请求,复制请求头里的关键字段。
  3. 对照自己代码里的Header,找出差异,重点关注User-Agent、Referer、Accept-Language。
  4. 加上缺失的Header后重试。如果仍然403,再考虑Cookie是否缺失。
  5. 以上都试过还是403,才去考虑IP被限制的问题。

很多时候403的秘密就在Referer里。比如你直接访问某个图片链接会403,加上Referer指向那个页面的地址后就正常了,这是防盗链机制在起作用。遇到403别慌,一步步对比就完了。

6.2 urlopen卡住不动怎么办

urlopen长时间不返回,通常是timeout参数没有设置。默认情况下,它会一直等待服务器响应,而有些服务器对爬虫请求会故意不返回任何内容,把你的程序活活挂住。

解决方式很简单:

resp = urlopen(req, timeout=10)

如果单次超时还不够,就要加重试逻辑:

import time from urllib.error import URLError for attempt in range(3): try: resp = urlopen(req, timeout=10) break except URLError: if attempt == 2: raise time.sleep(2)

有一点要提醒:URLError的父类是OSError,直接用OSError也能捕获,但建议用urllib.error.URLError,这样更精确,不会误吞其他IO异常。

6.3 状态码200但内容不对的陷阱

状态码200不代表拿到的就是你要的数据。这种情况我遇到过太多次了,返回内容可能是:

  • "请开启JavaScript后访问"的提示页。
  • 登录跳转页。
  • 验证码页。
  • JSON里的错误提示,比如{"code": -1, "msg": "param error"}。

排查方法就一句话:把resp.read()得到的内容先打印前几百个字符,肉眼看看到底是什么。我看到很多新手一拿到200就闷头解析,跑完整个流程才发现整个页面都是验证码。200这个数字会给人一种"成功"的错觉,但真正成功的标志是内容符合预期。

另外,有些服务器会对响应内容做gzip压缩。直接read()出来的是压缩后的乱码字节。这时候要检查响应头里的Content-Encoding是不是gzip,如果是,就需要用gzip.decompress()手动解压。在urllib里这个操作不会自动完成,需要自己处理。

6.4 下载与保存的工程化细节

练习阶段保存小页面很简单,直接写文件就行。但实际工程里有两个细节值得记住。

第一,文件名要有唯一标识。多次运行同一个脚本时,固定文件名会把上一次的结果覆盖掉。加上时间戳或序号,能保留每次运行的原始数据,调试时非常有用。

第二,下载大文件时分块写,别一次性read()进内存。改成分块读取:

with open("bigfile.zip", "wb") as f: while True: chunk = resp.read(1024 * 64) if not chunk: break f.write(chunk)

这样即使下载中途程序出错,已经写入的部分还在,不至于全部重来。

6.5 其他容易踩的小坑汇总

最后整理几个我在练习过程中踩过、身边的朋友也踩过的细节坑,一次性列给大家:

  • URL必须以http://或https://开头。少了协议头,会报"Unknown url type"。
  • Request的headers字典值必须是字符串。数字、布尔值都会导致异常。
  • Windows终端下直接print中文可能报Unicode编码错误,建议先decode再print,并且把终端编码切到utf-8。
  • 响应内容如果特别大,print时别一次性全部打印,用切片打印前几百字符就够了。
  • 练习爬虫时注意请求频率。我建议每请求一次sleep一小段时间,比如1到2秒,给服务器留出处理空间,同时降低自己的请求被识别为恶性访问的概率。

就我这几天的学习体验来说,urllib确实比第三方库啰嗦,但这种啰嗦恰恰是它的教学价值所在。它逼着你把每一个请求细节都搞清楚:UA从哪来、Referer为什么重要、Content-Type什么时候该设置、异常怎么分层处理。我后来切换到第三方库时,明显感觉自己不是"会调用接口",而是"知道底层每一步在干什么",排查问题的思路清晰了很多。如果你也刚开始学爬虫,别急着跳过urllib。标题里提到的这些点,挨个跑一遍,跑通了,后面的路会顺很多。今天先到这儿,下一篇接着写urllib.parse处理URL参数的部分,再把异常处理的闭环补完整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询