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令牌。把令牌当作密码对待:它用于验证你的请求身份,所以请不要把它放进版本控制。

搭建项目

创建一个项目文件夹,初始化它,并安装抓取器所需的两个库。

bash
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被捕获之前渲染完成。在解析之前检查状态码,能让失败显式暴露而非悄无声息。

javascript
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_waitpage_wait选项正是给客户端网格留出填充时间的东西;你可以在Crawling API文档中读到更多关于可用请求参数的内容。

Crawlbase Crawling API

那第一次请求刚刚返回了一个完全渲染好的Farfetch列表,而你这边不需要无头浏览器或代理。Crawling API在真实浏览器中运行页面,用ajax_waitpage_wait等待JavaScript网格加载,在服务端轮换住宅IP,并应对Farfetch抛给抓取器的各种质询,所以你能从一次调用中得到处理好的HTML。先在免费层把它指向一个男士鞋履品类试试。

第2步:用cheerio解析每张产品卡片

手里有了渲染后的HTML,把它加载进cheerio并遍历产品卡片。Farfetch把每件商品布局为目录网格内的一个列表元素,所以你先选中每张卡片,再从其内部读取品牌、描述、价格、折扣和链接。防御性地读取每个字段,能让单个缺失值不至于让整次运行崩溃。

javascript
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中,价格在PricePriceFinal中,所以选择器两者皆可匹配并取第一个。当商品促销时,折扣标签存放在PriceDiscount中。产品链接是卡片锚点的href,Farfetch将其作为相对路径提供,所以它会相对于https://www.farfetch.com解析,得到一个在页面之外也能用的绝对URL。

选择器会漂移

Farfetch的data-componentdata-testid属性是稳定的句柄,但标记会随时间变化。把上面的选择器当作起始模板,而非契约。当某个字段全线返回"N/A"时,在浏览器开发者工具中重新检查实时页面并更新选择器。对任何生产级抓取器来说,定期维护选择器都属正常,并不意味着出了什么问题。

第3步:处理分页

Farfetch把它的列表分散在许多页上。要采集一个完整品类,就通过向URL追加一个page参数来遍历各页,并在每一页上运行相同的获取与解析,然后把所有内容汇集到一个列表中。

javascript
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两种形式写入磁盘。

javascript
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.csvtoCsv辅助函数会给每个字段加引号并把任何内嵌的引号成对转义,这在这里很重要,因为品牌名称和产品描述里经常含有逗号。

输出长什么样

JSON文件为每张产品卡片保存一个对象,每个对象都带有品牌、描述、价格、折扣和链接。

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,或任何读取分隔文件的数据管道。

csv
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_waitpage_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提取描述,从PricePriceFinal提取价格,从PriceDiscount提取折扣标签,并从卡片锚点提取产品链接,解析为一个绝对的farfetch.com URL。缺失值回退为"N/A",所以一张不完整的卡片永远不会让整次运行中断。

我的选择器对每张卡片都返回"N/A"。变了什么?

几乎肯定是Farfetch的标记变了。它的data-componentdata-testid句柄相当稳定但也会变,一次布局更新可能挪动某个字段。在浏览器开发者工具中重新检查一个实时品类页,并更新选择器使其匹配。对任何生产级抓取器来说,定期维护选择器都属正常。

我该如何存储抓取到的数据?

把它存为像JSON或CSV这样的结构化格式。本指南两者都写:farfetch_listings.json供下游代码使用,farfetch_listings.csv供电子表格和BI工具使用。CSV写入器给每个字段加引号并转义内嵌的引号,这样品牌名称或描述里的逗号就不会把列错位。

我能从Farfetch抓取个人数据吗?

不能,本指南也不涉及它。客户账户详情、订单历史,以及任何登录之后的内容,都不是公开数据。抓取登录墙后的内容、超出页面所示公开文本、关于评论者的个人数据,或绕过身份验证,都不在此处的范围内,并且会与Farfetch的条款相抵触。要获得许可的访问,正确的途径是一份正式的合作关系或数据协议。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量