五笔词库合并工具

发布时间 2024-01-09 09:17:38作者: 飞麦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# Copyright © 2022, 飞麦 <fitmap@qq.com>, All rights reserved.

# frozen_string_literal: true

# 说明:极点词库是相对较好用的词库,收纳词的数量比较恰当,太少会导致很多词打不出来,太多导致有些词重码过多。
# 个人因地域、工作、生活等因素也需要一些专用词,可以自行收集整理,然后与极点词库合并再更新到系统词库中。

# 名称:五笔词库合并工具
# 功能:将极点五笔词库与个人词库合并, 生成供微软五笔用的词库, 供 WubiLex 生成并替换系统词库
# 免费软件 极点五笔 地址: https://pc.qq.com/detail/14/detail_214.html 从其导出的词库 freeime.txt 在压缩包中已包含
# 开源软件 WubiLex 地址: https://wubi.aardio.com/ 可执行文件 WubiLex.exe 在压缩包中已包含
# 个人词库分三类:
# ⑴ fix_*.txt: 个人需要固定的词汇[不测是否与已有词重复]{如行政区划/朋友/公交站/民族/股票/街道办与社区等等}
# ⑵ ins_*.txt: 个人需要增加的词汇[检测是否与已有词重复]{如健康类/户外类/工作类/生活类/理财类等等}
# ⑶ rmv_*.txt: 个人需要删除的词汇{如化为(因华为更常用)}
# 极点五笔词库: freeime.txt
# 运行本程序后的合并词库名称: fitmap.txt (供 WubiLex 导入)

@usual_zima_h = {} # 常用汉字编码表
@rare_zima_h = {} # 生僻汉字编码表
@mazici_a_a = [] # 编码字词表 [编码, 字词1, 字词2, ...]
@cima_h = {} # 词编码表(用于分辨重复编码词及统计已编码词数量)

# 定义字符串的汉字相关函数
class String
    # 返回汉字个数
    def han_size
        chars.count { |char| char.match(/\p{Han}/) }
    end

    # 返回纯汉字串
    def han_str
        chars.select { |char| char.match(/\p{Han}/) }.join
    end

    # 是否全为汉字
    def han?
        match(/^\p{Han}+$/)
    end

    # 是否全为汉字或全角大写字母[用于股票名称]
    def han_or_da?
        match(/^(\p{Han}|[A-Z])+$/)
    end

    # 是否全为汉字或全角大写字母或英文字母数字
    def han_or_da_or_word?
        match(/^(\p{Han}|[A-Z]|\p{word})+$/)
    end
end

# 导入原始码表文件(安装极点五笔后导出的码表文本文件)
def load_freeime(freeime_txt)
    File.read(freeime_txt, mode: 'rb:bom|utf-16le').encode('utf-8')
end

# 处理尾(尾), 取消所有拼音的编码并修订四声拼音字符的编码为4位以便使用
def deal_tail(tail)
    tail.sub(/zzpy .+?\r\n/m, '').gsub(/zzpy(\w)/, 'zzp\1') # ā ō ē ī ū ǖ
end

# 将极点码表文件分割为3部分(安装极点五笔后导出的码表文本文件): 返回头、身、尾
def split_ime_txt(freeime_txt)
    ime_content = load_freeime(freeime_txt) # 此文件为原始编码文件, 不得修改
    body_loc = ime_content.index(/^\w+\s/)
    tail_loc = ime_content.index(/^zz/)
    discard_loc = ime_content.index(%r{^/}) # 抛弃特殊编码
    head = ime_content[0...body_loc]
    body = ime_content[body_loc...tail_loc]
    tail = deal_tail(ime_content[tail_loc...discard_loc])
    [head, body, tail]
end

