自定义字体是一种很常见的反爬虫方法,本文将以晋江文学城为例详解如何对抗自定义字体反爬。

随便打开一章晋江VIP章节,查看源码,发现一部分在网页中正常显示的字符,在网页源码却中显示为了方块。

晋江文学城VIP章节截图

通过 Fonts 选项卡,可以发现这些方块字体使用了自定义字体(jjwxcfont)。

如果想要制作一个晋江小说下载器,那么这些使用了自定义字体的方块字是一道绕不开的障碍。

除了先全文截图再直接全图OCR,这种简章粗暴的方法之外。

另一种通用的方法是,先获取并生成自定义字体的字符映射表,然后根据字符映射表将自定义字体字符映射回通用字符。

下图即是某一晋江字体的字符映射表

某一晋江字体的字符映射表

本文将重点讲解如何快速高效自动化地生成晋江自定义字体字符映射表。

获取字体文件

想要破解自定义字体,你首先需要将自定义字体下载下来,这部分工作虽然也很重要,但并不是今天的重点,因此不再赘述。

如果你对这部分内容感兴趣,可以参考我另一个项目中相关功能代码

大致印象

mkdir fonts
wget http://static.jjwxc.net/tmp/fonts/jjwxcfont_0004v.woff2?h=my.jjwxc.net -O fonts/jjwxcfont_0004v.woff2
wget http://static.jjwxc.net/tmp/fonts/jjwxcfont_00rmg.woff2?h=my.jjwxc.net -O fonts/jjwxcfont_00rmg.woff2

使用上述命令下载 jjwxcfont_0004v.woff2 字体文件,之后本文将以该字体为例进行讲解。

使用 fontforge 打开该字体文件。

选择选项:Encoding > Reencode > Glyph Order

查看该字体文件,获得一个该字体文件的大致印象。

jjwxcfont_0004v

OCR法

OCR法即获取字体文件下所有字符,并将其转换为图片,通过OCR软件识别后进行标记,最终生成字符映射表。

In [1]:
import os
import shutil
import tempfile

from fontTools.ttLib import woff2, ttFont

PWD = os.getcwd()
TMP: str = tempfile.mkdtemp()
FontsDir: str = os.path.join(PWD, 'fonts')

def clear() -> None:
    """
    清除临时文件
    """
    shutil.rmtree(TMP)
    
def listTTF(ttf: ttFont.TTFont) -> list[str]:
    """
    输入字体文件,输出该字体文件下所有字符。
    """
    return list(set(map(lambda x: chr(x), ttf.getBestCmap().keys())))

载入并解析字体

In [2]:
fontname: str = 'jjwxcfont_0004v'
fontpath: str = os.path.join(FontsDir, f'{fontname}.woff2')
ttfpath: str = os.path.join(TMP,f'{fontname}.ttf')

woff2.decompress(fontpath, ttfpath)
ttf = ttFont.TTFont(ttfpath)

chars = listTTF(ttf)
2 extra bytes in post.stringData array

共有201个字符

