ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

字体反爬破解实战:从字体解析到搜索接口的完整实现

字体反爬破解实战:从字体解析到搜索接口的完整实现 做爬虫的朋友应该都遇过那种页面——浏览器里看着一切正常一抓HTML源码就傻眼价格、评分、姓名全成了乱码要么是#xe604;这类实体要么是根本对不上的字符细看才发现页面背后挂着一堆woff/ttf字体文件这就是典型的字体反爬。对付这种场景圈里常见的解法就是做一套字体搜索接口把网页里拿到的字体文件送进解析服务让它返回字形和真实字符的映射关系再用这份映射去还原密文。我这个py每日spider案例之字体搜索接口就是围绕这条完整链路写的——从requests请求、字体文件下载、正则提取到接口解析、脚本间参数传递、环境兼容全部覆盖。适合正在做采集项目、想搞定字体反爬的爬虫工程师也适合刚学爬虫、想通过实战案例理解正则和接口调用的新手参考。1. 这个案例到底在解决什么问题1.1 字体反爬的核心原理先把原理捋清楚。普通网页里字符编码和字形是一一对应的浏览器拿到字符3就去标准字体里画一个3用户看到的就是数字三。字体反爬的思路是页面里根本不放标准编码而是放一个自定义字体文件里某个特定字形的编码。浏览器有这份自定义字体文件作为支持所以页面渲染出来一切正常但我们用requests抓回来的HTML源码里看到的只是乱码实体或特殊编码。也就是说反爬方没有把文本加密成不可读的乱码而是把字符的显示方式换了一套让机器读到的东西和眼睛看到的东西不一致。这里要理解一个关键点字体反爬的本质是字形混淆。字体文件本身是下载到了客户端的不然浏览器没办法渲染。所以字体文件一定能在网络请求里抓到这份文件就是破局的钥匙。我们要做的是解析字体文件里的cmap表——这是一张记录编码到字形映射的表拿到这张表就能知道页面里的某个乱码编码对应的是哪个真实字形。但只有字形还不够因为字形是一个图形轮廓不是文字本身。你解析出来看到一个轮廓并不知道它到底代表数字几。这时候就需要搜索的动作把未知字形和已知字形做相似度匹配找出最接近的那个字符。1.2 案例的定位与适用场景这个案例叫每日spider案例意思是每天一个小的爬虫实战练手今天的主题落在字体搜索接口上。它解决的核心问题是拿到字体文件之后接下来怎么办。很多帖子教你识别字体反爬、教你下载woff文件但到解析映射这一步就含糊带过了。这个案例把字体文件解析成可用映射的全过程讲清楚并且把解析逻辑做成了一个独立的接口服务而不是写完就丢的临时脚本。适用场景非常典型采集电商平台的价格数据时数字全变乱码采集点评类网站时评分星星变成方块采集招聘站点时薪资范围变成一串看不懂的字符。这些场景里关键信息往往是数字和少量汉字整体数据量不大但频率高——每天、每个小时页面上的字体文件都在变。如果把解析逻辑写死在采集脚本里每改一次字体就要改一次代码维护成本很高。所以我才把字体解析单独拆出来做成一个接口采集脚本只需要把字体文件丢过去拿回映射字典剩下的事情不用操心。1.3 为什么要把字体解析做成分离接口一开始我也图省事直接在采集脚本里写死解析逻辑后来发现这是个坑。字体反爬是会变异的今天页面字体文件叫a.woff明天可能就变成b.ttf而且字形和字符的对应关系每次都在变。写死脚本意味着每次都要手动下载字体、跑一遍解析、看结果对不对效率极低。后来我把解析逻辑单独做成了接口服务好处有三个。第一是可复用多个采集任务可以共用同一个解析接口不需要每个任务重复实现一遍字体解析。第二是可缓存同一个字体文件的md5值不变时接口直接把上次的解析结果返回不用重新比对速度能快一个数量级。第三是易扩展以后遇到新的字体混淆方式只需要改接口这边采集脚本不用动。用生活里的话打比方一次性脚本像是每次去菜市场都要重新砍价接口则像办了张超市会员卡同一个东西以后直接扫一下就行省时省力还不会出错。2. 整体设计接口、脚本与调用链2.1 架构拆解从加密页面到明文数据这个案例的整体调用链可以做这样一个拆解。采集脚本先用requests请求目标页面拿到HTML源码后用正则表达式匹配出里面引用的字体文件URL或者是CSS文件里的字体链接。然后下载字体文件把它交给字体搜索接口接口解析出映射字典并返回采集脚本拿着这份映射字典去替换HTML里的加密实体最终得到可读的明文数据。我把这七八个环节理顺之后在代码里是这样组织的采集脚本(爬虫入口) ├─ 1. requests获取HTML / CSS ├─ 2. 正则提取字体文件URL ├─ 3. 下载woff/ttf到本地临时目录 ├─ 4. 调用字体搜索接口POST上传字体文件 ├─ 5. 接口返回 {加密编码: 真实字符} 映射字典 └─ 6. 采集脚本用该字典替换HTML中的乱码实体这里有个容易忽略的地方目标页面可能把字体文件链接写在CSS里而不是直接写在HTML里。所以采集脚本不能只抓一次页面可能要先拿到CSS文件的文本再从里面匹配字体文件的URL。我第一次做的时候只匹配了HTML结果什么都没匹配到排查半天才发现字体藏在CSS里。2.2 接口选型为什么用了FastAPI而不是Flask字体搜索接口我最终用了FastAPI来实现。选型理由很简单足够轻量自带API文档页面方便调试支持异步处理文件上传这种IO密集操作不阻塞其他请求响应模型支持类型校验接口返回的数据结构清晰采集脚本拿到就能直接用。对比一下常见的几个方案Flask更老牌、生态成熟但需要额外配CORS、写文档、做参数校验开发体验相对繁琐Django太重了为了一个字体解析功能起一个完整框架不划算FastAPI比较适中几十行代码就能把接口跑起来自动生成的Swagger文档在调试阶段非常方便。如果你的运行环境Python版本比较老装不上FastAPI用Flask写也是完全可以的核心逻辑都一样。2.3 脚本间参数传递的几种姿势做这个案例的时候我经常需要在一个Python脚本里调用另一个Python脚本比如采集脚本要调用一个单独的字形匹配脚本。这里踩过不少坑总结下来有三种常用的传参方式。最简单的是sys.argv直接往命令行后面追加参数适合传一个简单值比如字体文件路径。但要注意路径里如果有空格命令行解析会出问题传参时要记得加引号或者做转义。更规范一点的做法是用argparse它支持带参数名的传递方式比如--font-file ./a.woff脚本内部解析参数时还能带类型转换、默认值和必填校验容错性比raw的sys.argv高很多。如果两个脚本并行运行、需要传递复杂结构数据那就直接上subprocess把数据序列化成JSON通过标准输入输出交互。我在这个案例里用的是argparse方式因为参数虽然不多但需要清晰的语义。3. 核心代码实现与实操要点3.1 字体文件下载与预处理字体文件下载这一步看似简单其实有一个大坑很多字体文件并不是通过静态链接暴露的而是以base64编码的形式直接内嵌在CSS文件里。这时候如果还用requests去请求一个.woff的URL会直接404。所以下载之前要先判断字体文件是以什么形式存在的。如果是独立文件路径直接用requests下载即可注意设置一下User-Agent和Referer有些站点对静态资源请求有防盗链设置。如果是base64内嵌就要从CSS文件里把编码内容截出来再用base64.b64decode()解码成二进制。我用的是判断字符串里是否包含data:font/woff;base64,这个特征来区分两种情况。字体下载完成之后预处理阶段要注意字体文件格式。woff和ttf解析方式有细微差别但Java的fontTools库对两者都能处理。唯一需要留意的是解压后的字体文件要以二进制模式保存有些新手用open(file_path, w)写文本模式字体文件写出来就损坏了解析时直接报错。3.2 字形映射解析与搜索结果生成字体映射解析的核心代码是使用fontTools库核心逻辑是读取cmap表导出映射字典。我最初在案例中实现的是这样一段代码from fontTools.ttLib import TTFont import re def parse_font_mapping(font_path): font TTFont(font_path) cmap font.getBestCmap() mapping {} for code, glyph_name in cmap.items(): # 字体中的编码转成HTML实体格式方便和页面源码里的乱码对应 hex_code f#x{code:04x}; # glyph_name一般是数字形似操作需要清洗过滤 match re.search(runi([0-9A-Fa-f]{4}), glyph_name) if match: real_char chr(int(match.group(1), 16)) mapping[hex_code] real_char return mapping这里有一个容易踩的坑cmap表里的key是整数编码而页面源码里的乱码实体可能是十进制形式如#xe604;也可能就是原始编码形式。具体需要以页面源码的实际情况为准来转换。我第一次写这个函数的时候没有把整数编码转成HTML实体形式结果拿到的映射字典根本没办法去匹配页面上的乱码后面调试半天才发现是编码格式不匹配。但平时会遇到更复杂的情况字体文件里glyph_name并不是uniXXXX这种规范命名而是随机字母组合如glyph00012、c1_234这种。这时候就没办法直接从glyph_name还原真实字符必须做搜索动作——拿未知字形去和已知字形库做相似度匹配。我实现的搜索思路是提取字形的轮廓坐标序列做归一化后计算两个字形之间的轮廓距离选距离最小的作为匹配结果。这个方案不求百分百准确但准确率在九成以上对付常规字体混淆已经够用。3.3 正则表达式在提取和替换中的实战细节这个案例里正则表达式用得很频繁主要在三个位置出现提取页面里的字体文件URL、提取CSS里的base64字体内容、匹配HTML源码里的乱码实体。正则写得好能省很多事写得不好就会陷入无尽的调试循环。提取字体URL时我用的pattern是rurl\((.*?\.woff)\)但这里有个大坑有些CSS文件里字体链接是单引号有些是双引号甚至有的不带引号。我后来统一用一个更宽松的patternrurl\([\]?(.*?\.(?:woff|ttf))[\]?\)。排除了引号和路径后缀的问题。匹配HTML里的乱码实体时要注意贪婪匹配问题.*默认是贪婪的会一直匹配到最后一个符合条件的字符。如果两个实体之间隔着很多内容就会把中间内容全部吞掉。解决方法要么用非贪婪模式.*?要么用字符集限定范围。还有一个细节页面里的乱码实体是有固定格式的比如#xe604;、#xe603;这种形式。替换的时候直接用Python字符串的replace()方法就可以不一定用正则。但如果页面里同时出现了十进制和十六进制两种格式的实体就要先统一转成同一种格式否则映射字典会匹配不上。我在脚本里做了一个小工具函数把页面里的所有HTML实体统一转成十六进制的小写形式再交给后续替换逻辑处理。4. 运行环境与兼容性问题排查4.1 Windows下py命令不可用的原因与修复这个案例在Windows上跑的时候相信很多人遇到过py 不是内部或外部命令也不是可运行的程序或批处理文件这个报错。这个问题本质上是Python没有正确加入系统的环境变量。常见原因有两个。第一个原因是安装Python时没有勾选Add Python to PATH这个选项导致安装了Python之后命令行根本找不到python和pip命令。第二个原因是你电脑上的Python是Windows商店版安装了但位置很特殊命令行环境里搜不到它的启动命令。修复方法也分两种方法一重新配置环境变量 1. 右键“此电脑” → 属性 → 高级系统设置 2. 环境变量 → 在系统变量中找到Path 3. 添加Python安装目录例如 C:\Python312 和 C:\Python312\Scripts 4. 重启命令行窗口输入 py -V 验证 方法二使用Python官方启动器推荐 如果安装时顺带装了Python Launcher可以直接使用 py -3 命令 这个命令不依赖PATH环境变量优先推荐给多版本共存的情况我个人在项目里推荐用py -3而不是python因为Python Launcher能够自动帮我找到合适的Python版本尤其在机器上装了多个Python版本时优势很明显。如果你是Python打包工具的忠实用户那还是优先把环境变量配好很多东西后续更方便。4.2 Python 3.12跑旧脚本报错怎么办有几个热搜词提到了旧的.py文件在Python 3.12上运行出错这是真实存在的问题我在做字体解析时也踩过。Python 3.12做了很多清理工作删除了一批旧模块其中影响最大的就是distutils。很多老爬虫脚本里会写from distutils import util或使用distutils.spawn.find_executable这类函数在Python 3.12上直接ImportError。解决方式要看原来的代码用了什么。如果只是用distutils做一些通用的字符串处理可以直接改用packaging库功能基本能覆盖。如果确实用了distutils.dir_util等文件操作扩展可以用setuptools._distutils.util来替代需要执行以下替换# 旧代码 from distutils import util res util.strtobool(True) # Python 3.12兼容写法 from setuptools._distutils import util res util.strtobool(True)如果遇到的是asynchat、asyncore这类被移除的异步模块就要考虑重写代码用标准库的asyncio替代。自己在升级脚本过程中严重受挫的几类问题差不多是模块没了、函数重命名、默认参数变化、第三方库要求更高版本。排查的思路就是一个原则看报错信息定位到具体模块搜索官方文档确认替代方案。4.3 把字体搜索工具打包成exe这个案例做完之后如果你想分发给不装Python的小伙伴使用会遇到pycharm中把py程序变成exe这个问题。这本质上不是PyCharm的功能而是PyInstaller的活儿。PyCharm里只是集成调用了PyInstaller的命令。打包命令非常简单pyinstaller -F font_search_server.py-F参数代表打包成单个exe文件。但有几个坑必须提前说第一个坑是资源文件路径问题。字体搜索接口可能需要引用一些本地的字体文件或配置打包成exe之后这些资源文件不会被自动复制进去。需要手动在spec文件里添加这些资源并在代码里用sys._MEIPASS变量获取临时解压目录。第二个坑是杀毒软件误报。PyInstaller打包出的exe经常被360、Windows Defender报毒尤其是用了-upx压缩参数之后。我后来干脆不加-upx参数误报率降低了不少。第三个坑是打包后的exe体积偏大因为要把Python解释器和依赖库全部打进去。针对纯接口服务可以加--exclude-module排除不用的模块能适当减小体积。5. 常见问题速查表与避坑记录5.1 问题与解决方案速查表做这个案例过程中遇到的问题和解决方案我整理成了下面这个速查表方便大家直接对照排查问题现象可能原因解决方案提取不到字体URL字体链接在CSS而非HTML中先请求CSS再在CSS文本中匹配字体文件下载后解析报错内嵌base64字体没有解码识别data:font/woff;base64,并解码映射字典匹配不上页面编码cmap的key格式没转成HTML实体统一转成#xhhhh;格式再匹配正则匹配吞掉了多余内容贪婪匹配导致使用.*?非贪婪模式或用字符集限定py命令不存在环境变量未配置使用py -3或添加PATHPython 3.12导入distutils失败该模块已从标准库移除改用setuptools._distutils或packaging打包exe后字体文件找不到资源文件未包含使用sys._MEIPASS路径并修改spec文件字体解析太慢每次重新比对全部字形用字体文件md5做缓存存离线解析结果5.2 我的几次翻车现场这些坑我是真的踩过一次心疼自己一秒钟分享出来大家能少走点弯路。第一次翻车是字体文件下载后少写了二进制模式。当时存文件用的open(font_path, w)字体文件看着也保存下来了但解析时直接报TTLibError: cannot read head table排查了半小时才发现是文件写坏了。后来统一改成open(font_path, wb)问题消失。这个教训让我养成了个习惯凡是处理二进制文件一律用二进制模式读写不给自己留模糊空间。第二次翻车是编码格式问题。字体解析出来的映射是#xe604;: 3这种形式但页面源码里的实体是#xe604;没加引号看起来是对得上的但实际替换时死活替换不了。调试之后发现页面里那个#xe604;实际上是字符串#xe604;而我从cmap转出来的是字符串#xe604;看着一样但字符构成可能不同——比如分号一个是全角一个是半角。这种问题真的只能靠肉眼盯输出结果的repr表示才能发现。第三次翻车是缓存策略写错了。一开始用字体文件名做缓存key结果不同页面下相同文件名的字体内容是变化的直接导致缓存穿透。后来改用文件内容的md5值做key这样只要内容一样就命中缓存内容变化就一定重新解析逻辑上稳固可靠。这个思路可以推广到任何需要做缓存的爬虫场景文件名永远不要当唯一标识内容指纹才是正道。6. 最后的几个实操心得字体搜索接口这个案例做下来我最大的感受是别一上来就追求完美先把主链路打通再逐项加缓存、加相似度匹配。主链路就是下载字体 → 解析映射 → 替换密文这三步跑通了再说优化的事。接口做出来后还得考虑缓存设计我建议缓存key直接用字体文件的md5值否则不同页面同名的字体文件会反复解析白白浪费IO和CPU。另外打包exe分发之后记得把字体文件路径这类配置单独拎出来写到外部配置文件里别硬编码在代码中不然换台机器这个工具就废了。我自己的经验是做这种反爬对抗类的爬虫项目最值钱的不是代码本身而是遇到变化时的应对思路。字体反爬一定会天天变今天字体编码是uni开头明天可能就变成随机字符串今天手动匹配就行明天可能就上了动态混淆规则。只要你的接口设计足够独立、健壮不管对方怎么变你只需要改解析模块的细节采集侧完全不用动。这套解析逻辑和采集逻辑分离的思路不止适用于字体应对其他加密混淆的方案也基本适用。顺手再说一句这个接口服务如果部署在服务器上建议加个简单的token鉴权否则别人发现你的端口后可以随意调用你的解析能力白白消耗你的资源。就这些希望对正在折腾字体反爬的你有点帮助。
返回列表