Scrapy爬虫实战:破解大众点评反爬,全流程数据采集项目拆解
简介:这是一套基于Scrapy框架的大众点评爬虫实现,面向有一定Python基础、想研究电商及生活服务平台数据采集的爬虫开发者。资源覆盖城市、分类、店铺链接到店铺信息的完整链路,内置浏览器登录与手动过验证、代理设置、字体反爬等实用机制,能帮助读者快速搭建可运行的点评数据爬取项目。压缩包共19个文件,以Python源码为主(13个py),包含spiders爬虫模块、pipelines管道、middlewares中间件、settings配置和utils工具等,另附依赖清单、README说明与许可证;整包仅21KB,轻量易部署。项目明确了先爬城市和分类、再爬店铺链接,最后获取店铺信息的依赖顺序,DATA目录会自动落盘结果,结构清晰便于二次开发。已有596人学习下载,适合作为Scrapy框架进阶练习或改造为其他平台采集器的参考基底。
1. Scrapy 爬虫攻大众点评:一个能跑通全流程的实战项目拆解
大众点评的反爬在业内是出了名的硬骨头,字体反爬、CSS 偏移、轨迹检测、滑块验证一层套一层,网上能找到的爬虫教程十个里有八个跑到店铺详情页就断了。这份基于 Scrapy 的爬虫项目好在它不是玩具——城市、分类、店铺链接、店铺信息四级爬取链路完整,登录和滑块过验证用浏览器手动完成,后续请求全程带 Cookie 跑,还内置了代理中间件和字体反爬处理。也就是说,你拿到手不是看个 demo,而是能直接在自己的机器上跑出城市列表、商圈和店铺结构化数据的可用工程。
适合谁?有一定 Python 基础、想研究 Scrapy 框架在生产场景里怎么落地的人,以及需要大众点评店铺数据做分析、做市场调研但不想从零造轮子的从业者。接下来我会按「工程结构 → 核心配置 → 字段与存储 → 避坑记录 → 实战技巧」的顺序拆,全程贴关键代码和参数,照着复现就行。
2. 工程结构拆解与依赖安装:先看懂 Scrapy 项目的五脏六腑
2.1 目录里每个文件是干什么的
dianping-spider-master/ ├── scrapy.cfg ├── requirements.txt ├── .gitignore ├── LICENSE └── dianping/ ├── __init__.py ├── items.py ├── pipelines.py ├── settings.py ├── middlewares.py ├── spiders/ │ ├── __init__.py │ └── (城市/分类/店铺链接/店铺信息 爬虫) └── utils/标准 Scrapy 工程结构,scrapy.cfg是项目配置文件,dianping/是主模块。spiders/目录下放的是具体爬虫,middlewares.py管请求拦截和代理切换,pipelines.py管数据落盘,utils/放字体反爬映射这类工具函数。README 里对运行步骤有说明,但先看结构能少走弯路。
2.2 环境准备:Python3 与依赖安装
项目要求 Python3,依赖集中在requirements.txt里。在文件所在目录执行:
pip install -r requirements.txt核心依赖是 scrapy、requests 和 Pillow。requests 用于直接拉取字体文件,Pillow 用于把字体渲染成图片做映射,这两项是字体反爬处理的关键。安装完可以用scrapy version验证框架是否就绪,常见问题有 pip 源慢导致超时,建议加-i https://pypi.tuna.tsinghua.edu.cn/simple走国内镜像。
依赖装完不要急着跑爬虫,先看settings.py——Scrapy 的绝大多数反爬策略都在这个文件里配置,项目能不能稳定运行,一半看这里。
3. settings.py 与中间件:代理、UA、下载延迟与验证码策略
3.1 settings.py 里的关键参数
打开settings.py,重点看几个配置项。机器人的ROBOTSTXT_OBEY必须设成False,大众点评的 robots 协议不允许抓取,很多新手栽在这——默认是 True,一跑就被拒。
# settings.py 关键配置 ROBOTSTXT_OBEY = False DOWNLOAD_DELAY = 2.5 RANDOMIZE_DOWNLOAD_DELAY = True DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/json;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', } DOWNLOADER_MIDDLEWARES = { 'dianping.middlewares.RandomUserAgentMiddleware': 400, 'dianping.middlewares.ProxyMiddleware': 500, } ITEM_PIPELINES = { 'dianping.pipelines.JsonPipeline': 300, }DOWNLOAD_DELAY = 2.5表示每个请求间隔 2.5 秒,RANDOMIZE_DOWNLOAD_DELAY = True会在 2.5 秒基础上做随机波动,这是模拟人类浏览行为的基础操作。DEFAULT_REQUEST_HEADERS里设置基础请求头,但 UA 的随机化由中间件处理。
3.2 UA 中间件:让每个请求都换个身份
打开middlewares.py,UA 中间件的逻辑是每次请求都从预置的 UA 池里随机取一个:
# middlewares.py 中的 UA 中间件示意 import random class RandomUserAgentMiddleware: def __init__(self): self.user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15', ] def process_request(self, request, spider): request.headers['User-Agent'] = random.choice(self.user_agents) return None参数说明:process_request是 Scrapy 中间件钩子函数,在请求发出前被调用;random.choice决定了每次请求的 UA 都不同。400 这个数值是中间件优先级,数值越小越早执行。UA 池里建议放 Chrome 和 Safari 各几个版本,防指纹识别的基本功。
3.3 代理中间件:给请求套上 IP 轮换机制
项目的代理设计是开放的——本身不带代理池,而是留了接口让你接入。ProxyMiddleware的process_request会去读取自定义的代理配置:
# middlewares.py 代理中间件示意 class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.settings.get('PROXY_SERVER') if proxy: request.meta['proxy'] = proxy return Nonerequest.meta['proxy']是 Scrapy 原生支持的代理传递方式,值可以是http://ip:port格式。项目支持设置代理,意味着你可以把PROXY_SERVER配成自己买的长效代理或自建代理池的入口。我在实际使用中会改成从池子里动态取 IP,而不是写死一个;如果被反爬盯上,IP 被封是家常便饭,代理是必须的。
3.4 手动登录与过验证:别想着用代码硬刚滑块
README 里明确写了「通过浏览器登录和过验证(手动)」,这是最省心也最稳的策略。Scrapy 本身不做浏览器渲染,滑块验证码的轨迹模拟如果靠代码硬刚,成功率很低还容易被封账号。正确姿势是:
# 先手动用浏览器登录大众点评,确保 Cookie 有效 # 然后把 Cookie 导出或手动填入请求头实际操作是抓取一次浏览器里的请求,把 Cookie 字符串复制出来,配置进DEFAULT_REQUEST_HEADERS或中间件里。Cookie 的失效周期大概是几天,失效后需要重新手动过验证。这个流程看起来不优雅,但它避免了大量无谓的技术对抗,血泪经验告诉我,能用人工解决的验证码问题,绝不上自动化。
3.5 settings 边界:哪些参数要按场景调整
DOWNLOAD_DELAY不是越小越好。大众点评对高频率请求的封禁策略是累计式的——短期高频触发滑块,长期高频封 IP。我的建议是刚开始跑用 3 秒,稳定跑 5 分钟没触发验证再把延迟降到 2 秒。CONCURRENT_REQUESTS默认 16,建议调到 4~6,多并发 + 高延迟的组合在反爬场景里更稳。代理这块,如果只跑少量数据可以不开,批量采集必须开。
4. items.py 字段设计与 pipelines.py 存储:数据从抓取到落盘全流程
4.1 四级爬虫的数据模型设计
大众点评的数据结构是层级嵌套的:城市 → 分类 → 店铺链接 → 店铺详情。items.py里定义了多个 Item 类,分别对应这四层:
# items.py import scrapy class CityItem(scrapy.Item): city_name = scrapy.Field() # 城市名称 city_pinyin = scrapy.Field() # 城市拼音 class CategoryItem(scrapy.Item): category_name = scrapy.Field() # 分类名称 category_id = scrapy.Field() # 分类编号 class ShopLinkItem(scrapy.Item): shop_url = scrapy.Field() # 店铺链接 class ShopInfoItem(scrapy.Item): shop_name = scrapy.Field() # 店铺名称 shop_address = scrapy.Field() # 店铺地址 city = scrapy.Field() # 所在城市 star = scrapy.Field() # 店铺星星 star_score = scrapy.Field() # 星星分数 review_count = scrapy.Field() # 评价数量 avg_price = scrapy.Field() # 消费价格 score = scrapy.Field() # 店铺评分 biz_area = scrapy.Field() # 热门商圈字段设计是数据质量的地基。city_pinyin用于后续 URL 拼接,category_id对应大众点评分类页的编号,biz_area是热门商圈名。注意这里的字段类型没有定义死,scrapy.Field()默认是 Any,但后续在 pipelines 里写 JSON 时需要注意类型统一。
4.2 数据保存链路:JsonPipeline 与 data 目录
爬虫跑到yield item后,数据会进入 pipelines 链。项目里pipelines.py实现了JsonPipeline,把每条 item 追加写入对应的 JSON 文件:
# pipelines.py 简写示意 import json from scrapy.exporters import JsonItemExporter class JsonPipeline: def open_spider(self, spider): self.files = {} self.exporters = {} def process_item(self, item, spider): name = item.__class__.__name__ if name not in self.exporters: f = open(f'data/{name}.json', 'wb') self.files[name] = f self.exporters[name] = JsonItemExporter(f, ensure_ascii=False) self.exporters[name].start_exporting() self.exporters[name].export_item(item) return item def close_spider(self, spider): for exporter in self.exporters.values(): exporter.finish_exporting() for f in self.files.values(): f.close()逻辑说明:用 item 的类名做文件区分,城市数据落到CityItem.json,店铺信息落到ShopInfoItem.json,避免多种数据混在一个文件里。JsonItemExporter是 Scrapy 内置的 JSON 导出器,ensure_ascii=False保证中文正常显示而不是变成\uXXXX转义。
4.3 爬取顺序:为什么先城市再分类再店铺
README 里强调的两条顺序规则是硬约束:「爬取店铺链接之前,必须先爬取城市和分类;爬取店铺信息之前,必须先爬取店铺链接」。原因很直接——大众点评的店铺链接 URL 长这样:http://www.dianping.com/shop/1234567,这个店铺编号只有从分类列表页才能提取到,而分类列表页的 URL 又依赖城市拼音和分类编号。整条链路是:
城市列表 → 城市拼音 + 分类编号 → 分类列表页 → 店铺链接(shop_id)→ 店铺详情页 → 结构化字段
项目里spiders/下应该有对应四个爬虫文件。我一般这样组织运行顺序:
scrapy crawl city # 第一步:拿城市列表 scrapy crawl category # 第二步:拿分类列表(依赖城市拼音) scrapy crawl shoplink # 第三步:从分类页提取店铺链接(依赖城市+分类) scrapy crawl shopinfo # 第四步:爬取店铺详情(依赖店铺链接)4.4 数据完整性检查:跑完后校验一下
爬虫跑完不要急着收工,先检查 data 目录下的文件大小和行数。比如城市列表如果只有几十条,而实际应该有三百多个城市,那就是解析出了问题。我的检查方式是:
wc -l data/*.json如果CityItem.json只有 30 行,大概率是提取规则写窄了,或者请求被重定向到了验证码页。这个习惯能帮你提前发现反爬被触发的问题,而不是等数据分析时才意识到数据是残缺的。
5. 避坑手册:字体反爬、验证码、链接失效与数据漂移
5.1 字体反爬:页面数据全变乱码
现象:页面用浏览器打开显示正常,但用 requests 或 Scrapy 抓下来,店铺评分、评价数这类数字全部变成奇怪的符号或乱码。
原因:大众点评使用动态字体(WOFF)做数字混淆。页面的 class 对应一个字体坐标,同一个数字在不同时间可能对应不同字形,直接按 HTML 文本提取必然出错。这是业界最成熟的反爬手段之一。
解决:项目utils/目录下的工具函数就是干这个的。基本流程是先下载.woff字体文件,用 Pillow 渲染出字形图片,然后和已知数字的字体库比对,建立字符映射表,再把页面里的乱码字符映射回真实数字。核心逻辑是:
# utils 中字体映射示意(伪代码级理解) font = TTFont('font.woff') glyph_map = {} # 对每个字形渲染成小图片,与标准数字图片做相似度比较 for glyph_name in font.getGlyphOrder(): if is_digit_glyph(glyph_name): image = render_glyph_to_image(font, glyph_name) digit = match_digit(image) # 匹配 0-9 glyph_map[glyph_name] = digit这套方法的思路是:字体文件虽然变化,但数字 0~9 的字形本质不变,只是映射到的 Unicode 编码变了,所以通过渲染比对拿到底层映射即可,别迷信网上某个固定的 CSS 映射表。
5.2 滑块验证码:Cookie 明明没过期为什么还弹
现象:手动登录成功后,跑了几百个请求,突然页面返回的不是数据而是滑块验证中心,之后所有请求都变成验证码页。
原因:频率触发了风控。大众点评的反爬是动态阈值的——短时间内的请求密度超过阈值就弹验证,和 Cookie 是否有效无关。这和直接用 Selenium 模拟点击滑块还不一样,即使 Cookie 有效,请求频率图谱也会出卖你。
解决:降低并发、增大延迟、开代理。具体操作是把DOWNLOAD_DELAY调到 3 秒以上,CONCURRENT_REQUESTS降到 4,并确保代理中间件已生效。另外重要一点:跑批量的时段尽量避开饭点等业务高峰时段,服务端风控在高峰时段更敏感。
5.3 店铺链接爬取不全:分类页有分页,你只爬了第一页
现象:ShopLinkItem.json里只有一千多条,而分类下实际有上万家店铺。
原因:分类列表页有分页机制,但翻页 URL 不是简单递增页码,部分页面带有pn参数和lat/lng地域参数,直接拼 URL 容易漏数据。
解决:检查你爬的页面源码里的翻页逻辑,大众点评的分类页通常通过># middlewares.py 改造示意:动态代理池 import random class DynamicProxyMiddleware: def __init__(self, proxy_api_url): self.proxy_api_url = proxy_api_url @classmethod def from_crawler(cls, crawler): return cls( proxy_api_url=crawler.settings.get('PROXY_API_URL') ) def process_request(self, request, spider): proxy = self.get_random_proxy() request.meta['proxy'] = proxy return None def get_random_proxy(self): # 从代理 API 拉取可用代理列表,简单随机取一个 import requests resp = requests.get(self.proxy_api_url, timeout=3) proxies = resp.json().get('data', []) return random.choice(proxies)
from_crawler是 Scrapy 中间件的标准入口,能从settings.py里读配置。get_random_proxy每次请求前向代理服务请求一个 IP,如果代理服务支持按需拨号,这套方案的实际效果是 IP 永不重复,风控很难定位。设置里只需要加一行PROXY_API_URL = 'http://your-proxy-service/api'。
验证方法可以这样:开两个会话,一个不带代理跑 100 个请求,一个带动态代理跑 100 个请求,观察日志里被重定向到验证码页的请求比例。正常情况下,不带代理的会话大概跑到 20 个请求就会触发滑块,带代理池的会话能坚持到 100 个以上才出现第一次风险提示。
另外一个容易被忽略的小技巧:在pipelines.py里把每个 item 的抓取时间字段加上,比如crawled_at = time.strftime('%Y-%m-%d %H:%M:%S')。这样既能追溯数据时效性,又能在数据质量分析时快速排查是不是用的旧数据。从那以后我每次拿完数据都会先跑一遍wc -l和python -m json.tool做完整性校验,强制校验一遍才放心入库存取,希望帮到你。
本文还有配套的精品资源,点击获取