跳到主要内容

数据层

要实现汉字的自动拆分,首要的工作就是把汉字表示成容易被计算机理解并处理的数据格式。

字符 Character

在本系统中,汉字或不成字的汉字构件统一用「字符 Character」这个数据类型表示,它既是后端数据库中存储的类型,也是前端 TypeScript 中的类型。

interface Character {
unicode: number;
// 字集数据
tygf: 0 | 1 | 2 | 3; // 1, 2, 3 表示通用规范汉字集一至三级字,0 表示不属于
gb2312: boolean;
// 字音数据
readings: string[];
// 字形数据
glyphs: (BasicComponent | DerivedComponent | Compound)[];
// 其他辅助数据
name: string | null; // PUA 字符的别名
gf0014_id: number | null; // 兼容 GF0014-2009 汉字部件标准
ambiguous: boolean; // 维护标记,用于管理员标记字的分部有没有歧义,对数据的使用没有影响
}

其中,字形数据是一个列表,表示了这个字的零个、一个或多个可能的字形表示。每个表示可能是「基本部件 BasicComponent」、「衍生部件 DerivedComponent」和「复合体 Compound」这三者之一。

基本部件 BasicComponent

基本部件相当于传统意义上的独体字,凡是没有明显间隙的字形都可以算作一个部件。基本部件的表示方式是直接用 SVG 曲线在一张横坐标 0 至 100、纵坐标 0 至 100 的画布上绘制出汉字的各个笔画:

interface SVGStroke {
feature: string;
start: [number, number];
curveList: Draw[];
}

interface BasicComponent {
type: "basic_component";
tags?: string[]; // 给这个部件描述打上的标签
strokes: SVGStroke[];
}

在每个笔画中,首先声明笔画的种类,如横、竖、撇、点等,其具体的分类方式遵照 GF 2001-2001 分为 31 类。其次声明笔画的起始点位置坐标。最后是绘制笔画的 SVG 命令,由于折笔的存在,绘制命令可能有多个。

一个绘制命令由种类描述符(水平 H、竖直 V、倾斜 L 和三次曲线 C)和一系列参数构成,比如「沿水平方向绘制 10 个单位」的命令就是

const draw: Draw = { command: "h", parameterList: [10] };

哪种笔画应该包含哪些绘制命令,详见 https://github.com/hanzi-chai/hanzi-chai.github.io/blob/main/src/lib/classifier.ts#L39

衍生部件 DerivedComponent

很多部件其实包含的笔画都长得差不多,而且常常有「单笔画衍生」的关系,例如「戊」加一笔变成「戌」、「戍」、「成」。由于这些衍生部件中的笔画已经在基本部件中出现过了,所以我们没有必要把它们再写一遍,只需要描述是取自哪个部件的哪些笔画即可。这种方法主要是受鹤形的衍生小字启发。

interface ReferenceStroke {
feature: "reference";
index: number;
}

interface DerivedComponent {
type: "derived_component";
tags?: string[]; // 给这个部件描述打上的标签
source: string;
strokes: (SVGStroke | ReferenceStroke)[];
}

另外,在制作具体的形码方案时,衍生部件可以很方便地描述一些非成字的字根。

但是显然,汉字的数量非常多,我们不可能对所有汉字都进行如此细致的刻画。为此我们引出下一个概念:

复合体 Compound

复合体相当于传统意义上的合体字,凡是在视觉上有一定间隙的字形都可以划分成两或三个部分,如左右结构、上下结构、包围结构等。一个部分可以是一个部件,也可以是其他的复合体。只要构成复合体的各个部件有 SVG 笔画描述,那么我们就获得了关于这个复合体的描述;复合体还可以参与构造其他复合体,这样我们就可以表示任意复杂的汉字结构。

在本系统中,一个字可以有多种分部方式,每种分部方式包括四种信息:结构描述字符、各部分的名称、分部标签(可选)以及笔顺描述序列(可选)。下面依次进行详细介绍。

interface Compound {
type: "compound",
tags?: string[], // 给这个复合体描述打上的标签
operator: "⿰" | "⿱" | "⿲" | "⿳" | "⿴" | ...,
operandList: [string, string] | [string, string, string],
order?: { index: number, strokes: number }[]
}

结构描述字符

即 Unicode 表意文字描述字符(U+2FF0 至 U+2FFF,参见维基百科这 16 个中的任一个(但现在只用到了 12 个)。

各部分的名称

出于一致性考虑,各个部分的顺序并不是按照笔顺来的,而是只考虑几何关系:上下(上中下)结构从上到下,左右(左中右)结构从左到右,包围结构从外到内,重叠(⿻)结构从外到内。笔顺请在下面的字段中指定。

标签

标签是一种系统的处理分部歧义的方式。汉字的分部方式一般来自于字源或者字形,其中字源即是按照一个字在造字时的理据来分部(比如:形声字分为形旁和声旁;会意字分为各个有独立意义的部件),而字形则是按部件之间的自然间隙来划分。如果一个字根据字源和字形都能得出相同的一种分部方式,那么这个字就没有歧义。反之,若有分部的歧义,一般是由于以下两个原因:

  1. 字形和字源相矛盾。由于汉字的演变,按字源的分部可能并不是最直观的,例如「魔」从「麻」声,但是若分为「广」及余部可能更直观;「裹」、「街」等字的形旁包裹声旁;「班」、「辨」等字的声旁包裹形旁;「赢」、「微」等字通过省声构造了其他字,导致实际的声旁和形旁关系复杂。
  2. 在同一个方向上,存在多个结合紧密程度近似的部件,如「亭」、「亮」等,让人不知道应该从哪里分

标签可以帮助我们解决这一问题,例如如果把所有「赢」=「赢字框」+「贝」、「羸」=「赢字框」+「羊」等等的分部方式都打上「赢框」标签,用户就可以通过这个标签批量地指定所有具有「赢字框」的字都采用这种分部方式。再比如很多二笔方案会规定带「冖」的字将它看作是一个分部界限,这类规则也可以成为一种标签。

笔顺描述序列

如果整个复合体的笔顺并不是依次写完每个部分,而是有所交叉,那么需要指定一个笔顺序列,以便计算字根序。例如,「国」字的笔顺序列是第一部「囗」写 2 笔,然后写第二部「玉」,然后再写「囗」的第三笔