# 保存字编码(字, 编码, 字编码表), 其中编码取最长最后的
def save_char_code(char, code, zima_h)
    if zima_h.key?(char) # 若字已有编码
        if zima_h[char].size <= code.size # 若已记录编码较短
            # puts "多重编码字: #{char} 编码=#{zima_h[char]} #{code}" if zima_h[char].size == code.size && zima_h[char] != code
            zima_h[char] = code # 记录字的更长编码
        end
    else # 若字尚无编码
        zima_h[char] = code # 记录单字的编码
    end
end

# 保存词编码(词, 编码)
def save_word_code(word, code)
    if @cima_h.key?(word)
        puts "多重编码词: #{word} 编码=#{@cima_h[word]}#{code}"
    else
        @cima_h[word] = code
    end
end

# 保存字词编码(字词, 编码)
def save_zici_code(zici, code)
    if zici.size == 1 # 若为单字, 标准极点词汇不含非汉字
        save_char_code(zici, code, @usual_zima_h)
    else # 若为词
        save_word_code(zici, code)
    end
end

# 解析字词(字词, 编码), 返回字词是否为汉字/全角大写字母/英文字母数字
def parse_zici(zici, code)
    valid_word = false
    if zici.han_or_da_or_word? # 若全部为汉字/全角大写字母/英文字母数字
        save_zici_code(zici, code)
        valid_word = true
    elsif zici[0] == '~' # 带 ~ 为生僻字(后面仅接一个字), 不引入新词库
        raise "非法长度: #{zici}" if zici.size != 2

        # puts "非法汉字: #{zici}" unless zici[1].han? # 标准极点词库含少量非汉字
        save_char_code(zici[1], code, @rare_zima_h) if zici[1].han?
    end
    # 上面不处理 ^ 开头的一些极点指令
    valid_word
end

# 解析(身)
def parse_body(body)
    body.each_line do |line| # 每行格式为: 编码 字词1 字词2 ...
        code_or_word_a = line.split # 按空格拆分
        code = code_or_word_a[0] # 获取编码
        mazici_a = [code] # 初始长度为 1
        code_or_word_a[1..].each do |word|
            valid_word = parse_zici(word, code)
            mazici_a << word if valid_word
        end
        @mazici_a_a << mazici_a if mazici_a.size >= 2 # 说明至少有一个字词
    end
end

# 获取词中若干指定位置的字的编码(词, 位置列表)
def pick_codes(word, idx_a)
    code_a = []
    idx_a.each do |idx|
        char = word[idx]
        code = @usual_zima_h[char] || @rare_zima_h[char]
        raise "缺少 #{char} 的五笔码" unless code

        code_a << code
    end
    code_a
end

# 计算双字词编码(双字词), 首1码, 首2码, 尾1码, 尾2码
def code_two_han(word)
    code_a = pick_codes(word, [0, 1])
    code_a[0][0, 2] + code_a[1][0, 2]
end

# 计算三字词编码(三字词), 首1码, 次1码, 尾1码, 尾2码
def code_three_han(word)
    code_a = pick_codes(word, [0, 1, 2])
    code_a[0][0] + code_a[1][0] + code_a[2][0, 2]
end

# 计算多字词编码(多字词), 首1码, 次1码, 三1码, 尾1码
def code_more_han(word)
    code_a = pick_codes(word, [0, 1, 2, -1])
    code_a[0][0] + code_a[1][0] + code_a[2][0] + code_a[-1][0]
end

# 根据五笔编码规则生成词的五笔码(词)
def calc_word_wbm(word)
    word = word.han_str # 仅获取其中的汉字
    if word.size == 2
        code_two_han(word)
    elsif word.size == 3
        code_three_han(word)
    else
        code_more_han(word)
    end
end

# 合并编码已存在的词(词, 五笔码, 个人词库名, 编码字词表所在行)
def merge_exist(word, wbm, jet_txt, mazici_a)
    if mazici_a[1..].index(word) # 词已存在
        puts "#{jet_txt}#{word} 词已存在" unless jet_txt.match(/^fix_/)
    else
        mazici_a[2, 0] = word # 设置新词为第二候选词
        @cima_h[word] = wbm # 记录词的五笔码
    end
