news 2026/11/3 11:16:25

数据采集与清洗系统四层架构:从信源配置到ES落库的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据采集与清洗系统四层架构:从信源配置到ES落库的工程实践

简介:《数据清洗与采集论文》是一份面向大数据、数据工程方向学习者及课程论文写作者的专题文档,重点解决数据采集流程梳理、技术架构理解与写作素材组织等问题。文档共1个docx文件,压缩包约313KB,篇幅集中、便于检索。内容从数据采集的意义与网络爬虫的通用、聚焦分类讲起,依次拆解采集前置、数据采集层、数据存储层和辅助监控系统,并延伸到数据源类型、批处理与流处理工具、分布式存储及分析系统等环节,对Redis任务队列、Kafka消息中间件、Hadoop/HBase/Spark/ES等组件的作用也有说明,配有技术架构总结与数据质量校验思路。已有66人学习,适合用于课程作业参考、知识框架搭建、面试复习,也可为毕业设计或项目方案中的采集模块选型与架构表述提供借鉴。

1. 采集系统上线三个月,脏数据比没数据更难受

某公司做行业资讯聚合,第一版爬虫是三天赶出来的脚本,跑了两周,运营打开后台想拉一份周报,发现库里四成是重复标题,发布时间清一色是脚本运行当天,正文里还混着导航栏和版权声明。数据不是没有,是不能用。后来翻到这份《数据清洗与采集论文.docx》,它没给任何能直接跑的源码,给的是一张完整的工程地图:数据采集的信源怎么管、任务怎么排队、解析完的数据在哪一层做清洗和质量校验、监控该盯哪几个指标。这份文档适合两类人——正在从零搭采集系统的后端,以及手上已经堆了一摞爬虫脚本、但数据一进库就开始发臭的团队。它把链路拆成采集前置、采集层、存储层、辅助监控四段,每段都写清了职责边界和失败时的表现,这点比多数教程实在。

2. 四层架构拆解:从信源配置到 ES 索引的一条完整数据流

2.1 四层的职责边界和一次请求的走向

文档里画的那个结构,抽象成一句话就是:谁配、谁采、谁存、谁盯。采集前置是信源管理系统,负责网站、栏目、关键词、公众号、博主、电子期刊、APP 客户端这些采集源的增删改查,运营在后台改一个采集频率,链路末端立刻生效;数据采集层是干活的部分,Redis 放任务队列和过程数据,调度中心决定谁在什么时候采,多个采集器实例负责下载和结构化解析;数据存储层做两件事,一是把解析结果按统一格式推给 Kafka,二是在 Spark 里做质量校验、打标签,最后标题时间正文进 ES 建索引、全量信息进 HBase;辅助监控系统不属于主链路,但贯穿全程,分信源监控、采集监控、服务器监控、数据质量校验四个子系统。

这个分层最值得学的不是技术选型,是切分方式。它把「配置」和「执行」彻底隔开了——信源配置存在关系库里,采集器只认队列里的任务,不知道任务从哪来;采集器也不直接写数据库,所有数据统一走一个推送接口进 Kafka。这么做的好处是每个环节都能单独重启、单独扩容。我见过太多脚本式采集,配置硬编码在代码里,改一个站点的选择器要重新发版,而按这个结构,改选择器只是改一行模板记录。常见的错法是把调度逻辑写在采集器里,每个采集器各自算下一次什么时候跑,最后同一站点被采了八遍。

从数据源类型看,文档分了结构化(关系库、NoSQL)和非结构化(日志、社交媒体、音视频),对应的采集方式完全不同。结构化源一般走定时增量拉取,靠 update_time 或者自增 ID 做游标;非结构化源才需要爬虫这套东西。存储选型那段也说得直白:数据量过 TB 之后传统关系库顶不住,分布式存储成了必选项,Hadoop 管离线、HBase 管明细、ES 管检索、Kafka 管缓冲,各司其职。

2.2 信源管理系统的字段设计,整个链路的配置源头

文档只说了「方便运维人员对采集源增删改查」,没给表结构。真落地的时候这张表是整条链路的配置中心,字段少一个后面就得改代码。我一般会这么建:

