跳转到内容

爬虫

标签「爬虫」下的 2 篇文章

大规模无状态爬虫系统设计

这篇文章记录的是一套大规模无状态爬虫系统的设计。

先说明一下参与边界:这套系统不是我一个人独立设计完成的。我主要负责爬虫端的核心设计和实现,另一位同事是项目主要负责人,他有十余年的架构设计经验,整体系统设计、调度中心以及很多关键取舍都由他主导。我在这个系统里更多是站在爬虫端视角,参与了一套大规模无状态爬虫体系的落地。

也正因为那时自己还是初入职场,所以这套设计对我的意义不只是“写了一个爬虫”,而是第一次比较完整地看到:爬虫在工程系统里不应该只是脚本,它可以是一个被调度、被扩容、被监控、可替换的采集节点。

本文只讨论在授权和合规范围内的数据采集系统设计,不涉及绕过站点安全机制或采集敏感数据。

在做这个项目之前,Scrapy 是很自然会被想到的方案。

它有成熟的爬虫生命周期、调度器、下载器、中间件、管道、去重、状态管理等能力。对于中小规模、结构清晰、业务变化不频繁的采集任务来说,Scrapy 确实是一套完整方案。

但这套系统面对的问题不太一样。

我们更关注的是大规模任务下的采集吞吐、任务调度、账号分配、异常处理和快速扩容。Scrapy 自带的体系虽然完整,但学习成本较高,入手较慢,架构也相对复杂。尤其当系统需要把任务状态、账号状态、异常流转、代理分配、补偿处理这些能力统一放到一个调度中心管理时,爬虫本身再保留太多状态,反而会让边界变得不清楚。

所以最后的方向是:不沿用 Scrapy 的架构模式,而是结合现有高并发框架,设计一套更轻、更快、更容易水平扩展的无状态爬虫系统。

爬虫核心只负责一件事:拿到任务后尽快完成数据抓取,包括必要的增量更新,然后把结果交给后续链路。

按当时的架构草图抽象后,整体链路大概是这样:

大规模无状态爬虫系统架构图
任务、账号、代理由调度中心统一下发;爬虫节点保持无状态,采集结果进入 Kafka、Flink、ES 数据链路。

在这个体系里,Java 服务承担调度中心的角色。它负责任务协调、账号分配、账号状态管理、异常状态流转、代理下发等能力。

爬虫端则被刻意设计得很薄。

爬虫启动后向调度中心领取任务。调度中心在下发任务时,会同时给出这次采集所需的账号和代理。爬虫拿到这些一次性上下文后开始采集,采集完成后把数据写入 Kafka,并向调度中心汇报任务结果和心跳状态。

这里的“一次性”不是指账号用一次就丢弃,而是指一次采集任务内绑定一次任务、账号和代理。任务结束后,账号会根据结果重新回到有效账号池,或者进入异常账号池,等待专门的登录模块重新处理。

我觉得这套系统里最关键的设计,就是把爬虫做成无状态。

传统爬虫经常会在自己内部维护很多信息:当前任务跑到哪一步、账号是否可用、代理是否失效、失败后要不要重试、异常应该怎么处理、下次从哪里继续等。

这些能力当然有价值,但如果所有爬虫节点都各自维护状态,系统规模一大,就会出现几个问题:

  • 单个爬虫节点变重,扩容和迁移成本变高。
  • 账号、代理、任务状态分散在各处,难以统一判断。
  • 某个节点异常退出后,恢复逻辑复杂。
  • 错误处理混在采集逻辑里,爬虫代码越来越难维护。

无状态的思路是反过来的:爬虫只负责执行当前任务,不负责长期持有状态。

它不决定一个账号后续应该怎么处理,也不决定一个异常任务最终怎么补偿。它只把采集过程中的结果、错误和心跳上报给调度中心,由调度中心再调度给对应的处理模块。

这样做以后,爬虫端会变得非常轻。

如果某个爬虫节点挂了,系统只需要感知它心跳消失,再把未完成任务重新调度出去。爬虫本身不需要承担复杂恢复逻辑。对于我当时负责的爬虫端来说,这个设计最大的好处就是:代码目标非常明确,采集就是采集,错误就是上报。

