简介:基于Scrapy框架的大众点评爬虫源码,适合有一定Python基础、希望学习Scrapy架构及反爬实战的爬虫开发人员。项目通过浏览器手动登录与验证,规避了验证码识别难点,同时内置代理设置与字体反爬处理,可有效应对大众点评常见的反爬策略。爬取内容覆盖城市列表、分类列表与店铺详情,包含名称、地址、评分、评价数、人均消费等字段,字段结构清晰。资源共19个文件,以13个Python源码为核心,辅以requirements依赖清单、Scrapy配置与说明文档,压缩包仅21KB,轻量紧凑,便于阅读与移植。代码目录按Scrapy标准结构组织,包含items、pipelines、middlewares、spiders等模块,并明确了“先城市分类、再店铺链接、后店铺信息”的爬取顺序,数据自动落盘至data目录。截至目前已有596人学习,适合用于研究Scrapy框架、代理池接入、字体反爬对抗等实战场景。
1. 基于 Scrapy 框架的大众点评爬虫:先拆透反爬再谈采集
做爬虫的人早晚会接到一个需求:把大众点评的店铺数据抓下来。别急着写代码,这个站点的反爬强度在业内是出了名的——字体反爬、CSS 定位、滑块验证、IP 封禁层层叠加,很多人第一版代码跑不到 200 条数据就被封了 IP。本文要讲的就是怎么用 Scrapy 框架,把大众点评的店铺名称、地址、评分、人均消费、评论数这些核心字段稳定地抓下来。
这套方案适合三类人:想系统性学习 Scrapy 反爬技巧的中级开发者,需要给本地生活项目积累样本数据的分析师,以及被老板临时派活、需要快速评估可行性的一线工程师。文章会从反爬机制拆解、Scrapy 工程搭建、中间件设计、数据存储到最终的避坑排查,全程按可复现的步骤走。
2. Scrapy 与大众点评的反爬博弈:先知道对手怎么出牌
2.1 大众点评的字体反爬是怎么让数据"变脸"的
大众点评最经典的反爬手段是字体反爬。你在浏览器里看到的店铺评分是「4.8」,但爬虫直接拿到的 HTML 文本里可能是「x82」或者一串乱码。原理是站点自定义了一套字体文件,把标准数字 0-9 映射到自定义的 Unicode 码位,浏览器加载字体后显示正常,而 requests 或 Scrapy 默认拿到的文本就是映射前的乱码。
具体判断方法很直观:用 Scrapy shell 抓一个店铺页面,如果评分字段在响应体里是空白、乱码或者加密字符,基本可以断定命中了字体反爬。解决思路分两步:
scrapy shell "https://www.dianping.com/shop/xxx" >>> response.xpath('//span[@class="review-score"]/text()').get() '\ue286' # 典型字体加密字符第一步是从响应里提取字体文件 URL,比如//s3plus.meituan.net/v1/mss_0e1dxxx/...woff;第二步是下载字体文件,用fontTools解析出字符映射表,再把乱码还原成真实数字。这里的关键是:解析字体映射不能写死,因为大众点评会定期更换字体文件,写死映射表就是给自己埋定时炸弹。
2.2 动态 iframe 与异步加载:为什么 HTML 里看不到数据
现在的大众点评页面大量使用动态 iframe 和异步请求。你打开一个店铺详情页,直接查看网页源码,看不到评论内容、看不到推荐菜,这些数据是页面加载后由 JavaScript 发起 XHR 请求再渲染进去的。
用 Scrapy 默认的scrapy.Request只能拿到静态 HTML,取不到动态内容。这里有两个方案:一是用 Scrapy 配合 Splash 渲染 JS,二是抓取异步接口本身。我的经验是优先抓接口,因为接口返回的是干净的 JSON 数据,处理效率高,而且大众点评的接口路径往往比页面 DOM 结构更稳定。
scrapy shell "https://www.dianping.com/shop/xxx" >>> response.xpath('//iframe/@src').get() '//m.dianping.com/xxx/modal'遇到 iframe 嵌套时,Scrapy 的response.follow可以继续跟进 iframe 的 src 地址,但要注意 iframe 页面内部往往还有二次异步请求。排查技巧是:先开浏览器开发者工具,Network 面板里筛 XHR 请求,看数据到底是从哪个 URL 返回的,再把那个 URL 作为 Scrapy 的请求对象。
2.3 常见反爬机制的触发条件与应对优先级
大众点评的反爬是分层的,不同层级触发条件完全不同。最外层是 IP 限流,单位时间内请求次数超过阈值就返回验证码页面;中间层是 UA 和 Cookie 检测,非浏览器 UA 直接被拒;最内层才是字体加密和参数签名。
应对优先级应该反过来:先解决参数签名,再处理字体反爬,最后才是 IP 池。原因很简单,如果请求发不出去,后面一切白搭。我自己常用的中间件组合是:自定义 UA 轮换 + Cookie 自动携带 + IP 代理池 + 请求限速。这四件套能解决 90% 的常规问题,剩下 10% 是大众点评更新反爬策略后的临场博弈。
3. 搭建 Scrapy 工程并跑通首个店铺列表页
3.1 从零创建 Scrapy 项目:目录结构与基础配置
创建工程之前先确认环境,我用的 Python 3.9+ 和 Scrapy 2.x,如果你还在用 Python 2 时代的老 Scrapy,建议先升级,否则后面很多中间件写法不兼容。
python -m venv dianping-scrapy source dianping-scrapy/bin/activate pip install scrapy scrapy startproject dianping cd dianping scrapy genspider shop www.dianping.com生成的目录结构里,spiders/shop.py是核心爬虫文件,settings.py里要改几个关键配置。我一般先把ROBOTSTXT_OBEY设为 False,因为大众点评的 robots 协议明确禁止爬虫,但做数据采集时通常会绕过;DOWNLOAD_DELAY设为 2-3 秒,这是最低限度的礼貌;DEFAULT_REQUEST_HEADERS里把 UA 换成完整的浏览器 UA。
# settings.py 核心配置 ROBOTSTXT_OBEY = False DOWNLOAD_DELAY = 2.5 DEFAULT_REQUEST_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.dianping.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', }注意DOWNLOAD_DELAY只能限速单请求的间隔,如果开了并发,Scrapy 会乘以并发数。想要更精确的限速,可以配合AUTOTHROTTLE_ENABLED = True让 Scrapy 根据服务器响应时间动态调整请求频率,这个后面细说。
3.2 编写列表页爬虫:从搜索页提取店铺 ID
跑通列表页是整个项目的敲门砖。大众点评的列表页 URL 格式大约是https://www.dianping.com/shanghai/ch10/g110这种,其中g110是店铺分类编号,ch10是行政区编号。爬虫的任务就是遍历这些分类和区域的组合,提取每张卡片里的店铺详情页链接。
import scrapy class ShopSpider(scrapy.Spider): name = 'shop' start_urls = ['https://www.dianping.com/shanghai/ch10/g110'] def parse(self, response): # 店铺卡片链接形如 /shop/12345678 for link in response.css('a[href*="/shop/"]::attr(href)').getall(): yield scrapy.Request( url=response.urljoin(link), callback=self.parse_detail, meta={'source_url': response.url}, dont_filter=True ) # 翻页:大众点评列表页翻页参数是 ?p=2 next_page = response.css('a.next::attr(href)').get() if next_page: yield response.follow(next_page, callback=self.parse)这里有两个坑必须提醒。第一是dont_filter=True,店铺详情页 URL 可能在不同列表页重复出现,默认去重会把后续请求过滤掉,导致数据缺失;第二是翻页参数,大众点评的前几页是静态渲染,但页码变大后可能变成动态加载,所以翻页逻辑要写好条件判断,页面没有a.next就说明翻到头了。
3.3 用 Scrapy Shell 调试字段提取规则
字段提取是爬虫最容易写歪的地方,特别是大众点评这种 DOM 结构复杂、class 命名还会变的站点。我的习惯是先抓一个真实响应存成本地 HTML,再用 Scrapy Shell 反复调试 XPath 或 CSS 选择器,确认无误后再写进爬虫代码。
scrapy shell "https://www.dianping.com/shop/example" >>> response.css('h1.shop-name::text').get() >>> response.xpath('//div[contains(@class, "shop-info")]//text()').getall()调试时重点关注店铺名称、评分、人均价格、地址、评论数这几项。每个字段都要写多个候选选择器,因为大众点评改版的频率不低,第一套选择器失效时,备用选择器能让你少改几次代码。我见过有人硬编码 class 名,站点一改版全盘崩掉,这就是没有做选择器冗余的教训。
4. 破解字体反爬与动态数据:让数字回到真实值
4.1 下载并解析 woff 字体文件
字体反爬是大众点评最让人头疼的一层,但拆解的套路是固定不变的。当 HTML 里出现乱码字符时,先拿到@font-face声明里的字体文件 URL,用requests下载到本地,再用fontTools解析字体的 cmap 表,建立乱码字符到真实数字的映射。
from fontTools.ttLib import TTFont def parse_font(font_path): font = TTFont(font_path) cmap = font.getBestCmap() mapping = {} for code, glyph_name in cmap.items(): # 大众点评字体里 glyph 名通常形如 uniE286 if glyph_name.startswith('uni'): # 关键:字体文件里的坐标要与基准字体对比才能还原数字 pass return mapping这里有个容易遗漏的细节:光靠字体文件本身还原不了数字,必须拿加密字体和一份基准字体做坐标对比。大众点评的加密字体把数字的轮廓做了平移和变形,每个字符的实际轮廓坐标和标准数字的轮廓坐标存在对应关系。所以更稳妥的做法是:下载加密字体和一个已知的标准数字字体,用字符的轮廓坐标做最近邻匹配,才能得到正确的映射关系。
4.2 字体缓存策略:别每次都下载解析
字体文件如果每次都下载再解析,效率太低,而且高频访问字体 URL 容易被重点关注。常见做法是把解析好的映射表缓存到 Redis 或本地 JSON 文件里,键是字体文件的 MD5 值。
import hashlib import json def get_font_mapping(font_content): font_hash = hashlib.md5(font_content).hexdigest() cached = cache.get(font_hash) if cached: return json.loads(cached) # 解析逻辑省略 cache.set(font_hash, json.dumps(mapping), ex=86400)缓存过期时间我一般设 24 小时,因为大众点评不是每天都换字体,但也不排除大促期间临时更换。如果你的爬虫要求数据实时性高,可以把过期时间缩短到 6 小时。另外一个细节是字体文件存储路径,用hash = md5(url)做文件名最方便,既避免重复下载,又能追溯来源。
4.3 动态接口抓取:从 XHR 响应里拿干净数据
绕过动态渲染的方案是直接抓 XHR 接口。大众点评的评论接口、推荐菜接口都有固定的 URL 模式,关键是分析参数签名。我见过最简单的接口只需要shopId和page两个参数,但也见过带sign和token的,这类参数通常是用 JavaScript 加密生成的,逆向成本较高。
def parse_comments(self, response): # 假设接口返回 JSON data = json.loads(response.text) for item in data.get('commentList', []): yield { 'user': item.get('userName'), 'rating': item.get('star'), 'content': item.get('text'), 'time': item.get('createTime'), }没有 JS 逆向基础的人,我建议先别碰签名参数。更实际的方案是:列表页能拿到的字段先用列表页的数据,评论内容可以用 Splash 渲染 iframe 页面来取,虽然慢一点,但胜在稳定。等业务量证明这个方向值得投入,再去做接口逆向,性价比会更高。
5. 中间件与数据管道:反爬配置和存储落地
5.1 自定义 UA 轮换与 Cookie 中间件
UA 轮换是反爬的基础配置,但很多人做得太粗糙,把浏览器 UA 列表随机切换就行。实际上,大众点评对 UA 的检测不只是看 UA 字符串,还看 UA 与浏览器特征指纹的匹配度。比如 Common Crawl 的 UA 是机器人标识,就算换个浏览器版本号也一眼看穿。
class RandomUserAgentMiddleware: def process_request(self, request, spider): ua = random.choice(UA_LIST) request.headers['User-Agent'] = ua return NoneUA 列表可以从主流浏览器的真实访问记录里收集,比如 Chrome 最新版、Edge、Safari 的 UA 字符串,每种浏览器保持 3-5 个版本号。这里有个玄学经验:UA 轮换和请求频率要联动,如果你每 10 秒钟换一个全新 UA,但请求频率是每秒 10 次,一样会被识别为异常行为。
Cookie 中间件的作用是把登录后的 Cookie 保存下来,在每次请求时自动携带。大众点评的某些字段需要登录才能看到完整数据,不登录只能拿到部分信息。实现方式有两种:手动从浏览器复制 Cookie 到配置里,或者用 Scrapy 的 cookie jar 自动保存 Set-Cookie。
class CookieMiddleware: def process_request(self, request, spider): if spider.cookie: request.cookies = spider.cookie return None手动复制 Cookie 的方式适合短期采集,Cookie 失效后需要重新复制;自动保存 Cookie 更适合长期运行,但第一次请求之前需要在浏览器里完成登录流程。我一般会在爬虫启动时读取一个本地 Cookie 文件,启动后自动加载。
5.2 IP 代理池的接入方式与可用性检测
IP 被封是整个爬虫项目最大的翻车点。大众点评的封禁策略是阶梯式的:先返回验证码,再临时封 IP,最后直接拒绝访问。所以 IP 代理池的质量直接决定爬虫能跑多久。常见做法是维护一个代理池,定时检测代理的可用性,并把不可用的剔除。
class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_manager.get_proxy() request.meta['proxy'] = f'http://{proxy["ip"]}:{proxy["port"]}' return None def process_response(self, request, response, spider): # 429 或验证码页面标记为坏代理 if response.status == 429 or 'captcha' in response.url: spider.proxy_manager.remove_proxy(request.meta['proxy']) return response代理质量检测是容易偷懒但必须做扎实的事。检测标准不只是代理能否连通,还要看目标站点的可用性。有些代理 IP 在别的网站能用,在大众点评就是被屏蔽的状态。所以要定时用测试请求访问大众点评的首页,返回 200 才标记为可用。
接入代理池有两个选择:用现成的开源代理池项目,或者自己用付费代理 API 搭建。如果你跑的数据量不大,直接配置 3-5 个付费代理轮换就够了;如果目标数据量是几十万条,建议自建代理池管理几十上百个代理 IP。
5.3 用 SQLAlchemy 把数据写入 MySQL:管道设计
Scrapy 的数据管道是从 Item 到数据库的最后一公里。直接用 pymysql 写 SQL 虽然简单,但并发条件下容易出问题。用 SQLAlchemy 做 ORM 层,既能管理连接池,又能避免手工拼接 SQL 的注入风险。
# pipelines.py from sqlalchemy import create_engine, Column, String, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base = declarative_base() class Shop(Base): __tablename__ = 'dianping_shop' shop_id = Column(String(20), primary_key=True) name = Column(String(200)) address = Column(String(500)) rating = Column(Float) avg_price = Column(Float) engine = create_engine('mysql+pymysql://user:pass@localhost/dianping') Session = sessionmaker(bind=engine) class MySQLPipeline: def open_spider(self, spider): self.session = Session() def process_item(self, item, spider): shop = Shop(**item) self.session.merge(shop) # 主键去重 self.session.commit() return item用merge而不是add是这里的细节,merge会根据主键判断记录是否存在,存在就更新,不存在就插入。这样就算爬虫重复跑到同一个店铺也不会产生脏数据。另外,SQLAlchemy 的 session 不是线程安全的,Scrapy 的管道默认是单例运行,但如果你启用了多个并发管道实例,每个实例都要独立创建 session。
5.4 请求限速策略:DOWNLOAD_DELAY 和 AUTO THROTTLE 怎么配
很多人在 Scrapy 里把DOWNLOAD_DELAY设成 1 秒就觉得万无一失,实际上这个参数只是单个请求的间隔,实际 QPS 是并发数除以延迟。如果你的CONCURRENT_REQUESTS是 16,延迟 1 秒,实际 QPS 就是 16,这个频率大众点评绝对会封你。
# settings.py CONCURRENT_REQUESTS = 4 DOWNLOAD_DELAY = 2.0 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 2.0 AUTOTHROTTLE_MAX_DELAY = 15.0AUTO THROTTLE 是 Scrapy 内置的自适应限速,它会根据服务器响应时间来动态调整延迟。刚开始爬时可以保守一点,AUTOTHROTTLE_START_DELAY设大一些,让 Scrapy 慢慢试探服务器的容忍度。这里有个血泪经验:不要迷信 AUTO THROTTLE,它在普通站点好使,在大众点评这种反爬机制复杂的站点上,响应时间可能是 0.5 秒但也有可能直接就给你返回验证码页面,这时候限速算法判断不出来,还是得人工观察日志。
6. 避坑指南:大众点评爬虫的 5 个必踩的坑
6.1 被重定向到验证码页面却不报错
现象:爬虫日志里请求状态全是 200,但解析出来的数据全是空列表,或者店铺名称全是乱码。
原因:大众点评把所有需要验证的请求重定向到验证码页面,但重定向后的 URL 状态码是 200,所以 Scrapy 的日志不会报错。这是最麻的翻车场景,因为问题不出在你写的解析逻辑上。
解决:在中间件里显式检测验证码页面的特征。大众点评验证码页面的 URL 通常包含captcha或verify关键词,HTML 里也有明显的验证码 canvas 结构。检测到了就把当前代理加入黑名单,并让请求重新排队。
6.2 字体映射表在运行中途失效
现象:前 2 个小时数据正常,第 3 个小时开始评分字段变成乱码数字。
原因:大众点评在某个时间点更换了字体文件,但你的映射表缓存的是旧字体解析结果。
解决:解析字体映射时不要只按 URL 缓存,要按字体文件的 MD5 值缓存。每次解析前先下载字体内容,计算 MD5,如果 MD5 不在缓存里就重新解析。另一个思路是让映射表解析失败时主动清空缓存并重试,不要返回空映射。
6.3 列表页翻页到第 5 页后请求全部失败
现象:前 4 页采集正常,第 5 页开始返回空响应或直接超时。
原因:大众点评的翻页机制在页码变大后从静态渲染切换成了动态加载,a.next链接不再存在,Scrapy 的翻页逻辑直接断掉。
解决:在 parse 方法里,除了找a.next,还要检查是否有「加载更多」按钮或异步接口的线索。常见做法是把前 5 页用静态翻页,第 5 页之后改用 XHR 接口翻页,接口 URL 里的 offset 参数从 0 开始每页递增。
6.4 店铺详情页和列表页的 Cookie 不一致
现象:列表页提取到的数据正常,跟进详情页后请求返回 403 或验证码。
原因:列表页是未登录状态访问的,详情页的部分数据需要登录才能查看,Cookie 状态不一致导致详情页请求被拒。
解决:在爬虫启动前确保使用登录后的 Cookie,而且详情页的请求要显式携带这个 Cookie。不要依赖 Scrapy 默认的 cookie jar,因为 jar 可能会在跳转过程中丢失部分字段。在每次请求前手动设置request.cookies是最可靠的方式。
6.5 管道写入阻塞导致爬虫变慢
现象:爬虫前半段速度正常,运行半小时后越来越慢,最后几乎停滞。
原因:SQLAlchemy 的 session 在长时间运行后积累了大量未提交的事务,数据库连接池被占满。
解决:process_item里每条数据都 commit 虽然安全但性能差,可以改成每 50 条或 100 条批量 commit 一次。另一个技巧是bulk_save_objects批量插入,但要注意它不会自动更新已存在的记录,需要配合ON DUPLICATE KEY UPDATE或先查询再插入。
7. 进阶:用 Scrapy Playwright 集成处理更复杂的反爬场景
当常规中间件全部用上,还是被大众点评的某些页面卡住时,就该上 Playwright 了。Scrapy 2.x 可以通过scrapy-playwright插件集成浏览器自动化能力,专门处理那些必须模拟真实浏览器行为才能通过的场景。
# settings.py DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } PLAYWRIGHT_LAUNCH_OPTIONS = { "headless": True, }集成之后,可以在 Request 里指定meta={"playwright": True},让 Scrapy 用真实的 Chromium 去请求页面。这个方案对滑块的滑块验证、动态 iframe 渲染、JS 加密参数都有奇效,但代价是速度和资源消耗。一个 Playwright 浏览器实例占用的内存是普通请求的几十倍,所以只对关键页面启用,常规页面还是走原生的 Scrapy 请求。
验证方法很简单:跑一个 100 条数据的抽样任务,对比 Playwright 模式和纯 Scrapy 模式的成功率与耗时。一般实测结果会是:Playwright 模式成功率接近 100%,但速度慢 5-10 倍。所以业务规模小、数据实时性要求不高时,直接全量用 Playwright 反而省事;数据量大时还是要混合调度。
最后一个建议是记录每次反爬策略变更的时间点和触发条件。大众点评的反爬策略不是随机改的,通常跟活动节点、数据泄露事件有关。当你遇到策略调整时,先看自己是不是请求频率太高、代理质量下降,再确认是不是站点改版。这个排查顺序能省掉大量猫抓老鼠的时间。
做爬虫这么些年,我的一个习惯是给每个项目建立一个「翻车日志」,代码改动前先记录当前行为,改动后对比差异。这种经验积累看起来慢,但时间长了,踩过坑变成条件反射,新项目启动效率会快很多。希望帮到你。
本文还有配套的精品资源,点击获取