[原创] 在 Ubuntu 上更新「极点五笔」词库(fcitx4 码表)

  • 现在使用五笔输入法的人是越来越少了,像我一样在Linux系统上使用五笔的,就更是少得可怜。想当年,五笔输入法还是一个比较流行的输入法的时候,在Windows上有一个非常火爆的软件:极点五笔输入法。后来有人把它移植到了Linux系统,但这种没有大公司支持的项目,是没有持续更新支持的,所以,这么多年来,我一直使用着一个过时的词库,如果遇到词库里没有的专用词,我就自己用快捷键把它加到词库里,多年累积下来,也算是更新了一小部分词库。但实际上,还是有很多覆盖不到的词,比如原始词库连"支付宝"都不认识。
  • 为了解决这个问题,我在AI的辅助下做了一些调研,并在此基础上对Ubuntu上使用的极点五笔输入法词库做了一次与时俱进的更新。
  • 说得简单点,就是如下这个过程:从公网找词库 → 筛选 → 转成 fcitx4 二进制码表 → 校验 → 上线 → 回滚预案。
  • 配套代码已开源:LINK
  • 文中只展示核心代码片段;片段为示意节选,与仓库里的文件不完全逐行一致,请以仓库中的实际代码为准(每节标注了文件名并附链接)。
  • 以下所有命令都假设你已克隆并进入仓库目录:
  • git clone https://github.com/codelast/update-freewb-dict-on-linux.git && cd update-freewb-dict-on-linux
  • 适用:fcitx 4.x + fcitx-table-wbpy(「五笔拼音」,词库源自极点五笔的极爽词库)。


0. 速览

环节 要点
我的Ubuntu机器在用的东西 Linux 上没有 Windows 版极点五笔;实际是 fcitx4 的 table 引擎 + wbpy 码表,词库是「极爽词库 6.0」
词库文件 ~/.config/fcitx/table/wbpy.mb(二进制,约 10MB,30 万条量级)
能不能直接编辑 不能。私有二进制格式,必须解析→改→重新序列化
权威格式来源 fcitx4 源码 src/im/table/tabledict.c、tools/cli/mb2txt.c、tools/cli/txt2mb.c
关键工具 用 build_mb2txt.sh 编译官方 mb2txt 做回读校验
上线流程 停 fcitx → 备份 → 基于最新文件重建 → 官方工具全量 diff → 替换 → 启动 → 实打验证
回滚 ./rollback.sh <备份>,一分钟

本次实际成果(示例):词库 312,315 → 336,897 条,新增五笔 10,203 条、拼音 14,378 条,原有记录零丢失。

文章来源:https://www.codelast.com/
仓库文件清单

下表文件均位于仓库 main 分支,点击文件名可直达源码。

文件 作用 依赖
build_mb2txt.sh 编译官方 mb2txt(码表 → 文本) gcc、curl
tabledict.h 编译用的极简头文件 —
stub.c fcitx_utils_read_uint32 的替代实现 —
mb_table.py 码表读写库(解析 / 序列化 / 有序插入) 仅标准库
merge_wubi.py 并入五笔词条(type=0) mb_table.py
gen_pinyin.py 生成拼音码(含多音备选) pypinyin
merge_pinyin.py 并入拼音记录(type=1) mb_table.py
filter_candidates.py 候选筛选(差集 + 质量判据) 词库文件、字频表
apply.sh 停服→备份→校验→替换→重启 mb2txt
rollback.sh 回滚到备份 —

1. 背景与原理

1.1 「极点五笔」在 Linux 上到底是什么

平台 实现 词库载体
Windows 极点五笔(FreeWB,杜志民) freeime\mb\ 下的纯文本码表目录
Ubuntu(本文) fcitx4 的 table 引擎 + 码表 wbpy ~/.config/fcitx/table/wbpy.mb(二进制)

所以「更新极点五笔词库」在 Linux 上等于「替换 wbpy.mb 里的词条数据」。引擎、快捷键、皮肤、用户造词习惯全部不变,只是数据变了。

wbpy.conf 里几个决定格式的关键参数:

[CodeTable]
UniqueName=wbpy
Name[zh_CN]=五笔拼音
File=wbpy.mb
ExactMatch=False     ; 前缀匹配 → 拼音码被截断也照样能用
UsePY=False
PYKey=z