这套系统里,爬虫单次采集任务大约 15 秒左右就可以完成。

它能快起来,原因不只是“并发写得高”,更重要的是系统边界清楚。

调度中心已经提前准备好了任务、账号和代理,爬虫不需要在执行过程中再做大量决策。拿到任务后,爬虫可以直接进入采集流程。它只处理当前任务所需的请求、解析、增量判断和结果投递。

采集结果进入 Kafka 后,后面的清洗、聚合、存储交给 Flink 和 ES 链路。爬虫不在本地做过多处理,也不会把数据链路和采集链路耦合在一起。

从工程上看,这其实是在减少爬虫节点的职责。

节点职责越少,单次任务越短,失败成本也越低。即使某个任务失败,也可以快速上报并进入调度中心的异常处理流程,而不是让爬虫自己在本地反复纠缠。

爬虫选择 Docker 部署,是因为这个系统天然需要横向扩容。

如果爬虫直接跑在固定机器上,扩容会比较麻烦。新机器环境要配置,依赖要安装,版本要对齐,启动方式也容易不一致。Docker 把运行环境打包后,爬虫就可以在任意一台机器上快速启动。

这带来了两个非常直接的好处。

第一,可以一键扩容。

当采集任务变多,或者需要在短时间内提高吞吐时,只需要增加爬虫容器数量。因为爬虫是无状态的,新启动的容器不需要同步复杂上下文,只要能连上调度中心,就可以开始领取任务。

第二,可以按数据采集情况动态调整数量。

任务高峰期增加爬虫节点,任务低谷期减少节点。爬虫节点本身不保存长期状态,所以扩容和缩容都比较自然。

这也是无状态设计和容器化部署非常契合的地方:一个节点随时可以来,也随时可以走,系统的长期状态不依赖它。

从图上看,可能会有一个疑问:代理池去哪了?

实际设计里,代理也由调度中心负责。

爬虫在领取任务时,调度中心会把任务、账号、代理一起下发。对于爬虫来说,它不需要自己去代理池里挑选代理,也不需要判断某个代理是否还应该继续使用。它只需要使用调度中心给出的代理完成当前任务,并把结果反馈回去。

这样设计的好处是统一。

任务、账号、代理在一次采集里是绑定关系。如果采集失败,调度中心可以结合错误类型判断问题出在哪里:可能是任务本身异常,可能是账号失效,也可能是代理不可用。爬虫端只提供事实,不做最终裁判。

这让异常处理有了更清晰的入口。

账号管理是这个系统里非常重要的一部分。

有效账号池保存当前可用账号。调度中心给爬虫下发任务时,会从有效账号池里分配账号。任务完成后,如果账号表现正常,就重新回到有效账号池,等待后续继续使用。

如果采集过程中发现账号异常,爬虫不会自己尝试修复账号,而是把异常上报给调度中心。调度中心再把账号放入异常账号池,由账号登录模块或专门处理模块去恢复。

恢复成功后,账号重新进入有效账号池;恢复失败,则继续留在异常状态,等待后续处理或下线。

这套流转看起来绕了一步,但它让职责非常清楚:

  • 爬虫负责发现和上报异常。
  • 调度中心负责状态流转和资源分配。
  • 账号登录模块负责账号恢复。
  • 有效账号池只保留可用于任务分配的账号。

当系统规模变大时,这种职责拆分会比“爬虫自己判断一切”更稳。

爬虫采集到的数据不会直接写入最终存储,而是先进入 Kafka。

Kafka 在这里承担缓冲和解耦作用。爬虫只需要稳定地把采集结果投递出去,不需要关心后续清洗、转换和索引写入的具体细节。

Flink 负责消费 Kafka 中的数据,做实时清洗、转换、去重或补充处理。处理后的数据再写入 ES,供后续检索和查询使用。

这条链路的好处是采集和处理分离。

爬虫节点只追求采集效率,数据处理链路则可以按自己的节奏扩展。如果后续清洗逻辑变复杂,也不会直接拖慢爬虫侧的执行。

这套设计对我最大的影响,是让我第一次真正理解“少做一点”有时候是更好的工程设计。

