Readest 中 Babylon BGL 词典格式的解析实现与排障指南 Readest 中 Babylon BGL 词典格式的解析实现与排障指南【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readestBabylon.bgl是一种由 Babylon 词典软件使用的私有二进制词典格式包含词条、释义、字符集元数据与内嵌资源。本文以 Readest 的 Babylon Glossary 支持为主线系统讲解.bgl的二进制布局、块流解析、多字节编码推导、内联charset切换、定义尾部控制字段等核心机制并结合仓库中的 Reader 实现与真实测试 fixture 给出源码级佐证。读完本文你将能理解 Readest 是如何在浏览器端解析.bgl的以及当真实世界的 BGL 文件渲染异常时应该优先排查哪些路径。背景BGL 支持是如何进入 Readest 的Readest 在 2026 年 8 月合并了 PR #5428为应用加入了 Babylon.bgl自定义词典支持。整个实现由两部分构成解析器BglReader这是 PyGlossarybabylon_bgl插件的 TypeScript 移植。PyGlossary 插件本身又基于 ktranslator 项目对 Babylon 格式的逆向工程Raul Fernandes 与 Karl Grill 的工作。提供器createBglProvider负责把 Reader 接入阅读器的查词弹出窗lookup popup渲染方式与 Slob / StarDict 一致。从接入方式看BGL 属于单文件型词典kind: bgl与.slob同一模式导入时把单个.bgl文件打包成 bundle查询时由 provider 打开并解析。这一接线的落点位于 dictionary registry当dict.kind bgl时创建createBglProvider({ dict, fs })对应的元数据结构定义在 types.ts即ImportedDictionary.files.bgl。仓库内自带一个真实世界的测试 fixturehist-geog-en-fr.bgl这是 Olivier TABARY 编写的 History and Geography 英→法词汇表共 624 个词条源/目标编码均为 cp1252gzip 载荷起始于偏移 0x69。它与真实的 BGL 文件一起被用于 bglProvider.test.ts 中验证解析与渲染链路。文件布局从魔数到 gzip 块流BglReader.load()是解析入口见 bglReader.ts它先读取整个文件的字节数组再按以下布局逐段处理bytes [0..4) : magic 12 34 00 01 或 12 34 00 02 bytes [4..6) : u16 大端 —— gzip 流的偏移量 bytes [6..off): 垃圾字节不属于载荷 bytes [off..) : 一个 gzip member内含块流block stream验证逻辑非常严格文件长度小于 6 字节、或前三个字节不匹配12 34 00、或第四字节既不是0x01也不是0x02会直接抛出Not a Babylon glossary (bad magic)。gzipOffset由第 4、5 两个字节按大端拼出若小于 6 或超出文件长度同样抛出异常。解压使用的是fflate的gunzipSync。这一选择并非偶然——详见后文两个容易被忽略的实现细节一节。解压得到的块流被保存在this.data中所有词条定义都以字节区间span的形式指向它定义文本直到真正查询时才会被解码这是延迟解码lazy decode设计的关键。块流解析块类型与长度编码解压后的块流由连续的块组成每个块以 1 个字节开头其中低 4 位length 0xf表示块类型高 4 位length 4表示长度编码方式高 4 位 4 时表示后续还有高4位 1个字节是真正的大端长度否则数据长度就是高4位 - 4。例如首字节为0x1atype0xalen1时块类型为 10后续 2 个字节给出块数据长度。块类型定义如下类型含义载荷结构0杂项misccode 8 携带默认字符集1 / 7 / 10 / 13词条普通形式1 字节词长 词 2 字节大端释义长 释义 若干 1 字节长度的替身alternates11词条宽形式5 字节词长 词 4 字节替身数 每项 4 字节长度 4 字节释义长2内嵌资源文件1 字节名字长 名字 数据v1 跳过3词典属性2 字节大端属性码 值第一遍扫描pass 1会遍历整个块流完成三件事收集类型 1/7/10/11/13 的RawEntryBlock只记录{type, start, end}不解码把类型 3 的词典属性property code → 原始字节存入rawInfoMap供后续推导元数据与编码记录类型 0 中 code 8 携带的默认字符集CHARSET_BY_CODE查表。两种词条块的具体解析普通形式parseEntry类型 1/7/10/13的解析顺序为1 字节词长 → 词 → 2 字节大端释义长 → 释义 → 循环读取 1 字节长度的替身直到块结束见 bglReader.ts。宽形式parseEntryWide类型 11则使用更宽的字段5 字节词长、4 字节替身计数、每个替身 4 字节长度、4 字节释义长见 bglReader.ts。当某个替身长度为 0 时提前结束替身列表。两种解析都使用Set去重替身并删除与主词headword重复的项。解析出的词条元数据里只保留defiStart/defiEnd指向解压后块流中的释义字节区间——释义本体直到findEntries时才经processDefi处理。编码体系词条与释义的双编码推导BGL 最复杂的部分是编码推导词条keys使用源编码释义definitions使用目标编码两者往往不同比如英→法词典中英文词条用 cp1252 解码法语释义也用 cp1252但理论上可以是任意组合。BglReader的resolveMetadata见 bglReader.ts按照以下优先级推导全局 UTF-8 标志属性 0x11 的 bit 0x8000 置位时源/目标编码都直接取utf-8显式 charset 属性属性 0x1a 定义源编码、0x1b 定义目标编码每个属性值首字节通过CHARSET_BY_CODE查表语言属性属性 0x07源语言、0x08目标语言的编码值作为语言表中的索引取该语言对应的默认代码页类型 0 默认字符集兜底 cp1252。字符集属性表CHARSET_BY_CODE见 bglReader.ts把 Babylon 字符集属性字节映射为代码页名称属性字节代码页含义0x41 / 0x42cp1252默认 / Latin0x43cp1250东欧0x44cp1251西里尔0x45cp932日语0x46cp950繁体中文0x47cp936简体中文0x48cp1257波罗的0x49cp1253希腊0x4acp949韩语0x4bcp1254土耳其0x4ccp1255希伯来0x4dcp1256阿拉伯0x4ecp874泰语语言代码表与词性表LANGUAGES表bglReader.ts覆盖 0x000x3d 共 62 个语言代码每个代码映射到[语言名, 默认代码页]。例如 0x00→English/cp1252、0x07→Russian/cp1251、0x08→Japanese/cp932、0x09→Chinese/cp950、0x0a→Chinese/cp936、0x10→Thai/cp874。有意思的是同一个Chinese有繁体cp950与简体cp936两个代码说明 Babylon 会区分繁简中文的默认编码。PART_OF_SPEECH_BY_CODE表bglReader.ts把 0x300x44 的词性字节映射为英文标签如 0x30→noun、0x31→adjective、0x32→verb、0x33→adverb一直到 0x44→participle还包括 0x3c→abbreviation、0x3d0x42 各种名词/形容词组合。代码页到 WHATWG 标签的转换由于浏览器端解码通过TextDecoder完成代码页名称需要先映射为 WHATWG 标签。ENCODING_LABELSbglReader.ts完成了这份转换例如cp1250→windows-1250、cp932→shift_jis、cp936→gbk、cp949→euc-kr、cp950→big5。getDecoder还会做解码器缓存decodersMap并为非法标签兜底回退到windows-1252。内联charset cX标签切换个别 BGL 的释义会在中途切换编码通过内联标签charset cX…/charset实现。decodeCharsetTagsbglReader.ts的处理方式极具技巧先把原始字节通过latin1解码成字符串——latin1 是 1:1 的字节↔码元映射这样可以对字节流运行字符串正则用正则(charset\sc[]?(\w)[]?|\/charset)切分得到[文本, 标签, 标签类型]重复组维护一个编码栈U表示 utf-8、T表示 Babylon 4 位十六进制字符引用、K/E表示源编码、G表示 gbk其余落入默认编码遇到/charset出栈每段文本按当前栈顶编码经decodeTextBlock解码。decodeTextBlock中还处理了两个特例babylon-reference模式把分号分隔的 4 位十六进制引用逐段还原为字符String.fromCharCode(parseInt(ref, 16))cp1252 模式下 128255 范围内的数字字符引用如#0147;被当作字面 cp1252 字节处理而不是 Unicode 码点——这保证了弯引号等字符能正确还原。定义尾部字段0x14 控制序列定义字节的末尾可能附有一组由0x14引入的控制序列字段collectDefiFieldsbglReader.ts负责把它们从释义正文中分离出来。findDefiFieldsStart有一个关键启发式若0x14后紧跟空格0x20则视为正文的一部分继续向后搜索且最后一个字节永远不会是标记位。识别出的字段包括控制码含义载荷0x02词性part of speech1 字节查PART_OF_SPEECH_BY_CODE0x06 / 0x07不透明字段跳过固定字节数0x13 / 0x1a不透明字段1 字节长度 数据0x18显示标题display title1 字节长度 文本0x28标题翻译title translation2 字节大端长度 文本0x400x4f不透明字段长度由控制码自身推导0x50转录transcription1 字节编码码 1 字节长度 文本0x60转录transcription1 字节编码码 2 字节大端长度 文本processDefibglReader.ts按 Babylon 的排版惯例把这些字段渲染为 HTML 片段词性输出为绿色font color#007000标签随后是显示标题、标题翻译转录文本用方括号[...]包裹每个字段后追加br。转录字段仅在编码码为0x1b文本形式时才渲染其余编码码视为无法渲染的二进制数据。词条清理HTML 实体、$索引$与图片标记BGL 是 HTML 时代的格式词条文本通常混有 HTML 标签、HTML 实体和专有标记bglReader.ts移植了 PyGlossary 的bgl_text辅助函数逐一处理escapeXml/resolveEntity/replaceHtmlEntries解析#x...;、#...;数字实体和常用命名实体对csdot、fllig这类非标准命名实体Babylon 本身也是原样渲染因此保留原样不替换。注意替换结果会被再次转义。replaceHtmlEntriesInKeys与释义版本类似但要求结尾;且结果不转义。stripHtmlTags把标签替换为空格。removeControlChars/removeNewlines/normalizeNewlines清理控制字符与换行。fixImgLinksBabylon 中img的src用\x1e…\x1f包裹此函数全局剥离这两个标记字节。stripDollarIndexesbglReader.ts处理词条中的$index$后缀如make do$4$、word$$$$…。两个及以上连续$连同比率一并丢弃$…$之间夹有非数字字符的保留。该函数同样在 latin1 字符串上以字节级方式工作。词条key与替身alternate的清洗管线还略有不同processAlternate额外剥离词首的/如/make /do→make do并且先stripHtmlTags再解析实体。两遍解析与延迟解码的查询索引load()采用两遍策略这是性能与正确性的折中Pass 1遍历块流收集词条区间、原始属性和默认字符集并完成元数据与编码推导Pass 2此时源编码已知解码每个词条块的主词与替身构建小写化的查找索引index: Mapstring, number[]——同一主词可以有多个词条同形异义 homographs因此值是索引数组。释义保持未解码状态直到查询命中。findEntries(word)bglReader.ts把输入 trim 小写化后查索引对命中的每个词条区间调用processDefi延迟解码返回{ headword, alternates, definition }。查询是大小写不敏感的——测试里agriculture能命中Agriculture主词。Provider 与注册表查词弹出窗的接入createBglProvider 把 Reader 包装成DictionaryProvider首次lookup时通过initOnce惰性加载打开dict.bundleDir/dict.files.bgl基目录为Dictionaries构造并load()一个BglReader随后缓存缓存带错误记忆一旦初始化失败错误会被记录后续查询直接抛出不会反复尝试打开同一个坏文件lookup把命中的每个词条渲染为h1主词标题 divHTML 释义样式类分别为text-lg font-bold与mt-2 text-sm返回{ ok: true, headword, sourceLabel }其中sourceLabel优先取reader.metadata.title否则回退到 bundle 名。Provider 实例在 registry.ts 的模块级instanceCache中按 id 缓存同一个会话内的多次查询复用已解析的索引。测试专门验证了这一行为caches the reader — second lookup opens no extra files 断言连续两次lookup只发生一次openFile。测试验证真实 fixture 覆盖了哪些路径测试文件以真实 fixture 为样本覆盖了以下行为元数据解析title为 History and Geography、author为 Olivier TABARY、sourceLang为 English、targetLang为 French、entryCount为 624主词与替身Agriculture命中且替身包含Farming替身Big Emerging Market能反查到主词B.E.M.大小写不敏感agriculture→Agriculturecp1252 重音解码Airport的释义包含Aéroport错误处理错误魔数的文件抛出Not a Babylon glossaryProvider 全流程Airport查询成功容器内出现h1文本Airport与释义文本Aéroport未命中的词返回reason: empty导入集成groupBundlesByStem把单个.bgl分组为 bgl bundleimportDictionaries从 glossary title 推导词典名History and Geography损坏的.bgl会被标记为unsupported而不是让整个导入失败unsupportedReason匹配/not a babylon/i。两个容易被忽略的实现细节fflate 跳过 gzip CRC 是承重行为gunzipSync不校验 gzip 的 CRC32这一宽松恰恰是兼容大量真实 BGL 文件的关键许多 BGL 文件携带的是全零 CRCPyGlossary 也需要给 Python 的 gzip 模块打补丁才能解压这类文件。如果将来把解压换成DecompressionStream或 Node 的zlib这类文件就会直接解压失败。因此在更换解压实现之前必须三思。v1 有意跳过 type-2 内嵌资源当前 v1 版本会跳过类型 2 的块内嵌资源文件例如图片。后果是引用这些资源的定义会渲染出破损的img——\x1e…\x1f的 src 标记被剥离但资源本体并未暴露给渲染层。记忆文档给出的修复预案是在 Reader 中保存 type-2 块的name → bytes区间并在 Provider 渲染时把src重写为 data URL。若收到Babylon 词典图片破损的反馈优先检查这一路径。真实世界排障指南结合记忆文档的定位当真实世界的 BGL 渲染异常时按以下顺序排查复现用报告者提供的原始文件复现注意测试 fixture 只是理想样本真实文件可能包含 fixture 没有的特性检查 type-2 资源块v1 跳过内嵌资源图片破损属预期行为可先确认是否为该问题检查charset标签多编码混合的释义依赖内联标签切换fixture 中不含此类标签因此这条路径是已移植但未被测试覆盖的——若涉及多语言/多编码释义重点验证此路径检查 0x14 尾部字段同样测试 fixture 的 624 个词条中没有 0x14 尾随字段文件流中的 60 个0x14字节是信息块属性码 0x14 creationTime而不是尾部标记因此尾部字段解析器也是移植但未被真实数据驱动的验证 gzip CRC若文件解压失败先怀疑 CRC 是否为全零、解压路径是否被替换过。总结Readest 的 BGL 支持是一份相当完整的 PyGlossary 移植两遍解析 延迟解码的索引设计保证了查询性能latin1往返的字节级正则技巧让内联编码切换成为可能而真实 fixture 驱动测试则锚定了格式解析的正确性。对于想继续深入仓库的读者可以从 bglReader.ts、bglProvider.ts、registry.ts 与 bglProvider.test.ts 四个文件入手对照本文的解析流程逐段研读。【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考