OpenSea 是最大的 NFT 交易市场之一,每个藏品和商品列表页面都包含 NFT 价格追踪器、市场研究笔记或稀有度仪表板所需的结构化数据:作品名称、所属藏品、当前 ETH 挂牌价、最近成交价、代币 ID 和图片。问题在于 OpenSea 是一个 JavaScript 密集型的 React 应用,普通 HTTP 请求返回的几乎是一个空壳,而不是你所需要的条目。
本指南介绍如何使用 Python 以可靠的方式抓取 OpenSea 数据。你将构建一个小型可运行的爬虫,使用 JavaScript 令牌通过 Crawling API 获取渲染后的藏品页面,用 BeautifulSoup 解析每件作品,并打印整洁的结构化记录。整个教程仅限于任何人都能在藏品页面看到的公开 NFT 数据,靠近末尾的合法性说明不是套话,请在正式抓取大量数据之前认真阅读。
你将构建什么
一个 Python 脚本:接收 OpenSea 公开藏品 URL,通过 Crawling API 获取渲染后的 HTML,并为页面上的每件 NFT 提取结构化记录。我们以一个公开藏品为贯穿全文的示例,从每张作品卡片中提取以下字段:
- 作品名称 单件 NFT 的唯一名称,例如 "Courtyard #1024"。
- 藏品 该作品所属的藏品。
- 价格(ETH) 卡片上显示的当前挂牌价。
- 最近成交价 该作品上次售出的价格(如卡片显示)。
- 代币 ID 唯一的链上标识符,用于跨平台追踪代币。
- 图片 URL 该作品缩略图的来源地址。
- 作品 URL 指向该 NFT 详情页的链接。
为什么普通请求在 OpenSea 上会失败
如果你用普通 HTTP 客户端请求 OpenSea 藏品 URL,会得到状态 200 的响应,但 body 中几乎没有 NFT 数据。有两个因素对你不利。第一,OpenSea 是一个在浏览器中构建其作品网格的客户端 React 应用,因此初始 HTML 只是一个框架,直到页面脚本运行并通过网络加载市场数据后才填充内容。第二,OpenSea 会迅速标记自动化流量:来自数据中心的 IP 和不像真实浏览器的请求模式,在到达渲染完成的网格之前就会被挑战或封锁。
因此,一个可用的 OpenSea 爬虫需要在单次请求中同时具备两点:一个能真正渲染页面的浏览器,以及一个被平台识别为真实访客的 IP。你可以自己搭建无头浏览器加轮换住宅代理池,但维护这套组合才是大部分工作量所在。Crawling API 将两者合并为一次调用:你使用 JavaScript 令牌发送 URL,它在可信 IP 后渲染页面,并将完整 HTML 返回给你解析。如果客户端渲染对你来说是新概念,我们关于抓取 JavaScript 网站的指南解释了为什么渲染至关重要。
Crawlbase 提供两种令牌类型。普通令牌获取静态 HTML;JavaScript(JS)令牌先在真实浏览器中渲染页面。OpenSea 是客户端渲染的,因此在这里需要 JS 令牌。使用普通令牌会返回与普通获取相同的空框架,其中没有任何可解析的内容。你可以从 1,000 次免费请求开始,无需信用卡。
前提条件
在编写代码之前,你需要准备好以下几样东西。每一项都不费时。
基础 Python 知识。你应该能够编写和运行 Python 脚本,并使用 pip 安装包。如果 BeautifulSoup 对你来说是新工具,我们的 Python 中使用 BeautifulSoup 指南涵盖了本教程所假设的解析基础知识。
Python 3.8 或更高版本。使用 python --version 确认你的版本。如果尚未安装,可从 python.org 下载,或通过 Anaconda 等发行版安装。
Crawlbase 账号和 JS 令牌。注册账号,打开控制台,从账号文档页面复制你的 JavaScript(JS)令牌。请像保管密码一样保管该令牌:它用于验证你的请求身份,不要将其提交到版本控制系统。
搭建项目
创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。
python --version python -m venv opensea_env source opensea_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,将激活命令中的 source 行替换为 opensea_env\Scripts\activate。两个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。
第一步:获取渲染后的藏品页面
首先获取完整的页面。导入 CrawlingAPI 类,用你的 JS 令牌初始化,然后请求藏品 URL。在解析之前检查状态,可以让失败尽早暴露而非静默发生。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": page_url = "https://opensea.io/collection/courtyard-nft" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
两个等待选项对这类客户端渲染目标至关重要。ajax_wait 告诉 API 等待异步内容加载完成,OpenSea 正是用这种方式填充其作品网格;page_wait 在加载后固定等待指定毫秒数,使迟渲染的卡片在页面被捕获之前出现。五秒是合理的起始值;如果卡片返回为空,可以增加。使用 python scraper.py 运行脚本,你应该能看到真实的作品标记,而不是普通获取返回的空框架。这证明在你编写任何选择器之前,渲染已正常工作。
OpenSea 需要在单次调用中完成渲染 React 页面和可信 IP 访问。Crawling API 接受 JS 令牌,在真实浏览器中运行页面,等待加载作品网格的 AJAX 请求,在服务端轮换住宅 IP,并将完整 HTML 交给你,让你跳过自行运行无头浏览器集群和代理池的麻烦。先在免费套餐中指向一个公开藏品进行测试。
第二步:用 BeautifulSoup 解析作品卡片
拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,通过选择器提取每件 NFT。OpenSea 将藏品以重复作品卡片的网格形式展示,因此你只需选择所有卡片一次,然后从每张卡片中读取相同的字段。在浏览器开发者工具中检查线上页面以确认当前属性;以下选择器与撰写本文时的布局匹配。
from bs4 import BeautifulSoup BASE = "https://opensea.io" def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def token_id_from(href): # OpenSea item URLs end with the token id, e.g. /assets/.../1024 return href.rstrip("/").split("/")[-1] if href else None def parse_collection(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('article.AssetSearchList--asset') items = [] for card in cards: link = card.select_one("a.Asset--anchor") href = link["href"] if link else None img = card.select_one("img") items.append({ "name": text_of(card, 'span[data-testid="ItemCardFooter-name"]'), "price_eth": text_of(card, 'div[data-testid="ItemCardPrice"] span[data-id="TextBody"]'), "last_sale": text_of(card, 'div[data-testid="ItemCardPrice-secondary"]'), "token_id": token_id_from(href), "image_url": img["src"] if img else None, "item_url": BASE + href if href else None, }) return items
text_of 辅助函数同时做到两件事:在卡片内查询单个元素,当该元素缺失时返回 None,而不是对空元素调用 .get_text() 引发异常。这使提取在某个字段缺席时依然具有容错性,而这很常见,因为并非每件 NFT 都有最近成交价或当前挂牌价。作品名称来自 data-testid="ItemCardFooter-name" span,挂牌价来自 ItemCardPrice 内嵌套的 data-id="TextBody" span,链接来自 Asset--anchor 锚点。代币 ID 是该链接的最后路径段,完整作品 URL 是基础主机拼接相对 href。
OpenSea 的 class 名称和 data-testid 属性会无通知变更。将上述选择器视为起始模板,而非固定合同。当某个字段对每张卡片都返回 None 时,在浏览器开发者工具中重新检查线上作品并更新选择器。定期维护选择器是任何生产级爬虫的正常工作,不代表出了什么问题。
第三步:处理基于滚动的分页
藏品页面不会一次性加载所有作品。OpenSea 使用无限滚动,只有当你向下移动网格时才会出现更多 NFT。与其逆向工程分页调用,不如让 Crawling API 通过 scroll 和 scroll_interval 选项为你滚动页面。API 会滚动你指定的秒数,将更多卡片加载到你随后解析的同一 HTML 中。
def crawl_with_scroll(page_url): options = { "ajax_wait": "true", "scroll": "true", "scroll_interval": "20", # scroll for 20 seconds, max 60 } response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None
将 scroll 设为 true 告诉 API 滚动渲染后的页面,scroll_interval 控制滚动时长,最长 60 秒。更长的间隔加载更多作品,但每次请求耗时更长,因此根据你需要深入藏品的程度选择合适的值。注意,使用滚动时省略 page_wait,因为滚动本身已足够让页面保持打开,使新卡片有时间渲染。
第四步:整合
现在将启用滚动的获取和解析器串联成一个可运行的脚本。获取渲染后的 HTML,传给解析器,并将结果写入 JSON,之后可以在任何地方加载。
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) BASE = "https://opensea.io" def crawl(page_url): options = {"ajax_wait": "true", "scroll": "true", "scroll_interval": "20"} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def token_id_from(href): return href.rstrip("/").split("/")[-1] if href else None def parse_collection(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('article.AssetSearchList--asset') items = [] for card in cards: link = card.select_one("a.Asset--anchor") href = link["href"] if link else None img = card.select_one("img") items.append({ "name": text_of(card, 'span[data-testid="ItemCardFooter-name"]'), "price_eth": text_of(card, 'div[data-testid="ItemCardPrice"] span[data-id="TextBody"]'), "last_sale": text_of(card, 'div[data-testid="ItemCardPrice-secondary"]'), "token_id": token_id_from(href), "image_url": img["src"] if img else None, "item_url": BASE + href if href else None, }) return items def main(): page_url = "https://opensea.io/collection/courtyard-nft" html = crawl(page_url) if not html: return items = parse_collection(html) with open("opensea_data.json", "w") as f: json.dump(items, f, indent=2) print(f"Saved {len(items)} items") if __name__ == "__main__": main()
输出示例
使用 python scraper.py 运行完整脚本,你将为每件 NFT 获得一条整洁的结构化记录,可写入 JSON、CSV 或数据库。价格和最近成交价字段以 OpenSea 显示的字符串形式返回,包含 ETH 符号,如果你需要数值,请在下游进行规范化处理。
[ { "name": "Courtyard #1024", "price_eth": "0.018 ETH", "last_sale": "Last sale: 0.015 ETH", "token_id": "1024", "image_url": "https://i.seadn.io/s/raw/files/abc123.png", "item_url": "https://opensea.io/assets/matic/0x251be3.../1024" }, { "name": "Courtyard #2087", "price_eth": "0.021 ETH", "last_sale": null, "token_id": "2087", "image_url": "https://i.seadn.io/s/raw/files/def456.png", "item_url": "https://opensea.io/assets/matic/0x251be3.../2087" } ]
第二条记录的最近成交价为 null,这正是辅助函数的作用:并非每件作品都有成交记录,因此该字段简单地缺席,而不是导致程序崩溃。
抓取 NFT 详情页
藏品页面提供了卡片级别的摘要,但每件 NFT 也有自己的详情页,包含更丰富的字段,例如更长的描述、完整的价格历史,以及藏品公开的稀有度排名(如有)。方法与藏品页面相同:使用 JS 令牌渲染 URL,然后通过选择器读取字段。由于详情页面布局不同,选择器也会有所差异,因此在依赖它们之前先检查线上的详情页。
def parse_nft_detail(html, url): soup = BeautifulSoup(html, "html.parser") rank = soup.select_one('[data-testid="rarity-rank"]') return { "name": text_of(soup, "h1.item--title"), "collection": text_of(soup, "a.item--collection-detail"), "price_eth": text_of(soup, "div.Price--amount"), "rarity_rank": rank.get_text(strip=True) if rank else None, "token_id": token_id_from(url), "item_url": url, }
这复用了藏品爬虫中的 text_of 和 token_id_from 辅助函数,所以详情运行只是针对相同的获取-解析循环使用不同的选择器集。作品名称来自 item--title 标题,挂牌价来自 Price--amount,稀有度排名来自 rarity-rank 测试 ID(如果藏品暴露了该字段)。如果藏品没有稀有度排名,该字段保持 None,这是正确的行为而非缺陷。
扩展与保持不被封锁
单个藏品只是演示;真实任务是跨多个藏品运行。形式保持不变:维护藏品 URL 列表,通过启用滚动的 Crawling API 逐一获取,用同一函数解析,并收集结果。由于每个藏品页面共享相同的卡片结构,你已编写的解析器无需修改即可适用于所有藏品。即便渲染问题已解决,OpenSea 仍会监控形似爬虫的流量,因此以下几个习惯有助于保持任务顺畅运行,适用于任何难度较高的目标。
- 控制请求频率。在紧密循环中频繁请求藏品页面是被限速的最快途径。分散请求,对目标进行变化,而不是以全速抓取单个藏品。
- 善用轮换。住宅 IP 池将请求分散到众多真实用户地址,使任何单一地址都不会触发速率限制。Crawling API 为你处理这一切;如果你自行搭建,这一环节最为关键。
- 关注状态码。当运行过程中开始返回挑战或错误时,说明当前速率或 IP 层级已不够用。将其视为需要退让的信号,而非可以忽略的噪音。
关于更完整的应对策略,请参阅如何在不被封锁的情况下抓取网站。如果你更倾向于通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy(也称 AI Proxy)以直接代理接口的形式提供相同的住宅 IP 轮换。
抓取 OpenSea 是否合法?
抓取 OpenSea 是否被允许,取决于 OpenSea 的服务条款、你所在的司法管辖区以及你对数据的用途。OpenSea 的条款限制自动化访问,因此无论你的技术手段多么谨慎,抓取行为都可能与这些条款相抵触。本文中的代码无法改变这一点,它只是让技术层面的工作得以实现。请阅读 OpenSea 的服务条款和 robots.txt,并将两者视为你能收集内容的边界。
以下几条底线值得坚守。仅收集公开 NFT 数据:任何人无需账号即可在藏品页面看到的作品名称、藏品、挂牌价、最近成交价、代币 ID、图片和链接。遵守 OpenSea 的速率预期,将请求量控制在不给其服务器造成压力的水平。代币元数据本身存在于链上,因此对于许多用例,直接读取区块链既更简洁,也没有歧义。不要将 NFT 相关的艺术品或媒体文件当作你自己的内容进行转载;引用图片 URL 是可以的,但底层媒体属于创作者。
对于生产或商业用途,OpenSea 发布了官方 API,这是它期望开发者获取市场数据的方式。它在明确条款下提供结构化的商品列表、事件和藏品统计数据,无需渲染页面或维护选择器。抓取适用于对公开数据进行探索性研究和一次性分析;如果你的项目需要持续、大批量或商业访问,官方 API 或直接的数据协议才是正确路径,而非更聪明的爬虫。
核心要点
- OpenSea 是客户端 React 应用。普通获取返回空框架,因此在解析之前必须先渲染页面。
-
使用 JS 令牌。带 JavaScript 令牌的 Crawling API 在单次调用中在可信 IP 后渲染页面;
ajax_wait和page_wait控制等待内容出现的时长。 -
滚动处理分页。OpenSea 通过无限滚动加载作品,因此传入
scroll和scroll_interval而不是逆向工程其分页机制。 - BeautifulSoup 负责提取。选择所有作品卡片,然后从每张卡片读取名称、ETH 价格、最近成交价、代币 ID、图片和链接,并预期选择器会漂移。
- 坚守公开数据,生产用途优先使用官方 API。遵守 OpenSea 的服务条款和 robots.txt,将范围限定为公开 NFT 数据,商业或大批量工作使用官方 API。
常见问题
为什么普通获取从 OpenSea 返回不到 NFT?
因为 OpenSea 是一个在浏览器中构建其作品网格的客户端 React 应用。初始 HTML 只是一个框架,直到页面脚本运行并通过网络加载市场数据后才填充内容,所以原始 HTTP 请求返回状态 200 但网格为空。要获取真实数据,必须先渲染页面,这正是 Crawling API 的 JS 令牌为你处理的事情。
抓取 OpenSea 需要普通令牌还是 JS 令牌?
需要 JS 令牌。普通令牌获取静态 HTML,在 OpenSea 上与普通获取返回的空框架相同。JS 令牌在返回 HTML 之前在真实浏览器中渲染页面,使作品卡片在 BeautifulSoup 解析时已存在。
如何加载超出第一屏的作品?
OpenSea 通过无限滚动加载作品,只有当你向下移动网格时才会出现更多内容。将 scroll 设为 true,并向 Crawling API 传入以秒为单位的 scroll_interval,它会在捕获 HTML 之前为你滚动渲染后的页面,使更多卡片出现在解析时。更长的间隔以较慢的请求为代价加载更多作品。
我的选择器对每张卡片都返回 None,发生了什么变化?
几乎可以肯定是 OpenSea 的标记发生了变化。其 class 名称和 data-testid 属性会无通知变更,因此上个月有效的选择器可能已失效。在浏览器开发者工具中重新检查线上作品并更新选择器。定期维护是任何生产级爬虫的正常工作。
我应该抓取 OpenSea 还是使用其官方 API?
对于公开数据的探索性研究和一次性分析,抓取藏品页面是可以的,本指南涵盖的正是这种用途。对于生产、商业或大批量用途,优先使用 OpenSea 的官方 API:它在明确条款下返回结构化的商品列表、事件和藏品统计数据,无需渲染页面或追赶选择器变更。
可以从 OpenSea 抓取账号或个人数据吗?
不可以,本指南也不涉及这一内容。钱包账号详情、登录后的内容,以及你会转载的艺术品或媒体文件,都超出了公开 NFT 数据的范围,因此不在本文讨论范围内,且与 OpenSea 的条款相抵触。请坚守任何人都能看到的公开作品、藏品和商品列表数据,当你需要权威的代币元数据时,直接读取区块链。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
