拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python提取MDX/MDD词典数据:从HTML清洗到资源解包全流程实战

Python提取MDX/MDD词典数据:从HTML清洗到资源解包全流程实战

网上聊 md x 字典数据提取的帖子其实不少,但大部分都停在“装个软件点两下就导出”的程度,真正涉及到 mdx 内部结构、样式资源怎么剥离、词条 HTML 怎么批量清洗的,几乎没有系统讲过的。我自己因为做 Anki 牌组和本地语料库,前前后后折腾过十几部词典的 mdx/mdd 文件,踩了不少坑,也总结了一套从“拿到 mdx 文件”到“拿到干净数据”的完整流程。这篇就把它彻底聊透。

这个内容能帮你解决什么问题呢?简单说就是:把 mdx 词典文件里的词条 HTML、CSS 样式、音频图片资源全部提取出来,转成你自己能随意加工的纯文本、结构化数据或完整网页文件。这个过程对三类人特别有用:一是想做高质量 Anki 牌组的学习工具党,二是想把自己常用词典迁移到 Notion、Obsidian 等个人知识库的效率控,三是想分析词典数据做 NLP 语料或词频统计的技术爱好者。整个方案以 Python 脚本为主线,零基础也能跟着跑通,前提是你愿意装一个 Python 环境。

1. 为什么要提取和整体方案选择

1.1 先弄清 mdx/mdd 到底是个什么东西

很多人把 mdx 当成一个简单的“电子词典文件”,其实它远不止是一个文本容器。从格式内部看,mdx 文件由两大部分组成:一部分是词条正文数据,内部以 HTML 片段形式组织,每个词条就是一段完整的 HTML;另一部分是索引和元数据,负责把词头映射到对应的偏移位置。而 mdd 文件则是配套的资源包,存放音频、图片、CSS、JS 文件,本质上是一个没有文件名限制的压缩归档。

打个比方,mdx 就好比一本书的正文排版文件,而 mdd 是这本书附带的光盘素材,两者配合才能呈现出你在 GoldenDict 里看到的那种带发音按钮、带图片插图、带折叠展开效果的完整词条。所以“提取 mdx 字典文件中的数据”这件事,严格来说包含两条线:一条是把 mdx 里的词条 HTML 取出来,另一条是把 mdd 里的音频、图片资源解出来,最后再把两者按路径重新关联。

理解了这层结构,你就明白为什么很多“一键转换工具”导出后的效果很粗糙——它们只抓了 mdx 里的文本,却把 mdd 资源留在原地,导出的 HTML 打开后全是破图、没声音,自然没法用。

1.2 工具路线 vs 代码路线的取舍

在方案选型上,我第一轮就把所有“可视化图形工具”排除掉了,比如某些词典软件自带的导出插件、在线转换小工具。原因其实很现实:第一,大部分现成工具对超大词典支持很差,我之前拿一部 2GB 的带音频词典去转,工具直接卡死,等半小时都没反应;第二,可视化工具普遍把处理过程封装成了一个黑盒,出了问题你根本不知道它错在哪一步,也无法针对性地修改输出结果。

相比之下,代码路线的优势不是“看起来更专业”,而是你手里握的是完整的控制权。你可以一步步看数据、改正则、清洗格式、处理异常,哪怕某次运行出错,也只是停在那里等你调试,不会像黑盒工具那样直接崩掉或输出一堆你没法用的垃圾数据。

当然,代码路线的代价是你得花一两个小时熟悉环境。但从长期角度看,脚本是可以复用的——你这次提取了一部词典,下次换另一部词典,只要改改文件路径和参数就能直接跑,这个时间成本是稳赚的。

1.3 我选择的 readmdict 方案,以及它为什么靠谱

目前的 mdx 解析工具中,Python 生态里最成熟的就是readmdict这个库,它是开源项目,也在持续更新,兼容常见的 mdx/mdd 格式版本。它的原理是直接读取 mdx 的索引区,解析词条偏移量,然后按偏移把数据取出来,不需要任何中间转换。