end

# 合并编码不存在的词(词, 五笔码, 编码字词表下标)
def merge_lack(word, wbm, ndx)
    @mazici_a_a[ndx, 0] = [[wbm, word]] # 设置新编码与新词
    @cima_h[word] = wbm
end

# 合入一个词(词, 五笔码, 码字码表下标, 个人词库名)
def join_word(word, wbm, ndx, jet_txt)
    mazici_a = @mazici_a_a[ndx]
    if mazici_a[0] == wbm # 编码已存在
        merge_exist(word, wbm, jet_txt, mazici_a)
    else # 编码不存在
        merge_lack(word, wbm, ndx)
    end
end

# 合并一个词(词, 个人词库名)
def merge_word(word, jet_txt)
    wbm = calc_word_wbm(word) # 计算词的五笔码
    ndx = @mazici_a_a.bsearch_index { |mazici_a| mazici_a[0] >= wbm } # 二分查找五笔码
    join_word(word, wbm, ndx, jet_txt)
end

# 在编码已存在的词中删除指定词(词, 编码字词表所在行), 返回词数是否为零
def erase_exist(word, mazici_a)
    if mazici_a[1..].index(word) # 词已存在
        mazici_a.delete(word)
        @cima_h.delete(word)
    else
        puts "#{word} 不在当前词库中"
    end
    mazici_a.size <= 1
end

# 删除一个词(词, 五笔码, 码字码表下标)
def erase_word(word, wbm, ndx)
    mazici_a = @mazici_a_a[ndx]
    if mazici_a[0] == wbm # 编码已存在
        empty = erase_exist(word, mazici_a)
        @mazici_a_a.delete_at(ndx) if empty
    else # 编码不存在
        puts "#{word} 不在现有词库中"
    end
end

# 消除一个词(词)
def purge_word(word)
    wbm = calc_word_wbm(word) # 计算词的五笔码
    ndx = @mazici_a_a.bsearch_index { |mazici_a| mazici_a[0] >= wbm } # 二分查找五笔码
    erase_word(word, wbm, ndx)
end

# 插入个人词库
def ins_personal_words
    all_num = 0
    Dir['{fix,ins}_*.txt'].each_with_index do |jet_txt, jet_idx|
        usr_words = File.read(jet_txt).split(/\s+/)
        usr_words.each do |word|
            next unless word.han_or_da_or_word? && word.han_size >= 2 # 消除非汉字/非全角大写字母/非英文字母数字/单汉字

            merge_word(word, jet_txt)
        end
        all_num += usr_words.size
        puts "#{jet_idx}\t#{jet_txt}\t词数=#{usr_words.size}"
    end
    puts "所有个人词数=#{all_num}(含与已有词重复的)"
end

# 消除个人词库
def rmv_personal_words
    Dir['rmv_*.txt'].each_with_index do |jet_txt, jet_idx|
        rmv_words = File.read(jet_txt).split(/\s+/)
        rmv_words.each do |word|
            next unless word.han_or_da_or_word? && word.han_size >= 2 # 消除非汉字/非全角大写字母/非英文字母数字/单汉字

            purge_word(word)
        end
        puts "#{jet_idx}\t#{jet_txt}\t词数=#{rmv_words.size}"
    end
end

# 导入个人词库, 合并进原始码表中
def merge_personal_words
    ins_personal_words
    rmv_personal_words
end

# 根据新字词编码表生成文本
def gen_body
    mazica_str_a = []
    @mazici_a_a.each do |mazici_a|
        mazica_str_a << mazici_a.join('')
    end
    mazica_str_a << '' # 需要在尾部加个回车换行
    mazica_str_a.join("\r\n")
end

# 保存五笔编码文件(输出的码表文件)
def save_ime_txt(fitmap_txt, head, new_body, tail)
    text = ["\ufeff", head, new_body, tail].join.encode(Encoding::UTF_16LE)
    File.binwrite(fitmap_txt, text)