1.2 为什么不直接用文本编辑器

.mb 是 fcitx 私有的二进制表:头部含按键字符集、码长、组词规则,记录区是按码排序的定长头 + 变长词条。 用文本工具改,轻则加载失败,重则索引错乱(能启动但查不到词)。


文章来源:https://www.codelast.com/
2. 二进制格式(权威来源 + 实测)

2.1 源码位置

GitHub fcitx/fcitx(fcitx4,已归档):

文件 作用
src/im/table/tabledict.c TableData::load/save,格式的唯一权威
tools/cli/mb2txt.c 官方 二进制 → 文本
tools/cli/txt2mb.c 官方 文本 → 二进制

⚠️ 不要参考 nopdan/rose 的 format/fcitx4_mb:它假设码字段宽度 = codeLen + 1(=5),实际是 pyCodeLen + 1(本文示例 = 13)。按它的写法解析会整体错位一条记录。

2.2 文件结构

┌── 头部 ─────────────────────────────────────────────┐
│ u32  0                    新格式标志                  │
│ i8   version              3 = 当前版本(<3 为旧版)   │
│ u32  keyLen                                          │
│ u8   KeyCode[keyLen+1]    NUL 结尾,如 "abc...y"      │
│ u8   codeLen              码长(本文示例 4)          │
│ u8   pyCodeLen            拼音码长(本文示例 12)     │
│ u32  ignoreLen                                       │
│ u8   InvalidChar[ignoreLen+1]                        │
│ u8   hasRule             1 = 后面跟组词规则表         │
│ (codeLen-1) × [ u8 flag, u8 words,                   │
│                 codeLen × (u8 flag, u8 which, u8 idx) ]│
│ u32  recordCount                                     │
├── 记录区(按码排序,共 recordCount 条)──────────────┤
│ 每条:                                               │
│   码    pyCodeLen+1 字节(NUL 填充;本文 13 字节)    │
│   u32   词长(**含**结尾 NUL)                        │
│   词    UTF-8 字节 + NUL                             │
│   i8    type  0=五笔 1=拼音 2=自造词 3=提示           │
│   u32   iHit  用户命中次数(新词写 0)                │
│   u32   iIndex 全局递增序号(新词取 max+1)           │
└─────────────────────────────────────────────────────┘

2.3 三条硬约束(踩过才知道)

  1. 记录必须按「码的首字符」分组排序。table.c(上游源码,仅阅读参考) 建索引时只比较 cChar != strCode[0],顺序乱了就查不到。
  2. 同码候选的先后 = 记录在文件里的先后。想让它排最后,就插到该码组的末尾;想排第一,插到组首。
  3. 前 4 字节不是词长,而是码字段的第 5~8 字节。这是最容易搞错的地方(第一版解析器就栽在这)。

2.4 匹配行为(决定拼音码能不能截断)

tabledict.c::TableCompareCode() 只按用户输入长度逐字节比较,wbpy.conf 里 ExactMatch=False,所以是前缀匹配:

size_t tmp_len = strlen(strUser);
for (i = 0; i < tmp_len; i++) { if (strUser[i] != strDict[i]) return …; }
if (exactMatch && strlen(strUser) != strlen(strDict)) return -999;

含义:拼音码被截断到 12 字母也照样能用(打前几个字母就出候选);五笔 4 码同理。


文章来源:https://www.codelast.com/
3. 工具链:编译官方 mb2txt

文件:build_mb2txt.sh(配合 tabledict.h、stub.c)

mb2txt.c(脚本自动下载) 只外部依赖一个 fcitx_utils_read_uint32,所以可以脱离 fcitx 开发环境单独编译。脚本做的事就是:放好极简头文件 → 下载官方 mb2txt.c → 用 stub 编译。

核心三行:

mkdir -p im/table && cp -f tabledict.h im/table/tabledict.h
curl -fsSL -o mb2txt.c https://raw.githubusercontent.com/fcitx/fcitx/master/tools/cli/mb2txt.c
gcc -O2 -I. -o mb2txt mb2txt.c stub.c

产出用法:

./mb2txt        ~/.config/fcitx/table/wbpy.mb > table.txt   # 二进制 → 文本
./mb2txt -o 旧版.mb > old.txt                               # 旧格式(version<3)

输出形如:

;fcitx Version 0x03 Table file
KeyCode=abcdefghijklmnopqrstuvwxy
Length=4
Pinyin=@
PinyinLength=12
Prompt=&
ConstructPhrase=^
[Rule]
e2=p11+p12+p21+p22
e3=p11+p21+p31+p32
a4=p11+p21+p31+n11
[Data]
a 工
a 戈
@a 阿          ← @ 开头 = type=1(拼音记录)

文章来源:https://www.codelast.com/
4. 从公网找词库 + 筛选

文件:filter_candidates.py

4.1 数据源(全部开源可下载)

来源 地址(标识) 内容 备注
白霜五笔 GitHub gaboolic/rime-frost → cn_dicts_wb/{chars,words}.dict.yaml 词 + 五笔码 + 现代语料词频 词频用 7 亿字语料重算(GPL-3.0)
墨奇五笔 GitHub gaboolic/rime-wubi-sentence → cn_dicts_wb/base.dict.yaml 36 万条,含大量新词/专名 码字段是"整句码",需要还原(MIT)
现代汉语常用词表 gist indiejoseph/eae09c673460aa0b56db 5.6 万条规范词表 判定"是不是一个词"(无显式许可,仅作判据)
成语库 GitHub pwxcoo/chinese-xinhua → data/idiom.json 3.09 万条成语 4 字词精确筛选(MIT)
本机表 自己 mb2txt 出来的 dump 已有词、单字五笔全码、单字读音 差集 + 编码校验基准

4.2 两个词库的码字段怎么还原

白霜:词 \t 4字母码 \t 词频,直接用。 墨奇:词 \t "xx,ax xx,ax …" \t 权重,每段对应一个字、逗号前是该字前两码,按五笔词组规则还原:

def moqi_to_code(s):
    segs = [x.split(',')[0] for x in s.split()]
    n = len(segs)
    if n == 2:  return segs[0][:2] + segs[1][:2]
    if n == 3:  return segs[0][0] + segs[1][0] + segs[2][:2]
    return segs[0][0] + segs[1][0] + segs[2][0] + segs[-1][0]

(2 字=各前两码;3 字=首字首码+次字首码+末字前两码;4 字以上=前三字首码+末字首码)

4.3 质量判据:用「凝固度」把搭配垃圾挡在门外

拼音词库是给整句解码用的,天然塞满 有多、人是、的时候 这类高频二元组。它们对拼音输入有意义,对五笔是纯噪音。

  • PMI(内部凝固度):PMI = log2( P(w) / Π P(c) ),再除以 (词长-1) 做长度归一。
  • 阈值怎么定:拿"本机已收录的 2 字词"反推分布——实测 P25 ≈ 5.0、中位数 ≈ 8.0 → 取 归一化 PMI ≥ 5.0。
  • 对照:人是 -2.5、中有 -0.9、是最 -0.2、有多 1.7(都在底部分位);题主 11.5、刷题 8.2、算力 6.8。

PMI 不是充分条件(的时候 PMI 18.1 也不是词),所以再叠三层:虚词字过滤 → 方位/副词短语过滤 → 词典交叉(把"确认是词"和"疑似新词"分成两档)。

核心片段(filter_candidates.py):

def word_code(w):
    """用本机单字全码拼词码(比信外部词库更一致)"""
    cs = [wubi_single.get(ch) for ch in w]
    if any(not c for c in cs): return None
    if len(w) == 2:   r = cs[0][:2] + cs[1][:2]
    elif len(w) == 3: r = cs[0][0] + cs[1][0] + cs[2][:2]
    else:             r = cs[0][0] + cs[1][0] + cs[2][0] + cs[-1][0]
    return r if len(r) == 4 and r.isalpha() else None

def pmi(word, freq, tot_w):
    pc = 1.0
    for ch in word:
        if ch not in char: return None
        pc *= char[ch] / tot_c
    return math.log2((freq / tot_w) / pc) / max(1, len(word) - 1)

def keep(w):
    if set(w) & FUNC: return False                      # 含虚词字
    if len(w) >= 3 and w[-1] in LOC: return False        # 方位短语
    if len(w) >= 3 and w[0] in DEG: return False         # 副词开头
    if any(g in w for g in GLUE): return False           # 常见口语搭配
    return True