我把它比作“一封准确的信纸”:你不用关心整个邮局怎么运作,你只需要说“我要取 100 号邮箱里的信”,它就能精确地找到那封信并原封不动递到你手里。它在处理超大文件时的稳定性,实测下来是同类工具里最好的,这两 GB 的音频词典基本上几分钟就能完整读完。

此外,我还搭配了beautifulsoup4和lxml两个库做后续的 HTML 清洗工作。BeautifulSoup 负责把词条 HTML 解析成标签树,方便我们提取纯文本或按结构重组;lxml 是它的加速解析引擎。这套组合在后续数据加工环节会展现出巨大的灵活性,我后面几节会带着你实际操作。

2. 动手前的准备与格式认知

2.1 你需要了解的 mdx 词条结构:HTML 包裹着一层又一层

在真正写脚本之前,我建议你先花十分钟了解一下 mdx 词条在“裸奔”状态下长什么样。对新手来说,最直观的方式是先用 GoldenDict 查一个词,然后右键选择“查看源代码”或“审查元素”,你看到的那段 HTML,基本就是 mdx 文件里存的内容。

常见的 mdx 词条 HTML 结构大致长这样:外层是一个div容器,带上一堆样式类名,比如class="entry"或class="dcb";内部会有span包着音标、a标签指向音频资源路径、img标签指向图片路径、link标签引用外部的 CSS 文件。这些路径,都是相对于 mdd 资源包内根目录的相对路径。

理解这个路径关系是至关重要的一步。因为后续我们把 mdd 资源解压到本地文件夹后,必须保证 HTML 里引用的路径和实际解压出来的目录结构能对齐,才能让导出的 HTML 在浏览器里正常显示。不然你就算顺利提取了所有文件,打开词条时还是会看到一片乱码和一堆小红叉。

2.2 环境搭建:从零装好 Python 和依赖库

如果你之前没装过 Python,我建议直接装 Python 3.10 以上版本,安装时记得勾选“Add Python to PATH”选项。这个细节看似不起眼,但如果不勾选,后面在命令行里输入python会提示找不到命令,还得手动配环境变量,白白浪费时间。

装完 Python 后,打开命令行工具,Windows 可以用 PowerShell 或 CMD,macOS/Linux 用终端。首先创建一个独立的虚拟环境,这样不会跟系统里其他 Python 包互相干扰:

mkdir mdx-extract cd mdx-extract python -m venv venv

激活虚拟环境的命令在 Windows 上是:

venv\Scripts\activate

macOS/Linux 上是:

source venv/bin/activate

虚拟环境激活后,命令行前面会出现(venv)前缀,这时候再安装依赖库:

pip install readmdict beautifulsoup4 lxml

如果你下载速度很慢,可以临时切换到国内镜像源,命令是pip install -i https://pypi.tuna.tsinghua.edu.cn/simple readmdict beautifulsoup4 lxml。实测下来清华源的完整度和稳定性都不错。

2.3 准备测试文件与辅助工具

在进入实操之前,我强烈建议你别一上来就拿大词典跑,先准备一部小词典做测试。市面上很多学习型词典的 mdx 文件体积都在几十 MB 到几百 MB 这个量级,选这种小文件验证脚本,跑一次只要几秒,调试起来舒心很多。

另外一个很实用的预备步骤,是找一个“文本查看器”来查看杂乱的 HTML 源码。Windows 自带的记事本对超大文件不友好,我一般用 VS Code 或者 Notepad++,它们对超长行有自动换行和高亮支持,查看导出后的词条 HTML 会清楚得多。你不需要深度使用,只要能打开、搜索、查看就够用了。

另外请把所有词典文件和脚本放在同一个项目目录下,路径里尽量不要有中文和空格。虽然 Python 3 对中文路径的支持已经改善了不少,但 readmdict 在底层解析时偶尔会跟系统编码打架,路径纯英文能帮你避开一大堆诡异问题。

