JavaScript逆向与爬虫实战——基础篇(css反爬之动态字体实现原理及绕过)

目录

  • 一、什么是字体文件?
  • 二、认识字体文件的构成
    • [2.1 字体元数据(Font Metadata)](#2.1 字体元数据(Font Metadata))
    • [2.2 字形数据(Glyph Data)](#2.2 字形数据(Glyph Data))
    • [2.3 字符映射(Character Mapping)](#2.3 字符映射(Character Mapping))
    • [2.4 度量信息(Metrics)](#2.4 度量信息(Metrics))
    • [2.5 提示信息(Hinting Information)](#2.5 提示信息(Hinting Information))
    • [2.6 OpenType 与 TrueType 特性](#2.6 OpenType 与 TrueType 特性)
  • 三、字体文件实践与浏览器渲染流程
    • [3.1 下载并选择字体](#3.1 下载并选择字体)
    • [3.2 子集化(删除多余字符)](#3.2 子集化(删除多余字符))
    • [3.3 使用 Python + fontTools 转换成 XML](#3.3 使用 Python + fontTools 转换成 XML)
    • [3.4 浏览器字体加载与渲染机制](#3.4 浏览器字体加载与渲染机制)
  • 四、反爬案例实现
  • 五、绕过字体反爬实战
    • [5.1 某眼案例](#5.1 某眼案例)
    • [5.2 某职案例](#5.2 某职案例)
    • [5.3 SpiderDemo第8题](#5.3 SpiderDemo第8题)

一、什么是字体文件?

在计算机世界中,字体文件(Font File) 是一种用于存储文字外观信息的文件。它不仅决定了文字的形状、大小、粗细、间距等视觉特征,还包含了字符与图形(glyph)之间的 映射关系 。简单来说,字体文件就像一份 "字形图纸"------告诉系统每个字符该如何被绘制出来。常见的字体格式有:

格式 全称 特点 主要用途
TTF TrueType Font 使用 TrueType 轮廓(glyf 表),跨平台兼容性强 桌面、网页通用
OTF OpenType Font TrueType 的扩展,支持高级排版功能,包含 CFF 轮廓 高级排版、印刷
WOFF Web Open Font Format 针对网页优化的压缩版 TTF/OTF,加载快 Web 页面字体
WOFF2 Web Open Font Format 2 进一步提升压缩率,基于 Brotli 压缩算法 现代浏览器 Web 字体
EOT Embedded OpenType 微软为 IE 设计的嵌入式字体格式 旧版 IE 兼容用途

字体文件在操作系统与浏览器中的角色:在操作系统层面,字体文件是 系统级资源 。当我们在 Word、记事本或任何桌面程序中输入文字时,系统会调用对应的字体文件,根据字体文件中的字形信息,将 "字符编码" 转换成实际可显示的图形------这就是我们看到的字。而在浏览器中,字体文件的角色更加灵活。网页并不依赖用户本地的字体,而是可以通过 CSS 的 @font-face 规则 动态引入外部字体文件 。这意味着网站开发者可以:使用自定义字体来统一视觉;或者(重点来了)通过自定义字体文件来控制特定字符的显示效果

二、认识字体文件的构成

在开始研究 字体反爬 之前,我们需要先了解一下字体文件本身的结构。字体文件不仅仅是用来展示文字的,它实际上是一个包含 矢量数据、字符映射、排版信息、渲染提示 等多种信息的复杂容器。

2.1 字体元数据(Font Metadata)

元数据是字体的 "基本信息",描述字体的身份和属性。主要包括:

  1. 字体名称(Font Family Name) :比如 Times New RomanArialRoboto 等。它定义了字体系列的名称,便于系统或应用识别。
  2. 字体样式(Font Style) : 包括常规(Regular)、粗体(Bold)、斜体(Italic)、粗斜体(Bold Italic)等样式,用于区分同一字体家族下的不同外观。Windows 中的字体文件都存储在位置:C:\Windows\Fonts
  3. 版权与许可信息(Copyright / License):标明字体的作者、版权归属、授权范围。比如某些字体只能个人使用,商用需付费。
  4. 版本号与厂商信息(Version / Vendor):用于区分不同版本的字体更新,以及识别字体供应商。

双击打开 ,Windows 会自动启动字体查看器(Font Viewer); 这个查看器其实只展示了一部分元数据,完整的表(比如 head、name、OS/2、post 等)只能通过专业工具或 Python 代码查看(能用 fontTools 库中的 TTFont 来查看元数据)

2.2 字形数据(Glyph Data)

字形数据是字体文件的 "核心内容",决定了每个字符的实际外观。

  1. 字形轮廓(Glyph Outline) :每个字符(glyph)的形状通常以 矢量路径(由贝塞尔曲线组成)存储。矢量的好处是------无论放大或缩小都不会失真。
  2. 点阵数据(Bitmap Data): 一些字体(尤其是旧式位图字体)还会包含固定尺寸的点阵信息,用于特定分辨率下的渲染。不过在现代字体中,这类数据已较少见。

2.3 字符映射(Character Mapping)

字符映射决定了 "某个字符代码对应哪个字形"字符编码表(Character Map / cmap):这是字体文件中最重要的表之一,负责定义 Unicode 编码与字体内部 glyph ID 的映射关系。例如:

javascript 复制代码
// 字符 "A" (Unicode U+0041) → glyph 索引 36
// 字符 "中" (Unicode U+4E2D) → glyph 索引 512

浏览器或操作系统在渲染时,就是依靠这张映射表找到对应的字形。

2.4 度量信息(Metrics)

度量信息用于定义字体的排版特征,比如文字的高度、宽度、间距等。

  1. 基线(Baseline) :所有字符 "站立" 的基准线,用于行对齐。
  2. 上升(Ascender)与下降(Descender) :Ascender 是字符最高部分相对于基线的距离(如 "h""b" 的上半部分),
    Descender 则是最低部分(如 "g""p" 的下半部分)。
  3. 字距调整对(Kerning Pairs) :定义某些字符组合之间的间距微调规则,比如在排版中,"AV" 通常会稍微靠近,视觉更协调。

2.5 提示信息(Hinting Information)

Hinting 是字体文件中的一种 "智能微调" 机制。在低分辨率或小字号时,字体可能会出现模糊或笔画不均的情况。提示信息会告诉渲染引擎如何调整笔画,使得字体在像素级别下依然清晰。例如: 在 12px 显示下,某条水平线可能会自动对齐到像素网格上,避免出现半像素模糊。

2.6 OpenType 与 TrueType 特性

OpenType 特性(OT Features):主要为排版提供高级功能,如:

  1. 连字(Ligatures):如 "fi""fi"
  2. 替代字符(Alternates):用于美术或特殊排版
  3. 上下标、分数、旧式数字等高级排版形式

TrueType 指令(TrueType Instructions):TrueType 格式特有的字节码指令,用于控制字体在不同设备、分辨率下的渲染行为(属于 hinting 的一种底层形式)。

三、字体文件实践与浏览器渲染流程

在前一部分中,我们已经系统地认识了字体文件的构成,包括元数据、字形数据、字符映射、度量信息等核心组成部分。理解这些理论后,接下来我们就可以进入到实操环节:通过真实的字体文件,观察其内部结构,并学习浏览器在加载字体时的查找与映射过程。

为此,我们将选择一个 开源字体 ,并通过以下几个步骤进行深入学习:子集化字体(仅保留常用字符 0--9、A--Z、a--z、.、@) → 转换为 XML 格式 → 分析关键表结构 → 理解浏览器字体映射机制。

在本节中,我们将推荐一个合适的开源字体,提供下载链接,并通过子集化和 XML 转换的方式,带你逐步掌握字体文件的结构与映射逻辑。这不仅有助于理解字体在网页中的加载与渲染机制,也为后续的 字体反爬研究 打下坚实的基础。

3.1 下载并选择字体

推荐使用 Inconsolata(一个开源的等宽体字体):Inconsolata 是一个开源字体,设计者为 Raph Levien,授权为 SIL Open Font License(OFL): 可免费用于修改与再分发。 它覆盖 Latin 字母、数字、标点等,比较适合我们去做 "保留常用字符" 练习。官方下载链接(或通过 Google Fonts)都可以获取。https://fonts.google.com/selection 我这里已经上传到百度网盘,大家可以直接从网盘中获取:

text 复制代码
通过网盘分享的文件:Inconsolata.zip
链接: https://pan.baidu.com/s/1SSZpb2Ksv3M28CSHOryd7A?pwd=sk38 提取码: sk38 
--来自百度网盘超级会员v9的分享

3.2 子集化(删除多余字符)

打开 FontStore 这类在线工具(或任意子集化工具)来操作。流程如下:

上传字体文件

在字符集选项中,仅保留需要的字符:数字 0-9、大写字母 A-Z、小写字母 a-z、点号 .、@ 符号,其余的都删除(最前面的 .notdef 这些别动)。删除如下图所示:

删除完成之后导出子集后的字体,例如 Inconsolata-Subset.ttf.woff2,我们两种格式的都导出一下方便后续研究:

注意: 保留少量字符后,字体文件体积变小,更便于我们观察映射情况。

3.3 使用 Python + fontTools 转换成 XML

注意: WOFF/WOFF2 是 Web 优化压缩格式,需要先解压缩才能生成 XML。fontTools 支持直接加载,但需要 woff2 库支持 WOFF2。安装命令如下:

bash 复制代码
pip install fonttools[woff]
# 参考文章: 
# https://fonttools.readthedocs.io/en/latest/
# https://github.com/fonttools/fonttools

fontTools 是处理字体文件的 Python 库,支持 TTF、OTF、WOFF、WOFF2 等格式。提供 ttx 命令行工具,可以把字体转换为 XML,也可以把 XML 转回字体。Python 脚本如下:

python 复制代码
from fontTools.ttLib import TTFont

# ① 加载ttf文件
font = TTFont('Inconsolata.ttf')
# 导出为XML
font.saveXML("font.ttx.xml")
print("TTF 已转换为 XML: font.ttx.xml")

# ② 加载 WOFF/WOFF2 文件
font = TTFont("Inconsolata.woff")
# 导出 XML
font.saveXML("font_woff.ttx.xml")
print("WOFF 已转换为 XML: font_woff.ttx.xml")

TTFont 会自动识别 TTF 文件格式。saveXML 方法生成完整 XML 文件,方便后续分析字符和轮廓。同理 TTFont 会自动识别并解压 WOFF/WOFF2 文件。XML 文件中内容结构与 TTF 非常相似,但需要注意:原始 WOFF2 文件可能有压缩优化,XML 展开后才能看到完整字形数据。WOFF2 对字符表、轮廓表内容没有丢失,只是存储格式不同。无论 TTF 还是 WOFF2,XML 的主要结构类似:

xml 复制代码
<ttFont sfntVersion="OTTO" ttLibVersion="4.39.4">
    <GlyphOrder>
        ...
    </GlyphOrder>
    <head>
        ...
    </head>
    <hhea>
        ...
    </hhea>
    <maxp>
        ...
    </maxp>
    <OS_2>
        ...
    </OS_2>
    <hmtx>
        ...
    </hmtx>
    <cmap>
        ...
    </cmap>
    <loca>
        ...
    </loca>
    <glyf>
        ...
    </glyf>
    <name>
        ...
    </name>
    <post>
        ...
    </post>
</ttFont>

下面详细解释每一部分:
<GlyphOrder> 作用 :列出字体中所有字形(glyph)的顺序。特点 :这是 glyph 在字体内部索引的顺序。cmap 表会使用索引来映射 Unicode 字符。观察重点:了解哪些字形存在,对子集化后的字体可以快速对比哪些字形被删除了。

xml 复制代码
<GlyphOrder>
  <GlyphID id="0" name=".notdef"/>
  <GlyphID id="1" name="A"/>
  <GlyphID id="2" name="B"/>
  <GlyphID id="3" name="uni4E2D"/>
  <GlyphID id="4" name="glyph00004"/>
</GlyphOrder>
<!-- <GlyphOrder> 是一个 总目录 ,列出所有 glyph 的排列顺序 -->
<!-- <GlyphID> 就是一个条目,对应一个具体的 glyph ID 与 glyph 名称 -->

<head>(字体头表)作用 :存储字体全局元信息。主要字段unitsPerEm:每个字体单位的基准,比如 1000 或 2048。xMin, yMin, xMax, yMax:字体轮廓的全局边界。created / modified:字体创建和修改时间。观察重点:单位和字体边界,用于绘制时的缩放和定位。

xml 复制代码
<head>
    <!-- Most of this table will be recalculated by the compiler -->
    <tableVersion value="1.0"/>
    <fontRevision value="3.001"/>
    <checkSumAdjustment value="0x6bdefffd"/>
    <magicNumber value="0x5f0f3cf5"/>
    <flags value="00000000 00001011"/>
    <unitsPerEm value="1000"/>
    <created value="Wed Dec 11 20:47:13 2019"/>
    <modified value="Wed Apr  1 13:10:26 2020"/>
    <xMin value="16"/>
    <yMin value="-174"/>
    <xMax value="488"/>
    <yMax value="671"/>
    <macStyle value="00000000 00000000"/>
    <lowestRecPPEM value="6"/>
    <fontDirectionHint value="2"/>
    <indexToLocFormat value="0"/>
    <glyphDataFormat value="0"/>
</head>

<hhea>(水平头表)作用 :存储水平排版相关信息。主要字段ascender(ascent) / descender(descent):字体上下延伸范围,lineGap:默认行间距,numberOfHMetrics:水平度量数量。观察重点:用于确定文本行高,水平布局计算必读。

xml 复制代码
<hhea>
    <tableVersion value="0x00010000"/>
    <ascent value="859"/>
    <descent value="-190"/>
    <lineGap value="0"/>
    <advanceWidthMax value="500"/>
    <minLeftSideBearing value="16"/>
    <minRightSideBearing value="12"/>
    <xMaxExtent value="488"/>
    <caretSlopeRise value="1"/>
    <caretSlopeRun value="0"/>
    <caretOffset value="0"/>
    <reserved0 value="0"/>
    <reserved1 value="0"/>
    <reserved2 value="0"/>
    <reserved3 value="0"/>
    <metricDataFormat value="0"/>
    <numberOfHMetrics value="1"/>
</hhea>

<maxp>(最大值表)作用 :字体中各种表的最大值信息,供渲染器优化。主要字段numGlyphs:字形数量。其他字段:轮廓点最大数、组件最大数等。观察重点:验证字形总数是否符合预期,分析字体复杂度。

xml 复制代码
<maxp>
    <!-- Most of this table will be recalculated by the compiler -->
    <tableVersion value="0x10000"/>
    <numGlyphs value="62"/>
    <maxPoints value="95"/>
    <maxContours value="4"/>
    <maxCompositePoints value="0"/>
    <maxCompositeContours value="0"/>
    <maxZones value="2"/>
    <maxTwilightPoints value="16"/>
    <maxStorage value="47"/>
    <maxFunctionDefs value="154"/>
    <maxInstructionDefs value="0"/>
    <maxStackElements value="731"/>
    <maxSizeOfInstructions value="0"/>
    <maxComponentElements value="0"/>
    <maxComponentDepth value="0"/>
</maxp>

<OS_2>(Windows/OS2 指标表)作用 :包含字体兼容性和外观信息。主要字段usWeightClass:字体粗细,usWidthClass:字体宽度,sTypoAscender/Descender:推荐排版用的上/下延伸,fsSelection:样式标记(斜体、粗体等)。观察重点:用于排版引擎选择字体样式,Windows/Office 渲染字体常用。

<hmtx>(水平度量表)作用 :每个字形的水平宽度信息。主要字段advanceWidth(width):字形宽度,lsb(left side bearing):左侧间距。观察重点:绘制字符水平位置,字符间距调整依赖此表。

<cmap>(字符映射表Character to Glyph Index Mapping Table):是字体中 把字符码(Unicode 或其他编码)映射到字形名称/索引(glyph) 的表。它的作用非常核心:浏览器拿到 "字符码(U+0030)→ 在 cmap 中找到对应 glyph → 用 glyf 表画出形状"。TTX 中的 cmap 大致结构如下:

xml 复制代码
<cmap>
  <tableVersion version="0"/>
  <cmap_format_0 platformID="1" platEncID="0" language="0">
    <map code="0x0" name=".notdef"/>
    ...
  </cmap_format_0>

  <cmap_format_4 platformID="3" platEncID="1" language="0">
    <map code="0x30" name="zero"/><!-- DIGIT ZERO -->
    <map code="0x31" name="one"/><!-- DIGIT ONE -->
    ...
  </cmap_format_4>

  <cmap_format_12 platformID="3" platEncID="10" language="0">
    <map code="0x20" name="space"/>
    ...
  </cmap_format_12>
</cmap>

<tableVersion version="0"/>,表示 cmap 表头版本号,一般恒为 0。用于标识 cmap 的结构格式(OpenType 规定的固定值)。多个 <cmap_format_X> 的意义,字体可能支持 多种编码方案,例如:

cmap 格式标签 平台ID(platformID) 编码ID(platEncID) 含义
cmap_format_0 1 (Macintosh) 0 Macintosh Roman 编码(老系统)
cmap_format_4 3 (Windows) 1 Windows Unicode BMP (UCS-2)
cmap_format_12 3 (Windows) 10 Windows Unicode UCS-4 (扩展平面)

多个格式同时存在是为了 兼容不同系统 。浏览器或操作系统一般优先使用 Windows Unicode (platformID=3, platEncID=1 或 10)。<cmap> 下每一行 <map> 表示一个字符码和字形名的映射:

xml 复制代码
<map code="0x30" name="zero"/><!-- DIGIT ZERO -->
<!-- ① code="0x30":  字符码(16进制 Unicode),例如 0x30 = "0" -->
<!-- ② name="zero":  字形名(glyph name),对应 <GlyphOrder> 和 <glyf> 表 -->
<!-- ③ DIGIT ZERO :  注释(fontTools 自动生成)说明含义 -->

<loca>(字形偏移表)作用 :存储每个字形在 glyf 表中的偏移位置。观察重点:glyph 在文件中的定位,便于快速查找字形轮廓数据。

<glyf> 表(字形表Glyph Data Table)记录了字体中每个 字形(glyph) 的矢量轮廓数据。每个 glyph 对应一个字符(如 "A""B""0" 等),而真正的显示形状------线条、曲线、笔画结构,全都在这里。在 TTF/XML 中,它看起来像这样:

xml 复制代码
<TTGlyph name="A" xMin="16" yMin="0" xMax="484" yMax="634">
    <contour>
        <pt x="357" y="182" on="1"/>
        <pt x="131" y="182" on="1"/>
        <pt x="145" y="236" on="1"/>
        <pt x="341" y="236" on="1"/>
    </contour>
    <contour>
        <pt x="249" y="474" on="1"/>
        <pt x="85" y="0" on="1"/>
        <pt x="16" y="0" on="1"/>
        <pt x="246" y="634" on="1"/>
        <pt x="254" y="634" on="1"/>
        <pt x="484" y="0" on="1"/>
        <pt x="412" y="0" on="1"/>
    </contour>
    <instructions/>
</TTGlyph>

基本结构:每一个 <TTGlyph> 节点描述一个字形:

属性 含义
name 字形名称(对应 <cmap> 中的 name)
xMin, yMin 字形最小边界坐标(左下角)
xMax, yMax 字形最大边界坐标(右上角)

这四个坐标定义了该 glyph 的边界框(bounding box)。<contour><pt>(点):每个 <contour> 表示一条封闭的曲线轮廓(类似路径 path),内部 <pt> 是曲线上的控制点或锚点。

xml 复制代码
<contour>
    <pt x="357" y="182" on="1"/>
    <pt x="131" y="182" on="1"/>
    <pt x="145" y="236" on="1"/>
    <pt x="341" y="236" on="1"/>
</contour>

TrueType 使用 二次贝塞尔曲线(quadratic Bézier curves),而不是三次曲线。这意味着每个曲线段最多有一个控制点。

属性 含义
x, y 坐标位置(相对于字体坐标系)
on 是否为 "on-curve" 点(1 表示曲线实际经过该点,0 表示二次贝塞尔曲线控制点)

<instructions>(hinting),<instructions> 存放 TrueType 的 hinting 指令(位图优化指令),用于在小字号下微调笔画,保证字体清晰。例如:

xml 复制代码
<instructions>
  00 01 02 03 04 05 ...
</instructions>

这些是字节码指令(TrueType bytecode),在反爬方向中通常 不影响外观分析 ,可以忽略或删除。<component>(复合字形),某些字符并非独立绘制,而是由其他 glyph 组合而成,例如:

xml 复制代码
<TTGlyph name="Å">
  <component glyphName="A" xScale="1.0" yScale="1.0" xOffset="0" yOffset="0"/>
  <component glyphName="ring" xScale="1.0" yScale="1.0" xOffset="300" yOffset="600"/>
</TTGlyph>

"Å""A""ring" 两个子 glyph 组成:

属性 含义
glyphName 被引用的 glyph 名称
xScale, yScale 缩放比例
xOffset, yOffset 平移偏移量

字体的坐标系以 "em" 为单位(例如 1000 或 2048 units/em)。坐标原点 (0,0) 通常位于基线(baseline)上。向上是正 y 方向,向右是正 x 方向,如下图所示(简化):

diff 复制代码
   ↑ y+
   |
   |        ● (xMax, yMax)
   |      /|
   |    /  |
---+---•----+----→ x+
 (0,0)

这也是浏览器渲染字体时用于定位的基础。与其他表的关联:

表名 与 glyf 的关系
maxp 提供 glyph 总数(numGlyphs)
loca 存储每个 glyph 在 glyf 表中的偏移位置(offset)
hmtx 定义 glyph 的宽度与左右边距(影响排版)
cmap 提供字符码 → glyph name 的映射
head 定义坐标单位、字体方向等参数

因此:cmap 告诉你哪个 Unicode 对应哪个 glyph; glyf 告诉你这个 glyph 怎么画; hmtx 告诉你画完后怎么排版间距。

字体 XML 中的 <name> 表记录了字体的 名称信息(Naming Table) ,包括:字体的全名(Full Name)、字体家族(Family)、样式(Subfamily)、字体版本、字体制造商、版权信息、字体在系统中显示的名称等。这些信息主要供:操作系统字体管理器识别字体;浏览器或 Office 等程序显示正确的字体名称;字体嵌入时使用元数据。从 .ttx(XML) 中可以看到类似这样的结构:

xml 复制代码
<name>
  <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">Arial</namerecord>
  <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">Regular</namerecord>
  <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">Arial Regular</namerecord>
  <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">Version 2.95</namerecord>
  <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">Arial-Regular</namerecord>
  <namerecord nameID="13" platformID="3" platEncID="1" langID="0x409">http://www.adobe.com/type/legal.html</namerecord>
</name>

nameID 表示字段编号,对应特定类型的信息,这是最重要的属性。有些字体只有少数字段,比如 nameID=1,2,4,6,有些字体则包含十几项。

nameID 含义 示例
0 Copyright notice © 2023 Example Foundry
1 Font Family name(字体家族名) Arial
2 Font Subfamily(样式) Regular / Bold / Italic
3 Unique font identifier Arial-Regular-1.00
4 Full font name(全名) Arial Regular
5 Version string Version 2.95
6 PostScript name Arial-Regular
7 Trademark Arial®
8 Manufacturer Microsoft Corporation
9 Designer Monotype Design Team
10 Description A sans-serif typeface family.
11 URL Vendor http://www.microsoft.com
12 URL Designer http://www.monotype.com
13 License Description Licensed to Microsoft Corp.
14 License Info URL http://www.adobe.com/type/legal.html
16 Preferred Family Arial
17 Preferred Subfamily Regular
18 Compatible Full(Macintosh only) Arial Regular
19 Sample Text The quick brown fox jumps...

platformID 表示平台编码体系(一般现代字体使用 platformID="3"(Windows 平台)):

platformID 含义
0 Unicode
1 Macintosh
3 Windows

platEncID表示编码方式(取决于平台):对于 Windows 平台(platformID=3):0 = Symbol,1 = Unicode BMP (UCS-2),10 = Unicode Full (UTF-32),现代字体多为 platEncID="1"langID 语言编号,常见为:

langID 语言 十六进制值
0x409 English (United States) en-US
0x804 Simplified Chinese zh-CN
0x411 Japanese ja-JP

例如:

xml 复制代码
<!-- 表示这是简体中文语言下的字体全名 -->
<!-- 用于 CSS font-family 显示;版权、商用信息可查-->
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x804">思源黑体 常规</namerecord>

最后一个 <post>(PostScript 表)作用 :PostScript 相关信息,主要字段formatType:PostScript 类型(Type 1、Type 2),italicAngle:斜体角度,underlinePosition / underlineThickness观察重点:绘制斜体和下划线,PostScript 渲染兼容性。

3.4 浏览器字体加载与渲染机制

浏览器字体加载的整体流程图:

diff 复制代码
HTML + CSS
   ↓
解析 CSS 中的 @font-face
   ↓
下载字体文件(TTF/WOFF/WOFF2/OTF)
   ↓
解析字体元数据(name、cmap、glyf 等)
   ↓
建立"字符 → glyph"映射表
   ↓
浏览器字体引擎绘制字形
   ↓
渲染结果显示到屏幕上

核心 CSS 语法:@font-facefont-family,定义字体文件:

css 复制代码
@font-face {
  font-family: 'MyFont';
  src: url('myfont.woff2') format('woff2'),
       url('myfont.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
}

这段声明告诉浏览器,有一个叫 MyFont 的字体家族,它的字体文件在 myfont.woff2 或 myfont.ttf,支持格式为 woff2 或 truetype,浏览器会优先加载第一个支持的格式(现代浏览器多优先使用 .woff2)。本地新建一个 test.html 文件:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        /*font-face 上面已经解释过了,这里就不在赘述*/
        @font-face {
            font-family: 'MyFont';
            src: url("Inconsolata.ttf") format("truetype"),
                url("Inconsolata.woff") format("woff");
            font-weight: normal;
            font-style: normal;
        }
        #test-ttf{
            font-family: 'MyFont', Arial, sans-serif;
            font-size:  100px;
        }
    </style>
</head>
<body>
<div id="test-ttf">amO666@qq.cOm</div>
</body>
</html>

在页面中使用字体:

css 复制代码
#test-ttf{
    font-family: 'MyFont', Arial, sans-serif;
}

浏览器渲染文字时,会从左到右查找字体,若 'MyFont' 中有该字符,这个就是我们使用 @font-face 导入的字体名称,我们在 font-family 时直接引用名字就好,就用它;否则回退到系统字体 Arial;还找不到则使用默认 sans-serif。浏览器加载字体文件的过程,当浏览器解析 CSS 时:

步骤 说明
① 解析 @font-face 注册一个新字体族到浏览器字体缓存(Font Face Set)
② 检查缓存 如果字体文件已加载,则复用;否则发起下载请求
③ 下载字体 发起 HTTP 请求下载 .woff2.ttf
④ 解码字体 解压 WOFF2 / 解包 TTF,解析表结构(head、cmap、glyf等)
⑤ 注册字体 把字体注册到内存中,并建立字符映射表
⑥ 渲染字符 当遇到文字时,查找对应 glyph 并绘制到页面上

浏览器如何 "找到" 字形并画出来?下面是关键过程,浏览器拿到要渲染的文字,假设 HTML:

html 复制代码
<div id="test-ttf">amO666@qq.cOm</div>

找到当前 CSS 的字体规则,假设样式:

css 复制代码
/* #test-ttf id选择器 */
#test-ttf{
    font-family: 'MyFont', Arial, sans-serif;
}

在字体文件中查找映射(cmap 表),cmap 表记录:

xml 复制代码
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
<map code="0x6d" name="m"/><!-- LATIN SMALL LETTER M -->
<map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
<map code="0x36" name="six"/><!-- DIGIT SIX -->
<map code="0x36" name="six"/><!-- DIGIT SIX -->
<map code="0x36" name="six"/><!-- DIGIT SIX -->
<map code="0x40" name="at"/><!-- COMMERCIAL AT -->
<map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
<map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
<map code="0x2e" name="period"/><!-- FULL STOP -->
<map code="0x63" name="c"/><!-- LATIN SMALL LETTER C -->
<map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
<map code="0x6d" name="m"/><!-- LATIN SMALL LETTER M -->

浏览器读取字符 a(Unicode 0x61),通过 cmap 查到对应字形名 a。找到对应字形(glyf 表),浏览器进入 <glyf> 表,找到:

xml 复制代码
<TTGlyph name="a" xMin="49" yMin="-11" xMax="445" yMax="467">
    <contour>
        <pt x="211" y="-11" on="1"/>
        <pt x="133" y="-11" on="0"/>
        .......
    </contour>
    <instructions/>
</TTGlyph>

并读取所有点坐标。绘制字形路径(Path),浏览器内部字体引擎(如 Skia , FreeType , DirectWrite 等):将 <pt> 中的点坐标解析为贝塞尔曲线;转换为屏幕像素坐标;根据 hinting / rasterize 渲染出像素点。应用排版信息(hmtxkern):hmtx(Horizontal Metrics)提供每个字形的宽度与偏移;kern(Kerning)提供特定字符对间距调整;浏览器据此确定每个字的相对位置。绘制到 Canvas / GPU 图层,最终字形路径被交给绘图引擎:转成矢量 → 栅格化 → GPU 缓存;显示到页面上。从字体加载到渲染的关键对应关系小结:

层级 内容 对应表
CSS font-family 名称 <name> 表中的 family name (nameID=1 / nameID=4)
字符 Unicode 编码 <cmap>
字形 绘制路径 <glyf>
宽度/对齐 排版信息 <hmtx>
渲染优化 字节码指令 <instructions>

字体渲染流程小结:

  1. 载入字体文件或内容。网页加载字体的方式有两种,无论哪种方式,本质上都是让浏览器能够正确获取并解析字体内容。

    • 外部字体文件:通过字体文件链接由浏览器下载字体,再由 CSS 读取内容。
    • Base64 编码字体:将字体文件内容以 Base64 编码嵌入到 CSS 中,浏览器可直接解析使用。
    1. 使用 @font-face 定义字体,字体文件通常通过 @font-face 规则进行声明,定义字体名称和来源,例如:

      css 复制代码
      @font-face {
          font-family: 'YourWebFontName';
          /* 路径 */
          src: url('font.ttf');
      }
      
      @font-face {
          font-family: 'AntiSpiderFont';
          /* Base64 数据 ${base64Font}: 这里引用了变量base64Font的值,其实就是一个base64*/
          src: url(data:font/woff2;base64,${base64Font}) format('woff2');
          font-weight: normal;
          font-style: normal;
      }
      
      /* 其中: font-family 用于指定自定义字体的名称
      src 指定字体文件的路径或 Base64 数据 */
    2. 在样式中引用字体,定义好字体后,可以在 CSS 中通过 font-family 引用它。例如:

      css 复制代码
      @font-face {
          font-family: 'YourWebFontName';
          src: url('font.ttf');
      }
      
      div {
        font-family: 'YourWebFontName';
        font-size: 12px;
        color: red;
      }
      /*这里: @font-face 部分负责定义字体;div 的样式负责应用字体。
      这样,页面中的 <div> 标签内容就会使用我们自定义的字体显示,字号为 12px,文字颜色为红色。*/
      /*  自定义字体显示的过程在上面已经详细讲解过 这里就不再赘述 简单来说就是找到字体后,
      	会将html中的文字 找到其对应的 Unicode码,然后取字体文件中查cmap映射
      */

四、反爬案例实现

知道了原理之后,我们可以实现一个小小的反爬,我们知道页面上的字符是通过 cmp 映射表,来映射的,那么我们改变这个映射表就可以改变其页面上的显示,比如,我把 1 的映射弄到 2 上去,2 反之则用 1 的映射,但是这里有一个点需要注意,在 cmap 下有三个:

xml 复制代码
<cmap_format_4 platformID="0" platEncID="3" language="0"></cmap_format_4>
<cmap_format_0 platformID="1" platEncID="0" language="0"></cmap_format_0>
<cmap_format_4 platformID="3" platEncID="1" language="0"></cmap_format_4>

前面我们已经说过,浏览器或操作系统一般优先使用 Windows Unicode (platformID=3, platEncID=1 或 10)。所以这里我们需要改第 3 个,当然这个可能会因为操作系统和浏览器不同产生差异,具体你更改哪个生效,需要自行测试,我这里测试浏览器优先使用的第三个,即 platformID="3" platEncID="1",我们将前面两个删除,并且改第三个中 01 的映射:

xml 复制代码
<!-- 原始的 -->
<map code="0x31" name="one"/><!-- DIGIT ONE -->
<map code="0x32" name="two"/><!-- DIGIT TWO -->

<!-- 改动之后 -->
<!-- 简单来说如果页面上输入的是 1,对应Unicode为 0x31,但是却找的是two的<glyf>,绘制的是2 -->
<map code="0x32" name="one"/><!-- DIGIT ONE -->
<map code="0x31" name="two"/><!-- DIGIT TWO -->

如下图:

手动更改 xml 文件后,使用 fontTools 将 xml 文件转换为字体,然后在页面上进行导入,python 示例代码:

python 复制代码
from fontTools.ttLib import TTFont

# 1. 读取 XML(TTX)文件并生成字体对象
font = TTFont()
font.importXML("font.ttx.xml")

# 2. 保存为 TTF 格式
font.save("rebuild_font.ttf")

print("字体已成功从 XML 转换回 TTF!")

html 代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
      @font-face {
        font-family: 'MyFont';
        src: url("rebuild_font.ttf") format("truetype");
        font-weight: normal;
        font-style: normal;
      }

      #test{
        font-family: "MyFont", Arial, sans-serif;
        font-size:  100px;
      }

    </style>
</head>
<body>
  <div id="test">12</div>
</body>
</html>

浏览器中查看效果:

有趣的是你此时在浏览器中复制还是 12,上面手动更改映射然后导致页面显示的文字和 html 中的文字不一致是实现动态字体反爬的核心原理。可以通过代码直接更改映射关系:

python 复制代码
from fontTools.ttLib import TTFont

font = TTFont("Inconsolata.ttf")
cmap_table = font["cmap"].getcmap(3, 1)  # 平台ID=3, 编码ID=1(常见)
cmap = cmap_table.cmap

# 交换1(0x41)与2(0x42)的glyph
glyph_1 = cmap[0x31]
glyph_2 = cmap[0x32]
cmap[0x31], cmap[0x32] = glyph_2, glyph_1

font.save("swapped.ttf")

在 css 中引入 swapped.ttf,你会发现效果和手动更改 xml 一致。接下来,我们用代码将映射随机打乱就能生成动态的字体了。

  1. random_cmap 功能: 打乱字体的 cmap 映射表。cmap 是一个字典:键是 Unicode 码点,值是字体中的 glyph 名称。打乱后,每个 Unicode 码点会随机对应到另一个 glyph。作用: 实现 字体混淆 ,原本 'A' 的字符可能显示成 'B' 的样式,从而干扰爬虫。

    python 复制代码
    def random_cmap(cmap):
        unicode_keys = list(cmap.keys())
        glyph_names = list(cmap.values())
        random.shuffle(glyph_names)
        # 生成新的字符映射
        new_cmap = dict(zip(unicode_keys, glyph_names))
        return new_cmap
    
    # 测试
    # 要实现动态字体,即改变xml中的映射关系
    font = TTFont('Inconsolata.ttf')
    # 是一个键值对一样的东西 替代之前的font["cmap"].getcmap(3, 1) 省事
    cmap = font['cmap'].getBestCmap()
    print("原有的映射关系 cmap: ", cmap)
    new_cmap = random_cmap(cmap)
    print("新的映射关系 new_cmap: ", new_cmap)
  2. 将输入字符串根据新的 cmap 映射生成新的编码字符串。内部有一个 fft_mapping 字典,把 "@", ".", "0-9" 转换成对应的 glyph 名称。利用 new_cmap_inverted_dict 把 glyph 名称转换回 Unicode 码点,再生成字符。作用: 模拟网页或接口中使用混淆字体后显示的字符串编码。比如 '123456@qq.com' 会被映射成新的字符序列,对应打乱后的字体显示。

    python 复制代码
    def map_string(input_str, new_cmap):
        fft_mapping = {
            "@": "at",
            ".": "period",
            "0": "zero",
            "1": "one",
            "2": "two",
            "3": "three",
            "4": "four",
            "5": "five",
            "6": "six",
            "7": "seven",
            "8": "eight",
            "9": "nine",
            "nonbreakingspace": " ",
        }
    
        new_cmap_str = '' # input: '@', ==>
        # 将映射关系key,value反转,这样我们能快速通过某个字符获取其对应的unicode码点
        new_cmap_inverted_dict = {value: key for key, value in new_cmap.items()}
        print(new_cmap_inverted_dict)
        # 循环处理每一个字符
        for char in input_str:
            # 这里要注意一下,因为我们的映射关系中一些特殊字符是用英文对应的,比如我们传@,但是映射关系里面是用at表示的
            # 所以需要有一个 fft_mapping 对其进行转换,这样我们才能在new_cmap_inverted_dict中找到
            if char in fft_mapping:
                # 比如char='@' ,那么转换之后就 char='at'
                char = fft_mapping[char]
    
            # 转换之后 new_cmap_inverted_dict['at'] 就能拿到其码点 'at': 79
            # 那接下来我们就要看浏览器中读取哪个字符,得到它的unicode码点之后刚好是79,那么这个字符就是我们要镶嵌在html中的字符了
            # chr(79) 转换一下 ==》 O 所以当使用当前这套字体时 页面想显示@,html中需要镶嵌的是 'O'字符
            char = chr(new_cmap_inverted_dict[char]) # 79
            # 每处理一个字符就进行拼接,那么最后得到字符串就和想要显示在页面中的文字一一对应了
            new_cmap_str += char
        return new_cmap_str
  3. 接下来创建一个新的 cmap 表,将字体原来的 Unicode → glyph 映射替换成 new_cmap。使用 OpenType 的 subtable 类型 4(常用 Windows 平台编码)。作用: 真正把字体文件中的映射关系更新,使字体文件显示字符对应新的 glyph。

    python 复制代码
    from fontTools.ttLib import TTFont, newTable
    from fontTools.ttLib.tables._c_m_a_p import CmapSubtable
    
    def update_cmap_table(font, new_cmap):
        new_cmap_table = newTable('cmap')
        new_cmap_table.tableVersion = 0
        new_cmap_table.tables = []
        subtable = CmapSubtable.newSubtable(4)
        # 这个很熟悉了吧,前面我们操作过platformID于 platEncID
        # font["cmap"].getcmap(3, 1)  # 平台ID=3, 编码ID=1(常见)
        subtable.platformID = 3
        subtable.platEncID = 1
        subtable.language = 0
        subtable.cmap = new_cmap
        new_cmap_table.tables.append(subtable)
        font['cmap'] = new_cmap_table
  4. 测试:

    python 复制代码
    def main():
        # 页面上要显示的内容
        input_str = '123456@qq.com'
        # 要实现动态字体,即改变xml中的映射关系
        font = TTFont('Inconsolata.ttf')
        # 是一个键值对一样的东西 替代之前的font["cmap"].getcmap(3, 1) 省事
        cmap = font['cmap'].getBestCmap()
        print("原有的映射关系 cmap: ", cmap)
        new_cmap = random_cmap(cmap)
        print("新的映射关系 new_cmap: ", new_cmap)
    
    
        # 之前我们测试的是12 但是页面上显示的是21 因为我们的映射关系是: {0x31: '2', 0x32: '1'} 这里简写了
        # 根据字体加载的原理首先 html中的字符为 12 ==> 会将 1 转换成 Unicode 0x31 ==> 找到字体中的映射 对应的是 2 所以画出来的是2
        # 根据字体加载的原理首先 html中的字符为 12 ==> 会将 2 转换成 Unicode 0x32 ==> 找到字体中的映射 对应的是 1 所以画出来的是1
    
        # 如果页面上想要显示 123456@qq.com
        # 那我们就要想了? 什么东西镶嵌在html中 然后经过字体的渲染能变成 ==> 123456@qq.com
        # 那现在我们有了 123456@qq.com 首先我们要知道每个字符他现在对应的unicode值是啥? 然后现在的unicode值原始对应的字符是啥?
        # 这样我们就通过 unicode值 对我们要显示的字符和原始字符进行了关联
        # 那么当我们在html中嵌入原始字符时,浏览器渲染就会拿到原始字符对应的unicode值,那通过现在的映射关系unicode值就能对上要显示的字符
        # 举例: @,新的关系中 @对应的unicode值为82,那么我们就要求出unicode值为82的原始字符
        # 结果unicode值为82的原始字符为 'R',那么当我们在html中嵌入R字符时,
        # 浏览器 R==> 找到unicode码点,82 ==> 然后从字体中找 82码点对应的是 @ 那么页面上就显示出了我们想要的@
        # 但是爬虫工程师从源码中拿到的确实 R字符
        new_cmap_str = map_string(input_str, new_cmap)
        print(new_cmap_str)
    
        # 用新的映射替换掉原始映射
        update_cmap_table(font, new_cmap)
        font.save('test.ttf')
    
    if __name__ == '__main__':
        main()

    html:

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
          @font-face {
            font-family: 'MyFont';
            src: url("test.ttf") format("truetype");
            font-weight: normal;
            font-style: normal;
          }
    
          #test{
            font-family: "MyFont", Arial, sans-serif;
            font-size:  100px;
          }
    
        </style>
    </head>
    <body>
      <div id="test">bCQYDuwffXkW1</div>
    </body>
    </html>

    页面上显示的为:

后续你还可以随机修改字体每个 glyph 的坐标。利用 TTGlyphPen 重新生成字形路径。对特殊 glyph (.null, .notdef, nonbreakingspace) 不处理。作用: 增加字体随机性,防止被 OCR 精确识别。改变视觉形态但不影响字体合法性。

python 复制代码
from fontTools.pens.ttGlyphPen import TTGlyphPen

def modify_glyph_coordinates(font):
    glyf_table = font['glyf']
    # 修改字型的坐标
    for glyph_name in font.getGlyphOrder():
        if glyph_name in [".null", ".null#1", "nonbreakingspace", ".notdef"]:
            continue
        glyph = glyf_table[glyph_name]
        new_coordinates = [(x, y + random.randint(-1, 1)) for x, y in glyph.coordinates]
        pen = TTGlyphPen(glyf_table)
        pen.moveTo(new_coordinates[0])
        for coord in new_coordinates[1:]:
            pen.lineTo(coord)
        pen.closePath()
        glyf_table[glyph_name] = pen.glyph()

# 测试: 修改字体文件的坐标
modify_glyph_coordinates(font)       

还可以递归删除 XML 中的注释。作用: 清理 XML 文件,防止注释暴露字体信息或增加冗余。

python 复制代码
import xml.etree.ElementTree as ET

def remove_xml_comments(root):
    # 递归删除所有的注释
    for element in list(root):
        if isinstance(element.tag, str) and element.tag == 'comment':
            root.remove(element)
        else:
            remove_xml_comments(element)

将 XML 中的 glyph 名称替换为数字索引。replace_list 包含字母、数字、符号及其名称。

python 复制代码
def modify_xml_mapping(xml_bytesIO):
    replace_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                    'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'at', 'b', 'c', 'd', 'e', 'eight', 'f', 'five', 'four', 'g', 'h',
                    'i', 'j', 'k', 'l', 'm', 'n', 'nine', 'nonbreakingspace', 'o', 'one', 'p', 'period', 'q', 'r', 's',
                    'seven', 'six', 't', 'three', 'two', 'u', 'v', 'w', 'x', 'y', 'z', 'zero']
    xml_str = xml_bytesIO.read().decode("utf-8")
    for num, char in enumerate(replace_list):
        xml_str = xml_str.replace(f'name="{char}"', f'name="{num}"')
    xml_bytesIO.seek(0)
    xml_bytesIO.write(xml_str.encode())

这个就不具体演示了。

五、绕过字体反爬实战

5.1 某眼案例

网站链接:aHR0cHM6Ly93d3cubWFveWFuLmNvbS9maWxtcz9zaG93VHlwZT0y

很明显,这是一起典型的字体反爬案例。在理解了原理之后,我们的第一步就是要 定位字体文件的来源 。因此,第一步我们要做的,就是 在页面中搜索字体文件的加载位置 ,判断它是内联的 Base64 数据,还是通过 URL 请求的字体文件。接下来,再去提取页面中显示的 "乱码字符",并结合字体的 cmap 映射关系,分析这些字符在字体文件中实际对应的内容。

所以我们写一个请求把字体文件下载下来,放到在线工具中观察观察:

python 复制代码
import requests

headers = {
    'accept': '*/*',
    'accept-language': 'zh-CN,zh;q=0.9',
    'cache-control': 'no-cache',
    'origin': 'https://www.maoyan.com',
    'pragma': 'no-cache',
    'priority': 'u=0',
    'referer': 'https://www.maoyan.com/',
    'sec-ch-ua': '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'font',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'cross-site',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
}

response = requests.get(
    'https://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/20a70494.woff',
    headers=headers,
)

with open('maoyan.woff', 'wb') as f:
    f.write(response.content)

在线字体编辑器中查看:

接下来我们去看源码中返回的是什么:

这段 &#xe99c;&#xecdc;&#xecdc; 到底是什么?这其实是 HTML 实体编码(HTML Entity Encoding)

html 复制代码
&#xXXXX;

&#x 表示后面是一个十六进制 Unicode 编码。e99c 就是 16 进制的 Unicode 值。浏览器在渲染时,会将这个 Unicode 码当作一个字符去显示。如果字体文件(如 .ttf)中存在该码位对应的字形(glyph),浏览器就会使用该字体的绘制结果来展示。我们知道 Unicode 值(code)会通过字体文件的 cmap 映射到对应的字形名称(name)。例如:

xml 复制代码
<map code="0xe99c" name="uniE99C"/>

<!-- &#xe99c; 这其实等价于下面的 Unicode 写法: uniE99C >
<!-- 或者在 Python / JavaScript 中常见的十六进制数值形式: 0xe99c>

这意味着 Unicode 码 0xe99c 对应的字形是 uniE99C。其实结合字体在线编辑器,我们已经可以得出一个映射关系:

python 复制代码
mapping = {
    "uniEB92": 7,
    "uniE8D7": 3,
    "uniF7FF": 6,
    "uniF85E": 1,
    "uniE99C": 2,
    "uniF1FC": 8,
    "uniF726": 7,
    "uniE8EE": 4,
    "uniE9EA": 9,
    "uniECDC": 5,
}

但是这样的关系是写死的,如果字体是动态变化的,下次这样就不行了。所以此处介绍一种比较通用的方法,核心思路:**找到 code(Unicode 值) → 找到对应 name → 定位 TTGlyph → 绘制图像 → 识别出真实字符。**详细步骤如下:

  1. 提取字体文件(Base64 或 URL): 从接口或网页源码中拿到字体文件(如 b64Font.ttf 链接)。

  2. 解析 cmap 映射表: 使用 fontTools 提取 cmap 表,获得所有 code → name 的对应关系。

  3. 根据 name 定位字形(TTGlyph): 每个字形(glyph)包含绘图轮廓坐标数据(xMin, yMin, contours 等)。

  4. 渲染字形为图像: 利用 PIL.ImageFont 或直接用 fontTools 将字形绘制为图像。

  5. OCR 识别真实内容: 使用 OCR(如 ddddocr)识别每个字形对应的真实字符。

  6. 建立反向映射关系表: 将识别出的真实字符与 Unicode 值(或字形名)建立映射,例如:

    python 复制代码
    {0xe99c: '3', 0xecdc: '7', ...}
  7. 替换网页源码中的 Unicode: 最终,将网页源码中的 &#x****; 批量替换为识别出的真实内容,就能还原出正确的数据。

具体代码实现如下:
提取字体文件(Base64 或 URL):

python 复制代码
import re
import requests

cookies = {
    # 写你自己的cookie
}

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                  ' (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
}

params = {
    'showType': '2',
    'offset': '18',
}

response = requests.get('https://www.maoyan.com/films', params=params,
                        cookies=cookies,
                        headers=headers)

# url("//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/432017e7.woff")
# url("//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/20a70494.woff")
# 每次返回的字体文件都不一样 所以我们要提取 建立动态的映射关系
font_url = 'https:' + re.findall(r'url\(["\']?(//[^"\')]+\.woff)["\']?\)', response.text)[0]
font_res = requests.get(url=font_url, headers=headers)
with open('my.woff', 'wb') as f:
    f.write(font_res.content)

步骤 2 - 6

python 复制代码
from fontTools.ttLib import TTFont
import ddddocr
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont

if not hasattr(Image, 'ANTIALIAS'):  # 新版本没有这个属性
    Image.ANTIALIAS = Image.Resampling.LANCZOS


def convert_cmap_to_image(cmap_code, font_path):
    """
    :param cmap_code: 字体的 Unicode 码点(整数,例如 0x41 表示 'A')
    :param font_path: 字体文件路径(如 "my.woff")
    :return: 返回绘制好的 PIL.Image 图像对象
    """
    img_size = 1024  # 设置图片尺寸为 1024×1024 像素
    # 准备三要素: image画布  draw画笔 font字体

    # 创建一张空白图片(画布)"1": 表示黑白图像(1位深度,即只有黑白两色)
    # (img_size, img_size): 大小为 1024×1024
    # 255:表示白色背景(在"1"模式下,0是黑,255是白)
    img = Image.new("1", (img_size, img_size), 255)  # 创建一个黑白图像对象

    # 画笔: 这里的 draw(画笔) 是"绑定"在 img(画布)上的
    # 画笔需要知道要画到哪张纸上,否则它没法落笔
    draw = ImageDraw.Draw(img)  # 创建绘图对象

    # 字体(ImageFont): 是你手中的 "书法笔",控制文字的样式
    font = ImageFont.truetype(font_path, img_size)  # 加载字体文件

    # 将 cmap code 转换为字符
    character = chr(cmap_code)
    # print("character: ",character)  # 这里只能传 character
    bbox = draw.textbbox((0, 0), character, font=font)  # 获取文本在图像中的边界框
    # 返回一个 四元组 (x_min, y_min, x_max, y_max),代表文字区域的左上角和右下角坐标
    # bbox = (200, 100, 824, 900)
    # 左上角坐标(200, 100) 右下角坐标(824, 900)
    # 宽度 = 824 - 200 = 624 高度 = 900 - 100 = 800
    print(bbox)
    width = bbox[2] - bbox[0]  # 文本的宽度
    height = bbox[3] - bbox[1]  # 文本的高度
    # 正式开始写字了
    draw.text(((img_size - width) // 2, (img_size - height) // 2), character, font=font)  # 绘制文本,并居中显示
    return img


def extract_text_from_font(font_path):
    font = TTFont(font_path)  # 加载字体文件
    # 图像识别的模块: DdddOcr
    ocr = ddddocr.DdddOcr()  # 实例化 ddddocr 对象
    # 获取映射关系
    # (120, 'x'), (59455, 'uniE83F'), (59487, 'uniE85F'), (59670, 'uniE916'), (60751, 'uniED4F').....
    print("font.getBestCmap().items(): ", font.getBestCmap().items())

    font_map = {}  # 最后要返回的映射字典
    # TODO 1.解析 cmap 映射表
    for cmap_code, glyph_name in font.getBestCmap().items():
        # TODO 2.定位字形并渲染字形为图像
        image = convert_cmap_to_image(cmap_code, font_path)
        # TODO 3.OCR识别真实内容
        bytes_io = BytesIO()
        image.save(bytes_io, "PNG")
        text = ocr.classification(bytes_io.getvalue())  # 图像识别
        # TODO 4.建立反向映射关系表
        font_map[glyph_name] = text

    return font_map


if __name__ == '__main__':
    # 测试
    font_file_path = "my.woff"
    # {'x': '', 'uniE83F': '8', 'uniE85F': '3', 'uniE916': '7', 'uniED4F': '5', 'uniED98': '9', 
    # 'uniEDBA': '1', 'uniEFE9': '4', 'uniF0F0': '2', 'uniF70E': '6', 'uniF7B3': '0'}
    print(extract_text_from_font(font_file_path))

替换:

python 复制代码
def main():
    for offset in range(0, 90, 18):
        params = {'showType': '2','offset': offset,}
        response = requests.get('https://www.maoyan.com/films', params=params,
                                cookies=cookies,
                                headers=headers)
        text = response.text  # 网页源码
        # TODO 1.动态解析字体
        font_url = 'https:' + re.findall(r'url\(["\']?(//[^"\')]+\.woff)["\']?\)', text)[0]
        font_res = requests.get(url=font_url, headers=headers)
        font_file_path = 'my.woff'
        with open(font_file_path, 'wb') as f:
            f.write(font_res.content)

        # TODO 2.建立映射关系
        font_map = extract_text_from_font(font_file_path)
        print(font_map)
        for k, v in font_map.items():
            key = k.lower().replace('uni', '&#x')
            if key in text:
                text = text.replace(key + ';', v)

        # TODO 3.解析数据
        selector = Selector(text=text)
        dd_list = selector.xpath('//dl[@class="movie-list"]//dd')
        for dd in dd_list:
            title = dd.xpath('./div[@class="channel-detail movie-item-title"]/@title').extract_first('')
            people_watch = ''.join(dd.xpath('./div[@class="channel-detail channel-detail-orange"]//text()').extract())
            print(title, people_watch)
        print("-------------------------------------------------------")

测试,与网页上的一致,如下图所示:

5.2 某职案例

网站链接:aHR0cDovL3NoYW56aGkuc3BiZWVuLmNvbS8=

简单看一下:

同理看一下页面的源码:

按照 2.4.1 某眼案例 的步骤固定操作,发现返回的映射是这个样子的:

发现这玩意不是 Unicode 形式,那么此处我们就需要灵活变通一下,在 extract_text_from_font 函数中,最后返回的字典映射:

python 复制代码
# TODO 4.建立反向映射关系表
# 是通过glyph_name设置的键,glyph_name是unicode值形式,所以很方便和我们的源码对应起来
# 但是此处是 glyph00006, glyph00004,所以需要更改一下逻辑
font_map[glyph_name] = text

# 那么我就用 cmap_code 我们之前在研究字体xml文件的时候,其实知道 code也是一个十六进制 只不过是0x开头
# <map code="0x39" name="W"/>
# python中的常用形式,那么我们这里就可以直接用hex()函数将Unicode码值转换为十六进制形式
font_map[hex(cmap_code)] = text

执行:

这样就能和源码返回的对应上了,不过替换那里的逻辑需要改一下:

看一下替换后的源码,我这里就不去解析。测试了一下,它首页数据是动态变化的,观察比较麻烦,走它的搜索接口,在页面右上角搜索安卓开发,如下:

观察接口:

python 复制代码
http://shanzhi.spbeen.com/api/search/?word=安卓开发&page=1&_=1761583798706
http://shanzhi.spbeen.com/api/search/?word=安卓开发&page=2&_=1761583798706

稍微改一下请求的逻辑就好了,首页用来动态解析字体,但这个网站字体不是动态变化的,要替换的源码则是通过搜索接口返回的,上图的薪资解析正确,如下:

5.3 SpiderDemo第8题

https://blog.csdn.net/xw1680/article/details/153992358?spm=1011.2415.3001.5331

相关推荐
编程让世界美好4 小时前
选手评分问题(python)
python
java1234_小锋4 小时前
PyTorch2 Python深度学习 - PyTorch2安装与环境配置
开发语言·python·深度学习·pytorch2
CClaris4 小时前
深度学习——反向传播的本质
人工智能·python·深度学习
伊玛目的门徒4 小时前
Jupyter Notebook 配置使用虚拟环境中(virtualenv) 内核
python·jupyter·virtualenv
Geoking.6 小时前
PyTorch torch.ones()张量创建详解
人工智能·pytorch·python
conkl6 小时前
在 CentOS 系统上实现定时执行 Python 邮件发送任务完整指南
linux·运维·开发语言·python·centos·mail·邮箱
人工智能教学实践6 小时前
TCP 与 HTTP 协议深度解析:从基础原理到实践应用
python
查士丁尼·绵6 小时前
笔试-计算网络信号
python
淼_@淼7 小时前
python-xml
xml·python·1024程序员节