4.4 三次实际筛选结果(示例)

批次 范围 主要过滤 阈值 产出
A 常用词 2~3 字,本机缺失,且在现汉词表里 虚词字 + PMI≥5 — 1,163
B 新词/专名 2~3 字,本机缺失,不在词表 上述三层 + 搭配黑名单 词频≥500 2,302
C 成语 4 字,本机缺失,在成语库里 精确匹配成语库 全量 6,738

经验:2 字档噪音约 15%,3 字档约 25~30%,4 字档原始候选噪音六成(必须靠成语库/词表分档)。


文章来源:https://www.codelast.com/
5. 程序清单与核心逻辑

5.1 mb_table.py — 码表读写库(核心)

对外三个函数:read(path) -> Table、write(path, table)、insert_sorted(table, items)。

  • read:按 2.2 的结构解析,最后断言 len(records) == recordCount(防格式判断失误)
  • write:头部原样写回 + 记录逐条序列化(码左对齐 NUL 填充、词长含 NUL)
  • insert_sorted:按码插入,同码插在组尾,iIndex 取全局 max 递增

insert_sorted 的核心(候选顺序 = 文件顺序,所以插入位置决定排第几):

def insert_sorted(table, new_items):
    max_idx = max((r[4] for r in table.records), default=0)
    add = []
    for code, word, typ in new_items:
        max_idx += 1
        add.append([code, word + b'\x00', typ, 0, max_idx])
    add.sort(key=lambda r: (r[0], r[1]))

    out, i, ai, recs = [], 0, 0, table.records
    while i < len(recs):
        if ai < len(add) and add[ai][0] < recs[i][0]:
            out.append(add[ai]); ai += 1
        else:
            cur = recs[i]; out.append(cur); i += 1
            if ai < len(add) and add[ai][0] == cur[0]:   # 走到该码组末尾
                out.append(add[ai]); ai += 1
    out.extend(add[ai:])
    table.records = out
    return len(add)

5.2 merge_wubi.py — 并入五笔词条

读 CSV(至少含 词,五笔码),跳过表里已存在的词,调用 insert_sorted(..., type=0)。

have = {r[1].rstrip(b'\x00').decode('utf-8', 'ignore') for r in t.records}
items = [(row['五笔码'].encode(), row['词'].encode(), 0)
         for row in csv.DictReader(open(cand_csv, encoding='utf-8'))
         if row['词'] not in have]
n = mb_table.insert_sorted(t, items)
mb_table.write(dst, t)

用法:python3 merge_wubi.py <源.mb> <候选.csv> <输出.mb>

5.3 gen_pinyin.py — 生成拼音码(含多音字)

三个要点:词组级注音(避免多音字按单字乱读)、多音备选、按 pyCodeLen 截断。 关键设计:备选读音必须在"本机表里已出现过的单字读音"白名单内,否则会造出输入法根本打不出的废码。

# 1) 从 dump 里取单字读音白名单
local = collections.defaultdict(set)
for ln in body:
    if ln.startswith('@') and ' ' in ln:
        c, w = ln[1:].split(' ', 1)
        if len(w) == 1:
            local[w].add(c)

# 2) 词组级注音 + 按白名单过滤每个字的读音
base = ''.join(lazy_pinyin(w))[:12]
vs = pinyin(w, heteronym=True, style=Style.NORMAL)
vs = [[p for p in lst if p in local.get(ch, {p})] or lst for lst, ch in zip(vs, w)]

# 3) 组合候选,去重,最多保留 cap 个
for combo in itertools.product(*vs):
    p = ''.join(combo)[:12]
    if p not in cands: cands.append(p)
    if len(cands) >= cap: break

用法:python3 gen_pinyin.py <词表.csv> <输出.csv> <表dump.txt> [每词最多几个码]

5.4 merge_pinyin.py — 并入拼音记录

与 merge_wubi.py 同构,区别是 type=1,且支持跳过冲突码:

若某拼音码恰好等于一个"只有唯一候选"的五笔码,插入后会破坏"唯一即上屏"。这类码要放进跳过列表。

