尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于 Browser Harness 的 Product Hunt 实战抓取指南:data-test 选择器体系与完整数据提取方案

基于 Browser Harness 的 Product Hunt 实战抓取指南:data-test 选择器体系与完整数据提取方案 基于 Browser Harness 的 Product Hunt 实战抓取指南data-test 选择器体系与完整数据提取方案【免费下载链接】browser-harnessBrowser Harness | Self-healing harness that enables LLMs to complete any task.项目地址: https://gitcode.com/gh_mirrors/br/browser-harness本指南以 agent-workspace/domain-skills/producthunt/scraping.md 为核心骨架系统讲解如何使用 Browser Harness一个通过 CDP 直接控制真实浏览器的开源工具抓取 Product Hunt 的产品数据从首页每日产品流、日/周/月排行榜到主题页、分类页、产品详情页与搜索结果。文档中的全部选择器均于 2026-04-18 在真实浏览器运行中验证读者学完后可以直接复制运行其中的 Python JS 提取片段把 Product Hunt 的产品名、tagline、slug、票数、话题标签等元数据批量结构化输出。一、抓取前必读Product Hunt 页面结构的关键事实Product Hunt 是一个 React 单页应用SPA其 DOM 结构有几个直接影响抓取方案的事实这些事实是整套选择器体系的出发点无登录墙所有产品数据无需登录即可访问不需要处理登录流程或携带会话。无 Cookie 弹窗页面干净加载没有同意弹窗省去关闭 dialog 的步骤。产品 URL 使用/products/而非/posts/a[href^/posts/]匹配不到任何元素这是最容易踩的坑之一。data-test属性是全站最可靠的选择器相比 CSS class 和嵌套结构data-test属性语义稳定是整套抓取方案的核心锚点。首页包含 4 个版块today、yesterday、last week、last month每个版块约 5 个产品并带有 see all 入口。当天票数在每日前 4 小时被隐藏显示为—em dash而不是数字。首页固定 30 个 post-item滚动不会加载更多这是固定集合不是无限滚动流。goto_url()在某些浏览器会话中对 producthunt.com 可能返回ERR_ABORTED应改用new_tab()进行导航。这些事实决定了后续所有选择器与导航代码的写法。下文所有示例都依赖 Browser Harness 预置的 helper 函数new_tab、wait、page_info、js它们由 src/browser_harness/helpers.py 提供并通过browser-harnessCLI 自动导入。二、运行环境启用 Domain Skills 并以 heredoc 方式执行本抓取文档属于 Browser Harness 的Domain Skills体系。该体系默认关闭需要通过环境变量BH_DOMAIN_SKILLS1启用启用后在执行站点相关任务前应读取$BH_AGENT_WORKSPACE/domain-skills/site/目录下的所有文件而不是自行发明方案见 src/browser_harness/SKILL.md。goto_url()在BH_DOMAIN_SKILLS1时还会把匹配站点的技能文件名随导航结果一起返回。在 src/browser_harness/helpers.py 中可以看到该实现它从当前导航 URL 的主机名提取站点名在$BH_AGENT_WORKSPACE/domain-skills/site/目录下收集最多 10 个.md文件名附加到导航返回值中对应的测试见 tests/unit/test_helpers.py。执行方式采用 heredoc 传入 Python 代码helper 全部预导入run.py会先调用ensure_daemon()再执行代码见 src/browser_harness/run.pybrowser-harness PY print(page_info()) PY三、导航模式用 new_tab() 而不是 goto_url()Product Hunt 的 SPA 加载特性决定了导航写法。文档中给出的标准导航模式如下# goto_url() 在 Product Hunt 上可能失败 —— 可靠的做法是使用 new_tab() tid new_tab(https://www.producthunt.com) wait(4) # React SPA 需要渲染时间仅 wait_for_load() 不够 page page_info() # 验证: url 应为 https://www.producthunt.com/这条经验有两层源码支撑goto_url()可能返回ERR_ABORTED。从 src/browser_harness/helpers.py 可以看到goto_url()直接调用 CDP 的Page.navigate在部分浏览器会话中对该域名的导航会在到达目标页之前被中止因此文档明确要求使用new_tab()。new_tab()的内部实现更稳健src/browser_harness/helpers.py它会先创建about:blank后台标签页Target.createTarget使用backgroundTrue避免抢占前台再附加会话并执行goto_url()若当前已附加的标签是空白页则会直接复用并导航避免创建重复标签。对应测试见 tests/unit/test_helpers.py。为什么wait(4)而非只调wait_for_load()wait_for_load()只轮询document.readyState complete见 src/browser_harness/helpers.py而 React SPA 的 load 事件触发后框架仍在继续渲染 DOM。Product Hunt 首页的post-item-*元素在 load 之后才逐步出现因此需要额外等待几秒。抓取时建议用wait(3)~wait(4)给足渲染时间。四、首页提取[data-test^post-item-]选择器完整方案首页展示当天发布的产品同时附带 yesterday、last week、last month 三个滚动版块。核心选择器是[data-test^post-item-]通过^前缀匹配定位所有产品卡片再在卡片内二次查找名称、链接、票数、话题标签等子元素。# 完整提取 name、tagline、slug、votes、topics products js( JSON.stringify( Array.from(document.querySelectorAll([data-test^post-item-])).map(el { var id el.getAttribute(data-test).replace(post-item-, ); var nameEl el.querySelector([data-test^post-name-]); var productLink el.querySelector(a[href^/products/]); var voteBtn el.querySelector([data-testvote-button]); var voteCount voteBtn ? voteBtn.textContent.trim() : null; var topicLinks Array.from(el.querySelectorAll(a[href^/topics/])).map(a a.textContent.trim()); var name nameEl ? nameEl.textContent.trim() : ; var lines el.innerText.split(\\n).map(l l.trim()).filter(l l); var tagline lines.find(l l ! name !topicLinks.includes(l) l ! • !/^[0-9—]/.test(l) l.length 5); return { id: id, name: name, slug: productLink ? productLink.getAttribute(href) : null, votes: voteCount, topics: topicLinks, tagline: tagline || null }; }) ) )样本输出[ {id:1126372,name:Vercel Flags,slug:/products/vercel,votes:—,topics:[Software Engineering,Developer Tools],tagline:Feature flags, targeting rules, rollouts. All from Vercel.}, {id:1125388,name:1. Claude Opus 4.7,slug:/products/claude-opus-4-7,votes:466,topics:[API,Artificial Intelligence,Development],tagline:Claudes most capable model for reasoning and agentic coding} ]这段 JS 通过js()helper 执行。src/browser_harness/helpers.py 中的js()使用 CDPRuntime.evaluate并带returnByValueTrue执行表达式若表达式出现顶层非法return会自动重试包一层(function(){...})()因此文档中的JSON.stringify(...)写法总能正确返回序列化字符串。票数字段的三态解析votes字段存在三种形态必须按状态分别处理形态含义解析方式—当天产品在每日前 4 小时内的票数被隐藏视为未知改用昨日排行榜466数字字符串昨日或更早产品直接int()2,152大票数的千分位逗号格式先voteCount.replace(,, )再去掉逗号排名前缀处理排行榜产品名会带排名前缀如1. Claude Opus 4.7。清洗方式import re clean_name re.sub(r^\d\.\s, , name)五、排行榜获取完整每日列表的最佳入口排行榜页面可以拿到某一天的全部产品及真实票数是完整数据的最佳来源URL: https://www.producthunt.com/leaderboard/daily/YYYY/M/D 示例: https://www.producthunt.com/leaderboard/daily/2026/4/18关键点月/日不带前导零4 月写作4不是04。使用与首页完全相同的[data-test^post-item-]选择器。每天展示 12–19 个产品覆盖度高于首页固定 30 条。首页与排行榜共用同一套提取 JS代码无需改动。昨日结果带真实票数{id:1125388,name:1. Claude Opus 4.7,slug:/products/claude-opus-4-7,votes:466,tagline:Claudes most capable model for reasoning and agentic coding} {id:1118208,name:2. Build Check,slug:/products/build-check-for-outsiders,votes:396}这也解释了上一节票数陷阱的解法当天前 4 小时票数隐藏时用昨日排行榜获取确认过的票数。六、周榜与月榜时间窗口扩展周榜与月榜使用同样的选择器只是 URL 参数不同URL: https://www.producthunt.com/leaderboard/weekly/YYYY/WW 示例: https://www.producthunt.com/leaderboard/weekly/2026/16 URL: https://www.producthunt.com/leaderboard/monthly/YYYY/M 示例: https://www.producthunt.com/leaderboard/monthly/2026/4注意点周数是ISO 周示例中 2026 年第 16 周对应 4 月 13–19 日。当前周可能返回 0 个条目直到该周结束数据才完整抓取历史周数据更稳妥。月榜与日榜一样不带前导零。七、主题页与分类页[data-test^product:]选择器主题页和分类页的 DOM 结构不同于首页选择器从post-item-切换为[data-test^product:]。主题页URL 示例https://www.producthunt.com/topics/developer-tools# 导航到主题页 new_tab(https://www.producthunt.com/topics/developer-tools) wait(3) products js( JSON.stringify( Array.from(document.querySelectorAll([data-test^product:])).map(el { var slug el.getAttribute(data-test).replace(product:, ); var link el.querySelector(a[href^/products/]); return { slug: slug, href: link ? link.getAttribute(href) : null, text: el.outerText.trim().substring(0, 200) }; }) ) )样本输出{slug:figma,href:/products/figma,text:FigmaThe collaborative interface design tool4.9 (1.4K reviews)...}注意主题页返回的是该主题下约15 个评分最高的产品含评分与评论数不是最近发布的产品。这与首页/排行榜按时间排序的语义不同抓取目标不同时应选择对应的入口。分类页URL 示例https://www.producthunt.com/categories/ai-agents使用与主题页相同的[data-test^product:]选择器返回该分类下约 15 个高评分产品{slug:elevenlabs,href:/products/elevenlabs,text:ElevenLabs\nCreate natural AI voices instantly...\n4.9 (165 reviews)}八、产品详情页票数、评分、排名与评论产品详情页 URL 格式为https://www.producthunt.com/products/slug例如https://www.producthunt.com/products/claude-opus-4-7。详情页的选择器与列表页不同需要逐项提取。获取总票数侧边栏按钮注意区分两个选择器总票数用[data-testvote-button]而评论区的点赞是[data-testaction-bar-vote-button]两者含义不同。# 使用 [data-testvote-button] —— 与 [data-testaction-bar-vote-button] 不同 vote_text js(document.querySelector([data-test\vote-button\]).outerText.trim().replace(/\\s/g, )) # 返回: Upvote • 466 points # 解析票数: vote_text.split(•)[1].strip().replace( points, ).replace(,, )获取评论数与评分review_link js(JSON.stringify(Array.from(document.querySelectorAll(a)).filter(a a.href a.href.includes(/reviews) a.outerText.includes(review)).map(a a.outerText.trim()).slice(0, 1))) # 返回: [1 review] 或 [5.0\n(731 reviews)]获取当日排名侧边栏会显示类似#1 Day Rank的信息但没有专门的data-test属性可供定位需要从侧边栏上下文解析或者直接使用排行榜位置代替。评论点赞数每条评论自带一个[data-testaction-bar-vote-button]文本形如Upvote (13)可用于统计评论互动量。九、搜索结果spotlight-result-product-与 URL 重构搜索页 URL 格式https://www.producthunt.com/search?qAIagent注意空格编码为。new_tab(https://www.producthunt.com/search?qAIagent) wait(3) results js( JSON.stringify( Array.from(document.querySelectorAll([data-test^spotlight-result-product-])).map(el { var id el.getAttribute(data-test).replace(spotlight-result-product-, ); var lines el.outerText.trim().split(\\n).map(l l.trim()).filter(l l); return { id: id, name: lines[0] || null, tagline: lines[1] || null, review_text: lines[2] || null }; }) ) )样本输出{id:526014,name:/ai,tagline:Access ChatGPT anywhere you type /ai,review_text:2 reviews} {id:991302,name:Naoma AI Demo Agent,tagline:The first video agent that runs conversational product demos,review_text:5 reviews}重要限制搜索结果元素是button而不是a链接DOM 中没有href。同时元素的data-testID 对应的是数字产品 ID而不是 slug。因此无法直接从结果元素获得产品 URL需要从其他途径获取 slug 后自行拼接https://www.producthunt.com/products/slug。十、关键选择器参考表下表汇总了全套已验证的选择器是按页面对照查询的速查手册页面选择器数量说明首页[data-test^post-item-]304 个版块 × 每块约 5–7 个产品首页[data-test^post-name-]30产品名称元素首页/排行榜[data-testvote-button]不定—表示隐藏数字表示可见主题/分类[data-test^product:]~15评分最高的产品搜索[data-test^spotlight-result-product-]10Button 元素无 href产品详情[data-testvote-button]1主投票Upvote • N points产品详情[data-testaction-bar-vote-button]多个评论点赞Upvote (N)十一、常见陷阱8 条实战经验总结以下坑位全部来自真实浏览器运行中的验证记录建议在编写抓取脚本前通读复杂元素上innerText可能返回None——改用outerText或拆成简单的单属性表达式。避免在JSON.stringify()中对大对象做链式 DOM 遍历。goto_url()对 producthunt.com 可能返回ERR_ABORTED——始终改用new_tab(url)。a[href^/posts/]匹配不到任何元素——Product Hunt 产品 URL 用的是/products/不是/posts/。当天前 4 小时票数永远是———需要确认票数时使用昨日排行榜。首页滚动不会懒加载更多产品——30 条是固定集合完整的每日列表请使用排行榜页面。对 DOM 密集对象执行JSON.stringify会返回None——只序列化原始类型字符串、数字不要序列化活动的 DOM 节点属性。从实现层面看这是因为 src/browser_harness/helpers.py 中的_runtime_value依赖Runtime.evaluate的returnByValue把结果序列化回进程DOM 节点等非序列化值会走unserializableValue分支或返回None因此规范做法是像上文示例那样用JSON.stringify(...)把数据先转成纯 JSON 字符串。带排名的产品名包含排名前缀——如1. Claude Opus 4.7用re.sub(r^\d\.\s, , name)清洗。wait_for_load()之后仍需wait(3)——React SPA 在 load 事件后继续渲染选择器元素晚于 load 出现。第 8 条的底层原因已在前文说明wait_for_load()只轮询document.readyStatesrc/browser_harness/helpers.py无法感知框架渲染进度抓取 Product Hunt 这类 SPA 时务必在导航后补足等待时间。若需要更精确的等待也可以使用wait_for_element([data-test^post-item-], timeout10)轮询目标选择器出现。十二、推荐工作流抓取当日发布产品的完整代码将以上要点整合为一个可复制的完整流程。以抓取 2026-04-18 的日榜为例排行榜带真实票数避免当天前 4 小时票数隐藏的问题# 1. 在新标签页打开 Product Hunt 日榜 new_tab(https://www.producthunt.com/leaderboard/daily/2026/4/18) wait(4) # 2. 提取所有产品及元数据 products js( JSON.stringify( Array.from(document.querySelectorAll([data-test^post-item-])).map(el { var id el.getAttribute(data-test).replace(post-item-, ); var nameEl el.querySelector([data-test^post-name-]); var productLink el.querySelector(a[href^/products/]); var voteBtn el.querySelector([data-testvote-button]); var topicLinks Array.from(el.querySelectorAll(a[href^/topics/])).map(a a.textContent.trim()); var name nameEl ? nameEl.textContent.trim() : ; var lines el.innerText.split(\\n).map(l l.trim()).filter(l l); var tagline lines.find(l l ! name !topicLinks.includes(l) l ! • !/^[0-9—]/.test(l) l.length 5); return { id: id, name: name, slug: productLink ? productLink.getAttribute(href) : null, votes: voteBtn ? voteBtn.textContent.trim() : null, topics: topicLinks, tagline: tagline || null }; }) ) ) import json data json.loads(products) print(fFound {len(data)} products) for p in data: print(f {p[name]} — {p[votes]} votes — {p[tagline]})该工作流可以直接套用到首页、周榜、月榜仅替换 URL 与等待时间也可以在上游json.loads之后按需清洗票数replace(,, )与名称前缀re.sub再做排序、过滤或入库。十三、扩展通过 MCP Server 调用抓取能力除了 heredoc 方式Browser Harness 还提供了可选的 MCP serversrc/mcp_server.py把每个 helper 包装为 MCP 工具通过 stdio 暴露给客户端便于在 Agent 框架中直接调用。它覆盖了本指南用到的核心操作browser_new_tab对应new_tab、browser_goto对应goto_url、browser_page_info对应page_info、browser_click对应click_at_xy、browser_screenshot对应capture_screenshot等见 src/mcp_server.py。每个工具在调用前都会ensure_daemon()失败时通过 MCP 的 tool-error 通道返回错误。安装方式为pip install browser-harness[mcp]启动方式为uv run python -m mcp_serversrc/browser_harness/mcp_cli.py。不过本指南中的js()提取逻辑需要直接注入 JS 表达式在 MCP 工具集中尚无一一对应的封装因此大规模结构化提取仍推荐使用 heredoc 方式。小结Product Hunt 抓取的核心方法论可以概括为一句话——认准data-test前缀选择器按页面类型选择入口首页/日榜/周榜/月榜/主题/分类/搜索SPA 渲染用new_tab 足够等待票数三态与排名前缀按规则清洗。配合 Browser Harness 预置的js()、new_tab()、wait()等 helper这套方案无需登录、无需处理 Cookie 弹窗即可稳定批量获取产品结构化数据。【免费下载链接】browser-harnessBrowser Harness | Self-healing harness that enables LLMs to complete any task.项目地址: https://gitcode.com/gh_mirrors/br/browser-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表