CREATE TABLE collect_source ( id BIGINT PRIMARY KEY AUTO_INCREMENT, source_type VARCHAR(16) NOT NULL COMMENT 'WEB/COLUMN/KEYWORD/WECHAT/BLOGGER/JOURNAL/APP', name VARCHAR(128) NOT NULL COMMENT '信源名称,运营可见', entry_url VARCHAR(512) NOT NULL COMMENT '入口地址,关键词类存搜索词', charset VARCHAR(16) DEFAULT 'AUTO' COMMENT 'AUTO 走响应头+meta 探测', template_id BIGINT NOT NULL COMMENT '解析模板版本,决定选择器集合', cron_expr VARCHAR(64) NOT NULL COMMENT '采集频率,调度中心解析', priority TINYINT DEFAULT 5 COMMENT '1最高 9最低,决定入哪个队列', status TINYINT DEFAULT 1 COMMENT '1启用 0暂停 2仅监控不采集', fail_count INT DEFAULT 0 COMMENT '连续失败次数,达到阈值自动降频', last_run_at DATETIME NULL COMMENT '上次成功采集时间', UNIQUE KEY uk_type_url (source_type, entry_url(255)) );

逐字段说下为什么这么设计。template_id单独拎出来指向一张模板表,是为了让「选择器变更」和「信源变更」解耦——同一个新闻站的列表页模板可能被 30 个栏目信源共用,改一次全生效,而且模板表可以留版本历史,出问题能回滚。charset设成 AUTO 是有血泪经验的,国内不少老站点响应头声明 GBK 但实际内容是 UTF-8,硬编码必然乱码,只能靠 meta 探测加二次校验兜底。

priority不是排序用的,是分队列用的。高优先级信源(比如竞品官网)进collect:queue:p1,低优先级(历史归档站点)进collect:queue:p5,采集器实例按队列权重分配消费能力,避免一个慢站点把队列堵死。fail_count配合status做自动降频:连续失败 10 次把 cron 从 10 分钟拉到 2 小时,连续失败 50 次直接置为暂停并告警,防止调度中心对着一个已经下线的站点空转。uk_type_url这个唯一键是防手滑的,运营重复录入同一个栏目,插入时直接报错,比事后去重便宜得多。

提示:entry_url建前缀索引(255)是因为 MySQL 的索引长度限制,如果你的库开了更大的innodb_large_prefix,可以直接全字段索引,但采集 URL 超过 255 字符的情况极少,前缀够用。

2.3 一次采集任务从入队到入库的完整时序

信源配好之后,任务是怎么流动的?文档里没写时序,但按它描述的四层结构可以推出来,实际实现基本是这五步。

第一步,调度中心按固定周期(常见做法是每分钟一次)扫一遍collect_source里status=1且last_run_at + cron_expr已到期的记录,为每条生成一个任务,任务 ID 用source_id + 时间窗拼出来,比如src_1024_202406120900。这个 ID 设计很关键,它天然带时间窗去重——同一信源同一个十分钟窗口内只会有一个任务,即使调度中心因为抖动跑了两遍也不会重复入队。

第二步,任务 ID 按优先级LPUSH进对应的 Redis 队列,同时把任务详情写进collect:task:state:{task_id}这个 Hash 里,字段包括状态、重试次数、开始时间、worker 标识。

第三步,采集器实例用BRPOP阻塞式从队列取任务,取到之后先抢分布式锁,抢到才执行,抢不到就放回队列等下一轮。

第四步,采集器下载页面、按模板解析出标题、发布时间、正文、作者、来源,把结构化结果组织成统一格式,调 SpringBoot 的推送接口。

第五步,推送接口做基础校验后写 Kafka,同时把任务状态从RUNNING改成SUCCESS或FAILED,失败的任务按退避策略重新入队。Spark 那边消费 Kafka,做完深度校验和打标签,分别写 ES 和 HBase。

这五步里最容易出问题的是第四步和第五步之间——采集器解析成功但推送失败,如果没做本地暂存,这条数据就丢了。文档提到「数据传输负责将采集器解析出来的内容通过统一微服务接口推送」,隐含的要求是采集器必须能感知推送结果并重试,不能一推了之。

3. 采集层实操:Redis 任务队列与调度中心的并发唯一性控制

3.1 Redis 的键设计和队列优先级拆分

