Noon 是中东最大的电商平台之一,服务于阿联酋、沙特阿拉伯和埃及数百万购物者。其目录涵盖电子产品、时尚、美妆和食品杂货,这些列表页面上的价格和评分对于追踪竞争对手、研究品类或构建价格监控工具的人来说是清晰可用的公开信号。每个搜索页面上的数据一目了然:商品标题、价格、星级评分、品牌以及每件商品的链接。

本指南展示如何用 JavaScript 和 Node.js 配合 cheerio 来抓取 Noon 数据。您将构建一个小型、可运行的爬虫,通过 Crawling API 获取 Noon 搜索列表,解析每件商品的标题、价格、评分、品牌和链接,处理跨结果页面的分页,并将结果导出为 JSON 和 CSV。整个演练的范围仅限于公开商品列表数据,靠近末尾的法律部分并非套话,请在将此脚本应用于任何真实规模之前先阅读它。

您将构建什么

一个 Node.js 脚本,接收一个公开的 Noon 搜索 URL 和查询词,通过 Crawling API 获取已渲染的 HTML,并为列表中的每件商品提取结构化记录。我们以阿联酋商店和查询词"smartphones"作为示例,每件商品提取以下字段:

  • 标题 商品名称,从 data-qa="product-name" 元素读取。
  • 价格 卡片上显示的数字金额,例如"1,799"。
  • 货币 金额旁边的货币标签,如"AED"。
  • 评分 有评论的商品显示的星级评分文本。
  • 品牌 卡片上商品名称上方显示的品牌标签。
  • 链接 指向单个商品页面的完整 URL。

为什么普通请求在 Noon 上会失败

如果您用裸 HTTP 客户端请求 Noon 搜索 URL,很少能得到商品网格。有两个因素对您不利。第一,Noon 使用 JavaScript 在浏览器中渲染其列表卡片,因此在页面脚本运行并通过 AJAX 加载商品数据之前,初始 HTML 近乎空白。第二,Noon 会标记自动化流量:来自数据中心的 IP 以及不像真实浏览器的请求模式,会遭遇 CAPTCHA 挑战、速率限制或封禁,根本无法到达已渲染的列表。

因此,一个可运行的 Noon 爬虫需要在单次请求中同时具备两点:能真正渲染页面并等待 AJAX 内容的浏览器,以及平台识别为真实访客的 IP。您可以自行组合无头浏览器和轮换住宅代理池,但拼凑并维护这些组件本身就是主要工作量。Crawling API 将两者合为一次调用:您向它发送 URL,它在可信 IP 背后渲染页面,等待动态内容,并返回已完成的 HTML 供您用 cheerio 解析。

等待 AJAX 内容

Noon 异步加载商品卡片,因此最重要的参数是 ajax_waitpage_wait。设置 ajax_wait: 'true' 告诉 Crawling API 保持请求直到后台请求完成,而 page_wait 增加固定延迟(毫秒),让慢速卡片有时间在 HTML 被捕获之前渲染出来。

前提条件

在编写任何代码之前,您需要准备好以下几样东西。每项都不需要太长时间。

基本 JavaScript 和 Node.js 知识。您应该能够编写和运行 Node 脚本,并用 npm 安装包。如果您是 Node 新手,官方文档或任何入门课程都能覆盖本教程所需的水平。如需回顾工作流程,关于如何用 Node.js 构建网络爬虫的配套指南涵盖了基础知识。

Node.js 16 或更高版本。node --version 确认您的版本。如果没有,请从 Node.js 官网或通过 nvm 等版本管理器安装。

Crawlbase 账户和 token。注册后,打开控制台,从账户文档页面复制您的 token。免费套餐提供 1,000 次请求,无需绑定信用卡。像对待密码一样对待 token:它用于验证您的请求,不要将其提交到版本控制系统。

设置项目

创建项目文件夹,初始化它,然后安装爬虫所需的两个库。