skip = set((sys.argv[4] if len(sys.argv) > 4 else '').split(','))
items = [(row['拼音码'].encode(), row['词'].encode(), 1)
         for row in csv.DictReader(open(py_csv, encoding='utf-8'))
         if row['拼音码'] and row['拼音码'] not in skip
         and (row['词'].encode(), row['拼音码'].encode()) not in existing]

用法:python3 merge_pinyin.py <源.mb> <拼音.csv> <输出.mb> [跳过的码,...]

5.5 filter_candidates.py — 候选筛选

输入:本机 dump + 字频表(chars.dict.yaml)+ 一个或多个外部词库文件;输出 CSV:词,五笔码,词频,归一PMI。 判据见第 4.3 节。

用法:python3 filter_candidates.py <dump.txt> <chars.dict.yaml> <词库1> [词库2 ...] > cand.csv


文章来源:https://www.codelast.com/
6. 安装与验证

6.1 为什么必须先停 fcitx

fcitx-table 在退出时会重写整个 .mb(把内存里的表 + 用户造词落盘)。只要 fcitx 还在跑,你替换的文件可能被覆盖,或它下次退出时把内存旧表写回去。 所以顺序必须是:停 → 备份 → 基于最新文件重建 → 替换 → 启动。

6.2 apply.sh:一键替换(已封装上述顺序)

./apply.sh <new_table.mb> [before.txt]

脚本内部的关键几步:

pkill -TERM -x fcitx; sleep 3                      # 1) 停(让它把用户词落盘)
cp -p "$MB" "$HOME/wbpy.mb.bak-$TS"                # 2) 备份
"$HERE/mb2txt" "$NEW" > after.txt                  # 3) 回读新表
ADD=$(diff <(sort before.txt) <(sort after.txt) | grep -c '^>')
LOST=$(diff <(sort before.txt) <(sort after.txt) | grep -c '^<')
[ "$LOST" = "0" ] || exit 1                        # 4) 有丢失就中止,不动原库
cp "$NEW" "$MB"; chmod 600 "$MB"                   # 5) 替换
setsid fcitx -d &                                  # 6) 启动

手工等价流程(不用脚本时):

MB="$HOME/.config/fcitx/table/wbpy.mb"
pkill -TERM -x fcitx; sleep 3
cp -p "$MB" "$HOME/wbpy.mb.bak-$(date +%Y%m%d-%H%M%S)"
./mb2txt "$MB" > before.txt
python3 merge_wubi.py "$MB" cand_wubi.csv after.mb
./mb2txt after.mb > after.txt
diff <(sort before.txt) <(sort after.txt) | grep -c '^>'   # 新增条数
diff <(sort before.txt) <(sort after.txt) | grep -c '^<'   # 必须为 0
cp after.mb "$MB" && chmod 600 "$MB"
setsid fcitx -d &

 

  • 无图形界面的 shell 里启动 fcitx 需带上会话变量:DISPLAY=:0、XAUTHORITY=$XDG_RUNTIME_DIR/gdm/Xauthority、DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus。

  • 注意 fcitx-remote 在没有输入焦点的 shell 里可能返回 0,不能当判据;用 xprop -root | grep XIM_SERVERS(应含 @server=fcitx)或直接打字判断。

 

6.3 三层验证

层次 方法 通过标准
结构 官方 mb2txt 回读 + 全量 diff 新增条数准确、丢失 0
加载 fcitx 启动日志、xprop -root \| grep XIM_SERVERS 无表相关报错、@server=fcitx 在
功能 实际打字:唯一码 + 重码各挑几个 唯一码直接上屏;重码在候选里(新词排最后)

抽验示例:

五笔:iiid 泪流满面 / fiml 云淡风轻 / uspy 盖棺定论 / aflf 攻城略地
拼音:leiliuma → 泪流满面 / yundanfe → 云淡风轻 / zhifubao → 支付宝

6.4 回滚

./rollback.sh ~/wbpy.mb.bak-YYYYmmdd-HHMMSS

建议每批改动前都备份;确认稳定几天后只留最近一份(每个约 10MB)。