文档说 Redis 用来「缓存采集任务队列、过程数据」,一句话带过,实际上一套键名规划不好,后面排查能把人逼疯。我按职责把键分四类,命名统一用collect:前缀加业务域。

键名类型用途过期策略
collect:queue:p{n}List按优先级拆分的任务队列不设过期
collect:task:state:{taskId}Hash任务状态、重试次数、workerId7 天
collect:task:lock:{taskId}String执行权锁,NX 抢占300 秒 + 心跳续期
collect:source:meta:{sourceId}Hash信源配置缓存,避免每次查库10 分钟
collect:dedup:{yyyyMMdd}Set当日已采 URL 指纹,秒级去重2 天

按优先级拆队列的理由前面提过,这里补一句实际效果:p1 队列的消费超时设 30 秒,p5 设 120 秒,同样的采集器实例数,高优先级信源的实际延迟能压到 1 分钟以内。如果只有一个队列,一个卡死的视频站能拖慢整条链路,运营看到的就是「昨天还能采的站点今天全停了」,而服务器 CPU 才 5%,这种玄学问题查起来最费时间。

URL 指纹去重那个 Set 是为了挡掉列表页翻页导致的重复入队。做法很简单,入队前对规范化后的 URL 取 MD5,SADD返回 0 就说明今天采过,直接跳过。注意是「入队前」而不是「入库前」,越靠前挡掉越省资源。规范化要处理三件事:去掉 utm 之类跟踪参数、统一协议和域名大小写、去掉锚点。

缓存信源配置这一层也别省。调度中心每分钟扫一次,假设有 5000 个信源,每次都查库就是 5000 次查询,加上采集器取任务时可能还要读配置,数据库压力不小。用 Hash 缓存 10 分钟,配置变更时主动删 key,读的时候 miss 再回源,成本能降一个数量级。

3.2 唯一性保证:锁、心跳和任务状态机

「同一任务同一时间只能被一个采集器处理」这句话,文档只给了结论,落地要靠分布式锁。最朴素的写法是这样:

// 采集器消费循环,BRPOP 阻塞出队 + NX 锁抢占执行权 public CollectTask poll(String queueKey) { // 1. 阻塞 5 秒取任务,避免空轮询把 Redis QPS 打满 String taskId = redis.brpop(5, queueKey); if (taskId == null) return null; // 2. NX 保证只有一个 worker 拿到执行权,EX 是兜底 TTL String lockKey = "collect:task:lock:" + taskId; boolean locked = redis.set(lockKey, workerId, "NX", "EX", 300); if (!locked) { return null; // 已被别人持有,本轮放弃 } // 3. 抢到锁后写状态,标记为 RUNNING redis.hset("collect:task:state:" + taskId, "status", "RUNNING"); redis.hset("collect:task:state:" + taskId, "worker", workerId); return loadTask(taskId); }

这段的关键在参数。BRPOP的超时设 5 秒而不是 0(永久阻塞),是为了让 worker 有机会响应优雅停机信号,否则进程被 kill 的时候还挂在阻塞调用上,任务状态永远停在出队那一刻。EX 300是兜底过期,正常路径上必须起一个续期线程,每 60 秒把 TTL 重置回 300 秒,直到任务结束主动删除锁。不续期的后果很直接:一个大页面加图片下载跑了 6 分钟,锁在第 5 分钟自动过期,另一个 worker 抢到锁开始重复采,同一个任务被处理两遍,重试次数还被算乱了。

任务状态机建议只设五个状态:PENDING(入队)、RUNNING(抢到锁)、SUCCESS、FAILED(可重试)、DEAD(超过最大重试次数)。状态流转只能单向,SUCCESS和DEAD是终态。文档里监控子系统说「监控每个采集任务的状态,以便对异常任务、数据漏采等情况进行排查」,排查的前提就是状态机足够简单,运维看一眼状态分布就知道是队列堵了还是解析挂了。

锁的粒度也要想清楚。按任务加锁是最细的,同一个信源的不同时间窗任务可以并行;如果按信源加锁,能防止对同一站点并发请求过快被封,但会降低吞吐。折中做法是两层锁:信源级用 Redis 的令牌桶限速(比如每秒最多 2 个请求),任务级用上面的 NX 锁。文档没提限速,但没有限速的采集系统活不过一个月,这条属于必须自己补的。

3.3 调度频率、退避重试和采集器的解析职责

