写了个简单的mdx 2.0 及mdd 2.0 的reader,用于server,能查对tired

结构化 JSON 的确问题很多,尤其是样式层和数据层容易混在一起,这点你说得对。我之前没展开,是因为一个真正通用的 JSON schema 需要反复推敲需要有取舍,我现在也还是很纠结。

但我仍然倾向于 JSON,而不是继续沿用 XML/HTML。原因很现实:XML/HTML 的设计停留在前 AI 时代,它们不具备强语法约束,也不利于结构化处理,而 JSON 天生适合被 AI 理解、生成和校验,能衍生出更多新的玩法,吸引新的用户和开发者。

如果我们再做一个新的 MDict,本质上还是旧生态的延续,老用户不会迁移,新用户也不会加入。选择 JSON 不是因为它完美,而是因为它能支撑一个面向未来的生态,而不是复制过去的格式。

问题是最终词典要显示的,而显示是绕不开html的,用json最后也得转成html,而xml有schema,xpath可以验证格式提取数据,html可以用css selector选择数据,可以给词典搭配一个metadata文件,指定常用字段,比如发音,词性的xpath,方便提取数据。而json的一大致命缺点是他的object对应大部分编程语言里的hash map,不能保证字段是有序的,主流语言只有python和js的字典是语言规定了顺序,而且都是新版规定的,如果用json就不能直接用字典而是要扩展成复杂的写法。另外词典种类很多,很难制定一个字段标准。另外接近epub格式还可以让词典能按顺序阅读,也采用zip压缩,可以方便解压编辑修改。另外我觉得用大数据那套也不好,词典除了个别的比如wiki之类的大部分词典词条在10万以内,,但是有很深的层次结构,也不需要冗余什么的,方便最重要。

json最大的问题还是对人类可读性太差,纯机读格式,做词典编辑还是得基于XML和HTML弄(很多人做mdx都要用到的正则,对json几乎没用了),json可能只是最终的转换结果,既然要转换,何不一开始就用XML和HTML。

现实是没什么人遵守,你看看yomichan词典就知道了。渴望所谓的一套程序能快速提取所有词典的特殊部分,不要仰仗于词典制作者的自觉。

JSON格式词典,只有在线网站的API会使用,除此之外,研究无数离线词典格式,几乎没人用JSON。JSON的结构化,只有词典的官方有心力雕琢这些。你说JSON格式好,那对出版方是要考虑的优点,但对民间制作者是负担。

只提取元数据的话,带标识的 XML/HTML 确实可以,最后结果就是类似苹果词典,苹果词典在弹窗的时候,会根据 XPATH 控制显示的内容。

但我想进一步约束义项和例句的关系时就很困难了,比如说很多词典是没有层级关系的,如果我想知道某个例句从属于哪个义项,义项从属于哪个词头就非常困难,需要反复修改源文件,如果只提取 JSON 的话,会相对简单一些,但仍然繁琐。

我之前也有过类似的想法。

除了结构化的优势,JSON 渲染 HTML 还有一个优势就是没有兼容问题,控制力更强类似折叠和显隐,AI 翻译完全不需要词典作者自己来实现,样式也可以使用面板让用户自己修改。

JSON 规范里对象字段是无序语义,确实遇到过需要字段有序的情况,但很少见,真正有顺序需求的场景都会用数组,因为数组的顺序是规范保证的。

数组的问题就是必须是object的数组,然后object要把标签或和样式也放进去,所以就会产生很多冗余信息,不方便编辑。
还是举牛津初阶的例子
这样一个表示派生词的例子,xml是这样的

