摘要 :字体反爬虫通过 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,可以根据网页内的中文文本自动生成对应字体文件,操作简洁,但依赖 node.js 且只支持 ttf,不支持 otf;
  • 百度的 font-min 有 windows 客户端,生成字体的文字需要自行指定;
  • 阿里的 iconfont 可以在线生成字体,但是字体有限,开源的核心 font-carrier 支持的字体方式较多,但是操作较繁琐。

更细粒度的自定义

如果使用过程中遇到问题,或者想要做其他定制化(比如使用 WebFont 进行反爬 ),则可以使用 fonttools Python 工具。

Font­Tools  是一套以ttx为核心的工具集,用于处理与字体编辑有关的各种问题,程序用  Python  编写完成,代码开源,具有良好的跨平台性。

Font­Tools  由以下 4 个程序组成:

  • ttx 可将字体文件与  xml  文件进行双向转换
  • pyft­merge 可将数个字体文件合并成为一个字体文件
  • pyft­sub­set 可产生一个由字体的指定字符组成的子集
  • pyftin­spect 可显示字体文件的二进制组成信息

安装 pip 包

pip install fonttools

生成字体子集

pyft­sub­set可提取一个字体的部分字符,产生一个只由它们组成的新字体。通过这一子集化技术,可有效缩小字体文件的体积,便于网络传输。

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')