调度中心的频率控制有个容易忽略的点:cron_expr是「期望频率」,不是「保证频率」。如果队列积压,任务实际执行时间会晚于计划时间,这时候调度中心不该继续往队列里塞新任务,否则积压越来越深。常见做法是给每个信源维护一个「在途任务数」,超过 1 就不再生成新任务,等前一个出结果再说。文档说「保证任务按设置的采集频率被采集」,严格说是做不到的,能保证的是「不早于这个频率」。

重试策略我一般用阶梯退避:第 1 次失败 1 分钟后重试,第 2 次 5 分钟,第 3 次 30 分钟,第 4 次 2 小时,第 5 次直接进死信队列并把fail_count加一。区分错误类型很重要——连接超时、5xx 这类可以重试,404、403 这类重试没意义,直接进死信并触发信源监控告警。文档的监控子系统里有「信源系统监控:监控网站、栏目、公众号、博主等状态,保证其正常访问」,这个告警就是它的输入端。

采集器本身的职责,文档写得很清楚:网页下载、数据结构化解析、任务监控。拆开看是三块。下载层要做连接池复用、超时控制(连接 5 秒、读取 15 秒是个合理起点)、UA 和 Cookie 管理、编码探测;解析层靠模板,列表页用 CSS 选择器或者 XPath 定位条目,详情页要处理正文抽取——简单的用选择器,复杂的站点选择器经常失效,得fallback 到正文密度算法;任务监控层负责上报耗时、状态、失败原因,这些数据直接喂给辅助监控系统。

解析结果一定要带parseTemplateVersion。模板改了之后,历史数据的解析口径和新的不一致,排查「为什么上周的标题带尾巴这周不带」的时候,没有版本号就只能靠猜。这个字段成本几乎为零,价值极大。

4. 清洗与落库:SpringBoot 推送接口到 Kafka、Spark、ES 的完整链路

4.1 统一数据模型和推送接口的约定

数据存储层的第一道关口是推送接口。文档说「通过统一的 SpringBoot 微服务接口,推送到 Kafka 中间件,同时对数据的质量进行校验,主要校验发布时间、标题、正文等解析的准确度」。统一的意思是所有采集器的输出必须是同一个结构,不管来源是网站、公众号还是 APP。

消息体我一般定成这样:

{ "taskId": "src_1024_202406120900", "sourceId": 1024, "sourceType": "COLUMN", "url": "https://example.com/news/123.html", "title": "某行业季度报告发布", "publishTime": "2024-06-12 08:47:00", "author": "", "content": "正文纯文本,已去除标签和多余空白", "contentHash": "9f2c4a1e8b7d...", "parseTemplateVersion": 7, "collectTime": "2024-06-12 09:00:12", "retryCount": 0 }

字段解释一下。content存纯文本不存 HTML,是为了让下游 Spark 少做一次解析,而且 HTML 里的样式类名经常变,存下来反而干扰比对。contentHash是对「标题 + 正文前 2000 字」做 MD5,这是去重的主键,比 URL 去重可靠得多——同一篇文章经常有多个入口 URL,靠 URL 根本挡不住。publishTime允许为空字符串,不能填采集时间,这个坑后面单独说。

taskId和sourceId保留是为了溯源,出问题时能顺着这两个字段反查到具体哪次调度、哪个信源配置。parseTemplateVersion前面说过,是解析口径的版本标记。retryCount让下游能识别出这条数据是重试来的,Spark 里可以对重试数据放宽某些校验(比如时间戳容差),避免正常数据被误判。

接口层面有两个约定值得单独强调。一是幂等,同一个taskId重复推送必须能被识别,接口侧用 Redis 做SETNX挡掉,或者干脆不管,让下游按contentHash去重。二是背压,采集器推送要设超时和重试上限,接口返回 429 就退避,不能无限重试——Kafka 写满的时候接口会变慢,采集器如果死等,整个采集层就雪崩了。这两条文档没写,但凡是生产环境跑过的都知道躲不开。

4.2 Kafka topic 规划和 Spark 消费的清洗规则

topic 按数据量拆,别都塞一个。常见拆法是按信源类型分:collect.web(网站和栏目)、collect.social(公众号、博主)、collect.app(客户端、期刊)。分区数按峰值吞吐估,单分区 5~10 MB/s 是个保守值,3 个 topic 每个 6 分区起步够用一阵。消息 key 用contentHash,保证同一篇文章落到同一分区,下游做窗口去重方便。

