Mdict to macOS Dictionary转换笔记

首先介绍一下词典软件。

目前最流行的还是有道词典、金山词霸等app。但是这些app在专业性和权威性上又有所不足。而Mdcit系列词典可以添加各种自定义词库,而且很多网友已经制作好了很多权威词典对应的Mdict词库(见Pdawiki)。

而对于自己最常使用的macOS,支持Mdict词库的GoldenDict开发进度缓慢,界面不太好看,有时会遇到Bug。而另一款欧路词典需要收费才能添加自定义词库,而且界面、操作也不令人满意。

相反,macOS系统原生的Dictionary有着良好的操作体验,支持三指取词,阅读体验也很棒,却没有那么多自定义词库可以添加。

于是,就想着把两者的优点结合起来,将丰富的Mdict词库转换成macOS Dictionary支持的格式,而且支持图片显示、发音等等功能。

下面简要简要记录由Mdict词库文件转换成完美macOS Dictionary词库文件的过程。

所需环境

  • 开源项目

PYGLOSSARY:最新Releases见ilius/pyglossary/releases

python-lzosudo pip3 install python-lzo

  • Writing to AppleDict: sudo pip3 install lxml beautifulsoup4 html5lib

转换过程

  • 用PYGLOSSARY进行转换

假设词典文件为~/Downloads/oald8/oald8.mdx, 图片、语音文件oald8.mdd也在同一文件夹下。

先将词典文件转换成xml文件,并将其他图片、音频等文件提取出来放在OtherResources文件夹下。

1
2
3
cd ~/Downloads/oald8/
python3 ~/Software/pyglossary/pyglossary.pyw --read-options=resPath=OtherResources --write-format=AppleDict oald8.mdx oald8-apple
cd oald8-apple

PYGLOSSARY的说明中接下来是将xml文件中的相对链接中的”/“去掉以及替换spx为wav。

1
2
sed -i "" 's:src="/:src=":g' oald8.xml
sed -i "" 's|sound://\([/_a-zA-Z0-9]*\).spx|\1.wav|g' oald8.xml

其实这两步可以在之后用正则统一替换。

  • 转换语音文件

如果是spx文件,先转换成wav(需安装speex)。若是wav文件则可跳过这一步。

1
find OtherResources -name "*.spx" -execdir sh -c 'spx={};speexdec $spx  ${spx%.*}.wav' \;

然后用FFmpeg把wav文件转换成mp3文件(macOS中默认的iTunes与QuickTime均不支持wav格式)。

在当前目录新建Sounds文件夹,并新建sh脚本convert.sh文件如下。