end

# 输出字词数量(不同长度的字词的个数)
def output_num(len_h)
    print "长=1 量=#{@usual_zima_h.size}"
    len_h.keys.sort.each do |len|
        print " 长=#{len} 量=#{len_h[len]}"
    end
    puts
end

# 统计字词情况(场景名称)
def stats_zc(title)
    puts "#{title}\t字数=#{@usual_zima_h.size}\t词数=#{@cima_h.size}"
    len_h = {} # 不同长度的字词的个数
    @cima_h.each_key do |word|
        len_h[word.size] ||= 0 # 此时考虑所有字符
        len_h[word.size] += 1
    end
    output_num(len_h)
end

# 转换代码长度与代码编码
def gen_code_pair(code)
    code_len = code.size
    u16le_code = code.ljust(4, "\u0000").encode(Encoding::UTF_16LE).force_encoding(Encoding::BINARY)
    [code_len, u16le_code]
end

# 转换字词并计算块长
def gen_zici_blklen(zici)
    u16le_zici = zici.encode(Encoding::UTF_16LE).force_encoding(Encoding::BINARY)
    block_len = 16 + u16le_zici.bytesize
    [u16le_zici, block_len]
end

# 进行 Windows 五笔系统词库单个编码多个字词的转换
def trans_zici_a(code, zici_a)
    zici_str_a = []
    zici_a.each_with_index do |zici, idx|
        weight = idx + 1
        code_len, u16le_code = gen_code_pair(code)
        u16le_zici, block_len = gen_zici_blklen(zici)
        zici_str = [block_len, weight, code_len].pack('S3') + u16le_code + u16le_zici + [0].pack('S')
        zici_str_a << zici_str
    end
    zici_str_a.join
end

# 向前推进字词转换
def step_zicis(mazici_a, mazica_str_a, code, loc)
    zici_a = mazici_a[1..]
    mazica_str = trans_zici_a(code, zici_a)
    loc += mazica_str.bytesize
    mazica_str_a << mazica_str
    loc
end

# 根据新字词编码表生成 Windows 五笔系统词库主体
def gen_lex_main
    mazica_str_a = []
    first = nil
    loc = 0
    loc_a = []
    @mazici_a_a.each do |mazici_a|
        code = mazici_a[0]
        break if code[0] >= 'z'

        if code[0] != first
            first = code[0]
            loc_a[first.ord - 'a'.ord] = loc
        end
        loc = step_zicis(mazici_a, mazica_str_a, code, loc)
    end
    loc_a << loc
    [mazica_str_a, loc_a, loc]
end

# 根据新字词编码表生成 Windows 五笔系统词库头部
def gen_lex_head(file_len, loc_a)
    head_a = []
    head_a << 'imscwubi'.encode(Encoding::BINARY)
    head_a << [1, 1].pack('S2')
    head_a << [0x40, 0xA8, file_len, 0x78563412].pack('L4')
    head_a << ["\x00"].pack('a36')
    head_a << loc_a.pack('L26')
    head_a.join
end

# 根据新字词编码表生成 Windows 五笔系统词库
def gen_lex
    mazica_str_a, loc_a, loc = gen_lex_main
    file_len = 0xA8 + loc
    lex_head = gen_lex_head(file_len, loc_a)
    File.open('ChsWubiNew.lex', 'wb') do |file|
        file.write(lex_head)
        mazica_str_a.each do |mazica_str|
            file.write(mazica_str)
        end
    end
end

# 五笔词库合并工具主程序
def main
    head, body, tail = split_ime_txt('freeime.txt')
    parse_body(body)
    stats_zc('原始')
    merge_personal_words
    stats_zc('合并后')
    save_ime_txt('fitmap.txt', head, gen_body, tail)
    gen_lex
end

$PROGRAM_NAME == __FILE__ && Dir.chdir('/N/Jet') { main }