刚开始做爬虫时,很容易觉得爬虫应该什么都管:任务、状态、账号、代理、重试、异常、存储,最好都封装在一个完整框架里。但在大规模系统里,爬虫越重,越容易变成难以扩展的节点。

这套系统反而让我看到另一种思路:

爬虫不需要成为系统中心。它可以只是一个高性能、可替换、可扩容的执行单元。真正的状态和调度逻辑,应该放到更适合统一管理的位置。

对当时初入职场的我来说,这个认知很重要。

我开始意识到,架构设计不是把所有能力都堆进一个模块里,而是决定每个模块应该知道什么、不应该知道什么。一个模块越清楚自己不负责什么,边界往往越稳定。

回头看,这套无状态爬虫体系最让我印象深刻的地方,就是它把复杂性从爬虫端拿走了。

爬虫只领取任务、执行采集、上报结果;调度中心统一管理任务、账号、代理和异常;数据进入 Kafka、Flink、ES 组成的后续链路。每一层都有自己的职责,每一层也都可以独立扩展。

这比单纯写一个“能跑的爬虫”要更接近真正的工程系统。

爬虫与 JS 逆向面试题复盘

这是一组爬虫和 JS 逆向相关的面试题。

这类面试不会只问“会不会用 requests”,更常见的是从一个具体业务场景开始追问:怎么登录、怎么抓动态接口、怎么处理反爬、百万级数据怎么调度、数据怎么进入后续处理链路。

这篇文章按面试题复盘的方式整理,重点是把回答讲得更清楚、更工程化。

同时要先明确一点:爬虫和逆向要遵守法律、站点协议和数据合规要求。面试中可以讲技术思路,但不应该表达绕过风控、攻击站点或采集敏感数据的意图。更稳的说法是:在授权范围内做数据采集和接口分析。

题目:

登录有两种方式,一种是账号密码登录,并且需要输入动态 token;另一种是二维码登录。如果要自动登录,你会选择哪种方式,为什么?说说实现方法。

我的回答倾向是:优先选择账号密码加 token 的方式。

原因:

  • 账号密码登录更适合程序化请求。
  • 登录流程相对稳定,便于抓包分析。
  • 登录成功后可以拿到 token、cookie 或 session。
  • 二维码登录通常依赖人工扫码,不适合长期自动化任务。

可以这样回答:

我会优先选择账号密码加 token 的登录方式。因为它更容易通过请求和响应模拟,流程上可以先请求登录页或初始化接口,拿到登录所需的 token、cookie,再携带账号密码和动态 token 请求登录接口。登录成功后保存 cookie 或 access token,后续请求统一带上认证信息。二维码登录更适合人工确认,自动化成本更高,而且很多二维码登录会绑定设备、时效和扫码确认,不适合爬虫任务长期稳定运行。

一个简化流程:

请求登录页或初始化接口
|
获取 token / csrf / cookie
|
提交账号、密码、动态 token
|
登录成功
|
保存 cookie 或 access token
|
后续请求携带认证信息

需要注意:如果动态 token 是验证码、短信码、二次验证,不能假设可以无成本自动化。面试里可以强调“在授权账号和合规场景下处理登录态”。

题目:

这种前端返回数据的网站,如何爬取数据?

现在很多网站是前端框架渲染,HTML 源码里没有完整数据。此时不要急着解析页面,而是先看网络请求。

常规步骤:

  1. 打开 Chrome DevTools。
  2. 进入 Network 面板。
  3. 过滤 XHR / Fetch 请求。
  4. 找到真正返回 JSON 数据的接口。
  5. 分析 URL、请求方法、参数、Headers、Cookie。
  6. 用 Python 模拟请求。
fetch-api-data.py
import requests
url = "https://example.com/api/list"
headers = {
"User-Agent": "Mozilla/5.0",
"Referer": "https://example.com/list",
}
params = {
"page": 1,
"size": 20,
}
response = requests.get(url, headers=headers, params=params, timeout=10)
data = response.json()
print(data)

如果接口参数是动态生成的,就继续分析 JS。

如果页面确实没有接口,或者数据必须通过浏览器运行后才出现,可以考虑 Selenium 或 Playwright。但大规模采集时,优先分析接口,因为浏览器自动化成本更高。

