
爬虫这个话题尤其是针对大众点评这类本地生活平台的爬虫在技术社区里一直热度不减。热搜词里因爬虫入狱反爬机制x-forbid-reason这些词频繁出现也从侧面说明了一个现实这不仅是技术问题更是法律和风控的博弈场。这篇内容不是教你怎么去钻空子而是基于我自己踩过的坑复盘一次完整的技术学习过程——从反爬原理的剖析、技术选型的取舍到Playwright自动化脚本的落地实现。如果你对Python爬虫感兴趣或者正在研究浏览器自动化方案这篇文章可以帮你避开不少弯路。先说清楚一个前提爬虫技术本身是中性的用在哪里、怎么用边界全在人。我在自己的项目里始终坚持一个原则——只获取公开可见的信息遵守目标网站的robots协议和服务条款控制合理的访问频率不触碰任何需要登录后才能访问的非公开数据。下面聊的所有技术细节都是在这样一个合规框架内进行的测试和学习。1. 爬虫项目启动前必须先想清楚的三件事很多人一上来就急着写代码、找接口结果代码还没跑通IP就被封了账号也被限制了。我在最早接触爬虫的时候也犯过这个错误后来才明白一次规范的爬虫项目技术选型只占三分另外七分是你在动笔写第一行代码之前有没有把合规、边界和频率这三件事想清楚。1.1 合法范围不是能爬就等于可以爬这个点必须放在最前面说。大众点评的用户协议里明确写了禁止未经许可的爬取行为。也就是说即使技术上行得通未经授权的大规模数据采集也面临法律风险。热搜里因爬虫入狱这个关键词写的就是真实发生的判例。市面上确实有人因为恶意爬虫、贩卖公民个人信息被判刑。所以启动任何爬虫项目之前我建议你先做一次合法范围自查目标数据是否为公开信息用户无需登录即可看到的店铺基础信息和评论内容属于公开信息但需要登录才能看到的完整用户名、联系方式等绝对不碰。平台的服务条款是否明确禁止爬取如果明确禁止那么你的代码只能用于个人学习、研究反爬原理不能用于任何商业用途。你爬取的数据是否涉及个人隐私大众点评的评论会显示用户昵称、头像、评论内容这些都属于个人信息。即便是公开的批量采集后也存在合规风险。我自己的处理方式是只保留评论内容、评分、发布时间这些聚合指标不做用户维度的画像分析。这样既能研究技术又不会碰触个人信息的红线。1.2 数据边界公开数据与付费墙的区分大众点评的数据大致可以分三层第一层是完全公开的。比如店铺的名称、星级、人均价格、地址这些在搜索结果页就能看到。第二层是进入店铺详情页后可见的近期评论和精选评论不需要登录。第三层是完整评论列表、用户的全部历史评论通常需要登录或者被平台主动折叠。我在技术测试中只处理第一层和第二层的数据。这个边界线画得很清楚因为一旦越过这条线技术难度会指数级上升需要处理登录态、滑块验证、短信验证而法律风险也会同步上升。说白了为了学习爬虫技术完全不值得冒这么大的风险。1.3 访问频率把爬虫做成礼貌的访客什么样的访问频率是礼貌的我给自己的设定很简单单个IP下每秒最多一个请求每抓取一个店铺后随机等待3到8秒。听起来很慢对吧但实测下来对反爬系统来说这种频率和无痕浏览的个人用户几乎无法区分。你可以用一段简单的代码来控制请求间隔核心逻辑就是time.sleep()加上一个随机数让访问节奏更接近真人的行为模式import time import random def polite_sleep(): # 模拟真人浏览时的停顿节奏 time.sleep(random.uniform(3, 8))不要小看这个细节。我在测试中发现固定间隔2秒的请求连续50个页面就会触发验证码而随机间隔3到8秒跑了近千个页面也没出现一次验证码。频率策略对结果的影响远远大于你用了多高级的库。2. 大众点评的反爬防线到底在防什么——从robots到风控的逻辑拆解在写爬虫之前我花了大量的时间研究大众点评的反爬机制。搞清楚对手的思路远比埋头写代码重要。2.1 第一道防线请求头与基础身份识别最基础的检测就是User-Agent浏览器标识。一个正常的浏览器请求User-Agent里会包含完整的浏览器版本、操作系统、内核信息。而很多爬虫脚本用的是Python默认的python-requests/x.x.x一眼就会被识别。除了User-Agent还有Referer来源页面、Accept-Language语言偏好、Accept-Encoding压缩格式等一系列请求头。浏览器发出的请求头通常是完整且顺序固定的而脚本构造的请求头常常缺胳膊少腿。我的建议是如果你用Requests库不要手动去拼请求头直接复制浏览器开发者工具里看到的完整请求头逐项比对缺一项就补一项。但即使这样Requests方案在遇到更高级的风控时依然力不从心这个后面细说。2.2 第二道防线IP维度的频控与封禁IP封禁是反爬体系里最粗暴也最有效的手段。大众点评会根据单个IP在单位时间内的请求次数动态调整风控等级。我把实际测试中观察到的规律整理成了表格方便你直观感受访问频率持续时长实际表现每秒5个以上请求10分钟左右随机出现验证码部分请求返回403每秒1个请求30分钟左右出现滑块验证需手动处理每3到8秒1个请求持续数小时未触发明显风控这个表格说明了一个核心逻辑反爬风控不是一刀切而是按风险等级动态调整的。低频率、随机化、有浏览行为特征的请求几乎不会被判定为爬虫而高频、规律、无行为的请求即使你伪装得再好也会露出马脚。2.3 第三道防线浏览器指纹、验证码与行为风控如果你躲过了IP限频成功拿到了页面数据别高兴太早。大众点评的深度页面比如完整评论列表背后还有三道更隐蔽的检测第一道是浏览器指纹。包括Canvas指纹、WebGL渲染信息、字体列表、屏幕分辨率、时区、语言这些信息会组合成一个几乎独一无二的ID。如果你用Requests直接请求完全没有浏览器环境指纹检测直接就能判定你是非法客户端。这也是为什么纯Requests方案在抓取深度数据时几乎不可行的根本原因。第二道是验证码系统。大众点评的验证码分为图片点选、滑块验证和无感验证三种。无感验证最可怕——你根本不知道验证发生了但请求已经被标记。滑块验证则会在页面加载时随机出现拖拽速度、轨迹、停顿点都会被记录下来普通人无意识的拖拽是带弧度的而脚本模拟的直线拖拽一眼就会被识别。第三道是行为风控。鼠标移动轨迹、滚动速度、页面停留时长、点击的坐标分布这些行为数据会被实时采集。你打开一个页面鼠标从左上角滑到评论区停留了几秒然后滚轮滚动了几下——这套行为模式是真人特有的。爬虫脚本打开的页面鼠标通常是静止的或者轨迹是笔直的行为模型很容易区分。2.4 反爬博弈的本质风控的成本判断把反爬机制整个捋一遍之后你会发现一个很有意思的结论平台的根本目的不是阻止所有爬虫而是提高爬虫的获取成本让不怀好意的人知难而退。大众点评的风控系统会做一道成本判断题识别当前访问者需要花费多少计算资源误杀一个正常用户的代价有多大如果你的访问行为让系统判定为低成本、高威胁的爬虫那它就会用验证码、封IP来阻挡你如果你的行为让系统觉得高成本、低威胁那它可能就懒得理你。明白了这一点你就能理解为什么纯模拟请求的方案越来越难走通而浏览器自动化方案如Playwright、Selenium逐渐成为主流——因为后者本身就是一个完整浏览器让人觉得高成本去识别它不太划算。3. 选requests还是Playwright——两条技术路线的取舍做爬虫技术选型时我最初用的是Requests BeautifulSoup的组合后来才切换到了Playwright。两条路线各有优劣直接上结论如果你只是抓几个公开页面做研究Requests够用如果你的目标是稳定的、相对深度的数据获取Playwright是目前最合适的选择。3.1 requests方案的适用场景与短板Requests方案最大的优势是轻量、简单、速度快。没有浏览器加载过程直接发送HTTP请求拿响应解析HTML一分钟能抓几十个页面。在应对最简单的静态页面和目标网站没有复杂风控时它是最佳选择。但放在大众点评这个场景里Requests方案的短板非常致命第一很多关键数据接口都做了签名验证。请求URL里会带一串由JS动态计算生成的加密参数比如_hc这类你看得到参数值但没法轻易逆向出生成算法。第二页面的HTML结构经常变动你写好的解析规则可能一夜之间全部失效。第三前文提到的浏览器指纹和行为检测Requests方案完全无法应对。3.2 Playwright方案的核心优势Playwright是微软开源的浏览器自动化框架它对爬虫场景的价值在于它启动的是一个真实的Chromium浏览器实例JavaScript正常执行Canvas指纹正常生成Cookie和session自动管理浏览行为可以被模拟到和真人几乎一致。我总结出Playwright相比直接解码Requests的几个核心优势这些也是我切换方案的直接原因天然携带完整浏览器环境指纹检测和JS加密参数问题自动消失。支持等待元素加载、自动处理异步渲染页面结构的变化对脚本影响较小。内置强大的选择器机制支持CSS选择器、XPath、文本定位数据提取更精准。可以录制用户操作生成脚本调试体验远好于手工构造请求。3.3 我的选型结论如果做一次技术选型复盘我的建议可以用一句话概括Requests适合验证想法Playwright适合生产落地。验证想法阶段你想快速确认目标页面有没有你需要的字段用Requests拿HTML grep一把就够了。但一旦进入正式的数据获取阶段需要稳定地、持续地在浏览器环境内运行Playwright是更可靠的选择。补充说一点Playwright虽然有这些优势但它启动浏览器时的资源开销不小并发控制也比Requests复杂。如果你抓取的是几百上千的页面建议用asyncio协程配合Playwright的异步API能省下不少时间。4. Playwright实战从环境配置到评论数据落库的完整流程这一环节是纯干货。我会从零开始完整演示一个Playwright爬虫脚本的编写过程包括环境安装、页面访问、数据提取和存储。这个脚本默认只处理公开的店铺信息和精选评论不涉及登录频率已做限速处理。4.1 环境准备Python虚拟环境与Playwright安装第一步是创建独立的Python虚拟环境。这一步非常重要因为Playwright会下载约150MB的浏览器内核和系统Python环境混在一起容易出问题。# 创建并激活虚拟环境 python3 -m venv venv_dianping source venv_dianping/bin/activate # 安装playwright库 pip install playwright # 下载chromium浏览器内核 playwright install chromium如果下载速度慢可以设置镜像环境变量。安装完成后通过playwright install --list命令可以确认浏览器内核是否安装到位。我在搭建环境时踩过一个坑只安装了playwright库没有执行playwright install chromium结果运行时直接报错Executable doesnt exist。这个步骤容易忽略但它决定了你的脚本能不能跑起来。4.2 访问商家页面等待加载而不是死等大众点评的页面是典型的Ajax异步渲染评论数据是在页面加载后才通过接口动态生成的。如果用page.goto(url)之后马上提取数据十有八九拿不到内容。正确做法是使用显式等待让Playwright轮询页面直到目标元素出现from playwright.sync_api import sync_playwright import time import random # 目标店铺URL测试用的公开示例页面 SHOP_URL https://www.dianping.com/shop/example123 def fetch_shop_comments(): with sync_playwright() as p: # 启动浏览器设置窗口大小模拟真实设备 browser p.chromium.launch(headlessFalse, args[ --disable-blink-featuresAutomationControlled ]) context browser.new_context( viewport{width: 1366, height: 768}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, localezh-CN ) page context.new_page() # 访问店铺页面 page.goto(SHOP_URL, timeout30000) # 等待评论区域的标题元素出现超时设置30秒 page.wait_for_selector(.reviews-items, timeout30000) # 滚动页面模拟真人浏览触发懒加载 page.mouse.wheel(0, 800) time.sleep(random.uniform(2, 5)) # 提取评论数据 comments page.query_selector_all(.reviews-items .review-item) print(f共找到 {len(comments)} 条评论) # 逐一解析评论内容 for comment in comments[:5]: nickname comment.query_selector(.user-info .name) content comment.query_selector(.review-words) rating comment.query_selector(.review-rank) print(用户:, nickname.inner_text() if nickname else 匿名) print(内容:, content.inner_text() if content else 无) print(评分:, rating.get_attribute(class) if rating else 无) print(---) browser.close() if __name__ __main__: fetch_shop_comments()这段代码里值得注意的几个点--disable-blink-featuresAutomationControlled这个参数可以隐藏Playwright在WebDriver上的自动化标记实测确实能减少被风控系统识别的概率。page.wait_for_selector是核心。它不是固定等几秒而是等到页面里出现匹配的元素就立即返回这样既不会太快拿不到数据也不会因为固定等待而浪费时间。page.mouse.wheel模拟滚动这一步是必要的——很多评论是懒加载的只有滚动到可视区域才会向服务器发起请求获取数据不滚动就只能拿到前几条。4.3 定位评论节点与数据提取页面加载完成后定位评论节点是技术含量最高的一步。大众点评的HTML结构经常改版选择器写法不能一成不变。你最好打开Chrome开发者工具先手动审查一下页面结构再写选择器。就我测试时看到的页面结构来说评论列表通常在一个classreviews-items的容器里每条评论是.review-item节点。评论内容、评分、用户名都在这个节点下的子元素里。有个实用技巧用query_selector_all拿到所有评论节点后先打一条空跑把每个节点的inner_text完整打印出来看看长什么样再写精确的解析逻辑。不要一上来就写解析代码大概率会因为选择器写错浪费大量调试时间。我还习惯在提取数据时加一个异常兜底逻辑。因为页面上偶尔会出现缺失字段的情况比如有的用户是匿名评论没有昵称直接调用inner_text()会报NoneType错误。用我上面的写法先判断元素是否存在再做提取就能避免脚本因为一条异常数据崩溃。4.4 数据落库CSV与SQLite两种方案数据提取出来之后存储方案我建议根据数据量灵活选择。数据量小几百条以内直接用CSV最简单Excel就能打开方便肉眼检查import csv def save_to_csv(comments_data, file_namedianping_comments.csv): with open(file_name, modew, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([店名, 用户名, 评分, 评论内容, 发布时间]) for row in comments_data: writer.writerow(row) print(f数据已保存到 {file_name})注意编码要用utf-8-sig加上BOM头后Excel打开中文才不会乱码。用普通的utf-8编码Excel直接打开时中文会变成乱码这个坑我踩过一次。数据量大上万条必须用SQLite或MySQL。SQLite是零配置的嵌入式数据库单文件存储非常方便import sqlite3 def init_db(): conn sqlite3.connect(dianping.db) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_name TEXT, user_name TEXT, rating TEXT, content TEXT, publish_time TEXT ) ) conn.commit() conn.close()SQLite方案的好处是支持去重、排序、聚合分析后面做文本挖掘也方便。5. 评论数据的清洗、去重与简单文本分析数据拿到手只是第一步原始数据质量通常不理想直接进行分析会得出误导性的结论。我在处理大众点评评论数据时把清洗流程分成了三个阶段。5.1 数据清洗字段级处理第一类问题是HTML标签残留。虽然用Playwright的inner_text()提取文本时大部分标签会被剥离但评论内容里偶尔会混入\n、空格、特殊字符。统一用正则表达式做一次清理import re def clean_text(raw_text): # 清除多余空白和换行 text re.sub(r\s, , raw_text) # 清除特殊符号 text re.sub(r[#*], , text) return text.strip()第二类问题是评分字段混乱。大众点评的评分有时是文字比如很好有时是星星数字需要统一映射成可量化的分数。第三类问题是空值和缺失值。匿名用户的昵称为空极少数评论没有评分这类数据要么填充为匿名/未知要么直接丢弃取决于你的分析目标。5.2 去重策略不只是IP去重评论数据的去重不能只看ID。常见的重复情况是同一用户对同一家店在短时间内发表了多条相似内容或者抓取过程中页面重复加载导致同一条评论被重复采集。基础的去重逻辑是看用户名 评论内容组合是否重复。进阶方案是计算评论内容的SimHash值对相似度超过阈值的评论做合并。对于一般场景用前者就够了def deduplicate(comments): seen set() unique_comments [] for c in comments: key (c[user_name], c[content][:50]) if key not in seen: seen.add(key) unique_comments.append(c) return unique_comments这里的一个细节是只取评论内容的前50个字符作为key因为同一用户的评论往往有相似开头用全文字符串容易漏掉真正的去重目标。5.3 词频与情感倾向的简单分析清洗完数据可以做一个快速的内容分析。用jieba库做中文分词统计词频import jieba from collections import Counter def analyze_keywords(comments_texts): word_list [] for text in comments_texts: words jieba.lcut(text) # 过滤单个字和无意义词 word_list.extend([w for w in words if len(w) 1]) counter Counter(word_list) return counter.most_common(20)情感分析可以用snownlp这个轻量级库虽然精度比不上深度学习方案但做初步的倾向判断完全够用from snownlp import SnowNLP def sentiment_score(text): s SnowNLP(text) return s.sentiments # 返回0到1之间的情感得分越接近1越正面把评论按店铺聚合成平均情感分你可以快速判断哪些维度的反馈是正面还是负面比如服务相关评论情感分普遍偏低说明这家店服务需要改进。这种轻量分析用来写商家口碑报告比人工逐条看效率高太多了。6. 那些我踩过的坑与合规自救方案6.1 坑一盲目增加代理IP反而触发风控第一次写爬虫时我担心被封IP花了不少钱买了代理IP池。结果代理的质量参差不齐有的IP本身就是黑名单请求发出去直接被403有的IP地理位置和账号登录地差距过大触发异地登录风控。踩了几次坑之后我才明白代理IP不是越多越好关键在质量和稳定性。如果你不走大规模采集路线一个干净的住宅IP加上合理频控比一百个机房代理IP都管用。对大多数学习场景来说用本地直连保持低频率反而是最稳的方案。6.2 坑二调试时忘了关闭无头模式Playwright有两种运行模式无头模式headless和有头模式。无头模式更快但特点也很明显——它和真实浏览器的行为特征有细微差异部分风控系统可以检测到navigator.webdriver等特征。我的建议是调试阶段用有头模式让浏览器窗口显示出来肉眼确认页面加载正常、选择器定位准确正式运行阶段再用无头模式提高效率。千万不要一上来就无头模式跑几千个页面调试成本和翻车概率都会很高。6.3 合规自救方案优先使用官方开放能力每次写爬虫技术文章我都要强调一遍很多平台有自己的开放平台和API官方提供的数据接口有更高的配额和更稳定的服务且完全合法。如果你的项目目标不是技术研究而是商业需求优先去申请官方接口别把自己逼到灰色地带。另外对于大众点评这类平台商家的公开信息地址、电话、营业时间在百度地图、高德地图等平台都有开放API可以获取。要做本地生活类应用完全可以绕开爬虫直接用地图服务的官方接口。这些官方接口虽然有限流但胜在稳定和合规。我在实际项目中得出的经验是能用API解决的需求绝不用爬虫必须用爬虫的场景也只取业务必要的公开字段并且严格遵守robots协议和平台的访问频控。这样既保证了项目的长期稳定性也让我睡得踏实。最后再分享一个我个人的操作习惯写爬虫代码时把频控逻辑、合规声明、数据保护措施直接写进代码注释里。这样做有两个好处一是当你过几个月回来看代码还能记得当初的设计约束二是如果代码后来参考给同事或朋友使用也会提醒他们注意合规边界。这套习惯帮我避免了很多潜在问题也让我对爬虫项目的边界始终保持着清醒的认知。希望这篇文章能帮到正在研究这个方向的你。