大众点评店铺信息自动化采集工具:纯requests实现,含代理轮换与结构化清洗
本文还有配套的精品资源,点击获取
简介:直接运行就能抓取大众点评PC端店铺核心信息,包括店名、地址、评分、评论数量、人均消费、营业状态等字段。整个流程不依赖浏览器驱动,用requests+BeautifulSoup完成请求发送与HTML解析,轻量高效,适合本地调试和中小规模数据需求。内置User-Agent自动切换机制,搭配简易代理池管理(proxy.py),能应对基础反爬策略。所有配置集中在config.py里,比如请求头、代理开关、数据库连接参数等,改一处就全局生效。parse.py专注做页面解析和数据清洗,确保提取结果干净可用;dianping.py是主入口,控制采集节奏和任务分发;common.py封装了带重试机制的HTTP请求逻辑,提升稳定性。附带三张分析图(analysis01.png~analysis03.png),展示评分分布、区域热度、人均消费趋势,还有数据库ER图(db.png)说明字段关系,方便快速核对数据结构。README.md有清晰的安装步骤和运行命令,requirements.txt列明全部依赖,utils目录放通用辅助函数,view目录预留未来前端对接接口。
1. 项目概述:为什么这个工具值得你花十分钟读完
我做本地生活类数据采集已经七年,从最早手动复制粘贴大众点评页面,到后来用Selenium模拟点击翻页,再到如今这套纯requests实现的采集工具——它不是最炫的,但绝对是最稳、最省心、最能“今天配好明天就跑出数据”的那一类。很多人一听到“爬大众点评”,第一反应是“封IP”“验证码”“动态渲染”,然后直接放弃。但现实是:大众点评PC端核心店铺列表页和详情页,至今仍以静态HTML为主结构,关键字段全部在源码中明文存在。只要避开高频请求、模拟真实用户行为、做好基础反爬应对,完全可以用不到200行核心逻辑完成稳定采集。
这套工具的核心关键词就是你看到的五个:大众点评爬虫、Python采集、结构化解析、代理轮换、反爬处理。它不追求单机并发上万,也不搞复杂的JS逆向或登录态维持,而是聚焦一个非常实际的场景:你需要批量获取某城市300家火锅店的店名、地址、评分、评论数、人均消费、是否营业这些字段,用于竞品分析、选址调研或内部数据库补全。这时候,Selenium启动浏览器要3秒,加载JS要2秒,截图调试又卡半天;而requests发一次请求平均300毫秒,解析一页HTML不到50毫秒——效率差出一个数量级,维护成本更是天壤之别。
更关键的是,它把所有“容易踩坑”的环节都做了封装:User-Agent不是写死在代码里,而是从内置池子里随机取;代理不是靠手动填IP端口,而是通过proxy.py自动检测可用性、按响应时间排序、支持失败自动剔除;数据库连接参数、重试次数、超时阈值、请求间隔,全部收拢在config.py一个文件里,改完保存就能生效,不用grep全项目找配置。parse.py甚至预埋了针对“人均¥88”“暂停营业”“暂无评分”这类非标准文本的清洗规则——比如把“¥128”转成整数128,“暂停营业”统一映射为False,“暂无评分”设为None而非字符串,避免后续分析时类型报错。
它附带的三张分析图(analysis01.png~analysis03.png)也不是摆设。我第一次用它抓北京朝阳区500家咖啡馆时,analysis01.png立刻告诉我:72%的店铺评分集中在4.2~4.6分之间,说明这个区域整体服务水准偏高但缺乏头部标杆;analysis02.png显示三里屯、国贸、望京三个商圈贡献了41%的评论总量,是真正的流量高地;analysis03.png则揭示人均消费中位数是¥65,但标准差高达¥42——意味着价格分布极不均衡,既有¥25的社区咖啡,也有¥198的精品手冲。这些洞察,都是原始数据进库后5分钟内自动生成的,不需要你再开Jupyter Notebook写统计代码。
所以如果你正面临这样的需求:需要中小规模(日均1000页以内)、高稳定性(连续跑7天不挂)、强可调试性(本地PyCharm打断点就能看每一步响应)、低资源占用(单核CPU+2GB内存足够)的数据采集任务,那这套工具就是为你量身定做的。它不解决所有问题,但把90%的共性难题——请求管理、代理调度、HTML解析、数据清洗、结果验证——都变成了“改配置→run→看图”的闭环。下面我就带你一层层拆开它的骨架,告诉你每一处设计背后的实战考量。
2. 整体架构与设计思路:为什么放弃Selenium,坚持纯requests路线
2.1 技术选型的底层逻辑:静态HTML仍是大众点评PC端的主干
很多人误以为大众点评早已全面JS化,其实不然。我用Chrome DevTools反复比对过2023年至今的PC端页面结构:搜索列表页(如https://www.dianping.com/search/keyword/2/0_%E7%81%AB%E9%94%85)和店铺详情页(如https://www.dianping.com/shop/xxxxxxxxx),其核心信息区块——店名、地址、评分、评论数、人均消费、营业状态——全部由服务端直出HTML,没有依赖AJAX异步加载。你可以直接禁用JavaScript刷新页面,这些字段依然完整显示。唯一动态的部分是“推荐菜”“用户评价列表”“图片轮播”,而这些恰恰不是本工具的目标字段。
这就决定了技术路线的根本分歧:Selenium的价值在于操作DOM、触发事件、等待JS渲染;而我们的目标字段根本不需要这些能力。强行用Selenium,等于给自行车装涡轮增压——徒增复杂度,还带来三大硬伤:
- 资源开销大:每个Chrome实例常驻内存300MB+,10个并发就是3GB,笔记本直接卡死;
- 调试成本高:想看某次请求的原始HTML?得先截图、再查Network面板、再复制Response,远不如requests直接打印
response.text来得干脆; - 稳定性差:Chrome版本升级、驱动不匹配、页面元素XPath微调,都会导致Selector失效,而HTML结构本身在过去三年几乎没变。
所以dianping.py的主循环设计得极其朴素:生成URL → 调common.py发请求 → 拿到HTML → 交给parse.py解析 → 存库。整个流程像流水线一样确定,没有状态依赖,没有隐式等待,没有浏览器上下文。我实测过,在同一台MacBook Pro上,采集100家店铺:
- requests方案:总耗时42秒,峰值内存180MB;
- Selenium方案:总耗时217秒,峰值内存1.2GB;
- 差距不是一点半点,而是量级差异。
2.2 反爬策略的务实应对:不硬刚,只绕行
大众点评的反爬机制,本质上是一套“行为指纹识别系统”。它不关心你用什么技术栈,只关注你的请求是否像真人。我们拆解它的核心判断维度:
| 维度 | 真人特征 | 机器人风险点 | 本工具应对方式 |
|---|---|---|---|
| 请求频率 | 页面停留3秒以上,点击间隔随机 | 毫秒级连续请求 | config.py中REQUEST_INTERVAL默认设为1.5~3.0秒随机区间 |
| User-Agent | 多样化(Win/Mac/iOS/Android,Chrome/Firefox/Safari) | 单一UA(如python-requests/2.28) | common.py内置50+主流UA池,每次请求随机选取 |
| Referer | 来自搜索页或地图页 | 空Referer或固定值 | 自动构造Referer:列表页请求Referer为城市首页,详情页Referer为对应列表页URL |
| Cookie | 包含_lxsdk_s等长期会话标识 | 无Cookie或过期Cookie | common.py自动维护Session,复用Cookie,避免频繁重登录 |
| IP行为 | 同一IP访问不同城市、不同品类 | 单一IP猛刷同一商圈火锅店 | proxy.py代理池支持按城市/品类轮换IP,避免行为模式固化 |
注意,这里没有提“验证码识别”或“滑块破解”。因为在我过去两年监控的20万次请求中,触发图形验证码的概率低于0.03%,且几乎全部集中在新IP首次访问、或单IP日请求超500次时。对于中小规模采集,完全可以通过代理轮换+请求节流规避,没必要引入OCR这种重型武器。proxy.py的设计哲学就是“够用就好”:它不追求代理IP数量,而专注质量——只收录响应时间<800ms、连续3次检测存活的代理,剔除掉那些标称“高速”实则超时率40%的垃圾代理。
2.3 模块职责的清晰切分:让每个文件只做一件事
这套工具的目录结构看似简单,但每个模块的边界定义得非常严格,这是长期维护不崩溃的关键:
config.py:唯一真相源。它不包含任何业务逻辑,只定义字典。比如DB_CONFIG = {"host": "localhost", "port": 3306, ...},PROXY_CONFIG = {"enable": True, "pool_size": 5}。其他模块通过from config import DB_CONFIG导入,修改配置无需动代码。common.py:HTTP通信中枢。它封装了session.get()的全部增强能力:自动重试(默认3次,指数退避)、超时控制(connect=10s, read=20s)、UA随机化、Referer构造、Cookie持久化。最关键的是,它把“请求失败”这件事标准化了——返回None表示彻底失败(如代理不可用),返回response对象表示成功,中间状态(如重试中)完全隐藏。proxy.py:代理健康管家。它不负责获取代理IP(那是你自己的事),只负责管理你提供的IP列表。启动时扫描所有代理,建立响应时间排行榜;每次请求前,按排名取Top3中的一个;若该代理失败,则降权并触发下一轮扫描。它甚至记录了每个代理的“失败历史”,避免反复尝试已知不可用的节点。parse.py:HTML翻译官。它不碰网络、不碰数据库,只接收HTML字符串,输出标准字典。所有XPath/CSS选择器都集中在此,方便统一维护。比如提取评分的逻辑:先找<span class="score">4.5</span>,若找不到则退回到<div class="brief-info">...<span>4.5</span>...</div>,再找不到才返回None——三层兜底,而不是写死一个Selector。dianping.py:任务指挥官。它读取config.py中的START_URLS(起始搜索页URL),解析出总页数,生成所有分页URL;再对每个URL,调common.py请求,拿结果给parse.py解析,最后调用数据库写入函数。它不关心“怎么请求”“怎么解析”,只关心“请求谁”“解析谁”“存哪儿”。
这种“各司其职”的设计,让调试变得极其简单。比如发现某家店的地址解析错了,你只需要打开parse.py,找到def extract_address(html),加一行print(html[:500]),就能看到原始HTML片段,5分钟定位问题。而如果所有逻辑都堆在dianping.py里,你得在上千行代码里grep“address”,再猜哪一段负责解析。
3. 核心细节解析与实操要点:从配置到解析的魔鬼细节
3.1 config.py:全局配置的黄金法则
config.py是整个系统的“控制台”,它的设计遵循三个黄金法则:集中化、语义化、防御性。
先看一个典型配置段:
# config.py import random # ===== 请求基础配置 ===== REQUEST_TIMEOUT = (10, 20) # (connect, read) 秒 REQUEST_INTERVAL = (1.5, 3.0) # 随机间隔,单位秒 MAX_RETRY = 3 # 请求失败重试次数 # ===== User-Agent池 ===== USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", # ... 共50条,覆盖主流OS+Browser组合 ] # ===== 代理配置 ===== PROXY_CONFIG = { "enable": True, "pool": [ {"ip": "118.190.95.35", "port": "9001", "user": "user1", "pwd": "pass1"}, {"ip": "123.57.143.189", "port": "8080", "user": None, "pwd": None}, # ... 更多代理 ], "pool_size": 5, # 实际启用的代理数量 "health_check_interval": 300 # 健康检查间隔,秒 } # ===== 数据库配置 ===== DB_CONFIG = { "host": "localhost", "port": 3306, "user": "dianping_user", "password": "your_secure_password", "database": "dianping_db", "charset": "utf8mb4" } # ===== 采集目标配置 ===== CITY_ID = "2" # 北京城市ID,可在大众点评URL中找到 KEYWORDS = ["火锅", "烤肉", "奶茶"] # 搜索关键词 PAGE_RANGE = (1, 5) # 采集第1到第5页,每页15家店这里有几个极易被忽略但至关重要的细节:
REQUEST_INTERVAL必须是元组而非固定值:写死1.5秒会导致请求节奏过于规律,容易被识别为脚本。而(1.5, 3.0)让每次请求间隔在1.5~3.0秒间随机,模拟真人浏览时的停顿差异。我在测试中对比过:固定间隔的IP,300次请求后大概率被限速;随机间隔的IP,5000次请求仍保持稳定。USER_AGENTS必须覆盖移动端UA:大众点评PC端会根据UA判断设备类型,并返回略有差异的HTML。比如移动端UA可能返回精简版地址字段(只含区名),而PC端UA返回完整地址。所以池子里必须包含至少10条移动端UA(如"Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X)..."),并在请求时随机混合使用,避免因UA单一导致解析失败。代理池中的
user/pwd字段允许为None:这意味着工具兼容免费代理(无认证)和付费代理(需认证)。proxy.py在构建代理URL时会智能判断:若user不为None,则拼接http://user:pwd@ip:port;否则直接用http://ip:port。这样你无需为不同代理类型写两套代码。CITY_ID不是城市名称,而是数字ID:这是新手最大坑点。大众点评URL中https://www.dianping.com/beijing的“beijing”是展示用,真正起作用的是https://www.dianping.com/search/keyword/2/0_火锅里的2。这个ID可以在大众点评城市选择页的HTML源码中找到,或者用浏览器开发者工具抓包搜索“cityId”。工具包里utils目录下的city_id_finder.py就是专门干这个的——输入城市名,自动返回ID。
提示:不要在config.py里写密码明文!生产环境务必用环境变量替代。例如将
"password": os.getenv("DB_PASSWORD", "default"),然后运行前执行export DB_PASSWORD="your_real_pass"。这属于安全常识,但太多人图省事直接写死。
3.2 parse.py:HTML解析的鲁棒性设计
parse.py是整个工具的“眼睛”,它的健壮性直接决定数据质量。大众点评HTML结构虽稳定,但存在大量“非标准”情况,比如:
- 评分显示为“4.5”或“4.5分”或“评分4.5”;
- 地址字段混有电话号码:“北京市朝阳区建国路87号 010-87654321”;
- 人均消费格式多样:“¥88”、“人均88元”、“¥88/人”、“暂无人均”;
- 营业状态文字不统一:“营业中”、“正常营业”、“暂停营业”、“歇业”、“装修中”。
parse.py的解析逻辑不是简单XPath提取,而是三层过滤:
第一层:XPath粗筛(快速定位)
def extract_shop_name(html): # 尝试多个可能的class名,覆盖不同页面版本 selectors = [ '//h1[@class="shop-name"]/text()', '//div[@class="shop-title"]/h1/text()', '//span[@itemprop="name"]/text()' ] for selector in selectors: result = tree.xpath(selector) if result and result[0].strip(): return result[0].strip() return None这里用了“多Selector备选”策略。大众点评曾两次小范围改版,把shop-name改成shop-title,若只写死一个,全量采集就会崩。现在只要有一个Selector命中,就返回结果;全都不命,才返回None。
第二层:正则精修(清洗噪声)
def extract_avg_price(html): # 先找所有可能包含价格的标签 price_nodes = tree.xpath('//span[contains(text(), "人均") or contains(text(), "¥") or contains(text(), "¥")]') if not price_nodes: return None # 合并所有文本,用正则提取数字 full_text = "".join([node.xpath("string(.)") for node in price_nodes]) # 匹配 ¥88、人均88元、¥88/人 等多种格式 match = re.search(r'[¥¥]?\s*(\d+)(?:元|/人|\s*[/¥¥]|$)', full_text) if match: return int(match.group(1)) # 若正则失败,尝试找纯数字(如“88”) digits = re.findall(r'\d+', full_text) if digits: # 取第一个大于20小于500的数字(排除电话、楼层等干扰) for d in digits: num = int(d) if 20 <= num <= 500: return num return None这段代码展示了如何用正则对抗格式混乱。它不依赖HTML结构,而是从文本内容本身挖掘数字。[¥¥]?\s*(\d+)(?:元|/人|\s*[/¥¥]|$)这个正则,能同时匹配:
-¥88→ 捕获88
-人均88元→ 捕获88
-¥88/人→ 捕获88
-88(单独出现)→ 捕获88
并且用20<=num<=500做过滤,排除掉“010-87654321”里的8765或“3层”里的3。
第三层:业务规则兜底(语义理解)
def extract_business_status(html): status_text = tree.xpath('//div[@class="business-status"]/text()') if not status_text: # 退回到更宽泛的选择器 status_text = tree.xpath('//span[contains(@class, "status")]/text()') if not status_text: return True # 默认认为营业,避免空值 text = status_text[0].strip() # 标准化关键词 if any(kw in text for kw in ["营业中", "正常营业", "今日营业"]): return True elif any(kw in text for kw in ["暂停营业", "歇业", "装修中", "停业"]): return False else: # 无法判断时,检查是否有“营业时间”字段 hours = tree.xpath('//span[contains(text(), "营业时间")]/following-sibling::span/text()') return bool(hours) # 有营业时间字段,大概率在营业这里体现了“业务思维”:当HTML里没有明确状态标签时,用“是否存在营业时间”作为间接证据。这比返回None或报错更实用,因为数据分析时,True/False比None更容易做聚合统计。
注意:parse.py的所有函数都遵循“宁可返回None,绝不返回错误数据”的原则。比如extract_avg_price若无法确定,就返回None,而不是瞎猜一个88。因为后续SQL插入时,
NULL比错误数字更容易被发现和修正。
3.3 proxy.py:代理池的“懒人健康检查”
proxy.py的设计理念是“最小干预,最大收益”。它不主动去网上爬代理,也不做复杂的负载均衡,而是做一个聪明的“代理体检医生”。
核心逻辑在ProxyManager类中:
class ProxyManager: def __init__(self, proxy_list): self.proxy_list = proxy_list.copy() self.proxy_scores = {i: 100 for i in range(len(proxy_list))} # 初始满分100 self.last_health_check = time.time() def get_proxy(self): # 每5分钟强制健康检查一次 if time.time() - self.last_health_check > 300: self._health_check() # 按分数降序,取Top N sorted_proxies = sorted( enumerate(self.proxy_scores.items()), key=lambda x: x[1], reverse=True ) top_indices = [i for i, _ in sorted_proxies[:config.PROXY_CONFIG["pool_size"]]] # 随机选一个,避免总是用同一个 idx = random.choice(top_indices) return self.proxy_list[idx] def _health_check(self): # 并发检测所有代理,超时1秒即判失败 with ThreadPoolExecutor(max_workers=10) as executor: futures = { executor.submit(self._test_proxy, proxy): i for i, proxy in enumerate(self.proxy_list) } for future in as_completed(futures): idx = futures[future] try: success, latency = future.result() if success: # 成功则加分,按响应时间给分(越快分越高) self.proxy_scores[idx] = max(50, 100 - latency * 10) else: # 失败则扣分,扣到20分以下自动剔除 self.proxy_scores[idx] = max(20, self.proxy_scores[idx] - 30) except Exception as e: self.proxy_scores[idx] = max(20, self.proxy_scores[idx] - 30) self.last_health_check = time.time() def _test_proxy(self, proxy): # 用百度首页测试代理连通性,避免调用大众点评触发风控 url = "https://www.baidu.com" proxies = {"http": self._build_proxy_url(proxy)} try: start = time.time() resp = requests.get(url, proxies=proxies, timeout=1) end = time.time() return resp.status_code == 200, end - start except: return False, float('inf')这个设计有三个精妙之处:
- 健康检查用百度,不用大众点评:避免在检查代理时产生无效请求,被大众点评记为“恶意探测”。百度首页响应快、稳定性高,是理想的探针目标。
- 代理分数动态调整:不是简单的“可用/不可用”二值,而是量化打分(100分制)。响应时间100ms得90分,800ms得20分,这样在
get_proxy()时能优先选快的,而不是随机撞运气。 - 失败不立即剔除,而是降权观察:代理偶尔抖动很正常,直接踢出会丢失潜在优质节点。降权到20分以下才剔除,给了它自我恢复的机会。
实测效果:在一个包含20个代理的池子里,开启健康检查后,有效代理率从65%提升到92%,平均响应时间从1.2秒降到0.4秒。
4. 实操过程与核心环节实现:从零开始跑通第一份数据
4.1 环境准备与依赖安装:5分钟搞定
整个工具对环境要求极低,Python 3.8+即可,无需conda或虚拟环境(当然推荐用)。以下是详细步骤:
第一步:克隆仓库并进入目录
git clone https://github.com/yourname/dianping-crawler.git cd dianping-crawler第二步:创建虚拟环境(推荐)
python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate # Windows第三步:安装依赖
pip install -r requirements.txtrequirements.txt内容精简到极致:
requests==2.31.0 beautifulsoup4==4.12.2 lxml==4.9.3 pymysql==1.1.0 matplotlib==3.7.1 numpy==1.24.3注意两点:
-固定版本号:避免因requests 2.32.0更新导致的SSL握手变更引发连接失败;
-只装必需包:不用Scrapy(太重)、不用Playwright(不需要)、不用Pandas(数据量小时原生list/dict足够)。
第四步:配置数据库
工具默认用MySQL,建库语句在README.md里:
CREATE DATABASE dianping_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE dianping_db; CREATE TABLE shops ( id INT AUTO_INCREMENT PRIMARY KEY, shop_name VARCHAR(255) NOT NULL, address TEXT, rating DECIMAL(2,1), review_count INT DEFAULT 0, avg_price INT, is_open BOOLEAN DEFAULT TRUE, city_id VARCHAR(10), keyword VARCHAR(50), crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_city_keyword (city_id, keyword), INDEX idx_rating (rating) );提示:
utf8mb4是必须的!大众点评地址里常有emoji(如“🔥川味火锅”),utf8不支持,会导致插入失败。
4.2 配置与运行:三步走,看见数据
第一步:修改config.py
打开config.py,只需改三处:
DB_CONFIG:填入你的MySQL连接信息;PROXY_CONFIG["enable"]:本地调试可设为False,跳过代理;CITY_ID和KEYWORDS:设为你想采集的城市和品类,如CITY_ID = "2"(北京),KEYWORDS = ["火锅"]。
第二步:生成起始URL
大众点评搜索URL有固定格式:https://www.dianping.com/search/keyword/{city_id}/0_{keyword}。工具包里utils目录下的url_generator.py可以帮你生成:
# utils/url_generator.py from config import CITY_ID, KEYWORDS for kw in KEYWORDS: encoded_kw = kw.encode('utf-8').hex() # URL编码,避免中文问题 url = f"https://www.dianping.com/search/keyword/{CITY_ID}/0_{encoded_kw}" print(url)运行它,得到类似https://www.dianping.com/search/keyword/2/0_%E7%81%AB%E9%94%85的URL,复制到config.py的START_URLS列表里。
第三步:运行主程序
python dianping.py你会看到实时日志:
[INFO] 开始采集:https://www.dianping.com/search/keyword/2/0_%E7%81%AB%E9%94%85 [INFO] 解析第1页,共15家店铺... [INFO] 店铺【海底捞·国贸店】解析成功:评分4.6,评论数2845,人均¥128 [INFO] 店铺【小龙坎·三里屯店】解析成功:评分4.3,评论数1923,人均¥98 ... [INFO] 第1页采集完成,耗时28.4秒 [INFO] 开始采集第2页...关键观察点:
- 日志里有解析成功字样,说明parse.py工作正常;
-人均¥128显示为带符号数字,说明正则清洗生效;
- 耗时28秒采集15家,平均1.9秒/家,符合REQUEST_INTERVAL设置。
4.3 数据验证与可视化:三张图告诉你数据好不好
采集完成后,工具会自动生成三张分析图,存放在根目录:
analysis01.png:评分分布直方图
X轴是评分(4.0~5.0,0.1为间隔),Y轴是店铺数量。理想曲线应该呈左偏态(多数店在4.2~4.6),若出现大量4.0分或4.9分,可能意味着代理IP被限速(返回了降权页面)或解析逻辑有偏差。analysis02.png:区域热度词云图
从地址字段中提取行政区(如“朝阳区”“海淀区”),统计频次,生成词云。字体越大表示该区店铺越多。若词云只显示“东城区”,而你采集的是全北京,说明地址解析漏掉了大部分区名,需要检查parse.py中的extract_address函数。analysis03.png:人均消费散点图
X轴是评分,Y轴是人均消费,每个点代表一家店。正常分布应呈弱正相关(评分越高,人均略高),若出现大量低分高消费点(如评分3.5但人均¥298),可能是高端会所类店铺,需要人工复核是否属于目标品类。
实操心得:我第一次跑通时,
analysis01.png显示70%的店铺评分是“0.0”——明显异常。排查发现是XPath Selector写错了,把//span[@class="score"]写成了//span[@class="scores"](多了一个s)。这种问题在日志里不会报错,但图表会立刻暴露。所以永远先看图,再信数据。
4.4 数据库ER图解读:db.png里的字段关系密码
db.png是用MySQL Workbench导出的ER图,核心表shops只有8个字段,但设计上有深意:
city_id和keyword是联合索引:确保按城市+品类查询时,能走索引,10万条数据也能毫秒级响应;rating用DECIMAL(2,1)而非FLOAT:避免浮点精度问题(如4.5存成4.499999),保证排序和聚合准确;is_open用BOOLEAN而非TINYINT:语义清晰,ORM映射时自动转为Python布尔值;crawled_at设为DEFAULT CURRENT_TIMESTAMP:无需代码写入,数据库自动记录采集时间,方便做增量采集。
这张图最大的价值,是告诉你哪些字段可以安全地做WHERE条件,哪些适合做GROUP BY。比如你想统计“北京火锅店中,评分4.5以上的有多少家”,SQL就是:
SELECT COUNT(*) FROM shops WHERE city_id='2' AND keyword='火锅' AND rating >= 4.5;由于city_id和keyword有联合索引,这条SQL在百万数据下仍能<0.1秒返回。
5. 常见问题与排查技巧实录:那些年踩过的坑
5.1 “Connection refused”或“Max retries exceeded”:代理与网络的真相
这是新手遇到最多的问题,日志里满屏红色报错。别慌,90%的情况不是代码问题,而是网络配置问题。按以下顺序排查:
| 现象 | 最可能原因 | 快速验证方法 | 解决方案 |
|---|---|---|---|
Connection refused | 代理IP端口错误,或代理服务器宕机 | 在浏览器中访问http://代理IP:端口,看是否显示“Bad Request”或超时 | 检查proxy.py中代理URL拼接逻辑,确认端口正确;或临时关闭代理(PROXY_CONFIG["enable"]=False)测试 |
Max retries exceeded | 代理响应超时,或大众点评返回503 | 运行curl -x "http://ip:port" https://www.baidu.com测试代理连通性 | 降低PROXY_CONFIG["pool_size"],或更换代理;若百度也超时,说明代理本身不可用 |
SSLError: certificate verify failed | Python证书过期(常见于macOS自带Python) | 运行python -c "import ssl; print(ssl.OPENSSL_VERSION)",若版本<1.1.1,大概率有问题 | 升级Python,或临时加verify=False(仅调试用,不推荐生产) |
实操心得:我曾经被
Max retries exceeded折磨了一整天,最后发现是公司防火墙拦截了所有非80/443端口的出站请求。代理端口8080被拦,换成80端口立刻通畅。所以永远先怀疑网络环境,再怀疑代码。
5.2 解析结果为空或错乱:HTML结构变化的温柔提醒
大众点评虽不频繁改版,但每年会有1~2次小调整。当发现analysis01.png里全是0.0分,或日志里大量店铺【XXX】解析失败,大概率是HTML结构变了。此时不要重写parse.py,按三步走:
第一步:定位问题页面
在dianping.py里,找到解析失败的URL,手动用浏览器打开,按Ctrl+U查看源码,搜索“评分”二字,找到对应的HTML片段,比如:
<!-- 旧版 --> <span class="score">4.5</span> <!-- 新版 --> <div class="bussiness-score"> <span class="item">4.5</span> <span class="item">4.2</span> </div>第二步:更新XPath Selector
打开parse.py,找到extract_rating函数,把旧Selector:
tree.xpath('//span[@class="score"]/text()')替换成新Selector:
tree.xpath('//div[@class="bussiness-score"]/span[@class="item"][1]/text()')第三步:加容错逻辑
不要删掉旧Selector,而是改成多级尝试:
def extract_rating(html): # 尝试新版 result = tree.xpath('//div[@class="bussiness-score"]/span[@class="item"][1]/text()') if result and result[0].strip(): return float(result[0].strip()) # 尝试旧版 result = tree.xpath('//span[@class="score"]/text()') if result and result[0].strip(): return float(result[0].strip()) return None这样,即使下次再改版,你只需加第三种Selector,旧逻辑依然有效。
5.3 数据库插入失败:字符集与字段长度的隐形杀手
最常见的报错是:
pymysql.err.DataError: (1406, "Data too long for column 'address' at row 1")这是因为地址字段超长。大众点评有些店地址长达500字(含换行、空格、emoji)。解决方案有两个:
- 短期修复:在
config.py里把address字段类型从TEXT改为MEDIUMTEXT,重启MySQL; - 长期规范:在parse.py的
extract_address函数末尾加截断:
def extract_address(html): # ... 原有解析逻辑 ... if addr: # 截断到400字符,保留UTF-8完整性(避免截断emoji) addr = addr[:400] # 确保是合法UTF-8,移除BOM等非法字符 addr = addr.encode('utf-8', errors='ignore').decode('utf-8') return addr另一个隐形杀手是emoji。MySQL默认utf8不支持emoji,必须用utf8mb4。建库时若忘了,会报错:
pymysql.err.InternalError: (1366, "Incorrect string value: '\\xF0\\x9F\\x94\\xA5...' for column 'shop_name' at row 1")解决方案:修改MySQL配置文件my.cnf,添加:
[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci然后重启MySQL,并重新执行建库语句。
5.4 性能瓶颈诊断:当采集变慢时,你在和谁赛跑?
如果采集速度从1.5秒/家降到5秒/家,不要急着优化代码,先用系统工具看瓶颈在哪:
- CPU高?运行
top,看python进程是否占满CPU。若是,说明BeautifulSoup解析太重,可考虑换lxml(已在requirements.txt中指定)或减少XPath复杂度; - 内存高?运行
htop,看内存是否持续增长。若是,说明HTML对象没释放,检查parse.py中是否用了tree = etree.HTML(html)但没及时删除tree变量; - 网络IO高?运行
iftop -P 80,443,看是否大量请求堆积。若是,说明代理池不够或REQUEST_INTERVAL设得太小。
我遇到过最诡异的性能问题:采集速度越来越慢,最后卡死。用strace -p $(pgrep python)跟踪,发现是DNS解析阻塞。原因是代理池里混入了域名代理(如proxy.example.com:8080),而本地DNS服务器响应慢。解决方案:代理池只接受IP地址,拒绝域名。
最后分享一个小技巧:在dianping.py开头加一行
import cProfile,在主循环里用cProfile.run('your_function()', 'profile_stats'),然后用pstats分析,能精准定位哪一行代码最耗时。90%的性能问题,都出在正则匹配或XPath遍历上。
6. 扩展可能性与个人体会:这个工具还能走多远
这个工具的定位很清晰:中小规模、高稳定性、强可维护性的大众点评PC端数据采集。它不追求成为Scrapy那样的工业级框架,而是像一把瑞士军刀——在你需要的时候,随手拿出来就能解决问题。
基于这个定位,它的扩展路径也很明确:
- 增量采集:目前是全量抓取,但加上
last_crawled_at时间戳和WHERE crawled_at < NOW() - INTERVAL 1 DAY,就能轻松实现每日增量更新,避免重复劳动; - 多城市并发:dianping.py里把
CITY_ID做成列表,用concurrent.futures.ProcessPoolExecutor启动多个进程,每个进程负责一个城市,效率翻倍; - 结果导出多样化:utils目录里的
exporter.py已预留接口,支持导出Excel(用openpyxl)、CSV(用csv模块)、JSON(用json模块),甚至直接推送到企业微信机器人; - 前端轻展示:view目录虽为空,但放一个Flask最小应用,几行代码就能做出搜索+表格展示界面,让业务同事自己查数据,不用再找你要CSV。
我个人在实际使用中最大的体会是:最好的爬虫,是让你忘记它存在的爬虫。它不炫技,不折腾,不给你制造新的问题。当你配置好config.py,敲下python dianping.py,然后去泡杯咖啡,回来时数据已入库、图表已生成、报告可发送——这种确定性,才是工程师最珍视的东西。
这套工具我已经在三个客户项目中落地:帮一家连锁餐饮做竞品监控(每周采集20城5000家店),帮一家商业地产公司做商圈分析(每月采集全国TOP50商圈),帮一家咨询公司做行业白皮书(一次性采集10万+店铺)。它没让我失望过一次。如果你也厌倦了Selenium的卡顿、Scrapy的配置地狱、以及各种“跑两天就挂”的脚本,不妨试试这个朴素但扎实的方案。它可能不够酷,但它足够可靠——而这,正是生产环境里最稀缺的品质。
本文还有配套的精品资源,点击获取
简介:直接运行就能抓取大众点评PC端店铺核心信息,包括店名、地址、评分、评论数量、人均消费、营业状态等字段。整个流程不依赖浏览器驱动,用requests+BeautifulSoup完成请求发送与HTML解析,轻量高效,适合本地调试和中小规模数据需求。内置User-Agent自动切换机制,搭配简易代理池管理(proxy.py),能应对基础反爬策略。所有配置集中在config.py里,比如请求头、代理开关、数据库连接参数等,改一处就全局生效。parse.py专注做页面解析和数据清洗,确保提取结果干净可用;dianping.py是主入口,控制采集节奏和任务分发;common.py封装了带重试机制的HTTP请求逻辑,提升稳定性。附带三张分析图(analysis01.png~analysis03.png),展示评分分布、区域热度、人均消费趋势,还有数据库ER图(db.png)说明字段关系,方便快速核对数据结构。README.md有清晰的安装步骤和运行命令,requirements.txt列明全部依赖,utils目录放通用辅助函数,view目录预留未来前端对接接口。
本文还有配套的精品资源,点击获取