题目:

面对百万甚至千万数据量的爬取,你的爬取策略是怎么样的?爬取到的数据如何存储?

这题考察的是系统设计,而不是单机脚本。

可以从四层回答:

  • 任务拆分。
  • 并发控制。
  • 反爬与容错。
  • 数据存储和后续处理。

一个比较完整的链路是:

任务调度 -> 爬虫采集 -> Kafka -> Flink 清洗 -> 数据存储

如果只是普通项目,可以存 MySQL 或 CSV;如果是百万、千万规模,就要考虑分批写入、去重、失败重试、数据清洗和存储扩展。

可以这样回答:

面对百万级数据,我不会用一个单机脚本顺序爬。一般会先把任务拆成分页任务、关键词任务或 ID 区间任务,放到任务队列里,由多个爬虫节点并发消费。采集时会限制请求频率,设置超时重试和代理池,避免单点 IP 或账号压力过大。采集到的数据先进入 Kafka,后续由 Flink 做实时清洗,再写入 MySQL、ES 或数据仓库。对于失败任务会记录状态,后续补偿重试。

常用 Chrome DevTools 的 Network 面板。

主要看:

  • XHR / Fetch 请求。
  • 请求 URL。
  • 请求方法。
  • Query 参数和 Request Payload。
  • Headers。
  • Cookie。
  • Response。
  • Initiator 调用来源。

如果参数是动态生成的,会继续去 Sources 面板断点调试,或在 JS 文件中搜索参数名。

可以从几个现象判断:

  • 请求参数里存在动态加密参数。
  • 接口依赖 token、cookie、签名或时间戳。
  • 请求频率过高会被封 IP。
  • 返回内容出现验证码、空数据或风控页面。
  • 同一个接口在浏览器能访问,程序请求失败。
  • Headers 缺失时返回异常。

可以这样回答:

我会先比较浏览器正常请求和程序模拟请求的差异。如果同样的 URL 在浏览器里返回正常,但程序里返回空数据、验证码、403 或风控响应,就说明可能存在反爬。再继续分析是否有动态参数、token 校验、cookie 校验、频率限制或行为检测。

XPath 和 CSS Selector 都能定位 HTML 节点。

对比项XPathCSS Selector
语法类似路径表达式类似 CSS 选择器
能力更强,支持轴、文本、复杂路径简洁,适合常见选择
可读性复杂表达式可读性一般简单场景更清晰
爬虫常用度很常用也常用

面试可以说:

简单页面我会用 CSS Selector,因为语法简洁;复杂定位,比如按文本、层级、相邻节点查找时,我更倾向 XPath。

可以按流程回答:

  1. 使用 Network 抓包找到目标接口。
  2. 确认哪个参数是动态生成的。
  3. 全局搜索参数名。
  4. 在 Sources 面板下断点。
  5. 观察 Call Stack 调用链。
  6. 找到最终生成参数的函数。
  7. 用 Python 或 Node 复现算法。

更完整的回答:

我在项目中遇到过接口参数由 JS 加密生成的情况。处理时先通过 Network 找到接口和异常参数,然后在 JS 文件中搜索参数名。如果搜索不到,就从请求发起位置或 XHR 断点入手,在 Sources 里下断点,结合 Call Stack 分析调用链,找到参数生成函数。确定算法后,再用 Python 或 Node 复现,最后和浏览器生成结果对比,确保请求参数一致。

这类回答要强调“分析和复现授权接口参数”,不要说成攻击或绕过安全系统。

JS 混淆后,变量名和函数名可能没有意义,所以不要期待完全看懂所有代码。

常见思路:

  • 不全量还原,只找关键链路。
  • 通过 XHR/fetch 断点定位请求发起位置。
  • 使用 Call Stack 看调用链。
  • 打印关键变量。
  • 对关键函数做输入输出对比。
  • 必要时把关键函数拎出来运行。

可以这样回答:

遇到混淆 JS 时,我不会从头读完整文件,而是围绕目标接口定位关键参数。通过断点、调用栈、关键变量打印和函数输入输出分析,逐步缩小范围,最终定位生成参数的函数。