文章来源:https://www.codelast.com/
7. 经验与坑(按踩坑顺序)

  1. 别信"码宽 = 码长 + 1"。真实是 pyCodeLen + 1(w bpy 表里是 13)。按 4/5 字节解析会整体错位,且因为同码相邻记录"看起来也对",很难发现。
  2. 前 4 字节不是词长。它是码字段的中间部分;词长在码字段之后。第一版解析器能 100% "重建"文件(因为原样拷贝了未知字节),但新记录建不出来 —— round-trip 一致性只能证明"读懂了",不能证明"会写"。
  3. 记录必须按码首字符分组,否则引擎索引失效。
  4. 同码先后 = 文件先后,插入位置即候选位置。
  5. iIndex 用全局最大值 +1,iHit 新词写 0,type:五笔 0、拼音 1(写错会被当拼音/提示记录)。
  6. 拼音码截断到 pyCodeLen(12),但因为是前缀匹配,照样可用。
  7. 拼音码若与"唯一候选的五笔码"同码,会破坏"唯一即上屏",应跳过。
  8. fcitx 退出会重写 .mb,所以顺序不能颠倒;重建要基于"停之后"的最新文件。
  9. 拼音词库 ≠ 词表。有多/人是/的时候 是整句解码的 n-gram,必须靠 PMI + 词表交叉剔除。
  10. 4 字词噪音最大,不要只按词频排;用成语库精确匹配效果最好。

8. 一次完整操作清单(checklist)

[ ] git clone https://github.com/codelast/update-freewb-dict-on-linux.git && cd update-freewb-dict-on-linux   # 拿到脚本
[ ] ./build_mb2txt.sh                        # 编译官方工具
[ ] ./mb2txt <词库.mb> > dump.txt             # 差集基准 + 单字码/读音来源
[ ] 下载外部词库(白霜 / 墨奇 / 现汉词表 / 成语库)
[ ] python3 filter_candidates.py ... > cand.csv
[ ] 人工抽验候选(TOP + 中段随机)
[ ] python3 merge_wubi.py   <源.mb> cand.csv  after.mb
[ ] 可选:python3 gen_pinyin.py / merge_pinyin.py
[ ] ./mb2txt after.mb > after.txt;diff 校验:新增对、丢失 0
[ ] ./apply.sh after.mb          # 停服→备份→校验→替换→重启
[ ] 日志无报错 + xprop 有 fcitx + 实打若干词
[ ] 记下备份路径与回滚命令

附:常用命令速查

以下命令均需在仓库目录下执行。

# 查看表结构(文本形式)
./mb2txt ~/.config/fcitx/table/wbpy.mb | head -20

# 统计词条数 / 拼音记录数
./mb2txt ~/.config/fcitx/table/wbpy.mb | sed -n '/

\[Data\]

/,$p'
| tail -n +2 | wc -l ./mb2txt ~/.config/fcitx/table/wbpy.mb | grep -c '^@' # 按码查词 ./mb2txt ~/.config/fcitx/table/wbpy.mb | grep '^rdug ' # 重启输入法 fcitx -r

全部操作只在 ~/.config/fcitx/table/(词库与备份)与仓库目录(脚本)内进行,不改动系统文件。


附:授权与合规(数据来源)

组件 来源 许可证 用途
白霜五笔 gaboolic/rime-frost GPL-3.0 主要词源 + 现代词频
墨奇五笔 gaboolic/rime-wubi-sentence MIT 新词/专名
雾凇拼音(白霜上游) iDvel/rime-ice GPL-3.0 间接继承
成语库 pwxcoo/chinese-xinhua MIT 4 字成语判定
现代汉语常用词表 gist(2014) 无声明(默认保留) 仅作"是否成词"判据,未复制内容
fcitx4 源码 fcitx/fcitx GPL-2.0 编译本地校验工具
  • 本项目代码以 MIT 发布(见仓库 LICENSE)。
  • 本机自用(不分发):GPL/MIT 的义务都不触发,无风险。
  • 若要分发产出的 .mb:GPL-3.0 部分需提供生成脚本并同许可;MIT 部分保留版权声明;用户自定义词条版权归用户。

文章来源:https://www.codelast.com/
➤➤ 版权声明 ➤➤ 
转载需注明出处:codelast.com 
感谢关注我的微信公众号(微信扫一扫):
wechat qrcode of codelast
以及我的微信视频号:

发表评论