Tokopedia是印度尼西亚最大的电商平台之一,拥有数千万活跃买家和数十万在电子产品、服装、杂货和家居用品等品类上架商品的商家。其公开搜索和商品页面是丰富的需求信号:任何查询所呈现的标题、价格、卖家、评分和销量,正是团队用于价格追踪、竞争对手研究和品类趋势分析的数据。
本指南将向您展示如何使用JavaScript和Node.js结合cheerio抓取Tokopedia数据。您将构建一个小型可运行的爬虫,通过Crawling API获取Tokopedia搜索列表和商品页面,解析每个商品的标题、价格、店铺、评分、销量和链接,处理分页,并将结果导出为JSON和CSV格式。整个演练范围限定于公开商品列表数据,文末的合法性部分在您将其应用于任何实际规模之前值得认真阅读。
您将构建什么
一个Node.js脚本,接受一个公开的Tokopedia搜索URL,通过Crawling API获取渲染后的HTML,并为结果网格中的每个商品提取结构化记录。我们以耳机搜索作为示例,每个条目提取以下字段:
- 标题 商品卡片上显示的产品名称。
- 价格 显示的价格,例如"Rp178.000"。
- 店铺 上架该商品的店铺或卖家名称。
- 评分 卡片显示时的星级评分文本。
- 销量 存在时的已售件数,例如"60+ terjual"。
- 链接 到该商品详情页的URL。
为什么普通请求在Tokopedia上会失败
如果您用普通HTTP客户端请求Tokopedia搜索URL,几乎永远拿不到商品网格。有两个原因对您不利。首先,Tokopedia在浏览器中通过JavaScript渲染其商品卡片,因此初始HTML是一个接近空白的外壳,直到页面脚本运行并获取商品数据后才会填充。其次,平台会标记自动化流量:不像真实浏览器的数据中心IP和请求模式,在到达渲染列表之前就会被限速或封锁。
因此,一个有效的Tokopedia爬虫在一次请求中需要两样东西:一个真正渲染页面的浏览器,以及一个平台视为真实访客的IP。您可以自己用无头浏览器加上轮换住宅代理池来实现,但将这些组合在一起并保持健康运行才是大部分工作量所在。Crawling API将两者折叠进一次调用:您发送URL,它在受信任的IP背后渲染页面,并返回完整HTML供您用cheerio解析。
Crawlbase发放两种令牌:用于静态网站的普通令牌,以及用于浏览器渲染内容的JavaScript令牌。Tokopedia通过JavaScript加载商品,因此这里请使用JavaScript令牌。免费层提供1,000次请求且无需绑卡,您可以在付费前测试完整流程。
前提条件
在编写任何代码之前,您需要准备几样东西,每样都不费多少时间。
JavaScript和Node.js基础。 您应该能够编写和运行Node脚本,并用npm安装包。如果您是Node新手,Node.js网络爬虫构建指南涵盖了本教程所假设的基础知识。
Node.js 16或更高版本。 用node --version确认您的版本。如果没有,请从Node.js官网安装或通过版本管理器(如nvm)安装。
Crawlbase账号和令牌。 注册后打开仪表盘,复制您的JavaScript令牌。免费层提供1,000次请求且无需绑卡。请像对待密码一样对待令牌:它用于验证您的请求,所以不要将其放入版本控制。
搭建项目
创建一个项目文件夹,初始化它,然后安装爬虫所需的两个库。
node --version mkdir tokopedia-scraper && cd tokopedia-scraper npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase是Crawling API的官方Node客户端,cheerio使用类jQuery的API解析返回的HTML,让您可以通过CSS选择器提取各个字段。在此文件夹中创建名为tokopedia-scraper.js的文件,并添加以下步骤中的代码。
第1步:获取渲染后的搜索页面
首先获取完整页面。导入CrawlingAPI类,用您的令牌初始化,然后请求搜索URL。Tokopedia延迟加载结果,因此传入ajax_wait和page_wait延迟,给页面足够的渲染时间。在解析之前检查状态码,让失败可见而非静默。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const options = { ajax_wait: 'true', page_wait: '5000' }; const searchURL = 'https://www.tokopedia.com/search?q=headset'; api .get(searchURL, options) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
用node tokopedia-scraper.js运行脚本,您应该在响应体顶部看到真实的Tokopedia商品标记,而非精简过的外壳。这在您编写任何选择器之前就能确认渲染正常工作。ajax_wait标志指示API等待页内请求完成,page_wait则额外等待5秒,让延迟加载的商品卡片有时间出现。
第一次请求刚刚返回了完整渲染的Tokopedia搜索页面,无需在您这边部署无头浏览器或代理。Crawling API在真实浏览器中运行页面,等待JavaScript加载的商品卡片稳定,并在服务端轮换住宅IP,让您一次调用就能获得完整HTML。先在免费层对任何公开搜索查询试试。
第2步:用cheerio解析每个商品
手握渲染后的HTML,将其加载进cheerio并遍历商品卡片。Tokopedia将每个结果排布在搜索结果区域内的重复容器中,因此您选取每张卡片,然后从中读取标题、价格、店铺、评分、销量和链接。防御性地读取每个字段,确保一个缺失的值不会导致整个运行崩溃。
const cheerio = require('cheerio'); function parseSearchListings(html) { const $ = cheerio.load(html); const products = []; const cards = $( 'div[data-testid="divSRPContentProducts"] div.css-5wh65g' ); cards.each((index, element) => { const card = $(element); const title = card .find('span.OWkG6oHwAppMn1hIBsC3pQ\\=\\=') .text() .trim(); const price = card .find('div.ELhJqP-Bfiud3i5eBR8NWg\\=\\=') .text() .trim(); const shop = card .find('span.X6c-fdwuofj6zGvLKVUaNQ\\=\\=') .text() .trim(); // Rating and sold count sit in small text rows under the price const rating = card.find('span.nBBbPk2cBpbZJ2nFPN8jKA\\=\\=').text().trim(); const sold = card.find('span.eLNb-rRDe6X9p64ZsQAx9w\\=\\=').text().trim(); const link = card.find('a.Nq8NlC5Hk9KgVBJzMYBUsg\\=\\=').attr('href'); if (title) { products.push({ title: title, price: price || 'N/A', shop: shop || 'N/A', rating: rating || 'N/A', sold: sold || 'N/A', link: link || 'N/A', }); } }); return products; }
有几个细节让这段代码忠实于页面。商品卡片位于divSRPContentProducts test id下,每张卡片的标题在类名为OWkG6oHwAppMn1hIBsC3pQ==的span中,价格在类名为ELhJqP-Bfiud3i5eBR8NWg==的div中,店铺名在类名为X6c-fdwuofj6zGvLKVUaNQ==的span中。商品链接从卡片的锚点href读取。这些类名包含字面量==字符,因此在cheerio选择器字符串中需转义为\\=\\=。评分和销量选择器遵循价格下方小字文本行的相同模式。
Tokopedia的哈希类名(如OWkG6oHwAppMn1hIBsC3pQ==及其他)是自动生成的,会在无通知的情况下变更。请将上述选择器视为起始模板而非合同。当某个字段返回空时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期进行选择器维护是任何生产级爬虫的正常操作,并不意味着出了问题。
第3步:处理分页
Tokopedia将搜索结果分布在多个页面上,每一页都可以通过在URL后附加page参数来访问,例如&page=2。从第一页循环到您想要的最后一页,通过Crawling API获取每一页,用第2步中的函数解析,并将所有内容收集到一个数组中。如果某页获取失败,提前停止而不是推送空结果。
async function fetchHtml(url) { const response = await api.get(url, options); if (response.statusCode === 200) return response.body; console.error(`Failed to fetch page: ${response.statusCode}`); return null; } async function scrapeMultiplePages(baseUrl, maxPages) { const allProducts = []; for (let page = 1; page <= maxPages; page++) { const paginatedUrl = `${baseUrl}&page=${page}`; const html = await fetchHtml(paginatedUrl); if (!html) break; const products = parseSearchListings(html); allProducts.push(...products); console.log(`Page ${page}: ${products.length} products`); } return allProducts; }
这段代码遍历您指定的页面,从每页抓取列表,并汇总结果。由于每个搜索页面共享相同的卡片结构,您在第2步编写的解析器无需任何修改即可跨页面使用。
第4步:组装带JSON和CSV导出的完整脚本
现在将获取、解析器和分页整合为一个可运行的脚本,然后将记录写入磁盘,同时生成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' }; // Paste parseSearchListings, fetchHtml, and scrapeMultiplePages here function toCsv(rows) { const headers = ['title', 'price', 'shop', 'rating', 'sold', '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.tokopedia.com/search?q=headset'; const maxPages = 5; const products = await scrapeMultiplePages(baseUrl, maxPages); fs.writeFileSync( 'tokopedia_search_results.json', JSON.stringify(products, null, 2) ); fs.writeFileSync('tokopedia_search_results.csv', toCsv(products)); console.log(`Saved ${products.length} products to JSON and CSV`); } main();
将parseSearchListings、fetchHtml和scrapeMultiplePages函数粘贴到同一文件中,以便main能够调用它们。用node tokopedia-scraper.js运行,您会得到两个文件:包含完整结构化记录的tokopedia_search_results.json和可在电子表格中打开的tokopedia_search_results.csv。toCsv辅助函数对每个字段加引号并将内嵌的引号双写,这在Tokopedia标题通常很长且经常包含逗号的情况下至关重要。
输出示例
JSON文件按搜索顺序保存每个商品的对象,每个对象包含标题、价格、店铺、评分、销量和链接。
[ { "title": "Ipega PG-R008 Gaming Headset for P4 /X1 series/N-Switch Lite/Mobile", "price": "Rp178.000", "shop": "ipegaofficial", "rating": "4.9", "sold": "250+ terjual", "link": "https://www.tokopedia.com/ipegaofficial/ipega-pg-r008-gaming-headset" }, { "title": "Hippo Toraz Handsfree Earphone Stereo Sound - headset, Putih", "price": "Rp13.000", "shop": "HippoCenter", "rating": "4.8", "sold": "1rb+ terjual", "link": "https://www.tokopedia.com/hippocenter88/hippo-toraz-handsfree-earphone" } ]
CSV以带标题行的相同行格式展现,可直接在Excel、Google Sheets或任何读取分隔符文件的数据管道中使用。
title,price,shop,rating,sold,link "Ipega PG-R008 Gaming Headset for P4 /X1 series","Rp178.000","ipegaofficial","4.9","250+ terjual","https://www.tokopedia.com/ipegaofficial/ipega-pg-r008-gaming-headset" "Hippo Toraz Handsfree Earphone Stereo Sound - headset, Putih","Rp13.000","HippoCenter","4.8","1rb+ terjual","https://www.tokopedia.com/hippocenter88/hippo-toraz-handsfree-earphone"
抓取单个商品页面
搜索列表给您带来广度;商品页面给您带来深度。一旦您从搜索爬虫获得链接,就可以通过同一个Crawling API获取商品页面,并提取更丰富的字段。在Tokopedia商品页面上,名称位于带data-testid="lblPDPDetailProductName"的h1中,价格位于带data-testid="lblPDPDetailProductPrice"的div中,店铺位于带data-testid="llbPDPFooterShopName"的a中,描述位于带data-testid="lblPDPDescriptionProduk"的div中,缩略图位于data-testid="PDPImageThumbnail"按钮内的img标签中。
async function scrapeProductPage(url) { const html = await fetchHtml(url); if (!html) return null; const $ = cheerio.load(html); const images = $('button[data-testid="PDPImageThumbnail"] img') .map((i, el) => $(el).attr('src')) .get(); return { name: $('h1[data-testid="lblPDPDetailProductName"]').text().trim(), price: $('div[data-testid="lblPDPDetailProductPrice"]').text().trim(), shop: $('a[data-testid="llbPDPFooterShopName"]').text().trim(), description: $('div[data-testid="lblPDPDescriptionProduk"]').text().trim(), images: images, }; }
这复用了搜索爬虫中相同的fetchHtml辅助函数,因此商品页面爬虫无需任何额外设置即可继承渲染和IP轮换能力。向它传入搜索爬虫收集到的任何链接,它会返回一个结构化对象,您可以用JSON.stringify或CSV中的一行来存储它。
保持不被封锁
即使处理了渲染,Tokopedia仍会监控爬虫形态的流量。以下几个习惯能让抓取运行保持健康,它们适用于任何防御严密的商业目标。
- 控制请求节奏。 在页面获取之间引入延迟,而不是紧密循环地连续请求搜索结果。分散请求是保持在平台速率限制之下的最重要因素。
- 依赖轮换。 住宅IP池将请求分散到众多真实用户地址上,使单个地址不会触发限制。Crawling API为您处理这一切;如果您自己搭建,这是最需要做对的部分。
- 关注状态码。 运行开始返回非200响应时,说明当前速率或IP级别已不够用。将此视为信号需要退缩,而非可以忽略的噪音。
更广泛的操作手册,请参阅如何在不被封锁的情况下抓取网站以及JavaScript网站爬取更深入的指南,两者都更详细地介绍了渲染和轮换方面的内容。
抓取Tokopedia合法吗?
抓取Tokopedia是否被允许,取决于Tokopedia的服务条款、您所在的司法管辖区以及您对数据的使用方式。Tokopedia的条款限制自动化访问,因此无论您的工具多么谨慎,抓取行为都可能违反这些条款。这里的代码并不改变这一点,它只是让技术部分得以实现。请阅读Tokopedia的使用条款及其robots.txt,将两者都视为您采集范围的边界。
有几条值得坚守的底线。仅采集公开商品数据:任何人无需账号即可在搜索或商品页面看到的标题、价格、店铺名、评分、销量和商品链接。遵守Tokopedia的速率预期,将请求量控制在不对其服务器造成压力的水平。避免个人数据,包括任何与可识别买家或评论者相关的信息,超出页面上公开显示的评论文本和星级计数。不要将Tokopedia受版权保护的媒体(如商家商品摄影)重新分发为您自己的内容。
本指南有意限定在公开搜索和商品列表范围内,因为这是保持工作可辩护的边界。它不涵盖登录后的任何内容、买家或卖家个人数据、订单历史、聊天,或任何绕过身份验证或不应通过的CAPTCHA的尝试。如果您的项目需要超出公开列表的数据,正确的路径是获得授权的数据协议或Tokopedia提供的任何官方API渠道,而不是更聪明的爬虫。如有疑问,宁可取得许可,也不要假定沉默就是同意。
核心要点
- Tokopedia在客户端渲染列表,且限速严格。 普通请求返回空白外壳,因此您必须在解析之前用JavaScript令牌在受信任IP背后渲染页面。
-
Crawling API一次调用完成两件事。 它通过
ajax_wait和page_wait渲染页面,轮换住宅IP,并将完整HTML交给您用cheerio解析。 -
cheerio提取字段。 选取每张商品卡片,然后读取标题、价格、店铺、评分、销量和链接,同时对哈希的
==类名进行转义,并预期它们会随时间漂移。 -
分页和商品页面扩展了爬虫。 循环
page参数获取广度,然后重用相同的获取辅助函数,从单个商品页面提取名称、价格、店铺、描述和图片。 - 坚守公开数据。 遵守Tokopedia的服务条款和robots.txt,控制请求节奏,将结果导出为JSON和CSV,避免登录后的内容或任何个人数据。
常见问题
从Tokopedia抓取数据合法吗?
当您限于公开商品数据并遵循Tokopedia的服务条款时,抓取可能是允许的。请先查阅网站规则和robots.txt,保持合理的请求量,避免个人或需要登录的数据。将您采集的数据用于研究、价格追踪或分析等合法目的,不要做任何违反Tokopedia政策或当地法律的事情。
为什么我需要Crawling API来抓取Tokopedia?
Tokopedia通过JavaScript加载商品,因此原始HTTP请求通常返回空白外壳而非商品卡片。Crawling API在真实浏览器中渲染页面,通过ajax_wait和page_wait等待延迟加载的内容,并轮换住宅IP,让您无需管理浏览器集群或代理池就能获取完整列表。
我可以从Tokopedia提取哪些数据点?
从搜索列表中,您可以提取商品标题、价格、店铺名、评分、销量和链接。从单个商品页面,您可以提取名称、价格、店铺、完整描述和图片URL。这些合在一起涵盖了大多数团队进行价格监控、竞争对手研究和商品趋势分析所需的公开字段。
为什么我的Tokopedia选择器返回空值?
几乎可以肯定是标记发生了变化。Tokopedia的哈希类名(如OWkG6oHwAppMn1hIBsC3pQ==)是自动生成的,会在无通知的情况下变更,因此上个月还能用的选择器可能就失效了。在浏览器开发者工具中重新检查实时页面并更新选择器。定期进行选择器维护是任何生产级爬虫的正常操作。
如何处理Tokopedia搜索结果的分页?
在搜索URL后附加page参数,例如&page=2,并从第一页循环到您想要的最后一页。通过Crawling API获取每一页,用相同的cheerio函数解析,并将结果收集到一个数组中,然后再导出。如果某页获取失败,提前停止以免用空结果填充数据。
我可以从Tokopedia抓取买家或卖家个人数据吗?
不行,本指南也不涵盖这些内容。买家账户、订单历史、聊天以及任何需要登录的内容都不是公开数据。抓取需要登录的内容、买家或评论者超出公开评论文本范围的个人数据,或绕过身份验证,均超出本文范围且违反Tokopedia的条款。如有更广泛的需求,请参阅电商网络爬虫指南或网络爬取在价格情报中的应用概述,并对超出公开列表的需求优先寻求授权的数据协议。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
