eBay 是网络上最大的市场之一,每条公开商品列表都携带着驱动价格追踪、竞争对手研究和市场分析所需的结构化数据:标题、价格、商品状况、运费、卖家信息以及商品页面链接。问题在于,eBay 用 JavaScript 渲染价格和大量商品详情,并会对自动化流量发起强力挑战,因此普通的 HTTP 请求会返回近乎空白的壳,而非你想要的数据。
本教程展示如何用 JavaScript 和 Node.js 可靠地抓取 eBay。你将构建一个小型、可运行的爬虫,通过 Crawling API 获取已渲染的 eBay 搜索或商品页面,用 cheerio 解析每条结果,并为每件商品打印一条干净的结构化记录。整个教程仅限于公开商品列表数据,末尾的合法性章节不是套话,请在将本代码用于任何实际规模之前先阅读。
你将构建什么
一个 Node.js 脚本,接受 eBay 公开搜索 URL,通过 Crawling API 获取已渲染的 HTML,并从结果页面上的每条商品列表中提取结构化记录。我们以品类搜索作为运行示例,每件商品提取以下字段:
- 标题商品名称,例如"Apple iPhone 14 Pro Max 128GB Unlocked"。
- 价格显示的挂牌价格,例如"$1,429.49"。
- 状况商品状态,例如"Brand New"或"Pre-Owned"。
- 运费运费金额,或注明时显示"Free shipping"。
- 卖家卡片上显示的卖家名称或店铺(如有)。
- 商品链接指向单个商品页面的链接。
为什么普通请求在 eBay 上失败
如果你用裸 HTTP 客户端请求 eBay 搜索 URL,你会得到状态 200 的响应,但正文中只有部分商品数据。有两个不利因素。第一,eBay 在浏览器中用 JavaScript 渲染价格、运费和其他商品详情,因此初始 HTML 在页面脚本运行之前是不完整的。第二,eBay 能快速识别自动化流量:不像真实浏览器的数据中心 IP 和请求模式,在到达已渲染内容之前就会被挑战、限速或封锁。
因此,一个可用的 eBay 爬虫需要在一次请求中同时具备两样东西:一个能真正渲染页面的浏览器,以及一个被平台识别为真实访客的 IP。你可以自己搭建无头浏览器加轮换住宅代理池,但把它们组合起来并保持健康运行才是大部分工作。Crawling API 将两者折叠进一次调用:你把 URL 连同 JavaScript token 一起发过去,它在可信 IP 后面渲染页面,并返回已完成的 HTML 供你解析。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面。eBay 在客户端加载关键商品字段,因此 JS token 能给你最完整的页面。使用普通 token 可能返回价格或运费缺失的不完整结果,没有可靠内容可供解析。
前提条件
编写代码之前需要准备好几样东西,每样都不需要很长时间。
基础 JavaScript 和 Node.js。你应该能够编写和运行 Node 脚本,并用 npm 安装包。如果你是 Node 新手,官方文档和任何入门课程都能让你达到本教程所需的水平。更完整的教程请参阅我们关于如何用 Node.js 构建网络爬虫的指南。
Node.js 16 或更高版本。用 node --version 确认你的版本。如果没有安装,请从 Node.js 官网或通过 nvm 等版本管理器安装。
Crawlbase 账号和 JS token。注册后打开控制台,从账号文档页面复制你的 JavaScript(JS)token。把 token 当成密码对待:它用于验证你的请求,请不要提交到版本控制系统。
搭建项目
创建一个项目文件夹,初始化它,并安装爬虫所需的两个库。
node --version mkdir ebay-scraper && cd ebay-scraper npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 用类似 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。如果选择器对你来说还比较陌生,XPath 和 CSS 选择器入门是很好的配套阅读材料。
步骤 1:获取已渲染的搜索页面
首先获取完整页面。导入 CrawlingAPI 类,用你的 JS token 初始化,并请求搜索 URL。在解析前检查状态码,可以让失败显而易见,而非悄无声息。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' }); async function crawl(pageUrl) { const options = { ajax_wait: 'true', page_wait: 5000 }; const response = await api.get(pageUrl, options); if (response.statusCode === 200) { return response.body; } console.error(`Request failed: ${response.statusCode}`); return null; } const searchUrl = 'https://www.ebay.com/sch/i.html?_nkw=smartphone'; crawl(searchUrl).then((html) => { console.log(html ? html.slice(0, 500) : 'No HTML returned'); });
两个等待选项对于像这样的客户端渲染目标非常重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 在加载后再等待固定毫秒数,让延迟渲染的元素在页面被捕获前出现。5秒是合理的起点;如果价格或运费返回为空,可以适当增加。用 node scraper.js 运行脚本,你应该看到真实的商品列表标记,而非精简版的空壳。这能在你编写任何选择器之前确认渲染正常工作。
eBay 需要在可信 IP 后面渲染页面,而这只需一次调用。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并将渲染完成的 HTML 返回给你,省去了自己运行无头浏览器集群和代理池的麻烦。先在免费层级向公开搜索页面发起请求。
步骤 2:用 cheerio 解析每条商品列表
拿到已渲染的 HTML,将其加载到 cheerio 并遍历结果卡片。eBay 将每条搜索结果排列在一个重复的块中,因此你选取每张卡片,然后从中读取标题、价格、状况、运费、卖家和商品链接。防御性地读取每个字段,可以避免单个缺失值导致整次运行崩溃。
const cheerio = require('cheerio'); function parseSearch(html) { const $ = cheerio.load(html); const items = []; $('li.s-item').each((_, el) => { const card = $(el); const title = card.find('.s-item__title').text().trim(); if (!title || title === 'Shop on eBay') return; items.push({ title, price: card.find('.s-item__price').text().trim() || null, condition: card.find('.SECONDARY_INFO').text().trim() || null, shipping: card.find('.s-item__shipping').text().trim() || null, seller: card.find('.s-item__seller-info-text').text().trim() || null, itemUrl: card.find('a.s-item__link').attr('href') || null, }); }); return items; }
有几个细节让这段代码具有弹性。eBay 每个结果页面的第一张卡片是标题为"Shop on eBay"的宣传占位符,因此提前 return 会跳过它。当元素缺失时,每个字段回退到 null,这很常见,因为并非每条商品列表都显示卖家名称或单独的运费行。商品链接从锚点的 href 而非文本中读取,因此用 attr 而非 text 处理。
eBay 的类名(s-item、s-item__price、SECONDARY_INFO 等)会在没有通知的情况下变更,而且搜索页面和单个商品页面之间的类名也不同。请将上述选择器视为起始模板,而非固定合约。当某个字段返回 null 时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的,不代表有什么问题。
步骤 3:整合在一起
现在将获取和解析逻辑整合成一个可运行的脚本。获取已渲染的 HTML,传给解析器,并打印结构化记录。
const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' }); async function crawl(pageUrl) { const options = { ajax_wait: 'true', page_wait: 5000 }; const response = await api.get(pageUrl, options); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function parseSearch(html) { const $ = cheerio.load(html); const items = []; $('li.s-item').each((_, el) => { const card = $(el); const title = card.find('.s-item__title').text().trim(); if (!title || title === 'Shop on eBay') return; items.push({ title, price: card.find('.s-item__price').text().trim() || null, condition: card.find('.SECONDARY_INFO').text().trim() || null, shipping: card.find('.s-item__shipping').text().trim() || null, seller: card.find('.s-item__seller-info-text').text().trim() || null, itemUrl: card.find('a.s-item__link').attr('href') || null, }); }); return items; } async function main() { const searchUrl = 'https://www.ebay.com/sch/i.html?_nkw=smartphone'; const html = await crawl(searchUrl); if (!html) return; const items = parseSearch(html); console.log(JSON.stringify(items.slice(0, 3), null, 2)); } main();
输出结果示例
用 node scraper.js 运行完整脚本,你将得到一个干净的记录数组,每条商品列表一条记录,可直接写入 JSON、CSV 或数据库。
[ { "title": "Apple iPhone 14 Pro Max 128GB Unlocked New Sealed", "price": "$1,429.49", "condition": "Brand New", "shipping": "Free shipping", "seller": "beyond_theworld (4,512)", "itemUrl": "https://www.ebay.com/itm/354586733872" }, { "title": "Apple iPhone X 64GB Unlocked Good Refurbished", "price": "$249.99", "condition": "Pre-Owned", "shipping": "+$12.50 shipping", "seller": "thetechout (1,685)", "itemUrl": "https://www.ebay.com/itm/393541114176" } ]
循环遍历结果页面
一页结果只够演示;真正的任务需要遍历分页。eBay 通过 _pgn 查询参数暴露页码,因此你可以在循环中构建每页 URL,通过 Crawling API 获取,用相同的函数解析,并收集各行数据。由于每个结果页面共享相同的卡片结构,你已写好的解析器无需任何修改即可在所有页面上工作。
async function scrapePages(keyword, totalPages) { const all = []; for (let page = 1; page <= totalPages; page++) { const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(keyword)}&_pgn=${page}`; const html = await crawl(url); if (html) all.push(...parseSearch(html)); } return all; } scrapePages('smartphone', 3).then((rows) => { console.log(`Collected ${rows.length} listings`); });
要丰富每行数据以获取完整详情(描述、所有图片、完整运费表、卖家反馈),可以取每张卡片的 itemUrl,通过同一 crawl 函数获取该单个商品页面,然后为商品列表页面布局编写一个小型解析器。模式相同:先渲染,再解析。有关渲染密集型目标的更多内容,请参阅如何爬取 JavaScript 网站。
保持不被封锁
即使渲染问题已解决,eBay 仍会监视爬虫形态的流量。以下几个习惯可以让运行保持健康,适用于任何高难度商业目标。
- 控制请求节奏。在紧循环中猛敲页面是最快被限速的方式。分散请求,在关键词之间轮换,而不是全速爬取同一路径。
- 善用轮换。住宅 IP 池将请求分散到许多真实用户地址上,避免单个地址触发速率限制。Crawling API 会为你处理这一切;如果你自建技术栈,这是最关键的环节。
- 关注状态码。运行中开始返回挑战或错误,说明当前的速率或 IP 层级已经不够用了。把这当作需要退让的信号,而不是可以忽略的噪音。
更完整的方法请参阅如何在不被封锁的情况下抓取网站,以及关于如何在网络爬虫中绕过 CAPTCHA 的深度文章。如果你倾向于通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy 以直连代理端点的形式提供相同的住宅 IP 轮换。eBay 也是更广泛的电商网络爬虫工作的常见目标,相同的获取后解析模式可以跨网站复用。
抓取 eBay 合法吗?
抓取 eBay 是否被允许,取决于 eBay 的服务条款、你所在的司法管辖区以及你对数据的用途。eBay 的条款限制自动化访问,因此无论你的工具多么谨慎,抓取行为都可能违反这些条款。这里的任何代码都不能改变这一事实,它只是让技术部分得以实现。请阅读 eBay 用户协议和其 robots.txt,并将两者视为你采集内容边界的依据。
以下几条值得坚守。只采集公开商品数据:标题、价格、状况、运费、卡片上显示的卖家名称,以及任何人无需账号即可看到的商品链接。遵守 eBay 声明的速率预期,并将请求量控制在不给其服务器造成压力的范围内。避免个人数据,包括任何与结果页面公开列出之外可识别买家或卖家相关的内容。如果你计划商业再利用这些数据,请获得许可或正式协议,而非假设沉默即代表同意。
对于大量或商业用途,eBay 提供官方 API,包括 Browse API 和 Finding API,当你需要大量数据、保证结构或商业权利时,这些才是正确的工具。本教程刻意将范围限定在公开商品和搜索页面,因为这条线让工作有充分的合理性。它不涉及任何登录后才能看到的内容、买家或卖家个人数据、用户之间的私信、需要登录才能访问的账号或订单数据,或任何绕过认证的企图。如果你的项目需要公开商品列表之外的内容,eBay 的官方 API 或数据协议才是正确路径,而非更聪明的爬虫。
核心要点
- eBay 在客户端渲染商品列表。普通请求返回不完整的页面,因此必须在解析前渲染它。
-
渲染和可信 IP 需要同时到位。使用 JS token 的 Crawling API 在一次调用中同时完成两者;
ajax_wait和page_wait控制等待内容的时间。 -
cheerio 负责提取。选取每张
s-item卡片,然后将标题、价格、状况、运费、卖家和商品链接映射到当前选择器,并预期这些选择器会漂移。 -
通过循环遍历页面进行扩展。
_pgn参数遍历结果页面,相同的解析器适用于每一页,配合合理的请求节奏。 - 坚守公开数据。遵守 eBay 的服务条款和 robots.txt,大量或商业用途时优先使用官方 Browse 或 Finding API,绝不触碰登录、个人数据或私信。
常见问题
为什么普通请求从 eBay 返回不完整的数据?
因为 eBay 在客户端用 JavaScript 渲染价格、运费和大量商品详情。初始 HTML 在页面脚本在浏览器中运行之前是不完整的,因此原始 HTTP 请求返回状态 200,但关键字段缺失或为空。要获取完整页面,必须先渲染它,这正是 Crawling API 的 JS token 为你处理的事情。
抓取 eBay 需要普通 token 还是 JS token?
使用 JS token。普通 token 获取静态 HTML,在 eBay 上可能返回价格或运费缺失的结果。JS token 在返回 HTML 之前先在真实浏览器中渲染页面,因此当 cheerio 解析时,商品字段都已存在。
我的选择器返回 null。发生了什么变化?
几乎可以确定是 eBay 的标记发生了变化。其 s-item 卡片类、SECONDARY_INFO 标记和节点包装器会在没有通知的情况下变更,而且搜索页面和单个商品页面之间的类名也不同,因此上个月有效的选择器可能已经失效。在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的。
我应该使用 eBay 官方 API 还是抓取网站?
如果你需要大量数据、保证结构或商业再利用权利,请使用 eBay 官方 Browse 或 Finding API。这些 API 就是为此目的而构建的,并让你保持在 eBay 条款的框架内。只要遵守服务条款、robots.txt 和速率限制,本教程中的方法适合没有 API 访问权限的小型公开数据研究。
我可以从 eBay 抓取买家或卖家个人数据吗?
不可以,本教程也不涉及这方面内容。买家详情、私信和账号数据位于登录后,不属于公开数据。抓取登录后的内容、个人数据,或绕过认证来访问这些内容,超出了本教程的范围,且违反 eBay 的条款。获得授权访问的正确路径是 eBay 官方 API 或许可协议。
抓取 eBay 时如何避免被封锁?
保持每个 IP 的请求速率较低,在关键词之间轮换而不是循环同一路径,并通过轮换住宅 IP 路由,避免单个地址触发速率限制。Crawling API 为你管理轮换和可信 IP 池;如果你自建技术栈,这正是需要重点投入的地方。关注状态码,当挑战出现时退让。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