Spark 消费侧的核心工作是清洗。文档提到要校验「发布时间、标题、正文的解析准确度」,还提到打标签和特定信源监控。我把校验规则写成这样:

import re, hashlib from datetime import datetime, timedelta def normalize(doc, collect_time): errors = [] title = (doc.get("title") or "").strip() content = (doc.get("content") or "").strip() # 标题:长度 5~200,不能是站点名或纯导航词 if not (5 <= len(title) <= 200): errors.append("title_length") # 正文:少于 100 字基本是解析失败,大概率抽到了导航栏 if len(content) < 100: errors.append("content_too_short") # 发布时间:多格式尝试,失败就置空,绝不回填采集时间 raw = (doc.get("publishTime") or "").strip() pub = None for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M", "%Y年%m月%d日 %H:%M"): try: pub = datetime.strptime(raw, fmt); break except ValueError: continue if raw and pub is None: errors.append("publish_time_unparsable") # 未来时间或早于 2000 年,都视为解析错误 if pub and (pub > collect_time + timedelta(hours=1) or pub.year < 2000): errors.append("publish_time_out_of_range") pub = None doc["publishTime"] = pub.strftime("%Y-%m-%d %H:%M:%S") if pub else "" doc["contentHash"] = hashlib.md5((title + content[:2000]).encode()).hexdigest() doc["qualityFlags"] = ",".join(errors) return doc

逻辑上分三层。第一层是必填校验,标题和正文长度是硬指标,正文不足 100 字直接判为解析失败——这个阈值不是拍脑袋,正常新闻正文很少低于这个数,而导航栏加版权声明的长度恰好在这个区间。第二层是发布时间解析,多格式尝试之后仍然失败,就把字段置空并打标记,绝对不能用采集时间代替。这一条是全文最有价值的一句话,因为发布时间一旦被污染,后面所有的时间维度分析全是错的,而且错得很隐蔽,重新采集才能修复。第三层是异常值过滤,未来时间、2000 年以前的时间基本都是解析串位,置空比留着强。

qualityFlags这个字段是给运营看的。带 flag 的数据不进 ES 主索引,进一个「待复核」索引,运营抽样看几条就能判断是模板失效还是站点改版,比翻日志快得多。文档里「数据质量校验:根据异常数据反查信源等配置」说的就是这个闭环。

4.3 ES 索引设计和 HBase 的冷热分工

Spark 处理完往两边写。ES 存标题、时间、正文、来源、标签这些检索和展示需要的字段,HBase 存全量原始数据包括 HTML,供事后回溯。这个分工的逻辑很实际:ES 存全量 HTML 会让索引膨胀几倍,查询还慢;HBase 便宜、能存大字段,但没法做全文检索。

ES 的 mapping 一定要手写,别用动态映射。动态映射会把publishTime识别成 text,之后按天聚合直接报错,得重建索引才能救。我用的 mapping 骨架:

