前言
在 Python 爬虫技术体系中,requests 库属于第三方封装优化后的 HTTP 请求组件,而 urllib 是 Python 标准库内置原生网络请求模块,无需额外安装即可直接调用,是 Python 语言原生爬虫的底层实现载体。相较于 requests 高度封装的调用逻辑,urllib 拆分 url 请求发送、参数编码、异常捕获、响应读取等多个独立子模块,能够直观展现 HTTP 请求底层执行逻辑,在无权限安装第三方依赖的受限服务器环境、轻量化简易爬虫开发场景中具备不可替代的实用价值。网页编码错乱是原生 urllib 开发过程中的高频故障,中文站点存在 utf-8、gbk、gb2312、gb18030 多类编码格式,原生 urllib 缺少自动编码识别机制,极易出现页面正文、标题乱码问题。
本文围绕 urllib 四大子模块分层拆解底层原理,从请求头封装、GET/POST 参数编码、自定义请求类封装、多编码异常排查与修复、异常捕获全维度落地实战,结合真实网页抓取案例完成源码获取、编码校正、文本提取、本地 TXT/CSV 持久化存储全流程闭环,补齐原生爬虫底层请求技术栈。
本文配套官方参考资源链接:
- Python urllib 标准库官方文档:urllib 四大子模块原生 API 权威说明
- Python codecs 编码模块文档:字符编码解码底层接口参考
- csv 内置模块官方文档:结构化数据落地存储语法规范
一、urllib 模块架构与环境说明
1.1 模块组成划分
Python3 版本 urllib 拆分为四个功能独立子模块,各司其职完成网络请求全流程操作,模块功能汇总如下表:
表格
| 子模块名称 | 核心功能 | 爬虫使用场景 |
|---|---|---|
| urllib.request | 构造请求对象、发送 HTTP/HTTPS 请求、获取服务器响应 | 爬虫主体发起页面访问,最核心模块 |
| urllib.parse | URL 拼接、参数 url 编码、特殊字符转义 | GET 参数拼接、POST 表单数据转码 |
| urllib.error | 捕获请求阶段各类异常(链接失效、服务器拒绝、访问超时) | 爬虫容错处理,避免程序崩溃 |
| urllib.robotparser | 解析站点 robots.txt 协议规则 | 合规爬虫开发,校验目标站点抓取权限 |
urllib 为 Python 内置标准库,随 Python3 解释器预装,无需执行 pip 安装命令,代码中直接通过 import 导入对应子模块即可启用。
1.2 urllib 与 requests 核心底层差异
requests 基于 urllib.request 进行二次封装优化,二者底层同源但上层调用逻辑区别显著,结合爬虫开发场景对比:
- urllib:原生无默认请求头,必须手动构造 Request 对象添加 UA;无自动编码识别,需开发者手动指定解码格式;POST 表单需手动 url 编码,底层可控性强,代码冗余度更高。
- requests:内置默认 UA 标识,自动识别页面编码,表单参数传入字典即可自动编码,封装度高、开发效率更快,依赖第三方安装。
受限环境无法安装 requests 时,urllib 是唯一原生请求方案,也是理解 HTTP 请求底层细节的必备学习内容。
二、urllib.request 基础请求语法实战
2.1 最简无请求头 GET 请求
urllib.request.urlopen 为基础请求方法,可直接传入 URL 完成简单页面访问,但缺少浏览器标识极易被站点拦截返回 403 错误:
python
运行
from urllib import request # 目标测试网址 url = "https://www.baidu.com" # 发起基础请求 resp = request.urlopen(url, timeout=5) # 读取二进制原始响应数据 raw_byte = resp.read() # 手动指定编码解码 html = raw_byte.decode("utf-8") # 打印响应状态码 print("响应状态码:", resp.getcode())代码原理剖析
urlopen()底层封装 socket 套接字通信逻辑,建立客户端与目标服务器 TCP 连接,完成报文收发;resp.read()获取的是 bytes 二进制数据流,网页传输统一使用二进制字节流,必须通过指定字符集 decode 转为字符串;getcode()方法获取 HTTP 响应状态码,200 代表请求正常,4xx/5xx 标识异常访问。
2.2 Request 对象封装请求头(反爬基础配置)
原生 urlopen 无法直接自定义 headers,需要实例化 urllib.request.Request 对象,在构造参数中传入请求头字典,模拟浏览器客户端身份,规避基础 UA 校验反爬:
python
运行
from urllib import request url = "https://www.baidu.com" # 自定义请求头,模拟Chrome浏览器 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } # 封装请求对象 req = request.Request(url=url, headers=headers) # 传入请求对象发起访问 resp = request.urlopen(req, timeout=6) # 二进制数据解码 html = resp.read().decode("utf-8", errors="ignore") print("页面源码长度:", len(html))代码原理剖析
Request 对象作为请求报文封装载体,内部存储请求地址、请求头、请求体、请求方式等报文信息,urlopen 读取该对象参数组装完整 HTTP 报文发送至服务端;errors="ignore"参数用于忽略无法解码的异常字节,防止解码阶段程序抛出异常终止运行。
2.3 GET 请求动态参数拼接(urllib.parse 编码)
当 URL 附带中文、特殊符号查询参数时,直接拼接会出现 URL 编码异常,必须使用 urllib.parse.quote_plus 对参数字典进行 URL 编码转换,再拼接至主 URL 尾部:
python
运行
from urllib import request, parse base_url = "https://search.demo.com/search" # 搜索参数字典 param_dict = { "keyword": "宇宙黑洞科普", "page": 1 } # 字典参数url编码拼接 query_str = parse.urlencode(param_dict) full_url = base_url + "?" + query_str # 构造请求 headers = {"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36"} req = request.Request(full_url, headers=headers) resp = request.urlopen(req) html = resp.read().decode("utf-8")原理剖析
urlencode()自动将中文、空格、特殊符号转为 URL 标准百分号编码格式,例如 “宇宙黑洞科普” 被转译为%E5%AE%87%E5%AE%87%E9%BB%91%E6%B4%9E%E7%A7%91%E6%99%AE,符合 HTTP URL 传输编码规范,规避非法字符造成请求失败。
2.4 POST 表单数据提交实战
POST 请求数据放置在请求体中,不在 URL 上拼接,表单数据同样需要通过 urlencode 编码转为字节格式,传入 Request 对象 data 参数:
python
运行
from urllib import request, parse post_url = "https://demo-form.com/login" # 表单提交数据 form_data = { "username": "demo_user", "password": "123456" } # 表单编码+转bytes post_body = parse.urlencode(form_data).encode("utf-8") headers = {"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36"} # data参数携带POST请求体 req = request.Request(post_url, data=post_body, headers=headers, method="POST") resp = request.urlopen(req) result = resp.read().decode("utf-8") print("接口返回结果:", result)三、urllib 请求类封装(工程化自定义爬虫请求工具)
原生零散调用 Request、urlopen 不利于批量分页爬虫开发,面向对象封装通用请求类,统一管理请求头、超时时间、基础域名,实现代码复用,是 urllib 工程开发标准写法:
python
运行
from urllib import request, parse from urllib.error import URLError, HTTPError class UrllibCrawl: def __init__(self): # 初始化全局请求头 self.base_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9" } self.timeout = 6 def get_html(self, target_url, param=None, encode="utf-8"): """通用GET请求方法,支持动态参数、自定义解码格式""" if param: query = parse.urlencode(param) target_url = target_url + "?" + query req = request.Request(url=target_url, headers=self.base_headers) try: resp = request.urlopen(req, timeout=self.timeout) byte_data = resp.read() # 统一解码返回页面字符串 html = byte_data.decode(encode, errors="ignore") return html except (URLError, HTTPError) as e: print(f"GET请求异常:{str(e)}") return "" def post_html(self, target_url, form_data, encode="utf-8"): """通用POST请求封装""" post_body = parse.urlencode(form_data).encode(encode) req = request.Request(target_url, data=post_body, headers=self.base_headers, method="POST") try: resp = request.urlopen(req, timeout=self.timeout) return resp.read().decode(encode, errors="ignore") except Exception as e: print(f"POST请求异常:{str(e)}") return "" # 实例化调用 if __name__ == "__main__": crawl = UrllibCrawl() # 带参数GET抓取 params = {"keyword": "海洋生物科普", "page": 1} page_html = crawl.get_html("https://demo-kepu.com/list", param=params, encode="utf-8") print("页面源码长度:", len(page_html))代码原理剖析
- 类初始化统一配置 UA 与超时,后续所有请求自动复用配置,无需重复书写 headers;
- get_html、post_html 拆分两种请求逻辑,入参支持自定义编码格式,适配多编码站点;
- 内置异常捕获,单次链接异常不会中断整体爬虫任务。
四、网页编码异常成因与系统化解决方案
4.1 中文网页主流编码格式说明
国内资讯、科普站点常用四类字符编码,也是乱码问题的源头:
- utf-8:国际化通用编码,绝大部分新开发站点使用;
- gbk:简体中文专用编码,早期门户网站、地方站点高频使用;
- gb2312:gbk 子集,仅收录基础简体汉字,老旧静态网页居多;
- gb18030:gbk 扩展编码,兼容生僻汉字,部分政务、图书站点采用。
urllib 原生无自动探测编码逻辑,开发者错选解码字符集就会出现中文方框、问号乱码。
4.2 四种编码自动识别解决方案
方案 1:从响应头 content-type 字段提取编码
服务器响应头会标注 charset 字段,从中截取编码名称:
python
运行
from urllib import request resp = request.urlopen("https://demo-gbk-site.com") # 获取全部响应头字典 header_info = resp.info() content_type = header_info.get("Content-Type", "") # 截取charset后编码 if "charset=" in content_type: code = content_type.split("charset=")[-1].strip() else: code = "utf-8" html = resp.read().decode(code, errors="ignore")方案 2:chardet 第三方库自动字节探测编码
通过二进制页面数据自动分析最优解码格式,工程最常用方案,安装指令:
bash
运行
pip install chardet -i https://pypi.tuna.tsinghua.edu.cn/simple实战代码:
python
运行
import chardet from urllib import request resp = request.urlopen("https://demo-gbk-site.com") byte_raw = resp.read() # 探测编码结果 detect_res = chardet.detect(byte_raw) code_name = detect_res["encoding"] if detect_res["encoding"] else "utf-8" html = byte_raw.decode(code_name, errors="ignore")方案 3:cchardet 高性能探测(加速大批量网页编码识别)
cchardet 基于 C 语言开发,探测速率优于 chardet,海量分页爬虫优选;
方案 4:依次尝试多编码兜底解码
无探测库环境下,依次按 gbk、utf-8、gb2312、gb18030 尝试解码,捕获解码异常切换编码:
python
运行
def auto_decode(byte_data): code_list = ["utf-8", "gbk", "gb2312", "gb18030"] for code in code_list: try: return byte_data.decode(code) except UnicodeDecodeError: continue return byte_data.decode("utf-8", errors="ignore")4.3 集成自动编码至自定义爬虫类
将自动探测逻辑嵌入前文 UrllibCrawl 类,实现请求后自动适配编码,彻底解决乱码:
python
运行
import chardet from urllib import request, parse from urllib.error import URLError, HTTPError class UrllibCrawl: def __init__(self): self.base_headers = {"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36"} self.timeout = 6 def _auto_get_encode(self, byte_content): # 内置自动编码识别函数 res = chardet.detect(byte_content) return res["encoding"] if res["encoding"] else "utf-8" def get_html(self, target_url, param=None): if param: target_url = target_url + "?" + parse.urlencode(param) req = request.Request(target_url, headers=self.base_headers) try: resp = request.urlopen(req, timeout=self.timeout) byte_raw = resp.read() encode = self._auto_get_encode(byte_raw) html = byte_raw.decode(encode, errors="ignore") return html except (URLError, HTTPError): return ""五、urllib.error 异常分类与精细化捕获处理
urllib.error 拆分 URLError、HTTPError 两类异常,HTTPError 继承自 URLError,分别对应服务器 HTTP 状态异常与底层网络异常,异常明细汇总:
表格
| 异常类型 | 触发诱因 | 处理策略 |
|---|---|---|
| HTTPError | 服务器返回 404 页面不存在、403 禁止访问、500 服务器报错 | 通过 code 属性获取状态码,跳过失效链接 |
| URLError | 域名解析失败、断网、请求超时、SSL 证书错误 | 校验 URL 正确性、检查本地网络,添加重试逻辑 |
精细化捕获代码:
python
运行
from urllib import request from urllib.error import HTTPError, URLError url = "https://demo-error-site.com/nopage.html" headers = {"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36"} req = request.Request(url, headers=headers) try: resp = request.urlopen(req, timeout=5) except HTTPError as he: print(f"HTTP异常,状态码:{he.code},异常信息:{he.reason}") except URLError as ue: print(f"网络链接异常:{ue.reason}")六、完整综合项目:urllib 批量分页爬虫 + 编码自动处理 + 本地存储
整合自定义请求类、自动编码识别、正则 / 文本提取、CSV/TXT 存储,批量抓取科普栏目多页数据:
python
运行
import chardet, csv, time from urllib import request, parse from urllib.error import URLError, HTTPError class UrllibCrawl: def __init__(self): self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36"} self.timeout = 5 self.all_data = [] def get_encode(self, byte_data): det = chardet.detect(byte_data) return det["encoding"] if det["encoding"] else "utf-8" def fetch_page(self, url): req = request.Request(url, headers=self.headers) try: resp = request.urlopen(req, timeout=self.timeout) byte = resp.read() code = self.get_encode(byte) html = byte.decode(code, errors="ignore") return html except (URLError, HTTPError): return "" def parse_data(self, html): # 沿用前文正则提取标题+浏览数字 import re pat = re.compile(r'<div class="item">(.*?)\|阅读:(\d+)</div>', re.S) raw = pat.findall(html) res_list = [] for title, view in raw: clean_title = re.sub(r'\s|<.*?>', "", title).strip() res_list.append({"文章标题": clean_title, "浏览量": int(view)}) return res_list def batch_crawl(self, start_page, end_page, base_url): for page in range(start_page, end_page+1): page_url = base_url.format(page=page) print(f"正在抓取第{page}页:{page_url}") html = self.fetch_page(page_url) page_data = self.parse_data(html) if not page_data: print("无有效数据,终止分页") break self.all_data.extend(page_data) time.sleep(1) # 落地存储 self.save_file() def save_file(self): # CSV存储 with open("urllib科普数据.csv", "w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=["文章标题","浏览量"]) writer.writeheader() writer.writerows(self.all_data) # TXT备份 with open("urllib科普备份.txt", "w", encoding="utf-8") as f: f.write("标题|浏览量\n"+"-"*45+"\n") for item in self.all_data: f.write(f"{item['文章标题']}|{item['浏览量']}\n") print(f"数据保存完毕,总采集{len(self.all_data)}条") if __name__ == "__main__": crawl = UrllibCrawl() base = "https://demo-kepu.com/list?page={page}" crawl.batch_crawl(1,4,base)代码原理剖析
- 全流程封装至类中,抓取、解析、存储模块化拆分,便于后期新增代理 IP、重试机制;
- 页面源码自动识别编码,兼容 gbk/utf-8 混合编码站点,从根源杜绝乱码;
- 每页抓取后 sleep 休眠,降低请求频率规避 IP 封禁,符合爬虫合规访问规范。
七、进阶拓展:urllib 代理 IP 配置与 Cookie 处理
7.1 代理 IP 配置
目标站点单 IP 访问受限场景,通过 ProxyHandler 配置代理,切换出口 IP:
python
运行
from urllib import request # 配置代理 proxy_handler = request.ProxyHandler({"http":"127.0.0.1:7890", "https":"127.0.0.1:7890"}) opener = request.build_opener(proxy_handler) request.install_opener(opener) resp = request.urlopen("https://www.baidu.com")7.2 Cookie 保存与携带
使用 HTTPCookieProcessor 实现会话保持,适配需要登录鉴权的页面抓取,是模拟登录爬虫底层实现基础。
八、urllib 与 requests 选型总结
表格
| 工具 | 依赖 | 编码 | 请求效率 | 适用场景 |
|---|---|---|---|---|
| urllib | 内置无依赖 | 需手动处理编码 | 略低 | 受限服务器、底层原理学习、轻量化小爬虫 |
| requests | 第三方安装 | 自动编码识别 | 高 | 常规开发、项目快速落地、大规模爬虫 |
学习阶段优先掌握 urllib 吃透底层,工程开发优先选用 requests 提升迭代效率。