In [3]:
print(len(chars))
print(chars)
201
['\ue3ce', '\ued31', '\uea4a', '\ue2f0', '\ue707', '\ue949', '\ue452', '\ue8e6', '\ue33c', '\ue11c', '\ue79f', '\uecd2', '\ueb16', '\ue3d9', '\ued29', '\ue680', '\uea2f', '\ue876', '\ue188', '\ue8d2', '\ue9c9', '\ue76d', '\ue2ad', '\ue6ef', '\ue7f6', '\ue22f', '\ue1d7', '\uec78', '\ue768', '\uee4a', '\ueb0e', '\ue04e', '\uec7a', '\ue73a', '\uebf0', '\ue4fe', '\ue3c7', '\ue7d9', '\uefb6', '\uea48', '\uec38', '\ue428', '\uef6e', '\uefd2', '\ue7b0', '\ue86d', '\ue44d', '\ue50f', '\ue6c8', '\ue9cb', '\uedcf', '\ueef4', '\uef45', '\ue7ee', '\uebcc', '\ue2ea', '\uef78', '\ue1fa', '\ue5fb', '\ue5e1', '\ue6fa', '\ue89f', '\ue19b', '\ue98a', '\uedda', '\ue2e2', '\ue084', '\ue3a1', '\ue586', '\uea75', '\ue3ca', '\ue78d', '\ue11f', '\ue0d3', '\ue3aa', '\ue9a2', '\ue567', '\ue54d', '\ue72b', '\ue5c6', '\ue3e3', '\uee30', '\ue197', '\ue009', '\ue47d', '\ue490', '\ue0f4', '\ue005', '\ue055', '\uebfd', '\uec30', '\ue535', '\ue6c4', '\ue1ca', '\ue948', '\ue705', '\ue1e7', '\ued69', '\uee76', '\ue8de', '\ue7b4', '\uee6b', '\ue1a7', '\ue99b', '\ue3fe', '\ue52b', '\ue44c', '\uea63', '\uebe4', '\ue2df', '\ue774', '\ue57f', '\ue5ee', '\ue8a7', '\ue898', 'x', '\ue97d', '\ue6cb', '\ue801', '\uecd6', '\ue019', '\uebc4', '\ue999', '\uefaf', '\uec9c', '\ue1fb', '\ue085', '\ued5d', '\uee11', '\ue8dc', '\uea14', '\ue3ac', '\ue943', '\uee8b', '\ue08f', '\ue50a', '\ue5a3', '\ue803', '\ueaf1', '\uedf4', '\uebba', '\ue5c3', '\ue429', '\uecc8', '\uef7e', '\ue06a', '\ue804', '\ue0d6', '\ueff7', '\uefd5', '\uec84', '\ue36d', '\ue7cd', '\ued67', '\ue8a1', '\ue506', '\ue451', '\ue0a8', '\uead1', '\ue601', '\uebfe', '\ue4d0', '\ue076', '\ue087', '\ue9ab', '\ue8d6', '\ueb49', '\ue584', '\ue4df', '\ue98f', '\ue28b', '\ue42f', '\uede8', '\ue627', '\ue31a', '\ue4bb', '\ue4ad', '\ue530', '\uea6d', '\ue9ed', '\ue1eb', '\ue00d', '\uea17', '\ued4b', '\ueb9f', '\uef5e', '\ue613', '\ued80', '\ue44b', '\ue05a', '\ue150', '\ue218', '\ue1b2', '\ue7f3', '\uea35', '\ue1e1', '\uee44', '\ue5a2', '\uec17', '\uecca', '\uebf4']

将自定义字体的字符列表生成为每行20个字的文本,用于后续识别工作。

In [4]:
import itertools

_chars = filter(lambda x: x != 'x', chars)
_ = iter(_chars)
chars_split: list[list[str]] = [list(itertools.islice(_, 20)) for i in range(10)]
ocr_txt: str = '\n'.join([''.join(cs) for cs in chars_split])
del _
del _chars
In [5]:
print(ocr_txt)











调用 imagemagick 所附带的 convert 命令,将之前准备好的自定义字符文本绘制为图片。

In [6]:
import subprocess

txt_path: str = os.path.join(TMP, 'ocr.txt')
img_path: str = os.path.join(TMP, 'ocr.png')

with open(txt_path, 'w') as f:
    f.write(ocr_txt)
subprocess.call(["convert", "-font", ttfpath, "-pointsize", "64", "-background", "rgba(255,255,255)",
    f"label:@{txt_path}", img_path])
Out[6]:
0

结果如下:

In [7]:
from PIL import Image, ImageDraw, ImageFont
from IPython.display import display

ocr_img = Image.open(img_path)
display(ocr_img)
No description has been provided for this image

调用 tesseract 识别该图片。

In [8]:
tesseract_result_name: str = os.path.join(TMP, 'tesseract_result.txt')

subprocess.call(["tesseract", img_path, tesseract_result_name, "-l", "chi_sim", "--psm", "6"])
Tesseract Open Source OCR Engine v4.1.1 with Leptonica
Warning: Invalid resolution 0 dpi. Using 70 instead.
Out[8]:
0

tesseract 识别结果如下:

In [9]:
with open(f'{tesseract_result_name}.txt', 'r') as f:
    tesseract_result = f.read()
    print(tesseract_result)
