
爬虫程序遇到反爬限制时,可以先检查哪些问题
爬虫突然拿不到数据,常见表现包括返回 403 或 429、跳转到登录页、收到验证码、页面内容为空,或者程序运行一段时间后才开始失败。这些现象都可能与网站的访问控制有关,但原因并不总是“网站封了爬虫”:请求频率过高、请求头不完整、登录状态失效、页面改版、解析规则出错,也会造成相似结果。
排查时,第一步不是换代理、伪装浏览器或寻找绕过验证码的方法,而是确认自己是否有权访问、请求究竟返回了什么,以及程序是否按网站允许的方式工作。本文介绍一套从合规确认到代码诊断的检查顺序,并提供可直接运行的 Python 示例。示例面向公开、允许自动访问的页面;不要用它规避登录、付费墙、验证码、访问控制或网站明确设置的限制。
先明确适用场景和边界
下文方法适用于你拥有的网站、取得授权的数据采集项目、提供公开接口或明确允许自动访问的页面,以及符合网站条款和当地法律的低频采集。开始之前,应查看网站的使用条款、开发者文档、数据许可和 robots.txt。如果网站提供 API 或数据下载,应优先使用它们;它们通常比解析网页更稳定,也更容易遵守配额要求。
robots.txt 是站点向爬虫表达访问偏好的文件,不是访问授权,也不能覆盖法律、合同或网站条款。一个路径没有被它禁止,不代表你可以不限频率访问,也不代表可以获取需要登录或付费才能看到的内容。相反,如果规则不明确或禁止采集,应先联系站点确认,不要把技术上能请求到当成获得许可。
遇到登录页、验证码、访问令牌校验、付费墙、IP 封禁、明确的反自动化提示,或网站要求停止请求时,应停止自动采集并联系网站运营方或使用官方接口。不要通过轮换代理、模拟账号、共享 Cookie、绕过验证码、伪造来源或规避封禁来继续访问。代理和浏览器自动化本身并非在所有场景都不当,但不能用来逃避网站设置的访问控制。本文不提供此类绕过方案。
第一步:保留现场,不要只看“没有数据”
解析程序通常只告诉你“找不到元素”,却没有说明服务器实际返回了什么。排查时至少记录请求 URL、时间、状态码、响应类型、响应长度、重定向后的地址和少量响应正文。正文可能含个人信息或会话令牌,日志中应避免保存敏感内容;调试时只检查必要片段,并在输出前进行脱敏。
不同状态码只能提供线索,不能单独证明原因:
200表示服务器成功返回了某个响应,不保证它就是目标页面。登录页、错误提示页和反爬提示页也可能返回 200。301、302等重定向可能把请求送到登录页、地区选择页或新的 URL。检查最终响应地址及页面标题。401通常表示需要身份认证;403表示服务器拒绝请求。具体含义由站点实现决定,不能靠更改请求头推断或修复授权问题。404可能是路径错误、页面已下线,也可能是站点对未授权访问采用的响应方式。429通常表示请求过多。应立即降低频率,遵循响应中的Retry-After,必要时暂停任务,而不是并发重试。5xx表示服务端错误或网关问题。可以有限重试瞬时故障,但应设置次数上限和间隔。
先用浏览器人工确认目标页面是否仍然可见、是否需要登录、是否出现维护提示,再对比程序拿到的状态和内容。不要把带有账号 Cookie 的浏览器请求直接复制到爬虫中;Cookie 是访问凭证,处理不当可能造成账号泄露或违反站点规则。
第二步:检查请求是否合理
检查 URL、方法和参数
确认请求 URL 的协议、域名、路径和查询参数没有拼错,分页参数是否仍然有效,编码是否正确。请求方法也要与页面或接口预期一致:公开页面通常通过 GET 获取;不要为了试探接口而随意对生产站点发送 POST、PUT、DELETE 等会改变数据的请求。
对于查询参数,优先使用 HTTP 客户端的参数功能进行编码,而不是手工拼接字符串。手工拼接容易漏掉空格、中文和特殊字符的编码,也可能意外改变参数含义。不要构造超出文档规定的分页大小、筛选范围或其他参数来探测未公开数据。
检查请求头,而不是盲目伪装
请求头应如实、简洁。设置一个能说明采集用途和联系渠道的 User-Agent,并在确有需要时设置合适的 Accept。不要堆砌看似真实的浏览器指纹,也不要虚构 Referer、来源身份或浏览器能力。网站如果明确要求特定客户端或禁止自动请求,应遵守要求,而不是靠伪装继续访问。
一般网页请求可以使用常规 HTTP 客户端。只有当目标内容确实由浏览器执行 JavaScript 后生成,且网站允许自动访问时,才考虑浏览器自动化。即使使用真实浏览器,也不能借此绕过登录、验证码或访问控制;浏览器加载资源更多,对服务器的请求负担也更大。
检查会话状态和授权范围
如果项目依赖授权账号或 API 密钥,确认凭证是否过期、权限是否覆盖目标资源、是否超出配额。密钥应存储在环境变量或安全的凭证管理服务中,不要写入代码仓库、日志或公开示例。只访问授权范围内的数据;不要尝试借用他人账号、复用来源不明的 Cookie,或通过请求参数推测其他用户的数据。
第三步:降低请求频率并处理限流
初始采集应串行、低频、小批量进行。控制请求间隔、并发数和任务总量;每次只抓取实际需要的页面,并使用条件请求或缓存避免重复下载。没有一个适用于所有网站的“安全请求频率”:合理频率取决于站点规则、接口配额和服务承载能力。如果没有公开限制,选择保守速率并观察响应;出现限流或服务异常时立刻降速或暂停。
对 429 或 503 使用有上限的等待和重试。若服务器返回 Retry-After,应优先遵循;它可能是秒数,也可能是 HTTP 日期。没有该字段时,可以使用逐步增加的等待时间,但必须限制最大等待和重试次数。重试不是对拒绝访问的“修复手段”:对于 401、403、验证码或明确要求停止的响应,不应反复请求。
下面的代码演示一个保守的 GET 诊断请求:它设置清晰的客户端标识、记录响应信息、对 429 和 503 做有限等待,并在其他 HTTP 错误时停止。安装依赖后即可运行。请把示例地址替换为你有权访问且允许自动请求的页面;不要把它用于探测不受你控制的站点。
python -m pip install requests
import email.utils
import time
from datetime import datetime, timezone
import requests
URL = "https://example.com/"
HEADERS = {
"User-Agent": "ExampleResearchBot/1.0 (contact: [email protected])",
"Accept": "text/html,application/xhtml+xml",
}
MAX_RETRIES = 3
TIMEOUT = (5, 15) # 连接超时、读取超时,单位为秒
def retry_after_seconds(value):
"""解析 Retry-After;无法解析时返回 None。"""
if not value:
return None
try:
return max(0.0, float(value))
except ValueError:
pass
try:
retry_time = email.utils.parsedate_to_datetime(value)
if retry_time.tzinfo is None:
retry_time = retry_time.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return max(0.0, (retry_time - now).total_seconds())
except (TypeError, ValueError, OverflowError):
return None
with requests.Session() as session:
session.headers.update(HEADERS)
for attempt in range(MAX_RETRIES + 1):
try:
response = session.get(URL, timeout=TIMEOUT)
except requests.RequestException as exc:
print("请求失败:", type(exc).__name__, str(exc))
break
print("状态码:", response.status_code)
print("最终 URL:", response.url)
print("Content-Type:", response.headers.get("Content-Type"))
print("响应字节数:", len(response.content))
if response.status_code in (429, 503):
if attempt == MAX_RETRIES:
print("达到重试上限,停止请求。")
break
server_delay = retry_after_seconds(
response.headers.get("Retry-After")
)
# 站点未给出有效等待时间时,采用保守的递增间隔。
delay = server_delay if server_delay is not None else 2 ** attempt
delay = min(delay, 60.0)
print(f"暂时限流或服务不可用,等待 {delay:.1f} 秒。")
time.sleep(delay)
continue
if response.status_code in (401, 403):
print("访问被拒绝或需要授权;停止,不尝试绕过访问控制。")
break
try:
response.raise_for_status()
except requests.HTTPError as exc:
print("HTTP 错误:", exc)
break
print("响应正文开头:")
print(response.text[:500])
break
示例中的 60 秒是程序为单次等待设置的上限,不是网站允许的最大等待时间。如果响应要求等待更久,或持续返回限流状态,应暂停任务并核对站点规则,而不是把等待时间截短后继续请求。对于需要严格遵守服务器指定等待时间的任务,可选择暂停到指定时间再由人工或调度系统恢复,不要无限循环。
requests 的 timeout 是连接和读取等待的上限设置,不代表整个程序一定会在相同秒数内结束,也不等于限制下载的总字节数。面对大响应体或不可信目标,还应设置合理的响应体大小上限,并考虑整体任务的截止时间。
第四步:确认 robots.txt 与网站说明
可以用 Python 标准库读取并检查 robots.txt。此检查只是在程序侧解析站点公布的爬虫规则,不会授予访问权限,也不能代替阅读条款、接口文档或取得授权。无法读取文件时,不要简单地把它当作“允许”;应根据站点政策和项目授权判断是否继续。
from urllib.robotparser import RobotFileParser
from urllib.parse import urlsplit
def robots_allows(page_url, user_agent):
parts = urlsplit(page_url)
robots_url = f"{parts.scheme}://{parts.netloc}/robots.txt"
parser = RobotFileParser()
parser.set_url(robots_url)
try:
parser.read()
except OSError as exc:
print("无法读取 robots.txt:", exc)
return None # 未知,不等于允许
return parser.can_fetch(user_agent, page_url)
page_url = "https://example.com/catalog/"
user_agent = "ExampleResearchBot"
allowed = robots_allows(page_url, user_agent)
if allowed is True:
print("robots.txt 允许该路径;仍需确认条款、授权和访问频率。")
elif allowed is False:
print("robots.txt 不允许该路径;不要继续抓取。")
else:
print("规则无法确认;先查阅站点说明或联系站点。")
不同站点可能有自己的机器人规则或使用条款。标准库解析出的结果也不应被当作法律结论。对于合作项目,最好在采集方案中记录允许的域名、路径、时间范围、请求速率、字段范围和联系人,避免程序因配置错误访问到授权范围之外。
第五步:判断内容是服务器返回,还是浏览器动态生成
如果状态码是 200,但 HTML 中没有目标内容,先保存一份经过脱敏的响应副本,检查页面标题、主要文本、表单提示和最终 URL。常见原因包括:
- 目标元素的名称或层级发生变化,旧选择器已经失效。
- 服务器返回的是空壳 HTML,数据由 JavaScript 在浏览器端加载。
- 请求被重定向到登录、地区选择、同意提示或维护页面。
- 页面根据地区、语言、授权状态或 Cookie 显示不同内容。
- 响应不是 HTML,而是 JSON、错误页或其他内容类型。
若数据来自公开、文档化的接口,且你有权调用,优先按文档使用接口,并遵守配额。若页面确实依赖 JavaScript,可在确认许可后使用浏览器自动化,但要控制页面数、并发和加载资源,并避免触发或规避访问控制。无法确认接口是否开放或内容是否允许采集时,应向站点确认。
不要因为浏览器能打开网页,就假设自动化访问也被允许。浏览器中已经登录的状态可能依赖个人账号和会话凭证;自动化程序应仅使用明确授权、为项目准备的身份与权限,并保护凭证。遇到验证码或人工审核,应停止自动化并按站点提供的正规流程处理。
第六步:检查解析器和数据质量
请求成功并不代表提取结果正确。网页结构调整、字符编码变化、重复内容、空字段和分页逻辑错误,都可能让程序产出“看似正常、实际错误”的数据。解析前先检查响应的 Content-Type 和正文结构;解析后校验必填字段、记录数量、重复率及分页是否连续。若数据数量突然变为零,不要把空结果当成成功写入生产数据集。
以下示例用 BeautifulSoup 解析一个公开示例页面,并检查标题。它只演示 HTML 解析,不代表任何网站都允许采集;替换目标地址前,应先确认许可和访问规则。
python -m pip install beautifulsoup4 requests
import requests
from bs4 import BeautifulSoup
url = "https://example.com/"
headers = {
"User-Agent": "ExampleResearchBot/1.0 (contact: [email protected])",
"Accept": "text/html",
}
response = requests.get(url, headers=headers, timeout=(5, 15))
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if "text/html" not in content_type:
raise ValueError(f"预期 HTML,实际 Content-Type 为:{content_type}")
soup = BeautifulSoup(response.text, "html.parser")
title = soup.title.get_text(" ", strip=True) if soup.title else None
if not title:
raise ValueError("没有找到页面标题;请检查响应内容和解析规则。")
print("页面标题:", title)
示例域名用于说明代码结构,实际运行时应换成符合访问条件的地址。生产程序不应只用页面标题作为成功判据;应根据数据任务定义字段约束,例如标题非空、日期格式有效、记录 ID 唯一。解析规则失败时保存必要诊断信息并告警,而不是静默写入空数据。
常见错误及其后果
- 对所有错误无限重试:会加重站点负载,可能导致更长时间的限制。设置次数上限,按状态码区分处理;拒绝访问、认证失败和明确的停止提示不应重试。
- 提高并发来“提速”:高并发可能触发限流,也会影响网站服务。没有明确配额时从低并发开始,观察结果并遵守站点约定。
- 把 200 当成成功:错误提示、登录页和反爬提示可能也返回 200。检查最终 URL、内容类型、页面文本和实际字段。
- 只换 User-Agent:这不能解决权限、频率或内容许可问题。伪装身份还会让站点无法识别真实用途。
- 忽略 robots.txt 和使用条款:技术上能请求,不等于获得授权。无法确认规则时先暂停并询问。
- 把 Cookie 或密钥硬编码在代码里:容易泄露账号和访问凭证。使用环境变量或安全存储,日志中不要输出完整凭证。
- 采集与任务无关的个人信息:应遵守数据保护要求,只收集必要字段,设定保留期限和访问权限;不要把抓取到的数据随意公开或转售。
- 请求刚恢复就恢复原速:短期成功不表示限流已解除。应维持保守频率,必要时暂停整个任务并核实站点政策。
一个可执行的排查顺序
- 先核对授权:确认域名、页面、数据字段和采集方式都在许可范围内;查阅 API 文档、条款和 robots.txt。
- 复现单个请求:用低频、单请求方式记录状态码、最终 URL、响应类型和有限的正文片段,不要立刻启动全量任务。
- 判断响应类型:检查是否是登录页、验证码、限流提示、维护页或内容类型变化。遇到访问控制就停止并联系站点。
- 检查请求配置:核对 URL、参数、方法、必要请求头和授权状态,避免伪造浏览器身份或使用来源不明的 Cookie。
- 降频并做有限重试:遵循 Retry-After,限制并发和总请求数。持续出现 429、403 或明确拒绝时暂停。
- 验证页面和解析规则:确认目标数据是否在响应中,检查选择器、分页和字段校验,避免把空结果误记为成功。
- 保留最少量诊断日志:记录请求时间、状态和错误类别;对正文、账号信息及个人数据进行最小化和脱敏处理。
遇到反爬提示时,首先要回答的是“这项访问是否被允许”,其次才是“请求哪里出了问题”。如果授权明确,问题通常可以通过降低频率、修正参数、识别重定向、更新解析逻辑或改用官方接口解决。如果原因是网站拒绝访问或要求人工验证,正确处理是停止自动请求并沟通,而不是寻找规避办法。这样既能减少误判和无效排查,也能避免给网站和自己的项目带来不必要的风险。


