📕

mac の辞書をターミナルから使う ちょいリッチに

2021/01/04に公開

先日こんな記事を書きました
Apple Script で CLI を作るための要点

mac の 辞書.app をターミナルから使いたいという話です

その中のこの部分

実はやりたかったのは 辞書.app の起動ではない

ここについてちょっと切り込みます

シンプルな対処

objective-c 用の辞書 api があり、それを python の PyObjC 経由で使えばやりたいことは解決できるのは知っていた

実は結構簡単です

dictionary.py
#!/usr/bin/python

import sys
from DictionaryServices import *

sys.stdout = codecs.getwriter('utf8')(sys.stdout)

word = sys.argv[1].decode('utf-8')
result = DCSCopyTextDefinition(None, word, (0, len(word)))

print result

DCSCopyTextDefinitionを使うだけです

不満

このスクリプトは前から持っていたんだけど、不満が多かったので解消しようと思います

  1. ちょい遅い
  2. 国語辞典より和英辞典を優先したい
  3. 読みづらい
  4. ミススペルするとそっけない

1, 2, 3 を解決したコードと、1, 2, 3, 4 を解決したコードを末尾に記載します

1. ちょい遅い

事象

2 秒くらいかかる

$ for word (透過性 免罪 りんご 意識 資源 apple apriori fox edit macintosh); do
    echo -n `gdate +"%H:%M:%S.%3N"`
    python dictionary.py $word > /dev/null
    echo -n ' - '
    echo `gdate +"%H:%M:%S.%3N"`
done

12:07:34.767 - 12:07:36.903
12:07:36.906 - 12:07:38.996
12:07:39.000 - 12:07:41.062
12:07:41.066 - 12:07:43.111
12:07:43.115 - 12:07:45.219
12:07:45.223 - 12:07:47.268
12:07:47.271 - 12:07:49.408
12:07:49.413 - 12:07:51.496
12:07:51.500 - 12:07:53.617
12:07:53.621 - 12:07:55.649

ちょっと使い心地が悪い

対処: import を最低限にする

これが遅いみたい

python
>>> from DictionaryServices import *

修正して再計測

- from DictionaryServices import *
+ from DictionaryServices import DCSCopyTextDefinition

0.3 秒くらいになった

$ for word (透過性 免罪 りんご 意識 資源 apple apriori fox edit macintosh); do
    echo -n `gdate +"%H:%M:%S.%3N"`
    python dictionary.py $word > /dev/null
    echo -n ' - '
    echo `gdate +"%H:%M:%S.%3N"`
done

12:47:01.203 - 12:47:01.504
12:47:01.508 - 12:47:01.799
12:47:01.802 - 12:47:02.085
12:47:02.089 - 12:47:02.388
12:47:02.392 - 12:47:02.682
12:47:02.685 - 12:47:02.972
12:47:02.976 - 12:47:03.377
12:47:03.381 - 12:47:03.691
12:47:03.695 - 12:47:04.070
12:47:04.074 - 12:47:04.363

満足 🎉

2. 国語辞典より和英辞典を優先したい

問題

僕は英語が知りたくて日本語を引いてるのに、国語辞典を引いてしまう

$ python dictionary.py 引用

いんよう 0【引用】(名)スル① 古人の言や他人の文章,また他人の説や事例などを自分の文章の中に引いて説明に用いること。「古典の例を―する」② ポスト-モダンの芸術や建築で作品の中に過去の様式や他人の作品を部分的に組み入れる手法。〈子項目〉引用指数引用符

日本語の説明を望むことはほぼないので、日本語の場合は辞書を和英辞典に固定したい

対処: 引数で辞書を指定する(失敗)

見るからに第一引数が怪しいので、調べてみる

dictionary.py
DCSCopyTextDefinition(None, word, (0, len(word)))

DCSCopyTextDefinition | Apple Developer Documentation