其 可 她 就 们 来 眼 这 所 何 者 化 作 问 意 没 道 果 样 重
如 然 听 中 相 子 本 四 己 方 被 多 感 心 出 太 小 会 笑 家
由 自 说 把 年 世 么 看 使 论 下 儿 到 从 事 打 口 现 便 回
要 代 丸 成 间 实 话 进 他 很 因 得 而 情 别 好 无 少 特 关
同 但 人 全 定 声 公 什 老 了 等 仁 明 先 学 再 你 分 动 门
还 给 上 以 力 十 体 身 又 与 国 的 之 将 那 与 几 三 书 过
物 时 死 第 想 两 生 文 思 后 社 二 一 理 名 并 长 行 当 新
着 前 在 起 真 教 种 走 民 能 天 于 美 此 神 手 和 外 面 主
才 些 经 里 法 大 女 头 用 都 是 个 地 不 知 白 发 见 性 也
已 西 更 日 部 开 对 我 正 只 向 为 点 最 气 史 高 却 去 有


识别结果相当不错。

接下来就是生成字符映射表了。

In [10]:
table = dict(zip(
    filter(lambda x: x != '\n', list(ocr_txt)),
    filter(lambda x: 19967 < ord(x) < 40870, list(tesseract_result))
))
In [11]:
print(table)
{'\ue3ce': '其', '\ued31': '可', '\uea4a': '她', '\ue2f0': '就', '\ue707': '们', '\ue949': '来', '\ue452': '眼', '\ue8e6': '这', '\ue33c': '所', '\ue11c': '何', '\ue79f': '者', '\uecd2': '化', '\ueb16': '作', '\ue3d9': '问', '\ued29': '意', '\ue680': '没', '\uea2f': '道', '\ue876': '果', '\ue188': '样', '\ue8d2': '重', '\ue9c9': '如', '\ue76d': '然', '\ue2ad': '听', '\ue6ef': '中', '\ue7f6': '相', '\ue22f': '子', '\ue1d7': '本', '\uec78': '四', '\ue768': '己', '\uee4a': '方', '\ueb0e': '被', '\ue04e': '多', '\uec7a': '感', '\ue73a': '心', '\uebf0': '出', '\ue4fe': '太', '\ue3c7': '小', '\ue7d9': '会', '\uefb6': '笑', '\uea48': '家', '\uec38': '由', '\ue428': '自', '\uef6e': '说', '\uefd2': '把', '\ue7b0': '年', '\ue86d': '世', '\ue44d': '么', '\ue50f': '看', '\ue6c8': '使', '\ue9cb': '论', '\uedcf': '下', '\ueef4': '儿', '\uef45': '到', '\ue7ee': '从', '\uebcc': '事', '\ue2ea': '打', '\uef78': '口', '\ue1fa': '现', '\ue5fb': '便', '\ue5e1': '回', '\ue6fa': '要', '\ue89f': '代', '\ue19b': '丸', '\ue98a': '成', '\uedda': '间', '\ue2e2': '实', '\ue084': '话', '\ue3a1': '进', '\ue586': '他', '\uea75': '很', '\ue3ca': '因', '\ue78d': '得', '\ue11f': '而', '\ue0d3': '情', '\ue3aa': '别', '\ue9a2': '好', '\ue567': '无', '\ue54d': '少', '\ue72b': '特', '\ue5c6': '关', '\ue3e3': '同', '\uee30': '但', '\ue197': '人', '\ue009': '全', '\ue47d': '定', '\ue490': '声', '\ue0f4': '公', '\ue005': '什', '\ue055': '老', '\uebfd': '了', '\uec30': '等', '\ue535': '仁', '\ue6c4': '明', '\ue1ca': '先', '\ue948': '学', '\ue705': '再', '\ue1e7': '你', '\ued69': '分', '\uee76': '动', '\ue8de': '门', '\ue7b4': '还', '\uee6b': '给', '\ue1a7': '上', '\ue99b': '以', '\ue3fe': '力', '\ue52b': '十', '\ue44c': '体', '\uea63': '身', '\uebe4': '又', '\ue2df': '与', '\ue774': '国', '\ue57f': '的', '\ue5ee': '之', '\ue8a7': '将', '\ue898': '那', '\ue97d': '与', '\ue6cb': '几', '\ue801': '三', '\uecd6': '书', '\ue019': '过', '\uebc4': '物', '\ue999': '时', '\uefaf': '死', '\uec9c': '第', '\ue1fb': '想', '\ue085': '两', '\ued5d': '生', '\uee11': '文', '\ue8dc': '思', '\uea14': '后', '\ue3ac': '社', '\ue943': '二', '\uee8b': '一', '\ue08f': '理', '\ue50a': '名', '\ue5a3': '并', '\ue803': '长', '\ueaf1': '行', '\uedf4': '当', '\uebba': '新', '\ue5c3': '着', '\ue429': '前', '\uecc8': '在', '\uef7e': '起', '\ue06a': '真', '\ue804': '教', '\ue0d6': '种', '\ueff7': '走', '\uefd5': '民', '\uec84': '能', '\ue36d': '天', '\ue7cd': '于', '\ued67': '美', '\ue8a1': '此', '\ue506': '神', '\ue451': '手', '\ue0a8': '和', '\uead1': '外', '\ue601': '面', '\uebfe': '主', '\ue4d0': '才', '\ue076': '些', '\ue087': '经', '\ue9ab': '里', '\ue8d6': '法', '\ueb49': '大', '\ue584': '女', '\ue4df': '头', '\ue98f': '用', '\ue28b': '都', '\ue42f': '是', '\uede8': '个', '\ue627': '地', '\ue31a': '不', '\ue4bb': '知', '\ue4ad': '白', '\ue530': '发', '\uea6d': '见', '\ue9ed': '性', '\ue1eb': '也', '\ue00d': '已', '\uea17': '西', '\ued4b': '更', '\ueb9f': '日', '\uef5e': '部', '\ue613': '开', '\ued80': '对', '\ue44b': '我', '\ue05a': '正', '\ue150': '只', '\ue218': '向', '\ue1b2': '为', '\ue7f3': '点', '\uea35': '最', '\ue1e1': '气', '\uee44': '史', '\ue5a2': '高', '\uec17': '却', '\uecca': '去', '\uebf4': '有'}

