使用 fonttools 自定义字体实现 WebFont 反爬虫
/ / 阅读数:3678摘要 :字体反爬虫通过 CSS3 font-face 功能调用自定义的字体文件来渲染网页中的文字,网页中的文字是被修改过的任意 > 编码,无法复制或简单爬取文字内容 ï¼ 本文使用 fonttools 来生成反爬字体。
CSS3 引入了 font-face 功能 ,可以让网页自动下载自定义字体, 而不需要用户系统安装。目前浏览器对 WebFont 的支持非常好,在 caniuse 上可以看到基本是全平台兼容, 网页上的特殊字体再也不用通过切图来解决了。
WebFont 的优势
相对图片,WebFont 有如下优势:
- 支持选中、复制
- 支持 Ctrl+F 查找
- 对搜索引擎友好
- 支持工具翻译
- 支持无障碍访问,支持朗读
- 字体是矢量图形,支持矢量缩放,自动适配高清屏
- 文本修改方便
- 字形可以重复利用,节省网络资源
关于字体的格式,常见的有 EOT, TTF/OTF, WOFF, WOFF2, SVG 这几种,早期为了兼容通常会见到这种写法:
@font-face { font-family: '字体名'; src: url('字体名.eot'); /* IE9 + */ src: url('字体名.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('字体名.woff') format('woff'), /* 现代浏览器 */ url('字体名.ttf') format('truetype'), /* Safari, Android, iOS */ url('字体名.svg#grablau') format('svg'); /* Legacy iOS */ } |
但是现在各平台尤其是移动端对 .ttf 和 woff 格式的支持已经很好,所以通常只需要加载这两种字体即可。
中文 WebFont 的问题
当你需要显示特殊中文字体时,你会发现直接用 font-face 引用中文字体是极其不现实的。 因为英文只有 26 个字母,加上数字和标点符号,一套字体不过几十 KB; 而汉字却有数万个,导致字体文件通常有好几 MB 甚至 几十 MB。 直接页面上引用的话,不论是对服务器带宽资源,还是用户加载速度,都是极大的挑战。
解决方案
解决办法就是对字体进行子集化(subsetting),将页面中用到的少量特殊字体提取为一个定制的字体文件。
如果你使用 node.js,业界已经有比较成熟的软件可以使用:
- 腾讯: font-spider (核心使用的 font-min)
- 百度: font-min
- 阿里巴巴: icon-font font-carrier
以上三家各有优劣:
- 腾讯的 font-spider 的优化核心是用的百度的 font-min,可以根据网页内的中文文本自动生成对应字体文件,操作简洁,但依赖 node.js 且只支持 ttf,不支持 otf;
- 百度的 font-min 有 windows 客户端,生成字体的文字需要自行指定;
- 阿里的 iconfont 可以在线生成字体,但是字体有限,开源的核心 font-carrier 支持的字体方式较多,但是操作较繁琐。
更细粒度的自定义
如果使用过程中遇到问题,或者想要做其他定制化(比如使用 WebFont 进行反爬 ),则可以使用 fonttools Python 工具。
FontTools 是一套以ttx
为核心的工具集,用于处理与字体编辑有关的各种问题,程序用 Python 编写完成,代码开源,具有良好的跨平台性。
FontTools 由以下 4 个程序组成:
ttx
可将字体文件与 xml 文件进行双向转换pyftmerge
可将数个字体文件合并成为一个字体文件pyftsubset
可产生一个由字体的指定字符组成的子集pyftinspect
可显示字体文件的二进制组成信息
安装 pip 包
pip install fonttools |
生成字体子集
pyftsubset
可提取一个字体的部分字符,产生一个只由它们组成的新字体。通过这一子集化技术,可有效缩小字体文件的体积,便于网络传输。
pyftsubset <字体文件> --text=<需要的字形> --output-file=<输出> |
可以使用--help
命令,选项非常非常多,非常健壮和实用。
使用 fonttools 修改字体编码(反爬)
使用 FontBuilder 来提取子集,并修改 glyphNames 和 Unicode 编码,达到反爬的目的:
import random import struct from fontTools.ttLib import TTFont from fontTools.fontBuilder import FontBuilder from fontTools.pens.ttGlyphPen import TTGlyphPen def font_minify(name='Microsoft Yahei.ttf'): orig_font = TTFont(name) text = 'x0123456789' # https://github.com/fonttools/fonttools/blob/3.41.2/Lib/fontTools/fontBuilder.py#L28 familyName = 'myfont' styleName = 'Regular' nameStrings = { 'familyName': familyName, 'styleName': styleName, 'psName': familyName + '-' + styleName, 'copyright': 'Created by Allen', 'version': 'Version 1.0', 'vendorURL': 'https://seealso.cn', } glyph_names = ['.notdef', 'null', 'x', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] rand_names = ['.notdef'] + random.sample(glyph_names[1:], len(glyph_names[1:])) glyphs, metrics, cmap = {}, {}, {} # https://zh.wikipedia.org/wiki/Unicode字符平面映射#E000-F8FF私用区 codes = random.sample(range(0xE000, 0xF8FF), len(rand_names) * 3) # https://github.com/fonttools/fonttools/blob/3.41.2/Tests/pens/ttGlyphPen_test.py#L22 glyph_set = orig_font.getGlyphSet() pen = TTGlyphPen(glyph_set) for i, (gn, rn) in enumerate(zip(glyph_names, rand_names)): glyph_set[gn].draw(pen) glyphs[rn] = pen.glyph() metrics[rn] = orig_font['hmtx'][gn] if i == 0: continue cmap[codes[3 * i]] = rn cmap[codes[3 * i + 1]] = rn cmap[codes[3 * i + 2]] = rn hhea = { 'ascent': orig_font['hhea'].ascent, 'descent': orig_font['hhea'].descent, } fb = FontBuilder(orig_font['head'].unitsPerEm, isTTF=True) fb.setupGlyphOrder(rand_names) fb.setupCharacterMap(cmap) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader(**hhea) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() fb.save(f'{familyName}.ttf') |