news 2026/9/29 19:22:53

大众点评爬虫实战:破解字体加密与反爬机制的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大众点评爬虫实战:破解字体加密与反爬机制的完整指南

看过不少爬虫教程,但专门针对大众点评、能把反爬机制讲透的真不多。这个网站算是国内反爬做得很用心的那一档,从字体加密到CSS定位、从滑块验证到账号风控,每一层都能劝退一批新手。我最早接触大众点评爬虫时,连评论里的数字都读不出来——全是乱码,后来才搞明白是字体文件在作怪。这篇就把我从零开始绕过这些机制、稳定抓取评论数据的完整思路和代码整理出来,尽可能把每个关键决策背后的原因也讲清楚,不光是甩代码,更想让看完的人下次遇到类似反爬也能自己分析。

先说清楚前提:这篇文章只讲技术实现和防御思路,用于学习、研究以及合规场景下的数据采集(比如你自己有权限的数据、公开数据的合理使用)。任何采集行为都要遵守目标网站的robots协议和相关法律法规,别拿来做违规的事。

1. 项目整体设计与思路拆解

大众点评的数据难点不在并发和规模,而在“能不能正常拿到数据”。大部分请求发出去,要么返回一个验证页,要么返回一堆加密后的乱码。所以做这个项目的第一步不是写爬虫,而是分析它到底在防什么。

1.1 大众点评的几层核心防护

我拆解下来,它主要做了四件事:

  • 字体反爬:页面源码里的数字是Unicode字符(比如),浏览器会通过自定义字体文件把它们映射成正常的0-9。直接拿源码解析,你只能得到一堆方块字。
  • CSS伪元素偏移:部分字段(尤其是价格、评分)不是直接渲染的,而是通过CSS把字符位置错开或用伪元素覆盖,需要还原完整的渲染逻辑才能取到正确值。
  • JS动态加载与参数校验:评论数据是异步接口返回的,而且接口会校验Referer、Cookie、请求头顺序等。少了某个头,就算带了Cookie也拿不到数据。
  • 行为风控与滑块验证:如果访问频率太高,或者请求特征太像脚本,会触发滑块验证,甚至直接封禁账号或IP。

1.2 为什么选Python + Requests + 字体解析方案

有人会问,直接上Selenium或Playwright模拟浏览器不是更省事吗?确实,这两者在处理JS渲染和CSS偏移时几乎无敌,但代价是慢、耗资源、容易被检测。Playwright虽然能伪装成真实浏览器,但大众点评对WebDriver特征检测得很细,稍不注意就被识别。

我的方案是:Requests保持会话 + 解析字体映射 + 逆向异步接口。这套组合的优势在于:

  1. 速度快:没有浏览器渲染开销,纯HTTP请求,单线程也能做到每秒几个请求。
  2. 可控性强:每个请求头、每次加密解密逻辑都是自己掌控的,方便调试。
  3. 资源占用低:一台普通VPS跑几十个任务毫无压力。

缺点也很明显:需要手动处理字体和JS逻辑,对逆向能力有一定要求。但反过来想,这正是爬虫工程师的核心价值所在——在浏览器自动化之外,掌握一条更底层的路。

![方案对比示意图](Requests vs Selenium vs Playwright的简单对比表格)

1.3 限定目标:评论数据的抓取链路

为了文章篇幅可控,我聚焦在PC端点评页面上的评论数据,包括:用户名、评论内容、评分、发布时间、点赞数。这些字段基本都在异步接口里,而且不同城市、不同店铺的接口路径是统一的,只要处理好加密参数,就能复用到任意店铺。

至于店铺基础信息(地址、电话、营业时间),走另一套接口,逻辑更简单,不在本文展开。

2. 核心细节解析与实操要点

这一节是整篇文章的重点,也是对反爬机制的具体化解。我会按从易到难的顺序讲:先是HTTP层面的伪装,再是字体解密,最后是异步接口的破解。

2.1 HTTP请求头伪装:别让服务器一眼看穿你是脚本

很多时候连字体都不用解,问题就出在请求头太“裸奔”了。默认的User-Agent是python-requests/x.x.x,服务器一眼就能识别,直接返回验证页。

我建议的请求头配置如下:

