ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大众点评评论数据爬虫实战:从合规边界到工程化稳定抓取

大众点评评论数据爬虫实战:从合规边界到工程化稳定抓取 如果你是因为“5分钟搞定大众点评评论数据抓取”这个标题点进来的那我得先说句实话真正从零开始写代码、跑通请求、解析评论、落库整套流程5分钟确实不够至少我第一次折腾的时候光是和验证码斗智斗勇就耗掉一个晚上。但这个标题也不是完全标题党。它真正想表达的是当你把爬虫工程拆成“请求、解析、存储、稳定性”这四个模块之后每一块的代码量其实都很小。多数人觉得难是因为把四件事揉在一个文件里出了问题不知道从哪查起。这篇文章我就把这四件事拆开讲从合规边界讲到实际代码再讲我踩过的坑和让爬虫持续稳定跑的工程细节。不管你是刚学Python、想拿真实网站练手还是工作中确实有合规的数据采集需求这篇文章都能给你一套可以复现、可以改造成自己项目的思路。1. 动手前先看清边界大众点评评论数据的合规与风险边界1.1 法律红线什么能抓什么不能抓很多人第一次写爬虫脑子里只有“能不能跑通”完全没有“能不能抓”这个概念。我见过不少新手爬大众点评、爬电商、爬社交平台抓了一堆用户ID、手机号、交易记录最后被平台发律师函才反应过来出事了。在动手之前你至少要分清这几类数据的性质公开数据商家名称、地址、营业时间、星级评分、公开评论内容这类信息本身是商家主动展示给公众看的抓取的风险相对低但依然受平台用户协议约束。用户个人信息评论用户的昵称、头像、主页链接、消费金额、所处城市这类信息属于个人信息保护范畴拼接后可以识别到特定个人的数据风险等级直接上升。商业化用途的数据即使抓的是公开评论如果拿去卖给第三方、做竞品分析报告收费、或者大规模迁移到自己的商业产品里都可能构成不正当竞争。这里有一个很现实的判断标准你抓数据是为了个人学习、做一次性的研究分析还是为了持续供给某个产品前者风险可控后者必须走正规渠道比如找平台合作、使用开放接口或者委托专业的数据服务商。1.2 现实约束接口限制、robots协议与平台风控除了法律层面的问题技术层面也有三条线你绕不过去。第一条是robots协议。大众点评的robots.txt里明确规定了哪些路径允许爬虫访问。虽然robots协议没有强制法律效力但它是衡量爬虫善意程度的标尺。一个遵守robots协议的爬虫和一个无视规则横冲直撞的爬虫在法律纠纷中的处境完全不同。第二条是接口访问频率。任何网站的数据接口都有吞吐上限你以为自己只是“礼貌地慢慢爬”但平台看到的可能是一台机器每隔几秒就发起一次请求。大数据风控系统很容易把这种规律性请求识别为爬虫。第三条是平台的数据版权。评论内容、图片、商户信息这些数据平台投入了巨大成本去采集、整理和维护它在法律上对这些数据的汇编享有权益。即使单个评论是用户生产的但平台作为内容聚合方同样会主张权利。提示做爬虫之前先写清楚自己的数据用途。如果是个人学习技术原理控制好规模、不传播、不商用风险是可控的。如果你自己都不确定用途是否合规那就不要开始。2. 先拆请求链路评论数据到底藏在哪个接口里2.1 从浏览器开发者工具看请求全景很多人写爬虫的时候第一步就做错了——拿到一个商家页面地址直接requests.get()去请求然后把返回的HTML丢给BeautifulSoup解析。结果发现解析出来一堆空列表要么是页面结构变了要么是评论内容根本不在这个HTML里。正确的第一步永远是打开浏览器开发者工具按F12切到Network面板然后手动刷新页面、点击翻页、滚动加载。你需要观察的是每当页面上出现新的评论内容时浏览器发出了哪些网络请求。以大众点评的商家评论页为例你通常会看到这几类请求document请求加载页面的主HTML文档javascript请求各种JS文件往往就是它们在动态渲染评论内容xhr/fetch请求异步加载评论数据的接口多半是JSON格式图片请求头像、评论配图、验证码图片判断评论数据来自哪里有一个很实用的技巧在Network面板过滤XHR和Fetch类型然后翻页看哪几个接口返回了新的数据。如果响应内容可以在Preview面板里直接看到评论文字那这个接口就是你的目标。2.2 评论页面的两种形态服务端渲染与异步加载爬虫领域有个经典分法页面数据是服务端渲染还是客户端渲染。服务端渲染的意思是当你请求一个URL时服务器直接返回完整的HTML评论内容就在里面用BeautifulSoup就能解析。这是最简单的爬虫形态。早期很多网站都是这样但现在越来越少了。客户端渲染则是HTML结构只是个空壳真正的评论数据由JS脚本在浏览器里执行后、通过异步请求从后端接口获取再渲染到页面上。这种情况下直接请求评论页URL只能拿到空壳真正的数据在一个XHR接口里。大众点评的评论页属于典型的混合形态首屏评论可能直接渲染在HTML里但翻页数据要通过异步接口加载。所以想抓全量评论就不能只盯着一个页面地址看要同步分析那些XHR接口。2.3 关键Header与参数哪些字段决定了请求成败无论是直接请求HTML还是调用异步接口浏览器发出的每一个请求都带着一堆Header字段。对爬虫来说真正起决定性作用的通常只有几个Header / 参数作用缺失后果User-Agent标识请求来自哪种浏览器环境直接被拒绝访问返回403Referer告诉服务器请求是从哪个页面发起的异步接口常校验该字段缺失则返回400或403Cookie携带登录态、浏览行为标识登录后才能看的信息抓不到触发频率风控Accept-Language声明期望的返回语言可能返回英文或错误页面签名参数接口参数中常见如token、sign参数不全会返回参数错误这里我特别想提醒一句很多爬虫教程里会有“复制浏览器Cookie放到代码里”这个操作。Cookie确实能让请求更像是真人但Cookie是有时效的今天能用不代表明天还能用。等你的爬虫因为Cookie过期突然挂掉的时候你会回来感谢我这句话的。3. 一个能跑的基础版本requests配合BeautifulSoup的抓取骨架3.1 环境准备Python版本、依赖库与虚拟环境正式的爬虫项目我建议用Python 3.9以上版本不要纠结哪个版本“最稳定”3.10、3.11、3.12都没问题。关键是管理好依赖不要一股脑装几十个包到系统环境里不然过两个月你的Python环境会变成一团浆糊。用虚拟环境隔离项目依赖这个习惯越早养成越好。创建方式很简单mkdir dianping-crawler cd dianping-crawler python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate依赖库我只装最核心的三个requests、beautifulsoup4、lxml。pandas和sqlite3后面需要再装。pip install requests beautifulsoup4 lxml pandas为什么用lxml解析器而不是html.parser因为lxml速度快容错性强。真实网页HTML经常有一堆标签嵌套不规范、属性缺引号的问题lxml能尽量解析出结构html.parser在这种场景下更容易返回空结果。3.2 第一版代码构造请求与解析评论先给一个最基础的版本它做的事情很简单请求商家评论页用BeautifulSoup解析评论内容然后打印出来。import requests from bs4 import BeautifulSoup # 这里替换成你要采集的商家评论页地址 # shop_id 是商家在平台上的唯一标识 shop_id 你的目标店铺ID url fhttps://www.dianping.com/shop/{shop_id}/review_all 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: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Referer: fhttps://www.dianping.com/shop/{shop_id} } try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() except requests.RequestException as e: print(f请求失败: {e}) exit(1) soup BeautifulSoup(response.text, lxml) # 评论容器通常有约定的class真实验证时以页面实际结构为准 review_items soup.select(div.review-list div.review-item) if not review_items: print(没有解析到评论节点可能是页面结构变化、验证码或需要登录) else: for item in review_items: username_node item.select_one(a.name) content_node item.select_one(div.review-content) rating_node item.select_one(span.score) print(f用户: {username_node.get_text(stripTrue) if username_node else 匿名}) print(f评分: {rating_node.get_text(stripTrue) if rating_node else 无}) print(f评论: {content_node.get_text(stripTrue) if content_node else 无}) print(---)这段代码是典型的“教材版本”短小、清晰放在小网站上大概率能跑通放在大众点评上大概率会触发风控。为什么因为一个全新的浏览器环境第一次访问就去翻评论页没有一个真人会这样操作。但这不妨碍我们用它理解爬虫的骨架逻辑构造请求、获取响应、解析内容、提取字段。这四个步骤是后续所有复杂爬虫的底座。3.3 解析规则的设计技巧CSS Selector和XPath怎么选BeautifulSoup本身支持两种定位方式CSS Selector和XPath。我非常推荐用CSS Selector因为它写起来直观调试成本低。比如要选取所有评论节点可以先在浏览器开发者工具里定位到一个评论的DOM元素右键Copy - Copy selector就能得到一条现成的CSS路径。把这条路径放回代码里跑一次如果解析出内容说明路径是对的如果解析为空检查是否有动态class名称——很多前端框架生成的class是带哈希后缀的每次部署都会变。XPath则更适合复杂条件比如“选取所有class包含review且文本长度大于50字的节点”。但BeautifulSoup的XPath支持需要依赖lxml的xpath方法实际上我很少在BeautifulSoup里用XPath如果遇到特别复杂的解析需求我通常会直接换scrapy的Selector它的XPath和CSS支持都更规范。解析这块最容易翻车的地方是你以为某个class名是固定的结果它是JS动态生成的。所以每次代码跑空第一步不是改选择器而是重新打开浏览器确认当前的页面结构。4. 反爬不是敌人UA、代理池与请求节奏的现实取舍4.1 大众点评常见的反爬手段盘点我没有办法给你一份实时更新的平台风控策略清单因为风控规则每天在变。但我可以告诉你所有平台通用的反爬逻辑你只要理解了这套逻辑换任何网站都能快速判断自己该怎么做。平台的反爬主要围绕五个维度请求身份识别你是什么浏览器版本、什么操作系统、Cookie是否真实、指纹是否一致。最常见的拦截依据就是User-Agent和Cookie异常。请求频率检测同一IP、同一设备在单位时间内的请求次数。频率过高直接触发封禁或验证码。行为模式识别真实用户有滚动、悬停、点击行为访问路径是分散的爬虫的访问路径是线性的翻页节奏机械规律。内容加密处理评论内容中的文字可能被特殊字体替换直接解析HTML看到的是乱码或错误字符。验证码机制当以上检测发现可疑行为时弹出滑块、点选、短信验证等方式阻断自动化操作。4.2 常规应对之道请求头伪装、代理池与限速先说请求头伪装。一个像样的请求头至少要有完整的User-Agent、Accept、Accept-Language、Accept-Encoding、Referer。更好的做法是维护几个不同浏览器的User-Agent列表每次请求随机使用一个避免单一指纹被盯上。再看代理池。代理IP的核心用途是分散请求来源防止单一IP因频率过高被拉黑。但对于小规模学习型爬虫我个人的建议是本地IP配合限速就够了没有必要一上来就搭什么代理池。分布式代理池的维护成本非常高IP失效、延迟、被封任何一个问题都比大众点评的反爬更让你头疼。真正重要的是请求节奏。我在实际项目里给抓取模块设定的默认规则是单次请求完成后随机等待1到3秒再发起下一次请求。如果是批量抓取多个商家每次切换商家时等待时间拉长到3到5秒。这个速度听起来很慢但对小规模数据采集已经够了。import random import time def polite_delay(min_seconds1, max_seconds3): # 随机延时模拟真实用户的浏览节奏 time.sleep(random.uniform(min_seconds, max_seconds))4.3 对抗的边界哪些操作不能做这里必须把话说清楚。我见过一些人为了突破平台反爬去研究字体反爬的映射表、去破解前端JS的加密逻辑、去模拟滑块验证码的轨迹甚至通过打码平台实时识别验证码。这些技术在黑灰产领域被大量使用但对一个正当的爬虫项目来说它们不仅没有必要而且危险。判断标准很简单如果绕过验证码、破解加密需要你花费的时间比业务本身还长说明你要么选错了数据获取方式要么业务数据量已经大到需要和平台正式合作了。我的原则是遇到验证码停下来。这通常意味着对方已经识别到你的请求异常继续硬刚只会让IP进入更严格的黑名单。正确的做法是降低频率、检查Cookie、更换代理等风控消退后再继续。5. 完整代码拆解从单页抓取到批量落地5.1 项目结构别把全部代码塞进一个文件网上的爬虫教程大多是一段代码打天下跑通就完事。但真实项目里你会面临接口调整、数据结构变化、新增字段、任务中断恢复各种情况。如果代码全是面条式结构改一行可能牵动全身。我建议最小规模的爬虫项目也分成三个模块dianping-crawler/ ├── config.py # 配置项目标URL、请求头、延时范围、存储路径 ├── fetcher.py # 请求模块负责发送请求、处理异常 ├── parser.py # 解析模块负责从HTML/JSON中提取评论字段 ├── storage.py # 存储模块负责去重、写入数据库 └── main.py # 调度入口串联整个流程这样一个文件负责一件事出问题的时候你能在30秒内定位到位置请求挂了就去查fetcher解析为空就去查parser数据重复就去查storage。5.2 核心代码解读从单页抓取到批量执行完整的代码我拆成几个部分来讲你按顺序拼接起来就是一个可以运行的工程版本。首先是配置模块把所有可变参数集中管理# config.py BASE_URL_TEMPLATE https://www.dianping.com/shop/{shop_id}/review_all HEADERS_TEMPLATE { 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: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } REQUEST_TIMEOUT 10 DELAY_RANGE (1, 3)然后是请求模块。这里的异常处理是关键请求失败不能直接抛异常导致整个程序退出而应该重试几次重试仍然失败就记录日志跳过当前请求# fetcher.py import time import random import requests import config def fetch_html(url, headersNone, max_retries3): if headers is None: headers config.HEADERS_TEMPLATE.copy() for attempt in range(max_retries): try: response requests.get(url, headersheaders, timeoutconfig.REQUEST_TIMEOUT) response.raise_for_status() return response.text except requests.RequestException as e: print(f[第{attempt 1}次请求失败] {url}错误: {e}) if attempt max_retries - 1: time.sleep(config.DELAY_RANGE[1] * (attempt 1)) return None解析模块把评论从HTML里抽出来返回结构化的字典列表。这样解析逻辑和抓取逻辑解耦后面平台改版了你只需要改解析函数# parser.py from bs4 import BeautifulSoup def parse_reviews(html, shop_id): soup BeautifulSoup(html, lxml) results [] review_items soup.select(div.review-item) for item in review_items: username_node item.select_one(a.name) content_node item.select_one(div.review-content) rating_node item.select_one(span.score) results.append({ shop_id: shop_id, user_name: username_node.get_text(stripTrue) if username_node else , content: content_node.get_text(stripTrue) if content_node else , rating: rating_node.get_text(stripTrue) if rating_node else , }) return results存储模块用SQLite是因为它单文件、零配置、跨平台处理百万级别以下的数据完全够用# storage.py import sqlite3 class ReviewStorage: def __init__(self, db_pathreviews.db): self.conn sqlite3.connect(db_path) self._create_table() def _create_table(self): self.conn.execute( CREATE TABLE IF NOT EXISTS reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_id TEXT NOT NULL, user_name TEXT, rating TEXT, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(shop_id, user_name, content) ) ) self.conn.commit() def save_reviews(self, reviews): for rv in reviews: try: self.conn.execute( INSERT OR IGNORE INTO reviews (shop_id, user_name, rating, content) VALUES (?, ?, ?, ?), (rv[shop_id], rv[user_name], rv[rating], rv[content]) ) except sqlite3.Error as e: print(f写入失败: {e}) self.conn.commit() def close(self): self.conn.close()最后是main.py把整个流程串起来# main.py import config from fetcher import fetch_html from parser import parse_reviews from storage import ReviewStorage def scrape_shop(shop_id): url config.BASE_URL_TEMPLATE.format(shop_idshop_id) html fetch_html(url) if html is None: print(f[跳过] {shop_id} 多次请求失败) return reviews parse_reviews(html, shop_id) print(f[完成] 店铺{shop_id} 解析到 {len(reviews)} 条评论) return reviews if __name__ __main__: storage ReviewStorage() shop_ids [123456, 234567] # 替换为目标店铺ID列表 for shop_id in shop_ids: reviews scrape_shop(shop_id) if reviews: storage.save_reviews(reviews) time.sleep(random.uniform(*config.DELAY_RANGE)) storage.close()5.3 可配置项与扩展点如何接上未来的需求这个工程版代码留下了三个很容易扩展的口子第一个是翻页。当前只抓了第一页真实需求里至少要看20页。扩展方式是在continue循环中构造不同的分页参数每抓完一页调用一次延时函数。第二个是字段扩展。如果你需要抓评论发布时间、点赞数、回复数只需要在parse_reviews里增加对应的选择器和数据库字段这个操作本身不涉及请求层改动非常集中。第三个是请求方式替换。如果未来需要抓取异步接口只需要在fetch_html的基础上构造带有参数的接口URL返回JSON后用json库解析完全不破坏现有存储结构。6. 数据清洗和存储评论内容拿到手之后的第二场战役6.1 脏数据从哪来去重、噪声与字段残缺很多人以为数据抓到本地就算大功告成后来做分析的时候才发现数据根本没法用。大众点评评论数据常见的脏数据来源有三个重复数据翻页时接口偶发返回同一批数据或者多个评论节点内容相同。噪声内容广告评论、无意义字符、纯表情符号、被截断的文本。字段残缺用户已注销后昵称为空、评论被折叠、评分内容不展示。如果你只抓一次数据、只做简单统计这些脏数据影响不大。但如果你想按月监控评论变化、做语义分析脏数据会直接污染你的分析结论。6.2 去重策略与增量更新我在存储模块里用了一个很关键的设计在表上建立UNIQUE约束约束字段是shop_id user_name content。这个组合能天然过滤掉重复评论。但要注意这个去重策略是在数据库层做的。你还需要在代码层做一层保护特别是在断点续跑的场景下。比如程序跑了2000条评论后进程崩溃重启后从头抓取如果没有代码层去重你会把相同数据重新写入一遍。INSERT OR IGNORE语法在存储层会帮你挡掉重复这是对的。增量更新的逻辑其实更简单每次抓取前查询本地已有评论的最大页数或最新评论时间从那个位置开始继续抓。由于大众点评的评论是按时间倒序排列的你只需要记住上次抓到的最后一条评论ID即可。6.3 存储方案对比与选型建议不同数据量级下存储选型思路完全不同数据量推荐方案理由几千条CSV / Excel量小直接分析最方便几万到几十万条SQLite单文件查询方便支持SQL语法百万条以上MySQL / PostgreSQL并发读写、索引、事务都更成熟我的建议是哪怕你最终要用MySQL开发阶段也先用SQLite把流程跑通避免本地环境装数据库浪费大量时间。SQLite的数据可以很方便地迁移到MySQL导出CSV再用LOAD DATA导入即可。另外数据落盘的历史版本管理值得做。我在每天抓取结束后会把当天的SQLite文件另存一份带日期的副本。这样一旦后续数据处理逻辑写错导致数据被污染还能回退到前一天的版本。7. 稳定性与排错实录爬虫能跑三天才是真本事7.1 高频报错清单与排查建议我把实际跑大众点评这类站点时最常遇到的报错整理成一张表按出现频率排序报错现象可能原因排查方向403 ForbiddenUser-Agent被识别、IP被风控检查请求头完整性更换IP或代理连接被重置 / ConnectionResetError请求频率过高服务端主动断连增大延时检查是否被限流返回页面含验证码关键字触发验证码风控停止当前抓取降低频率人工处理验证码后重试解析结果为空列表页面结构变动、CSS选择器失效重新打开页面用开发者工具确认节点路径字体乱码平台使用字体反爬确认是否必须采集评估合规性后再决定是否继续请求超时网络问题或目标服务器响应慢增加timeout值增加重试机制7.2 Cookie失效、登录态与前后端联动的坑Cookie失效是最坑的问题因为程序不会报错它只会静默地返回一个空数据或者验证码页面。你看着日志里一切正常检查数据却发现什么都解析不到。我处理这个问题的办法分三层第一层是每日定时检测Cookie是否有效。写一个健康检查函数用Cookie请求一个已知存在的页面检测响应中是否包含正常内容关键字。不包含就触发告警发邮件或者企业微信通知。第二层是让程序在启动时从配置文件读取Cookie如果失效就自动暂停并发出提醒等人手动更新。绝不自动尝试破解登录或打码平台前面说过这个红线不能碰。第三层是把Cookie过期检测当作异常分支处理而不是日志里的一行普通信息。这样运维人员才会重视它。7.3 日志、监控与优雅退出最后一个稳定性建议给爬虫写日志。不是用print把内容打印到控制台而是用logging模块输出到文件。print在任务跑挂之后什么都留不下logging能记录每次请求的成功与否。import logging logging.basicConfig( filenamecrawler.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) logging.info(f开始抓取店铺 {shop_id}) logging.warning(f店铺 {shop_id} 请求失败已跳过)另外程序一定要支持KeyboardInterrupt优雅退出。长时间运行的爬虫难免需要手动中断。如果没有善后处理SQLite连接没关闭、任务状态没保存下次启动可能出现各种奇怪问题。if __name__ __main__: storage ReviewStorage() try: for shop_id in shop_ids: ... except KeyboardInterrupt: print(收到中断信号正在保存状态...) finally: storage.close() print(数据库连接已安全关闭)我见过太多人写的爬虫CtrlC一按程序直接崩掉数据库连接没关下次重跑文件锁还在。这种细节才是工程爬虫和个人脚本的根本区别。8. 一个现实的收尾5分钟到底能做什么回到标题本身。5分钟到底够干什么我的答案是5分钟足够你搭出一个包含请求、解析、存储的最小爬虫骨架也足够你跑通第一个请求、看到第一条评论被打印出来。这已经是很不错的学习成果。但如果你要的是一个能长期稳定运行、每天自动抓取新评论、数据干净可用的生产级爬虫那真相是这不是5分钟的活而是需要持续维护的工程。平台的页面结构会变、风控策略会变、你的数据需求也会变爬虫不是写完就结束的工具它更像一个需要定期养护的植物。我在实际操作里的体会是遇到“抓不到”的情况先不要急着上更高级的技术而是先退一步重新审视请求链路的每个环节请求头是否完整、Cookie是否过期、频率是否过高、数据结构是否变动。90%的爬虫问题都出在这四件小事上。把这四件事管好了剩下的就是时间和耐心的问题了。最后再分享一个小技巧如果你只是想研究某家店的评论分布或者做极少量数据的试验分析完全可以直接用平台自己的搜索和筛选功能手动导出根本不用写爬虫。当你评估后发现自己真正需要的量级远没有大到必须写代码的程度那就不要写。省下来的时间足够你把这个决策过程想得更清楚。
返回列表