3. 提取 MDX 文本数据的完整实战

3.1 理解 readmdict 的输出逻辑:从压缩包到 Python 字典

readmdict 的 API 设计得非常直白,你把文件路径传进去,它返回一个可迭代对象,每次迭代给出一组(key, value),其中key是词头,value是对应的 HTML 数据(bytes 类型)。

这里有一个关键认知:它返回的数据不是字符串,而是 bytes 字节串。我在第一次用的时候就在这里栽了跟头,直接对 bytes 做字符串替换,结果怎么弄都报编码错误。正确做法是先对 bytes 调用.decode()方法,转成字符串,再进行后续处理。编码格式通常优先尝试 UTF-8,如果解码过程中出现UnicodeDecodeError,就换成 GBK 或其他常见编码试一下。

另外要注意,readmdict 读取大文件时,词条不是一次性全部载入内存的,它是边读边迭代的。这样设计的好处是内存占用低,但也意味着你不能像操作普通列表那样随意跳转,比如想先看第 1000 个词条再回头看第 500 个,就得从头迭代到目标位置。对于绝大多数按顺序批量处理的场景,这完全够用。

3.2 完整脚本:从 mdx 里导出所有词条的 HTML

下面放一个可以直接用的脚本,整体流程分四步:打开文件、读取词条、拼接字符串、写盘输出。我把每一步的注释都写详细一些,方便你对照理解:

from readmdict import MDX import os def extract_mdx_entries(mdx_path, output_dir): # 第一步:初始化 MDX 对象,传入 mdx 文件的路径 mdx = MDX(mdx_path) # 第二步:遍历所有词条 # items() 方法返回一个可迭代对象,每个元素是 (词头, HTML字节串) for key, value in mdx.items(): # key 是 bytes 类型,需要解码成字符串 word = key.decode('utf-8') # value 是 bytes 类型的 HTML,需要解码 try: html_content = value.decode('utf-8') except UnicodeDecodeError: # 如果 utf-8 解码失败,尝试 gb18030(兼容 GBK 和大部分中文编码) html_content = value.decode('gb18030') # 第三步:处理非法文件名字符 # Windows 不支持尖括号、冒号、问号等字符出现在文件名中 safe_word = word.replace('/', '_').replace('\\', '_') \ .replace(':', '_').replace('*', '_') \ .replace('?', '_').replace('"', '_') \ .replace('<', '_').replace('>', '_') \ .replace('|', '_') # 第四步:写入单独文件 os.makedirs(output_dir, exist_ok=True) file_path = os.path.join(output_dir, safe_word + '.html') with open(file_path, 'w', encoding='utf-8') as f: f.write(f'<html><head><meta charset="utf-8"></head><body>') f.write(html_content) f.write('</body></html>') print('提取完成') if __name__ == '__main__': extract_mdx_entries('example.mdx', 'output_html')

运行后,output_html文件夹里会出现大量以词头命名的 HTML 文件。这一步输出的其实是“原汁原味”的词典渲染文件,但因为 mdd 资源还没解压,所以你在浏览器里打开时大概率看不到样式和音频,这很正常,接下来我们就来解决资源问题。

3.3 样式资源怎么处理:CSS 提取与路径重写

词条 HTML 里引用的 CSS 文件,通常在 mdd 资源包内。如果只提取 mdx 而不处理 mdd,HTML 里的<link rel="stylesheet" href="./styles.css">就会指向一个不存在的文件,页面会以默认样式展示,难看不说,很多排版逻辑也会丢。

有两条路可以解决这个问题:一条是把 mdd 里的 CSS 文件解压到和 HTML 同目录下,保持相对路径不变,这种方法最省事;另一条是把 CSS 内容直接内联插入到 HTML 的<style>标签里,这样即使你把单个 HTML 文件发给别人,样式也不会丢。

