Python 爬虫项目实战:urllib 请求封装与网页编码异常处理
2026/6/6 17:51:52 网站建设 项目流程

前言

在 Python 爬虫技术体系中,requests 库属于第三方封装优化后的 HTTP 请求组件,而 urllib 是 Python 标准库内置原生网络请求模块,无需额外安装即可直接调用,是 Python 语言原生爬虫的底层实现载体。相较于 requests 高度封装的调用逻辑,urllib 拆分 url 请求发送、参数编码、异常捕获、响应读取等多个独立子模块,能够直观展现 HTTP 请求底层执行逻辑,在无权限安装第三方依赖的受限服务器环境、轻量化简易爬虫开发场景中具备不可替代的实用价值。网页编码错乱是原生 urllib 开发过程中的高频故障,中文站点存在 utf-8、gbk、gb2312、gb18030 多类编码格式,原生 urllib 缺少自动编码识别机制,极易出现页面正文、标题乱码问题。

本文围绕 urllib 四大子模块分层拆解底层原理,从请求头封装、GET/POST 参数编码、自定义请求类封装、多编码异常排查与修复、异常捕获全维度落地实战,结合真实网页抓取案例完成源码获取、编码校正、文本提取、本地 TXT/CSV 持久化存储全流程闭环,补齐原生爬虫底层请求技术栈。

本文配套官方参考资源链接:

  1. Python urllib 标准库官方文档:urllib 四大子模块原生 API 权威说明
  2. Python codecs 编码模块文档:字符编码解码底层接口参考
  3. csv 内置模块官方文档:结构化数据落地存储语法规范

一、urllib 模块架构与环境说明

1.1 模块组成划分

Python3 版本 urllib 拆分为四个功能独立子模块,各司其职完成网络请求全流程操作,模块功能汇总如下表:

表格

子模块名称核心功能爬虫使用场景
urllib.request构造请求对象、发送 HTTP/HTTPS 请求、获取服务器响应爬虫主体发起页面访问,最核心模块
urllib.parseURL 拼接、参数 url 编码、特殊字符转义GET 参数拼接、POST 表单数据转码
urllib.error捕获请求阶段各类异常(链接失效、服务器拒绝、访问超时)爬虫容错处理,避免程序崩溃
urllib.robotparser解析站点 robots.txt 协议规则合规爬虫开发,校验目标站点抓取权限

urllib 为 Python 内置标准库,随 Python3 解释器预装,无需执行 pip 安装命令,代码中直接通过 import 导入对应子模块即可启用。

1.2 urllib 与 requests 核心底层差异

requests 基于 urllib.request 进行二次封装优化,二者底层同源但上层调用逻辑区别显著,结合爬虫开发场景对比:

  1. urllib:原生无默认请求头,必须手动构造 Request 对象添加 UA;无自动编码识别,需开发者手动指定解码格式;POST 表单需手动 url 编码,底层可控性强,代码冗余度更高。
  2. 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())
代码原理剖析
  1. urlopen()底层封装 socket 套接字通信逻辑,建立客户端与目标服务器 TCP 连接,完成报文收发;
  2. resp.read()获取的是 bytes 二进制数据流,网页传输统一使用二进制字节流,必须通过指定字符集 decode 转为字符串;
  3. 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))
代码原理剖析
  1. 类初始化统一配置 UA 与超时,后续所有请求自动复用配置,无需重复书写 headers;
  2. get_html、post_html 拆分两种请求逻辑,入参支持自定义编码格式,适配多编码站点;
  3. 内置异常捕获,单次链接异常不会中断整体爬虫任务。

四、网页编码异常成因与系统化解决方案

4.1 中文网页主流编码格式说明

国内资讯、科普站点常用四类字符编码,也是乱码问题的源头:

  1. utf-8:国际化通用编码,绝大部分新开发站点使用;
  2. gbk:简体中文专用编码,早期门户网站、地方站点高频使用;
  3. gb2312:gbk 子集,仅收录基础简体汉字,老旧静态网页居多;
  4. 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)
代码原理剖析
  1. 全流程封装至类中,抓取、解析、存储模块化拆分,便于后期新增代理 IP、重试机制;
  2. 页面源码自动识别编码,兼容 gbk/utf-8 混合编码站点,从根源杜绝乱码;
  3. 每页抓取后 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 提升迭代效率。

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

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

立即咨询