恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python爬虫原理与基本请求库urllib详解
首页
资讯中心
/
Python爬虫原理与基本请求库urllib详解
Python爬虫原理与基本请求库urllib详解
发布时间:2026/10/17 11:25:02
前言「爬虫原理」这四个字经常被讲得玄乎好像有什么特殊机制。其实剥到最里面爬虫就是一件很朴素的事程序代替浏览器发一次 HTTP 请求把响应体取回来再从里面挑出你要的数据。所谓「反爬」绝大多数时候就是服务器识别出这次请求不像人类浏览器发的于是拒绝服务或者返回假数据。要讲清原理就得先把一次 HTTP 请求响应拆开看请求由哪几行组成、响应里哪些信息决定了你的程序该怎么处理。然后把urllib放进标准库的坐标系里看看它到底站在哪一层——这一点很多教程含混带过导致读者分不清urllib、urllib3、requests的关系甚至以为requests是urllib的封装。本文讲机制与定位不重复基础 API 的逐个演示。请求怎么发、参数怎么编码那类操作层面的内容属于另一篇。本文以 Python 3 为基准urllib2是 Python 2 的模块2.7 已于2020 年 1 月 1 日停止维护PEP 373Python 3 把它并进了urllib.request文中一律按 Python 3 写。一、一次 HTTP 请求响应到底发生了什么客户端发出去的报文分成三部分请求行request line、请求头header、请求体body。请求行的格式是「方法 目标路径 协议版本」请求头是一堆Key: Value请求体只在 POST、PUT 这类方法里才有。服务器回来的报文同样是三部分状态行、响应头、响应体。状态行由协议版本、状态码、原因短语组成。把urllib构造出来的请求「翻译」成这三大块是理解它的最快方式。Request对象上正好有对应的属性# 适用于 Python 3.8import urllib.requestreq urllib.request.Request(https://example.com/search?qpython,databa1b2,headers{User-Agent: my-crawler/1.0 (contact: meexample.com)},)print(请求行:, req.get_method(), req.full_url)print(方法:, req.method) # 传了 data 且没指定 method默认 POSTprint(请求体:, req.data) # ba1b2print(请求头:)for name, value in req.header_items():print( , name, , value)这段代码的价值在于它把「一次请求」从抽象概念变成了可打印的对象。上面header_items()一般会打印出User-agent和Content-type两项后者因为data不为None而被自动补成表单编码类型Host、Content-Length这类头则要等到真正发给服务器前才由底层补上。响应那一侧urlopen()返回的对象上有# 适用于 Python 3.8import urllib.requestwith urllib.request.urlopen(https://example.com/, timeout10) as resp:print(状态行:, resp.status, resp.reason)print(响应头示例:, resp.headers.get(Content-Type))print(最终 URL:, resp.geturl())body resp.read()print(响应体字节数:, len(body))resp.geturl()返回的是最终URL。如果中途发生过重定向它和请求时写的 URL 会不一样这正是下一节要讲的机制留下的痕迹。二、状态码与重定向状态码的第一位决定了它的类别区间含义爬虫里的典型处理2xx成功正常解析响应体3xx重定向看Location头跳到新地址4xx客户端错误404 是路径错了403 通常是对方拒绝你5xx服务端错误一般稍后重试且要退避urllib的urlopen()默认会自动跟随重定向。这件事由HTTPRedirectHandler这个 handler 负责它是build_opener()默认安装的 handler 之一默认清单还包括ProxyHandler、HTTPDefaultErrorHandler、HTTPErrorProcessor等。也就是说你请求一个返回 302 的地址urlopen会自己跳到Location指向的新地址最后拿到 200 的结果——resp.geturl()显示的就是跳转后的地址。但有一个例外必须记住只要最终状态码是 4xx 或 5xxurlopen不会返回响应对象而是直接抛HTTPError异常。这个异常由HTTPDefaultErrorHandler在没有任何其他 handler 处理时抛出。理解这一点后try/except的结构就顺理成章了——本小节的代码只做示意异常的完整体系留给进阶那篇# 适用于 Python 3.8import urllib.errorimport urllib.requesttry:resp urllib.request.urlopen(https://example.com/reports, timeout10)except urllib.error.HTTPError as exc:# exc.code 是状态码HTTPError 也能当响应对象读正文print(服务端返回错误状态:, exc.code, exc.reason)else:print(请求成功状态行是:, resp.status, resp.reason)resp.close()注意这里用的是try / except / else结构else里的代码只在没有异常时执行把「成功分支」和「异常分支」分得很干净。记住结论就够了——状态码 ≥ 400 时urlopen不返回响应对象而是抛异常所以「解析响应」的代码天然只会在成功路径上执行。三、urllib 在标准库里的定位先把容易混的三个名字摆清楚它们不是同一个东西名字来自哪里装不装一句话定位urllibPython 标准库不用装官方自带的高层 HTTP 客户端包urllib3第三方 PyPI 包要装更底层的连接池与重试实现requests第三方 PyPI 包要装在urllib3之上做人机友好的封装urllib本身是一个包内部按职责拆成子模块urllib.request管发请求、urllib.parse管 URL 解析与编码、urllib.error管异常、urllib.robotparser管 robots.txt。这种拆法决定了你写代码时至少要import两个模块才能完成一次带参数的请求。从层次上说urllib.request.urlopen()只是一个便捷入口。它内部做的事情是拿到或创建一个OpenerDirector让这个 director 依次调用一串注册好的 handler由 handler 分别负责代理、重定向、Cookie、错误处理最后把结果返回给你。所以urlopen不是「一个函数包打天下」而是「一个默认配置好的 handler 流水线」。这个设计的好处是你可以自己替换流水线里的环节比如换成用环境变量以外的方式指定代理代价是入门时要先理解 handler 这个概念。这也是它比requests啰嗦的根本原因。四、urllib 与 requests 是什么关系网上流传很广的一句话是「requests是对urllib的封装」。这句不准确。requests的 README 和文档里从来没有把urllib列为它的底层它做 HTTP 传输实际依赖的是第三方库urllib3再往上做会话、Cookie、认证、编码猜测这些便利功能。名字里都有url只是巧合urllib3与标准库的urllib是两个独立项目。它们真正的差别在定位维度urllibrequests来源标准库零依赖第三方需pip install返回内容bytes解码自己来text自动解码content给原始字节参数编码自己urlencodeparams自动处理连接复用需要自己搭 openerSession一行搞定默认重定向自动跟随自动跟随head默认关闭适用场景交付环境不能装包、轻量脚本日常开发、功能要求全选型建议很朴素能装第三方库就用requests不能装就用urllib。两者学一个会一个底层的 HTTP 知识是共用的——毕竟无论用哪个库发出去的请求头、回来的状态码都是一样的。五、原理之外的合规底线明白了原理反而更要克制正因为知道一次请求有多轻才容易写出循环几千次把别人站点压垮的脚本。无论用urllib还是requests都请遵守先读目标站的 robots.txt标准库的urllib.robotparser.RobotFileParser提供了can_fetch()和crawl_delay()用可识别的真实User-Agent写明用途与联系方式请求之间限速遇到 5xx 时指数退避而不是加倍猛冲只取公开页面不绕过登录墙、付费墙、验证码不为了「快」而并发压站。这些不是技术限制是使用者的选择。本文不提供任何绕过反爬的手法——那类内容只会让守规矩的人一起遭殃。常见坑点1. 以为urlopen在 404 时返回响应对象。❌resp urlopen(url); print(resp.status)直接写遇到 404 直接抛异常中断。 ✅ 用try/except urllib.error.HTTPError包住且注意HTTPError本身也能.read()。2. 把 3xx 当成失败。❌ 看到 301、302 就以为请求出错直接放弃或报错 —— 其实那只是「资源搬家了」。 ✅ 3xx 属于重定向urllib默认会自动跟随关心最终落点就去看resp.geturl()。3. 以为requests的底层是标准库urllib。❌ 「requests就是urllib的封装所以装机不用联网」——概念错了且它仍需安装。 ✅requests依赖的是第三方urllib3与标准库urllib是两个项目。4. 拿resp.read()的结果直接当文本。❌print(resp.read()[0])期望看到首字符实际看到的是字节的十进制值。 ✅ 先.decode(charset, errorsreplace)。5. 忘了重定向会改变最终地址。❌ 用请求时的 URL 去拼相对链接结果全拼错。 ✅ 用resp.geturl()拿最终地址再基于它做urljoin。6. 不带 timeout。❌urlopen(url)—— 对端半死不活时脚本长时间不动。 ✅urlopen(url, timeout10)并把超时当成可重试的正常情况处理。7. 用 Python 2 的写法。❌import urllib2或print resp.read()—— 前者在 3 里不存在后者是 2 的语句语法。 ✅import urllib.requestprint(resp.read())2.7 已于 2020-01-01 停止维护。8. 只盯状态码不看响应头。❌ 拿到 200 就直接解析忽略Content-Encoding: gzip导致解析出乱码。 ✅ 先看Content-Encoding官方文档给出的示例就是判断到 gzip 时用gzip.decompress()解压再解码。总结概念要点一次请求请求行 请求头 请求体一次响应状态行 响应头 响应体2xx / 3xx / 4xx / 5xx成功 / 重定向 / 客户端错 / 服务端错重定向urllib默认自动跟随由HTTPRedirectHandler负责状态码 ≥ 400urlopen抛HTTPError不返回响应对象urllib结构包含 request / parse / error / robotparser层级urlopen是 OpenerDirector handler 链的便捷入口与requests无封装关系requests依赖第三方urllib3把「一次请求响应」这件小事弄明白urllib的种种设计就都有了解释为什么它要分那么多子模块、为什么要引入 handler、为什么异常要分两层。原理清楚之后换任何一个 HTTP 库都只是换语法不换脑子。