我的建议是:如果你只是自用,选第一条路,简单直接;如果你想分享出去,或者想把词条批量导入到别的软件里,选第二条路,更稳妥。第二条路的实现也不复杂,用 BeautifulSoup 解析 HTML,找到<link>标签,读到 href 指向的 CSS 文件内容,替换成<style>即可,我后面综合案例里会给整合好的代码。

3.4 从中提取纯文本:去标签的两种实战姿势

有时候我们不需要整个 HTML,只需要词条的纯文本内容,比如做词频统计或者语料库分析。这时候有两套方案:一种是用 BeautifulSoup 的get_text(),另一种是用正则表达式粗暴删除标签。

get_text()的优点是会保留文本本身的结构,自动处理嵌套标签之间的文本拼接,而且你可以通过参数控制分隔符。比如遇到音标和例句时,我们可以让不同部分之间用|分隔,方便后续处理。缺点是速度略慢,对大词典来说会有一定的时间消耗。

正则方案就干脆利落多了,直接re.sub(r'<[^>]+>', '', html),几百万词条也能飞快跑完。但它有个容易踩的坑:如果 HTML 里有<或>的转义字符实体(比如&lt;),会被误判成标签痕迹,导致文本里出现残留。所以正则方案适合快速出草稿,正式用还是要靠 BeautifulSoup 兜底。

这里我放一个提取纯文本的示例代码:

from bs4 import BeautifulSoup def html_to_text(html_content): soup = BeautifulSoup(html_content, 'lxml') # 去掉 script 和 style 标签,避免把 JS 代码当作正文 for tag in soup(['script', 'style']): tag.decompose() # 用换行分隔各个文本块 return soup.get_text('\n')

4. 提取 MDD 资源文件:音频图片一个都不能少

4.1 MDD 解包的原理与正确姿势

MDD 文件的内部格式本质与 mdx 是同源的,两者都基于相同的索引机制,只不过 mdd 里面存放的是一堆独立文件,而不是词条正文。这也意味着读取 mdd 的方式跟读取 mdx 高度类似,差异点在于 mdd 的 key 通常是完整的资源路径,比如\audio\apple.m4a。

这里有个特别容易踩的细节:mdd 内部的路径分隔符在不同词典里可能不同,有的是\,有的是/。你在 Windows 上解压时,如果直接拿\audio\apple.m4a当文件名用,创建出来的目录名可能带反斜杠,看起来非常乱。所以每次拿到 key 之后,我第一件事就是把它替换成当前操作系统的标准分隔符。

解包 mdd 有两个可选方向:一个是用 Python 脚本逐文件写出,另一个是先提取所有资源到一个临时文件夹,再用压缩工具打包。我推荐前者,因为脚本方式可以顺便做去重、重命名和目录结构整理,更有掌控感。

4.2 资源导出脚本:把 mdd 里的文件全裸出来

下面这段代码可以直接把 mdd 里所有文件解压到指定目录,同时做了路径清洗和冲突处理:

from readmdict import MDD import os import re def extract_mdd_resources(mdd_path, output_dir): mdd = MDD(mdd_path) os.makedirs(output_dir, exist_ok=True) for key, value in mdd.items(): # 1. 拿到资源路径,去掉开头的反斜杠或正斜杠 resource_name = key.decode('utf-8') resource_name = re.sub(r'^[\\/]+', '', resource_name) # 2. 统一替换路径分隔符 if os.sep == '/': resource_name = resource_name.replace('\\', '/') else: resource_name = resource_name.replace('/', '\\') # 3. 构造完整的输出路径 out_path = os.path.join(output_dir, resource_name) os.makedirs(os.path.dirname(out_path), exist_ok=True) # 4. 写文件 with open(out_path, 'wb') as f: f.write(value) print('资源解包完成') if __name__ == '__main__': extract_mdd_resources('example.mdd', 'output_resources')

