1. 字体加密原理与Python实战解析
最近在爬取某电商网站时遇到了一个棘手问题——页面显示的价格明明是"¥128",但用requests获取的HTML源码里却是一堆乱码符号。这种看似简单的反爬手段背后,其实是网站采用了字体加密技术。作为爬虫开发者,我们必须掌握破解字体加密的核心方法。
字体加密的本质是网站自定义了特殊字体文件,将常规字符映射到乱码或特殊符号。比如数字"1"可能被映射成Unicode中的""这类生僻字。这种技术最早出现在电商、票务等对价格敏感的平台,现在已蔓延到各类内容网站。
2. 字体加密的三种实现方式
2.1 动态字体加载
网站每次访问都会生成新的字体文件,字符编码随机变化。这种方式的破解难点在于需要实时获取最新字体映射关系。典型代表是某知名票务网站,他们的字体文件URL包含时间戳参数:
https://static.ticket.com/fonts/5a3b8c.woff?t=2023061512302.2 字符位置随机化
字体文件中字符的编码顺序每次生成都不同,但字形本身不变。比如第一次访问时"1"对应Unicode E001,第二次可能变成E00A。这种加密需要通过字形比对来破解。
2.3 复合字形加密
将多个字符组合成一个特殊字形,比如把"¥128"整体设计成一个特殊符号。这种加密需要先拆解复合字形,再提取原始字符。
3. Python破解字体加密全流程
3.1 识别字体加密
当发现页面显示内容与源码不符时,按F12打开开发者工具:
- 在Elements面板搜索"font-face"
- 查看Network面板的Font类型请求
- 检查CSS中是否包含@font-face规则
3.2 下载字体文件
使用requests自动下载woff/ttf字体:
import requests from fontTools.ttLib import TTFont font_url = "https://example.com/font.woff" font_data = requests.get(font_url).content with open("encrypted.woff", "wb") as f: f.write(font_data) font = TTFont("encrypted.woff")3.3 解析字体映射
提取字体文件的cmap表和glyf表:
cmap = font.getBestCmap() glyf = font["glyf"] # 打印字符映射关系 for code, name in cmap.items(): coordinates = glyf[name].coordinates print(f"Unicode: {hex(code)}, Glyph: {name}, Coords: {coordinates}")3.4 建立映射字典
通过坐标比对识别真实字符:
def get_char_mapping(font): standard_font = TTFont("standard.woff") # 提前准备的基准字体 mapping = {} for code, name in font.getBestCmap().items(): target_glyph = font["glyf"][name] # 遍历基准字体中的字形进行比对 for std_code, std_name in standard_font.getBestCmap().items(): if glyphs_similar(target_glyph, standard_font["glyf"][std_name]): mapping[chr(code)] = chr(std_code) break return mapping def glyphs_similar(g1, g2, threshold=0.9): # 基于轮廓坐标计算相似度 overlap = calculate_overlap(g1.coordinates, g2.coordinates) return overlap >= threshold4. 实战案例:电商价格解密
以某电商网站为例,完整破解流程:
- 发现价格显示为""但源码是"& #xe011;& #xe012;& #xe013;"
- 从CSS中找到字体URL:
//static.mall.com/font/price.woff - 下载并解析字体:
font = TTFont("price.woff") cmap = font.getBestCmap() # 得到{0xe011: "uniE011", ...}通过基准字体比对,建立映射:
- uniE011 → "1"
- uniE012 → "2"
- uniE013 → "8"
实现替换函数:
def decrypt_price(html): mapping = {"\ue011": "1", "\ue012": "2", "\ue013": "8"} for encrypted, real in mapping.items(): html = html.replace(encrypted, real) return html5. 进阶技巧与反反爬策略
5.1 动态字体处理
当遇到每次访问都变化的字体时:
- 实现字体缓存机制
- 设置请求头模拟浏览器行为
- 使用自动化工具定期更新字体库
from hashlib import md5 def get_font_mapping(url): cache_key = md5(url.encode()).hexdigest() if cache_key in font_cache: return font_cache[cache_key] # 下载并解析新字体 new_mapping = parse_font(download_font(url)) font_cache[cache_key] = new_mapping return new_mapping5.2 机器学习辅助识别
对于复杂字形,可以训练CNN模型进行识别:
import tensorflow as tf from PIL import Image, ImageDraw def create_training_data(font): images, labels = [], [] for code, name in font.getBestCmap().items(): img = render_glyph(font["glyf"][name]) images.append(img) labels.append(get_true_char(name)) # 已知的真实字符 return np.array(images), np.array(labels) model = tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3,3), activation='relu'), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dense(10) # 0-9数字分类 ])5.3 常见问题排查
- 字体加载失败:检查Referer和User-Agent头
- 映射不准:调整字形相似度阈值
- 性能瓶颈:使用LRU缓存已解析的字体
- 动态内容:结合Selenium等工具获取完整DOM
6. 法律与伦理边界
虽然技术上讲我们可以破解字体加密,但需要注意:
- 遵守robots.txt协议
- 控制请求频率(建议≥3秒/次)
- 不爬取敏感数据(用户信息、版权内容等)
- 设置合理的爬取时间(避开高峰时段)
建议在代码中加入延迟和限制:
import time from random import uniform def throttled_request(url): time.sleep(uniform(1, 3)) # 随机延迟 headers = { "User-Agent": "Mozilla/5.0", "Referer": "https://example.com" } return requests.get(url, headers=headers)我在实际项目中发现,字体加密虽然增加了爬虫难度,但只要掌握核心原理,配合Python强大的字体处理库,完全可以实现高效解密。关键是要理解字体文件的结构,以及如何通过程序化的方式建立字符映射关系。对于特别复杂的案例,可以考虑结合OpenCV进行图像识别,但这通常会显著增加处理时间。