每次写完一个爬虫脚本,最尴尬的不是代码报错,而是自信满满跑起来后,却发现目标站早就改版了、接口换了、登录态失效了,甚至首页直接返回验证码。这个问题的根源在于:很多爬虫在“第一次真正抓取”之前,根本不知道目标站点当前是否允许正常访问。与其等项目挂掉再排查,不如在正式抓取前先做一次低成本探测(probe),把“能不能抓”这个问题前置解决。本文围绕这个思路,从探测机制设计、代码实现到工程化落地方案,逐步拆解一套带探测能力的爬虫方案。
1. 背景与核心概念
1.1 什么是爬虫探测(probe)
在半导体测试领域,有一类设备叫探针卡(Probe Card),它的作用是在芯片封装前,先用探针接触晶圆上的每个测试点,把坏芯片提前筛出去,避免后续无意义的封装成本。爬虫场景里的 probe 思路很相似:在正式发起批量抓取之前,先用一个很小的请求去“触碰”目标站点,检查它当前是否是活的、是否允许抓取、页面结构是否还和预期一致。
也就是说,探测不是“抓取”,而是“检查是否可以抓取”。它是一种低成本的预检动作,核心输出是一个结论:当前目标站点是否满足本次抓取的前提条件。如果条件不满足,我们可以直接放弃本次抓取,保留证据并报警,而不是让大量请求撞在一个已经失效的服务上。
很多开发者会忽略这个前置步骤,认为只要状态码是 200 就代表能抓。实际上,200 响应的页面可能是一个登录跳转页、一个人机验证页、一个空壳页面,甚至是一段提示“请开启 JS 后再访问”的静态 HTML。仅凭状态码判断会导致爬虫在字段解析阶段崩溃,或者更糟糕,在反爬页面里无效空转。
1.2 为什么在承诺“能用”之前先探测
标题中的“promising it works”点出了一个常见现象:不少开源爬虫项目在 README 里宣称自己可以抓取某站点,但用户实际运行后发现无法工作。原因通常是:
- 目标站点改版,页面结构变化,原有的 CSS 选择器失效。
- 增加了登录、验证码、风控校验。
- 接口签名或请求头校验升级。
- 服务器区域限制,或网络环境变化导致无法访问。
- 目标站点增加了 robots 限制或 WAF 策略收缩。
这些情况很难在静态代码里提前预料,但可以通过动态探测发现。一个负责任的爬虫项目,应该把“探测”作为运行前置步骤:我先试一下目标站点当前的状态,根据探测结果决定是否继续执行。这样既能减少无效请求,也能在任务调度系统里形成自动化的健康检查机制。
1.3 探测的典型应用场景
- 定时任务前检查:每天定时抓取前,先通过 probe 判断目标站点是否可访问,失败则直接跳过。
- 爬虫服务健康检查:像探活接口一样,为爬虫服务提供
/probe入口,监控系统可以随时检查目标站可用性。 - 代理或出口 IP 切换判断:当某个节点被封时,用探测结果快速切换代理。
- 多站点抓取时做路由决策:一批 URL 候选中,只抓取探测通过的站点。
- 上线发布前自检:新版本爬虫上线前,自动探测线上环境的目标站点结构是否匹配。
从这些场景可以看出,探测机制的核心价值不是提高单次抓取速度,而是提高整体任务的成功率和稳定性。
2. 环境准备与版本说明
先统一说明一下运行环境。本文示例使用的技术栈如下:
- Python 3.8 或更高版本
- requests 库,用于发起 HTTP 请求
- beautifulsoup4 库,用于页面解析和内容指纹提取
- lxml 库,作为 beautifulsoup4 的解析器
版本不需要完全固定,如果你的项目已经使用了较新的 Python 3.10、3.11,示例代码同样适用。如果 Python 版本较低,建议至少保持 3.8 以上,因为代码中会用到 f-string、dataclass 等特性。
可以通过 pip 安装依赖:
pip install requests beautifulsoup4 lxml如果你希望在测试时不真正访问外网目标站,可以用 Python 内置的http.server启动一个临时本地站点,或者用 Flask 搭一个简单的模拟页面。本文后面的示例会给出本地验证方式,方便你跑通代码后再对接真实目标。
项目整体结构规划如下:
scraper_probe/ ├── requirements.txt ├── probe.py # 探测模块 ├── scraper.py # 抓取模块,正式抓取前先探测 ├── main.py # 入口示例 └── cache/ # 可选,存放探测缓存或日志这种拆分的目的是保持职责单一:probe.py 只回答“目标站点是否适合抓取”,scraper.py 只回答“在探测通过的前提下如何抓取”。后面实战部分会完整展开。
3. 核心设计:探测机制到底探测什么
3.1 探测的五个核心维度
探测不能只查一个维度,至少要从下面五个方面检查目标站点。
第一是连通性与状态码。这一层判断网络连接是否正常、服务器是否返回有效响应。常见的异常包括超时、连接拒绝、SSL 证书错误、404、500 等。需要注意的是,状态码 200 并不代表页面内容有效,所以它只能作为最基础的过滤条件。
第二是响应速度。如果页面响应时间过长,可能是目标站点性能不佳,也可能是已经触发了限流等待。在定时任务里,过长的响应时间会影响整个调度周期。我们可以设定一个探测阈值,比如超过 10 秒就认为不适合抓取。
第三是页面内容特征。这一步需要检查页面中是否存在预期的标题、关键元素、数据容器等。例如一个商品列表页,预期的特征是页面标题包含“商品列表”或存在class="product-item"的节点。如果这些特征丢失,大概率是页面改版了。
第四是反爬与拦截痕迹。页面中如果出现验证码、安全验证、滑块、访问被拒绝等关键词,说明当前节点可能已经被风控拦截。此时继续抓取只会增加被封风险。
第五是业务规则限制。例如 robots.txt 是否允许爬取指定路径,接口是否需要登录态,会话是否已经过期。这层检查可以帮助我们提前停止违规或无效请求。
3.2 探测结果的数据结构
为了让探测结果能在多个场景里复用,建议用数据结构统一表达。下面用 Python 的dataclass定义一个ProbeResult:
from dataclasses import dataclass, field from typing import Optional @dataclass class ProbeResult: url: str ok: bool # 是否通过探测 status_code: Optional[int] = None response_time: float = 0.0 # 响应时长,单位秒 final_url: Optional[str] = None # 重定向后的最终 URL content_type: Optional[str] = None title: Optional[str] = None # 页面标题 blocked_by: Optional[str] = None # 拦截类型,如 captcha/login/waf robots_allowed: bool = True reason: str = "" # 失败原因摘要 details: dict = field(default_factory=dict) # 扩展信息这个结构里,ok表示最终结论,reason给人类可读的失败原因,details存放探测过程中的附加信息。这样在日志输出、监控告警、决策分支中都可以直接使用。
3.3 探测的流程设计
一个完整的探测流程可以分成 6 步:
- 检查 robots.txt 是否允许爬取目标路径。
- 发起一个带超时和浏览器 User-Agent 的 GET 请求。
- 记录状态码、重定向链、响应时间。
- 分析响应内容,提取标题和关键特征。
- 检测反爬特征词,判断是否触发验证码或安全拦截。
- 汇总结果,返回
ProbeResult。
其中第一步不要放在最后,因为如果 robots 明确不允许,连请求都可以省掉,避免无意义访问。但这不绝对,一些项目有自己的合规策略,需要根据业务场景调整。
4. 完整实战:带探测功能的爬虫
4.1 创建项目结构与依赖文件
先创建项目目录:
mkdir scraper_probe cd scraper_probe在项目根目录创建requirements.txt:
requests>=2.28.0 beautifulsoup4>=4.12.0 lxml>=4.9.0安装依赖:
pip install -r requirements.txt下面开始编写核心文件。
4.2 编写探测模块 probe.py
先编写一个基础版探测函数。它接受 URL 和超时时间,返回一个ProbeResult。为了便于理解,这里先不加入 robots 检查,只实现连通性、内容特征和拦截特征检测。
# 文件路径:scraper_probe/probe.py import re import time from urllib.parse import urlparse import requests from bs4 import BeautifulSoup from dataclasses import dataclass, field from typing import Optional from probe_result import ProbeResult BLOCK_KEYWORDS = [ "captcha", "安全验证", "验证码", "access denied", "访问被拒绝", "slider", "请开启js", "verify", ] def detect_blocked(text: str) -> Optional[str]: """检测页面中是否包含反爬或拦截特征。""" lowered = text.lower() for keyword in BLOCK_KEYWORDS: if keyword.lower() in lowered: return keyword return None def probe_site(url: str, timeout: int = 10) -> ProbeResult: """ 对目标站点进行一次低成本探测。 :param url: 目标 URL :param timeout: 超时时间,单位秒 :return: ProbeResult """ result = ProbeResult(url=url) 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", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } start = time.time() try: response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) except requests.Timeout: result.ok = False result.reason = "请求超时" return result except requests.ConnectionError: result.ok = False result.reason = "连接失败,请检查域名解析和网络连通性" return result except requests.SSLError: result.ok = False result.reason = "SSL 证书校验失败" return result elapsed = time.time() - start result.status_code = response.status_code result.final_url = response.url result.response_time = round(elapsed, 3) result.content_type = response.headers.get("Content-Type", "") if response.status_code >= 400: result.ok = False result.reason = f"HTTP 状态码异常: {response.status_code}" return result html_text = response.text soup = BeautifulSoup(html_text, "lxml") result.title = soup.title.get_text(strip=True) if soup.title else "" # 检测拦截特征 blocked_flag = detect_blocked(html_text) if blocked_flag: result.blocked_by = blocked_flag result.ok = False result.reason = f"检测到拦截特征: {blocked_flag}" return result # 空内容检测 if len(html_text.strip()) < 100: result.ok = False result.reason = "页面内容过短,疑似空页面或纯 JS 渲染页面" return result result.ok = True result.reason = "探测通过" return result如果希望运行上述代码,还需要定义ProbeResult。为了方便,可以在项目中单独建一个probe_result.py:
# 文件路径:scraper_probe/probe_result.py from dataclasses import dataclass, field from typing import Optional @dataclass class ProbeResult: url: str ok: bool = False status_code: Optional[int] = None response_time: float = 0.0 final_url: Optional[str] = None content_type: Optional[str] = None title: Optional[str] = None blocked_by: Optional[str] = None robots_allowed: bool = True reason: str = "" details: dict = field(default_factory=dict)我们先不追求代码最精简,而是把每个检查逻辑都独立拆开,方便后续在真实项目中按需扩展。
4.3 增强探测:加入 robots 与内容指纹校验
基础版探测只解决了“页面能不能访问”的问题,但还没有解决“结构对不对”的问题。下一步加入两步增强检查。
第一步,检查 robots.txt。以测试页为例,假设目标站点是https://example.com,我们可以先请求https://example.com/robots.txt,然后解析是否包含User-agent: *以及Disallow规则。
# 文件路径:scraper_probe/probe.py 中的增强函数(节选) from urllib.robotparser import RobotFileParser def check_robots(url: str, user_agent: str = "*") -> bool: """ 检查 robots.txt 是否允许爬取指定 URL。 注意:这里只做基础检查,生产环境要结合自身 User-Agent 判断。 """ parsed = urlparse(url) robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt" try: rp = RobotFileParser() rp.set_url(robots_url) rp.read() return rp.can_fetch(user_agent, url) except Exception: # 如果无法获取 robots.txt,默认放行并记录日志 return True第二步,内容指纹校验。在很多抓取任务里,页面改版是导致爬虫失效的头号原因。我们可以在探测函数中接收一个expected_keywords参数,页面标题或正文中包含这些关键词才认为结构未变化。
def check_content_fingerprint(html_text: str, expected_keywords: list[str]) -> Optional[str]: """检查页面是否包含预期关键词,返回缺失的第一个关键词或 None。""" for keyword in expected_keywords: if keyword not in html_text: return keyword return None然后把内容指纹校验合并到probe_site中,增加一个可选参数:
def probe_site( url: str, timeout: int = 10, expected_keywords: Optional[list[str]] = None, check_robots_enabled: bool = True, ) -> ProbeResult: result = ProbeResult(url=url) # ... 之前的请求逻辑 ... # 在获取 html_text 之后增加: if check_robots_enabled: result.robots_allowed = check_robots(url) if not result.robots_allowed: result.ok = False result.reason = "robots.txt 禁止抓取该路径" return result if expected_keywords: missing = check_content_fingerprint(html_text, expected_keywords) if missing: result.ok = False result.reason = f"页面缺少预期特征关键词: {missing}" return result # ... 后续拦截检测和空内容检测 ...这样,一个探测模块就基本完整了。它的结论不再只是“通了”,而是“通、合法、且结构符合预期”。
4.4 编写抓取模块 scraper.py
抓取模块的核心职责是:先调用probe_site,探测通过后才执行真正的解析逻辑。
下面编写一个简单的商品标题抓取示例。为了演示expected_keywords,我们假设目标页面的标题或正文包含“产品”或“列表”等词。
# 文件路径:scraper_probe/scraper.py import requests from bs4 import BeautifulSoup from probe import probe_site from probe_result import ProbeResult def fetch_page(url: str, expected_keywords: list[str], timeout: int = 10) -> str: """ 抓取页面 HTML。抓取前先执行探测。 :param url: 目标 URL :param expected_keywords: 页面内容指纹关键词 :param timeout: 请求超时时间 :return: 页面 HTML 字符串 :raises RuntimeError: 探测未通过时抛出异常 """ probe_result: ProbeResult = probe_site( url, timeout=timeout, expected_keywords=expected_keywords, check_robots_enabled=False, # 演示时先关闭,实际项目按需开启 ) if not probe_result.ok: raise RuntimeError(f"探测未通过: {probe_result.reason}") 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" ) } response = requests.get(url, headers=headers, timeout=timeout) response.raise_for_status() return response.text def parse_product_titles(html_text: str) -> list[str]: """解析页面中的商品标题。""" soup = BeautifulSoup(html_text, "lxml") titles = [] for node in soup.select(".product-item .title"): text = node.get_text(strip=True) if text: titles.append(text) return titles在这个示例里,fetch_page是带探测能力的入口,parse_product_titles是具体的解析函数。这样拆分后,如果目标站点的选择器变了,只需要改解析函数;如果目标站点不可访问,探测逻辑会直接拦截,不会进入解析阶段。
4.5 编写入口 main.py
入口示例逻辑如下:
- 定义目标 URL。
- 从命令行或配置读取预期关键词。
- 调用
fetch_page获取 HTML。 - 解析并输出结果。
- 捕获探测失败异常并记录日志。
# 文件路径:scraper_probe/main.py import logging import sys from scraper import fetch_page, parse_product_titles logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger(__name__) def main(url: str, keywords: list[str]) -> None: try: logger.info("开始探测目标站点: %s", url) html = fetch_page(url, expected_keywords=keywords) titles = parse_product_titles(html) logger.info("抓取成功,共解析到 %d 条标题", len(titles)) for title in titles[:10]: print(title) except RuntimeError as exc: logger.error("任务结束:%s", exc) sys.exit(1) except Exception as exc: logger.exception("未知错误:%s", exc) sys.exit(2) if __name__ == "__main__": target_url = "https://example.com/products" expected = ["产品", "列表"] main(target_url, expected)这里使用日志而不是print输出关键状态,是因为在实际项目里日志可以接入集中式日志平台,方便事后排查。
4.6 运行与验证
为了在本地无网络环境下验证效果,可以先用 Flask 启动一个模拟页面。创建mock_site.py:
# 文件路径:scraper_probe/mock_site.py from flask import Flask app = Flask(__name__) @app.route("/products") def products(): html = """ <html> <head><title>产品列表</title></head> <body> <div class="product-item"><span class="title">测试商品A</span></div> <div class="product-item"><span class="title">测试商品B</span></div> </body> </html> """ return html, 200, {"Content-Type": "text/html; charset=utf-8"} if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=False)安装 Flask:
pip install flask启动模拟站点:
python mock_site.py然后修改main.py中的目标 URL:
target_url = "http://127.0.0.1:5000/products" expected = ["产品", "列表"]再次运行:
python main.py预期输出:
2025-01-01 12:00:00 [INFO] 开始探测目标站点: http://127.0.0.1:5000/products 2025-01-01 12:00:00 [INFO] 抓取成功,共解析到 2 条标题 测试商品A 测试商品B如果页面改版,比如删除了“产品列表”这个关键词,探测函数会返回失败,main.py会打印类似 “探测未通过: 页面缺少预期特征关键词: 产品” 的日志并退出。这就是“先探测再承诺能用”的效果。
4.7 将探测结果用于任务调度
除了同步调用,探测结果还能用于定时任务调度。例如使用 Python 的schedule库或 APScheduler 时,可以每天先执行探测,探测失败则跳过抓取并发送告警:
# 伪代码示例:定时任务中接入探测 from apscheduler.schedulers.blocking import BlockingScheduler from probe import probe_site scheduler = BlockingScheduler() def daily_job(): result = probe_site("https://example.com/products", expected_keywords=["产品"]) if not result.ok: # 发送告警,例如企业微信、钉钉、邮件 print(f"抓取任务取消,原因:{result.reason}") return # 执行正式抓取 print("开始抓取任务") scheduler.add_job(daily_job, "cron", hour=8, minute=0) scheduler.start()这一段展示了探测机制的另一个价值:它不只是一个工具函数,而是任务调度的“前置阀门”。
5. 常见问题与排查思路
在实际使用探测机制时,可能会遇到各种问题。下面整理几种常见现象、原因和解决思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 探测请求总是超时 | 目标站点响应慢,或本地网络不通 | 调大 timeout,检查网络代理设置,测试能否用 curl 访问 |
| 状态码 200 但内容为空 | 目标站点是 JS 动态渲染,或返回了空模板 | 改为使用 Playwright/Selenium 渲染页面,或在探测中增加内容长度阈值 |
| 页面提示访问被拒绝 | 当前出口 IP 被目标站的风控拦截 | 更换节点或代理 IP,降低请求频率,检查是否被 WAF 拦截 |
| 页面标题正常但缺少预期关键词 | 目标站点改版,页面结构调整 | 更新 expected_keywords,并同步修改解析函数中的选择器 |
| robots.txt 模块返回不准确 | robots 解析失败,或目标站点没有 robots 文件 | 默认放行,但要在日志中记录异常,避免误判 |
| 探测通过,但正式抓取失败 | 正式抓取请求头和探测请求头不一致 | 保证探测请求和抓取请求使用同一套请求头、Cookie、会话 |
| 探测成功但频繁被限流 | 探测和正式抓取都算请求频率,短时间内请求过多 | 合并探测和抓取逻辑,降低总请求频率,设置随机间隔 |
排查时可以按这个顺序进行:先看网络层,再看 HTTP 状态层,然后看内容结构层,最后看业务规则层。不要一遇到失败就怀疑代码,先用 curl 或浏览器手动访问一遍目标 URL,确认目标站点当前的真实状态,再判断是探测逻辑写错了,还是目标站点确实发生了变化。
有一个容易被忽略的点是:探测请求本身也会产生访问压力,如果每个任务都做一次完整探测,在站点数量很大时可能反而增加被风控的风险。对于这种情况,可以把探测结果缓存一段时间,比如 5 分钟内的探测结论不重复请求。同时,多目标站点可以使用共享的 Robots 缓存,避免每次都去抓取 robots.txt。
6. 最佳实践与工程建议
6.1 合法合规与最小权限原则
爬虫探测和抓取都必须遵守目标站点所在地区和服务条款。使用 robots.txt 检查是一个基本门槛,但不是全部。实际工作中还需要满足:
- 仅访问公开页面,不尝试绕过登录、验证码、接口签名等安全机制。
- 遵守
robots.txt中的Crawl-delay指令。 - 不在高并发下压测目标站点。
- 生产环境抓取前获得合法授权,或在合法业务合作范围内进行。
探测模块并不代表“探测通过与合规等价”。它只能说明目标站点当前可访问,从技术层面做了基本检查,不能越过业务和法律边界。
6.2 请求头的稳定性
探测请求和正式抓取请求必须使用相同的请求头,最好通过统一的一个build_headers()函数生成。如果探测时用的是自定义 UA,抓取时用的是默认 requests UA,目标站点的风控可能对两个请求做出不同响应,导致“探测通过但抓取失败”的怪问题。
下面是一个统一定义请求头的示例:
# 文件路径:scraper_probe/headers.py DEFAULT_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", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", } def build_headers(extra: dict = None) -> dict: headers = DEFAULT_HEADERS.copy() if extra: headers.update(extra) return headers6.3 超时、重试与退避策略
爬虫请求不能无限等待。建议设置三档超时:
- 连接超时:3 秒。
- 读取超时:5 到 10 秒。
- 总探测超时:10 到 15 秒。
requests 的timeout参数可以传元组,例如timeout=(3, 5),分别代表连接超时和读取超时。重试时不要连续快速重试,推荐指数退避策略:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒。
使用requests.adapters.HTTPAdapter可以方便地配置重试策略:
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry( total=2, connect=2, read=2, backoff_factor=1, status_forcelist=[500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter)注意,重试只对幂等的请求安全。对于会触发写操作的请求,重试前要确认是否会重复执行业务逻辑。
6.4 日志与监控
探测结果本身就是监控指标。建议记录以下字段:目标 URL、探测结论、状态码、响应时间、拦截特征、失败原因、耗时。格式可以使用 JSON 便于日志平台检索:
{ "event": "probe", "url": "https://example.com/products", "ok": false, "status_code": 200, "response_time": 1.32, "blocked_by": "captcha", "reason": "检测到拦截特征: captcha", "ts": "2025-01-01T12:00:00Z" }如果任务运行在 Kubernetes 环境,可以把探测结果暴露为/probeHTTP 接口,供监控探针调用。这样当目标站点不可访问时,调度系统可以及时感知并告警。
6.5 内容指纹的维护
内容指纹是探测机制中非常实用但容易被忽略的部分。推荐维护一个site_profile配置文件,记录每个目标站点的特征关键词和解析选择器:
# site_config.yaml sites: - name: example_products url: "https://example.com/products" expected_keywords: ["产品", "列表"] parse_config: item_selector: ".product-item" title_selector: ".title"抓取前读取这个配置,探测时自动使用expected_keywords,解析时使用parse_config。这样即使站点改版,也能在配置变更后快速恢复,不需要改代码。
6.6 防止探测本身造成风险
最后要强调的是,探测请求本身也会产生日志和流量,对于日志和访问频率比较敏感的目标站点,需要在三层做好控制:
- 频率控制:同一目标站点最低间隔不低于 1 秒。
- 缓存:短时间内重复探测直接返回上次结果。
- 降级:robots.txt 获取失败时默认放行,但记录 warning 日志。
爬虫的稳定性不是靠更复杂的伪装请求头,而是靠对目标站点的状态感知和更保守的请求策略。探测机制是这一点的基础设施。
7. 总结与后续学习方向
本文从“很多爬虫运行时才发现目标站点不可用”的痛点出发,介绍了爬虫探测(probe)机制的设计思路与完整实现。核心要点是:正式抓取前先做低成本探测,从状态码、响应速度、页面特征、反爬拦截、robots 规则五个维度综合判断,得到ProbeResult后再决定是否执行抓取。实战部分给出了probe.py、scraper.py、main.py的完整代码,并演示了如何把探测结果接入定时任务调度。
接下来如果有余力,可以继续扩展这几个方向:
- 爬虫监控可视化:把每次探测结果写入数据库,用 Grafana 展示目标站点可用率趋势。
- 分布式抓取:把探测结果作为任务分发依据,让集群只抓取状态正常的节点。
- 渲染型页面探测:对依赖 JavaScript 的站点,结合 Playwright 或 Selenium 做渲染后的内容指纹检查。
- 代理池质量评估:用探测结果给代理 IP 打分,自动剔除被封节点。
本文代码只是一个起点,实际项目中你还需要结合自己的业务领域,为目标站点定制更精确的探测规则。如果你手头也有“每次跑都失败,换个人跑又能通”的爬虫项目,不妨先把探测模块加上,至少能让问题出现得更早、定位得更准。