Temu 是一个快速成长的电商平台,以极具竞争力的价格提供庞大的商品目录,涵盖电子产品、时尚和家居用品。其搜索和商品列表页面载有零售商、分析师和价格追踪者所关注的公开信号:产品名称、当前价格、买家评分以及已售数量。综合分析某个品类的这些字段,可以了解哪些商品正在走红以及价格的变化趋势。
本指南将向你展示如何使用 JavaScript 和 Node.js 结合 cheerio 抓取 Temu。我们将构建一个简洁可运行的爬虫,通过 Crawling API 获取 Temu 搜索结果页面,解析每件商品的标题、价格、评分、已售数量和链接,处理"查看更多"分页,并将结果导出为 JSON 和 CSV 格式。整个教程仅限于公开产品列表数据,文末的合法性章节并非套话,请在将其应用于实际大批量采集前务必阅读。
你将构建什么
一个 Node.js 脚本,接受 Temu 的公开搜索 URL,通过 Crawling API 获取渲染后的 HTML,并为列表中的每件商品提取一条结构化记录。我们以通用搜索词为示例,每件商品提取以下字段:
- Title 卡片上显示的产品名称,用于识别商品及其类别。
- Price 显示的价格文字,用于趋势监测和比较同类产品。
- Rating 卡片上显示的星级评分,是客户满意度的快速参考。
- Sold count Temu 在许多卡片上显示的"N 已售"数字,是粗略的需求信号。
- Image URL 商品缩略图,用于视觉数据库或下游应用。
- Link 指向该商品详情页的 URL,供后续深入查看使用。
为什么普通请求在 Temu 上会失败
如果你用基础 HTTP 客户端请求 Temu 的搜索 URL,你很少能得到产品网格。有两个原因。首先,Temu 在浏览器中用 JavaScript 渲染其商品卡片,因此初始 HTML 几乎是一个空壳,直到页面脚本运行并加载商品列表。其次,Temu 积极地对抗自动化流量:不像真实浏览器的数据中心 IP 和请求模式,在到达渲染后的产品数据之前就会收到 CAPTCHA、被限速或被封锁。
因此,一个能正常工作的 Temu 爬虫需要在单次请求中同时具备两点:一个真正渲染页面的浏览器,以及一个平台认为是真实访客的 IP。你可以自行搭建无头浏览器加轮换住宅代理池,但将它们拼接在一起并保持正常运行才是主要工作量。Crawling API 将两者整合为一次调用:你向它发送 URL,它在受信任 IP 后面渲染页面,并返回渲染完成的 HTML,供你用 cheerio 解析。由于商品列表由客户端脚本加载,你需要告知 API 在返回前等待该内容。
抓取像 Temu 这样的 JavaScript 渲染内容,需要使用 Crawlbase 控制台中的 JavaScript 请求 token,而非普通 token。免费层级包含 1,000 次请求且无需信用卡,且只对成功请求收费。请参阅定价了解普通请求和 JavaScript 请求的计费方式。
前提条件
在编写任何代码之前,你需要准备几样东西,都不会花太长时间。
基础 JavaScript 和 Node.js 知识。 你应该熟悉编写和运行 Node 脚本,以及使用 npm 安装包。如果你是 Node.js 新手,官方文档或任何入门课程都能让你达到本教程所需的水平。
Node.js 16 或更高版本。 使用 node --version 确认你的版本。如果尚未安装,请从 Node.js 官网或通过 nvm 等版本管理器安装。
Crawlbase 账户和 token。 注册后,打开控制台,从账户文档页面复制你的 JavaScript 请求 token。请像对待密码一样保管 token:它用于验证你的请求,因此不要将其提交到版本控制系统中。
设置项目
创建项目文件夹,初始化它,并安装爬虫所需的两个库。
node --version mkdir temu-scraper && cd temu-scraper npm init -y npm install crawlbase cheerio
两个依赖完成主要工作:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 使用类 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。在该文件夹中创建名为 temu-scraper.js 的文件,并将以下步骤中的代码添加进去。
步骤 1:获取渲染后的搜索页面
首先获取渲染完成的页面。导入 CrawlingAPI 类,使用你的 JavaScript token 初始化它,并请求搜索 URL。Temu 使用客户端脚本加载其商品列表,因此传入 ajax_wait 和 page_wait 延迟,让内容在 API 返回之前有时间出现。在解析之前检查状态码,可以让失败情况清晰可见而非悄无声息。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const searchURL = 'https://www.temu.com/search_result.html?search_key=wireless+earbuds'; const options = { ajax_wait: 'true', page_wait: '5000' }; 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 temu-scraper.js,你应该在 body 顶部看到真实的 Temu 产品标记,而非一个精简的空壳。这证明在你编写任何选择器之前渲染功能已经正常工作。ajax_wait 标志告知 API 等待页内请求稳定,page_wait 添加毫秒级的固定延迟,两者共同给商品列表留出填充时间。
刚才那次请求已在你不需要无头浏览器或代理的情况下返回了完整渲染的 Temu 搜索页面。Crawling API 在真实浏览器中运行页面,等待 JavaScript 商品列表加载,在服务器端轮换住宅 IP,并处理 Temu 对爬虫发出的 CAPTCHA,让你通过一次调用获得渲染完成的 HTML。先在免费层级以某个搜索词试用。
步骤 2:使用 cheerio 解析每件商品
拿到渲染后的 HTML,将其加载到 cheerio 中并遍历产品卡片。Temu 将每个搜索结果排列在商品列表内的重复容器中,因此你选择每个卡片,然后从中读取标题、价格、评分、已售数量、图片和链接。防御性地读取每个字段,可以避免因单个缺失值导致运行崩溃。
const cheerio = require('cheerio'); function parseSearchResults(html) { const $ = cheerio.load(html); const products = []; const cards = $( 'div.js-search-goodsList > div.autoFitList > div.EKDT7a3v' ); cards.each((index, element) => { const card = $(element); const title = card.find('h2._2BvQbnbN').text().trim(); const price = card.find('span._2de9ERAH').text().trim(); const rating = card.find('div._1bGyLOoB').text().trim(); const sold = card.find('span._3VxQjs6a').text().trim(); const imageUrl = card.find('img.goods-img-external').attr('src') || ''; const href = card.find('a._2Tl9qLr1').attr('href'); const link = href ? new URL(href, 'https://www.temu.com').href : ''; if (title) { products.push({ title, price, rating, sold, imageUrl, link }); } }); return products; }
有几个细节让这段代码忠实于页面。每个卡片位于 div.js-search-goodsList > div.autoFitList > div.EKDT7a3v 下,标题来自 h2._2BvQbnbN 标题,价格来自 span._2de9ERAH,评分和已售数量各来自其自己的小文本节点,缩略图来自 img.goods-img-external 的 src,链接来自 a._2Tl9qLr1 的 href(解析为绝对 URL,使其在页面之外也能正常使用)。if (title) 防护丢弃有时出现在网格中的空占位单元格。
Temu 的类名(_2BvQbnbN、_2de9ERAH、EKDT7a3v 等)是生成的,会在没有通知的情况下发生变化。将上面的选择器视为起始模板,而非永久合同。当某个字段返回空时,在浏览器开发者工具中重新检查实时页面并更新选择器。对于任何生产级爬虫,定期维护选择器都是正常操作,而非出了问题的迹象。
步骤 3:处理分页并组合完整脚本
Temu 在"查看更多"按钮后面加载更多结果,而非使用独立的编号页面。Crawling API 可以在返回 HTML 之前为你点击该按钮,使用 css_click_selector 选项,这样单次渲染就可以呈现更大批量的卡片。将获取、点击和解析整合到一个可运行的脚本中,然后将记录以 JSON 和 CSV 两种格式写入磁盘。
const fs = require('fs'); const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); async function crawl(url) { const response = await api.get(url, { ajax_wait: 'true', page_wait: '5000', css_click_selector: 'div.R8mNGZXv[role="button"]', }); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function toCsv(rows) { const headers = ['title', 'price', 'rating', 'sold', 'imageUrl', '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 url = 'https://www.temu.com/search_result.html?search_key=wireless+earbuds'; const html = await crawl(url); if (!html) return; const products = parseSearchResults(html); fs.writeFileSync('temu-products.json', JSON.stringify(products, null, 2)); fs.writeFileSync('temu-products.csv', toCsv(products)); console.log(`Saved ${products.length} products to JSON and CSV`); } main();
将步骤 2 中的 parseSearchResults 函数粘贴到同一个文件中,这样 main 就可以调用它。css_click_selector 指向 Temu 的"查看更多"控件(div.R8mNGZXv[role="button"]);API 在渲染期间点击它,使返回的 HTML 中包含该点击加载的额外卡片。运行 node temu-scraper.js,你将得到两个文件:包含完整结构化记录的 temu-products.json 和可直接在电子表格中打开的 temu-products.csv。toCsv 辅助函数对每个字段加引号并转义内嵌的引号,这在产品标题较长且经常包含逗号的情况下非常重要。
输出结果示例
JSON 文件按列表顺序每件商品对应一个对象,包含标题、价格、评分、已售数量、图片 URL 和链接。
[ { "title": "Wireless Earbuds Bluetooth 5.3 with Charging Case", "price": "$8.97", "rating": "4.6", "sold": "12K+ sold", "imageUrl": "https://img.kwcdn.com/product/open/earbuds-001.jpg", "link": "https://www.temu.com/goods-detail-g-601099527865713.html" }, { "title": "True Wireless Sports Earphones Noise Cancelling", "price": "$11.49", "rating": "4.4", "sold": "3.2K+ sold", "imageUrl": "https://img.kwcdn.com/product/open/earbuds-002.jpg", "link": "https://www.temu.com/goods-detail-g-601099537192760.html" } ]
CSV 以表头行镜像相同的行,可直接导入 Excel、Google Sheets 或任何读取分隔文件的数据管道。
title,price,rating,sold,imageUrl,link "Wireless Earbuds Bluetooth 5.3 with Charging Case","$8.97","4.6","12K+ sold","https://img.kwcdn.com/product/open/earbuds-001.jpg","https://www.temu.com/goods-detail-g-601099527865713.html" "True Wireless Sports Earphones Noise Cancelling","$11.49","4.4","3.2K+ sold","https://img.kwcdn.com/product/open/earbuds-002.jpg","https://www.temu.com/goods-detail-g-601099537192760.html"
跨查询扩展规模并保持畅通运行
单个搜索词只是演示;实际工作需要遍历多个搜索词或品类。建立搜索词列表,通过 Crawling API 使用相同的等待和点击选项获取每个搜索词,用相同的函数解析,并在导出前为每行打上搜索词标签。由于每个搜索页面共享相同的卡片结构,你已编写好的解析器无需修改即可应用于所有搜索词。
async function scrapeQueries(queries) { const all = []; for (const query of queries) { const term = encodeURIComponent(query); const url = `https://www.temu.com/search_result.html?search_key=${term}`; const html = await crawl(url); if (!html) continue; const rows = parseSearchResults(html).map((p) => ({ query, ...p })); all.push(...rows); await new Promise((r) => setTimeout(r, 2000)); } return all; } scrapeQueries(['wireless earbuds', 'phone case', 'led lights']).then((rows) => { console.log(`Collected ${rows.length} products across queries`); });
即使渲染已由 Crawling API 处理,Temu 仍会监测类似爬虫的流量,因此一些良好习惯可以让运行保持健康。控制请求速率:上面查询之间的 setTimeout 延迟分散了流量,而非在紧密循环中大量请求页面,这是保持在速率限制下最关键的因素。善用轮换:住宅 IP 池将请求分散到多个真实用户地址,使任何单个地址都不会触发限制或 CAPTCHA,Crawling API 为你处理这一切。关注状态码:运行开始返回非 200 响应,说明你需要退后,而非忽略这个信号。更广泛的操作手册,请参阅如何在不被封锁的情况下抓取网站;关于渲染部分,请参阅抓取 JavaScript 网站。
这种模式可直接用于定价和研究工作。关于将房源数据转化为定价决策,请参阅如何将网络抓取用于价格情报;关于电商抓取的更广泛视野,电商网页抓取指南涵盖了可在其他商店复用相同获取-解析方法的模式。
抓取 Temu 合法吗?
抓取 Temu 是否被允许,取决于 Temu 的服务条款、你所在的司法管辖区以及你对数据的用途。Temu 的条款限制自动化访问,因此无论工具多么谨慎,抓取行为都可能违反这些条款。这里的代码并不改变这一点,它只是让技术部分得以实现。请阅读 Temu 的使用条款及其 robots.txt,并将两者视为你采集内容的边界。
几条值得坚守的原则。只采集公开产品数据:任何人无需账户即可在搜索页面上看到的标题、价格、评分、已售数量、图片和产品链接。遵守 Temu 的速率预期,将请求量保持在不对其服务器造成压力的水平。避免个人数据,包括任何与可识别评论者相关的内容(页面上显示的聚合评分和数量除外)。不得以自己名义商业性地重新托管 Temu 受版权保护的媒体(如产品摄影);将图片 URL 作为引用与重新托管图片是不同的行为。
本指南刻意将范围限于公开搜索和房源数据,因为这是让工作站得住脚的底线。它不涵盖任何登录后才能访问的内容、顾客或卖家个人数据、订单历史,也不涉及任何绕过身份验证或你本不应通过的 CAPTCHA 的尝试。如果你的项目需要更多公开房源以外的内容,或你计划商业性地再利用数据,正确路径是官方协议或授权的合作伙伴或联盟计划,而非更巧妙的爬虫。当电商平台提供官方数据渠道时,对于大批量或商业用途,请优先使用它。
核心要点
- Temu 在客户端渲染房源并有严密防护。 普通请求返回空壳或 CAPTCHA,因此必须在受信任 IP 后面渲染页面并等待商品列表,然后再进行解析。
-
Crawling API 通过一次调用完成繁重工作。 它渲染页面,使用
ajax_wait和page_wait等待 AJAX 内容,轮换住宅 IP,并处理 CAPTCHA,返回渲染完成的 HTML。 - cheerio 提取各字段。 选择商品列表下的每个卡片,然后读取标题、价格、评分、已售数量、图片和链接,并预期生成的类名会漂移。
-
分页是一次"查看更多"点击。 Temu 在按钮后面加载更多结果,因此
css_click_selector告知 API 在渲染期间点击它并返回更大批量的结果。 - 坚守公开数据原则。 遵守 Temu 的服务条款和 robots.txt,控制请求速率,避免个人数据和登录后内容,并将整洁的 JSON 和 CSV 导出供下游使用。
常见问题
为什么普通请求从 Temu 返回的数据不完整?
因为 Temu 使用 JavaScript 在客户端渲染其产品网格,并用 CAPTCHA 对抗自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回空壳或封锁页面,而非产品卡片。要获得完整页面,必须在受信任 IP 后面渲染它并等待商品列表加载,而这正是 Crawling API 通过 ajax_wait 和 page_wait 为你处理的工作。
抓取 Temu 需要 JavaScript token 吗?
需要。Temu 的内容是 JavaScript 渲染的,因此你使用 Crawlbase 控制台中的 JavaScript 请求 token 而非普通 token,并传入等待选项使渲染后的商品列表出现在响应中。免费层级包含 1,000 次请求且无需信用卡,JavaScript 请求与普通请求的计费方式不同,请查看定价页面了解详情。
Temu 上的分页是如何工作的?
Temu 在"查看更多"按钮后面加载更多房源,而非使用独立的编号页面。Crawling API 可以使用 css_click_selector 选项在渲染期间点击该按钮,因此单次请求可以返回更大批量的卡片。将选择器指向该按钮(例如 div.R8mNGZXv[role="button"]),额外结果就会包含在你解析的同一份 HTML 中。
我的选择器返回空值。发生了什么变化?
几乎可以肯定是 Temu 的标记发生了变化。其容器和类名如 _2BvQbnbN、_2de9ERAH 和 EKDT7a3v 是生成的,会在没有通知的情况下发生变化,因此上个月还有效的选择器可能会失效。在浏览器开发者工具中重新检查实时页面并更新选择器。对于任何生产级爬虫,定期维护选择器都是正常操作。
我可以将 Temu 数据存储在数据库中而非 CSV 中吗?
可以。CSV 和 JSON 适合首次数据获取,但对于较大或持续性的项目,像 PostgreSQL 或 MongoDB 这样的数据库使数据更容易随时间查询和分析。将 main 中的 writeFileSync 调用替换为数据库客户端的插入逻辑,爬虫的其余部分保持不变。
如何避免在抓取 Temu 时被封锁?
保持较低的单 IP 请求速率,在查询之间添加延迟,并通过轮换住宅 IP 路由请求,确保没有单个地址触发速率限制或 CAPTCHA。Crawling API 为你管理轮换、受信任 IP 池和 CAPTCHA 处理;如果你自建栈,这正是需要投入的部分。关注状态码,一旦开始看到挑战就立即退后。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