Selenium 是浏览器自动化工具,适合复杂页面、需要真实浏览器环境的场景。

但它的问题也明显:

  • 启动浏览器成本高。
  • 并发能力弱。
  • 资源占用大。
  • 速度慢。
  • 大规模采集不划算。

所以一般优先分析接口直接请求。只有接口很难复现、页面强依赖浏览器环境、或需要真实交互时,才考虑 Selenium 或 Playwright。

面试回答:

Selenium 可以用,但我不会作为首选。因为大规模采集更关注吞吐和稳定性,直接请求接口效率更高。Selenium 更适合登录、复杂交互或无法绕开浏览器渲染的页面。

长期稳定运行靠的不是一个脚本,而是容错和监控。

常见机制:

  • 请求超时。
  • 失败重试。
  • 指数退避。
  • 异常捕获。
  • 失败任务记录。
  • 账号状态检测。
  • IP 或代理状态检测。
  • 任务监控。
  • 健康检查。
  • 失败告警。

可以这样回答:

我会为爬虫设计超时重试、异常捕获、失败任务记录和任务监控机制。如果请求失败,会根据错误类型决定重试、切换账号、切换代理或标记任务失败。系统层面会有健康监测和失败上报,保证爬虫可以长期稳定运行。

如果项目里支持 500万+ / 日 的采集规模,可以这样回答:

系统支持 500 万以上日采集量。采集任务不是由单个脚本完成,而是通过任务调度系统统一拆分和分发,多节点并发执行。采集结果进入 Kafka,再由 Flink 进行实时清洗和处理。

面试时不要只报数字,最好补上支撑数字的架构。

整体链路可以这样描述:

调度系统
|
v
爬虫节点
|
v
Kafka
|
v
Flink
|
v
MySQL / Elasticsearch / 数据仓库

各模块职责:

模块作用
调度系统生成任务、分配任务、协调账号
爬虫节点执行采集、解析数据、处理重试
Kafka解耦采集和处理,缓冲流量
Flink实时清洗、过滤、转换
存储层存储清洗后的业务数据

这类回答会比“我用 Scrapy 分布式”更有工程感。

调度系统主要负责任务生成和账号协调。

你笔记中的规模是:

  • 1400+ 爬虫任务。
  • 400+ 账号 Cookie。
  • 任务信息存储在 Redis。

可以这样回答:

调度系统会把采集目标拆成具体任务,任务状态存储在 Redis 中。爬虫节点从 Redis 获取任务,执行后回写任务状态。账号 Cookie 也由调度系统统一管理,分配任务时会根据账号状态选择可用账号,避免单个账号压力过大。

Redis 适合做任务队列和状态缓存。

原因:

  • 读写性能高。
  • 支持 List、Set、Hash、Sorted Set 等结构。
  • 适合存任务状态、账号状态和临时调度数据。
  • 操作简单,延迟低。

可以补一句:

如果任务需要更强的可靠性、确认机制和重试语义,也可以引入消息队列;Redis 更适合轻量级任务调度和状态管理。

账号失效的表现:

  • 登录失败。
  • Cookie 失效。
  • 返回 401、403。
  • 返回验证码或风控页面。
  • 请求结果为空或异常。

处理方式:

  • 标记账号不可用。
  • 暂停该账号任务。
  • 重新调度任务。
  • 切换可用账号。
  • 触发重新登录或人工处理。

可以这样回答:

系统会根据响应状态和页面内容判断账号是否异常。一旦发现 Cookie 失效或登录状态异常,就标记账号状态,避免继续分配任务,同时把未完成任务重新放回队列,交给其他可用账号处理。

常用库:

  • requests:发送 HTTP 请求。
  • httpx:支持同步和异步请求。
  • scrapy:爬虫框架。
  • lxml:解析 HTML,支持 XPath。
  • beautifulsoup4:HTML 解析。
  • selenium:浏览器自动化。
  • playwright:现代浏览器自动化。

项目里如果主要使用 requests + XPath,可以这样说:

普通接口采集我主要使用 requests,请求接口后用 XPath 或 JSON 解析数据。如果是复杂任务调度和大规模采集,会考虑 Scrapy 或自研调度系统。