1
for x in ./OtherResources/*.wav; do ffmpeg -i "$x" "Sounds/`basename "$x" .wav`.mp3"; done

然后执行sh脚本。

1
sh ./convert.sh

如果语音文件较多的话需等待较长时间。

  • 编译
1
make

有时生成的词典包文件中并没有图片、音频文件。这时将OtherResources文件夹中除音频外的其他文件以及放置mp3文件的Sounds文件夹复制到xxx.dictionary/Contents目录下(右键dictionary文件,选择Show Package Contents)。

  • 安装

将生成的dictinary文件复制到当前用户文件下下的/Library/Dictionaries中(或者打开Dictionary App,选择菜单栏中的文件->打开词典文件夹,即可进入该文件夹),之后在Dictionary App的菜单栏中词典->偏好设置中启用一下就行。

修正词典文件的各种问题

以上方法生成的词典会有开头说到的词典内链接跳转不正常,不能发音、图片不能显示等种种小问题。下面修复这些问题。

  • Dictionary的URI Scheme介绍

以下引自Dictionary Services Programming Guide

x-dictionary: is an URI scheme that describes cross references between entries in dictionaries. It is used in tag such as <a href="x-dictionary:r:another_id">.

The x-dictionary:URI contains three elements separated by colons as the general form—target selector, target text, and dictionary bundle ID. The target selector must be either d (for definition) or r (for reference). Use d if you want to search definitions of the following key text. Use r if you want to refer to the entry specified by the reference ID which must be unique to each dictionary.

1
2
x-dictionary:d:key_text:dict_bundle_id
x-dictionary:r:reference_id:dict_bundle_id

The dictionary bundle ID can be omitted in both forms, as shown in the following lines. If it is omitted, Dictionary Services searches the target text in all active dictionaries.

1
2
x-dictionary:d:key_text
x-dictionary:r:reference_id
  • 修复词典中的链接跳转问题。

举例来说,假设dict_bundle_id为Longman5。(dict_bundle_id可在生成的词典文件中的Info.plist中修改。)

假设xml文件中为:

1
<a href="x-dictionary:d:entry://hour">hour</a>

可替换为:

1
<a href="x-dictionary:d:hour:Longman5">hour</a>

可用正则表达式匹配然后处理。

  • 修正词典中页面内的定位问题

例如点击apple1会跳转到同一个页面中的apple2处。

1
2
<a href="x-dictionary:d:entry://#_hke1">apple1</a>
<a name="_hke1">apple2</a>

将其替换为

1
2
<a href="#_hke1">apple1</a>
<a name="_hke1">apple2</a>
  • 修复词典的发音问题

假设xml文件中为:

1
<a href="x-dictionary:d:sound://1.spx"><img border="0" src="Br.gif"/></a>

可替换为

1
<audio id="1" src="Sounds/1.mp3"/><img border="0" src="Br.gif" onmousedown="document.getElementById('1').play(); return false;"/>

onmousedown可替换成onmouseover,这样鼠标悬浮上发音图标就开始发音,而不用单击。

可用正则表达式匹配然后处理。

  • Java正则替换代码示例

以Longman5_Activator为示例,假设只需要修复链接跳转问题和页面内的定位问题。写得比较随便,随意看一下就好。

当然挺多编辑器(比如Sublime Text、Visual Studio Code)直接支持正则表达式查找和替换,这样就不用写程序了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Replace {
static final String REGEX_1 = "x-dictionary:d:entry:\\/\\/#";
static final String REGEX_2 = "entry:\\/\\/([^#\"][^\"]*)\"";
static final String Bundle_Id = "Longman5_Activator";
static final String inputName = "Longman5_Activator.xml";
static final String outputName = "Longman5_Activator_Output.xml";
static final String ENCODE = "UTF-8";
BufferedReader br = null;
BufferedWriter bw = null;

public static void main(String[] args) throws IOException {
new Replace().replaceFile();
}

public String replacefun1(String line) {
Pattern pattern = Pattern.compile(REGEX_1);
StringBuffer sbr = new StringBuffer();
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
matcher.appendReplacement(sbr, "#");
}
matcher.appendTail(sbr);
return sbr.toString();
}

public String replacefun2(String line) {
Pattern pattern = Pattern.compile(REGEX_2);
StringBuffer sbr = new StringBuffer();
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
matcher.appendReplacement(sbr, matcher.group(1) + ":" + Bundle_Id + "\"");
}
matcher.appendTail(sbr);
return sbr.toString();
}

public void replaceFile() throws IOException {
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(
inputName), ENCODE));
bw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(outputName)));
String line = null;
while ((line = br.readLine()) != null) {
bw.write(replacefun2(replacefun1(line)));
bw.newLine();
}
} catch (IOException e) {
} finally {
bw.close();
br.close();
}

}

}

其他注意事项

  • 文本匹配的正则表达式需一一调试

以上每个词典文件中的具体模式均不太一样,而且有时会有与一般模式不同的个例(如多加了几个空格、标签间加了个<br/>、中间换了一行、图片大多是png个别是gif等等),需要一一调试。推荐调试网站Regexr

  • 空格相关问题

连续两个粗体单词之间的空格会不显示。例如

1
<b>apple</b> <b>pie</b>

会显示为applepie。此时将其替换为

1
<b>apple</b>&#160;<b>pie</b>

注意xml中不能用&nbsp;

另外有时xml中会有&amp;nbsp;,原意应显示为空格,却显示为&nbsp;。此时将其替换为&#160;就好。

  • 取词小窗口修改大小

若取词小窗口中字体太小,可在词典包内容中的DefaultStyle.css文件中加上以下内容:

1
.apple_client-panel body{margin: 0 0 15px;font-size: 16px;}
  • CSS样式文件

若OtherResources文件夹中有其它CSS文件,或者原来CSS文件就独立于MDict文件之外,将其内容复制到词典文件包中的DefaultStyle.css文件中。

  • 深色模式

在深色模式中,转换的词典背景颜色依旧是白色,这可以通过修改词典包内容中的DefaultStyle.css文件解决。具体参考Make Mojave custom dictionaries work better

  • 其它问题

@@@LINK=abc替换成<a href="x-dictionary:d:abc:dict_bundle_id">abc</a>。不过这样处理后显示的是指向另一个词条的链接,不是直接显示另一个词条。解决办法是扫描整个XML文件,将@@@LINK引用移动到正确的词条下。具体可以参考下述Python代码(引用自该链接)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python3

import xml.etree.ElementTree as ET

'''
1. find the d:entry node which descendant contains the keyword "@@@LINK=" in text
2. parse the value behind "@@@LINK=" keyword and save it to variable title
3. save the index node belongs to the d:entry to variable index
4. find the d:entry node which title property exactly matches title
5. append index to the current d:entry node as child
6. remove the d:entry which descendant contains the keyword "@@@LINK="

NOTE: please remove the default namespace from oald8.xml file manually before the script running and add it back when script done.
'''

tree = ET.parse('./oald8.xml')
root = tree.getroot()

ns = {'d': 'http://www.apple.com/DTDs/DictionaryService-1.0.rng'}
ET.register_namespace('d', 'http://www.apple.com/DTDs/DictionaryService-1.0.rng')

# find all d:entry nodes which contains the keyword "@@@LINK=" in text
link_entries = root.findall('.//p/..', ns)

for l_entry in link_entries:
    index = l_entry.find('./d:index', ns)
    title = l_entry.find('./p', ns).text.replace('@@@LINK=', '')
    entry = root.find('./d:entry[@d:title="' + title + '"]', ns)

    # remove l_entry directly if title cannot be found in entries
    if entry is None:
        print(title)
        root.remove(l_entry)
        continue

    # append the index element to right entry
    entry.insert(0, index)
root.remove(l_entry)tree.write('oald8f.xml', 'UTF-8')

如果图片无法正常显示,将相对路径名最前面的/去掉,例如将<img src="/dir/abc.png"/>替换成<img src="dir/abc.png"/>.

由于macOS的Dictionary中鼠标悬浮于某一内容之上时会将其视作一个span,如果CSS中原本就有应用于所有span的样式,会把该样式应用于鼠标经过的区域。这样鼠标经过的区域会有很奇怪的样式。解决办法是在原有span中加一个class,并把原来span的样式归到新建的class中。

等等。

  • 其他功能

Dictinary还支持高亮搜索关键词、家长控制功能等等,参考之前提到过的Dictionary Services Programming Guide

与Alfred结合的一些功能

  • 快捷键取词

虽然三指轻点取词很方便,但是有些软件并不支持三指取词。结合Alfed Workflow即可实现选中待查内容后快捷键取词。

1
2
3
4
query="{query}"
word1="${query//\'/%27}"
word2="${word1// /%20}"
open dict://"$word2"
  • 快捷键发音

下载韦氏142000单词发音库(密码:w4mw)后,假设解压至~/Voice文件夹下。同理即可实现选中待查内容后快捷键发音。

1
2
3
query="{query}"
char="${query::1}"
afplay ~/Voice/"$char"/"$query".wav

当然系统中开启Speech功能,结合快捷键也能实现类似功能。缺点是这是合成的发音而非真人发音,优点是单词变形乃至句子段落都能发音。

这里附上快捷键取词和快捷键发音的Alfred Workflow下载地址(密码:f8c8)。​
更新:在单词中有空格或单引号时,原workflow失效。现在已修正。

Reference:

  1. MAC OSX词典转换笔记
  2. \BGL\DSL\MDX\CD\ ===> OS X dictionary