运行完之后,output_resources里应该会有完整的 CSS、JS、图片、音频目录。这里我要多说一句:mdd 里的文件数量可能极其庞大,尤其是一些合集型大词典,音频能有几万甚至几十万个文件。解包过程虽然不需要很久,但磁盘占用会非常可观,建议提前确认磁盘剩余空间足够。

4.3 音频链接映射:让 HTML 里的发音按钮活起来

资源解出来只是第一步,更关键的是让 HTML 里的音频链接指向真实存在的文件。大多数 mdx 的音频链接是一个相对路径,比如sound://audio/apple.m4a或file:///audio/apple.m4a。浏览器默认不认识这些协议,必须做替换。

最简单的处理方式是把sound://替换成相对路径audio/,这样只要 HTML 文件和 audio 文件夹在同一个根目录下,音频就能正常播放。再进一步,如果你有强迫症,想让所有资源路径动态适配不同的目录,可以用相对路径加 JS 做一个自动重映射,脚本里遍历所有audio标签的src属性,统一指向实际资源目录。这部分我建议等你在浏览器里打开导出的 HTML 后,看到实际报错再去修路径,不然盲目改代码反而容易改出问题。

4.4 文件去重与目录结构规范化

mdd 资源包里有个常见的坑:同一个音频或图片,可能在多个子目录下各存了一份,导致解压后大量重复文件。这在磁盘空间有限的设备上尤其浪费,而且也没必要。

我一般解包后会做一轮去重,规则是:先记录所有文件的哈希值,遇到内容完全相同的文件就只保留一份,然后把其他位置的引用重定向。实际做起来可以用一个全局字典按文件哈希值建立索引,遍历时若发现重复,就删掉当前文件。当然,如果你只是做简单提取自用,不去重也完全没关系,这个属于进阶优化项。

目录结构规范化方面,建议确立一个固定规则:output/audio放所有音频,output/images放所有图片,output/styles放 CSS 和 JS,output/html放词条文件。这样后续你写脚本引用路径时会非常省心,不用再猜文件在哪儿。

5. 数据的进阶加工场景

5.1 制作 Anki 牌组:从词条 HTML 到卡片字段的拆分

提取 mdx 数据的最常见下游需求,就是做 Anki 牌组。假设你想把一部词典的释义、音标、例句分别填到 Anki 卡片的不同字段里,那么关键是把词条 HTML 中的信息块拆开。

拆分的思路是:先观察一个典型词条的 HTML 结构,找到音标、释义、例句对应的标签或类名,然后用 BeautifulSoup 按选择器精准提取。比如音标可能在<span class="phonetic">里,例句可能在<div class="example">里,这些类名在不同词典里是不同的,但你只要花几分钟看一下源码,就能定出准确的选择器。

拆好字段后,把它拼成 Anki 支持的下划线分隔文本格式,再利用genanki库直接生成.apkg牌组文件。整个流程能实现从词典到卡片的流水线自动化,以后想换词典重做牌组,跑一遍脚本就行。

5.2 导入 Notion 或个人知识库:把词典变成你的私有词库

如果你想在 Notion、Obsidian 这样的工具里建立一个可搜索的私人词库,同样可以用提取出来的数据。最简单的方式是生成 Markdown 格式的文件,每个词条一个.md文件,前面带属性头,后面跟释义正文。

文章的正文可以从 HTML 的纯文本版本中获取,用html_to_text函数处理。不过要注意,Notion 导入 Markdown 时对表格和复杂嵌套列表的支持有限,所以如果你词典释义里有很多复杂结构,建议先做一轮简化处理,只保留必要的层级和加粗。

我更推荐的玩法是,把提取出来的词条数据整理成 csv 或 jsonl 格式,再借助自动化工具批量导入数据库。这比手动复制粘贴高效得多,而且后续可以跟自己的阅读笔记、错题本打通,形成真正的个人知识网络。

5.3 转换成纯文本语料库:词频统计和自然语言处理