OCR法(无外部依赖版)

上述方法除了python之外,还需要额外安装 imagemagick、tesseract以及tesseract中文训练数据,这三个部件加起来大概快有100MB。

好像有一点臃肿,那么能不能精简一点呢?

让我们再看一看晋江的自定义字体。

我们很容易发现:晋江自定义字体与方正兰亭黑(微软雅黑)极其相似。

晋江自定义字体与方正兰亭黑对比图

因此可以基于这点构建一个轻量级的识别程序。

大致流程:

  1. 绘制微软雅黑所有字符
  2. 绘制晋江自定义字体所有字符
  3. 使用已知的前者对未知的后者进行匹配,根据差异度找出最有可能的字符。

载入双方字体

In [12]:
SIZE: int = 228

FZfont: ImageFont.FreeTypeFont = None
try:
    # Windows平台直接调用系统字体即可
    FZfont = ImageFont.truetype(font='Microsoft YaHei', size=SIZE)
except Exception:
    # 其他平台需手动将字体文件放入 fonts 目录
    FZfont = ImageFont.truetype(font=os.path.join(FontsDir, 'FZLanTingHei-M-GBK.ttf'), size=SIZE)
    
JJfont: ImageFont.FreeTypeFont = ImageFont.truetype(font=os.path.join(ttfpath), size=SIZE-5)

建构基本函数

In [13]:
W, H = (SIZE, SIZE)

def draw(character: str, fontTTF: ImageFont.FreeTypeFont) -> ImageDraw:
    """
    输入字符以及字体文件,输出绘制结果。
    """
    image = Image.new("RGB", (W, H), "white")
    d = ImageDraw.Draw(image)
    offset_w, offset_h = fontTTF.getoffset(character)
    w, h = d.textsize(character, font=fontTTF)
    pos = ((W - w - offset_w) / 2, (H - h - offset_h) / 2)
    d.text(pos, character, "black", font=fontTTF)
    return image

def drawFZ(character: str) -> ImageDraw:
    """
    输入字符,输出方正兰亭黑字体绘制结果。
    """
    return draw(character, FZfont)

def drawJJ(character: str) -> ImageDraw:
    """
    输入字符,输出晋江自定义字体绘制结果。
    """
    return draw(character, JJfont)
In [14]:
import numpy as np