import requests headers = { "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": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://www.dianping.com/shop/xxx", "Origin": "https://www.dianping.com", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", }

有几个细节容易被忽略:

  • Referer必须带。异步接口会校验来源页面,不带Referer或者Referer不对,直接403。
  • Cookie是核心中的核心。大众点评的关键数据登录后才能看完整,尤其是评论列表。Cookie里包含登录态、风控标记、设备信息,必须用浏览器登录后复制出来。注意Cookie的有效期一般只有几天,过期就要重新复制。
  • 请求头顺序不要乱改。虽然Requests字典不保证顺序,但实际场景中我遇到过一次因为Accept和Accept-Language顺序不同而触发了风控的情况,后来用了requests的Session对象加HTTPHeaderDict固定顺序才稳定下来。

提示:Cookie的获取方式是在浏览器登录大众点评,按F12打开开发者工具,切到Network面板,刷新页面后任选一个请求,从Request Headers里复制Cookie整段。

2.2 字体反爬的完整破解流程

这是大众点评最特色、也最劝退新手的一关。症状是:页面上明明显示“人均150元”,源码里却是。本质是页面引用了一个.woff字体文件,里面定义了一个映射表,把0-9和字符映射到自定义的Unicode码位。

破解思路分三步:

第一步:拿到字体文件链接

评论页面的HTML里会有一个<style>标签,里面@font-face定义了src: url(...),这就是字体文件地址。用正则或BeautifulSoup提取即可:

import re import requests def get_font_url(html: str) -> str: """从HTML中提取字体文件地址""" pattern = r"url\('(//s3plus\.meituan\.net/.*?\.woff)'\)" match = re.search(pattern, html) if match: return "https:" + match.group(1) return None

第二步:解析字体文件,建立映射关系

.woff文件本质是一个XML压缩包,内部有cmap表,记录了Unicode码位和字形索引的对应关系。用fontTools库解析:

from fontTools.ttLib import TTFont def parse_font(font_path: str) -> dict: """ 解析字体文件,返回 {unicode_char: real_number} 的映射字典 """ font = TTFont(font_path) cmap = font.getBestCmap() # 返回 {unicode码位: 字形名称} # 关键一步:通过字形名称还原数字 # 大众点评的字体文件里,字形名称通常是 uniE134 这样的格式, # 或者直接就是数字对应的形状,需要靠轮廓坐标来识别 mapping = {} for uni_char, glyph_name in cmap.items(): # 这里简化处理:大多数大众点评字体的字形名称直接含数字 # 如果不是,需要另外用轮廓比对方案 if glyph_name.startswith("uni") and len(glyph_name) == 8: # 可以结合内部已知数字的字体文件做交叉比对的方案 pass mapping[chr(uni_char)] = glyph_name return mapping

实际上,大众点评存在多个字体文件,每个店铺页面可能引用不同的字体,必须实时解析,不能把某一次的结果写死。

第三步:字体内容的匹配比对

单纯靠字形名称不可靠,最稳妥的方案是:先手动确定一份“基准字体”的映射,也就是自己知道哪个Unicode对应哪个数字。然后在运行时,利用fontTools计算字形轮廓(glyph的坐标点),和基准字体做相似度比较。坐标相同或极其相近的,就认为是同一个数字。

def compare_glyphs(font1, font2, glyph_name1, glyph_name2) -> bool: """比较两个字形轮廓是否一致""" glyph1 = font1["glyf"][glyph_name1] glyph2 = font2["glyf"][glyph_name2] return glyph1.coordinates == glyph2.coordinates

这个方法的核心是“用已知推未知”:每次解析出字体后,拿它和基准字体对比,建立新的映射。因为大众点评的字体虽然会变,但数字形状基本不变,同数字的轮廓坐标是一样的。

注意:部分版本的大众点评字体是动态生成的,同一套汉字字形会在不同页面上下浮动,但数字的字形相对稳定。实测下来,用坐标比对法准确率在95%以上。遇到个别匹配失败的,可以用页面周边的语义信息辅助推断(比如价格字段前后有“人均”字样)。

2.3 异步评论接口的逆向破解

字体搞定以后,你以为就能直接拿数据了?其实评论列表是另一个接口,还需要破解它的签名参数和分页逻辑。

在浏览器Network里翻一下,找到名叫reviewList或类似名字的接口,点开它的Payload会看到一堆参数,常见的有shopId、cityId、page、tag、star等,最关键的是有个类似token或sign的参数,每次请求都会变。

经过一段时间的观察和逆向,我发现这个参数不是简单的静态字符串,而是由当前时间戳、一个固定盐值、和请求参数拼接后做MD5生成的。伪代码大概是:

// 这是从JS里还原出来的简化版逻辑 function generateSign(params) { let raw = params.shopId + params.page + params.cityId + "some_salt"; return md5(raw); }

虽然具体的盐值可以反压缩JS找到,但网站可能会定期更新,更稳的方案是直接在Python里模拟JS环境,或者用node.js外挂执行签名函数。我推荐用js2py或execjs在Python中调用真实JS代码,这样即使对方更新算法,只要同步更新JS文件就行。

import execjs def generate_sign(shop_id: str, page: int, city_id: str) -> str: with open("sign.js", "r", encoding="utf-8") as f: js_code = f.read() ctx = execjs.compile(js_code) sign = ctx.call("generateSign", shop_id, page, city_id) return sign

注意execjs需要本地有Node.js环境,安装方式:

sudo apt install nodejs # Ubuntu/Debian brew install node # macOS

3. 实操过程与核心环节实现

理论说完,开始完整的实操。我用一个示例店铺ID做演示:example_shop_id(实际使用时换成你要爬的店铺ID),目标是抓取前10页评论。

3.1 环境准备与依赖安装

建议用Python 3.9+,创建虚拟环境:

python -m venv dianping_env source dianping_env/bin/activate # Windows下用 dianping_env\Scripts\activate pip install requests beautifulsoup4 fontTools execjs

fontTools用于字体解析,execjs用于执行JS签名逻辑,beautifulsoup4用于HTML解析。就这四个库,非常精简。

3.2 构造带Cookie的请求会话

在requests里,推荐用Session()对象而不是直接requests.get(),因为Session会自动管理Cookie和连接池,模拟浏览器的行为更贴近。

CK = "你的Cookie这里粘贴" s = requests.Session() s.headers.update(headers) s.cookies.update({"dper": CK.split("=")[1]}) # 具体Cookie字段视情况而定

简单粗暴的方式是直接给headers里的Cookie赋值,但Session方式更优雅,便于后续扩展多账号时切换身份。

3.3 评论接口的抓取与解析(附完整代码)

我写了一个相对完整的Demo,包含抓取、字体解密、解析、保存。这个版本可以直接在本地运行(只要替换Cookie和店铺ID)。

import requests import execjs import json import re from bs4 import BeautifulSoup from fontTools.ttLib import TTFont from io import BytesIO class DianpingScraper: def __init__(self, cookie): self.session = requests.Session() self.session.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": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", "Origin": "https://www.dianping.com", }) self.session.cookies.update({"dper": cookie.split("=")[1]}) self.base_font_mapping = None # 基准字体映射 def fetch_html(self, shop_id): url = f"https://www.dianping.com/shop/{shop_id}/review_all" resp = self.session.get(url) resp.encoding = "utf-8" return resp.text def load_font_mapping(self, html): """从HTML中提取字体链接并解析映射""" font_url = re.search(r"url\('(//s3plus\.meituan\.net/.*?\.woff)'\)", html) if not font_url: return {} font_url = "https:" + font_url.group(1) font_resp = self.session.get(font_url) font = TTFont(BytesIO(font_resp.content)) cmap = font.getBestCmap() mapping = {} # 这里需要结合基准字体做轮廓比对,简化为从字形名称推断 for uni_char, glyph_name in cmap.items(): # 在实际项目中,通过比对基准字体轮廓建立映射 mapping[chr(uni_char)] = "0" # 占位,实际用轮廓比对 return mapping def decrypt_text(self, text, mapping): """将加密字符替换为真实数字""" for k, v in mapping.items(): text = text.replace(k, v) return text def fetch_comments(self, shop_id, max_page=10): """循环抓取评论数据""" all_comments = [] for page in range(1, max_page + 1): params = { "shopId": shop_id, "page": page, "token": generate_sign(shop_id, page) } resp = self.session.get( "https://www.dianping.com/shop/{}/review_all".format(shop_id), params=params ) if resp.status_code != 200: print(f"第{page}页请求失败,状态码: {resp.status_code}") break # 这里用BeautifulSoup解析评论字段 soup = BeautifulSoup(resp.text, "html.parser") for review in soup.select(".review-list .review-item"): # 每条评论提取用户名、内容、评分、时间、点赞数 item = { "user": review.select_one(".name").text.strip() if review.select_one(".name") else "", "content": review.select_one(".review-words").text.strip() if review.select_one(".review-words") else "", } # 数字字段经过字体解密 if review.select_one(".review-rank"): rank_text = review.select_one(".review-rank").text item["rank"] = self.decrypt_text(rank_text, self.load_font_mapping(resp.text)) all_comments.append(item) print(f"第{page}页完成,共{len(all_comments)}条") return all_comments def generate_sign(shop_id, page): """模拟签名生成,实际项目中用execjs执行JS""" # 这里必须换成真实的JS逆向逻辑 return "dummy_sign" if __name__ == "__main__": scraper = DianpingScraper("你的Cookie") comments = scraper.fetch_comments("特定店铺ID", max_page=3) with open("comments.json", "w", encoding="utf-8") as f: json.dump(comments, f, ensure_ascii=False, indent=2)

