
简介面向计算机相关专业学生、毕业设计开发者及爬虫初学者的大众点评爬虫Python源码包聚焦大众点评平台数据采集场景包含完整可运行的爬虫脚本与说明文档。代码经过测试验证可帮助读者快速上手网页解析、请求处理与数据提取等核心环节。压缩包内共3个文件包括两个Python脚本与一个Markdown说明文档脚本构成爬虫与数据处理主体说明文档提供使用说明整个zip包仅3KB结构精简便于直接阅读和二次修改。目前已有380人学习下载适合用于课程设计、毕业设计起步或Python爬虫技能进阶。基于这份源码读者既能跑通大众点评数据采集的基本流程也能在此基础上扩展店铺信息抓取、评论分析等功能从而加深对requests、BeautifulSoup等常用库的实际运用能力。1. 大众点评爬虫的难点不在请求而在字体映射与 Cookie 校验直接拿 requests 打开大众点评的列表页第一眼会觉得很顺利HTML 能返回状态码也是 200。把页面里的店铺名、评分、人均消费存进 CSV 后才发现数字字段全是乱码评论数也是空的。再刷新两次请求头里稍微少了某个 Cookie 字段服务器就直接返回 403。这个项目带 getData.py 和 wjk.py 两个脚本前者负责带 Cookie 的请求和数据落地后者专门处理 HTML 解析和字体映射。它的代码结构不像 Scrapy 那样重但对于想弄懂“一口气到底能卡在哪”的人来说正好适合当毕业设计或课程练手把反爬最常见的三个环节——请求构造、选择器匹配、字体还原——完整走了一遍。2. 项目结构与请求链路getData.py 怎么把数据拉下来2.1 两个 py 文件的分工与运行顺序解压后能看到的有效代码文件是两个getData.py 和 wjk.py外加一个 README.md。从命名习惯来看getData.py 是数据采集入口wjk.py 是解析模块。README 里一般会写依赖清单和运行顺序先执行 getData.py 把列表页 HTML 存成 list_1.html、list_2.html 这样的本地文件再执行 wjk.py 读取这些文件输出结构化结果。把抓取和解析拆开是一般爬虫项目里比较克制又实用的做法。大众点评的响应里同时含有列表数据和字体文件链接如果不先把原始 HTML 落盘解析时遇到验证码或字体变化就很难回查。我通常会在 getData.py 里保留响应头、最终 URL、状态码和 Cookie 信息把这些信息并列写进一个 debug 文件。这样 wjk.py 解析结果不对劲时可以先看 debug 文件判断是不是请求阶段就已经被风控拦截而不是怀疑解析器写错。反过来如果先跑 wjk.py程序会因为找不到 list_1.html 直接报 FileNotFoundError跑不起来。这个顺序也说明项目作者把数据抓取和数据处理当成了两个独立环节便于逐步调试。2.2 请求头、Cookie 和 Referer 的组装方式大众点评对请求来源的判断很敏感单纯设置一个 User-Agent 并不够。Cookie 里缺少_lxsdk_cuid、dper这类字段时服务端会把这个请求当成未登录访客返回部分字段缺失或者直接跳转验证码。把请求构造封装在一个函数里后续所有请求都复用同一个 Session是更稳妥的写法import requests def build_session(cookie_str: str) - requests.Session: s requests.Session() s.headers.update({ 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-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.dianping.com/shanghai/ch10, }) # 这里只是示例字段实际使用时从浏览器复制整段 Cookie for item in cookie_str.split(; ): if in item: key, value item.split(, 1) s.cookies.set(key, value) return s这段代码的关键点是Session会自动管理 Cookie不需要每次请求都手动拼 Cookie 头。User-Agent建议固定成一个真实浏览器版本不要每次随机变换否则服务端能看到同一 IP 的 UA 在跳变反而更容易被识别。Referer要指向大众点评站内页面直接从外部站点带过来的来源经常触发 403。把cookie_str按;切分并写进 Session可以避免手动维护 Cookie 字典。下表列出请求头里最影响结果的几个字段字段作用建议值User-Agent告诉服务器客户端类型固定为 Chrome 120 的 UA 字符串Referer标志访问来源当前分类页或大众点评首页Cookie维持登录态和风控标记浏览器登录后复制完整 Cookie 头Accept-Language决定返回页面语言zh-CN,zh;q0.9请求头字段并不是越多越好。像Accept-Encoding如果不设置requests 会默认发送gzip, deflate而大众点评有时返回压缩流处理起来会多一层解码。直接把Accept-Encoding留空让 requests 自动解压反而更省事。2.3 从列表页到详情页的 URL 构造列表页 URL 结构一般是https://www.dianping.com/{city}/ch10/g{page}其中ch10是美食频道的分类编号page是分页序号。还有一类写法是城市走子域名例如https://shanghai.dianping.com/food需要以浏览器实际跳转后的地址为准。构造 URL 时最好写一个函数方便后续扩展其他分类def build_list_url(city: str, category: str ch10, page: int 1) - str: return fhttps://www.dianping.com/{city}/{category}/g{page}拿到 URL 后用构建好的 Session 发起请求同时把返回结果保存到本地s build_session(cookie_str你的完整cookie) resp s.get(build_list_url(shanghai), timeout10) if resp.status_code 200 and shop-all-list in resp.text: with open(list_1.html, w, encodingutf-8) as f: f.write(resp.text) else: print(请求失败状态码:, resp.status_code, 响应长度:, len(resp.text))这里的判断语句有实际意义shop-all-list是列表容器在页面里的稳定标识。只要响应里存在这个字符串就说明拿到的是正常列表页而不是验证码页。如果状态码 200 但响应体里没有这个标识多半是触发了风控页面此时保存下来的 HTML 对解析没有帮助。timeout10设置请求超时时间防止某个 IP 被拖死时脚本长期卡住。3. wjk.py 的解析逻辑从 HTML 到结构化字段3.1 解析入口与选择器设计wjk.py 的输入是 getData.py 保存的 HTML 文件。解析库选择 BeautifulSoup 加 lxml 解析器因为大众点评的 HTML 标签嵌套并不标准大量li和div存在未闭合的情况。lxml 在容错性上比纯 html.parser 好同时保留 CSS 选择器语法写起来比 XPath 短不少。解析入口可以先这样写from bs4 import BeautifulSoup with open(list_1.html, encodingutf-8) as f: soup BeautifulSoup(f.read(), lxml) items soup.select(#shop-all-list ul li) print(本页店铺数量:, len(items))选择器#shop-all-list ul li用的是 ID 加子选择器而不是li.shop-list这种依赖 class 的写法。大众点评改版后经常给元素加上带随机后缀的 class例如shop-list-8f3a这类选择器一旦页面更新就失效。ID 属性通常保持不变号限定直接子元素能避开外层嵌套的干扰。如果items的数量为 0第一反应不应该是换选择器而是回头检查 getData.py 保存的 HTML 是否真的是列表页。3.2 店铺名、评分、人均消费的字段提取每个li内部包含一个店铺卡片结构大致是li div classtxt div classtit a hrefhttps://www.dianping.com/shop/123456示例餐厅/a /div span classscore4.5/span span classmean-price¥120/span /div /li要从中提取店铺名、详情页 URL、评分和人均消费可以逐个解析import re shops [] for li in items: title_node li.select_one(div.tit a) name title_node.get_text(stripTrue) if title_node else url title_node.get(href, ) if title_node else score_node li.select_one(.score, .star-score) score score_node.get_text(stripTrue) if score_node else price_node li.select_one(.mean-price, .price) price_text price_node.get_text(stripTrue) if price_node else price re.sub(r[^\d.], , price_text) shops.append({ name: name, url: url, score: score, price: price, }) print(shops[:3])这里用了多项选择器.score, .star-score这是为了兼容新旧两套页面模板。get_text(stripTrue)能去掉标签内部的空白和换行提取出来的字符串更干净。为什么用正则[^\d.]而不是直接replace(¥, )因为“人均”附近的文本可能是“人均 ¥120”或“¥120/人”正则可以把所有非数字和小数点的字符去掉避免漏掉看不见的全角空格。shops列表里的每个字典可以直接交给json.dump或写入数据库。3.3 处理字体反爬的通用方案大众点评的数字加密属于字体反爬页面里的数字用font-face定义的自定义字体渲染真实数字被映射到私人使用区。把 HTML 里的文本抓出来会看到类似“ ”或“” 的字符把这些字符直接写进 CSV最后落到 Excel 里就是乱码。字体反爬的通用处理分三步下载 woff 文件、读取 cmap 表、做字符替换。这里先给出一个手动维护映射字典的版本FONT_MAP { \ue8a9: 0, \ue8b0: 1, \ue8c1: 2, \ue8d2: 3, \ue8e3: 4, \ue8f4: 5, \ue905: 6, \ue916: 7, \ue927: 8, \ue938: 9, } def restore_number(text: str) - str: for font_char, real_num in FONT_MAP.items(): text text.replace(font_char, real_num) return text注意上面这个映射表里的 code point 是示例真实值会随着大众点评更新字体而改变。最稳妥的做法是每次解析时都动态读取 woff 文件里的 cmap再和这份字典做对照。下面这段代码先从页面 HTML 里提取 woff 地址再用fontTools读取码表import io import re import requests from fontTools.ttLib import TTFont def extract_woff_url(html: str) - str: m re.search(rurl\((//[^]\.woff)\), html) return https: m.group(1) if m else def get_cmap_entries(html: str, session: requests.Session) - dict: url extract_woff_url(html) if not url: return {} resp session.get(url, timeout10) font TTFont(io.BytesIO(resp.content)) cmap font.getBestCmap() return {chr(code): name for code, name in cmap.items()}getBestCmap()返回的字典里key 是 Unicode 码点对应的字符value 是字形名称。把这些字形名称和FONT_MAP里的真实数字逐一对应就能生成完整的替换表。由于字体文件每次发布都可能更换把映射关系存成 JSON 文件解析前先读取本地映射比硬编码进代码更容易维护import json with open(font_map.json, r, encodingutf-8) as f: current_map json.load(f)如果发现页面里出现了current_map不认识的字符就把这条记录打印出来手动确认真实数字后更新 JSON。这种半自动方式比全自动模板匹配好在可控毕业设计演示时也能解释清楚“字体反爬的还原链路”。4. 跑通爬虫环境依赖、限速与代理配置4.1 Python 环境与依赖安装项目源码没有依赖 Scrapy所以安装成本很低。在 Python 3.8 以上的环境里直接执行pip install requests beautifulsoup4 lxml fake-useragent fonttoolsrequests负责网络请求beautifulsoup4负责页面解析lxml是解析加速器fake-useragent用来生成 User-Agent。fonttools在 3.3 节已经出现过负责读取字体文件。如果所在的机器不能正常访问外网源把fake-useragent去掉换成固定 UA 字符串即可其他几个库在内网 pip 镜像里都能找到。装完之后在项目根目录运行python getData.py run.log 21把输出重定向到日志文件避免终端信息被大量打印刷屏。4.2 运行参数与限速配置从 README 看getData.py 应该支持通过命令行参数指定城市和页码范围。实际改起来也不复杂用argparse接收参数循环抓取时加入随机睡眠可以避免固定间隔被识别import argparse import random import time parser argparse.ArgumentParser(description大众点评列表页抓取) parser.add_argument(--city, defaultshanghai, help城市名或拼音) parser.add_argument(--start, typeint, default1, help起始页码) parser.add_argument(--end, typeint, default10, help结束页码) args parser.parse_args() for page in range(args.start, args.end 1): url build_list_url(args.city, page) resp s.get(url, timeout10) if resp.status_code 200 and shop-all-list in resp.text: with open(flist_{page}.html, w, encodingutf-8) as fp: fp.write(resp.text) print(f第 {page} 页保存成功) time.sleep(random.uniform(3, 6))限速不是单纯把 sleep 时间调大就安全关键是“随机”。固定 sleep 3 秒会在服务端形成等间距访问痕迹随机区间配合每次请求重置连接更容易混在真实流量里。random.uniform(3, 6)的意思是每次暂停 3 到 6 秒之间的随机秒数。如果目标数据量大把间隔调到 8 到 15 秒宁可多一点时间也不要让 IP 提前被封。4.3 代理轮换与会话保持的常见做法当单个 IP 连续抓取超过几十页后大众点评会返回验证码或者 403。常见做法是准备一个 HTTP 代理列表每次请求前随机选取一个proxy_list [ {http: http://user:pass101.1.2.3:8080, https: http://user:pass101.1.2.3:8080}, {http: http://user:pass45.67.8.9:8080, https: http://user:pass45.67.8.9:8080}, ] def random_proxy(): return random.choice(proxy_list)调用时传入proxiesrandom_proxy()。注意代理有失效概率遇到超时要切换下一个代理并重试而不是无限等待。同一个 Session 里的 Cookie 在换 IP 后仍然有效这是 HTTP 代理和登录态可以共存的根本原因。下面的状态码表可以拿来判断当前 IP 是否被限制返回码含义应对方式403Forbidden被服务器拒绝检查 Cookie 和 Referer换代理418被反爬机制识别清空会话重新初始化 Session444响应为空降低请求频率重试一次503服务暂不可用停止 30 秒再换代理重试如果响应码常驻 403先别急着换代理把前两次成功的请求头和当前失败的请求头做 diff查看 Cookie 是否在同一会话里发生了变化。很多 403 是因为代码里重新build_session导致 Cookie 丢失不是代理问题。5. 验证数据质量与把结果落到 SQLite 的实操细节5.1 字段完整度自检解析完成后先不要直接写入文件先做一次字段完整度检查def validate(shops): total len(shops) if total 0: raise ValueError(没有解析到任何店铺) filled sum(1 for item in shops if item[name] and item[score] and item[price]) print(f字段完整度: {filled}/{total}) if filled / total 0.8: print(大量字段缺失可能是字体映射失效)字段完整度低于 80% 时优先检查wjk.py里的选择器是否还匹配页面其次检查字体映射是否被更新。单独打印一页的原始 HTML用文本编辑器搜索“score”看页面里是否存在这个 class能快速区分是请求问题还是解析问题。5.2 写入 SQLite 并处理重复数据数据量不大时不用上 MySQLSQLite 足够存储几万条店铺记录而且 Python 标准库直接支持。把shops列表写进数据库时可以带上shop_id从url里正则提取import sqlite3 import re conn sqlite3.connect(dianping.db) conn.execute( CREATE TABLE IF NOT EXISTS shops ( shop_id TEXT PRIMARY KEY, name TEXT, url TEXT, score TEXT, price TEXT ) ) rows [] for item in shops: match re.search(r/shop/(\d), item[url]) shop_id match.group(1) if match else rows.append((shop_id, item[name], item[url], item[score], item[price])) conn.executemany( INSERT OR REPLACE INTO shops(shop_id,name,url,score,price) VALUES(?,?,?,?,?), rows, ) conn.commit() conn.close()INSERT OR REPLACE以shop_id作为主键第二次抓到同一家店铺时会直接覆盖旧记录。这样做的好处是后续增加评论页爬取时可以用shop_id关联店铺和评论两张表形成一对多关系。如果只做毕业设计展示把rows改成csv.writer写 CSV 也一样只是 CSV 没有主键概念重复运行会产生大量重复行。sqlite3的executemany适合批量插入比一行一条 INSERT 快很多。最后那句conn.commit()一定要执行否则数据只停留在内存里。如果后续还要做关键词筛选比如只看人均消费 50 到 100 的店铺直接执行SELECT name, price FROM shops WHERE CAST(price AS REAL) BETWEEN 50 AND 100即可不需要把 CSV 再读回内存。这也算是在原项目基础上多走了一步。本文还有配套的精品资源点击获取