背景

今年年初我启动了 JobBot 项目——一个自动化的招聘信息采集与投递辅助系统。项目的初期目标很简单:定时抓取 Boss 直聘上目标岗位的薪资、技能要求和职位描述,做数据分析和岗位匹配。然而在实际开发中,第一个遇到的障碍并不是 API 签名校验或 IP 频率限制,而是一个看起来更「底层」的问题——页面上显示的是乱码。

打开 Chrome DevTools 查看元素,薪资字段显示的是  这样的 Unicode 私有使用区字符。普通爬虫用 requests 库拿到的 HTML 里,这些字段就是一堆不可读的 PUA 编码,根本无法从中提取有效数据。这就是 Boss 直聘采用的 PUA 字体编码反爬机制。

🔍 现场实录

首次发现该问题时,我以为是编码设置错了。试了 UTF-8、GBK、Latin-1 —— 全都不行。直到在 Network 面板里看到字体文件请求 boss.woff,才意识到这不是编码问题,而是字体层面的加密。

什么是 PUA 字体编码

PUA(Private Use Areas,私有使用区)是 Unicode 标准中保留给自定义字符的码位范围。主要的几个区间是:

字体厂商可以用这些码位来存放自定义图标(如 FontAwesome 的图标字符)。但 Boss 直聘的做法是:把数字 0123456789 和常用单位字符(如 K· 等)映射到 PUA 区域的码位上,然后通过自定义字体文件(WOFF)将 PUA 码位渲染为对应的数字图形。这样用户看到的是正常的数字,但浏览器 DOM 和爬虫拿到的是乱码。

举个例子:U+E001 可能对应数字 "1"U+E002 对应 "2"U+E00A 对应 "K"
页面上的 K 看起来是 "12K" 但 DOM 里全是乱码。

这种方案的精妙之处在于:

解密思路

破解的关键在于建立 PUA 编码到真实字符的映射关系。整体的解密流程分为三个步骤:

1. 下载自定义字体文件

打开 Boss 直聘的职位详情页,在 Network 面板中过滤 .woff 请求,可以看到页面加载了一个自定义字体文件。URL 通常是这样的:

# 字体文件 URL 示例
https://img.bosszhipin.com/static/static/font/"xxxxx".woff

现代反爬方案可能使用 WOFF2 格式,并且 URL 中带有时间戳或一次性 Token 防止缓存。使用 Camoufox 浏览器渲染页面时,字体文件会自动加载,我们可以通过拦截响应来获取字体文件内容。

2. 解析字体映射表

下载到 WOFF 文件后,我们需要解析字体内部的 CMAP 表(Character-to-Glyph Index Mapping Table)。CMAP 表记录了从 Unicode 码位到字形索引的映射关系。这里要注意:PUA 码位映射到的字形,是 Boss 直聘自定义绘制的,而不是标准的数字字形。

使用 fonttools 库(Python)可以轻松读取 WOFF 字体文件:

from fontTools.ttLib import TTFont
import requests

# 下载字体文件
font_url = "https://img.bosszhipin.com/static/static/font/xxxxx.woff"
resp = requests.get(font_url, headers=headers)
with open("boss.woff", "wb") as f:
    f.write(resp.content)

# 解析 CMAP 表
font = TTFont("boss.woff")
cmap = font.getBestCmap()
print(cmap)
# 输出:{0xe001: 1, 0xe002: 2, 0xe003: 3, ...}

这里 cmap 的输出中,key 是 PUA 码位的十进制值,value 是字形索引。但是字形索引 并不直接等于真实字符!字形索引只是字体内部的一个编号。要得到真实字符,需要将字体渲染为图像,然后用 OCR 识别每个字形对应的数字——或者更巧妙的方法:利用已知的「参考字符」做一一映射。

一个更高效的方案是:从字体中提取每个字形的轮廓(Glyph)数据,通过比对字形轮廓的哈希值来确定它对应哪个数字。因为数字 0-9 的字形轮廓是固定的,不同字体文件只是码位映射不同,但同一数字的字形轮廓是相同的。

from fontTools.pens.t2Pen import T2Pen
import hashlib

glyph_hash_map = {}
for cmap_code, glyph_name in cmap.items():
    glyph = font.getGlyphSet()[glyph_name]
    # 提取轮廓数据并计算哈希
    pen = T2Pen(glyph)
    glyph.draw(pen)
    h = hashlib.md5(str(pen.points).encode()).hexdigest()
    glyph_hash_map[cmap_code] = h

# 与已知数字轮廓哈希对照表匹配
known_digit_hashes = load_known_digit_hashes()
mapping = {}
for code, h in glyph_hash_map.items():
    if h in known_digit_hashes:
        mapping[chr(code)] = known_digit_hashes[h]