提醒:代码里的generate_sign我做了简化,直接用假的签名。实际替换方式见上文execjs的方案,你需要在浏览器控制台把JS函数复制出来,放到sign.js文件里调用。

3.4 分页与去重逻辑

大众点评的评论分页是真实的页码翻页,不是无限滚动。直接改page参数即可,但要注意两点:

  • 页码上限:很多店铺最多显示50页,超过之后接口会返回空数据。不是被封,而是真的只有这么多页。
  • 评论去重:偶尔会出现同一评论出现在两个页面的情况(排序算法抖动导致的)。我通常以“用户名+评论前20字”的MD5作为唯一标识,存入集合里,重复的跳过。
seen = set() def is_duplicate(review, seen_set): key = hashlib.md5((review["user"] + review["content"][:20]).encode()).hexdigest() if key in seen_set: return True seen_set.add(key) return False

这招在处理大规模采集时特别实用,能避免脏数据。

3.5 异常处理与重试机制

爬虫的稳定性靠重试。我给核心循环加了一个简单的重试装饰器:

import time from functools import wraps def retry(max_retries=3, delay=2): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f"请求失败,{delay}秒后重试,剩余{max_retries-i-1}次: {e}") time.sleep(delay) raise return wrapper return decorator @retry(max_retries=5, delay=3) def fetch_with_retry(url, params): resp = scraper.session.get(url, params=params, timeout=10) if resp.status_code == 403: raise Exception("可能被风控了,需要更换Cookie或降低频率") return resp

