
本文还有配套的精品资源点击获取简介直接运行就能抓取大众点评PC端店铺核心信息包括店名、地址、评分、评论数量、人均消费、营业状态等字段。整个流程不依赖浏览器驱动用requestsBeautifulSoup完成请求发送与HTML解析轻量高效适合本地调试和中小规模数据需求。内置User-Agent自动切换机制搭配简易代理池管理proxy.py能应对基础反爬策略。所有配置集中在config.py里比如请求头、代理开关、数据库连接参数等改一处就全局生效。parse.py专注做页面解析和数据清洗确保提取结果干净可用dianping.py是主入口控制采集节奏和任务分发common.py封装了带重试机制的HTTP请求逻辑提升稳定性。附带三张分析图analysis01.pnganalysis03.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.pnganalysis03.png也不是摆设。我第一次用它抓北京朝阳区500家咖啡馆时analysis01.png立刻告诉我72%的店铺评分集中在4.24.6分之间说明这个区域整体服务水准偏高但缺乏头部标杆analysis02.png显示三里屯、国贸、望京三个商圈贡献了41%的评论总量是真正的流量高地analysis03.png则揭示人均消费中位数是¥65但标准差高达¥42——意味着价格分布极不均衡既有¥25的社区咖啡也有¥198的精品手冲。这些洞察都是原始数据进库后5分钟内自动生成的不需要你再开Jupyter Notebook写统计代码。所以如果你正面临这样的需求需要中小规模日均1000页以内、高稳定性连续跑7天不挂、强可调试性本地PyCharm打断点就能看每一步响应、低资源占用单核CPU2GB内存足够的数据采集任务那这套工具就是为你量身定做的。它不解决所有问题但把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实例常驻内存300MB10个并发就是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.53.0秒随机区间User-Agent多样化Win/Mac/iOS/AndroidChrome/Firefox/Safari单一UA如python-requests/2.28common.py内置50主流UA池每次请求随机选取Referer来自搜索页或地图页空Referer或固定值自动构造Referer列表页请求Referer为城市首页详情页Referer为对应列表页URLCookie包含_lxsdk_s等长期会话标识无Cookie或过期Cookiecommon.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.pyHTTP通信中枢。它封装了session.get()的全部增强能力自动重试默认3次指数退避、超时控制connect10s, read20s、UA随机化、Referer构造、Cookie持久化。最关键的是它把“请求失败”这件事标准化了——返回None表示彻底失败如代理不可用返回response对象表示成功中间状态如重试中完全隐藏。proxy.py代理健康管家。它不负责获取代理IP那是你自己的事只负责管理你提供的IP列表。启动时扫描所有代理建立响应时间排行榜每次请求前按排名取Top3中的一个若该代理失败则降权并触发下一轮扫描。它甚至记录了每个代理的“失败历史”避免反复尝试已知不可用的节点。parse.pyHTML翻译官。它不碰网络、不碰数据库只接收HTML字符串输出标准字典。所有XPath/CSS选择器都集中在此方便统一维护。比如提取评分的逻辑先找span classscore4.5/span若找不到则退回到div classbrief-info...span4.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条覆盖主流OSBrowser组合 ] # 代理配置 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.53.0秒间随机模拟真人浏览时的停顿差异。我在测试中对比过固定间隔的IP300次请求后大概率被限速随机间隔的IP5000次请求仍保持稳定。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:pwdip: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_PASSWORDyour_real_pass。这属于安全常识但太多人图省事直接写死。3.2 parse.pyHTML解析的鲁棒性设计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[classshop-name]/text(), //div[classshop-title]/h1/text(), //span[itempropname]/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并且用20num500做过滤排除掉“010-87654321”里的8765或“3层”里的3。第三层业务规则兜底语义理解def extract_business_status(html): status_text tree.xpath(//div[classbusiness-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()), keylambda x: x[1], reverseTrue ) 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_workers10) 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, proxiesproxies, timeout1) 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内容精简到极致requests2.31.0 beautifulsoup44.12.2 lxml4.9.3 pymysql1.1.0 matplotlib3.7.1 numpy1.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 fhttps://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.05.00.1为间隔Y轴是店铺数量。理想曲线应该呈左偏态多数店在4.24.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[classscore]写成了//span[classscores]多了一个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_id2 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 failedPython证书过期常见于macOS自带Python运行python -c import ssl; print(ssl.OPENSSL_VERSION)若版本1.1.1大概率有问题升级Python或临时加verifyFalse仅调试用不推荐生产实操心得我曾经被Max retries exceeded折磨了一整天最后发现是公司防火墙拦截了所有非80/443端口的出站请求。代理端口8080被拦换成80端口立刻通畅。所以永远先怀疑网络环境再怀疑代码。5.2 解析结果为空或错乱HTML结构变化的温柔提醒大众点评虽不频繁改版但每年会有12次小调整。当发现analysis01.png里全是0.0分或日志里大量店铺【XXX】解析失败大概率是HTML结构变了。此时不要重写parse.py按三步走第一步定位问题页面在dianping.py里找到解析失败的URL手动用浏览器打开按CtrlU查看源码搜索“评分”二字找到对应的HTML片段比如!-- 旧版 -- span classscore4.5/span !-- 新版 -- div classbussiness-score span classitem4.5/span span classitem4.2/span /div第二步更新XPath Selector打开parse.py找到extract_rating函数把旧Selectortree.xpath(//span[classscore]/text())替换成新Selectortree.xpath(//div[classbussiness-score]/span[classitem][1]/text())第三步加容错逻辑不要删掉旧Selector而是改成多级尝试def extract_rating(html): # 尝试新版 result tree.xpath(//div[classbussiness-score]/span[classitem][1]/text()) if result and result[0].strip(): return float(result[0].strip()) # 尝试旧版 result tree.xpath(//span[classscore]/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, errorsignore).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端店铺核心信息包括店名、地址、评分、评论数量、人均消费、营业状态等字段。整个流程不依赖浏览器驱动用requestsBeautifulSoup完成请求发送与HTML解析轻量高效适合本地调试和中小规模数据需求。内置User-Agent自动切换机制搭配简易代理池管理proxy.py能应对基础反爬策略。所有配置集中在config.py里比如请求头、代理开关、数据库连接参数等改一处就全局生效。parse.py专注做页面解析和数据清洗确保提取结果干净可用dianping.py是主入口控制采集节奏和任务分发common.py封装了带重试机制的HTTP请求逻辑提升稳定性。附带三张分析图analysis01.pnganalysis03.png展示评分分布、区域热度、人均消费趋势还有数据库ER图db.png说明字段关系方便快速核对数据结构。README.md有清晰的安装步骤和运行命令requirements.txt列明全部依赖utils目录放通用辅助函数view目录预留未来前端对接接口。本文还有配套的精品资源点击获取