周一至周五 09:00 - 18:00(UTC+08:00) 
©2026 NST LABS TECH LTD. 保留所有权利。Marcus ChenProduct & Network Architect
如何使用Python和Nstproxy构建一个可靠的列表爬虫
TL;DR
- 列表抓取提取控制页面序列中的重复记录。 序列可以使用编号页面、下一个链接、游标、加载更多操作或提供的 URL 队列。
- BeautifulSoup 是小型静态列表的最佳选择。 它提供直接控制,但您必须实现请求、分页、重试、去重、检查点和导出。
- Scrapy 是强大的 Python 基础,适合重复的多页面工作。 它的调度器、重试中间件、数据导出、统计数据和可恢复作业消除了基础设施代码,而不影响选择器维护。
- Nstproxy Crawl 在检索是更大问题时适用。 它提供有限的发现、JavaScript 渲染、代理路由和页面输出,而您的管道保持项目身份和验证的责任。
- 一个架构并不能保证数据的正确性。 生产接受还应检查来源、重复率、必填字段完整性、分页终止和意外值分布。
介绍:列表抓取是一个状态管理问题
列表抓取看起来像一个选择器练习,直到第一次重试、重叠页面、静默模板更改或无限游标循环。一个可靠的爬虫必须知道尝试过哪些页面、接受了哪些记录、为何拒绝了一条记录以及重启应从何处恢复。本指南对两种 Python 方法进行了测试,针对公共抓取沙盒进行了测试,然后展示了 Nstproxy Crawl 如何替代访问和渲染层。
示例在两页后故意停止。这个有限的运行足以验证选择器和控制流,而不会让教程变成不必要的全站收集工作。
什么是列表抓取?
列表抓取是从列表页面或预定义的 URL 队列中重复发现和提取相似形状记录的过程。典型目标包括产品网格、公共目录、文章索引、事件日历和授权的职位列表。每条记录通常携带业务字段和来源字段,如 item_id、detail_url、list_url、observed_at 和 crawl_run_id。
列表抓取结合了两个应该保持分开的工作。抓取找到下一个页面或 URL;提取将每个页面转换为记录。进一步的区分可以参考 网页抓取与网页爬取。
分页决定队列形状。编号页面暴露索引,下一个链接分页暴露链,游标 API 暴露不透明状态,而无限滚动通常暴露后台请求或浏览器操作。分页词汇表对此机制进行了更详细的介绍。
为什么列表抓取在成功演示后失败
列表抓取在生产中失败,因为下载的页面与接受的记录并不相同。响应可以返回 HTTP 成功状态,但显示的是同意屏幕、软错误、空的应用外壳或重复页面。解析器在布局更改后也可能返回似是而非但错误的文本。
第一次 Python 测试暴露了这个问题的小规模版本:服务器的主体声明为 UTF-8,但初始的 Requests 解码在货币和标点符号中产生了字符乱码。根据检测到的内容设置响应编码纠正了文本。这是“发现的记录”计数器所遗漏的缺陷。
在抓取之前,请检查网站的条款、发布政策和官方 API 选项。谷歌的 机器人排除协议文档 说明了如何将规则应用于特定主机、协议和端口。机器人规则管理爬虫访问信号;它们并不授予重用个人、受版权保护或受限制数据的权利。
您应该选择哪种列表抓取方法?
根据操作责任选择方法,而不是代码长度。
| 决策领域 | BeautifulSoup + Requests | Scrapy | Nstproxy Crawl |
|---|
| 队列和分页 | 您编写并持久化循环 | 调度器跟踪已发出的请求 | 由服务处理的有限站点发现或提交目标 |
| JavaScript 页面 | 需要单独的浏览器 | 需要渲染集成 | 可用浏览器渲染作为抓取选项 |
| 提取模型 | 您拥有的 CSS/标签解析 | 您拥有的 CSS/XPath 解析和管道 | 页面输出由管理;确切的业务记录仍需要验证 |
| 重启和可观察性 | 构建检查点和指标 | 统计数据加上持久作业支持 | 抓取记录和任务进度由管理;下游记录状态仍然是您的 |
| 维护表面 | HTTP、解析、重试、存储 | 选择器、爬虫规则、管道、部署 | 爬取配置、记录解析器、模式测试和供应商集成 |
| 最佳适应 | 小型静态列表和原型 | 循环的自定义逻辑的Python爬虫 | 动态或访问敏感网站,其爬虫基础设施是瓶颈 |
Beautiful Soup 文档 将该库定义为HTML/XML解析工具,而不是调度程序或下载器。Scrapy覆盖了更广泛的爬取生命周期。Nstproxy Crawl涵盖了检索、渲染、有界发现、路由和页面级输出,但不应被视为域验证的替代品。
详细教程
本教程使用https://books.toscrape.com/catalogue/page-1.html,这是一个用于抓取练习的公共沙箱。两种Python方法在当前包下执行,并从两个列表页面返回40条独特记录。
方法1:使用BeautifulSoup爬取静态列表
当记录和下一个链接存在于初始HTML中,并且运行足够小以适合单个进程时,BeautifulSoup是合适的选择。
第1步:安装并定义记录合同
安装requests和beautifulsoup4。示例保留原始价格文本,而不是强制转换为数字,因为货币解析是一个单独的领域规则。它从规范的详细路径派生出稳定的项目ID,并保留源列表URL以方便追溯。
Requests 文档 推荐为生产请求设置明确的超时,并区分成功的JSON解码与成功的HTTP响应。同样的规则也适用于HTML。
第2步:跟随下一个链接并拒绝重复页面
import hashlib, json
from datetime import datetime, timezone
from urllib.parse import urljoin, urlsplit
import requests
from bs4 import BeautifulSoup
next_url = "https://books.toscrape.com/catalogue/page-1.html"
max_pages = 2
session = requests.Session()
session.headers["User-Agent"] = "Nstproxy-list-crawl-tutorial/1.0"
seen_items, seen_pages = {}, set()
run_time = datetime.now(timezone.utc).isoformat()
pages = 0
while next_url and pages < max_pages:
response = session.get(next_url, timeout=(5, 20))
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select("article.product_pod")
if not cards:
raise RuntimeError(f"未找到产品卡片在 {response.url}")
page_ids = []
for card in cards:
link = card.select_one("h3 a[href]")
detail_url = urljoin(response.url, link["href"])
parts = [p for p in urlsplit(detail_url).path.split("/") if p]
record = {
"item_id": parts[-2],
"title": link["title"].strip(),
"detail_url": detail_url,
"list_url": response.url,
"price_text": card.select_one("p.price_color").get_text(strip=True),
"observed_at": run_time,
}
seen_items[record["item_id"]] = record
page_ids.append(record["item_id"])
fingerprint = hashlib.sha256("\n".join(sorted(page_ids)).encode()).hexdigest()
if fingerprint in seen_pages:
raise RuntimeError(f"检测到重复页面在 {response.url}")
seen_pages.add(fingerprint)
pages += 1
next_link = soup.select_one("li.next a[href]")
next_url = urljoin(response.url, next_link["href"]) if next_link else None
for record in seen_items.values():
print(json.dumps(record, ensure_ascii=False))
print({"pages": pages, "accepted_records": len(seen_items)})
第3步:在扩展规模之前解读结果
经过验证的运行返回 {"pages": 2, "accepted_records": 40}。这证明选择器和下一个链接遍历对采样页面有效。并不能证明每个后续页面都有相同的模板,价格语义上有效,或者未来的运行会是相同的。
在增加max_pages之前,将记录写入具有item_id唯一性约束的表中,在成功提交后持久化最后完成的页面,并在要求字段完整性或每页记录急剧变化时发出警报。
方法2:将队列迁移到Scrapy
当您需要调度程序、重试中间件、导出、爬取统计信息以及持久作业的路径,同时保留Python选择器和管道时,Scrapy是合适的选择。
第1步:将记录和分页表达为爬虫输出
安装scrapy,保存此爬虫,并在验证期间保留两页保护:
from urllib.parse import urlsplit
import scrapy
class BookListSpider(scrapy.Spider):
name = "book_list"
start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]
custom_settings = {
"ROBOTSTXT_OBEY": True,
"DOWNLOAD_DELAY": 0.5,
"CONCURRENT_REQUESTS_PER_DOMAIN": 2,
"RETRY_TIMES": 2,
"LOG_LEVEL": "INFO",
}
def parse(self, response):
cards = response.css("article.product_pod")
if not cards:
self.logger.error("在 %s 上找不到产品卡片", response.url)
return
for card in cards:
detail_url = response.urljoin(card.css("h3 a::attr(href)").get())
parts = [p for p in urlsplit(detail_url).path.split("/") if p]
yield {
"item_id": parts[-2],
"title": card.css("h3 a::attr(title)").get().strip(),
"detail_url": detail_url,
"list_url": response.url,
"price_text": card.css("p.price_color::text").get().strip(),
}
page = int(response.url.rsplit("-", 1)[-1].split(".")[0])
next_href = response.css("li.next a::attr(href)").get()
if next_href and page < 2:
yield response.follow(next_href, callback=self.parse)
步骤 2:运行一个安全覆盖的测试导出
运行 scrapy runspider list_scrapy.py -O books.jl。验证运行使用 Scrapy 2.13.4,接收了两个列表页面,导出了 40 条 JSON Lines 记录,并正常结束。额外请求是网站缺失的 robots.txt,返回了未找到响应;该观察应记录在运行日志中,而不是静默忽略。
步骤 3:添加可恢复性和数据质量门限
对于计划的抓取器,使用 Scrapy 的持久作业目录,而不是将输出文件视为检查点。Scrapy 作业文档 描述了如何使用 JOBDIR 暂停和恢复抓取,并警告一个目录只能属于一个作业。
添加一个项目管道以规范化字段,通过 item_id 进行插入更新,并拒绝缺失的必需值。将蜘蛛统计数据导出到监控中。完成的蜘蛛在接受记录计数和字段分布也通过时,才算操作成功。
快速浏览
在扩展边界之前,先在 Nstproxy Crawl 中测试一个代表性列表路径。比较渲染页面的完整性、发现的 URLs 以及下游接受的记录与 Python 基准。
方法 3:使用 Nstproxy Crawl 进行访问和渲染
Nstproxy Crawl 适用于 JavaScript 渲染、代理路由、有限发现或爬虫操作所需的努力超过记录解析器的情况。当前产品表面支持一个游乐场和 API 工作流、深度和页面限制、包含/排除规则、浏览器渲染、代理选项,以及 Markdown、HTML、JSON、链接或 PDF 页面输出。计费基于成功抓取的 URL 数量;价格数据故意省略,因为会发生变化。它非常适合希望管理页面检索和发现,同时将模式逻辑保留在自己管道中的团队。不能保证每个页面都可访问,或者每个返回的字段都是正确的。
步骤 1:提交一个代表性列表路径
步骤 2:在启用递归之前设置边界
设置一个小的最大页面计数和深度,只包含相关的列表/详情路径,排除购物车、账户、日历、分面重复和文件。仅在所需记录缺失于原始 HTML 时启用 JavaScript。这些选择限制了意外的 URL 扩展,并使测试运行的解释变得可理解。
步骤 3:将页面输出作为输入验证,而不是正义
选择最轻的页面格式,以保留解析器所需的字段。然后应用与 Python 方法相同的 item_id、来源、重复和完整性检查。结构化数据词汇表 解释了为什么类型形状是有用的,但类型有效性仍不能证明一个值属于正确的项目。
该指南验证了实时产品页面,但由于没有账户凭证,因此未执行经过身份验证的爬取。将 Nstproxy 方法视为一个文档化的操作路径,直到您自己的代表性运行通过相同的接受准则。
大多数列表爬取教程遗漏的内容
生产列表爬取需要明确的终端状态。每个页面或 URL 应以接受、永久拒绝或可重试并有限次尝试的状态结束。“没有下一个链接”本身是不够的:还应在重复的游标、重复的页面指纹、页面限制或截止日期上停止。
模式验证是必要的,但并不完整。一个模型可以接受从错误卡片获取的语法上有效的标题、跨每一行复制的默认价格,或页面不支持的 LLM 提取属性。存储证据字段,样本原始输出,并比较运行之间的分布。
- 候选记录与已接受记录;
- 按字段要求字段的完整性;
- 标准化后的重复率;
- 列表页面成功与详细页面成功分别;
- 未添加任何未见项 ID 的页面;
- 每个接受记录的新鲜滞后和爬取使用情况。
还要将列表发现与详细丰富分开。如果某个详细页面失败,保留发现记录及其丰富状态。否则,部分丰富失败可能会抹去该项存在的证据。
结论:选择您可以操作的所有权边界
当计划、重试、导出和可恢复性很重要时,BeautifulSoup 是处理有限静态列表的最清晰学习工具,而 Scrapy 则是更好的 Python 基础。选择 Nstproxy Crawl 当浏览器呈现、代理路由和有限页面发现是操作瓶颈时,然后在数据层中保持稳定的 ID、验证、检查点和接受指标。从两个代表性页面开始,证明记录合约,并在爬虫可以可预测地停止和恢复后再扩展。
体验 Nstproxy — 今天开始您的免费试用
运行一个有限列表爬取示例,与您的 Python 基线进行比较,仅在接受的记录(而不仅仅是页面响应)通过审查后才进行扩展。
常见问题解答
不一样。列表爬虫管理页面或 URL 的发现和状态,而爬虫提取字段;实用的列表爬取管道同时执行这两项工作。
问:我应该使用 BeautifulSoup 还是 Scrapy?
在您想要明确控制的小型静态工作流中使用 BeautifulSoup,在调度、重试、导出、爬取统计和可恢复性工作合理化时使用 Scrapy。
结合信号:没有下一个令牌、没有未见的项 ID、没有重复的指纹,以及强制的页面和时间限制。通常应该调查一个空响应,而不是将其视为完成的证据。
没有。模式可以强制形状和类型,但您仍然需要来源、标准化、去重、语义验证和漂移监控。
仅在初始 HTML 中缺少所需记录或分页控制并在 JavaScript 执行后出现时启用浏览器呈现。
合法性取决于数据、管辖区、访问方式、网站条款和预期用途。合适时优先使用官方 API,尊重发布的爬取控制,尽量减少收集,并对敏感或商业用例进行法律审核。
Lena Zhou
Aug. 11th 2026
立即访问住宅、数据中心、IPv6 与 ISP 高质量代理池。