bash
node --version

mkdir noon-scraper && cd noon-scraper
npm init -y

npm install crawlbase cheerio

两个依赖完成工作:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 以 jQuery 风格的 API 解析返回的 HTML,让您可以通过 CSS 选择器提取各个字段。在此文件夹中创建一个名为 noon-scraper.js 的文件,并将下面各步骤中的代码添加进去。

第一步:获取已渲染的搜索页面

首先获取已完成的页面。导入 CrawlingAPI 类,用您的 token 初始化它,并以设置好 AJAX 等待选项的方式请求 Noon 搜索 URL。在解析之前检查状态码,可以让失败更明显而不是静默发生。

javascript
const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

function searchUrl(query, page) {
  return `https://www.noon.com/uae-en/search/?q=${query}&page=${page}`;
}

const options = { ajax_wait: 'true', page_wait: '5000' };

api
  .get(searchUrl('smartphones', 1), options)
  .then((response) => {
    if (response.statusCode === 200) {
      console.log(response.body.slice(0, 500));
    }
  })
  .catch((error) => console.error('API request error:', error));

node noon-scraper.js 运行脚本,您应该在响应体顶部看到真实的 Noon 列表标记,而不是简化的外壳。这确认了渲染和 AJAX 等待在您编写任何选择器之前已正常工作。如果您不需要自定义字段,而是希望直接获取结构化 JSON 而不用编写解析器,可以在选项中传入 autoparse: 'true',API 将直接返回解析后的数据。

Crawlbase Crawling API

第一次请求刚刚返回了一个完整渲染的 Noon 搜索页面(包含所有 AJAX 卡片),您这边没有无头浏览器,也没有代理。Crawling API 在真实浏览器中运行页面,用 ajax_waitpage_wait 等待动态内容,在服务器端轮换住宅 IP,并处理 Noon 向爬虫抛出的 CAPTCHA,让您从单次调用获得已完成的 HTML。先在免费套餐中指向智能手机搜索试试。

第二步:用 cheerio 解析每件商品

拿到已渲染的 HTML 后,将其加载到 cheerio 并遍历商品卡片。Noon 将每个搜索结果布局在重复的容器中,因此您选择每张卡片,再从中读取标题、价格、货币、评分、品牌和链接。防御性地读取每个字段,确保一个缺失值不会让整个运行崩溃。

javascript
const cheerio = require('cheerio');

function extractProducts(html) {
  const $ = cheerio.load(html);
  const products = [];

  $('div.grid > span.productContainer').each((index, element) => {
    const card = $(element);

    const title = card
      .find('div[data-qa="product-name"]')
      .text()
      .trim();
    const price = card.find('strong.amount').text().trim();
    const currency = card.find('span.currency').text().trim();
    const rating = card.find('div.dGLdNc').text().trim();
    const brand = card.find('div[data-qa="product-brand"]').text().trim();

    const href = card.find('a').attr('href');
    const link = href ? new URL(href, 'https://www.noon.com').href : '';

    if (title && price) {
      products.push({ title, price, currency, rating, brand, link });
    }
  });

  return products;
}

几个细节使这段代码忠实于页面。标题来自 data-qa="product-name" 元素,数字价格位于 strong.amount,货币标签从 span.currency 单独读取,这样"AED"就不会混入金额。评分文本位于 div.dGLdNc,对于没有评论的商品为空;品牌位于 data-qa="product-brand" 元素;链接从卡片锚点的 href 读取,并解析为绝对 URL 以便在页面外使用。末尾的条件检查只有在标题和价格都存在时才推入记录,这样可以过滤掉广告位和空占位卡。

选择器会发生变化

Noon 的哈希类名(如 dGLdNc)由构建系统生成,会不经通知地更改。将上述选择器视为起始模板,而非合同。当某个字段返回空值时,在浏览器开发者工具中重新检查实时页面并更新选择器。页面提供 data-qa 属性的地方优先使用,因为它们比哈希类名更耐用。定期维护选择器是任何生产环境爬虫的正常工作。