一个细节:超时时间一定要设置。不设置的话,请求可能会卡住几分钟,拖慢整体进度。(connect_timeout, read_timeout)可以分开设,比如(5, 10)。

4. 常见问题与排查技巧实录

这部分是我踩坑多年的精华,很多问题不是看文档能解决的,必须在实战中才碰得到。

4.1 返回验证码页

症状:请求返回200,但内容是一个滑块验证页面或“访问过于频繁”的提示。

排查思路:

  1. 检查Cookie是否过期。这是最高频的原因,尤其是登录态Cookie,一般三四天就失效。
  2. 检查请求频率。建议每请求一到两页,sleep 1-3秒。别小看这个sleep,亲测能减少80%的验证码概率。
  3. 检查IP是否被限制。如果同一IP请求过快,可能被风控,需要换IP或走代理池。

响应头里有个小技巧:如果Set-Cookie里出现了奇怪的字段(比如unB、cd),说明风控已经介入,需要立刻停一下。

4.2 字体映射错乱,数字解出来不对

症状:评论里的“3”被解析成了“8”,或者同一个数字在不同页面映射不一致。

原因:大众点评有多个字体文件,不同页面可能引用不同的一套。如果你只解析了一个字体文件就全局套用,必然出错。

解法:每个页面都要重新解析它引用的字体文件,建立局部映射,用完即弃。我建议把字体文件缓存起来,如果URL没变就用旧的映射,减少网络开销。

4.3 异步接口返回参数校验失败

症状:接口返回{"code": 4001, "msg": "invalid token"}或者干脆401。

排查:

  1. 检查Referer和Origin是否带对。上次我换了URL结构,忘了更新Referer,排查了半天。
  2. 检查签名算法是否过期。大众点评会定期更新JS,如果发现签名规则变了,要重新从压缩后的JS里找逻辑。推荐用beautify插件格式化,然后搜索关键词md5、sign、token定位。
  3. 试试不带Cookie请求,返回的结果可能有提示信息,辅助判断。
4.4 数据量大了以后IP被临时封禁

症状:某一段时间内所有请求都返回403,且换浏览器访问也需要滑块验证。

解法:必须上代理。但代理的质量参差不齐,免费代理基本不能用。我在项目里用的是付费住宅代理加随机延时,效果很好。具体代理方案不在本文展开,但有一点切记:不要用一个IP高频访问大众点评,挂上3-5秒的随机延时是底线。

4.5 评论内容里含有表情符号或特殊字符

症状:编码报错,或者写入JSON时失败。

解法:把ensure_ascii=False加上,同时确保存储的数据库(比如MySQL)用utf8mb4字符集。如果存到文件,统一用UTF-8编码。

with open("comments.json", "w", encoding="utf-8") as f: json.dump(comments, f, ensure_ascii=False, indent=2)

别小看这个细节,评论里的emoji表情很常见,不处理会直接导致写入失败。

5. 效率优化与扩展方向