def compare(image1: ImageDraw, image2: ImageDraw) -> float:
    """
    输入两字体图像,输出差异度。
    """
    array1 = np.asarray(image1.convert('1'))
    array2 = np.asarray(image2.convert('1'))
    diff_array: np.ndarray = array1 ^ array2
    diff = np.count_nonzero(diff_array) / np.multiply(*diff_array.shape)
    return diff

绘制方正字体所有字符,本步骤需花费大量时间以及大量内存。

In [15]:
FZttf = ttFont.TTFont(os.path.join(FontsDir, 'FZLanTingHei-M-GBK.ttf'))
FZkeys = list(filter(lambda x: 19967 < ord(x) < 40870, listTTF(FZttf)))
FZimgs = map(lambda x: drawFZ(x), FZkeys)
FZtable = dict(zip(FZkeys, FZimgs))
del FZkeys
del FZimgs

绘制晋江字体

In [16]:
JJkeys = list(filter(lambda x: x != 'x', chars))
JJimgs = list(map(lambda x: drawJJ(x), JJkeys))
JJtable = dict(zip(JJkeys, JJimgs))

比较两字体

In [17]:
def match(jjimg: ImageDraw) -> tuple[str, float]:
    """
    将晋江字符绘制结果与方正字体进行匹配
    """
    m:str = None
    d:float = None
    for fzkey in FZtable:
        fzimg = FZtable[fzkey]
        diff = compare(jjimg, fzimg)
        if d is None:
            m = fzkey
            d = diff
        else:
            if diff < d:
                m = fzkey
                d = diff    
    return m, d
In [18]:
i = 1
jjkey = JJkeys[i]
jjimg = JJimgs[i]
jjmatch = match(jjimg)
In [19]:
jjmatch
Out[19]:
('可', 0.07107956294244383)
In [20]:
jjkey
Out[20]:
'\ued31'
In [21]:
jjimg
Out[21]:
No description has been provided for this image

可以看出识别成功。

如需识别所有晋江字符,for i in range(len(JJkeys)) 跑一个循环即可。


比较一下方正字体以及晋江字体

In [22]:
from PIL import ImageChops

i0 = drawJJ(jjkey)
i1 = drawFZ(jjmatch[0])
img_diff = ImageChops.difference(i0, i1)
display(img_diff)
del i0, i1, img_diff
No description has been provided for this image

当然每次这样跑,速度有一些慢。

因此可以先使用 imagehash 先筛选一下,只对 imagehash 相近的字符进行精细比较。这样可以节省大量时间。

具体代码就不在这里列了,详细代码可以参见此处

直接比较字体法

不同晋江字体比较图

如同许多网站一样,晋江的自定义字体并不是只有一套,而是有很多套。

但比较不同自定义字体,可以很容易发现:虽然每种字体的字符排序不同,但对于同一个汉字,不同之字体之间好像都长得一样呀!

不同晋江字体同一汉字比较图

打开编辑界面,果然是一样的(注意左上角被选择的点的坐标值)。

因此,可以根据这一点,通过直接比较字体快速识别。

In [23]:
def getCoord(char: str, ttf: ttFont.TTFont) -> list[tuple[int, int]]:
    """
    获取特定字体,指定字符的 coord
    """
    cmap = ttf.getBestCmap()
    glyf_name = cmap[ord(char)]
    coord = ttf['glyf'][glyf_name].coordinates
    coord_list = list(coord)
    return coord_list


def getCoorTable(fontTable: dict[str, str], fontttf: ttFont.TTFont) -> dict[str, list[tuple[int, int]]]:
    """
    获取指定字体的 coordTable
    """
    fontTableR = dict(zip(fontTable.values(), fontTable.keys()))
    coordTable = dict(
        zip(
            fontTableR.keys(),
            map(lambda x: getCoord(x, fontttf), fontTableR.values())
        )
    )
    return coordTable

本法要求一套已经识别完毕并且没有错识的字体作为标本,这里使用第一部分识别产生的 table