把 mdx 数据转换成纯文本后,它就可以变成一个高质量的语料库,用来做词频分析、短语搭配研究、词汇量评估等等。技术实现上没有太多门槛,核心就是遍历所有词条,把释义文本按词条追加到一个大文件里,然后交给分词工具去统计。

这里有个值得注意的细节:词典的释义文本通常是“高度压缩”的书面语言,很多句型在词典里是残片式的,跟真实语境语料有差异。所以如果你要做语言学研究,建议把词典语料和真实语料混合使用,不要单靠词典数据下结论。但如果你是做词汇表、学习清单、高频词排序这类应用型分析,词典语料的覆盖面和规范性反而比普通网页语料更有价值。

6. 高频疑难排查与避坑实录

6.1 编码问题的终极解法:乱码、报错与空文件的真相

在提取 mdx 的过程中,最常见的问题就是乱码和编码报错。一部分老词典的 HTML 数据用的是 GBK 编码,还有一些词典虽然用的是 UTF-8,但个别词条里嵌入了其他编码的字符,导致解码中断。

我的解决策略是写一个“自动探测编码”的小函数,先试 UTF-8,再试 gb18030。gb18030是 GBK 的超集,可以兼容绝大多数中文编码场景,基本能一网打尽。如果两种都不行,再通过chardet或charset-normalizer做概率性探测,最终的兜底方案是errors='ignore',宁可扔几个坏字符,也不能让整个脚本中断。

6.2 HTML 样式丢失和链接失效:在不同软件里打开缺不一样的解决方案

提取后的 HTML 在浏览器里正常,不代表在 Anki 或 GoldenDict 里也正常。一个常见问题是资源文件路径带着绝对路径,比如C:\xxx\audio,换了电脑就全失效;另一个问题是某些词典的 CSS 依赖外部网络字体,断网状态下样式退化严重。

解决办法是将所有资源路径全部改为相对路径,并且把字体等外部依赖尽可能内联或本地化。如果你准备把词条 HTML 直接拖进 Anki 使用,还建议把图片处理成 base64 内嵌格式,虽然文件体积变大,但完全避免了卡片换设备后图片挂掉的问题。这个取舍我觉得很值。

6.3 大词典文件的内存与性能优化

处理几个 GB 的 mdx+mdd 时,最怕的是内存爆掉和读写卡顿。readmdict 的迭代器设计已经帮我们省了一部分内存,但如果你在循环中把所有词条都塞进一个列表等着后续处理,内存照样会爆。正确姿势是流式处理:每读完一个词条,立刻写盘或做统计,不保留中间结果。

在加速上,建议先用小词典跑通全流程,再上大词典。大词典跑的时候,一定要开print日志或者进度条,方便判断脚本是卡住了还是在正常工作。如果只是做数据探索,可以先按词头首字母分组,只处理你想看的那部分,比如只处理A开头的词条,等逻辑验证完备后再全量跑。

6.4 常见问题速查表

下面表格里的这些问题,都是我实际操作中反复遇到的,你照着排查基本能解决八成以上的情况:

症状可能原因解决方案
输出 HTML 是乱码编码判断错误先试 utf-8,再试 gb18030
文件名超长导致报错Windows 文件名长度限制截断文件名或改用哈希命名
音频不播放sound:// 协议未转换替换成相对路径,确认音频存在
图片全部是小红叉mdd 未解包或路径不对解包 mdd,检查相对路径层级
导出脚本内存溢出把所有词条缓存到内存改成流式处理,边读边写
mdd 内文件打不开内部 encoding 为 latin1用 utf-8 和 latin1 分别尝试

我个人在实际操作中最深的体会是:mdx 提取这件事,真正的时间杀手从来不是脚本写不出来,而是“你以为提取成功,但打开一看样式是乱的、路径是断的、编码是花的”。所以强烈建议你在跑大词典之前,先拿三个不同类型的词条做一轮“端到端验证”,把 HTML、CSS、音频、图片全部在浏览器里确认无问题,再启动全量流程。这样能省下的调试时间,比你写脚本本身还要多。

返回列表