简介面向大数据入门者与数据工程方向学习者的数据清洗与采集专题文档围绕采集技术架构的完整链路展开可用于课程论文写作、技术梳理与项目方案参考。内容从数据采集的意义与技术架构入手依次讲解网络爬虫的定义、分类及其在搜索引擎、数据挖掘、市场调研等场景中的应用并拆解由采集前置、数据采集层、数据存储层与辅助监控系统四部分组成的采集系统信源管理如何维护网站、栏目与关键词Redis 缓存与任务调度中心如何保证任务唯一性与并发控制数据经 SpringBoot 微服务接口推送至 Kafka 后由 Spark 消费、写入 HBase 并建立 Elasticsearch 索引。文档还讨论了结构化与非结构化数据源差异、批处理与流处理采集工具的选型标准以及分布式存储与分析系统的落地方式并给出服务器监控、数据质量校验等运维排错思路。整包为单个 docx 文档约 313KB已有 66 人学习。1. 采集系统最先出问题的地方往往不是爬虫写错了大多数人以为数据采集的难点在解析页面正则写错、结构一变就抓空。真正拖垮一套采集系统的常常是另一件事同一个采集任务在同一时刻被两个采集器同时取走同一篇文章进了两次库下游的数据清洗、去重、指标统计全部跟着返工。数据采集的技术架构之所以值得拆开看是因为它要解决的核心问题始终是“抓得全、抓得准、抓得可追溯”而“能不能抓到”反倒不是重点。网络爬虫只是这条流水线上最显眼的一环队列、调度、校验、监控才是真正决定数据清洗成本的隐藏环节。2. 采集前置为什么“信源管理”必须排在爬虫前面2.1 一条信源不是一条网址而是一组带状态的配置很多自建采集脚本的第一版都是这样写一个 URL 列表放在配置文件里循环抓取。跑到几十个站点还行跑到几百个就开始失控。原因不在于爬虫慢而在于“抓什么”这件事本身没有被建模。采集前置层要解决的就是这件事。它把每个抓取目标抽象成一条信源记录不同类型的信源在结构上并不等价字段含义为什么必须有source_id信源唯一标识后续所有任务、监控、数据质量都要按它回溯source_type网站 / 栏目 / 关键词 / 公众号 / 博主 / 电子期刊 / APP入口地址的构造方式与解析模板完全不同entry_url入口地址栏目是列表页关键词需要拼接搜索参数list_rule列表页抽取规则正则或选择器决定一次能拿到多少条详情链接detail_rule详情页抽取规则决定标题、正文、发布时间能不能取出freq采集频率调度层据此计算下次抓取时间status启用 / 暂停 / 降频 / 封禁运维要能在不改代码的前提下止损last_success_at最近一次成功时间判断“任务在跑但抓不到”这类隐蔽故障把“栏目”单列成一种信源类型而不是当成网站的附属字段原因是采集粒度不同一个站点可能有几十个栏目每个栏目的更新频率、翻页深度、页面模板都可能不一样。如果只按站点配置运维就无法做到“只精确采集某个栏目”只能整站扫既浪费资源又容易触发对端限流。关键词型信源同理它不是“某个网址”而是“某个搜索入口 一组查询词”它的产出量随热词波动必须独立统计。状态字段的价值在于它给了运维一个不必改代码的干预点。某个站点突然大面积 403正确动作是把这条信源置为降频或暂停让调度层不再给它派发任务而不是去重启采集器。这个设计看起来朴素却决定了故障发生时你是“改一条记录”还是“发一次版”。2.2 正则不只是解析规则它是信源的健康指标采集前置里通常还存着“正则状态”这类信息容易被误解成纯粹的配置项。实际上正则命中率是整套系统里最灵敏的故障信号之一。页面改版时最先失效的往往不是详情页抽取而是列表页规则列表正则匹配不到任何链接任务照常执行、照常“成功”返回只是产出为 0。如果监控只看任务有没有抛异常这种漏采可以安静地持续几周。业内常见的做法是把“单任务单次产出条数”作为一个指标落库与它过去 7 天的中位数做比对跌破某个比例就标记为可疑信源再反查是列表规则失效还是站点本身停更。这里有个容易忽略的细节产出为 0 有两种完全相反的含义。“站点没更新”和“规则失效”在数据上长得一模一样必须靠外部信号区分比如详情页规则是否还能命中、站点首页是否可访问、抓取耗时是否异常变短。只靠产出条数一个指标误报率会高到运维直接忽略告警。2.3 配置与运行态分离信源配置放关系库任务放 Redis一个常见的返工点是把信源配置和任务队列都塞进 Redis图省事结果每次抓取都去读一遍配置QPS 被放大几十倍更糟的是运维在页面上改了频率正在执行的任务读到的是半新半旧的配置。更稳妥的划分是信源配置属于“慢变量”放在关系库带版本号采集器启动时拉一份快照之后每隔固定周期比对版本号增量更新任务队列、任务归属、心跳这些“快变量”放 Redis只承载运行态。两层之间靠 source_id 关联而不是把配置整体塞进队列消息里。消息里只带 source_id 和少量参数采集器执行时从本地快照取配置这样配置变更对正在跑的任务是“下一轮生效”不会出现一个任务用两套规则解析同一批页面。这条边界的判断标准很简单如果某个字段的变化需要用事务保证一致性它就不该放在缓存里当运行态用。3. 采集层任务唯一性到底由谁保证3.1 唯一性不是调度中心保证的是存储层的原子操作保证的“同一任务、同一时间只能被一个采集器处理”这句话很多人第一反应是让调度中心做单点由它统一分发。这个思路在采集器数量少的时候能跑但它把可用性和正确性绑在了一起调度中心一挂全线停摆调度中心为了高可用做多实例又立刻面临两个实例同时派发的问题。真正的保证来自存储层的原子性而不是调度进程的唯一性。常见的有三条路径单键锁SET task:{id}:lock {worker} NX PX 30000靠 Redis 单线程执行命令的原子性。字段锁用 Hash 存task_id - worker_id靠HSETNX的原子性一个 Hash 可以装下成千上万个任务的归属省 key。状态机 CASUPDATE task SET statusrunning, owner? WHERE id? AND statuswaiting靠数据库行锁和受影响行数判定适合任务量不大但要求强一致的场景。调度中心多实例部署时每个实例都去竞争同一批任务谁抢到算谁的。这样调度层本身变成无状态的扩缩容不影响正确性只影响吞吐。3.2 频率是“下次抓取时间”不是 sleep“按设置的采集频率被采集”这句话实现上有一个分水岭是在采集器里sleep(interval)还是把下次抓取时间写进一个有序集合。sleep 的问题在于它把状态放在了进程内存里。采集器一重启所有节奏丢失任务一多每个任务占一个线程或协程等 sleep资源利用率极低更麻烦的是无法做全局错峰几百个任务如果都在同一时刻启动就会形成整点洪峰对端站点看到的就是一波集中请求。把下次抓取时间作为 score 写进 Redis 的 zset调度就变成了一个范围查询取出score now的任务即可。这套结构本质上是一个持久化的延时队列重启不丢、天然支持多实例竞争、还能通过给 score 加随机抖动来打散洪峰。import time, random def next_fetch_at(interval_sec: int) - int: # 基础间隔 ±10% 抖动避免同批任务在同一秒集中触发 jitter random.uniform(-0.1, 0.1) * interval_sec return int(time.time() interval_sec jitter) # score 存的是最早可以抓的时间不是入队时间 # 调度侧只做 ZRANGEBYSCORE dispatch -inf now不关心任务是什么时候进来的抖动幅度不宜过大超过 20% 就相当于悄悄改变了运维配置的频率语义排查时会对不上账。经验值是 5% 到 10%既能打散又不至于让“每小时一次”变成“50 分钟一次”。3.3 租约与回收崩溃后的任务不能凭空消失认领动作如果只是“从 zset 里弹出一条、写个 owner、然后执行”中间任何一步崩溃都会丢任务弹出去了但没写 owner任务既不在队列里也没人执行写了 owner 但采集器 OOM任务永远停在“处理中”。解决办法是把认领做成一次原子操作并且给归属加租约。下面这段 Lua 把“取出到期任务”和“抢占归属”合成一个原子步骤-- KEYS[1] dispatch:zset 待调度任务score 下次抓取时间戳 -- KEYS[2] task:owner Hashfield task_idvalue worker_id -- ARGV[1] now 当前时间戳 -- ARGV[2] worker_id 当前采集器标识 -- ARGV[3] batch 单次最多领取条数 local now tonumber(ARGV[1]) local batch tonumber(ARGV[3]) local ids redis.call(ZRANGEBYSCORE, KEYS[1], -inf, now, LIMIT, 0, batch) local taken {} for _, id in ipairs(ids) do -- HSETNX 保证只有 owner 为空时才能写入天然互斥 if redis.call(HSETNX, KEYS[2], id, ARGV[2]) 1 then redis.call(ZREM, KEYS[1], id) -- 同一脚本内出队杜绝二次领取 table.insert(taken, id) end end return taken脚本的原子性来自 Redis 的单线程执行模型整段逻辑执行期间不会插入其它命令所以“检查归属 出队”之间不存在竞态窗口。batch控制单次领取量取值要结合单任务的执行耗时来定——领太多会让某个慢站点拖住整个批次领太少则调度往返开销占比过高。租约是这套机制的另一半。task:owner里的归属需要配一个过期时间采集器在执行期间定期续租守护进程扫描“归属已过期但任务不在 zset 里”的记录把它重新推回队列。这里有个必须接受的代价回收意味着至少一次重复执行所以采集器本身必须是幂等的——同一篇文章被抓两次最终在存储层只能落一份。幂等这件事不能指望采集器自觉得靠下游的唯一键兜住这一点在第 4 章会展开。4. 存储层为什么要绕一圈 Kafka 再落库4.1 采集器只负责“拿到并结构化”落库是下游的事采集器直连数据库写 ES 或 HBase在任务量小的时候是最短路径一旦上量就会暴露三个问题。第一是背压传导。ES 写入变慢时采集器线程被阻塞队列里的任务开始积压最终整个调度节奏被打乱而真正该被限流的其实是“写入”这一段不该让“抓取”陪着一起卡住。第二是回放能力缺失。解析规则改了一版想把过去三天的数据重新处理一遍如果数据是采集器直接写进去的中间没有留存原始报文就只能重抓。第三是下游耦合。加一个新消费方比如做实时打标签要改采集器代码并重新发版。所以更常见的链路是采集器完成网页下载和结构化解析产出一条规范化记录经由统一的微服务接口推送到消息中间件再由计算侧消费。采集器在这里的职责边界非常清晰——只保证“我抓到的东西是结构化正确的”不保证“它一定被正确入库”。中间件承担削峰、解耦和短周期回放这三件事恰好是采集链路上最需要的。提示消息里要塞足够多的溯源信息至少包含 source_id、原始 URL、抓取时间、解析规则版本。缺了任何一个出问题时都只能靠猜。4.2 质量校验分两级入口拒收与旁路打标数据质量校验如果只放在一个地方通常两头不讨好。放在入口太严一条格式异常的数据会把整批卡住放在下游太松脏数据已经进了索引清理成本极高。更实用的做法是分级。入口侧只做“能不能进”的硬判定判定项少而明确标题非空且长度在合理区间、发布时间能解析成合法时间戳且不晚于当前时间太多、正文长度超过阈值、URL 归一化后不为空。任何一条不满足直接拒收并投递到死信通道附上失败原因避免它污染主链路。下游侧做“进得对不对”的软判定比如正文里是否混入了导航栏文本、标题是否与列表页标题不一致、发布时间是否落在站点历史发布时段的分布之外。这些判定不适合做拦截只适合给记录打标让它进 HBase 但不建 ES 索引。这样业务查询看不到脏数据排查时又拿得到原始样本。时间字段是最容易出问题的一项。很多页面显示的是“3 小时前”“昨天”这类相对时间正则只匹配yyyy-MM-dd就会大面积取空。取空之后如果用抓取时间兜底填充数据表面上完整了实际上把所有历史文章的时间都压成了采集当日——这个错误几乎无法事后修复因为原始信息已经丢了。4.3 ES 与 HBase 的字段组织一份数据两种排布同一份采集结果在检索侧和存储侧的组织方式完全不同。检索侧要的是“按关键词、时间、信源快速筛出一批文档”所以建倒排索引存储侧要的是“按主键取出完整正文”所以存宽表。ES 的 mapping 有几个点值得固定下来标题同时保留分词字段和keyword子字段前者供全文检索后者供精确聚合source_id用keyword避免被分词后聚合出奇怪的桶dedup_key也必须是keyword它是幂等写入的锚点。{ mappings: { properties: { dedup_key: { type: keyword }, source_id: { type: keyword }, title: { type: text, fields: { raw: { type: keyword, ignore_above: 256 } } }, publish_time: { type: date, format: strict_date_optional_time||epoch_second }, content: { type: text }, quality_flag: { type: keyword } } } }dedup_key的构造方式决定了去重质量。常见组合是“信源标识 归一化 URL”归一化要处理的包括协议差异、末尾斜杠、追踪参数各类 utm 前缀、分页参数顺序。只用原始 URL 做键同一个页面在带不带追踪参数时会被当成两条只用标题做键转载和多版本标题会被误合并。HBase 侧的 rowkey 要解决的是热点问题。如果把source_id直接放在 rowkey 开头同一站点的数据会全部落在一个 Region 上写入倾斜非常明显。常见做法是加一级散列前缀再接反转时间戳// rowkey hash(source_id) 前 2 字节 反转后的发布时间 doc_id // 散列前缀打散同一信源的写入热点反转时间让最新数据落在 rowkey 相邻位置便于范围扫描 String prefix md5Hex(sourceId).substring(0, 4); String reversedTs String.format(%019d, Long.MAX_VALUE - publishTimeMillis); String rowKey prefix _ reversedTs _ docId;加散列前缀的代价是“按时间扫全表”变成了“扫 N 个前缀再归并”如果业务上确实需要按时间做全量扫描这个代价要提前评估不能等到上线后发现扫描性能不达标才回头改 rowkey 结构。5. 避坑与排查采集链路上最常见的五类故障5.1 同一篇文章重复入库现象检索页里同一个标题出现多条正文一致但dedup_key不同HBase 里对应多个 rowkey。原因几乎都是 URL 没归一化。同一个详情页会以带追踪参数、带不同分页来源、http 与 https 混用等多种形态出现采集器把它们当成不同页面各抓了一次。更隐蔽的一种是列表页翻页产生了重复链接而列表层没有做页内去重。解决在解析完成、推送之前做一次 URL 归一化去掉已知的追踪参数、统一协议与末尾斜杠、参数按键名排序dedup_key用归一化结果构造写入侧靠dedup_key做幂等ES 直接把它当_idHBase 用CheckAndPut保证同键不覆盖已有内容。这套兜底做完即使采集器重复执行数据层也只留一份。5.2 发布时间大面积变成抓取时间现象某个信源的历史文章发布时间全部集中在采集当天时间分布图上出现一根尖峰。原因有两层。表层是解析规则只认标准日期格式遇到相对时间就取空深层是有人为了避免空值在兜底逻辑里写了“取不到就用当前时间”。这条兜底一旦上线错误就会被永久固化因为原始信息在入库那一刻就丢了。解决解析链做成“标准格式 → 相对时间换算 → 页面内其它时间线索如结构化标记中的发布时间”三级全失败则置空并打标绝不用当前时间填充。质量校验侧加一条规则单信源单批次中发布时间等于采集时间的比例超过阈值就告警。5.3 某个信源失败率突然飙升服务器指标却一切正常现象单个站点的任务失败率从个位数涨到 80% 以上采集器 CPU、内存、队列长度都在正常区间。原因通常在对端一侧频率过高触发限流、UA 单一被识别、出口地址集中。因为失败是“请求返回异常状态码”采集器本身没崩所以服务器监控看不出任何问题。解决把失败率按信源维度统计纳入信源健康度失败重试走指数退避加抖动而不是固定间隔重试触发阈值后自动降频并告警让运维决定是继续降频还是暂时停用。这类问题的排查入口必须挂在信源上挂在机器上永远找不到。5.4 消息积压重启后检索侧数据比存储侧多现象消费延迟持续上涨服务重启后对账发现 ES 文档数比 HBase 多出一截。原因是消费侧对外部系统的写入与位点提交不在同一个事务里。处理到一半崩溃位点没提交消息被重新消费而第一次的写入已经生效于是产生了重复。解决接受至少一次投递这个前提把幂等做在写入侧。ES 用dedup_key作为文档_id做覆盖写HBase 用确定性 rowkey 加CheckAndPut两边都做到“同一份数据写 N 次结果一致”。这样位点提交时机就只剩性能意义不再有正确性意义。5.5 任务“凭空消失”队列里没有、也没人在跑现象某个信源明明配了采集频率但连续几天没有任何产出队列里查不到它的任务采集器日志也没有相关记录。原因在认领与执行的顺序上任务被弹出队列后、归属写入前进程崩溃或者归属写了但租约过期后没有回收程序处理任务就停在了一个既不在队列、也没人负责的中间态。解决认领必须原子化3.3 节的 Lua 就是干这个的归属必须带租约和心跳续期并配一个守护进程定期扫描过期归属并重新入队。同时给“任务在队列中的最长停留时间”设一个上限超过就告警——任务可以慢但不能无声无息地停在那里。6. 监控与取舍四个子系统为什么不能合并成一个采集链路的监控通常会被拆成信源监控、采集监控、服务器监控、数据质量校验四块很多人第一反应是“这不就是四个看板吗合成一个页面不就行了”。真把它们合并最先出问题的是告警归因。这四类监控观测的对象根本不在一个维度上信源监控看的是外部站点的可访问性采集监控看的是任务状态与产出服务器监控看的是本端资源水位质量校验看的是数据本身对不对。它们之间的对应关系是多对多的——一台服务器上的采集器可能负责几十个信源一个信源的任务可能被调度到任意一台采集器上。合并成一个看板等于把“哪台机器出问题”和“哪个站点出问题”混在同一张图里故障发生时无法快速定位到是资源不足还是对端限流。监控子系统观测对象典型失败特征主要响应动作信源监控站点、栏目、公众号、博主的可访问性单信源失败率陡升本端资源正常降频、暂停、换入口采集监控任务状态、产出条数、调度延迟产出为 0 但任务显示成功反查解析规则、校验信源服务器监控CPU、内存、磁盘、存活状态多信源同时变慢本端指标异常扩容、调整采集器部署数据质量校验字段完整度、时间分布、去重率字段批量取空、重复率上升回滚解析规则、修数据拆开的真正价值在于每条告警都能直接映射到一个动作。信源监控告警对应“改配置”采集监控告警对应“查规则”服务器监控告警对应“加机器”质量校验告警对应“修数据或回滚版本”。合并之后告警只剩下“采集系统异常”运维还得自己从头判断是哪一层宝贵的响应时间全耗在归因上。这里也有取舍。拆分意味着四套采集与存储指标口径要对齐否则会出现“采集监控说今天抓了 10 万条质量校验只统计到 8 万条”的对账问题排查成本反而更高。我的做法是所有子系统共用同一套标识source_id贯穿始终task_id在采集监控和质量校验之间对齐时间窗口统一按抓取时间而不是入库时间切分。口径对齐的代价是一次性的归因能力的收益是长期的。验证这套架构有没有真的跑通我一般不看架构图只看三个可复现的动作一是把某个信源的列表规则故意改错看质量校验能不能在半小时内报出来二是杀掉一个正在执行任务的采集器看任务是否在租约过期后被重新捡起且数据不重复三是把消息中间件的消费停掉十分钟再恢复看检索侧和存储侧的对账是否仍然一致。这三个动作能过说明链路里的幂等、回收、解耦都是真的而不只是画在图上。从那以后我每次评审采集方案都会强制走一遍这三个动作尤其是“杀掉采集器”那一条——它最容易暴露“以为做了幂等其实只是没遇到过并发”的问题。希望帮到你。本文还有配套的精品资源点击获取