如何使用 Scrapeless 构建增量网页抓取管道
Senior Web Scraping Engineer
TL;DR:
- 条件请求将未改变的页面转换为一个
304,其主体为 0 字节 — 完整响应为 50,388 字节,十次条件检查在 4.21 秒内完成,而十次完整获取耗时 5.52 秒。 - 内容哈希检测变化但不节省带宽:第二次获取没有验证器的页面在哈希比较之前仍然传输了 11,064 字节。
- 每条记录的指纹是缩小写入的关键。一个移动的价格使得 20 条记录的页面减少到下游写入的 1 行。
- 关注你关心的字段进行指纹处理,而不是整个记录,否则不相关字段的改变将标记所有内容为脏。
- HTTP 验证器描述服务器发送的文档,因此当记录通过客户端渲染到达时,它们就失去了作用。
304只有在上一个运行存储了验证器的情况下可用,这使得状态表成为管道的一部分,而不是一种优化。- 在渲染的页面上进行增量爬取,使用 Scrapeless 免费计划。
大多数调度的抓取程序会重新下载所有内容,重新解析所有内容,并重写每一行,然后报告成功。这在目录增长时会出现问题,此时日常任务几乎花费所有时间确认昨天的数据仍然是昨天的数据。
增量抓取是相反的安排:询问哪些地方发生了变化,仅在回答“是”的地方进行工作。这个问题可以在三个层次上提出,每个层次节省的东西不同——带宽、解析或写入。下面的每个层次都是针对同一个实时目录页面进行测量的,因此权衡的结果是通过数字而不是形容词来呈现的。
Pipeline at a Glance
| Layer | Question | Mechanism | Measured result |
|---|---|---|---|
| HTTP | 文档是否改变了? | If-None-Match / If-Modified-Since |
304, 0 字节 vs 50,388 |
| Document | 字节是否改变了? | 身体的 SHA-256 | 检测到,但仍传输了 11,064 字节 |
| Record | 哪些行改变了? | 每条记录字段的指纹 | 20 行中有 1 行被写入 |
流程是 检查 → 仅在变化时获取 → 提取 → 比较指纹 → 仅写入差异。各层叠加:HTTP 检查是成本最低、可用性最差的,记录检查始终可用且成本为一次完整获取。
检查频率较低是同一问题的另一半。 机器人排除协议 是一个网站声明其希望被抓取的内容,而增量设计则允许抓取程序以较慢的频率进行抓取而不会落后——每次检查只需一次往返,而不是完整传输。
Layer 1: Let the Server Answer
HTTP 验证器是服务器关于其表示是否发生变化的看法。有两种存在: ETag,一个不透明的版本令牌,和 Last-Modified,一个时间戳。第一个响应携带它们。
python
import requests
URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
first = requests.get(URL, timeout=30)
first.raise_for_status()
etag = first.headers["ETag"]
print(f"first GET {first.status_code} {len(first.content)} bytes ETag={etag}")
second = requests.get(URL, headers={"If-None-Match": etag}, timeout=30)
print(f"second GET {second.status_code} {len(second.content)} bytes")
print(f"bytes avoided: {len(first.content) - len(second.content)}")
text
first GET 200 50388 bytes ETag=W/"63e40de8-c4d4"
second GET 304 0 bytes
bytes avoided: 50388
304 完全不携带主体。 条件请求机制 的定义是客户端保留其已有的副本,这意味着抓取程序必须保留一个。
Last-Modified 通过不同的头以相同方式工作:
python
third = requests.get(URL, headers={"If-Modified-Since": first.headers["Last-Modified"]}, timeout=30)
print(first.headers["Last-Modified"], "->", third.status_code, len(third.content), "bytes")
text
Wed, 08 Feb 2023 21:02:32 GMT -> 304 0 bytes
当两者都存在时,优先选择 ETag。 HTTP 语义规范 使 Last-Modified 可以是一个一秒分辨率的时间戳,因此同一秒内的两个变化是不可区分的,而实体标签可随时在任何编辑中改变。
节省确实存在,但并不是整个运行的全部:
text
10 conditional GETs 4.21s
10 full GETs 5.52s
条件检查运行得快 1.31 倍,并且留下 492 KB 无法传输。请求仍然涉及一次往返——消失的是主体,而不是连接。
Layer 2: Hash the Document When the Server Says Nothing
许多目标既不发送验证器。那么,知道文档是否发生变化的唯一方法就是获取它并查看。加密摘要是比较的标准工具 — SHA-256 标准 提供一个固定宽度的值,其中任何单字节差异都会产生一个不相关的摘要,因此相等是一个可靠的“没有移动”的信号。
python
import hashlib
import requests
q1 = requests.get("https://quotes.toscrape.com/", timeout=30)
q1.raise_for_status()
print("ETag:", q1.headers.get("ETag"), " Last-Modified:", q1.headers.get("Last-Modified"))
q2 = requests.get("https://quotes.toscrape.com/", timeout=30)
h1 = hashlib.sha256(q1.content).hexdigest()
h2 = hashlib.sha256(q2.content).hexdigest()
print(f"sha256 run 1: {h1[:16]}...")
print(f"sha256 run 2: {h2[:16]}...")
print(f"unchanged: {h1 == h2} bytes still transferred: {len(q2.content)}")
text
ETag: None Last-Modified: None
sha256 run 1: efdc2605a2062dce...
sha256 run 2: efdc2605a2062dce...
unchanged: True bytes still transferred: 11064
注意这一层做与不做的事情。这种哈希正确报告没有变化,并且 11,064 字节仍然穿越了网络。文档哈希节省了解析、数据库写入、下游警报和任何重新嵌入——绝不节省带宽。
这里也存在一个误报问题,这里显示的数字并不准确。带有会话令牌、轮换广告或渲染时间戳的页面在每次获取时产生不同的哈希,而数据却是相同的。对提取的记录进行哈希而不是对原始正文进行哈希,这使得信号保持稳定,这是下一层。
第3层:记录指纹
上面的层处理有关文档的问题。管道通常需要知道的是要写入哪些行。
python
import json, hashlib
def fingerprint(record):
payload = json.dumps({k: record[k] for k in ("price", "rating")}, sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()[:16]
选择字段的子集进行指纹识别而不是整个记录,是使其有效的决定。包括一个自动变化的字段——查看次数、"最后查看"时间戳、在排名列表中的位置——每条记录在每次运行时看起来都会很脏。
每条记录存储一个指纹,然后将下一次抓取与之进行比较:
python
known = {title: (fp, price) for title, fp, price in conn.execute("SELECT title, fp, price FROM snap")}
new, changed, same = [], [], 0
for record in second_pass:
previous = known.get(record["title"])
if previous is None:
new.append(record)
elif previous[0] != fingerprint(record):
changed.append((record["title"], previous[1], record["price"]))
else:
same += 1
在实时的20条记录页面上,有一个价格被更改以代表真实的变动:
text
parsed 20 records from the live page
stored baseline: 20 fingerprints
example fingerprint: 'Sharp Objects' -> e974692e9d935048
unchanged 19 | changed 1 | new 0
CHANGED A Murder in Time £16.64 -> £99.99
rows written downstream: 1 of 20
写入一行而不是二十行。在一个几乎没有任何变动的目录中,这个比例就是进行指纹识别的全部论据:写入量跟踪变化率,而不是目录大小。
将前一个值与指纹一起保存,是将检测转变为可用事件的关键。£16.64 -> £99.99 是一个价格变动记录;脏标志仅仅是某些事情发生的提示。
对渲染客户端页面进行增量抓取?Scrapeless 免费计划涵盖了足够的请求,以建立基线和前几个差异。
HTTP层停止适用的地方
验证器描述了服务器发送的文档。当记录通过客户端渲染到达时,该文档就是应用壳,其ETag跟踪的是壳的部署,而不是目录。
其结果是特定的:一个页面可以返回304,而其背后的价格已经全部变动,因为壳确实没有变化。任何内容在浏览器中组装的目标都必须在记录层次上进行检查,利用通用抓取API进行渲染后再进行比较。
这同样适用于页面调用的内部JSON端点。这些通常会发送验证器,当它们发送时,顶层会再次对实际携带记录的负载进行工作。
存储状态
没有地方存储上次运行所学的内容,这一切都无法正常工作。管道所需的状态很小:
| 列 | 目的 |
|---|---|
url |
检查的内容 |
etag / last_modified |
作为条件头重放 |
body_sha256 |
当没有验证器存在时的文档级比较 |
checked_at |
最后一次确认答案的时间 |
每条记录,表中保存关键字、指纹和您希望报告的任何先前值。两者都适合与抓取的数据存储在相同的数据库中,并且ETL管道的形状没有改变——在提取前添加了检查步骤。
一个容易忽视的操作性说明:ETag仅对发出它的URL有效。将存储的标签重新应用于不同的查询字符串或分页变体将产生200和完整正文,这是正确行为,而不是错误。
结论
增量抓取是三个问题,知道您在询问哪个问题决定了您保存什么。HTTP验证器保存正文——50,388字节变为0字节的304。文档哈希保存了解析和写入,但从不节省带宽,因为11,064字节在比较之前到达。记录指纹保存了写入,并将20条记录的页面缩减为一条行。
当服务器提供验证器时,从验证器开始,当提取的记录进行哈希而不是对原始正文进行哈希,然后只对您实际关心的字段进行指纹识别。定价列出了未变动的抓取停止发生后,剩余抓取的费用。
准备停止重新抓取没有移动的页面吗?从Scrapeless免费计划开始并构建您的下一个运行进行比较的基线。
常见问题解答
问:什么是增量网页抓取?
仅抓取自上次运行以来发生变化的内容,而不是重新收集整个目标。它被实现为在获取或写入之前运行的检查:条件HTTP请求、文档哈希比较或逐条记录指纹比较。这里测量的效果是在HTTP层避免了50,388字节,并且在记录层跳过了20条记录中的19条。
问:我该如何在抓取器中使用ETag?
存储响应中的 ETag 头,然后在下一次针对相同URL的请求中将其作为 If-None-Match 发送回去。一个同意没有变化的服务器会回复 304,并且响应体为空,这信号表明可以跳过管道的其余部分。该标签与确切的URL相关联,因此对不同查询字符串重放的存储标签返回正常的 200。
问:如果网站不发送ETag或Last-Modified怎么办?
则改为获取并比较哈希。对提取的记录进行哈希,而不是对原始HTML进行哈希——当会话令牌、广告或呈现的时间戳发生变化时,原始正文哈希会改变,这标记页面为脏数据,而数据本身是相同的。这种方式无论如何都消耗带宽;节省是在解析、写入和任何下游处理上。
问:我应该对整个记录进行哈希还是特定字段?
特定字段。整个记录的哈希包括页面恰好携带的任何内容,因此排名位置或“最近查看”的计数器会使每个记录在每次运行时看起来都发生了变化。仅对那些重要移动字段进行指纹化,这是产生上述稳定19个未变化结果的原因。
问:如果页面返回304,但其数据实际上已更改,这可能吗?
是的,在客户端渲染的页面上。验证器描述了服务器发送的HTML文档,对于单页面应用程序来说,这是外壳而不是记录,因此标签跟踪的是外壳的部署。这些目标在呈现后需要在记录层进行比较,或与携带数据的内部JSON端点进行比较。
问:增量抓取实际上节省了多少?
这取决于哪个层作出响应。在上述测量中,条件请求完全删除了响应体,并且在十次检查中运行速度提高了1.31倍,而记录层在一个有20个项目中有一个移动的页面上将写入减少了95%。无论如何往返的时间仍然存在,因此节省的规模与页面大小和变化率有关,而不是请求数量。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