第三步:处理跨结果页面的分页

抓取一个搜索页面只是演示;实际运行需要遍历完整结果集。Noon 使用 page 查询参数对搜索结果分页,因此您依次获取每一页,用第二步中的函数解析,当某页返回零件商品时停止。这个空页检查使您无需猜测页面总数就能检测到结果末尾。

javascript
async function fetchPage(query, page) {
  const options = { ajax_wait: 'true', page_wait: '5000' };
  const response = await api.get(searchUrl(query, page), options);
  if (response.statusCode === 200) return response.body;
  console.error(`Failed to fetch page ${page}: ${response.statusCode}`);
  return null;
}

async function scrapeAllPages(query, maxPages) {
  const all = [];
  for (let page = 1; page <= maxPages; page++) {
    console.log(`Scraping page ${page}...`);
    const html = await fetchPage(query, page);
    if (!html) break;

    const products = extractProducts(html);
    if (products.length === 0) {
      console.log('No more results found. Stopping.');
      break;
    }
    all.push(...products);
  }
  return all;
}

fetchPage 辅助函数包装单次请求,在非 200 状态时返回 nullscrapeAllPages 从第 1 页循环到 maxPages,一旦某页返回零件商品就提前中断。由于每次请求都渲染完整页面,在测试时保持 maxPages 适中是友好的做法:每次 JavaScript 渲染请求比普通请求消耗更多额度,因此在增加页数之前,先用一两页确认解析器正常工作。

第四步:组装完整脚本并导出 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' });

function searchUrl(query, page) {
  return `https://www.noon.com/uae-en/search/?q=${query}&page=${page}`;
}

function extractProducts(html) {
  const $ = cheerio.load(html);
  const products = [];

  $('div.grid > span.productContainer').each((index, element) => {
    const card = $(element);
    const title = card.find('div[data-qa="product-name"]').text().trim();
    const price = card.find('strong.amount').text().trim();
    const currency = card.find('span.currency').text().trim();
    const rating = card.find('div.dGLdNc').text().trim();
    const brand = card.find('div[data-qa="product-brand"]').text().trim();
    const href = card.find('a').attr('href');
    const link = href ? new URL(href, 'https://www.noon.com').href : '';

    if (title && price) {
      products.push({ title, price, currency, rating, brand, link });
    }
  });

  return products;
}

async function fetchPage(query, page) {
  const options = { ajax_wait: 'true', page_wait: '5000' };
  const response = await api.get(searchUrl(query, page), options);
  if (response.statusCode === 200) return response.body;
  console.error(`Failed to fetch page ${page}: ${response.statusCode}`);
  return null;
}

async function scrapeAllPages(query, maxPages) {
  const all = [];
  for (let page = 1; page <= maxPages; page++) {
    console.log(`Scraping page ${page}...`);
    const html = await fetchPage(query, page);
    if (!html) break;
    const products = extractProducts(html);
    if (products.length === 0) break;
    all.push(...products);
  }
  return all;
}