{ "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "fields": { "raw": { "type": "keyword" } } }, "content": { "type": "text", "analyzer": "ik_max_word" }, "publishTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" }, "sourceId": { "type": "keyword" }, "contentHash": { "type": "keyword" }, "qualityFlags":{ "type": "keyword" }, "collectTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" } } } }

title同时给 text 和 keyword 两种类型,是为了既能全文搜又能精确聚合去重;contentHash设 keyword 是因为要做terms聚合查重;sourceId用 keyword 不用 long,是为了兼容部分信源 ID 带前缀的情况。索引按天或按月滚动,用别名指向最新索引,老索引关掉副本降成本。

HBase 的 rowkey 用contentHash反转加时间前缀,反转是为了避免写热点——同一时间段的数据 hash 前缀分布随机,region 能均匀扛住写入。列族拆两个,info存结构化字段,raw存原始 HTML 和响应头,后者压缩率开高一点。

监控这块,文档列的四个子系统基本覆盖了必须盯的维度,落到具体指标上大概是:信源监控看各站点最近一次成功采集时间和连续失败次数;采集监控看队列长度、任务成功率、平均耗时、死信数量;服务器监控看 CPU、内存、磁盘、进程存活;质量校验看qualityFlags的分布变化。四个维度里最容易漏的是最后一个,前三个挂了会立刻报警,质量是慢慢劣化的,等运营发现数据不对,可能已经污染了半个月的数据。

5. 避坑排查:上线后最容易翻车的五个环节和对应解法

5.1 队列和锁:任务卡住、积压、重复采

现象一:后台看到一批任务状态永远停在 RUNNING,队列长度不降。原因基本是采集器进程被 kill 或者 OOM 之后,锁没释放,任务状态也没回写,而队列里的任务已经被BRPOP弹出去了。解决分两步:锁必须带 TTL 加心跳续期,worker 启动时扫一遍自己名下的 RUNNING 任务做补偿,超过 2 倍超时时间还没结束的直接改回 PENDING 重新入队。另外给 worker 注册优雅停机钩子,收到信号先把当前任务处理完或者主动释放锁,别硬扛。

现象二:队列堆积了几万条,服务器 CPU 却只有 5%。原因是单个 worker 用同步阻塞的方式处理任务,一个响应慢的站点(比如要等 30 秒才返回的政府站)把整个消费线程占住。解法是拉取和处理分离:主线程只负责BRPOP和抢锁,把任务丢进本地线程池执行,线程池大小按「平均耗时 × 目标 QPS」估算。同时给每个信源配独立的信号量,防止慢站点占满所有线程。文档没写这块,但并发模型选错,后面加机器都救不回来。

现象三:同一篇文章在库里出现三遍,contentHash还不一样。原因通常不是去重失效,是正文里混进了随机内容——推荐位、阅读数、时间戳。同一篇文章两次抓取正文有细微差异,hash 自然不同。解决是先做正文清洗再算 hash:去掉连续的空白和换行、剔除常见的干扰段落(含「责任编辑」「本文来源」「点击查看更多」的行),再取前 2000 字。如果还压不住,降到 SimHash 加汉明距离阈值 3,用 Spark 侧做近似去重。这条属于典型的「以为去重写好了其实没写对」。

5.2 解析和落库:时间污染、大消息、映射翻车

现象四:报表里所有文章的发布时间都是同一天。原因很明确,解析器抽不到发布时间时用了new Date()兜底,或者取的是列表页的渲染时间。这条错误的可怕之处在于它不会报错,数据看起来完整,只有做时间序列分析时才发现全是平的。解决是把空值当合法值处理,抽不到就留空并打publish_time_missing标记,让质量看板去暴露问题,而不是用假数据填坑。另外时间解析要限定合理区间,未来时间、2000 年以前一律判为异常。

现象五:Kafka 偶发拒绝消息,日志里是 RecordTooLargeException。公众号长文加上内嵌的图片 base64,一条消息能到几 MB,超过 broker 默认的 1 MB 限制。治标的做法是调大max.request.size和message.max.bytes,但治标不治本,broker 的内存压力会变大。我一般改成正文超过 512 KB 就把内容落对象存储,消息里只带一个 key,Spark 消费时按需拉取。这样消息体恒定在几 KB,吞吐稳定,重放也方便。

还有一个和 ES 相关的:索引用了动态映射,sourceId被识别成 long,后来某个信源 ID 带了前缀变成字符串,写不进去还报类型冲突。解法是上线前就把 mapping 固化,所有可能做聚合和精确匹配的字段统一用 keyword,新增字段走索引模板而不是让 ES 自己猜。这类问题在索引建立当天发现最省事,等积累了半年数据再改,只能重建。

6. 二次开发:把文档里的架构图改造成能跑的最小闭环

拿到这份文档别一上来就照着搭 Hadoop 全家桶,那是给已经有大数据平台的团队准备的。我建议先砍到最小闭环:一个 Redis、一个采集器、一个本地文件或 SQLite 落地,把「信源配置 → 入队 → 抢锁 → 下载解析 → 清洗 → 去重」这条链路跑通,再逐个替换组件。替换顺序按痛点来,通常是先加 Kafka 解耦采集和入库,再加 ES 支撑检索,HBase 最后上,因为它是成本最高、收益最晚的一环。

# 解析回归脚本:每次改模板前先跑一遍,看通过率有没有掉 import json, sys from extractor import parse_detail # 你的解析实现 cases = json.load(open("golden_samples.json", encoding="utf-8")) failed = [] for c in cases: doc = parse_detail(open(c["html"], encoding="utf-8").read(), c["url"]) for field, expect in c["expect"].items(): if doc.get(field) != expect: failed.append((c["name"], field, expect, doc.get(field))) print(f"通过 {len(cases) - len(failed)}/{len(cases)}") for f in failed[:10]: print(f) sys.exit(1 if failed else 0)

这个脚本的价值在于给解析模板上保险。golden_samples.json里每条记录一个信源、一份存档 HTML、一份期望输出,字段比对要求完全相等。模板改动后先跑一遍,通过率掉了就地回滚,比上线之后靠运营反馈快得多。样本量不用大,每个信源两条(一条正常、一条异常页面)就够,关键是长期维护——信源改版的时候顺手把新页面的样本补进去,半年后你会感谢自己。

验证方法上还有个小技巧:给每个信源加一个「影子模式」,新模板先不落库,只把解析结果和旧模板的结果做 diff 打到日志里,观察一两天没问题再切正式。这个机制在信源批量改版的时候能救命,我吃过一次亏,三十个栏目一次性换模板,有个站点的标题选择器写错了一位,跑到第二天才发现标题全是列表页的栏目名,那批数据只能重采。从那以后我每次改解析模板都强制走一遍黄金样本集加影子比对,不管改动多小。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 11:16:18

AI论文写作工具千笔:提升科研效率的全流程助手

1. 项目概述&#xff1a;AI论文写作的革命性工具 作为一名在学术圈摸爬滚打多年的研究者&#xff0c;我深知写论文的痛苦&#xff1a;从选题构思、文献综述到实验设计、论文撰写&#xff0c;每个环节都让人头秃。直到遇到"千笔"这个AI论文工具&#xff0c;我才发现原…

作者头像 李华
网站建设 2026/11/3 11:15:07

2026制造业人事系统测评:复杂考勤与薪资计算解决方案

1. 项目背景与测评目标 2026年的制造业人事管理正面临前所未有的复杂挑战。随着柔性生产模式的普及和全球供应链重组&#xff0c;制造企业普遍呈现出"多基地协同灵活排班复合薪酬"的运营特征。传统人事系统在应对以下场景时已显乏力&#xff1a; 跨时区工厂的协同排…

作者头像 李华
网站建设 2026/11/3 11:13:42

MySQL连接机制与性能优化实战指南

1. MySQL连接机制深度解析 作为关系型数据库的核心功能&#xff0c;MySQL连接机制直接影响着应用程序的性能和稳定性。在实际生产环境中&#xff0c;我们经常遇到连接超时、连接池耗尽、连接泄漏等问题&#xff0c;这些问题往往源于对MySQL连接机制理解不够深入。今天我将结合十…

作者头像 李华
网站建设 2026/11/3 11:12:33

电商交易系统设计:核心挑战与分布式事务实践

1. 交易模块设计的核心挑战与业务边界 交易系统作为商业行为的数字载体&#xff0c;其设计复杂度往往被严重低估。我曾参与过某跨境电商平台从零构建交易模块的过程&#xff0c;最初团队认为"不就是下单付款么"&#xff0c;结果上线首日就因促销活动导致订单状态机崩…

作者头像 李华
网站建设 2026/11/3 11:11:46

STM32F427+C620/M3508:CAN与PWM控制、PID调参与避坑

简介&#xff1a;面向RoboMaster竞赛调试与STM32嵌入式控制学习的C620电调Demo程序说明文档&#xff0c;围绕M3508电机在PWM与CAN两种模式下的驱动展开&#xff0c;适合已具备Keil5和HAL库基础、希望理清电调接线、工程配置与控制逻辑的读者。压缩包仅含1个PDF文件&#xff0c;…

作者头像 李华
网站建设 2026/11/3 11:10:39

Node.js核心技术优势与2024年开发现状解析

1. Node.js 现状与争议解析 Node.js 自2009年诞生以来&#xff0c;经历了从爆发式增长到稳定发展的完整技术生命周期。近年来随着Deno、Bun等新兴JavaScript运行时的出现&#xff0c;社区中确实存在"Node.js是否过时"的讨论。但根据2024年Stack Overflow开发者调查报…

作者头像 李华