Yelp 汇聚了数亿条关于本地商家的众包评论,这些公开反馈是开放网络上衡量餐厅、商店或服务实际口碑最丰富的信号之一。分析师用它来追踪情感随时间的变化,运营者用它来发现反复出现的投诉,研究人员用它来对整个类别的商家进行横向比较。有用的部分就摆在每个商家页面上:星级评分、评论文本和发布日期。
本指南将向你展示如何使用 JavaScript 和 Node.js 结合 cheerio 爬取并抓取 Yelp 评论。你将构建一个可运行的小型爬虫,通过 Crawling API 获取公开的 Yelp 商家评论页面,解析每条评论的评分、文本和日期,处理评论分页,并将结果导出为 JSON 和 CSV。整个演示仅限于公开评论数据,专为聚合分析而设计,不用于分析个人评论者。在将此方案用于任何真实规模之前,请仔细阅读结尾处的法律说明。
你将构建什么
一个 Node.js 脚本,接受公开的 Yelp 商家 URL,通过 Crawling API 获取渲染后的 HTML,并为页面上的每条评论提取一条结构化记录。我们以一个餐厅页面为示例,每条评论提取以下字段:
- 评分 评论者给出的星级评分,例如"4 out of 5"。
- 文本 评论的公开正文,即你实际分析的内容。
- 日期 评论发布的日期,用于排序和趋势分析。
注意列表中没有的内容。我们刻意不收集或以评论者姓名、个人主页链接、照片或位置作为键。这些属于个人数据,而本教程的全部目的在于聚合分析:情感、反复出现的主题、随时间变化的评分分布。将评论视为一个评分、一段文本和一个日期,而不是关于某个人的档案。
为什么普通请求在 Yelp 上会失败
如果你用裸 HTTP 客户端请求 Yelp 商家 URL,很少能拿到评论。两个因素对你不利。首先,Yelp 在浏览器中用 JavaScript 渲染评论列表,所以初始 HTML 几乎是空壳,直到页面脚本运行为止。其次,Yelp 积极标记自动化流量:来自数据中心 IP 的、或请求模式不像真实浏览器的流量,会在到达渲染后的评论之前遭到 CAPTCHA 挑战、限速或封锁。
因此,一个可用的 Yelp 爬虫需要在单次请求中同时具备两点:能实际渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自行组合无头浏览器加上轮换住宅代理池,但将它们拼合在一起并保持健康运行,本身就是大部分工作量所在。Crawling API 将两者合并为一次调用:你发送 URL,它在可信 IP 后面渲染页面,并返回已渲染的 HTML 供你用 cheerio 解析。Yelp 是 JavaScript 渲染的,因此我们启用渲染来请求它。
下面的一切都有意让输出在批量分析时有用,而作为个人档案则毫无意义。我们提取评分、文本和日期,将评论者身份留在页面上。如果你的分析确实需要按评论者进行加权,请在采集时哈希或假名化标识符,使存储的记录永远不会将文本追溯到具名个人。
前置条件
在编写任何代码之前,需要准备好几样东西,都不需要太长时间。
基础的 JavaScript 和 Node.js 知识。 你应该能够编写和运行 Node 脚本,并用 npm 安装包。如果你是 Node 新手,可以先参考使用 Node.js 构建网络爬虫指南,那里涵盖了本教程所需的基础知识。
Node.js 16 或更高版本。 使用 node --version 确认版本。如果没有,请从 Node.js 官网或通过 nvm 等版本管理器安装。
Crawlbase 账号和 token。 注册后打开控制台,复制你的 JavaScript 请求 token。免费套餐最多提供 20,000 次请求且无需绑定信用卡。由于 Yelp 需要渲染,请使用 JavaScript token 而非普通 token。请像对待密码一样保管 token:它用于验证你的请求,因此不要放入版本控制系统。
搭建项目
创建项目文件夹,初始化,并安装爬虫所需的两个库。
node --version mkdir yelp-reviews && cd yelp-reviews npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 使用类 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。本教程的旧版本使用 request 和 cheerio;我们保留 cheerio 和相同的选择器方式,用官方客户端替换原始 HTTP 调用,使渲染和重试都得到处理。在此文件夹中新建一个名为 yelp-scraper.js 的文件,并按以下步骤添加代码。
第 1 步:获取渲染后的评论页面
从获取完整页面开始。导入 CrawlingAPI 类,用你的 JavaScript token 初始化,并请求商家 URL。Yelp 在客户端渲染评论,因此我们传入 pageWait,让脚本在 API 抓取 HTML 之前有片刻时间填充列表。在解析前检查状态码,可以让错误显而易见而不是悄无声息。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const yelpPageURL = 'https://www.yelp.com/biz/sushi-yasaka-new-york'; api .get(yelpPageURL, { pageWait: 3000 }) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
运行 node yelp-scraper.js,你应该能在 body 开头看到真实的 Yelp 标记,而不是精简的薄壳。这在你编写任何选择器之前就确认了渲染的有效性。示例 URL 指向一个公开的餐厅页面;换成任何其他公开商家页面,同样的流程都适用。
第一次请求就返回了一个完整渲染的 Yelp 页面,而你无需在自己这边运行无头浏览器或代理。Crawling API 在真实浏览器中运行页面,等待评论列表填充,在服务端轮换住宅 IP,并处理 Yelp 对爬虫的 CAPTCHA,让你从一次调用中就能获得已渲染的 HTML。先从免费套餐开始,将其指向单个公开商家页面。
第 2 步:用 cheerio 解析每条评论
拿到渲染后的 HTML 后,将其加载到 cheerio 并遍历评论块。Yelp 将每条评论布局在一个重复的容器中,因此你选择每条评论,然后从中读取评分、文本和日期。本教程旧版本的选择器 .review.review--with-sidebar 用于评论容器,.review-content p 用于正文;我们保留这些选择器,并增加评分和日期。防御性地读取每个字段,避免单个缺失值导致运行崩溃。
const cheerio = require('cheerio'); function parseReviews(html) { const $ = cheerio.load(html); const reviews = []; // Legacy container selector, kept faithfully const blocks = $('.review.review--with-sidebar'); blocks.each((index, element) => { const block = $(element); // Public review text const text = block.find('.review-content p').text().trim(); // Rating: read it from the star widget's aria/title text const ratingLabel = block.find('[role="img"][aria-label*="star"]').attr('aria-label') || block.find('.i-stars').attr('title') || ''; const ratingMatch = ratingLabel.match(/([\d.]+)\s*star/i); const rating = ratingMatch ? parseFloat(ratingMatch[1]) : null; // Date the review was posted const date = block .find('.review-content .rating-qualifier, span[class*="date"]') .first() .text() .trim(); if (text) { reviews.push({ rating, date, text }); } }); return reviews; }
几个细节保证了此函数对页面的忠实还原。评论正文来自 .review-content p,与原始教程完全一致。Yelp 上的评分不是纯文本;它存在于一个星级组件元素中,其 aria-label 或 title 读作类似"4 star rating"的内容,因此用一个小正则表达式提取数字。日期也位于同一个 review-content 块中。每条记录只包含 { rating, date, text },没有评论者姓名,这是有意为之。如果某条评论没有正文,我们跳过它而不是存储空行。
Yelp 的类名会不经通知地变动,上述确切的评分和日期选择器可能需要在实时页面上更新。将它们视为起始模板,而非固定契约。当某个字段返回为空时,在浏览器开发者工具中重新检查实时页面并更新选择器。如果你想复习构建健壮选择器的方法,可以参阅爬取 JavaScript 网站指南。定期维护选择器是任何生产级爬虫的正常操作。
第 3 步:处理评论分页
单个页面只显示第一批评论。Yelp 使用 start 查询参数分页显示其余评论,步长为若干条(通常每页 10 条),因此 ?start=10 是第二页,?start=20 是第三页,以此类推。要收集一个商家的所有评论,你需要遍历这些偏移量,直到某个页面没有返回任何评论块为止,每次都用你已经写好的函数获取和解析。
async function crawlPage(pageUrl) { const response = await api.get(pageUrl, { pageWait: 3000 }); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } async function crawlAllReviews(businessUrl, maxPages = 5) { const all = []; for (let page = 0; page < maxPages; page++) { const start = page * 10; const url = `${businessUrl}?start=${start}`; const html = await crawlPage(url); if (!html) break; const pageReviews = parseReviews(html); if (pageReviews.length === 0) break; // no more reviews all.push(...pageReviews); // Pace requests so we stay a polite visitor await new Promise((r) => setTimeout(r, 2000)); } return all; }
循环在第一个空页面、非 200 响应或达到 maxPages 时停止,因此永远不会无限循环。两秒的暂停保持了合理的请求速率,这在 Yelp 这样严苛的目标上比原始速度更重要。在开发过程中保持 maxPages 较低,只有在解析输出看起来正确之后才提高它。
第 4 步:组装完整脚本并导出 JSON 和 CSV
现在将获取、解析和分页整合为一个可运行的脚本,然后将聚合记录以 JSON 和 CSV 格式写入磁盘。CSV 只包含评分、日期和文本这三个驱动情感和主题分析的字段。
const fs = require('fs'); const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); function toCsv(rows) { const headers = ['rating', 'date', 'text']; 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 businessUrl = 'https://www.yelp.com/biz/sushi-yasaka-new-york'; const reviews = await crawlAllReviews(businessUrl, 5); if (reviews.length === 0) { console.log('No reviews parsed; check your selectors.'); return; } fs.writeFileSync('reviews.json', JSON.stringify(reviews, null, 2)); fs.writeFileSync('reviews.csv', toCsv(reviews)); console.log(`Saved ${reviews.length} reviews to JSON and CSV`); } main();
将前面步骤中的 parseReviews、crawlPage 和 crawlAllReviews 函数粘贴到同一个文件中,以便 main 可以调用它们。运行 node yelp-scraper.js,你将得到两个文件:reviews.json 包含完整的结构化记录,reviews.csv 可以直接在电子表格中打开或输入到分析管道。toCsv 辅助函数对每个字段加引号并将嵌入的引号加倍,这在评论文本较长且频繁包含逗号和换行符时尤为重要。
输出结果示例
JSON 文件每条评论包含一个对象,包括评分、日期和文本。没有评论者身份信息,这正是聚合工作所需要的。
[ { "rating": 5, "date": "3/14/2024", "text": "Consistently fresh fish and quick service at lunch. The omakase set is great value for the quality." }, { "rating": 3, "date": "2/2/2024", "text": "Good food but the wait was long on a weekend. Worth it if you can get there early." } ]
CSV 以带表头行的格式呈现相同的数据,可以直接在 Excel、Google Sheets 或任何读取分隔符文件的数据管道中打开。
rating,date,text "5","3/14/2024","Consistently fresh fish and quick service at lunch. The omakase set is great value for the quality." "3","2/2/2024","Good food but the wait was long on a weekend. Worth it if you can get there early."
将评论转化为聚合洞察
收集评论的价值在于批量分析,而不是单条记录。一旦数据存入 JSON 或 CSV,你就可以计算评分分布,使用日期字段按月追踪平均评分,并对评论文本进行情感评分或主题聚类,以发现反复出现的话题,如等待时间、价格或服务质量。这些分析都不需要评论者姓名,将姓名排除在管道之外既更简洁,也更安全。
如果你计划将文本输入模型,请先对其进行规范化:去除样板内容,折叠空白字符,并决定如何处理非英语评论。为 AI 和 ML 整理和清洗抓取数据指南详细介绍了这一步骤。关于从多个来源拉取和比较反馈的更广泛模式,抓取客户评论指南针对其他评论平台也采用了相同的聚合优先方法。
保持不被封锁
即使渲染问题已处理好,Yelp 仍会监控爬虫形态的流量。以下几个习惯可保持抓取任务健康运行,适用于任何难度较高的商业目标。
- 控制请求速率。 如脚本所示,保持分页获取之间的延迟。分散请求是保持在 Yelp 速率限制以内最关键的单一因素。
- 依赖 IP 轮换。 住宅 IP 池将请求分散到多个真实用户地址,使单个地址不会触发限制或 CAPTCHA。Crawling API 为你处理了这一点;如果你自行搭建,这是最需要做好的环节。
- 关注状态码。 如果运行开始返回挑战或非 200 响应,说明当前速率或 IP 套餐已不够用。将此视为退出的信号,而非可忽略的噪音。
完整的操作手册,请参阅如何在不被封锁的情况下抓取网站。
抓取 Yelp 评论合法吗?
抓取 Yelp 是否被允许,取决于 Yelp 的服务条款、你所在的司法管辖区,以及你对数据的使用方式。Yelp 的服务条款明确限制从网站复制或抓取内容,无论是手动还是通过机器人、扩展程序或软件,因此无论你的工具多么谨慎,自动化采集都可能违反这些条款。本文中的任何代码都不会改变这一点;它们只是让技术层面的工作变得可行。请阅读 Yelp 的服务条款和 robots.txt,并将两者视为你采集内容的边界。对于任何真实规模或商业用途,最清晰的路径是官方的 Yelp Fusion API,它在明确的条款下提供商家和评论数据。
商家页面上显示的评论文本和星级评分是公开的,但撰写它们的人在隐私法律层面并非匿名。评论者姓名、个人主页链接、照片和位置属于个人数据。根据 GDPR 和 CCPA,处理这些数据需要合法依据,个人对其数据保有权利。这正是为什么本教程刻意只收集评分、文本和日期,也是为什么我们建议你不要存储、以此为键或将某人的评论与其身份绑定后再发布。保持分析的聚合性:情感、主题和评分趋势,而非个人评论者的档案。
几条值得坚守的原则。只收集公开的评论内容,以慢速和尊重的频率抓取,将请求量保持在不给 Yelp 服务器造成压力的水平。远离任何需要登录的内容。不要将 Yelp 的受版权保护内容(包括评论媒体)作为自己的内容再发布。如果你在 GDPR 或 CCPA 司法管辖区运营或收集该辖区内人员的数据,且你的用途涉及个人数据,请在继续之前寻求法律建议。如有疑问,Yelp Fusion API 或数据协议是正确的路径,而非更聪明的爬虫。
核心要点
- Yelp 在客户端渲染评论并严格封锁爬虫。 普通请求返回空壳或 CAPTCHA,因此必须先在可信 IP 后面渲染页面再进行解析。
- Crawling API 一次调用完成两件事。 它使用 JavaScript token 渲染页面,等待评论列表加载,轮换住宅 IP,并处理 CAPTCHA,返回你用 cheerio 解析的已渲染 HTML。
-
只解析评分、文本和日期。 保留旧版
.review.review--with-sidebar和.review-content p选择器,增加星级评分和日期,并遍历start偏移量实现分页。 - 保持聚合性和隐私安全。 将评论者姓名排除在记录之外;分析情感、主题和评分趋势,永远不要建立个人档案或将评论与具名人员绑定后再发布。
- 大规模使用时优先选择官方 API。 遵守 Yelp 的服务条款和 robots.txt,在涉及个人数据时注意 GDPR 和 CCPA,商业或大规模用途请使用 Yelp Fusion API 或数据协议。
常见问题
抓取 Yelp 评论合法吗?
这取决于 Yelp 的服务条款、你所在的司法管辖区和你的用途。Yelp 的条款限制对其内容的抓取,因此自动化采集可能违反条款。评论文本和评分是公开的,但评论者身份属于 GDPR 和 CCPA 等法律所覆盖的个人数据。只收集公开评论内容,保持聚合性,慢速抓取,对于任何真实规模或商业用途请优先使用官方 Yelp Fusion API。
为什么普通请求从 Yelp 返回的数据不完整?
因为 Yelp 使用 JavaScript 在客户端渲染评论列表,并使用 CAPTCHA 挑战自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回空壳或封锁页面,而不是评论。要获得完整的页面,必须在可信 IP 后面渲染它,并给脚本时间填充列表,这正是 Crawling API 为你处理的事情。
如何获取所有评论而不仅仅是第一页?
Yelp 使用 start 查询参数分页评论,步长约为十条,因此 ?start=10 是第二页,?start=20 是第三页,以此类推。在这些偏移量上循环,获取并解析每个页面,当某页没有返回评论块或达到页面上限时停止。本指南中的 crawlAllReviews 函数正是这样做的,并在获取之间加入了暂停。
我应该存储评论者姓名吗?
不应该。评论者姓名、个人主页链接和照片属于个人数据,本教程专为聚合分析而设计,而非个人分析。只存储评分、文本和日期。如果你的分析确实需要区分评论者,请在采集时将标识符假名化或哈希,使存储的记录永远不会将文本追溯到具名人员,并遵守所有适用的 GDPR 或 CCPA 义务。
我的评分或日期返回为空。是什么改变了?
几乎可以肯定是 Yelp 修改了标记语言。它的类名和星级组件的结构会不经通知地改变,之前有效的选择器可能就此失效。在浏览器开发者工具中重新检查实时评论块,确认评分 aria-label 或 title 以及日期现在的位置,并更新选择器。定期维护选择器是任何生产级爬虫的正常操作。
有没有获取 Yelp 评论数据的官方途径?
有。Yelp Fusion API 是 Yelp 的官方程序,在明确的条款下提供商家信息和有限的评论数据访问。对于商业项目、大规模需求,或需要有保证结构和使用权的场景,Fusion API 或直接数据协议才是正确的工具,而非爬虫。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
