在实际的爬虫开发项目里,最容易被低估的问题不是解析规则写不对,而是明明已经在本地跑通了,换一个站点或者隔几天再跑,整条任务却突然失效。原因通常不是代码本身,而是我们过早地假设了目标站点可用:页面结构没变、接口没过期、编码没换、反爬没升级。要减少这种返工,可以在真正执行抓取之前加一道探测(probe)流程。所谓探测,就是用一个极小的请求,先检查目标站点的可达性、正文类型、robots 约束、渲染方式、登录墙和关键数据是否存在,再根据探测结果判断当前方案能不能继续。这个思路和部署前的健康检查很像,非常适合做爬虫任务编排、采集平台接入新数据源以及需要自动化验证的爬虫服务。下面用最小可运行代码从零实现一个带探测能力的 Python 爬虫脚手架,演示如何用一份探测报告决定抓取任务是否真的值得执行。
1. 为什么爬虫要先探测站点而不是直接开抓
1.1 爬虫失效往往不是代码错了,而是前提变了
很多爬虫项目的维护成本都来自“目标站点悄悄变化”。今天还能正常解析的页面,明天可能因为加了登录跳转、改成了div[data-id]、把数据迁移到接口异步加载,导致昨天的代码全部失效。这时候回滚、改 Selector、重新调试,费时费力。
这里的关键问题是:我们在没有验证前提的情况下,直接做了“承诺”。爬虫代码一旦进入生产任务,就等于承诺“当前站点可以用当前方案抓取”。但 Web 页面天然是不稳定的,它由目标方控制,随时可能调整。正确的做法是把抓取任务拆成两个阶段:
- 探测阶段:用少量请求确认站点当前状态是否满足抓取条件。
- 抓取阶段:只有在探测通过后才批量请求并解析数据。
这个拆分很像数据库迁移前的 preflight 检查,也像服务发布前的健康检查。先确认环境,再执行高风险动作。
1.2 探测是什么,它和试抓有什么区别
探测不是“试着抓一次数据”。它只关心关于页面的元信息,目标是回答“这个站点现在能不能抓、按现有方案能不能抓、抓之前还需要解决什么问题”。
试抓的问题是成本太高。一个完整的抓取流程通常包含多次分页请求、数据清洗、入库、去重。如果目标站点结构已经变化,这些步骤都白跑了,还可能把脏数据写进库里。更糟的是,完整抓取会比探测发送更多请求,更容易触发目标站点的限流或风控。
探测只做最轻量的请求:通常是页面本身加上robots.txt,有时再加一次资源类型判断。它不依赖具体业务字段,也不执行入库逻辑,所以失败时可以快速定位是哪一层出了问题。
1.3 一份合格的探测报告应回答哪些问题
在设计探测模块时,可以把以下问题作为验收标准:
- URL 是否可访问,最终状态码是什么。
- 响应内容是不是 HTML,还是 PDF、图片、JSON 接口。
- 页面声明的编码和实际内容编码是否一致。
- 目标站点是否通过
robots.txt禁止抓取。 - 是否存在登录墙、验证码或者 403/503 这类风控特征。
- 页面内容是否依赖 JavaScript 动态渲染。
- 我们在配置里声明的目标关键词或 DOM 样本是否真实存在。
- 页面文本量是否足够,是否存在空壳页。
这些问题都得到明确答案后,爬虫任务才能进入“可执行”或“不可执行”的状态。
1.4 探测结果不是永久承诺
探测报告只能代表“探测这一刻”的站点状态。目标站点后续仍可能改版、加验证码、换域名,所以探测结论不能写死成“这个站点永远能抓”。更合理的表达是:“在 HTTP 200、页面是 HTML、robots 允许、无验证码、非动态渲染、目标字段存在这些条件都满足时,当前爬虫方案可以工作。”这样不仅更准确,也让任务失败时能通过探测报告快速定位原因。
2. 搭建 probe-first 爬虫项目结构
2.1 环境与依赖
示例项目使用 Python 3.10+,核心依赖如下:
| 依赖 | 版本建议 | 作用 |
|---|---|---|
| Python | 3.10+ | 使用 dataclass 和现代类型标注 |
| requests | 2.31+ | 发送探测请求 |
| beautifulsoup4 | 4.12+ | 解析 HTML,提取标题和文本 |
| lxml | 5.0+ | 作为 BeautifulSoup 的解析器 |
创建虚拟环境后,先安装依赖:
python -m venv .venv source .venv/bin/activate pip install -r requirements.txtrequirements.txt内容:
requests>=2.31,<3 beautifulsoup4>=4.12,<5 lxml>=5.0,<6版本号建议以实际安装时 pip 解析出的稳定版本为准。这里写宽松版本,是为了避免镜像源或 Python 版本不同导致安装失败。
2.2 项目目录和模块边界
最小可运行的项目结构如下:
probe_first_scraper/ ├── requirements.txt ├── probe_site.py ├── decider.py ├── run_job.py ├── crawler.py └── samples/ └── mock_server.py每个文件只负责一件事:
| 文件 | 职责 |
|---|