In [24]:
print(table)
{'\ue3ce': '其', '\ued31': '可', '\uea4a': '她', '\ue2f0': '就', '\ue707': '们', '\ue949': '来', '\ue452': '眼', '\ue8e6': '这', '\ue33c': '所', '\ue11c': '何', '\ue79f': '者', '\uecd2': '化', '\ueb16': '作', '\ue3d9': '问', '\ued29': '意', '\ue680': '没', '\uea2f': '道', '\ue876': '果', '\ue188': '样', '\ue8d2': '重', '\ue9c9': '如', '\ue76d': '然', '\ue2ad': '听', '\ue6ef': '中', '\ue7f6': '相', '\ue22f': '子', '\ue1d7': '本', '\uec78': '四', '\ue768': '己', '\uee4a': '方', '\ueb0e': '被', '\ue04e': '多', '\uec7a': '感', '\ue73a': '心', '\uebf0': '出', '\ue4fe': '太', '\ue3c7': '小', '\ue7d9': '会', '\uefb6': '笑', '\uea48': '家', '\uec38': '由', '\ue428': '自', '\uef6e': '说', '\uefd2': '把', '\ue7b0': '年', '\ue86d': '世', '\ue44d': '么', '\ue50f': '看', '\ue6c8': '使', '\ue9cb': '论', '\uedcf': '下', '\ueef4': '儿', '\uef45': '到', '\ue7ee': '从', '\uebcc': '事', '\ue2ea': '打', '\uef78': '口', '\ue1fa': '现', '\ue5fb': '便', '\ue5e1': '回', '\ue6fa': '要', '\ue89f': '代', '\ue19b': '丸', '\ue98a': '成', '\uedda': '间', '\ue2e2': '实', '\ue084': '话', '\ue3a1': '进', '\ue586': '他', '\uea75': '很', '\ue3ca': '因', '\ue78d': '得', '\ue11f': '而', '\ue0d3': '情', '\ue3aa': '别', '\ue9a2': '好', '\ue567': '无', '\ue54d': '少', '\ue72b': '特', '\ue5c6': '关', '\ue3e3': '同', '\uee30': '但', '\ue197': '人', '\ue009': '全', '\ue47d': '定', '\ue490': '声', '\ue0f4': '公', '\ue005': '什', '\ue055': '老', '\uebfd': '了', '\uec30': '等', '\ue535': '仁', '\ue6c4': '明', '\ue1ca': '先', '\ue948': '学', '\ue705': '再', '\ue1e7': '你', '\ued69': '分', '\uee76': '动', '\ue8de': '门', '\ue7b4': '还', '\uee6b': '给', '\ue1a7': '上', '\ue99b': '以', '\ue3fe': '力', '\ue52b': '十', '\ue44c': '体', '\uea63': '身', '\uebe4': '又', '\ue2df': '与', '\ue774': '国', '\ue57f': '的', '\ue5ee': '之', '\ue8a7': '将', '\ue898': '那', '\ue97d': '与', '\ue6cb': '几', '\ue801': '三', '\uecd6': '书', '\ue019': '过', '\uebc4': '物', '\ue999': '时', '\uefaf': '死', '\uec9c': '第', '\ue1fb': '想', '\ue085': '两', '\ued5d': '生', '\uee11': '文', '\ue8dc': '思', '\uea14': '后', '\ue3ac': '社', '\ue943': '二', '\uee8b': '一', '\ue08f': '理', '\ue50a': '名', '\ue5a3': '并', '\ue803': '长', '\ueaf1': '行', '\uedf4': '当', '\uebba': '新', '\ue5c3': '着', '\ue429': '前', '\uecc8': '在', '\uef7e': '起', '\ue06a': '真', '\ue804': '教', '\ue0d6': '种', '\ueff7': '走', '\uefd5': '民', '\uec84': '能', '\ue36d': '天', '\ue7cd': '于', '\ued67': '美', '\ue8a1': '此', '\ue506': '神', '\ue451': '手', '\ue0a8': '和', '\uead1': '外', '\ue601': '面', '\uebfe': '主', '\ue4d0': '才', '\ue076': '些', '\ue087': '经', '\ue9ab': '里', '\ue8d6': '法', '\ueb49': '大', '\ue584': '女', '\ue4df': '头', '\ue98f': '用', '\ue28b': '都', '\ue42f': '是', '\uede8': '个', '\ue627': '地', '\ue31a': '不', '\ue4bb': '知', '\ue4ad': '白', '\ue530': '发', '\uea6d': '见', '\ue9ed': '性', '\ue1eb': '也', '\ue00d': '已', '\uea17': '西', '\ued4b': '更', '\ueb9f': '日', '\uef5e': '部', '\ue613': '开', '\ued80': '对', '\ue44b': '我', '\ue05a': '正', '\ue150': '只', '\ue218': '向', '\ue1b2': '为', '\ue7f3': '点', '\uea35': '最', '\ue1e1': '气', '\uee44': '史', '\ue5a2': '高', '\uec17': '却', '\uecca': '去', '\uebf4': '有'}
In [25]:
coorTable = getCoorTable(table, ttf)
In [26]:
print(coorTable['见'])
[(934, 1119), (934, 1119), (934, 1321), (1100, 1321), (1100, 1117), (1100, 846), (1060, 643), (1210, 643), (1210, 57), (1210, -69), (1348, -69), (1666, -69), (1800, -69), (1820, 61), (1820, 61), (1820, 61), (1840, 187), (1852, 371), (1852, 371), (1852, 371), (1932, 339), (2016, 313), (2016, 313), (2016, 313), (1998, 129), (1972, -5), (1972, -5), (1972, -5), (1936, -215), (1688, -215), (1308, -215), (1046, -215), (1046, 29), (1046, 575), (999, 378), (910, 251), (910, 251), (910, 251), (720, -41), (158, -277), (158, -277), (158, -277), (114, -217), (42, -133), (42, -133), (42, -133), (570, 75), (756, 341), (756, 341), (756, 341), (934, 575), (934, 1119), (456, 365), (292, 365), (292, 1623), (1764, 1623), (1764, 377), (1600, 377), (1600, 1475), (456, 1475)]

