Product Hunt 是每天发布和排名新软件、硬件及副业项目的平台,公开的排名对追踪创作者动态的人而言是一个清晰的信号。创始人用它来评估竞品,研究人员借助它发现品类趋势,增长团队则研究哪些发布获得了最多点赞。数据就摆在每个分类页面上:产品名称、标语、点赞数以及每条列表的链接。
本指南将向你展示如何使用 JavaScript 和 Node.js 配合 Cheerio 来抓取 Product Hunt。你将构建一个小型、可运行的爬虫,通过 Crawling API 获取 Product Hunt 分类页面,解析每条列表的产品名称、标语、点赞数和链接,可选择从主页获取基本的公开创作者详情,处理分页,并将结果导出为 JSON 和 CSV。整个演示仅限于公开产品数据,文末的合法性部分不是走过场,请在正式大量抓取前认真阅读。
你将构建什么
一个 Node.js 脚本,接收 Product Hunt 的公开分类 URL,通过 Crawling API 获取渲染后的 HTML,并为列表中的每个产品提取结构化记录。我们以工程与开发分类页面作为运行示例,每个条目提取以下字段:
- 名称:列表卡片上显示的产品名称。
- 标语:名称下方的简短描述行。
- 点赞数:点赞计数,存在时解析为数字。
- 评论数:评论数量文本,如"151 reviews"。
- 链接:指向产品独立页面的 URL。
指南后续部分会添加一个独立的流程,读取创作者的基本公开简介字段(如姓名、简介、粉丝和关注数量及积分),以便你了解同样的方法如何从列表扩展到单个公开简介。
为什么普通请求在 Product Hunt 上会失败
如果你用普通 HTTP 客户端请求 Product Hunt 分类 URL,很少能得到产品列表。两个因素对你不利。首先,Product Hunt 在浏览器中使用 JavaScript 渲染其列表卡片,因此初始 HTML 只是一个接近空白的外壳,直到页面脚本运行后才会有内容。其次,平台监控自动化流量:来自数据中心 IP 且请求模式不像真实浏览器的访问,会在触达渲染后的产品数据之前被限速或封锁。
因此,一个有效的 Product Hunt 爬虫需要在一次请求中同时具备两样东西:一个真正渲染页面的浏览器,以及一个被平台识别为真实访客的 IP。你可以自己用无头浏览器加轮换住宅代理池来拼凑这一切,但将两者整合并保持健康运行就是大部分工作量。Crawling API 将两者合并为一次调用:你发送 URL,它在可信 IP 后面渲染页面,返回处理完毕的 HTML 供 Cheerio 解析。
由于 Product Hunt 在客户端构建其列表,你需要启用 Crawling API 的 JavaScript 渲染来请求这些页面。在官方 Node 客户端中,这意味着用你的 JavaScript token 初始化客户端。渲染请求比普通请求消耗更多额度,但免费套餐仍足以让你在扩展之前端到端测试整个流程。
前提条件
编写任何代码之前,你需要准备几样东西。这些都不需要太长时间。
基本的 JavaScript 和 Node.js 知识。你应该能熟练编写和运行 Node 脚本、使用 npm 安装包,以及理解 Cheerio 所镜像的 DOM 概念。如果你是 Node 新手,使用 Node.js 构建网络爬虫涵盖了本教程所假设的基础知识。
Node.js 16 或更高版本。用 node --version 确认你的版本。如果没有安装,请从 Node.js 官网或通过 nvm 等版本管理器安装。
Crawlbase 账号和 token。注册后,打开控制台,从账号文档页面复制你的 JavaScript token。免费套餐提供 1,000 次请求,无需信用卡。将 token 视为密码:它用于验证你的请求,因此请勿放入版本控制。
配置项目
创建项目目录,初始化,并安装爬虫需要的两个库。
node --version mkdir producthunt-scraper && cd producthunt-scraper npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 通过类 jQuery 的 API 解析返回的 HTML,让你能通过 CSS 选择器提取各个字段。在该目录下创建名为 scraper.js 的文件,将下方步骤中的代码添加进去。
第一步:获取渲染后的分类页面
从获取完整页面开始。导入 CrawlingAPI 类,用你的 JavaScript token 初始化,然后请求分类 URL。将响应体保存到磁盘,让你只需一次请求就能检查真实的标记,之后可以反复调整选择器而无需每次消耗请求额度。
const { CrawlingAPI } = require('crawlbase'); const fs = require('fs'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const producthuntPageURL = 'https://www.producthunt.com/categories/engineering-development'; api .get(producthuntPageURL) .then((response) => { if (response.statusCode === 200) { fs.writeFileSync('response.html', response.body); console.log('HTML saved to response.html'); } }) .catch((error) => console.error('API request error:', error));
运行 node scraper.js,你应该看到真实的 Product Hunt 标记被写入 response.html,而不是一个精简的外壳。这证明在你编写任何一个选择器之前,渲染已正常工作。将示例 URL 替换为你想要的任何分类页面;所有分类都位于同一域名下的 /categories/ 路径。
第一次请求就返回了完整渲染的 Product Hunt 分类页面,你这边既没有无头浏览器,也没有代理。Crawling API 在真实浏览器中运行页面,在服务器端轮换住宅 IP,并处理平台对爬虫施加的频率限制,让你从一次调用中获得完整的 HTML。先在免费套餐上试试工程与开发分类。
第二步:用 Cheerio 解析每个产品
拿到渲染后的 HTML,将其加载到 Cheerio 并遍历产品卡片。Product Hunt 将每条列表呈现在一个重复的容器中,因此你选中所有卡片,然后从中读取名称、标语、点赞数、评论数和链接。防御性地读取每个字段,可以防止一个缺失值导致整次运行崩溃。
const fs = require('fs'); const cheerio = require('cheerio'); function parseProducts(html) { const $ = cheerio.load(html); const products = []; const containers = $( 'div.flex.direction-column.mb-mobile-10.mb-tablet-15.mb-desktop-15.mb-widescreen-15' ); containers.each((index, element) => { const card = $(element); const name = card .find('div.color-blue.fontSize-18.fontWeight-600') .text() .trim(); // Each filled star is one label element inside the rating row const upvotes = card.find( 'div.flex.direction-row.align-center label' ).length; const reviews = card .find('div.ml-3.color-lighter-grey.fontSize-14.fontWeight-400') .text() .trim(); const tagline = card .find( 'div.color-lighter-grey.fontSize-mobile-14.fontSize-tablet-16.fontSize-desktop-16.fontSize-widescreen-16.fontWeight-400' ) .text() .trim(); const href = card.find('a').first().attr('href'); const link = href ? new URL(href, 'https://www.producthunt.com').href : ''; if (name) { products.push({ rank: index + 1, name, tagline, upvotes, reviews, link }); } }); return products; }
几个细节确保了对页面的忠实还原。产品名称来自 color-blue fontSize-18 fontWeight-600 块,标语来自 Product Hunt 用于描述行的长 color-lighter-grey 字体大小类链。点赞和评分行对每个已填充的标记渲染一个 label 元素,因此计数这些 label 元素即可获得数量,无需解析任何文本。评论数文本在其自己的 ml-3 color-lighter-grey 块中,链接从卡片的第一个锚点读取并解析为绝对 URL,以便在页面外使用。
Product Hunt 的类名(长 fontSize-* 链及其余类名)是生成的,随时可能无通知地改变。将上面的选择器视为起始模板,而非合同。当某个字段返回空时,在浏览器开发工具中重新检查实时页面并更新选择器。定期进行选择器维护对任何生产爬虫来说都是正常的,不是出了问题的迹象。更广泛的技术请参见如何爬取 JavaScript 网站。
第三步:完整脚本与 JSON 及 CSV 导出
现在将抓取和解析整合到一个可运行的脚本中,然后将记录写入 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(pageUrl) { const response = await api.get(pageUrl); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function toCsv(rows) { const headers = ['rank', 'name', 'tagline', 'upvotes', 'reviews', '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.producthunt.com/categories/engineering-development'; const html = await crawl(url); if (!html) return; const products = parseProducts(html); fs.writeFileSync('products.json', JSON.stringify(products, null, 2)); fs.writeFileSync('products.csv', toCsv(products)); console.log(`Saved ${products.length} products to JSON and CSV`); } main();
将第二步中的 parseProducts 函数粘贴到同一文件,以便 main 调用它。运行 node scraper.js,你会得到两个文件:products.json 包含完整的结构化记录,products.csv 可直接在电子表格中打开。toCsv 辅助函数对每个字段加引号并将嵌套引号双写,这里尤为重要,因为产品标语较长且常包含逗号。
输出示例
JSON 文件按列表顺序保存每个产品的对象,每个对象包含排名、名称、标语、点赞数、评论数文本和链接。
[ { "rank": 1, "name": "The Free Website Guys", "tagline": "A free website program that has helped over 10,000 entrepreneurs.", "upvotes": 5, "reviews": "151 reviews", "link": "https://www.producthunt.com/products/the-free-website-guys" }, { "rank": 2, "name": "Zipy", "tagline": "A debugging platform with session replay and network monitoring.", "upvotes": 5, "reviews": "132 reviews", "link": "https://www.producthunt.com/products/zipy" } ]
CSV 文件镜像相同的行,带有标题行,可直接导入 Excel、Google Sheets 或任何读取分隔符文件的数据管道。
rank,name,tagline,upvotes,reviews,link "1","The Free Website Guys","A free website program that has helped over 10,000 entrepreneurs.","5","151 reviews","https://www.producthunt.com/products/the-free-website-guys" "2","Zipy","A debugging platform with session replay and network monitoring.","5","132 reviews","https://www.producthunt.com/products/zipy"
处理整个分类的分页
一个页面只是演示;真实任务需要遍历整个分类。Product Hunt 在滚动时加载更多列表,分类页面也接受可逐步递增的 page 参数。最简单可靠的模式是依次获取各页,用同一函数解析,当某页返回零条新产品时停止。在循环中加入短暂延迟,可将请求频率保持在合理范围内。
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function scrapeCategory(slug, maxPages = 5) { const all = []; for (let page = 1; page <= maxPages; page++) { const url = `https://www.producthunt.com/categories/${slug}?page=${page}`; const html = await crawl(url); if (!html) break; const rows = parseProducts(html); if (rows.length === 0) break; all.push(...rows.map((p) => ({ category: slug, ...p }))); await sleep(2000); } return all; } scrapeCategory('engineering-development').then((rows) => { console.log(`Collected ${rows.length} products`); });
由于每个分类页面共享相同的卡片结构,你已经写好的解析器无需任何修改即可跨分类工作。导出前为每行打上分类标签,你就可以在一个数据集中比较工程、设计和 AI 等分类的发布情况。这种模式直接迁移到产品研究场景;关于将排名列表转化为决策的更深入探讨,参见如何自动化电商产品研究。
抓取基本公开简介数据
同样的获取后解析方法适用于单个公开创作者简介。简介公开展示基本字段,如显示名称、简介、粉丝和关注数量、积分,以及该创作者发布过的产品。以下片段沿用相同字段。请将抓取范围限定在公开简介基本信息内,运行前请阅读合法性部分。
function parseProfile(html, handle) { const $ = cheerio.load(html); const profile = {}; profile.name = $( 'h1.color-darker-grey.fontSize-24.fontWeight-600' ).text().trim(); profile.headline = $( 'div.color-lighter-grey.fontSize-18.fontWeight-300' ).text().trim(); profile.followers = $( `a[href="/@${handle}/followers"]` ).text().trim(); profile.following = $( `a[href="/@${handle}/following"]` ).text().replace(/\n\s+/g, ' ').trim(); profile.points = $( 'span.color-lighter-grey.fontSize-14.fontWeight-400:contains("points")' ).text().trim(); // Public list of products the maker has shipped profile.products = []; $('.styles_even__Qeyum, .styles_odd__wazk7').each((i, el) => { profile.products.push({ name: $(el).find('img.styles_thumbnail__Y9ZpZ').attr('alt'), }); }); return profile; } async function scrapeProfile(handle) { const html = await crawl(`https://www.producthunt.com/@${handle}`); return html ? parseProfile(html, handle) : null; }
选择器映射了原有字段:显示名称来自 h1.color-darker-grey 标题,简介来自 fontSize-18 fontWeight-300 行,粉丝和关注数量来自链接到用户自己的粉丝和关注页面的锚点,积分总数来自包含"points"一词的 span。产品列表读取每个缩略图的 alt 文本。只提取这些公开基本信息,不要进一步深入任何标识私人个人的内容。
保持不被封锁
即使渲染已由服务处理,Product Hunt 仍会监控爬虫形态的流量。以下几个习惯有助于保持运行顺畅,适用于任何商业目标网站。
- 控制请求节奏。在页面请求之间引入延迟,而不是在紧凑循环中轰炸页面。分散请求是保持在平台频率限制之下的最有效方法。
- 依靠轮换。一个住宅 IP 池将请求分散到许多真实用户地址,避免单个地址触发限制。Crawling API 为你处理这些;如果你自己搭建方案,这是需要做好的部分。
- 关注状态码。运行开始返回挑战或非 200 响应,就是在告诉你当前的频率或 IP 层级已不够用。将其视为退出的信号,而不是可以忽略的噪音。
更广泛的操作手册请参见如何在不被封锁的情况下抓取网站。
抓取 Product Hunt 合法吗?
是否允许抓取 Product Hunt 取决于 Product Hunt 的服务条款、你所在的司法管辖区以及你对数据的用途。Product Hunt 的条款限制自动化访问,因此无论你的工具多么谨慎,抓取行为都可能违反这些条款。本文的任何代码都不会改变这一现实,它只是让技术层面的工作得以实现。阅读 Product Hunt 的服务条款及其 robots.txt,并将两者视为你收集数据范围的边界。
有几条原则值得坚守。只收集公开产品数据:任何人无需账号即可在分类页面看到的名称、标语、点赞数、评论数和链接。除了页面公开展示的公开简介基本信息外,不要收集创作者的个人数据,也不要将简介汇总成针对或标识私人个人的数据集。遵守 Product Hunt 的频率预期,将请求量控制在不会给其服务器造成压力的范围内。不要以自己的名义重新分发 Product Hunt 的版权媒体,如产品图片或创作者头像。如果你打算将数据用于商业目的,请先获得许可或签订官方协议,而不是默认沉默即为同意。
对于大规模或商业使用,Product Hunt 提供了官方 API。它使用 OAuth2 token 验证并有自己的频率限制,商业使用默认受限,因此在用其构建商业应用之前须联系 Product Hunt 获得批准。当你需要大量数据、有保障的结构或商业权利时,官方 API 是正确的工具。本指南故意将范围限定在公开列表和公开简介基本信息,因为这是保持工作合法性的边界。它不涉及登录后的任何内容、私人用户数据,或任何绕过身份验证的尝试。如果你的项目需要的不只是公开数据,官方 Product Hunt API 或数据协议才是正确的路径,而不是打造一个更聪明的爬虫。
核心要点
- Product Hunt 在客户端渲染列表。普通请求返回空壳,因此必须在可信 IP 后面渲染页面才能解析。
- Crawling API 一次调用搞定两件事。它用 JavaScript token 渲染页面,轮换住宅 IP,并返回处理完毕的 HTML 供 Cheerio 解析。
- Cheerio 提取字段。选中所有产品容器,然后读取名称、标语、点赞数、评论数和链接,从循环索引推导排名,并预期生成的类名会发生变化。
- 分页并导出。逐步遍历分类页面直到某页返回空,然后将结构化记录写入 JSON 和 CSV,为 CSV 字段加引号以保持含逗号的标语完整。
- 坚守公开数据。遵守 Product Hunt 的服务条款和 robots.txt,只获取公开简介基本信息,不收集个人数据,大规模或商业使用时优先选择官方 Product Hunt API。
常见问题
我可以从 Product Hunt 抓取哪些数据?
从公开分类页面可以提取每个产品的名称、标语、点赞数、评论数文本,以及列表的链接。从公开创作者简介可以读取基本公开字段,如显示名称、简介、粉丝和关注数量、积分,以及该创作者发布过的产品。将收集范围限定在这些公开基本信息内,避免任何超出页面公开展示范围的标识私人个人的信息。
为什么普通请求从 Product Hunt 返回的数据不完整?
因为 Product Hunt 在客户端用 JavaScript 渲染其列表,并监控自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回接近空白的外壳,而不是产品卡片。要获得完整页面,必须在可信 IP 后面渲染,这正是使用 JavaScript token 的 Crawling API 为你处理的工作。
Product Hunt 需要 JavaScript token 吗?
是的。Product Hunt 在浏览器中构建其分类和简介页面,因此需要启用 JavaScript 渲染来请求这些页面。在官方 Node 客户端中,这意味着用你的 JavaScript token 初始化 Crawling API。渲染请求比普通请求消耗更多额度,但免费套餐仍足以在扩展前测试完整流程。
我的选择器返回空值,是什么变了?
几乎可以肯定是 Product Hunt 的标记发生了变化。其生成的类名(长 fontSize-* 链和哈希 styles_* 类)随时可能无通知地改变,因此上个月还能用的选择器可能已经失效。在浏览器开发工具中重新检查实时页面并更新选择器。定期进行选择器维护对任何生产爬虫来说都是正常的。
Product Hunt 有官方 API 吗?
有。Product Hunt 提供使用 OAuth2 token 验证并有频率限制的官方 API,商业使用默认受限,因此须在用其构建商业应用之前申请批准。当你需要大量数据、有保障的结构或商业权利时,官方 API 是正确的路径,而不是抓取。
抓取 Product Hunt 时如何避免被封锁?
降低每个 IP 的请求频率,在页面请求之间加入延迟,并通过轮换住宅 IP 路由流量,以免单个地址触发频率限制。Crawling API 为你管理轮换和可信 IP 池;如果你自己搭建方案,这是需要投入的部分。监控状态码,一旦开始遇到挑战就退出。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