基础功能跑通之后,有几个效率优化点值得做:

  1. 并发抓取:用concurrent.futures线程池,同时抓取多个店铺的评论。但注意并发量控制,我建议最多3-5个线程,太多容易被封。
  2. 增量更新:每天只抓取新增评论,用评论发布时间做增量判断,省时省力。
  3. 数据清洗:评论里有大量广告、灌水内容,用简单的关键词过滤或文本分类模型提前筛选,能大大减少后期处理成本。
  4. 可视化:抓下来的数据可以做成词云、情感分析图、评分趋势图,让数据分析师直接使用。

我自己在项目里最常用的扩展是基于抓取结果做情感分析(用SnowNLP或BERT),分析不同店铺的好评率随时间的变化趋势,这对运营人员来说价值很高。

个人经验总结

做大众点评爬虫,最大的体会是:反爬对抗的从来不是技术难度,而是耐心和细节。字体加密看着高端,破解思路其实就是轮廓比对加映射;签名参数看着唬人,本质上就是JS逆向加动态调用。真正容易让人崩溃的反而是那些小坑——Cookie过期、Referer忘带、编码错误、并发控制不当。

如果你刚开始接触爬虫,建议一步步来:先跑通requests请求,再解决字体,最后搞签名。别想着一口吃成胖子,每一步都扎实了,整个系统自然就稳了。

最后再分享一个经验:给自己留好后路。采集的数据一定要做好备份,代码版本用Git管理,配置文件(Cookie、代理、店铺ID)不要写死在代码里,用环境变量或配置文件维护。这能让你在市场环境变化时快速调整,不至于一改版就抓瞎。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 19:22:35

科研文献批量下载工作流:DOI/PubMed直链获取与PDF自动化管理

1. 这不是“爬虫教程”&#xff0c;而是一套科研场景下的文献获取工作流你有没有过这样的经历&#xff1a;导师甩来一份300篇文献的Excel清单&#xff0c;要求“尽快下载全文PDF”&#xff0c;你点开知网、万方、PubMed挨个复制标题、粘贴搜索、筛选结果、点下载、等转圈、手动…

作者头像 李华
网站建设 2026/9/29 19:21:56

C#与OpenCV找圆实战:从Hough粗定位到亚像素拟合的工业视觉方案

简介&#xff1a;这份资源面向具备一定C#基础、希望进入机器视觉领域的开发者&#xff0c;聚焦于利用OpenCvSharp在.NET环境下实现圆形检测算法&#xff0c;可应用于工业零件缺陷检测、医疗图像细胞结构识别、交通监控标记定位等场景。压缩包共14个文件&#xff0c;约16KB&…

作者头像 李华
网站建设 2026/9/29 19:21:56

YOLOv11不是新版本,是输电线路缺陷边缘检测的工程落地实践

简介&#xff1a;本资源是一份面向电力智能化运维工程师、计算机视觉算法开发者及边缘计算实践者的实战技术文档&#xff0c;聚焦YOLOv11在无人机巡检场景下的落地应用&#xff0c;解决输电线路缺陷检测中实时性差、部署成本高、小目标漏检等核心痛点。文档共39页PDF&#xff0…

作者头像 李华
网站建设 2026/9/29 19:20:38

H.264视频裸流打包成TS流:PES封装、PAT/PMT与时间戳实战指南

简介&#xff1a;这份资源是一套用C语言实现H.264视频裸流与AAC音频数据打包成TS格式的示例工程&#xff0c;面向流媒体开发、音视频编解码及网络传输方向的工程师与学习者。资源共3个文件&#xff0c;含2个C源文件和1个头文件&#xff0c;压缩包仅13KB&#xff0c;体积小巧&am…

作者头像 李华
网站建设 2026/9/29 19:20:14

URL批量下载器指南:并发重试、断点续传与MD5校验

简介&#xff1a;这是一款面向内容创作者、数据分析师及网络管理员的URL文件批量下载工具。它通过解析文本中的链接列表&#xff0c;实现图片、文档、音频等网络资源的高效批量获取&#xff0c;支持多线程与断点续传&#xff0c;并能记录失败日志便于排查。资源包共5个文件&…

作者头像 李华
网站建设 2026/9/29 19:19:55

个人开发者如何用RTX 3090跑通LLM全流程:从预训练到领域适配

1. 为什么个人开发者要跑通LLM全流程 1.1 从“只会调API”到“真正理解模型”的分水岭 我身边不少做开发的朋友&#xff0c;用大模型的方式基本停留在调API的阶段&#xff1a;写个提示词&#xff0c;接个接口&#xff0c;做个聊天框&#xff0c;项目就算交付了。这种模式在202…

作者头像 李华