Farfetch是全球最大的奢侈时尚市场之一,将购物者与精品店和高端品牌相连,覆盖数千件服装、鞋履和配饰的商品。它的公开产品页面承载着时尚分析师、价格追踪者和电商团队所关心的那类数据:哪些品牌有上架、商品是什么、定价多少,以及每件商品链接到哪里。长期观察这份目录,能告诉你奢侈品在不同品牌和品类间是如何定价的,以及哪些品牌正在走红。
本指南向你展示如何使用JavaScript和Node.js配合cheerio来抓取Farfetch零售数据。你将构建一个小巧、可运行的抓取器,通过Crawling API获取Farfetch的列表页,解析每件商品的品牌、产品名称、价格和链接,处理分页,并将结果导出为JSON和CSV。整个演练始终限定在公开的产品列表数据范围内,而靠近文末的合法性部分并非套话,所以在你将其指向任何真实规模之前请先读一读。
你将构建什么
一个Node.js脚本,它接收一个公开的Farfetch品类URL,通过Crawling API取回渲染后的HTML,并为列表上的每张产品卡片提取一条结构化记录。我们以男士鞋履品类作为贯穿全文的示例,逐件抓取以下字段:
- 品牌卡片上显示的奢侈品牌名称,例如"Gucci"。
- 描述用于标识商品的简短产品文案,比如"Screener皮革运动鞋"。
- 价格卡片上显示的价格,包含货币符号。
- 折扣商品促销时的折扣标签,否则为"N/A"。
- 链接指向farfetch.com上该商品独立页面的绝对URL。
为什么普通请求在Farfetch上会失败
如果你用一个裸HTTP客户端请求Farfetch的品类URL,你很少能拿回产品网格。Farfetch用JavaScript在浏览器中渲染它的列表卡片,所以在页面脚本运行之前,初始HTML只是一个近乎空白的外壳。除此之外,Farfetch还会监控自动化流量:数据中心IP以及看起来不像真实浏览器的请求模式,会在到达渲染后的产品数据之前就被质询、限速或封锁。
因此一个能用的Farfetch抓取器需要在一次请求中具备两样东西:一个真正渲染页面的浏览器,以及一个被平台读作真实访客的IP。你可以自己用无头浏览器加上一组轮换的住宅代理来拼凑这套方案,但把它们缝合起来并保持健康才是大部分工作。Crawling API把两者折叠进一次调用:你把URL发给它,它在受信任的IP背后渲染页面,并返回处理好的HTML供你用cheerio解析。
由于Farfetch在客户端加载它的网格,你要告诉Crawling API在捕获HTML之前先等待JavaScript稳定下来。设置ajax_wait: 'true'和几千毫秒的page_wait,这样产品卡片就会出现在你拿回的标记中。渲染使用JavaScript请求类型,这也是为什么这些列表需要JS令牌而不是普通请求。
前提条件
在写任何代码之前,你需要准备好几样东西。它们都不会花太长时间。
基础的JavaScript和Node.js。你应该能自如地编写并运行Node脚本,并用npm安装包。如果你刚接触Node,官方文档和任何入门课程都能让你达到本教程所假设的水平。如果你想要一个,关于用Node.js构建网络爬虫的指南是个不错的热身。
Node.js 16或更高版本。用node --version确认你的版本。如果你还没有,请从Node.js网站或通过像nvm这样的版本管理器安装它。
一个Crawlbase账户和令牌。注册、打开你的仪表盘,并复制你的令牌。免费层最多为你提供20,000次请求且无需绑卡。Farfetch需要渲染,所以这些请求请使用你的JavaScript令牌。把令牌当作密码对待:它用于验证你的请求身份,所以请不要把它放进版本控制。
搭建项目
创建一个项目文件夹,初始化它,并安装抓取器所需的两个库。
node --version mkdir farfetch-scraper && cd farfetch-scraper npm init -y npm install crawlbase cheerio
两个依赖完成主要工作:crawlbase是Crawling API的官方Node客户端,cheerio用类似jQuery的API解析返回的HTML,让你能按CSS选择器逐个提取字段。在这个文件夹里创建一个名为farfetch-scraper.js的文件,并加入下面各步骤的代码。
第1步:获取渲染后的列表页
先从拿到处理好的页面开始。导入CrawlingAPI类,用你的令牌初始化它,并带上等待选项请求品类URL,这样卡片会在HTML被捕获之前渲染完成。在解析之前检查状态码,能让失败显式暴露而非悄无声息。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const options = { ajax_wait: 'true', // wait for the JavaScript grid to load page_wait: '5000' // give the page 5 seconds to settle }; const listingURL = 'https://www.farfetch.com/shopping/men/shoes-2/items.aspx'; api .get(listingURL, options) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
用node farfetch-scraper.js运行脚本,你应该会在响应体顶部附近看到真实的Farfetch产品标记,而不是一个被剥离的外壳。这在你写下任何一个选择器之前就确认了渲染是有效的。ajax_wait和page_wait选项正是给客户端网格留出填充时间的东西;你可以在Crawling API文档中读到更多关于可用请求参数的内容。
那第一次请求刚刚返回了一个完全渲染好的Farfetch列表,而你这边不需要无头浏览器或代理。Crawling API在真实浏览器中运行页面,用ajax_wait和page_wait等待JavaScript网格加载,在服务端轮换住宅IP,并应对Farfetch抛给抓取器的各种质询,所以你能从一次调用中得到处理好的HTML。先在免费层把它指向一个男士鞋履品类试试。
第2步:用cheerio解析每张产品卡片
手里有了渲染后的HTML,把它加载进cheerio并遍历产品卡片。Farfetch把每件商品布局为目录网格内的一个列表元素,所以你先选中每张卡片,再从其内部读取品牌、描述、价格、折扣和链接。防御性地读取每个字段,能让单个缺失值不至于让整次运行崩溃。
const cheerio = require('cheerio'); function parseListings(html) { const $ = cheerio.load(html); const products = []; const cards = $( 'ul#catalog-grid > li[data-testid="productCard"]' ); cards.each((index, element) => { const card = $(element); const brand = card .find('p[data-component="ProductCardBrandName"]') .text() .trim() || 'N/A'; const description = card .find('p[data-component="ProductCardDescription"]') .text() .trim() || 'N/A'; const price = card .find('p[data-component="Price"], p[data-component="PriceFinal"]') .first() .text() .trim() || 'N/A'; const discount = card .find('p[data-component="PriceDiscount"]') .text() .trim() || 'N/A'; const href = card.find('a').first().attr('href'); const link = href ? new URL(href, 'https://www.farfetch.com').href : 'N/A'; products.push({ brand, description, price, discount, link }); }); return products; }
这些选择器直接来自页面。每张卡片是ul#catalog-grid内的一个li[data-testid="productCard"]。品牌位于一个标注了data-component="ProductCardBrandName"的p里,描述在ProductCardDescription中,价格在Price或PriceFinal中,所以选择器两者皆可匹配并取第一个。当商品促销时,折扣标签存放在PriceDiscount中。产品链接是卡片锚点的href,Farfetch将其作为相对路径提供,所以它会相对于https://www.farfetch.com解析,得到一个在页面之外也能用的绝对URL。
Farfetch的data-component和data-testid属性是稳定的句柄,但标记会随时间变化。把上面的选择器当作起始模板,而非契约。当某个字段全线返回"N/A"时,在浏览器开发者工具中重新检查实时页面并更新选择器。对任何生产级抓取器来说,定期维护选择器都属正常,并不意味着出了什么问题。
第3步:处理分页
Farfetch把它的列表分散在许多页上。要采集一个完整品类,就通过向URL追加一个page参数来遍历各页,并在每一页上运行相同的获取与解析,然后把所有内容汇集到一个列表中。
async function crawl(pageUrl) { const response = await api.get(pageUrl, options); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } async function scrapeMultiplePages(baseUrl, totalPages) { const allProducts = []; for (let page = 1; page <= totalPages; page++) { const paginatedUrl = `${baseUrl}?page=${page}`; console.log(`Scraping page: ${page}`); const html = await crawl(paginatedUrl); if (!html) continue; allProducts.push(...parseListings(html)); } return allProducts; }
这会从第1页循环到totalPages,把每个页面URL构建为?page=N,带着同样的等待选项通过Crawling API获取它,解析卡片,并把结果展开进一个合并的数组中。因为每个品类页共享相同的卡片结构,你在第2步写的parseListings函数无需改动就能在它们全部之上工作。
第4步:组装带JSON和CSV导出的完整脚本
现在把获取、解析和分页接进一个可运行的脚本,然后把记录以JSON和CSV两种形式写入磁盘。
const fs = require('fs'); const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const options = { ajax_wait: 'true', page_wait: '5000' }; async function crawl(pageUrl) { const response = await api.get(pageUrl, options); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function parseListings(html) { const $ = cheerio.load(html); const products = []; $('ul#catalog-grid > li[data-testid="productCard"]').each((i, el) => { const card = $(el); const brand = card.find('p[data-component="ProductCardBrandName"]').text().trim() || 'N/A'; const description = card.find('p[data-component="ProductCardDescription"]').text().trim() || 'N/A'; const price = card.find('p[data-component="Price"], p[data-component="PriceFinal"]').first().text().trim() || 'N/A'; const discount = card.find('p[data-component="PriceDiscount"]').text().trim() || 'N/A'; const href = card.find('a').first().attr('href'); const link = href ? new URL(href, 'https://www.farfetch.com').href : 'N/A'; products.push({ brand, description, price, discount, link }); }); return products; } async function scrapeMultiplePages(baseUrl, totalPages) { const allProducts = []; for (let page = 1; page <= totalPages; page++) { console.log(`Scraping page: ${page}`); const html = await crawl(`${baseUrl}?page=${page}`); if (html) allProducts.push(...parseListings(html)); } return allProducts; } function toCsv(rows) { const headers = ['brand', 'description', 'price', 'discount', 'link']; const escape = (value) => `"${String(value).replace(/"/g, '""')}"`; const lines = [headers.join(',')]; for (const row of rows) { lines.push(headers.map((h) => escape(row[h])).join(',')); } return lines.join('\n'); } async function main() { const baseUrl = 'https://www.farfetch.com/shopping/men/shoes-2/items.aspx'; const products = await scrapeMultiplePages(baseUrl, 5); if (!products.length) return; fs.writeFileSync('farfetch_listings.json', JSON.stringify(products, null, 2)); fs.writeFileSync('farfetch_listings.csv', toCsv(products)); console.log(`Saved ${products.length} products to JSON and CSV`); } main();
用node farfetch-scraper.js运行它,你会得到两个文件:包含完整结构化记录的farfetch_listings.json,以及可直接在电子表格中打开的farfetch_listings.csv。toCsv辅助函数会给每个字段加引号并把任何内嵌的引号成对转义,这在这里很重要,因为品牌名称和产品描述里经常含有逗号。
输出长什么样
JSON文件为每张产品卡片保存一个对象,每个对象都带有品牌、描述、价格、折扣和链接。
[ { "brand": "Gucci", "description": "Screener leather sneakers", "price": "$890", "discount": "N/A", "link": "https://www.farfetch.com/shopping/men/gucci-screener-sneakers-item-27582236.aspx" }, { "brand": "Common Projects", "description": "Original Achilles low-top sneakers", "price": "$425", "discount": "30% off", "link": "https://www.farfetch.com/shopping/men/common-projects-original-achilles-item-12345678.aspx" } ]
CSV以一行表头镜像同样的行,所以它能直接落入Excel、Google Sheets,或任何读取分隔文件的数据管道。
brand,description,price,discount,link "Gucci","Screener leather sneakers","$890","N/A","https://www.farfetch.com/shopping/men/gucci-screener-sneakers-item-27582236.aspx" "Common Projects","Original Achilles low-top sneakers","$425","30% off","https://www.farfetch.com/shopping/men/common-projects-original-achilles-item-12345678.aspx"
横跨多个品类扩展
一个品类只是演示;真实的任务会遍历多个Farfetch部门。每个品类都存在于它自己的列表路径下,例如男士鞋履、女士包袋或童装,所以你可以构建一个品类URL列表,对每一个运行scrapeMultiplePages,并在导出前为每一行打上它所属品类的标签。因为每个列表页都共享相同的卡片结构,你已经写好的解析器无需改动就能在它们全部之上工作。
这套模式可直接延续到定价和产品研究中。要了解从在线商店中提取结构化数据的更宏观图景,参见电商网络爬虫指南;要把这些价格转化为决策,参见如何将网络爬虫用于价格情报。如果你想把同样的方法应用到另一家时尚零售商,关于如何创建Zalando抓取器的演练讲解了一个类似的JavaScript密集型目录。
保持不被封锁
即便渲染问题已经解决,Farfetch仍会监控具有抓取器特征的流量。一些习惯能让一次运行保持健康,它们适用于任何难啃的商业目标。
- 给请求放慢节奏。在各页获取之间引入延迟,而不是在一个紧凑的循环里猛锤目录。把请求摊开,是保持在站点速率限制之下的单一最大因素。
- 依靠轮换。一组住宅IP把请求分散到许多真实用户地址上,这样就没有任何单一地址会触发限制或质询。Crawling API替你处理这件事;如果你自己搭建技术栈,这就是需要做对的部分。
- 读懂状态码。一次开始返回质询或非200响应的运行,是在告诉你当前的速率或IP层级已经不够用了。把那当作收手的信号,而不是可以忽略的噪声。
关于这方面更宏观的攻略,参见如何抓取网站而不被封锁;关于这些目录一开始为何就需要一个真实浏览器的机理,关于如何爬取JavaScript网站的指南讲得更深。
抓取Farfetch合法吗
抓取Farfetch是否被允许,取决于Farfetch的服务条款、你所在的司法辖区,以及你用这些数据做什么。Farfetch的条款限制自动化访问,所以不论你的工具多么谨慎,抓取都可能与那些条款相抵触。这里的代码没有一行能改变这一点;它只是把技术部分跑通。请阅读Farfetch的条款与条件以及它的robots.txt,并把两者都当作你采集内容的边界。
有几条值得坚守的底线。只采集公开的产品数据:任何人无需账户就能在品类页上看到的品牌、描述、价格、折扣和产品链接。尊重Farfetch声明的速率预期,把你的请求量保持得足够低,以免给它的服务器带来压力。避开个人数据,包括任何超出页面所示公开文本、可关联到可识别评论者的内容。不要把Farfetch受版权保护的媒体内容(例如产品摄影)当作你自己的来重新分发。如果你打算把数据用于商业用途,去取得许可或正式协议,而不要假定沉默就是同意。
对于规模化或商业用途,正确的做法是寻找一个获得许可的渠道。Farfetch通过它的精品店与品牌关系运营合作伙伴和联盟项目,当你需要大批量、有保证的结构或商业权利时,那些才是正确的途径。本指南刻意限定在公开列表数据上,因为那是让这项工作站得住脚的那条线。它不涉及任何登录之后的内容、客户或精品店的个人数据、订单历史,或任何绕过身份验证、或绕过你本不应通过的质询的尝试。如果你的项目所需超出公开列表,正确的路径是一份正式的合作关系或数据协议,而不是一个更聪明的抓取器。
核心要点
-
Farfetch在客户端渲染列表,并且封锁很狠。普通请求返回的是一个空壳,所以你必须在受信任的IP背后渲染页面,设好
ajax_wait和page_wait,再去解析它。 - Crawling API在一次调用中完成繁重工作。它渲染页面,等待JavaScript网格,轮换住宅IP,并应对质询,返回你用cheerio解析的处理好的HTML。
-
cheerio提取字段。选中每个
li[data-testid="productCard"],再从每张卡片内部读取品牌、描述、价格、折扣和绝对产品链接。 -
分页并导出。用
?page=N遍历各页,汇集记录,并把它们写入JSON和CSV,给CSV字段加引号,让逗号繁多的品牌名称和描述保持完整。 - 守在公开数据上。尊重Farfetch的条款和robots.txt,给请求放慢节奏,并在规模化或商业用途上优先选择正式的合作关系或联盟渠道。
常见问题
抓取Farfetch时如何处理JavaScript内容?
Farfetch在客户端加载它的产品网格,所以在页面脚本运行之前原始HTML几乎是空的。把请求通过Crawling API路由,并带上ajax_wait: 'true'和几千毫秒的page_wait,这给JavaScript留出在HTML被捕获之前渲染的时间。这些请求请使用你的JavaScript令牌,因为渲染是一种JS请求类型。
我能从Farfetch的多个页面抓取产品详情吗?
可以。Farfetch对它的品类分页,所以你向列表URL追加一个page参数,例如?page=2,并循环遍历你想要的页面。在每一页上运行相同的获取与解析,并把结果合并进一个数组,正如本指南中的scrapeMultiplePages函数所做的那样。把每一页都通过Crawling API路由,能让轮换和质询处理在整次运行中保持一致。
这个抓取器提取哪些字段?
对每张产品卡片,它从ProductCardBrandName提取品牌,从ProductCardDescription提取描述,从Price或PriceFinal提取价格,从PriceDiscount提取折扣标签,并从卡片锚点提取产品链接,解析为一个绝对的farfetch.com URL。缺失值回退为"N/A",所以一张不完整的卡片永远不会让整次运行中断。
我的选择器对每张卡片都返回"N/A"。变了什么?
几乎肯定是Farfetch的标记变了。它的data-component和data-testid句柄相当稳定但也会变,一次布局更新可能挪动某个字段。在浏览器开发者工具中重新检查一个实时品类页,并更新选择器使其匹配。对任何生产级抓取器来说,定期维护选择器都属正常。
我该如何存储抓取到的数据?
把它存为像JSON或CSV这样的结构化格式。本指南两者都写:farfetch_listings.json供下游代码使用,farfetch_listings.csv供电子表格和BI工具使用。CSV写入器给每个字段加引号并转义内嵌的引号,这样品牌名称或描述里的逗号就不会把列错位。
我能从Farfetch抓取个人数据吗?
不能,本指南也不涉及它。客户账户详情、订单历史,以及任何登录之后的内容,都不是公开数据。抓取登录墙后的内容、超出页面所示公开文本、关于评论者的个人数据,或绕过身份验证,都不在此处的范围内,并且会与Farfetch的条款相抵触。要获得许可的访问,正确的途径是一份正式的合作关系或数据协议。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