接下来,便可以直接通过比较字体而进行识别了。

加载并识别另外一个字体 jjwxcfont_00rmg.woff2

In [27]:
def is_glpyh_similar(a: list[tuple[int, int]], b: list[tuple[int, int]], fuzz: int):
    """
    比较两字符 coor 是否相似。
    """
    if len(a) != len(b):
        return False
    found = True
    for i in range(len(a)):
        if abs(a[i][0] - b[i][0]) > fuzz or abs(a[i][1] - b[i][1]) > fuzz:
            found = False
            break
    return found

def quickMatch(jj: str, ttf: ttFont.TTFont, stdCoorTable: dict[str, list[list[int, int]]]) -> str:
    """
    通过直接比较字体快速匹配
    """
    FUZZ = 20

    jjCoord = getCoord(jj, ttf)
    for stdKey in stdCoorTable:
        stdCoord = stdCoorTable[stdKey]
        if is_glpyh_similar(jjCoord, stdCoord, FUZZ):
            return stdKey


#加载字体 jjwxcfont_00rmg
font2path = os.path.join(FontsDir, 'jjwxcfont_00rmg.woff2')
with tempfile.TemporaryFile() as tmp:
    woff2.decompress(font2path, tmp)
    tmp.seek(0)
    ttf2 = ttFont.TTFont(tmp)
    
#比较字体 jjwxcfont_00rmg
results = {}
jj2Keys = listTTF(ttf2)
for jj2 in jj2Keys:
    mchar = quickMatch(jj2, ttf2, coorTable)
    results[jj2] = mchar