<if-gs> (<if-g><if>goes</if>, <if>going</if>, <if>went</if><pron-gs><pron-g><form/><phon>went</phon><audio>went#_gb_2</audio></pron-g></pron-gs>, <if>has gone</if><pron-gs><pron-g><form/><phon>ɡɒn</phon><audio>gone#_gb_4</audio></pron-g></pron-gs></if-g>) </if-gs>

它在原始json数据里是这样表示的,有非常多的冗余信息,读取也不方便

"top_text": [
    {
        "tag": "if-gs",
        "path": "h-g/top-g/if-gs",
        "value": " (",
        "font_Italic": "0"
    },
    {
        "tag": "if-g",
        "path": "h-g/top-g/if-gs/if-g",
        "font_Italic": "1"
    },
    {
        "tag": "if",
        "bold": 1,
        "path": "h-g/top-g/if-gs/if-g/if",
        "color": "#000000",
        "value": "goes"
    },
    {
        "tag": "if-g",
        "path": "h-g/top-g/if-gs/if-g",
        "value": ", ",
        "font_Italic": "1"
    },
    {
        "tag": "if",
        "bold": 1,
        "path": "h-g/top-g/if-gs/if-g/if",
        "color": "#000000",
        "value": "going"
    },
    {
        "tag": "if-g",
        "path": "h-g/top-g/if-gs/if-g",
        "value": ", ",
        "font_Italic": "1"
    },
    {
        "tag": "if",
        "bold": 1,
        "path": "h-g/top-g/if-gs/if-g/if",
        "color": "#000000",
        "value": "went"
    },
    {
        "tag": "pron-g",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g",
        "font_Italic": "1"
    },
    {
        "tag": "form",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/form",
        "font_Italic": "0"
    },
    {
        "tag": "phon",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/phon",
        "value": "went"
    },
    {
        "tag": "audio",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/audio",
        "value": "went#_gb_2"
    },
    {
        "tag": "pron-gs",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/audio",
        "value": "/"
    },
    {
        "tag": "if-g",
        "path": "h-g/top-g/if-gs/if-g",
        "value": ", ",
        "font_Italic": "1"
    },
    {
        "tag": "if",
        "bold": 1,
        "path": "h-g/top-g/if-gs/if-g/if",
        "color": "#000000",
        "value": "has gone"
    },
    {
        "tag": "pron-g",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g",
        "font_Italic": "1"
    },
    {
        "tag": "form",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/form",
        "font_Italic": "0"
    },
    {
        "tag": "phon",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/phon",
        "value": "ɡɒn"
    },
    {
        "tag": "audio",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/audio",
        "value": "gone#_gb_4"
    },
    {
        "tag": "pron-gs",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/audio",
        "value": "/"
    },
    {
        "tag": "if-gs",
        "path": "h-g/top-g/if-gs/if-g/pron-gs/pron-g/audio",
        "value": ") ",
        "font_Italic": "0"
    }
]
},

牛津初阶这种 JSON 就算喂给大模型它也用不了。冗余信息过多,语义丢失, 数据与样式耦合,第三方想要解析和维护都很困难。

从数据表达能力上看,JSON 会更受限,它本质上只是 HTML 的一个子集,从 HTML 提取的内容必然会损失一些信息。我预计很多人会难以接受这一点,但为了获得结构化的数据、稳定的语义边界,以及与 AI 更深度的协作能力,这种取舍是值得的。

我想要的是这种:

{
  "ifs": [
    {
      "hw": "goes"
    },
    {
      "hw": "going"
    },
    {
      "hw": "went",
      "prs": [
        {
          "notation": "ipa",
          "text": "went",
          "region": "gb",
          "href": "went#_gb_2"
        }
      ]
    },
    {
      "hw": "gone",
      "label": "has",
      "prs": [
        {
          "notation": "ipa",
          "text": "ɡɒn",
          "region": "gb",
          "href": "gone#_gb_4"
        }
      ]
    }
  ]
}

JSON 确实不适合直接编辑,词典制作依然由 MDict 生态来承担更合适。新格式只需要承载最常用、最有价值的那部分词典,而不是试图复制整个旧生态。作为新格式,必须做减法,想要面面俱到反而可能会处处拉胯。

Yomichan 的问题就是用了 ZIP,加上兼容 HTML,谁都能打包,又难以约束词典作者的自由发挥,导致内容结构混乱、质量控制失效。类似 MDict 也这样,本来所有跳转和本地资源的链接都可以在制作阶段就完成强制校验,但都没有做。

MDX 把 Key 和 Record 分离且分块压缩,本质上就是一种专用的列存实现。

而现在的通用列存格式(如 Parquet/Lance) 配合按 Key 排序,消除“结构冗余”的能力其实更强。在列存里,不管是 HTML 标签还是 JSON Key,重复的结构都挤在一起,Zstd 压缩起来效率很高。

再加上 Metadata/Bloom Filter 辅助 SQL 引擎(如 DuckDB) 做精准的数据跳跃,这套组合拳在架构原理上完全等同于 MDX,但赢在了标准化的高兼容性上。

这样虽然很适合程序处理,但是对应制作者来说也需要程序编辑,会提高门槛。而ai一般读文件也不要求格式,更精简的xml消耗token更少。

我的想法是用xml配合一个描述结构的xml,结构用xpath或者css selector表示,如果想要内容提取可以根据这个来处理

比如这个可以写成


<inflections xpath="if-gs">
<form xpath="./if">
<pron xpath="./following-sibling::pron-gs/pron-g”>
<phon xpath="pron" />
<audio xpath="audio" />
</pron>
</form>
</inflections>

