简而言之:本指南将带您逐步构建一个完整的 Python 版 Yelp 爬虫,涵盖搜索结果、商家详情和评论,并附有可运行的代码。您还将学习如何应对反机器人防护措施、将数据导出为 CSV 或 JSON 格式,以及将爬取的评论输入到大型语言模型(LLM)中进行情感分析——这是其他任何 Yelp 爬虫教程都没有涉及的内容。
简介
Yelp 拥有网络上最丰富的本地商家数据集合之一:评分、评论、营业时间、分类、照片等,所有这些都与数百个城市中的数百万家商家相关联。如果您需要了解如何通过编程方式抓取 Yelp 数据,Python 是完成这项任务最实用的工具。
Yelp 网页抓取是指通过 HTTP 请求和 HTML 解析,从 Yelp 的公开页面(通常包括搜索结果、单个商家列表和用户评论)中提取结构化数据,而非手动复制粘贴。无论您是构建竞争情报仪表盘、监测评论情绪,还是从本地目录中获取潜在客户,其底层工作流都是一致的:获取页面、解析 HTML 并存储结果。
本教程将为您提供一个完整的端到端项目。您将从搜索结果抓取开始,继而进行商家详情提取,随后处理带分页功能的评论收集。在此基础上,我们将涵盖反机器人策略、异步扩展、数据导出,以及将 Yelp 数据导入大型语言模型(LLM)进行自动摘要的独特工作流。每个代码片段均可运行,每个章节在讲解“如何做”的同时,也会阐明“为什么”这样做。
为何要抓取 Yelp?值得开发的商业应用场景
在深入代码之前,有必要先了解 Yelp 数据为何如此有价值。Yelp 不仅仅是一个点评网站;它是一个结构化的目录,包含其他地方难以获取的精细化信号。以下这些应用场景,正是让 Yelp 数据抓取值得投入工程资源的原因。
竞争情报与行业对标。若您经营餐厅、美发沙龙或任何本地服务类企业,Yelp 评论能精准揭示客户对竞争对手的喜爱(与厌恶)之处。通过抓取特定类别的星级评分、评论数量及回复率,您可以将自身业务与当地市场环境进行横向对比。
评论监控与情感分析。追踪评论随时间的变化可揭示趋势:本季度顾客对等候时间的抱怨是否增多?抓取的 Yelp 评论可直接输入情感分析管道,将定性反馈转化为定量信号。
潜在客户开发。Yelp 商家信息包含企业名称、电话号码、地址和分类。对于以本地企业为目标的 B2B 销售团队(例如 POS 系统供应商或营销机构),Yelp 数据抓取工具就是一台潜在客户生成引擎。
本地 SEO 审计。将您的 Yelp 商家信息完整性(照片、营业时间、分类、回复率)与排名靠前的竞争对手进行对比,可揭示您在本地市场中的不足之处。
市场调研与选址。计划开设新分店?通过抓取 Yelp 数据,按社区绘制竞争对手密度、平均评分及评论数量分布图。这些数据可直接用于选址模型。
关键在于,学习如何抓取 Yelp 数据绝非纸上谈兵。这些数据能为实际商业决策提供有力支撑。
先决条件与项目设置
您需要 Python 3.9 或更高版本。创建一个新的项目目录并安装核心依赖项:
pip install requests beautifulsoup4 lxml
以下是各包的功能说明:
- requests:处理对 Yelp 页面的 HTTP 请求
- beautifulsoup4:将返回的 HTML 解析为可导航的树结构
- lxml:BeautifulSoup 作为后端使用的快速 HTML/XML 解析器
在后续章节中,您还需要:
pip install httpx openai
httpx 提供异步 HTTP 支持,用于并发抓取,以及 openai (或任何 LLM 客户端)将驱动我们在最后构建的数据到洞察的管道。
创建一个 scraper.py 文件并添加标准导入:
import requests
from bs4 import BeautifulSoup
import csv
import json
import time
import random
这就是你的基础。下文的每个章节都将基于此配置展开。
抓取 Yelp 搜索结果
任何 Yelp 抓取项目的第一步都是从搜索结果页面收集商家信息。当你在 Yelp 上搜索“纽约州纽约市”的“披萨”时,URL 遵循可预测的模式:
https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY&start=0
start 参数控制分页,每页增加 10。让我们构建一个能够收集多页列表的爬虫。
def scrape_search_results(query, location, max_pages=5):
results = []
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
for page in range(max_pages):
offset = page * 10
url = (
f"https://www.yelp.com/search?"
f"find_desc={query}&find_loc={location}&start={offset}"
)
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Blocked or error on page {page}: {response.status_code}")
break
soup = BeautifulSoup(response.text, "lxml")
cards = soup.select('[data-testid="serp-ia-card"]')
if not cards:
break
for card in cards:
name_tag = card.select_one("a.css-19v1rkv")
rating_tag = card.select_one('[aria-label*="star rating"]')
review_count_tag = card.select_one("span.css-chan6m")
results.append({
"name": name_tag.get_text(strip=True) if name_tag else None,
"url": "https://www.yelp.com" + name_tag["href"] if name_tag else None,
"rating": rating_tag["aria-label"] if rating_tag else None,
"review_count": review_count_tag.get_text(strip=True) if review_count_tag else None,
})
time.sleep(random.uniform(2, 5))
return results
关于选择器策略有几点需要注意。Yelp 的类名是动态生成的(那些 css-* 字符串),因此不同部署版本之间可能会发生变化。 data-testid 属性通常更为稳定,因为它们用于内部测试。在进行大规模运行前,请务必在实时页面上验证您的选择器。
Yelp 还提供了一个搜索片段接口,该接口可能直接返回 JSON,从而完全跳过 HTML 解析。但是,该接口的可用性和结构可能会在未经通知的情况下发生变化,因此上述 HTML 方法是可靠的基础方案。
分页循环在每次迭代中 start 每次迭代增加 10,并在找不到更多列表卡片时停止。请求之间的随机延迟对于避免速率限制至关重要,我们稍后将详细讨论这一点。
从 Yelp 列表页面提取商家详情
搜索结果仅提供名称和评分,但单个商家页面包含真正有价值的数据:完整地址、电话号码、营业时间、分类、价格范围等。以下是从 Yelp 列表页面提取商家数据的方法。
def scrape_business_details(business_url, headers):
response = requests.get(business_url, headers=headers)
if response.status_code != 200:
return None
soup = BeautifulSoup(response.text, "lxml")
def safe_text(selector):
tag = soup.select_one(selector)
return tag.get_text(strip=True) if tag else None
# Extract business_id from meta or script tags for API use
meta_biz = soup.select_one('meta[name="yelp-biz-id"]')
business_id = meta_biz["content"] if meta_biz else None
details = {
"business_id": business_id,
"name": safe_text("h1"),
"rating": None,
"phone": safe_text('[data-testid="phone-info"] p'),
"address": safe_text("address"),
"categories": [],
"hours": {},
}
# Star rating from aria-label
rating_el = soup.select_one('[aria-label*="star rating"]')
if rating_el:
details["rating"] = rating_el["aria-label"]
# Categories
cat_links = soup.select('span.css-1xfc281 a')
details["categories"] = [a.get_text(strip=True) for a in cat_links]
# Hours table
hours_rows = soup.select("table.hours-table tr")
for row in hours_rows:
cols = row.select("td, th")
if len(cols) >= 2:
day = cols[0].get_text(strip=True)
time_range = cols[1].get_text(strip=True)
details["hours"][day] = time_range
return details
从 business_id 从 meta 标签中提取的 ID 特别有用。Yelp 内部使用该 ID,它可作为去重键,或用于构建访问评论端点的 URL。当大规模抓取 Yelp 商家数据时,为每个商家拥有一个稳定的标识符对于保持数据集的纯净至关重要。
请注意,Yelp 的 HTML 结构会因商家类别而略有不同。例如,餐厅页面包含菜单板块,而水管工页面则没有。您的解析代码应能优雅地处理缺失的元素(这就是 safe_text 辅助函数所做的)。在尝试访问属性或文本之前,请先检查每个选择器的返回值。
大规模抓取 Yelp 评论
评论通常是 Yelp 数据提取中最有价值的部分。每条评论都包含评论者的姓名、星级评分、日期以及完整文本,这正是您进行情感分析或竞争对手监测所需的。
Yelp 使用 start 查询参数分页显示评论,通常每页显示 10 条。以下是一个遍历评论页面的爬虫:
def scrape_reviews(business_url, max_pages=10, headers=None):
reviews = []
for page in range(max_pages):
offset = page * 10
url = f"{business_url}?start={offset}&sort_by=date_desc"
response = requests.get(url, headers=headers or {})
if response.status_code != 200:
print(f"Review page {page} returned {response.status_code}")
break
soup = BeautifulSoup(response.text, "lxml")
review_containers = soup.select('[data-testid="review"]')
if not review_containers:
break
for container in review_containers:
user_tag = container.select_one("a.css-19v1rkv")
rating_tag = container.select_one('[aria-label*="star rating"]')
date_tag = container.select_one("span.css-chan6m")
text_tag = container.select_one("p.comment__09f24__D0cxf span")
reviews.append({
"user": user_tag.get_text(strip=True) if user_tag else None,
"rating": rating_tag["aria-label"] if rating_tag else None,
"date": date_tag.get_text(strip=True) if date_tag else None,
"text": text_tag.get_text(strip=True) if text_tag else None,
})
time.sleep(random.uniform(2, 5))
return reviews
处理动态评论加载。Yelp 有时会在页面初始渲染后通过 JavaScript 加载评论。如果您的 requests基于的爬虫返回的评论数量少于浏览器中显示的数量,则该页面很可能是在客户端动态加载评论内容。这种情况下,你有两种选择:使用无头浏览器(如 Playwright 或 Puppeteer)来渲染 JavaScript,或者查找页面发出的底层 API 调用。
有迹象表明,Yelp 内部使用 GraphQL 端点以结构化 JSON 格式获取评论数据。如果可用,这将使您完全绕过 HTML 解析,直接获取干净、结构化的评论数据。但是,应根据实时网站验证确切的端点 URL 和有效负载结构,因为内部 API 可能会随时更改且不另行通知。上述 HTML 抓取方法仍然是提取 Yelp 评论最可靠的方法。
评论排序。URL 中的 sort_by=date_desc 参数可确保您优先获取最新评论。其他选项包括 rating_desc 和 rating_asc。对于监控场景,按日期排序的抓取可通过与上次抓取的时间戳对比来检测新评论。
应对反机器人防护与速率限制
Yelp 非常重视数据保护。若从单一 IP 地址发起数百次快速请求,很可能会被封禁。以下是一套分层策略,可确保可靠地抓取 Yelp 数据。
轮换 User-Agent 字符串。每次请求都发送相同的 User-Agent 头部会暴露指纹。维护一份真实的浏览器 User-Agent 字符串列表,并随机轮换使用。您可以从 user-agents.net 等资源获取最新的 UA 字符串。
import random
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36",
]
def get_headers():
return {
"User-Agent": random.choice(USER_AGENTS),
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml",
}
添加合理的延迟。2 到 5 秒之间的随机延迟可以模拟人类的浏览模式。对于大规模抓取,建议在商家详情页之间设置更长的回退时间(10 到 30 秒),因为这些请求负载较重,在服务器日志中会显得格外突出。
代理轮换。当抓取 Yelp 超过几十个页面时,轮换 IP 至关重要。您可以搭建自己的代理池或使用代理轮换服务。关键在于将请求分散到多个 IP 上,以免单个地址触发速率限制。
def make_request(url, proxies_list):
proxy = random.choice(proxies_list)
proxy_dict = {"http": proxy, "https": proxy}
headers = get_headers()
try:
response = requests.get(url, headers=headers, proxies=proxy_dict, timeout=15)
return response
except requests.RequestException as e:
print(f"Request failed via {proxy}: {e}")
return None
采用指数退避的重试逻辑。当收到 429(速率限制)或 403(被封锁)响应时,不要立即重试。请等待一段时间,然后延迟更久再尝试:
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=get_headers(), timeout=15)
if response.status_code == 200:
return response
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Retrying in {wait:.1f}s (status {response.status_code})")
time.sleep(wait)
return None
遵守 robots.txt 规则。截至本文撰写时,Yelp 的 robots.txt 限制了对某些路径的爬取,并指定了爬取延迟偏好。在启动爬虫之前,务必检查当前的指令。无视 robots.txt 不仅会招致封禁风险,还会引发道德乃至潜在的法律问题。负责任的抓取意味着必须遵守网站已公布的规则。
利用异步请求加速爬虫
上述同步爬虫适用于小型任务,但若需收集数千家 Yelp 商户的数据,逐个等待 HTTP 响应将导致等待时间迅速累积。异步 HTTP 允许您并发发送多个请求,从而大幅缩短总爬取时间。
以下是一个使用 httpx:
import httpx
import asyncio
async def fetch_page(client, url):
try:
response = await client.get(url, timeout=15)
return response.text if response.status_code == 200 else None
except httpx.RequestError:
return None
async def scrape_urls_async(urls, concurrency=5):
semaphore = asyncio.Semaphore(concurrency)
results = []
async def bounded_fetch(client, url):
async with semaphore:
html = await fetch_page(client, url)
results.append((url, html))
await asyncio.sleep(random.uniform(1, 3))
async with httpx.AsyncClient(headers=get_headers()) as client:
tasks = [bounded_fetch(client, url) for url in urls]
await asyncio.gather(*tasks)
return results
The Semaphore 使用 concurrency=5 将并发请求数限制在五个以内。这一点至关重要:若向 Yelp 发起 50 个并发连接,极易导致 IP 池中的所有 IP 地址被封禁。建议从 3 到 5 个并发请求开始,并在监控成功率的同时谨慎增加。
每个任务在完成后还会包含一个随机延迟。这可以防止出现“雷霆之群”模式——即所有五个槽位同时释放,并立即一次性发起五个新请求。
当你已经拥有待访问的 URL 列表时(例如,从搜索结果阶段收集的商家页面 URL),异步抓取便大显身手。你可以使用之前相同的 BeautifulSoup 代码解析 HTML 响应,因为解析逻辑不会因数据获取是异步的而改变。
存储和导出 Yelp 数据
只有将结果存储为下游工具可用的格式,爬取才有意义。以下是爬取的 Yelp 数据的三个常见导出路径。
CSV 导出是最简单的选项,几乎适用于任何分析工具:
def export_to_csv(data, filename="yelp_data.csv"):
if not data:
return
keys = data[0].keys()
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(data)
print(f"Exported {len(data)} records to {filename}")
JSON 导出能保留嵌套结构(如营业时间或评论列表),而这些结构在 CSV 中会被笨拙地展平:
def export_to_json(data, filename="yelp_data.json"):
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Exported {len(data)} records to {filename}")
若您希望获得可查询的存储空间,又不想搭建数据库服务器,SQLite 是一个不错的中间方案:
import sqlite3
def export_to_sqlite(data, db_name="yelp.db", table="businesses"):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
if data:
cols = ", ".join(f"{k} TEXT" for k in data[0].keys())
cursor.execute(f"CREATE TABLE IF NOT EXISTS {table} ({cols})")
placeholders = ", ".join("?" for _ in data[0])
for row in data:
cursor.execute(
f"INSERT INTO {table} VALUES ({placeholders})",
list(row.values())
)
conn.commit()
conn.close()
对于大多数 Yelp 抓取项目,建议先使用 JSON(它能自然处理嵌套的评论数据),然后在需要将数据加载到电子表格或 Pandas DataFrame 时再转换为 CSV。若需反复执行抓取操作,且希望在不将所有数据加载到内存的情况下查询历史数据,则使用 SQLite 更为合理。
将 Yelp 数据转化为适用于 LLM 的洞察
本教程在此处与其他所有 Yelp 爬取指南有所不同。收集评论后,您可以将其输入大型语言模型,从而提取那些手动整理需要数小时才能完成的洞察。
该工作流包含三个步骤:清理数据、将其格式化为结构化提示词,以及调用 LLM。
步骤 1:将评论转换为 Markdown 摘要块。当输入数据结构清晰时,LLM 的表现最佳:
def reviews_to_markdown(reviews, business_name):
lines = [f"# Reviews for {business_name}\n"]
for r in reviews:
lines.append(f"- **{r['rating']}** ({r['date']}): {r['text']}\n")
return "\n".join(lines)
步骤 2:构建能获取特定输出的提示词。不要只说“总结这些评论”,而要明确说明你的需求:
def build_analysis_prompt(markdown_reviews):
return (
"Analyze the following Yelp reviews and provide:\n"
"1. A 2-sentence overall summary\n"
"2. Top 3 positive themes with example quotes\n"
"3. Top 3 negative themes with example quotes\n"
"4. An estimated sentiment score (1-10)\n\n"
f"{markdown_reviews}"
)
步骤 3:将提示发送至您选择的 LLM。以下是一个使用 OpenAI 客户端的简易示例,但任何 LLM API(或本地模型)均可适用:
from openai import OpenAI
def analyze_reviews(reviews, business_name):
client = OpenAI()
md = reviews_to_markdown(reviews, business_name)
prompt = build_analysis_prompt(md)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
return response.choices[0].message.content
该流程将原始的 Yelp 评论转化为结构化的竞争情报。您可以针对某个类别中的所有竞争对手运行该流程,自动生成一份映射情感格局的报告。在潜在客户开发方面,您可以将情感评分下降的企业标记为服务的潜在客户。
关键洞见在于:当在抓取的 Yelp 数据之上叠加 LLM 摘要层时,其价值将大幅提升。数据抓取提供原始素材;LLM 则将其转化为决策依据。
最佳实践与道德抓取指南
负责任地抓取 Yelp(或任何网站)不仅在于避免被封禁,更在于以可持续且经得起推敲的方式进行操作。
严格控制请求频率。即便您每秒能发送 100 次请求,也不意味着您应该这样做。激进的抓取会损害真实用户的体验。请确保每次请求之间至少间隔 2 到 5 秒,并在高峰时段降低并发请求量。
缓存响应。如果您正在迭代优化解析器,请将原始 HTML 缓存到本地,以免在调试选择器时反复访问 Yelp 的服务器。一个简单的基于文件的缓存(将每个页面保存为 {business_id}.html)即可大幅减少请求次数。
负责任地处理数据。评论中包含个人信息(评论者姓名,有时还包括位置)。若您存储此类数据,请实施适当的访问控制和数据保留政策。若您位于欧盟或处理欧盟用户数据,则需遵守《通用数据保护条例》(GDPR)。
请勿重新发布抓取的内容。将 Yelp 数据用于内部分析与在您自己的网站上重新发布评论有本质区别。前者通常是合理的;后者则会引发法律和道德问题。
关键要点
- 从搜索结果入手,逐步扩展。一个可运行的 Yelp 抓取工具需经历三个阶段:搜索列表、商家详情,最后是评论。在进入下一阶段前,请先构建并验证当前阶段。
- 选择器的稳定性至关重要。Yelp的
data-testid属性比生成的 CSS 类名更可靠。务必对照实时页面验证选择器,并构建优雅的备用方案。 - 速率限制是您最重要的防封措施。随机延迟、代理轮换和 User-Agent 随机化虽可协同作用,但控制请求频率才是最有效的单一策略。
- 导出格式取决于您的下游工作流。使用 JSON 处理嵌套的评论数据,使用 CSV 进行电子表格分析,使用 SQLite 进行可查询历史记录的重复抓取。
- LLM 管道能将原始评论转化为可操作的情报。将结构化的评论数据输入 LLM 进行情感分析和主题提取,其效果是任何人工阅读都无法比拟的。
常见问题
从 Yelp 抓取数据是否合法?
合法性取决于您的管辖区域、数据获取方式以及数据用途。法院通常会区分抓取公开数据与规避访问控制的行为。Yelp 的服务条款禁止自动化访问,但条款的可执行性因管辖区域而异。请针对您的具体使用场景咨询法律专业人士,并务必避免在登录墙后抓取数据或绕过技术限制。
Yelp 是否有公开 API 可替代数据抓取?
Yelp 提供了 Yelp Fusion API,该接口支持结构化访问商家搜索、商家详情及评论数据。但该 API 存在显著限制:每家商家的评论数据仅限提取三段摘要,请求速率限制相对严格,且网站上部分字段无法通过 API 获取。若需全面收集评论或满足大规模数据需求,爬取通常是更切实可行的替代方案。
如何避免在抓取 Yelp 时被封禁 IP?
使用代理池轮换 IP 地址,随机化 User-Agent 头部,并在请求之间添加合理的延迟(2 到 5 秒)。在收到 429 或 403 响应时实施指数退避。保持低并发(3 到 5 个并发请求)。监控成功率,若低于 90% 则减少请求频率。相比数据中心代理,住宅代理更难被网站检测到。
不使用无头浏览器能否抓取 Yelp 评论?
可以,对于大多数商家而言。Yelp会在服务器端 HTML 中渲染首批评论,您可以使用 requests 和 BeautifulSoup 进行解析。分页功能通过 start 查询参数实现。仅当 Yelp 通过 JavaScript 动态加载特定页面的评论时才需要无头浏览器,而标准评论分页通常不采用这种方式。
抓取 Yelp 评论的最佳 Python 库是什么?
对于大多数项目,结合使用 requests (HTTP 抓取)与 BeautifulSoup 配合 lxml (HTML解析)的组合是最佳的起点。若您需要为大规模数据采集提供异步支持, httpx 是 requests。对于需要 JavaScript 渲染的页面,Playwright 或 Selenium 是首选方案,尽管它们的速度明显较慢。
结论
现在,您已拥有了一套完整的 Python Yelp 爬取工具包。从收集搜索结果、解析商家详情到大规模提取评论,整个流程的每个环节都配有可运行的代码。异步模式让您能在项目需要时进行扩展,而 LLM 集成则能将原始评论文本转化为可供行动的结构化洞察。
任何 Yelp 爬取项目中最大的挑战并非解析,而是维持可靠的访问。在 IP 封禁、验证码挑战和不断变化的 HTML 结构之间,请求层消耗的工程时间甚至超过了数据提取逻辑。 如果您更希望专注于数据处理而非与反机器人系统周旋,我们的 Scraper API 可在单一接口后自动处理代理轮换、验证码和重试,因此您的 BeautifulSoup 代码完全无需修改。
无论您选择哪种方法,都应从小处着手,在真实页面上验证您的选择器,并逐步扩展。我们探讨过的用例(情感分析、竞品对比、潜在客户开发)都建立在同一个基础上:长期可靠收集到的、干净且结构化的 Yelp 数据。