3. DOM 层面替换加密文本

拿到 PUA → 真实字符的映射表后,在浏览器环境中遍历 DOM 树,找到包含加密文本的节点,进行替换:

// 在页面中执行替换
function decryptText(node, mapping) {
    if (node.nodeType === Node.TEXT_NODE) {
        let text = node.textContent;
        let decrypted = "";
        for (let char of text) {
            decrypted += mapping[char] || char;
        }
        if (decrypted !== text) {
            node.textContent = decrypted;
        }
    }
    node.childNodes.forEach(child => decryptText(child, mapping));
}

这套方案在 JobBot 中经过两个月的实际运行验证,解密成功率达到 99.5% 以上。少数失败的 case 是字体文件加载超时或映射表变化导致临时未命中。

Camoufox 反检测方案

字体解密只是第一步。Boss 直聘的反爬体系远不止字体编码。在实际运行中,还需要对抗浏览器指纹检测、IP 频率限制、Cookie 合法性校验等多层防护。我们使用了基于 Camoufox 的自动化浏览器方案:

WebGL / Canvas / 字体指纹模拟

Camoufox 是一个专门的反指纹检测浏览器,它通过以下方式来模拟真实浏览器环境:

Cookie 持久化与会话恢复

Boss 直聘会对会话进行滑动窗口校验。如果检测到某个会话的访问频率异常,或者 Cookie 中缺少某些关键的校验字段(如 __zp_seo__lr 等),就会弹出滑块验证码。

我们的方案是:

滑动窗口频率控制

即使有最完美的指纹模拟,请求频率过高依然会被封杀。我们实现了一个 自适应频率控制器

class RateController:
    def __init__(self):
        self.base_interval = 5  # 基础间隔(秒)
        self.window_size = 60   # 滑动窗口(秒)
        self.max_requests = 10   # 窗口内最大请求数
        self.timestamps = []

    def wait_if_needed(self):
        now = time.time()
        # 清理过期的时间戳
        self.timestamps = [t for t in self.timestamps
                          if now - t < self.window_size]
        if len(self.timestamps) >= self.max_requests:
            sleep_time = self.timestamps[0] + self.window_size - now
            if sleep_time > 0:
                time.sleep(sleep_time)
        self.timestamps.append(time.time())

这套控制器不仅限制了请求速率,还加入了随机抖动(±20% 的间隔波动)和节假日感知功能——在工作日的非工作时间段自动降低频率,在周末使用更慢的速率,模拟真实用户的浏览行为。

💡 经验之谈:反爬对抗中最容易被忽略的是「行为模式」层面的检测。大多数网站的反爬系统已经不再是简单的 IP 频率限制,而是通过机器学习模型分析用户的行为序列。如果你的程序访问页面总是按照固定顺序、固定间隔,即使指纹伪装得再好,也很容易被判定为爬虫。

字体编码反爬的演进与防御思路

研究过程中,我注意到 Boss 直聘的字体编码方案也在不断迭代:

针对这些演进,我们的解密方案也需要持续升级。目前 V3 阶段的噪声字形可以通过「交叉引用未在 DOM 中出现的码位」来过滤——页面中实际使用的 PUA 码位是有限的,不在 DOM 中的码位大概率是噪声。

总结

字体编码反爬是目前较为高级且隐蔽的反爬手段之一。它不同于传统的 IP 封禁、User-Agent 校验或 JS 混淆,而是在浏览器最基础的渲染环节——字体层——进行加密。这使得大多数基于 HTTP 库的爬虫完全无法读取数据。

破解这一机制的核心思路是:

  1. 理解 PUA 字体编码的原理——将数字字符映射到 Unicode 私有使用区
  2. 通过字体解析工具(fonttools)建立 PUA 码位到真实字符的映射
  3. 结合自动化浏览器(Camoufox)在 DOM 层面实时替换加密文本
  4. 辅以完整的反指纹检测策略,确保浏览器环境不被识别

最终,JobBot 的 Boss 直聘适配模块运行稳定,每天定时抓取目标岗位数据,解密成功率维持在 99% 以上。这一整套方案不仅适用于 Boss 直聘,对其他使用字体编码反爬的网站(如部分招聘平台、电商平台的价格加密等)也有参考价值。

当然,反爬技术是一把双刃剑。作为开发者,在对抗反爬机制的同时,我们也应当遵守网站的 robots.txt 相关规则,控制合理的数据采集频率,不要对目标服务造成压力。技术无罪,但使用技术的方式决定了它的价值。

🔗 相关资源

● fonttools 官方文档:https://github.com/fonttools/fonttools
● Camoufox 反检测浏览器:https://github.com/daijro/camoufox
● Unicode PUA 规范:https://unicode.org/faq/private_use.html