func DCSCopyTextDefinition(_ dictionary: DCSDictionary?, 
                           _ textString: CFString,
                           _ range: CFRange) -> Unmanaged<CFString>?

指定すれば良さそうだが、どうやら指定できないらしい

dictionary : This parameter is reserved for future use, so pass NULL. Dictionary Services searches in all active dictionaries.

対処: 辞書.app の設定を変更する

見つかった最初の結果を使うみたいなので、この並びを変えれば解決できそう

This function returns the description of the first matching record found in the the active dictionaries. It searches first in the default word definition dictionary which, in the English environment, is the Oxford dictionary.

調べたところ 辞書.app の設定から変更できた

つまんで並び替える

再実行

$ python dictionary.py 引用
いんよう 【引用】名詞(a) quotation ⦅from⦆; (a) citation ⦅from⦆. ▸ 聖書からの引用 a quotation from the Bible.▸ 引用符付きの文 a sentence in quotation marks [in quotes]. (!いずれも複数形で用いることに注意) ▸ この本は他の作品からの引用が多い This book has a lot of citations from other works. 引用する 動詞quote ; 【例として引用する】cite. (!quote は言葉をそのまま引用することで, cite は quote ほど正確な引用ではない ) ▸ ミルトンを引用する quote (from) Milton. (!from は出所を強調する) ▸ バイロンから一節を引用する quote a passage from Byron.▸ 大統領はリンカーンの言葉を引用して演説を締めくくった The President finished his speech with a quotation from Lincoln.引用句[文] a quotation ⦅話⦆ a quote / a citation.引用書 reference books / the books referred to.

ちゃんと和英辞典になった

満足 🎉

3. 読みづらい

問題

これはもう見ての通り

文字列としてこうなってます、ひどすぎます、もう少しどうにかしようとは思わなかったんだろうか

$ python dictionary.py injection
in・jec・tion | ɪndʒékʃ(ə)n | 名詞複~s | -z | 1 U〖具体例ではan (...) ~/~s〗 注射(shot1) ▸ give A an injectionA〈人〉に注射する ▸ by injection注射によって ▸ lethal injection 毒物注射(による死刑). 2 CU(燃料などの)噴射, 注入. 3 C(資金の)投入. 4 UC(宇宙船を)軌道にのせること. ~̀ mólding 射出成形〘金属・プラスチック・セラミックを型に射出して成形する方法〙.

対処: 正規表現で強引になんとかする

色々試しつつ、和英辞典は日本語と英語の境目と品詞の後ろに改行を入れてみた

dictionary.py
def format_for_j_to_e(line):
    line = re.sub(u'([ぁ-んァ-ン一-龥])\s*([a-zA-Z])', r'\1\n\2', line)
    line = re.sub(u'([a-zA-Z.\)!?])\s*([ぁ-んァ-ン一-龥▸])', r'\1\n\n\2', line)
    line = re.sub(u'(名詞|動詞|形容詞|副詞)', r'\1\n', line)

    return '\n' + line

before

$ python dictionary.py 透過
とうか 【透過】透過する 動詞〘物理〙 transmit (-tt-); 〘生物〙 permeate ⦅into; through⦆. 透過光 transmitted light.透過性 permeability.透過装置 a permeation device.

after

$ python dictionary.py 透過

とうか 【透過】透過する 動詞
〘物理〙 transmit (-tt-); 〘生物〙 permeate ⦅into; through⦆.

透過光
transmitted light.

透過性
permeability.

透過装置
a permeation device.

辞書.app

英和辞典は数字の前と品詞の前後と ▸ の前に改行を入れてみる

dictionary.py
def format_for_e_to_j(line):
    line = re.sub(u'([0-9] )', r'\n\n\1', line)
    line = re.sub(u'(名詞|動詞|形容詞|他動詞|接頭辞|副詞)', r'\n\n\1\n ', line)
    line = re.sub(u'(▸)', r'\n\1 ', line)

