整理了一份MdxBuilder.exe 和的 mdict-utils排序规则对照,供开发者们参考,如有错漏,实属正常。
1. 总体流程
| 项目 | MdxBuilder | mdict-utils |
|---|---|---|
| 排序前处理 | 根据 Wide/Narrow 模式处理 | Python str.lower() |
| Case 转换 | 自定义 MdxBuilderToLower 或 ASCII lower |
Python Unicode lower() |
| 去标点 | 默认不去;stripKey=true 时去除非字母数字 |
固定去除 ASCII punctuation + ASCII space |
| 排序单位 | Wide:UTF-16 code unit;Narrow:byte | Unicode code point |
| Locale | 无 | locale.strxfrm(),但默认 LC_COLLATE=C 时实际等同 identity |
| Stable sort | 是 | 是 |
| 原始 Key 是否修改 | 否 | 否 |
| Sort Key 是否写入 MDX | 否 | 否 |
2. Case / 大小写处理
这是两者最重要的差异之一。
MdxBuilder Wide
MdxBuilderToLower()
它不是现代 Unicode lowercase,也不是 Unicode CaseFold。
一共验证了 827 个发生变化的 UTF-16 code points。
而且里面存在一些历史/实现上的特殊行为。
例如:
| 字符 | MdxBuilder |
|---|---|
A |
a |
É |
é |
Σ |
σ |
ς |
σ |
µ |
μ |
× |
÷ |
ſ |
s |
ß |
ß |
ẞ |
ẞ |
其中最值得注意的是:
× U+00D7 → ÷ U+00F7
这是 MdxBuilder 中的 bug/quirk,已经验证过,不能“修正”。
mdict-utils
使用:
key.lower()
所以:
| 字符 | Python lower() |
|---|---|
A |
a |
É |
é |
Σ |
σ |
ς |
ς |
µ |
µ |
× |
× |
ſ |
ſ |
ß |
ß |
ẞ |
ß |
所以例如:
µ vs μ
MdxBuilder:
µ → μ
μ → μ
=> 两者相同
mdict-utils:
µ → µ
μ → μ
=> 两者不同
3. lower() vs CaseFold
这里特别容易混淆。
MdxBuilder
不是:
Unicode lower()
也不是:
Unicode casefold()
而是自己的历史 mapping。
mdict-utils
也是:
str.lower()
不是:
str.casefold()
所以:
ß
mdict-utils:
ß → ß
不会变成:
ss
而:
ẞ → ß
因此:
ẞ
ß
会产生相同 sort key。
4. Final Sigma
这是另一个非常重要的差异。
MdxBuilder
MdxBuilder 的自定义 mapping 中:
Σ → σ
ς → σ
因此:
Σ
σ
ς
最终会进入同一个 sort key。
mdict-utils
Python:
"Σ".lower() → "σ"
"σ".lower() → "σ"
"ς".lower() → "ς"
所以:
Σ == σ
但:
ς != σ
而且 Python lower() 对 Greek final sigma 存在上下文相关行为。
这也是确认:
MdictUtils 的 sort key 不具备严格的 prefix-preserving 特性。
5. 标点处理
这是另一个非常大的区别。
MdxBuilder
默认:
stripKey = false
时:
标点保留。
例如:
a-b
a b
a_b
a$b
sort key 中仍然存在:
a-b
a b
a_b
a$b
如果:
stripKey = true
才进行过滤。
mdict-utils
MDX 路径固定执行:
string.punctuation + " "
也就是:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
再加:
ASCII space U+0020
因此:
a-b → ab
a b → ab
a_b → ab
a$b → ab
(a)b → ab
所以:
a-b
ab
a b
a_b
在 mdict-utils 中可能产生相同 sort key。
6. Unicode 标点
这里很容易误以为 mdict-utils 会删除“所有标点”。
实际上不是。
它只删除 Python:
string.punctuation
也就是 ASCII 32 个 punctuation。
因此:
中,文
中。文
中、文
em—dash
«x»
‘x’
这些 Unicode punctuation 都会保留。
同样:
×
÷
也不会被删除。
7. 空格和 whitespace
MdxBuilder
默认情况下空格可以参与排序。
mdict-utils
只有:
U+0020 ASCII space
会被删除。
例如:
"a b" → "ab"
但:
"a\tb"
"a b" NBSP
"a b" U+3000
不会因为 punctuation stripping 而被删除。
另外,pack_mdx_txt() 在解析 TXT 时还有一个不同的行为:
line.strip()
它会先去除 ASCII whitespace 两端。
所以这里要区分:
TXT parsing
vs
sorting
这两个阶段不是一回事。
8. Unicode accent / diacritic
这是你最近发现那个问题的关键。
MdxBuilder
不会自动进行 accent stripping。
例如:
e
é
通常仍然是不同 sort key。
mdict-utils
也不会:
é → e
它只是:
lower()
+
ASCII punctuation stripping
因此:
e = U+0065
é = U+00E9
排序时:
e < é
所以正常的 mdict-utils 结果应该是:
delegue...
délégue...
两个 block 分开。
你实际看到的:
delegue
délégué
délégue
...
不是 mdict-utils comparator 的行为,而更像阅读器自己的 accent-insensitive index/sort。你上传的调查报告也通过实际 MDX 构建验证了这一点。
9. 排序单位
这是 MdxBuilder 和 mdict-utils 结构上最大的区别之一。
MdxBuilder Wide
比较:
unsigned UTF-16 code unit
例如 supplementary character:
𐐀 = U+10400
UTF-16:
D801 DC00
所以 MdxBuilder 实际比较的是:
D801 ...
而不是:
00010400
mdict-utils
Python str 比较的是:
Unicode code point
因此:
𐐀 = U+10400
就是直接按照:
0x10400
比较。
这会导致 supplementary characters 在两个系统中的排序不同。
例如:
A = U+FF21
𐐀 = U+10400
Unicode code point:
FF21 < 10400
所以 mdict-utils:
A < 𐐀
但是 MdxBuilder UTF-16:
A = FF21
𐐀 = D801 DC00
所以:
𐐀 < A
这不是 bug,而是两个实现定义不同。
10. Narrow MdxBuilder 更特殊
MdxBuilder 还有一个 Narrow pipeline。
它不是 Wide 的 Unicode 排序。
ASCII
只做:
A-Z → a-z
Big5
识别 Big5 lead/trail,然后保留原始双字节。
GBK
根据原始 byte pair 判断。
最后:
unsigned byte comparison
所以:
MdxBuilder Wide
→ UTF-16 code units
MdxBuilder Narrow
→ raw bytes
mdict-utils
→ Unicode code points
三个完全不同的排序单位。
11. GBK / Big5
MdxBuilder Narrow
如果输入是 GBK / Big5:
decode → ❌
不是先转换成 Unicode 再排序。
而是直接按照:
encoded bytes
排序。
mdict-utils
相反。
它先:
bytes
↓
Python str
↓
lower()
↓
sort
↓
encode GBK/Big5
所以即使最终 MDX 是 GBK:
排序仍然是 Unicode code-point ordering。
Claude 实际测试 UTF-8、UTF-16、GBK 三种输出,排序结果相同,也验证了这一点。
12. Locale
MdxBuilder
没有 locale collation。
就是直接:
unsigned UTF-16 / byte comparison
mdict-utils
代码中有:
locale.strxfrm()
但当前实际环境:
LC_COLLATE = C
所以:
strxfrm(s) == s
实际效果仍然是:
Unicode code-point comparison
而且 mdict-utils 自己没有调用:
setlocale()
所以实现 native backend 时,不应该真的调用 std::locale 或 strxfrm,否则反而可能引入 Android device locale 依赖。
13. Tie / 相同 sort key
两者都是:
stable sorting。
MdxBuilder
使用 stable sort。
如果:
key1 == key2
保持 source order。
mdict-utils
Python:
list.sort()
是 stable Timsort。
例如:
deleguee
deleguee
delegue, e
因为:
delegue, e
↓ remove punctuation
deleguee
所以三个 sort key 相同。
最终顺序取决于 source order。Claude 的实际 MDX 验证也确认了这一点。
14. MdictUtils 一个“看起来有 tie-break,实际上没有”的地方
原始代码还有:
if len(key1) > len(key2):
return -1
elif len(key1) < len(key2):
return 1
以及后面的:
rstrip(string.punctuation)
但这些代码实际上是 unreachable。
因为前面已经:
if key1 > key2:
return 1
elif key1 < key2:
return -1
如果两个 Python strings 既不大于也不小于,那么它们就是完全相等。
因此:
len(key1) == len(key2)
后面的 rstrip 也不会产生区别。
所以最终就是:
sort key equal
↓
return 0
↓
stable sort
报告中的 200,000 次 instrumented comparison 也验证了这些 branch 从未触发。
15. Original Key / 存储内容
这一点两者其实相同:
排序 key ≠ 存储 key。
MdxBuilder
例如:
Apple
可能生成:
apple
作为 sort key。
但最终 MDX 保存:
Apple
mdict-utils
例如:
a-b
sort key:
ab
但最终 MDX 保存:
a-b
所以:
Original Key
├──→ Sort Key → sorting
│
└──→ Stored Key → MDX
16. 最终完整对照
| 排序规则 | MdxBuilder Wide | MdxBuilder Narrow | MdictUtils |
|---|---|---|---|
| Case conversion | 自定义 MdxBuilderToLower |
ASCII A-Z → a-z | Python str.lower() |
| Unicode CaseFold | |||
ß → ss |
|||
µ → μ |
|||
ς → σ |
|||
ſ → s |
|||
ẞ → ß |
|||
× → ÷ |
|||
| ASCII punctuation | 默认保留 | 默认保留 | 删除 |
| ASCII space | 默认保留 | 默认保留 | 删除 |
| Unicode punctuation | 默认保留 | 默认保留 | 保留 |
| Accent stripping | |||
| Locale collation | 实际 C → identity |
||
| Sort unit | UTF-16 code unit | byte | Unicode code point |
| Supplementary characters | surrogate units | raw bytes | code points |
| GBK/Big5 | Wide decode / Narrow raw bytes | raw bytes | Unicode |
| UTF-8 | Wide | 不属于 Narrow | Unicode |
| Tie-break | stable | stable | stable |
| Equal key | source order | source order | source order |
| Original key modified? | |||
| Sort key stored? | |||
| stripKey | 有 | 有 | 没有,忽略该参数 |
| Prefix preserving | 原有行为 | 原有行为 | 不保证 |
| External dependency | 无 | 无 | 无 |
17. 最容易混淆的几个例子
µ vs μ
MdxBuilder:
µ → μ
μ → μ
=> equal
MdictUtils:
µ → µ
μ → μ
=> different
ς vs σ
MdxBuilder:
ς → σ
σ → σ
=> equal
MdictUtils:
ς → ς
σ → σ
=> different
ß vs ss
MdxBuilder:
ß → ß
ss → ss
=> different
MdictUtils:
ß → ß
ss → ss
=> different
两者都不是 CaseFold。
a-b vs ab
MdxBuilder default:
a-b
ab
=> different
MdictUtils:
a-b → ab
ab → ab
=> equal
delegue vs délégue
MdxBuilder:
e / é
=> different
MdictUtils:
e / é
=> different
两者都不会把 é 自动变成 e。
因此:
delegue...
délégue...
应该是两个连续 block。
之前看到的:
delegue
délégué
délégue
...
不是 MdictUtils 的排序规则。 这点已经通过真实 Python + 真实 MDX 双重验证。