搜狗的细胞词库文件是搜狗自创的一种二进制格式。其中所有字符都使用unicode编码,固定2字节。几乎所有的数字都使用16位无符号整数。字符和数字都使用小端模式存储。
词库信息
Scel格式中包含的信息包含词库名,词库类型,词库信息,词库示例。这些信息都使用unicode编码,每个字符占用两个字节。实际读取的时候,以python为例,只需读取2个字节,作为uint16转化为int,再使用chr函数即可转换为一个字符。
词库位于在 0x130 – 0x338 字节(包含0x130,不包含0x338,下同),词库类型位于 0x338 – 0x540,词库信息位于 0x540 – 0xD40 ,词库示例位于 0xD40 – 0x1540。
读取词库信息示例:
unicode_array2str = lambda data: ''.join(
map(lambda x: '\n' if x == '\r' else x,
map(lambda x: ' ' if x == '\u3000' else x,
filter(lambda x: x != '\0',
(chr(struct.unpack('H', data[i: i + 2])[0]) for i in range(0, len(data), 2))
))))
with open('temp.scel', 'rb') as f:
f.seek(0x130)
print(f'词库名: {unicode_array2str(f.read(0x338-0x130))}')
print(f'词库类型: {unicode_array2str(f.read(0x540-0x338))}')
print(f'词库信息: {unicode_array2str(f.read(0xD40-0x540))}')
print(f'词库示例: {unicode_array2str(f.read(0x1540-0xD40))}')
这四个词库信息中可能会出现一些特殊字符。
在每个信息的最后全部填充的是\x00,我们需要去除。词库信息可能会出现\u3000,可能是搜狗用于分割示例和注释用的,我们将其替换为普通空格。词库信息中可能会出现’\r’,如果直接将字符串打印出来会导致只出现最后一个’\r’后面的内容。在此处我们将其替换为换行符。
词库内容
搜狗词库由两个部分组成,第一个部分为拼音表,第二个部分为实际的词组。
拼音表
拼音表从0x1540字节开始,0x2628字节结束。前四个字节是固定的0x9D010000。
从0x1544字节开始是一个列表,列表里面每个元素定义如下:
字段 | index,uint16 | 字符串长度N,uint16 | 字符串本身,编码与上一节词库信息相同 |
长度 (bytes) | 2 | 2 | N |
单个列表元素示例: 0x0000040061006900
前两个字节0x0000表示这是第0个列表元素,接着前两个字节前两个字节0x0400表明字符串长度为4字节,即2个字符。第一个字符为0x6100,即’a’,第二个字符为0x6900,即’i’。故0x040061006900转换为字符串后为’ai’。
实际上index字段总是从0开始并且依次加1,所以我们可以不用解析这个字段。
读取拼音表示例:
read_uint16 = lambda buffer: struct.unpack('<H', buffer.read(2))[0]
read_str = lambda buffer: ''.join(
chr(struct.unpack('<H', buffer.read(2))[0])
for i in range(int(read_uint16(buffer) / 2))
)
file = open('test.scel', 'rb')
pinyin_table = []
file.seek(0x1540 + 4)
while self._buffer.tell() < 0x2628:
# skip 2 bytes
file.seek(1, 2)
pinyin_table.append(read_str(file))
词组表
词组表才是词组最重要的一部分,词组表里面包含了词组、词组对应的拼音和一些其他信息。
词组表从0x2628字节开始,紧跟着的是一个列表。此列表中的每个元素为所有同音的词组。列表的每个元素的定义如下:
字段 | 词组数量M, uint16 | 词组长度N, uint16 | 词组拼音, uint16列表 | 词组列表,M个元素,见下 |
长度(bytes) | 2 | 2 | 2N | – |
其中词组拼音为一个uint16列表,每个整数都是上一节的拼音表的index。
例:拼音表为[‘a’, ‘ai’, ‘ao’],词组拼音为[0, 2, 1],那么此词组的拼音为a ao ai。
词组列表中的每个元素定义如下:
字段 | 词组字符串长度N, uint16 | 词组字符串 | 额外信息长度M, uint16 | 额外信息 |
长度(bytes) | 2 | 2N | 2 | M |
额外信息的长度通常为10字节,额外信息的前4个字节为词组的编号(uint32),后6个字节的作用未知,有可能所有10个字节都是一个无符号整数。
额外信息所对应的整数看起来像是每个词组在此scel文件中的排序,但是将词组按照此整数重新排序之后也不像是词组的出现频率之类的数值。
读取词组表示例:
read_uint32 = lambda buffer: struct.unpack('<I', buffer.read(4))[0]
table: List[Tuple[str, str ,int]] = []
file.seek(0x2628)
while word_count := read_uint16(file):
pinyin = ' '.join(pinyin_table[read_uint16(file)] for i in range(int(read_uint16(file) / 2)))
if not pinyin:
break
for _ in range(word_count):
phrase = read_str(file)
skip_length = read_uint16(file)
order = read_uint32(file)
self._buffer.skip(skip_length - 4)
table.append((phrase, pinyin, order))
'''
table is like:
[("吃饭", "chi fan", 1), ("零食", "ling shi", 2), ...]
'''
随便写了一个垃圾python库:https://github.com/xyqyear/scel2rime