如何通过实体解析去重抓取的数据
Web Data Collection Specialist
TL;DR:
- 四个列表页面返回了 71 条记录,解析为 66 个实体,其中有 5 个真实的重复组 — 同一本书在类别列表和分页目录中均出现。
- 在比较之前进行标准化是使精确密钥有效的关键:大小写折叠、Unicode 标准化和标点符号去除将同一标题的三种渲染形式转变为一个密钥。
- 阻塞是可测量的,而不是模糊的。66 个实体进行 2,145 次全对比;一个四字符的阻塞密钥将数量减少到 48 个块中的 154,减少了 92.8%。
- 在阈值之前选择评分者。同一对标题在
ratio上得分 87.8,而在token_set_ratio上得分 100.0。 - 阈值范围可以在您自己的数据中进行测量:不相关的实时标题达到了 63.4,而同一本书的三种渲染得分在 93.5 及以上。
- 在单个干净的目录中,精确密钥捕获了每个重复项,而模糊匹配没有找到超过 90 的内容 — 模糊匹配在不同来源之间获得其位置,而不是在单一来源中。
- 收集多源记录,这是与 Scrapeless 免费计划 对账的。
抓取一个网站时,重复项很少。通过类别列表和分页索引抓取同一目录,或在两个零售商之间抓取相同产品,构建的重复项就会出现 — 爬虫通过两条路径访问同一项目,而没有办法知道。
实体解析是将这些记录重新转化为事物的步骤。它在提取之后和存储之前运行,主要是一个廉价决策的序列:什么算作相同的字符串,什么算作相同的记录,以及哪个版本存活下来。
以下每个数字均来自于从四个实时列表页面收集的 71 条记录的一个集合。
Pipeline at a Glance
| 阶段 | 问题 | 机制 | 测量结果 |
|---|---|---|---|
| 标准化 | 这是相同的字符串吗? | 大小写折叠,NFKD,去除标点 | 从 71 条记录中得到 66 个独特密钥 |
| 精确密钥 | 这是相同的记录吗? | 按标准化密钥分组 | 5 个重复组,10 条记录归为 5 |
| 阻塞 | 哪些对值得比较? | 4 字符密钥前缀 | 从 2,145 对减少到 154,减少 92.8% |
| 模糊 | 这是同一事物,不同拼写吗? | token_set_ratio |
对于真实匹配得分 93.5–100,对无关则上限为 63.4 |
| 生存权 | 哪个记录胜出? | 字段规则,保持来源 | 具有 seen_in 和观察价格的一个实体 |
流程是 标准化 → 精确密钥 → 阻塞 → 模糊比较 → 合并。每个阶段的成本低于后一个阶段,因此每个阶段的存在是为了减少下一个阶段的工作量。
Stage 1: Normalise Before Comparing
描述同一产品的两个记录很少携带字节完全相同的字符串。大小写、重音和标点符号都会变化。
python
import re
import unicodedata
def norm(text):
text = unicodedata.normalize("NFKD", text or "").casefold()
text = re.sub(r"[^a-z0-9 ]+", " ", text)
return re.sub(r"\s+", " ", text).strip()
NFKD 的通行比看起来更重要。 Unicode 标准化附录 定义了几种形式,而兼容分解使得预先组合的 é 和裸 e 及组合重音进行比较时相等 — 这两种拼写在视觉上是完全相同的,而字节上不同,这正是会在输出中产生无形重复的情况。
大小写折叠而非小写化是匹配的对等者,W3C 对标准化的字符模型说明 是解释这两者在非 ASCII 文本中不同的参考文献。
在所收集的记录中:
text
[1] collected 71 records from 4 listing pages
raw distinct titles 66
normalised distinct titles 66
在这里是相同的,因为这个目录是干净的。这是值得知道而不是假设的 — 进行比较可以告诉您标准化在您的数据上是否在任何工作,才在其上构建任何内容。
Stage 2: Group on an Exact Key
使用标准化密钥,第一次通行是分组,而不是比较。其复杂度是 O(n),并且完全捕获每个完全相同的重复项。
python
from collections import defaultdict
by_key = defaultdict(list)
for record in records:
by_key[norm(record["title"])].append(record)
dupe_groups = {k: v for k, v in by_key.items() if len(v) > 1}
text
[2] exact-key duplicates: 5 group(s), 10 records collapse to 5
Sharp Objects x2 ['mystery', 'catalogue1']
In a Dark, Dark Wood x2 ['mystery', 'catalogue2']
In Her Wake x2 ['catalogue2', 'thriller']
The Elephant Tree x2 ['catalogue2', 'thriller']
Behind Closed Doors x2 ['catalogue2', 'thriller']
注意重复项来自哪里:每个组跨越两个不同的列表页面。没有单个页面包含重复项。这是总体形态 — 重复项是爬虫的属性,而不是页面属性,因此只读取一个列表的抓取器将不会看到它们,而读取四个的抓取器会看到。
每当页面发布一个稳定的标识符时,请将其作为密钥使用。产品 ID、ISBN 或规范 URL 路径优于标题,因为标题是营销文案,而不会在产品不变的情况下变化。发布的标识符方案正是为了让独立方能够就身份达成一致 — ISBN URN 命名空间规范 是书籍领域的例子,而暴露出一个的抓取页面已经为您解决了匹配问题。
Stage 3: Block Before Comparing Pairs
模糊比较是逐对进行的,而逐对比较是二次方的。对于66个实体,这意味着2,145次比较;对于10,000个实体,则接近5000万次。
阻止仅通过比较已经共享某些廉价内容的记录来缩小字段:
python
blocks = defaultdict(list)
for entity in merged:
blocks[norm(entity["title"])[:4]].append(entity)
blocked_pairs = sum(len(b) * (len(b) - 1) // 2 for b in blocks.values())
text
[4] 66 entities
all-pairs comparisons 2145
blocked on 4-char key 154 across 48 blocks
reduction 92.8%
交易是明确的:标题开始不同的记录永远不会被比较,因此一个过于激进的区块键会隐藏真正的匹配。对前四个字符进行阻止会错过像The Elephant Tree与Elephant Tree这样的一对,因为文章已经移动。常见的答案是基于排序令牌前缀、数字标识符,或者同时使用几个键并取候选对的并集来进行阻止。
第4阶段:模糊匹配,何时不需要
在这个目录上运行模糊匹配生成了值得诚实报告的结果:
text
[5] fuzzy near-duplicates above 90 (token_sort_ratio)
brute force 2145 pairs in 2.5 ms -> 0 candidate(s)
没有。经过规范化和精确分组后,一个干净的目录没有留下近重复项。这里的模糊匹配将是从未触发的代码。
当记录来自格式化标题不同的来源时,模糊匹配才显得必要。取一个真实的标题,并以三种不同的列表格式呈现它:
python
from rapidfuzz import fuzz
VARIANTS = [
"A Study in Scarlet (Sherlock Holmes #1)",
"A Study In Scarlet - Sherlock Holmes Book 1",
"A Study in Scarlet, Sherlock Holmes #1 [Paperback]",
]
keys = [norm(v) for v in VARIANTS]
print("distinct exact keys:", len(set(keys)))
for i in range(len(keys)):
for j in range(i + 1, len(keys)):
print(f"ratio {fuzz.ratio(keys[i], keys[j]):5.1f} | "
f"token_sort {fuzz.token_sort_ratio(keys[i], keys[j]):5.1f} | "
f"token_set {fuzz.token_set_ratio(keys[i], keys[j]):5.1f}")
text
distinct exact keys: 3
ratio 93.5 | token_sort 93.5 | token_set 100.0
ratio 87.8 | token_sort 87.8 | token_set 100.0
ratio 85.1 | token_sort 82.8 | token_set 93.5
一本书的三个键——精确键阶段在这里无能为力。而得分者改变答案的程度超过了阈值的影响。ratio将字符串作为序列进行比较,受到[Paperback]后缀的拖累;token_set_ratio比较令牌的集合,因此额外的单词不造成任何成本,前两个变体得分干净的100。
来自多个来源的记录需要对账吗?Scrapeless免费计划覆盖足够的请求,以收集第二个目录,使重复项显现。
从你自己的数据中选择阈值
阈值只能在测量的分离中进行辩护。这里有两个数字限制它:
| 测量 | 得分 |
|---|---|
两个真实不同的实时标题之间的最高token_sort_ratio |
63.4 |
三个同一本书的渲染中最低的token_set_ratio |
93.5 |
在这两个数字之间的任何东西都能在这些数据上清晰地分开这些集合。该方法具有普遍性:对已知匹配和已知非匹配的样本进行评分,查看分布停止重叠的位置,并将阈值放在缝隙中。从文章复制的单个全局数字是对他人数据的猜测。
在分布重叠的地方,诚实的答案是一个复审带——上限以上自动合并,下限以下自动拒绝,落在它们之间的则排队。统计记录链接几十年来一直以这种方式处理这个问题,美国人口普查局的记录链接研究是概率框架的标准参考。
第5阶段:生存性
判断两个记录是否相同留下了合并记录的内容问题。静默丢弃失败者会悄然扔掉匹配发生的证据。
python
def survivor(group):
best = sorted(group, key=lambda r: (r["href"] is None, len(r["href"] or "")))[0]
return {**best,
"seen_in": sorted({g["source"] for g in group}),
"prices": sorted({g["price"] for g in group})}
text
[3] 71 records -> 66 entities
merged example: 'Sharp Objects' seen_in=['catalogue1', 'mystery'] prices=['£47.82']
合并记录的两个属性很重要。seen_in保留了来源,因此错误合并在之后是可追踪的,而不是不可见的。而prices是一个集合而不是单个值:当两个来源不一致时,分歧是有趣的部分,将其简化为第一个排序的记录会摧毁这一点。
字段级规则胜过整个记录的获胜者。最长描述、最新时间戳、最完整的记录、最高可信赖来源——以字段为单位选择,而非记录,是防止合并继承某一来源的缺口之法。
这在管道中的位置
去重应在转换步骤中进行,在提取之后和写入之前。过早运行意味着规范化尚未解析的字符串;过晚运行意味着重复项已经在表中,修复变成了迁移。
收集来自多个来源的相同产品正是使该阶段必要的原因——竞争定价管道恰好具有这种形状,而通用抓取API则在其中一个来源进行客户端渲染时保持记录的形状一致。定价列出了额外来源的成本。
结论
实体解析是在一个昂贵阶段之前的四个便宜阶段。标准化决定哪些字符串算作相同,精确分组捕捉到所有一致的内容——这里有5组和10条记录——阻止移除92.8%不需要进行比较的配对,只有存活下来的才能达到模糊评分器。
有两个发现值得带入你自己的数据。评分器比阈值更重要:在相同配对上87.8对比100.0。而且在选择数字之前要测量分离,因为63.4到93.5的差距使这个选择显而易见,这是该目录的特性,而不是一个常量。
准备好从多个来源调和记录吗?从Scrapeless免费计划开始并收集第二个使重复项可见的目录。
常见问题解答
问:如何从抓取的数据中删除重复项?
标准化关键字段,基于它进行分组,然后合并每个组。大小写折叠、Unicode NFKD 标准化和标点符号剥离将视觉上相同的字符串变成一个关键字段,而分组是O(n)而不是成对的。在上面的71条记录中压缩了10条记录到5个实体,没有任何相似性评分。仅对通过的内容采用模糊匹配。
问:什么是实体解析?
决定哪些记录指向相同的现实事物并将其合并为一个规范记录。在单个来源内去重是相同的操作;该术语通常保留用于更难的跨来源情况,在这种情况下没有共享标识符,必须从字段相似性中做出判断。
问:什么是阻止,为什么它重要?
只有比较已经共享便宜关键的记录,因此成对阶段不会对所有内容进行运行。66个实体是2,145个可能的配对;一个四字符前缀关键字将其减至154,减少92.8%。其成本是不同关键的记录永远不会被比较,因此一个过于严格的块关键字会默默隐藏匹配。
问:我应该使用哪个模糊匹配评分器?
token_set_ratio 适用于从不同来源获取额外单词的标题,因为它比较令牌集并忽略额外内容——它在ratio 对同一对给出87.8时评分为100.0。当位置和顺序有意义时,例如代码或地址,请使用 ratio。在选择之前,测试两者与自己数据中的已知匹配项。
问:我应该设置什么相似性阈值?
量度它,而不是复制它。对已知匹配项和已知不匹配项的样本进行评分,并将阈值设置在分布停止重叠的地方。在这里,来自不同书籍的最高分是63.4,而来自一本书的不同渲染中的最低分是93.5,因此在那个范围内的任何内容都有效。在两个重叠的地方,高于自动合并、低于自动拒绝,并将中间排队进行审核。
问:哪个记录应该在合并后存活?
按字段选择,而不是按记录选择。取最长的描述,最新的价格,最完整的地址,并保留来源——上面的合并实体保留seen_in和所有观察到的价格集。保留源列表使得糟糕的合并可追踪;保留每个观察到的价格则保留了来源之间的分歧,这通常是你想要的信号。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