基本做法:

request-timeout.py
import requests
try:
response = requests.get(
"https://example.com/api",
timeout=(3, 10),
)
response.raise_for_status()
except requests.Timeout:
# 记录超时并重试
pass
except requests.RequestException:
# 记录其他请求异常
pass

可以配合:

  • 固定次数重试。
  • 指数退避。
  • 失败任务入库。
  • 切换代理或账号。

常见方式:

  • 设置请求间隔。
  • 限制并发数量。
  • 使用任务队列控制消费速度。
  • 对单域名限速。
  • 对单账号限速。
  • 对异常响应动态降速。

面试里可以说:

控制速度不只是 sleep,而是结合并发数、任务队列、账号维度和站点响应来动态调整,避免触发反爬,也保护目标站点和自身系统。

Docker 的价值:

  • 保证运行环境一致。
  • 方便部署。
  • 方便横向扩展多个爬虫节点。
  • 便于隔离依赖。
  • 适合配合 CI/CD。

爬虫系统里尤其适合把爬虫节点容器化。需要扩容时,可以快速启动多个容器实例。

Kafka 主要承担数据通道和缓冲层。

作用:

  • 解耦采集和处理。
  • 缓冲高峰流量。
  • 支持高吞吐数据传输。
  • 方便后续多个消费者处理数据。

可以这样回答:

爬虫采集速度和后续清洗入库速度不一定一致,所以中间用 Kafka 解耦。爬虫只负责把原始数据写入 Kafka,Flink 再从 Kafka 消费并清洗处理。

Redis 在项目中可以承担:

  • 任务队列。
  • 任务状态缓存。
  • 账号 Cookie 管理。
  • 去重集合。
  • 临时失败记录。
  • 限速计数。

面试回答:

Redis 主要用于调度层,保存任务队列、任务状态和账号 Cookie。因为它读写快,并且数据结构丰富,适合管理这种高频变化的临时状态。

可以回答 JS 加密参数逆向。

更完整的说法:

最难的是 JS 加密参数逆向。因为网站 JS 做了混淆,不能直接通过阅读代码看懂逻辑。我通过 Network 定位接口和动态参数,再用 Sources 下断点,结合调用栈分析参数生成流程,最后把关键算法用 Python 或 Node 复现出来。这个过程比较考验调试能力和耐心。

处理步骤:

  1. 先复现问题,确认是哪些请求失败。
  2. 对比正常浏览器请求和爬虫请求差异。
  3. 判断新增机制:token、cookie、签名、频率、验证码、行为检测。
  4. 如果是参数变化,重新调试 JS。
  5. 如果是频率问题,调整限速和调度策略。
  6. 如果涉及强验证或合规风险,停止采集或走授权接口。

可以这样回答:

我会先分析新增反爬属于哪一类,再决定策略。如果是参数签名变化,就重新定位 JS 生成逻辑;如果是频率限制,就降低并发、调整账号和代理策略;如果是登录态或 Cookie 变化,就更新账号状态检测和重新登录流程。对于验证码或强风控场景,需要评估合规性,不能盲目绕过。

如果面试官让你整体介绍这个爬虫项目,可以这样组织:

这个项目主要做大规模数据采集。整体链路是爬虫采集、Kafka 缓冲、Flink 清洗、最终写入存储。爬虫侧通过 Chrome DevTools 分析接口,优先直接请求接口而不是 Selenium。调度系统负责管理 1400 多个任务和 400 多个账号 Cookie,任务状态存储在 Redis。系统支持超时重试、失败任务记录、账号失效检测和健康监控。项目中比较难的是 JS 加密参数逆向,我通过断点调试、调用栈分析和算法复现解决过接口动态参数问题。

爬虫和逆向面试题,重点不是只会某个库,而是能把采集链路讲完整:

接口分析 -> 登录态处理 -> 参数逆向 -> 任务调度 -> 并发控制 -> 数据通道 -> 清洗入库 -> 监控补偿

如果能把这条链路讲清楚,再结合自己实际做过的规模、账号调度、Kafka/Flink、Redis 和 Docker,回答就会更像真实项目经验,而不是零散知识点。