before

$ python dictionary.py injection
in・jec・tion | ɪndʒékʃ(ə)n | 名詞複~s | -z | 1 U〖具体例ではan (...) ~/~s〗 注射(shot1) ▸ give A an injectionA〈人〉に注射する ▸ by injection注射によって ▸ lethal injection 毒物注射(による死刑). 2 CU(燃料などの)噴射, 注入. 3 C(資金の)投入. 4 UC(宇宙船を)軌道にのせること. ~̀ mólding 射出成形〘金属・プラスチック・セラミックを型に射出して成形する方法〙.

after

$ python dictionary.py injection

in・jec・tion | ɪndʒékʃ(ə)n |

名詞
 複~s | -z |

1 U〖具体例ではan (...) ~/~s〗 注射(shot1)
▸  give A an injectionA〈人〉に注射する
▸  by injection注射によって
▸  lethal injection 毒物注射(による死刑).

2 CU(燃料などの)噴射, 注入.

3 C(資金の)投入.

4 UC(宇宙船を)軌道にのせること. ~̀ mólding 射出成形〘金属・プラスチック・セラミックを型に射出して成形する方法〙.

辞書.app

文中に副詞の場合は〜とか出てきてしまうと改行してしまうけど、これだけ改善されれば十分かな

品詞とかに色をつけたりもできたけど、僕はいらないかな

やろうと思えば同じ発想で改行ではなく Select Graphic Rendition を置換で埋めれば可能

満足 🎉

4. ミススペルするとそっけない

問題

単語がヒットしなかった場合は、DCSCopyTextDefinitionNoneを返す

python
>>> print DCSCopyTextDefinition(None, 'moniter', (0, len('moniter')))
None

ちょっとそっけない

対処: スペルチェックをして再実行する

スペルチェックはこれを使う
Apple Script で CLI を作るための要点 # 実践 スペルチェック

$ osascript spelling.scpt moniter
monster, monitor, moniker

パスを通すなり何なりしておく

僕はspという短いコマンド名にしてパスを通してあります

まずはprocessの処理中でヒットしたかのチェックをして

  def process(word):
      result = look_up(word)

      if re.match('[a-zA-Z]', word) is None:
-         print format_for_j_to_e(result)
+         if result is None:
+             print '\nno results.'
+ 
+         else:
+             print format_for_j_to_e(result)

      else:
-         print format_for_e_to_j(result)
+         if result is None:
+             suggest_and_re_process(word)
+ 
+         else:
+             print format_for_e_to_j(result)

spを実行して対話で選択肢を出して、もう一度processを呼べば良いかな

dictionary.py
def suggest_and_re_process(word):
    suggests = filter(lambda s: s != '', commands.getoutput('sp %s' % word).split(', '))

    if suggests:
        print ''

        for i, suggest in enumerate(suggests, 1):
            print '%d: %s' % (i, suggest)

        print '\n0: abort\n'

        n = input('enter: ') - 1

        if n != -1:
            process(suggests[n])

    else:
        print '\nno suggests.'

ctrl-Cで止めるとエラーが出てしまうので、except KeyboardInterrupt:も足しておく

-     process(word)
+     try:
+         process(word)
+ 
+     except KeyboardInterrupt:
+         sys.exit(0)

試してみよう

$ python dictionary.py moniter

1: monster
2: monitor
3: moniker

0: abort

enter: 2

mon・i・tor | mɑ́(ː)nətər|mɔ́nɪ- |

動詞
 ~s | -z | ; ~ed | -d | ; ~ing | -t(ə)rɪŋ |

他動詞


1 〈状況など〉を監視する, 監督する, 観察する; …を追跡する; …を測定する; 〖~ that節/wh節〗 …を監視[観察]する
▸  Her blood pressure increased, so she was closely monitored. 血圧が上がり, 彼女は慎重なチェック体制の元におかれた.
# 略
$ python dictionary.py hoge

