资讯动态

Python爬虫入门:urllib核心模块与反爬策略实战

发布时间:2026/10/16 1:01:06 来源:尧图企业网站定制
1. 从零理解爬虫为什么选择 urllib 作为起点很多人第一次接触 Python 爬虫脑子里冒出来的第一个念头就是“我要把某个网站的数据全抓下来”。这个想法很直接但真正动手的时候第一个拦路虎往往不是代码写不出来而是不知道该从哪个库开始。市面上有 requests、httpx、aiohttp、scrapy 一大堆选择为什么还要回头讲 urllib 这个看起来有点“老派”的标准库答案其实很简单urllib 是 Python 标准库自带的不需要额外安装任何东西。你只要装了 Python打开编辑器就能直接 import。这意味着你在任何一台机器上、任何一个干净的环境里都能立刻开始写爬虫逻辑不用先折腾 pip install不用担心中间某个依赖版本冲突。对于刚入门的人来说少一层环境配置的坑就多一分把注意力放在核心逻辑上的机会。更重要的是urllib 把 HTTP 请求的底层细节暴露得比较充分。你用 requests 的时候一个requests.get(url)就拿到响应了很爽但你也失去了理解请求头怎么拼、URL 怎么编码、cookie 怎么管理的机会。urllib 虽然写起来啰嗦一点但它逼着你去面对这些细节。等你把这些细节吃透了再去用 requests 或者 scrapy你会发现那些高级库帮你做的事情你心里一清二楚遇到问题也知道该往哪个方向排查。这篇文章面向的是有一定 Python 基础、想系统理解爬虫原理和反爬应对思路的读者。我会从 urllib 的核心模块讲起把请求构造、响应解析、异常处理、编码问题这些基础打牢然后重点展开反爬策略的识别与应对。文章里涉及的代码和思路都是我在实际项目中反复用过、踩过坑之后沉淀下来的你可以直接拿去改改就用。注意本文所有示例仅用于技术原理讲解实际抓取任何网站数据前请务必确认目标网站的服务条款和 robots.txt 规则遵守相关约定。2. urllib 核心模块拆解四个组件各司其职urllib 不是一个单一的模块而是一个包里面包含四个子模块request、parse、error、robotparser。很多人用了很久 urllib其实只用了request.urlopen()这一个函数其他三个模块基本没碰过。但恰恰是后面这几个模块在处理复杂场景时能帮你省下大量时间。2.1 request 模块请求的发起与响应的获取urllib.request是使用频率最高的部分。最基础的用法就是一行代码import urllib.request response urllib.request.urlopen(https://example.com) html response.read().decode(utf-8) print(html)这行代码背后发生的事情其实不少它帮你解析了 URL建立了 TCP 连接发送了 HTTP 请求接收了响应还把响应封装成了一个类似文件的对象。你可以用response.status看状态码用response.headers看响应头用response.read()读取响应体。但urlopen有个明显的局限它默认使用 GET 方法而且请求头非常简陋User-Agent 是类似Python-urllib/3.x这样的标识。很多网站一看到这个 User-Agent 就直接拒绝服务了。所以实际使用中我们几乎总是需要自己构造一个Request对象import urllib.request url https://example.com/api/data headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, Referer: https://example.com/page } req urllib.request.Request(url, headersheaders) response urllib.request.urlopen(req, timeout10)这里有几个关键点值得展开说。User-Agent 是反爬识别中最基础的字段默认的 Python-urllib 标识几乎等于在额头上写着“我是爬虫”。把它伪装成常见浏览器的 UA是最低成本的绕过手段。Referer 字段告诉服务器你是从哪个页面跳转过来的有些网站会校验这个字段如果缺失或者不匹配就会返回 403。timeout 参数一定要设否则遇到响应慢的服务器你的程序会一直卡在那里在生产环境里这是大忌。如果需要发送 POST 请求就需要用到data参数import urllib.request import urllib.parse import json payload {keyword: python, page: 1} data json.dumps(payload).encode(utf-8) req urllib.request.Request( https://example.com/api/search, datadata, headers{Content-Type: application/json, User-Agent: Mozilla/5.0} ) response urllib.request.urlopen(req)注意data必须是 bytes 类型不能直接传字符串。如果是表单格式还需要用urllib.parse.urlencode()先编码。2.2 parse 模块URL 编码与参数拼接的利器urllib.parse这个模块看起来不起眼但在实际爬虫开发中出场率极高。最常用的两个函数是quote()和urlencode()。先说quote()。URL 里不能直接出现中文、空格、特殊符号必须进行百分号编码。比如你想请求https://example.com/search?q爬虫教程直接拼进去是不行的需要import urllib.parse keyword 爬虫教程 encoded urllib.parse.quote(keyword) url fhttps://example.com/search?q{encoded} print(url) # https://example.com/search?q%E7%88%AC%E8%99%AB%E6%95%99%E7%A8%8Bquote()默认不编码/和:等字符如果你需要更严格的编码可以传safe参数。再说urlencode()它专门用来把字典转换成 URL 查询字符串params {q: 爬虫教程, page: 2, sort: new} query_string urllib.parse.urlencode(params) print(query_string) # q%E7%88%AC%E8%99%AB%E6%95%99%E7%A8%8Bpage2sortnew这个函数会自动帮你做百分号编码比手动拼接字符串靠谱得多。还有一个parse_qs()和parse_qsl()用来把查询字符串反向解析成字典或列表在处理回调 URL 或者分析请求参数时很有用。2.3 error 模块异常处理不能只靠 try-excepturllib.error定义了两个核心异常类URLError和HTTPError。HTTPError是URLError的子类所以捕获的时候要注意顺序先捕HTTPError再捕URLError。import urllib.request import urllib.error try: response urllib.request.urlopen(https://example.com/not-exist, timeout5) except urllib.error.HTTPError as e: print(fHTTP 错误{e.code} {e.reason}) except urllib.error.URLError as e: print(fURL 错误{e.reason})HTTPError携带了状态码和原因短语你可以根据状态码做不同的处理404 说明页面不存在直接跳过403 说明被拒绝了可能需要换 IP 或调整请求头500 说明服务器内部错误可以稍后重试。URLError通常意味着网络层面的问题比如 DNS 解析失败、连接超时、SSL 证书错误等。实际项目中我习惯把异常处理封装成一个重试装饰器配合指数退避策略import time import functools import urllib.error def retry_on_failure(max_retries3, delay1, backoff2): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): retries, current_delay 0, delay while retries max_retries: try: return func(*args, **kwargs) except (urllib.error.URLError, urllib.error.HTTPError) as e: retries 1 if retries max_retries: raise time.sleep(current_delay) current_delay * backoff return None return wrapper return decorator这个装饰器的逻辑是第一次失败后等 1 秒重试第二次失败后等 2 秒第三次失败后等 4 秒。指数退避的好处是给服务器喘息的时间避免在对方已经压力很大的时候继续猛冲反而导致 IP 被永久封禁。2.4 robotparser 模块合规抓取的第一道门槛urllib.robotparser用来解析网站的 robots.txt 文件判断某个 URL 是否允许被抓取。虽然这个模块在实际爬虫中很少被主动使用但它是合规意识的一种体现。import urllib.robotparser rp urllib.robotparser.RobotFileParser() rp.set_url(https://example.com/robots.txt) rp.read() can_fetch rp.can_fetch(*, https://example.com/some-page) print(can_fetch)can_fetch的第一个参数是 User-Agent第二个参数是目标 URL。返回True表示允许抓取False表示禁止。在实际项目中我建议至少对目标网站的 robots.txt 做一次人工检查了解哪些目录被禁止、有没有 Crawl-delay 限制。这不是技术问题而是基本的网络礼仪。3. 反爬策略全景从识别到应对的完整思路反爬和反反爬是一场持续的博弈。网站方要保护自己的数据和服务稳定性爬虫方要高效获取信息。理解对方的检测手段才能有针对性地调整策略。下面我按照从简单到复杂的顺序把常见的反爬手段和应对思路梳理一遍。3.1 基于请求头的识别与伪装技巧这是最基础也是最容易绕过的一层。网站服务器会检查请求头里的几个关键字段检测字段常见反爬逻辑应对思路User-Agent缺失或包含 Python、curl 等关键词伪装成主流浏览器 UA定期更新Referer缺失或来源域名不匹配补上目标网站的内部页面地址Accept-Language缺失或与 IP 归属地不符设置为 zh-CN 或 en-US 等常见值Cookie缺失关键会话标识先访问首页获取 Cookie再带 Cookie 请求Connection异常值保持默认或设置为 keep-alive我见过不少新手写爬虫所有请求都用同一个固定的 User-Agent跑了几百次之后突然全部返回 403还找不到原因。其实服务器早就通过 UA 的重复频率和请求间隔把你标记出来了。更稳妥的做法是准备一个 UA 池每次请求随机选一个import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0, ] def get_random_ua(): return random.choice(USER_AGENTS)UA 池不需要很大十几个就够用了。关键是要看起来“像真人”而不是一个模子里刻出来的。3.2 基于访问频率的限流与应对节奏频率检测是网站最常用的反爬手段之一。逻辑很简单如果一个 IP 在短时间内发起了大量请求那大概率不是正常用户。常见的限流策略包括固定窗口限流比如每分钟最多 60 次请求超过就返回 429滑动窗口限流更精细统计最近 N 秒内的请求数令牌桶限流允许一定程度的突发流量但长期平均速率受限应对频率检测的核心思路就是控制请求节奏。最直接的方式是每次请求后time.sleep()一个随机时长import time import random def polite_request(url, headers, min_delay1.0, max_delay3.0): time.sleep(random.uniform(min_delay, max_delay)) req urllib.request.Request(url, headersheaders) return urllib.request.urlopen(req, timeout10)随机延迟比固定延迟更安全因为固定间隔的请求模式很容易被识别出来。如果目标网站对频率要求很严格可以把延迟设长一点比如 3 到 8 秒。虽然慢但稳定。实操心得我曾经在一个项目中为了追求速度把延迟设成了 0.5 秒结果跑了不到 200 个请求就被封了 IP。后来改成 2 到 5 秒的随机延迟连续跑了 3 天都没出问题。慢就是快这句话在爬虫领域特别适用。3.3 Cookie 与 Session 管理让请求看起来像同一个人很多网站通过 Cookie 来跟踪用户会话。如果你每次请求都不带 Cookie服务器会认为你是一个新访客某些需要登录或者需要连续操作的功能就无法使用。更关键的是有些反爬系统会把“无 Cookie 的高频请求”作为爬虫特征。用 urllib 管理 Cookie 需要用到http.cookiejar和urllib.request.HTTPCookieProcessorimport urllib.request import http.cookiejar cookie_jar http.cookiejar.CookieJar() handler urllib.request.HTTPCookieProcessor(cookie_jar) opener urllib.request.build_opener(handler) # 第一次请求服务器会下发 Cookie response opener.open(https://example.com/login-page) # 后续请求会自动携带 Cookie response2 opener.open(https://example.com/dashboard)这个opener对象会像浏览器一样自动管理 Cookie 的存取。你还可以手动往cookie_jar里塞入从浏览器开发者工具里复制出来的 Cookie用于模拟已登录状态。3.4 验证码与 JavaScript 渲染urllib 的边界在哪里当网站开始使用验证码或者依赖 JavaScript 动态渲染内容时urllib 就有些力不从心了。验证码需要图像识别或人工打码平台介入JavaScript 渲染则需要浏览器引擎来执行脚本。对于 JavaScript 渲染的页面urllib 拿到的 HTML 里往往只有一堆空的 div 和 script 标签真正的数据是通过 AJAX 请求加载的。这时候有两个方向分析 AJAX 接口打开浏览器开发者工具的 Network 面板找到实际返回数据的 XHR 请求直接用 urllib 请求那个接口。这是最高效的方式因为接口返回的通常是干净的 JSON 数据省去了 HTML 解析的麻烦。使用浏览器自动化工具如果接口有复杂的加密参数或者页面逻辑太绕可以考虑用 Selenium、Playwright 这类工具驱动真实浏览器。但这已经超出了 urllib 的范畴属于另一个技术栈了。我的建议是优先找接口实在找不到再上浏览器自动化。浏览器自动化的资源消耗大、速度慢、稳定性也差一些能不用就不用。4. 实战演练构建一个带反爬应对的完整爬虫前面讲了原理和策略这一节我把它们串起来写一个相对完整的爬虫示例。这个示例会包含请求头伪装、随机延迟、异常重试、Cookie 管理这几个核心环节。4.1 项目结构设计与模块划分一个好的爬虫项目不应该把所有代码堆在一个文件里。我习惯按职责拆分成几个模块spider_project/ ├── config.py # 配置UA 池、延迟范围、重试次数 ├── fetcher.py # 请求发起与响应获取 ├── parser.py # 响应内容解析 ├── storage.py # 数据存储 └── main.py # 调度入口这样拆分的好处是当反爬策略需要调整时你只需要改config.py和fetcher.py解析和存储逻辑不受影响。4.2 请求头的精细化构造在config.py里定义好 UA 池和基础请求头模板# config.py USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, ] BASE_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, Upgrade-Insecure-Requests: 1, } MIN_DELAY 1.5 MAX_DELAY 4.0 MAX_RETRIES 3 TIMEOUT 10注意Accept-Encoding里我写了gzip, deflate。urllib 默认不会自动解压 gzip 响应如果你在请求头里声明支持 gzip服务器可能会返回压缩内容你需要自己解压。为了避免麻烦可以干脆不写这个字段或者加上解压逻辑import gzip import io def decompress_response(response): if response.headers.get(Content-Encoding) gzip: compressed response.read() return gzip.decompress(compressed) return response.read()4.3 带重试与延迟的请求封装fetcher.py是核心中的核心它负责把请求发出去并且处理好各种异常情况# fetcher.py import urllib.request import urllib.error import urllib.parse import http.cookiejar import time import random import gzip from config import USER_AGENTS, BASE_HEADERS, MIN_DELAY, MAX_DELAY, MAX_RETRIES, TIMEOUT class Fetcher: def __init__(self): self.cookie_jar http.cookiejar.CookieJar() self.opener urllib.request.build_opener( urllib.request.HTTPCookieProcessor(self.cookie_jar) ) def _build_headers(self, extraNone): headers dict(BASE_HEADERS) headers[User-Agent] random.choice(USER_AGENTS) if extra: headers.update(extra) return headers def fetch(self, url, headersNone, dataNone, methodNone): for attempt in range(1, MAX_RETRIES 1): try: time.sleep(random.uniform(MIN_DELAY, MAX_DELAY)) req urllib.request.Request( url, datadata, headersself._build_headers(headers), methodmethod ) response self.opener.open(req, timeoutTIMEOUT) raw response.read() if response.headers.get(Content-Encoding) gzip: raw gzip.decompress(raw) return raw.decode(utf-8, errorsreplace) except urllib.error.HTTPError as e: print(f[尝试 {attempt}] HTTP {e.code}: {url}) if e.code in (403, 429): time.sleep(random.uniform(5, 10)) elif e.code 404: return None except urllib.error.URLError as e: print(f[尝试 {attempt}] URL 错误: {e.reason}) except Exception as e: print(f[尝试 {attempt}] 未知错误: {e}) if attempt MAX_RETRIES: time.sleep(2 ** attempt) return None这个Fetcher类做了几件事每次请求随机选 UA、请求前随机延迟、遇到 403/429 时额外等待、失败后指数退避重试、自动处理 gzip 解压、自动管理 Cookie。基本上覆盖了前面讲的大部分反爬应对策略。4.4 响应解析与数据提取拿到 HTML 之后下一步是提取需要的数据。urllib 本身不提供解析功能需要配合re、html.parser或者第三方库如 BeautifulSoup、lxml。这里我用标准库的html.parser做一个简单示例# parser.py from html.parser import HTMLParser class TitleExtractor(HTMLParser): def __init__(self): super().__init__() self.in_title False self.title def handle_starttag(self, tag, attrs): if tag title: self.in_title True def handle_endtag(self, tag): if tag title: self.in_title False def handle_data(self, data): if self.in_title: self.title data.strip() def extract_title(html): parser TitleExtractor() parser.feed(html) return parser.title实际项目中如果解析逻辑复杂我还是推荐用 BeautifulSoup 或 lxml它们的 CSS 选择器和 XPath 支持能大幅提升开发效率。标准库的html.parser更适合轻量级、无依赖的场景。4.5 调度入口与运行日志最后把所有模块串起来# main.py from fetcher import Fetcher from parser import extract_title def main(): fetcher Fetcher() urls [ https://example.com/page/1, https://example.com/page/2, https://example.com/page/3, ] for url in urls: html fetcher.fetch(url) if html: title extract_title(html) print(f{url} - {title}) else: print(f{url} - 抓取失败) if __name__ __main__: main()运行日志里记录了每个 URL 的抓取结果方便后续排查。如果某个 URL 反复失败你可以单独把它拎出来分析原因。5. 常见问题与排查技巧实录这一节我整理了一些在实际开发中经常遇到的问题以及我的排查思路和解决方法。这些问题在官方文档里通常不会写但每一个都可能让你卡上半天。5.1 返回乱码怎么办乱码是爬虫新手遇到最多的问题之一。根本原因是编码不一致服务器返回的字节流用某种编码GBK、UTF-8、ISO-8859-1而你用另一种编码去解码。排查步骤先看响应头里的Content-Type字段比如text/html; charsetgbk这里的 charset 就是服务器声明的编码。如果响应头没有声明看 HTML 里的meta charset...标签。如果两者都没有可以尝试用chardet库自动检测或者手动试几种常见编码。def smart_decode(raw_bytes, response_headersNone): # 优先从响应头获取编码 if response_headers: content_type response_headers.get(Content-Type, ) if charset in content_type: charset content_type.split(charset)[-1].split(;)[0].strip() try: return raw_bytes.decode(charset) except (LookupError, UnicodeDecodeError): pass # 尝试常见编码 for encoding in (utf-8, gbk, gb2312, latin-1): try: return raw_bytes.decode(encoding) except UnicodeDecodeError: continue return raw_bytes.decode(utf-8, errorsreplace)注意errorsreplace是最后的兜底方案它会把无法解码的字节替换成特殊符号保证程序不崩溃但数据可能不完整。能确定编码的情况下尽量不要用这个参数。5.2 请求被重定向到验证页面有时候你请求一个 URL返回的 HTML 里不是你想要的内容而是一个验证页面或者跳转页面。这通常意味着服务器识别出了异常把你引导到了验证流程。排查思路检查response.geturl()看看最终请求的 URL 是否和原始 URL 一致。如果不一致说明发生了重定向。检查响应状态码301、302、303、307 都是重定向相关的状态码。对比浏览器里正常访问时的请求头和你的请求头找出差异。应对方法如果是简单的 302 跳转urllib 默认会自动跟随。但如果跳转到了验证页面你需要分析验证页面的逻辑看是需要携带特定 Cookie 还是需要完成某个挑战。这种情况下往往需要回到浏览器里手动完成一次验证然后把生成的 Cookie 复制到代码里。5.3 如何判断 IP 是否被封IP 被封的典型表现是之前能正常访问的 URL突然全部返回 403 或者连接超时而用浏览器访问同样的 URL 却一切正常。这时候基本可以确定是你的 IP 被目标网站拉黑了。验证方法import urllib.request def check_ip_blocked(test_url): try: req urllib.request.Request(test_url, headers{User-Agent: Mozilla/5.0}) response urllib.request.urlopen(req, timeout5) return response.status 200 except urllib.error.HTTPError as e: if e.code 403: return False return None except Exception: return None如果确认被封能做的事情包括降低请求频率、更换网络环境、增加请求间隔、完善请求头。千万不要在被封之后立刻用同样的方式继续猛冲那样只会让封禁时间更长。5.4 常见问题速查表问题现象可能原因排查方向解决思路返回 403UA 被识别、IP 被封、Referer 缺失对比浏览器请求头更换 UA、补 Referer、降低频率返回 429请求频率过高检查请求间隔增加随机延迟、减少并发返回内容为空页面是 JS 渲染、需要登录查看页面源码分析 AJAX 接口、补充 Cookie返回乱码编码不一致检查 Content-Type用正确编码解码连接超时网络问题、服务器限流检查网络、ping 目标增加 timeout、重试SSL 证书错误证书过期或自签名检查证书状态谨慎处理优先确认安全性Cookie 丢失未使用 CookieJar检查 opener 配置使用 HTTPCookieProcessor5.5 几个容易被忽略的细节第一URL 里的参数顺序可能影响结果。有些网站的接口对参数顺序敏感或者会对参数做签名校验。如果你发现同样的参数换个顺序就请求失败那大概率是签名机制在起作用。第二POST 请求的 Content-Type 必须和实际数据格式匹配。发送 JSON 数据时Content-Type要设为application/json发送表单数据时要设为application/x-www-form-urlencoded。类型不匹配会导致服务器解析失败。第三注意 URL 编码的层级。如果参数值本身包含%或等特殊字符需要先对参数值做一次编码再拼接到 URL 里。否则这些字符会被误认为是分隔符。第四robots.txt 不是摆设。虽然技术上你可以忽略它但从合规和长期稳定的角度尊重目标网站的抓取规则是明智的选择。很多网站会对遵守 robots.txt 的爬虫更宽容。6. 从 urllib 出发的进阶方向urllib 帮你把 HTTP 请求的底层逻辑摸清楚了接下来往哪个方向走取决于你的具体需求。如果你追求开发效率requests是最自然的下一步。它的 API 设计更人性化Session 管理、连接池、超时控制都封装得很好。你会发现用 urllib 写几十行的代码用 requests 可能五六行就搞定了。但因为你已经理解了底层原理遇到问题时不会两眼一抹黑。如果你需要处理大规模抓取scrapy这样的框架值得投入时间学习。它提供了完整的调度、去重、中间件、管道机制适合构建长期运行的爬虫系统。scrapy 的下载中间件机制本质上就是把 urllib 层面的请求头伪装、重试、代理切换等逻辑做成了可插拔的组件。如果你面对的是 JavaScript 重度渲染的页面Playwright或Selenium是绕不开的工具。它们能驱动真实浏览器执行页面脚本拿到渲染后的 DOM。但代价是资源消耗大、速度慢所以能用接口解决的就不要上浏览器。不管往哪个方向走理解 HTTP 协议本身始终是最核心的竞争力。请求方法、状态码、请求头、响应头、Cookie 机制、缓存控制这些概念在任何爬虫工具里都是通用的。urllib 只是一个入口真正值钱的是你对整个请求-响应模型的理解。我在实际项目中最大的体会是反爬策略千变万化但底层逻辑就那么几类。识别请求来源、限制访问频率、校验会话状态、增加人机验证。你只要把每一类的原理搞清楚遇到新的反爬手段时就能快速定位它属于哪一类然后有针对性地调整策略。工具会过时框架会更迭但这种分析问题的能力不会贬值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