字体反爬把关键文字换成 Unicode 私有区码位,页面看得见、源码里全是乱码。本文讲清它的原理、六种实现方式的强度差异、怎么生成映射字体、怎么防一次反解,以及它挡不住什么、代价有多大。
Q:字体反爬到底是什么原理?
- 网页上的文字能显示出来,是因为浏览器按字符的码位去字体文件里找对应字形。字体反爬改的就是这层映射:把真实字符挪到 Unicode 私有区(U+E000 到 U+F8FF),字体文件里只放私有区码位对应的字形。
- 于是 HTML 里写的是
这样的字符,源码、复制、纯文本抓取拿到的都是看不懂的私有区字符;浏览器加载字体后把它画成数字 8。 - 关键点:数据本身没加密,加密的是「怎么读」。谁拿到字体文件和映射表,谁就能还原。
- 所以它天然只能抬成本。真正的访问控制必须落在服务端,字体这层是给批量采集加一道工程门槛。
Q:常见实现方式有几种,强度差在哪?
- 静态替换字体:一套字体长期不变。攻击者下载一次、用 fontTools 读一遍 cmap 就永久破解,只能挡住最粗糙的采集脚本。
- 动态生成字体:每次请求重新生成码位映射与字形顺序,字体文件跟着变。这是主流做法,破解成本最高。
- 只替换关键字段:价格、库存、电话、评分。影响面小、性价比最高,绝大多数业务场景够用。
- CSS 伪元素拼字:
::before里写私有区字符,HTML 正文干脆没有文字。挡得住正则,挡不住渲染。 - 图片拼字与雪碧图:把字画成图片,用 background-position 定位。可读性最差,也最容易被 OCR。
- SVG path 内联:字形是路径数据,必须渲染后才能识别。体积大,一般只用于少量字段。
Q:怎么用 fontTools 生成一套映射字体?
- 思路是拿一套基础字体,改它的 cmap 表,再子集化输出 woff2。
from fontTools.ttLib import TTFont
import random
chars = "0123456789"
src = TTFont("base.ttf")
# 私有区码位随机打乱后分配给真实字符
codes = random.sample(range(0xE000, 0xE200), len(chars))
mapping = dict(zip(chars, codes))
# 重写 cmap:私有区码位指向原字符的字形
best = src.getBestCmap()
new_cmap = {code: best[ord(ch)] for ch, code in mapping.items()}
for table in src["cmap"].tables:
table.cmap = dict(new_cmap)
src.save("out.ttf")
- 再子集化并转 woff2,体积能压到 20KB 以内:用
pyftsubset只保留 U+E000 到 U+E200 这一段码位,输出格式选 woff2 即可。 - 每次请求都要重新生成,映射表随响应一起下发,字体和映射必须配套,否则用户看到的就是错字。
- 字体里可以塞几个空字形噪声码位,让攻击者没法靠「码位数量等于字符数量」反推。
Q:前端怎么接,缓存怎么处理?
- 用
@font-face声明字体,地址带上本次会话的随机 token,避免浏览器或 CDN 复用旧字体。
/* 字体地址带 token,与本次映射表绑定 */
@font-face {
font-family: "obf";
src: url("/font/8f3a2c.woff2") format("woff2");
font-display: block;
}
.price { font-family: "obf", sans-serif; }
- 字体文件要 no-store 或极短缓存。用户拿到上一次的字体,价格就会显示错,这一点比防爬重要得多。
- 用
font-display: block而不是 swap。swap 会先渲染系统字体,用户会看到一小段乱码或错误数字,体验很糟。 - Nginx 侧给字体路径单独加
Cache-Control: no-store,同时单独限速,因为字体文件本身就是攻击者最想批量下载的东西。
Q:怎么防止被一次反解?
- 静态字体是纸糊的。核心只有一条:映射必须动态,且与请求绑定。
- 同一字符在不同会话对应不同码位。攻击者拿两份字体交叉对比,只能得到「这两个码位不是同一个字」,得不到「这个码位是几」。
- 字形顺序打乱、字体名随机、woff2 内部表顺序扰动,这些只增加工作量,真正起作用的是动态映射。映射表要是明文写在前端 JS 里,等于没加密。
- 字体下载次数单独计数:同一个 IP 十分钟拉几百次字体,基本可以判定为破解行为,给这个路径配独立限流。
- 别忘纵深:字体只影响「读文本」。对方直接调后端接口拿明文 JSON,字体一点用都没有。
Q:爬虫一般怎么绕过字体反爬?
- 下载字体反解:fontTools 读 cmap 拿到码位到字形的对应,再对字形做模板匹配或 OCR。静态映射一次就破。
- 渲染后取文本:无头浏览器截图再 OCR,或者用 canvas 测量字符宽度、配合字形轮廓比对。
- 多份字体交叉比对:同一页面反复请求,统计同一字段的码位变化,再结合已知数量级(比如价格总在某个区间)做推断。
- 直接打接口:最省事也最常见。只要后端返回明文,前端做什么都白搭。
- 结论很清楚:字体反爬是抬成本的手段,价值在于把「写个 requests 就能拿」变成「要搭一套渲染加 OCR 流水线」。
Q:上字体反爬要付什么代价?
- SEO:搜索引擎未必加载字体,关键内容加密后可能抓不到。要对验证过的蜘蛛放行,或给蜘蛛输出明文的服务端渲染版本。
- 无障碍:屏幕阅读器读出来是私有区乱码,合规上直接不合格。
- 复制粘贴失效:用户想复制订单号、价格去比对,复制到的是乱码,客诉会明显上升。
- 显示风险:字体加载失败或映射错位时,页面显示的是错误但看起来正常的数字,比乱码危险得多。价格错一位就是事故。
- 字体版权:拿商业字体改造再分发,授权要过关,别为了反爬惹上字体厂商。
Q:怎么配才不误伤正常用户和搜索引擎?
# 字体路径单独限速,防批量下载
limit_req_zone $binary_remote_addr zone=fontzone:10m rate=6r/m;
location ^~ /font/ {
limit_req zone=fontzone burst=3 nodelay;
limit_req_status 429;
add_header Cache-Control "no-store" always;
add_header X-Robots-Tag "noindex" always;
}
- UA 能伪造,只当第一层筛子。真要对蜘蛛放行,得反解 DNS 确认来源域名,再决定是否输出明文。
- 字体路径加
noindex,避免字体文件本身被搜索引擎收录。 - 关键动作要有开关:一个配置项控制明文与混淆两种渲染,出问题十秒切回去。
Q:效果怎么衡量,要和其他反爬手段怎么配?
- 看三个指标:字体文件下载次数的 IP 分布、内容外泄的发现时间、页面错误率(映射错位会直接反映在客诉里)。
- 分层看更清楚:字体反爬解决「复制文本」,签名与 JS 指纹解决「调用方是不是我的客户端」,频率与行为解决「是不是人」,账号维度解决「登录之后慢慢搬」。
- 内容侧还有一招成本更低:在展示或导出数据里埋暗记(零宽字符、字段顺序、错字替换)。对方站上出现你的标记,就能定位泄露源。
- 别指望单一手段。真正拦得住的是多层叠加之后的成本曲线,而不是某一种技术。
Q:还有什么补充建议?
- 只加密真正值钱的字段。整页文字都混淆,维护成本和客诉成本会远高于收益。
- 上线前跑一轮自测:断网加载、清缓存刷新、字体 404 三种情况下页面显示对不对。映射错位这类 bug 在测试环境极难复现。
- 把字体生成与映射下发做成可开关的中间件,出问题能单独关掉,不影响其他反爬规则。
- 定期比对自家内容在外部出现的形态。字体反爬挡的是采集成本,真正能定责的还是暗记与取证。