no suggests.
$ python dictionary.py ほげさん

no results.

満足 🎉

結論

早くなって、和英辞典が使えるようになって、若干読みやすくなって、サジェスト機能がついた!

こんくらいやれば実用品になるかなー

英語や中国語の似た記事はあったけど、日本語の整形対応は見つけられなかったので書きました

正規表現はあんまり得意でもこだわりがあるわけでもないので詰めが甘いけど、満足かな!

実装: 1, 2, 3 対応版

dictionary.py
dictionary.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import re, sys, codecs
from DictionaryServices import DCSCopyTextDefinition


sys.stdout = codecs.getwriter('utf8')(sys.stdout)


def look_up(word):
    return DCSCopyTextDefinition(None, word, (0, len(word)))


def format_for_j_to_e(line):
    line = re.sub(u'([ぁ-んァ-ン一-龥])\s*([a-zA-Z])', r'\1\n\2', line)
    line = re.sub(u'([a-zA-Z.\)!?])\s*([ぁ-んァ-ン一-龥▸])', r'\1\n\n\2', line)
    line = re.sub(u'(名詞|動詞|形容詞|副詞)', r'\1\n', line)

    return '\n' + line


def format_for_e_to_j(line):
    line = re.sub(u'([0-9] )', r'\n\n\1', line)
    line = re.sub(u'(名詞|動詞|形容詞|他動詞|接頭辞|副詞)', r'\n\n\1\n ', line)
    line = re.sub(u'(▸)', r'\n\1 ', line)

    return '\n' + line


def process(word):
    result = look_up(word)

    if re.match('[a-zA-Z]', word) is None:
        print format_for_j_to_e(result)

    else:
        print format_for_e_to_j(result)


if __name__ == '__main__':
    word = sys.argv[1].decode('utf-8')

    process(word)

実装: 1, 2, 3, 4 対応版

dictionary.py
dictionary.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import re, sys, commands, codecs
from DictionaryServices import DCSCopyTextDefinition


sys.stdout = codecs.getwriter('utf8')(sys.stdout)


def look_up(word):
    return DCSCopyTextDefinition(None, word, (0, len(word)))


def format_for_j_to_e(line):
    line = re.sub(u'([ぁ-んァ-ン一-龥])\s*([a-zA-Z])', r'\1\n\2', line)
    line = re.sub(u'([a-zA-Z.\)!?])\s*([ぁ-んァ-ン一-龥▸])', r'\1\n\n\2', line)
    line = re.sub(u'(名詞|動詞|形容詞|副詞)', r'\1\n', line)

    return '\n' + line


def format_for_e_to_j(line):
    line = re.sub(u'([0-9] )', r'\n\n\1', line)
    line = re.sub(u'(名詞|動詞|形容詞|他動詞|接頭辞|副詞)', r'\n\n\1\n ', line)
    line = re.sub(u'(▸)', r'\n\1 ', line)

    return '\n' + line


def suggest_and_re_process(word):
    suggests = filter(lambda s: s != '', commands.getoutput('sp %s' % word).split(', '))

    if suggests:
        print ''

        for i, suggest in enumerate(suggests, 1):
            print '%d: %s' % (i, suggest)

        print '\n0: abort\n'

        n = input('enter: ') - 1

        if n != -1:
            process(suggests[n])

    else:
        print '\nno suggests.'


def process(word):
    result = look_up(word)

    if re.match('[a-zA-Z]', word) is None:
        if result is None:
            print '\nno results.'

        else:
            print format_for_j_to_e(result)

    else:
        if result is None:
            suggest_and_re_process(word)

        else:
            print format_for_e_to_j(result)


if __name__ == '__main__':
    word = sys.argv[1].decode('utf-8')

    try:
        process(word)

    except KeyboardInterrupt:
        sys.exit(0)

Discussion