function toCsv(rows) {
  const headers = ['title', 'price', 'currency', 'rating', 'brand', '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 query = 'smartphones';
  const maxPages = 3;
  const products = await scrapeAllPages(query, maxPages);
  if (products.length === 0) return;

  fs.writeFileSync('noon-products.json', JSON.stringify(products, null, 2));
  fs.writeFileSync('noon-products.csv', toCsv(products));
  console.log(`Saved ${products.length} products to JSON and CSV`);
}

main();

node noon-scraper.js 运行,您将得到两个文件:包含完整结构化记录的 noon-products.json,以及可在电子表格中打开的 noon-products.csvtoCsv 辅助函数对每个字段加引号并对嵌入的引号进行双写,这在此处很重要,因为商品标题通常很长且经常包含逗号。将 query 改为任何搜索词,准备好进行更大规模抓取时增加 maxPages

输出示例

JSON 文件按 Noon 返回的顺序保存每件商品的对象,包含标题、价格、货币、评分、品牌和链接。

json
[
  {
    "title": "Galaxy S25 AI Dual SIM Silver Shadow 12GB RAM 256GB 5G",
    "price": "3,199",
    "currency": "AED",
    "rating": "4.5",
    "brand": "Samsung",
    "link": "https://www.noon.com/uae-en/galaxy-s25-ai-dual-sim-silver-shadow-12gb-ram-256gb-5g/N70140511V/p/"
  },
  {
    "title": "A78 5G Dual SIM Glowing Black 8GB RAM 256GB",
    "price": "899",
    "currency": "AED",
    "rating": "4.3",
    "brand": "OPPO",
    "link": "https://www.noon.com/uae-en/a78-5g-dual-sim-glowing-black-8gb-ram-256gb/N70115717V/p/"
  }
]

CSV 以表头行镜像了相同的数据行,可直接加载到 Excel、Google Sheets 或任何读取分隔符文件的数据处理管道。

csv
title,price,currency,rating,brand,link
"Galaxy S25 AI Dual SIM Silver Shadow 12GB RAM 256GB 5G","3,199","AED","4.5","Samsung","https://www.noon.com/uae-en/galaxy-s25-ai-dual-sim-silver-shadow-12gb-ram-256gb-5g/N70140511V/p/"
"A78 5G Dual SIM Glowing Black 8GB RAM 256GB","899","AED","4.3","OPPO","https://www.noon.com/uae-en/a78-5g-dual-sim-glowing-black-8gb-ram-256gb/N70115717V/p/"

这些记录可直接用于价格追踪和研究工作。关于将列表数据转化为决策的更广泛视角,请参阅如何用网络爬虫进行价格情报;关于跨电商平台通用的模式,电商网络爬虫指南有详细介绍。

保持不被封禁

即使渲染处理好了,Noon 仍然会监视爬虫形态的流量。以下几个习惯可以让运行保持健康,适用于任何难度较大的商业目标。

  • 控制请求频率。在页面获取之间引入延迟,而不是在紧密循环中轰炸搜索。分散请求是保持在 Noon 速率限制以下最重要的单一因素。
  • 依赖轮换。住宅 IP 池将请求分散到众多真实用户地址,避免任何单一 IP 触发限制或 CAPTCHA。Crawling API 会为您处理这些;如果您自建方案,这是最需要做好的环节。
  • 关注状态码。当运行开始返回挑战或非 200 响应时,说明当前频率或 IP 层级已不再足够。将此视为回退的信号,而不是可以忽略的噪音。

由于 Noon 是客户端渲染的,相同的渲染-解析方法也适用于其他动态商店。更广泛的指南请参阅如何在不被封禁的情况下抓取网站;如果您想单独了解底层技术,如何爬取 JavaScript 网站对渲染进行了更深入的介绍。

抓取 Noon 是否合法?

抓取 Noon 是否被允许,取决于 Noon 的服务条款、您所在的司法管辖区以及您如何使用数据。Noon 的条款限制自动化访问,因此无论您的工具多么谨慎,抓取都可能违反这些条款。本文中的代码不会改变这一点;它只是让技术部分得以实现。请阅读 Noon 的使用条款及其 robots.txt,并将两者视为您收集内容的边界。

有几条底线值得坚守。仅收集公开商品数据:任何人无需账户即可在搜索页面看到的标题、价格、货币、评分、品牌和商品链接。尊重 Noon 所述的速率预期,保持您的请求量足够低,避免给其服务器造成压力。避免个人数据,包括任何与超出页面公开评论文本和星级数的可识别评论者相关联的内容。不要将 Noon 的受版权保护的媒体(如商品摄影)当作自己的内容重新发布。如果您计划商业用途地重用这些数据,请获得许可或正式协议,而不是假设沉默就是默许。

本指南特意将范围限定在公开搜索和列表数据,因为这是使工作具有可辩护性的边界。它不涵盖登录后的内容、客户或卖家的个人数据、订单历史,以及任何绕过您无权通过的身份验证或 CAPTCHA 的尝试。如果 Noon 或其合作伙伴为您的用例提供了经过授权的数据源或官方 API,那当您需要大规模、有保证的结构或商业权利时,那才是正确的工具。如果您的项目需要超出公开列表的内容,官方协议才是正确路径,而不是设计更聪明的爬虫。

回顾

核心要点

  • Noon 在客户端渲染列表且防御严密。普通请求返回空壳或 CAPTCHA,因此您必须在解析之前在可信 IP 背后渲染页面并等待其 AJAX 内容。
  • Crawling API 一次调用完成。传入 ajax_wait: 'true'page_wait 使动态卡片加载,API 在服务器端轮换住宅 IP 并处理 CAPTCHA;如果您想要 JSON 而不是原始 HTML,可以添加 autoparse: 'true'
  • cheerio 提取字段。选择每个 span.productContainer,再读取标题、价格、货币、评分、品牌和链接,优先使用稳定的 data-qa 属性,因为哈希类名会发生变化。
  • 分页是页面参数。循环 page 查询值,当某页返回零件商品时停止,在测试时保持 maxPages 适中,因为每次渲染请求消耗更多额度。
  • 坚守公开数据的边界。尊重 Noon 的服务条款和 robots.txt,控制请求频率,避免个人数据和需要登录的内容,对于大规模或商业用途优先选择官方数据源。

常见问题

我可以从 Noon 搜索页面抓取哪些数据?

每张搜索卡片上的公开字段:商品标题、数字价格、货币、有评论的商品的星级评分、品牌以及商品页面的链接。本指南从网格中的每个 span.productContainer 读取这些字段。登录后的内容(如账户详情或订单历史)超出范围,不是公开数据。

为什么普通请求从 Noon 返回不完整的数据?

因为 Noon 使用 JavaScript 在客户端渲染商品网格,通过 AJAX 加载卡片,然后用 CAPTCHA 挑战自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回空壳或拦截页面,而不是商品卡片。要获取完整页面,您必须渲染它,等待 AJAX 内容,并在可信 IP 背后请求,这正是 Crawling API 为您处理的事情。

ajax_wait 和 page_wait 有什么作用?

它们控制 Crawling API 在捕获 HTML 之前等待多长时间。设置 ajax_wait: 'true' 保持请求直到后台请求完成,而 page_wait 增加以毫秒为单位的固定延迟,让慢速加载的卡片有时间渲染出来。两者在 Noon 上都很重要,因为商品数据在初始页面加载后才到达,而不是在第一个响应中。

如何处理 Noon 上的分页?

Noon 使用 page 查询参数对搜索结果分页,因此您递增它并依次获取每一页。本指南中的 scrapeAllPages 函数从第 1 页循环到 maxPages 限制,当某页返回零件商品时停止,无需预先猜测总页数就能检测到结果末尾。

我的选择器返回空值。什么发生了变化?

几乎可以肯定是 Noon 的标记发生了变化。像 dGLdNc 这样的哈希类名由构建系统生成,会不经通知地更改,因此上个月有效的选择器可能突然失效。在浏览器开发者工具中重新检查实时页面并更新选择器,优先使用页面提供的稳定 data-qa 属性。定期维护选择器是任何生产环境爬虫的正常工作。

我可以从 Noon 抓取客户个人数据吗?

不行,本指南也不涉及此类内容。客户账户详情、订单历史以及登录后的任何内容都不是公开数据。抓取需要登录的内容、超出公开评论文本的评论者个人数据,或绕过身份验证,超出了本文范围且违反 Noon 的条款。对于大规模的授权访问,官方数据协议才是正确路径。

开始构建

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

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

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