2 extra bytes in post.stringData array
In [28]:
print(results)
{'\ue944': '行', '\ue0c2': '也', '\ue5b2': '便', '\ue395': '又', '\ue278': '就', '\ue0ec': '动', '\ue0de': '仁', '\ue00b': '主', '\ue79f': '而', '\ue901': '与', '\ue4f3': '要', '\ue187': '已', '\ue4c6': '和', '\ue8f8': '这', '\ue289': '外', '\uef76': '什', '\ue470': '很', '\ue710': '知', '\ue293': '身', '\ue729': '起', '\uebd3': '物', '\ueae9': '情', '\uebb7': '特', '\ue2ab': '开', '\ue5bd': '教', '\uece5': '下', '\uec02': '进', '\ue18b': '论', '\ue768': '第', '\ue52f': '好', '\uec64': '儿', '\ue1b1': '所', '\uefb2': '两', '\ue357': '手', '\ue9b0': '部', '\uebf0': '时', '\uedd2': '等', '\uea2d': '新', '\ue020': '西', '\ue4fe': '人', '\uec7c': '在', '\uee82': '发', '\ue1f6': '名', '\uebed': '都', '\ued25': '几', '\uec38': '太', '\ue964': '别', '\uea80': '长', '\ue176': '笑', '\ue917': '何', '\ue24a': '的', '\uece7': '体', '\ue9ac': '上', '\ue579': '打', '\ue7b0': '作', '\ueabf': '以', '\ueb0f': '生', '\uef3a': '眼', '\ue5f8': '事', '\ue0fc': '民', '\ueec3': '学', '\ue86b': '才', '\ue233': '自', '\ue4c1': '其', '\ue7ee': '还', '\ue1aa': '感', '\uee4b': '公', '\uea95': '家', '\ue6d1': '法', '\ueeb9': '问', '\ued61': '十', '\ue216': '点', '\ue195': '二', '\ue25e': '成', '\ue3f1': '后', '\uea9a': '子', '\ue77c': '经', '\ueda2': '但', '\ue5e1': '相', '\ueac6': '门', '\ue10e': '大', '\uef27': '从', '\ue4a2': '如', '\ue371': '理', '\uebd9': '史', '\ue455': '力', '\ue53f': '三', '\ue159': '去', '\ue4ed': '当', '\uec73': '她', '\ue255': '正', '\ue39a': '代', '\ue18a': '重', '\ue41c': '话', '\uef80': '使', '\uefa4': '由', '\uefec': '年', '\ue3e3': '我', '\uee30': '过', '\ue197': '么', '\ue294': '社', '\ueab2': '只', '\ue91d': '小', '\ue151': '更', '\ue6b9': '一', '\ue9fb': '没', '\uea85': None, '\ueae1': '最', '\ueaf6': '现', '\uea64': '将', '\ue449': '会', '\ue619': '全', '\ue10f': '口', '\ue1ca': '被', '\ue092': '世', '\ue9ad': '回', '\uee0d': '于', '\uec35': '里', '\ue907': '意', '\ue563': '明', '\uecc5': '真', '\ue24b': '向', '\ue52a': '道', '\ued16': '定', '\ueee5': '头', '\ue8ee': '气', '\ue592': '实', '\uecaf': '样', '\ue12b': '因', '\ue2bf': '无', '\ue16d': '之', 'x': None, '\ue27c': '四', '\ue25a': '文', '\ue13a': '说', '\ue6e3': '前', '\uecd6': '国', '\ue5ed': '那', '\ue8d0': '中', '\uebe3': '高', '\ueaf3': '来', '\ue372': '种', '\ue8dc': '给', '\ue6a7': '本', '\ue7da': '并', '\uec59': '却', '\ue990': '性', '\ue99d': '用', '\ueb41': '不', '\ue69b': '思', '\uebb0': '再', '\ue4f4': '为', '\ue265': '见', '\ued8e': '了', '\ue0db': '出', '\uee6f': '少', '\ue094': '己', '\ue913': '分', '\ue06f': '日', '\ue08a': '关', '\ue903': '能', '\ueb9b': '听', '\uebf8': '想', '\ue0a8': '白', '\uead1': '是', '\uef8d': '同', '\ue889': '看', '\ue182': '多', '\ueae3': '对', '\ue23e': '心', '\ue5d3': '然', '\uec97': '们', '\uefcf': '者', '\ue9f7': '美', '\ueec1': '老', '\ue690': '丸', '\ue6da': '声', '\ue7df': '女', '\uebd5': '书', '\ue1e6': '把', '\ue2b5': '方', '\uea3d': '着', '\ue467': '你', '\ue006': '到', '\ue05a': '他', '\ue74c': '天', '\ue6bc': '得', '\ue8e1': '间', '\ueb1d': '走', '\ue7f3': '此', '\uee72': '地', '\ue16e': '个', '\uefb4': '先', '\ue0b7': '死', '\ue40f': '面', '\ue472': '果', '\uec17': '神', '\ue341': '有', '\ued8a': '化', '\ue101': '些', '\ue950': '可'}

结语

全文内容到这里就结束了。

本文是编写程序晋江自定义字体破解辅助工具的副产品,具体应用可以参见项目代码。