3.2 供货源评审:数据源选择与评估 本节摘要:数据源评审用五维记分卡打分——内容质量、可获取性、稳定性、合规风险、增量能力——并坚持"记分卡只做初筛、试抓数据才有投票权"的原则。本节给出记分卡的实现与试抓验证的完整流程,配一个二选一的真实决策案例。 备料单(3.1)列出了要什么,这一节决定从哪里拿。评审的常见误区是凭印象拍板:"那个网站内容多"往往翻译过来是"那个网站首页看起来内容多"。调度室的规矩是:记分卡初筛,试抓终审。 五维记分卡 五个维度的定义先立清楚,打分才有共同语言: 内容质量:字段覆盖备料单的程度、内容深度、噪声率。一个源哪怕只能供两个字段,只要这两个字段极准,也比字段全但错漏百出的强。 可获取性:渲染复杂度、反爬强度、是否需要登录。
本节摘要:数据源评审用五维记分卡打分——内容质量、可获取性、稳定性、合规风险、增量能力——并坚持"记分卡只做初筛、试抓数据才有投票权"的原则。本节给出记分卡的实现与试抓验证的完整流程,配一个二选一的真实决策案例。
备料单(3.1)列出了要什么,这一节决定从哪里拿。评审的常见误区是凭印象拍板:"那个网站内容多"往往翻译过来是"那个网站首页看起来内容多"。调度室的规矩是:记分卡初筛,试抓终审。
五个维度的定义先立清楚,打分才有共同语言:

def score_source(name: str, quality: float, accessibility: float, stability: float, compliance: float, incremental: float, weights: dict | None = None) -> dict: """五维打分:合规是硬门槛,其余四维加权""" w = weights or {"quality": 0.3, "accessibility": 0.2, "stability": 0.2, "incremental": 0.3} if compliance < 0.6: # 合规低分直接出局 return {"name": name, "verdict": "出局", "reason": "合规风险高"} total = (quality * w["quality"] + accessibility * w["accessibility"] + stability * w["stability"] + incremental * w["incremental"]) return {"name": name, "verdict": "入围", "score": round(total, 2)} candidates = [ score_source("司法公开平台", quality=0.9, accessibility=0.7, stability=0.8, compliance=0.9, incremental=0.8), score_source("聚合转载站", quality=0.6, accessibility=0.9, stability=0.4, compliance=0.3, incremental=0.6), score_source("律所专栏集", quality=0.8, accessibility=0.6, stability=0.7, compliance=0.8, incremental=0.4), ] for c in sorted(candidates, key=lambda x: x.get("score", 0), reverse=True): print(c) # 输出: # {'name': '司法公开平台', 'verdict': '入围', 'score': 0.81} # {'name': '律所专栏集', 'verdict': '入围', 'score': 0.65} # {'name': '聚合转载站', 'verdict': '出局', 'reason': '合规风险高'}
聚合转载站可获取性最好(0.9),但条款明确禁止自动化采集且内容版权链条不清——硬门槛把它拦在加权计算之前。这一拦省下的不只是纠纷风险,还有后面为高反爬对抗支付的成本。
初筛前两名进入试抓。试抓不是正式采集的缩小版,它有专属的验收动作:按备料单字段算完整率、抽样看噪声、验证翻页与去重行为:
import asyncio, json from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode from crawl4ai import JsonCssExtractionStrategy async def trial_crawl(url: str, schema: dict, pages: int = 5) -> dict: """试抓终审:小规模抓取并出具完整率报告""" cfg = CrawlerRunConfig( cache_mode=CacheMode.BYPASS, extraction_strategy=JsonCssExtractionStrategy(schema=schema), ) rows: list[dict] = [] async with AsyncWebCrawler() as crawler: for p in range(1, pages + 1): result = await crawler.arun(f"{url}?page={p}", config=cfg) rows.extend(json.loads(result.extracted_content or "[]")) required = ["question", "answer", "publish_date"] complete = sum(all(r.get(k) for k in required) for r in rows) unique = len({json.dumps(r, ensure_ascii=False, sort_keys=True) for r in rows}) return {"抓取条数": len(rows), "字段完整率": round(complete / len(rows), 3), "重复率": round(1 - unique / len(rows), 3)} schema = {"name": "问答对", "baseSelector": "div.qa-item", "fields": [ {"name": "question", "selector": "h3.q", "type": "text"}, {"name": "answer", "selector": "div.a", "type": "text"}, {"name": "publish_date", "selector": "time", "type": "attribute", "attribute": "datetime"}, ]} report = asyncio.run(trial_crawl("https://legal.example.com/qa", schema)) print(report) # 输出:{'抓取条数': 473, '字段完整率': 0.964, '重复率': 0.032} # 结论:完整率过线,重复率低,准予进入正式采集
试抓报告的三个数字各有用场:完整率对照备料单验收线;重复率异常高说明翻页参数或去重逻辑有问题;抓取条数与预期的偏差提示页面结构与预想不同。报告连同记分卡一起留档——三个月后数据源出问题时,"当初为什么选它"的答案就在这里。
背景:舆情任务要在两个候选源里定主力。源甲是大型门户的评论区,内容量大、更新快,但页面为重脚本渲染,试抓五百条里三十七条是校验页(2.4 节的信号检测立功);源乙是中型垂直社区,量只有甲的三分之一,静态分页、robots 明确允许、试抓完整率 98.7%。
操作:记分卡初筛两源分差不大(甲 0.68、乙 0.73),按流程进入试抓终审。终审数据揭开了差距——甲的"可获取性 0.8"在试抓里露馅:有效产出率只有 76%,且渲染等待让单页成本是乙的四倍。
结果:定乙为主力源、甲为补充源(只抓其热门话题榜的轻量接口)。调整后单位成本产出提升两倍,封禁事件归零。
解读:这个案例的关键不是"静态源比动态源好",而是记分卡估的是名义值,试抓测的是实际值,两者之间的折扣(渲染成本、校验页损耗)只有真实跑一遍才现形。二选一的竞争性评审,比逐一独立评审更能暴露这种折扣。
变式:三个以上候选时,用"守擂"制——当前最佳源接受挑战者试抓对比,挑战失败就留档弃用,避免评审组合爆炸。
评审定案,货源进清洗车间。下一节讲脏数据的三层规整与两级去重——那是质检流水线上最脏也最见功力的一段。