这样其实可以给任意xml加上描述,而描述是可以规定字段和格式的,这样就解决了结构化问题,现有的mdx也可以用这个方式写个描述xml,其实这是xslt的简化版,xslt也是w3c标准,最新版标准可以支持xml转json,但是比较复杂。

你这套方案太过重量级了,比如移动端就用不了。

DuckDB 是纯嵌入式的(类似 SQLite),没有后台守护进程,核心库编译完也就 20 MB。

它支持流式读取,查词时利用 Metadata 索引,只会加载命中数据所在的那个 Block/Page(通常几百 KB 到 1MB),并不会把整个文件读入内存。

至于资源占用,DuckDB 允许强制设定全局内存上限和最大线程数。我实测其引擎大概每个活跃线程需要 ~16MB 的 Buffer,在移动端只需限制并发数,内存开销就非常低且可控。WASM 版都能在手机浏览器里流畅跑,Native 集成只会更快。

退一步讲,DuckDB 只是个引擎。由于 Parquet/Lance 是通用标准格式,如果真觉得引擎重,开发者完全可以脱离 DuckDB 自行实现一个极简读取器。这比去解析 MDX 这种非标私有格式要容易且规范得多。

对比 SQLite 这个开销还是太大了。

XML 确实对词典制作者来说更加友好,但对开发者来说远不如 JSON 直接。

因为相比 JSON,XML 的自由度还是过高。JSON 是纯数据交换格式,类型系统与各语言的原生数据结构一一对应,天然友好,XML 虽然也能用于数据交换,但首先是一门描述性的语言,XML 的标签天然携带顺序语义,而原生类型普遍无序两者难以直接对应,再考虑文本和节点混合的情况,就需要引入额外的结构来表达,复杂度会进一步上升。

token 消耗一般与字节长度相关:

{"ifs":[{"hw":"goes"},{"hw":"going"},{"hw":"went","prs":[{"notation":"ipa","text":"went","region":"gb","href":"went#_gb_2"}]},{"hw":"gone","label":"has","prs":[{"notation":"ipa","text":"ɡɒn","region":"gb","href":"gone#_gb_4"}]}]}
<ifs><if><hw>goes</hw></if><if><hw>going</hw></if><if><hw>went</hw><prs><pr><notation>ipa</notation><text>went</text><region>gb</region><href>went#_gb_2</href></pr></prs></if><if><hw>gone</hw><label>has</label><prs><pr><notation>ipa</notation><text>ɡɒn</text><region>gb</region><href>gone#_gb_4</href></pr></prs></if></ifs>

再考虑最精简的情况:

{"ifs":[{"hw":"goes"}]}
<ifs><if><hw>goes</hw></if></ifs>

还是要回到需求层面,需要解决现有mdict生态什么样的痛点再决定要开发什么。非常同意提到的要做的其实是减法而不是加法。
比方像我用mdx觉得最麻烦的是mdd和css匹配问题,而且需要多种不同设备同步(iOS/android/mac/pc),可能我最需要的只是一个能够直接解读zip包里面mdx/mdd/css的词典软件,至于里面是html还是json都没关系。
如果有人需要的是一个能用AI chatbot输出词典数据的问答系统,可能需要的是转换数据本身更加结构化让app更好读取数据本身,至于样式标签就无所谓了。
也有可能有人需要的是更符合现代html规范的输出(旧ie内核太老有兼容问题),可能设计的又是另一套转换或者读取规范+搭配支持现代html的app就可以了。

文件匹配的问题,只要软件允许加载外部文件,这种问题避免不了。我倾向于将 MDX/MDD/CSS 打包在同一个文件里,这需要一个全新的格式,或者直接使用 SQLite,ZIP 的问题是内部文件必须先解压才能使用,所以不适合体积大的文件。

结构化数据可以采用 JSON 或 XML,但从现有词典中提取这些结构并不容易。许多词典本身缺乏结构,或经过人工修改而产生大量细节问题需要处理。不过,一旦这些数据被整理出来,我相信整个词典生态都能重新焕发生机。

规范化一是可以在工具打包阶段,对 HTML 结构进行强制校验,确保其符合 XHTML 的语法规范就可以了,XHTML = HTML 的语义 + XML 的语法约束;二是可以提前检查所有跳转和链接的有效性。现在的 MDict 没有任何前置检查的工具。这样做的好处不仅可以保证用户的体验,也让开发者能够放心地使用正则对 HTML 内容进行匹配和替换,因为 XML 级别的语法约束为这一过程提供了可靠的保障。

我觉得应该允许额外的资源包和外部文件,否则修改文本数据如果有几g的图片或者音频都要重新打包。zip不是问题,只要可以把文件分割成小文件就没问题,甚至可以一个词条一个文件,然后还应该保持顺序,这方面应该参考epub。