mac の辞書をターミナルから使う ちょいリッチに
先日こんな記事を書きました
Apple Script で CLI を作るための要点
mac の 辞書.app をターミナルから使いたいという話です
その中のこの部分
実はやりたかったのは 辞書.app の起動ではない
ここについてちょっと切り込みます
シンプルな対処
objective-c 用の辞書 api があり、それを python の PyObjC 経由で使えばやりたいことは解決できるのは知っていた
実は結構簡単です
#!/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 を解決したコードと、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 を最低限にする
これが遅いみたい
>>> 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【引用】(名)スル① 古人の言や他人の文章,また他人の説や事例などを自分の文章の中に引いて説明に用いること。「古典の例を―する」② ポスト-モダンの芸術や建築で作品の中に過去の様式や他人の作品を部分的に組み入れる手法。〈子項目〉引用指数引用符
日本語の説明を望むことはほぼないので、日本語の場合は辞書を和英辞典に固定したい
対処: 引数で辞書を指定する(失敗)
見るからに第一引数が怪しいので、調べてみる
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 射出成形〘金属・プラスチック・セラミックを型に射出して成形する方法〙.
対処: 正規表現で強引になんとかする
色々試しつつ、和英辞典は日本語と英語の境目と品詞の後ろに改行を入れてみた
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
英和辞典は数字の前と品詞の前後と ▸ の前に改行を入れてみる
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. ミススペルするとそっけない
問題
単語がヒットしなかった場合は、DCSCopyTextDefinition
はNone
を返す
>>> 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
を呼べば良いかな
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
#!/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
#!/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