オランダ発!無料で使える Mac 用の電子単語帳を作ったので公開してみる (ただし Python)

Version 2 を公開しました。curses を使って、少し使いやすくなっています。(2011/2/24 追記)
数日前に、はてなブックマークオランダ発!記憶に残る画期的な単語学習法というのが上がっていたので、Mac 用に無料の電子単語帳を作ってみました。英語学習熱が再燃した自分用ですが、誰にでも使えるように公開します。ただし、Terminal.app と Python を使用します。誰でも簡単に使えるわけではないので悪しからず。実際に同様のことを実践している方の記事はこちら

1. 使い方

1.1 好きな folder を作る

あなたの Mac の home (家の icon の folder) に、好きな名前の folder (directory) を作ります。例えば、wordbook という folder を作ります。以降、この場所を ~/wordbook と書きます。

1.2 Python Script を保存する

"3. Python Script" にある script を、コピペして ~/wordbook の中に wordbook2.py という名前で保存します。つまり、~/wordbook/wordbook2.py ができ上がります。もしくは、 wordbook.py から落として下さい。最新版は wordbook2.py です。

1.3 Terminal.app からの操作

Terminal.app (ターミナル) を開き、次の内容を打ちます。

cd ~/wordbook
chmod +x wordbook2.py
1.4 Terminal.app から実行

Terminal.app を開き、次の内容を打ちます。

cd ~/wordbook
./wordbook2.py 1

2 行目の 1 の部分は、1 から 5 の間の好きな数字を入れて下さい。元記事の箱 1 から箱 5 に対応します。初めての起動時には前処理が走り、箱 1 に 30 個の単語が登録されます。他の箱は空っぽですので、最初は 1 から始めて下さい。

初回起動時は次のような出力が表示されるはずです。30 個の単語は random に選ばれるので、必ずしもここの表示とは一致しません。

$ ./wordbook2.py 1
┌──────────────────────────────────────────────────────────────────────────────┐
│ Do you know 'humanitarian'? [Y/N/L/S/Q/B/H/1/2/3/4/5]                        │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Adding 'interest' 'twofold' 'admission' 'amateur' 'screech' 'publication' 's │
│ tipulation' 'pallet' 'idyllic' 'admit' 'heed' 'unruly' 'humanitarian' 'poten │
│ tial' 'obtain' 'clamorous' 'index' 'invigorate' 'table' 'dash' 'grouchy' 'pr │
│ osperous' 'refute' 'deny' 'appearance' 'beckon' 'prolific' 'scanty' 'jerk' ' │
│ inferior' to Box 1                                                           │
│                                                                              │
│                                                                              │
│                                                                              │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

1.5 基本的な使い方

Do you know 'affect'? [Y/N/L/S/Q/P/1/2/3/4/5] 

と質問されたら、知っている場合は Y (Yes)、知らない場合は N (No)、辞書で調べる場合は L (Lookup)、発音を聞く場合は S (Say)、終了する場合は Q (Quit)、箱の中の単語数を表示する場合は B (Box)、操作方法を見る場合は H (Help) を入力します。箱の番号を変更したいときは、1 から 5 を入力して下さい。

L を入力すると、以下のように表示されます。小文字入力で構いません。

┌──────────────────────────────────────────────────────────────────────────────┐
│ Do you know 'screech'? [Y/N/L/S/Q/B/H/1/2/3/4/5]                             │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ 動 詞•自動詞金切り声をたてる, 鋭い声[音]を出す, 悲鳴をあげる⦅out⦆(⇒SCR │
│ EAM[類語]);⦅〜 to a stop[halt]で⦆キーっと音を立てて止まる.             │
│ ━━他動詞…を金切り声で言う⦅out⦆                                             │
│ ━━名 詞                                                                     │
│ 1 荒々しく甲高い音, 金切り声, 悲鳴                                           │
│ 2 けたたましい笑い声.                                                        │
│ [中英語scritch(擬声語)の変異形]                                          │
│ screecher                                                                    │
│ 名 詞                                                                       │
└──────────────────────────────────────────────────────────────────────────────┘

1.6 辞書の変更

日本語環境で OS X を使っている場合、初期設定では英和辞書の設定になっているはずです。これを英英辞書に変更したい場合は、Dictionary.app (辞書.app) を開いて、環境設定で "New Oxford American Dictionary" を優先にします。


▲ 辞書.app の環境設定

2. 仕組み

http://www.kilgarriff.co.uk/bnc-readme.html から、BNC で使われている頻出単語の一覧を自動で取得し、~/wordbook/BNC.txt として保存しています。http://www.drrajus.com/forum/viewtopic.php?p=93&sid=a9f287d8d0ecaade842fcf50db8efeb4から TOEFL の 2000 語をまとめ、また http://toeic.youpla.be/words から TOEIC の 3400 語をまとめて、それぞれここここに置きました。これらから単語を抽出し、box0 にそれぞれの単語と同一名の file を作成しています。この単語 file を、box1 から box5 に移動することで、オランダ発 と同じ動作をさせています。

単語数は 5,000 個くらいしかないので、少し物足りないと思います。その場合は、どこか他から同様の単語一覧を取得することで単語数は増やせます。例えば z で始まる単語は "zone" しか含まれていません。"Zombie" や "zoo" や "zebra" や "zip" など、いくらでも思いつくので、多分 3 万語くらいは必要なんだと思います。よい一覧表があれば、どなたか教えて下さい。BNC のものから TOEFLTOEIC 用のものに変更したので、初学者には難しいかもしれませんが、自分には丁度よいです。"you" とか "a" のような基本的過ぎるものは含まれていません。

辞書の表示と発音は、全て OS X に含まれる機能を使っています。発音時に辞書から発音記号のみを抽出して表示したかったのですが、方法が分かりませんでした。

Python 2.5 をわざと指定しているのは、DictionaryServices module が 2.6 で正しく動作しないためです。基本的な操作は辞書(Dictionary).appを使い倒そうを参考にしました。

3. Python Script

コピペして使って下さい。

#!/usr/bin/python2.5

import curses
import locale
import glob
import os
import random
import shutil
import sys
import urllib
from DictionaryServices import *

class Wordbook(object):
    def __init__(self, box_number):
        locale.setlocale(locale.LC_ALL, '')
        
        for i in range(1, 6):
            if not os.path.exists('box%d' % i):
                os.mkdir('box%d' % i)

        if box_number < 1:
            self.box_number = 1
        elif box_number > 5:
            self.box_number = 5
        else:
            self.box_number = box_number

        self.init_windows()
        self.init_database()
        self.print_msg("Welcome to Python Wordbook!")

    def init_database(self):
        if not os.path.exists('box0'):
            os.mkdir('box0')

        URL_BASE = 'http://dl.dropbox.com/u/16653989/wordbook/'
        TOEFL = 'TOEFL2000.txt'
        TOEIC = 'TOEIC3420.txt'

        if not os.path.exists(TOEFL):
            url = URL_BASE + TOEFL
            self.print_msg('Downloading %s' % url)
            urllib.urlretrieve(url, TOEFL)

        data = open(TOEFL)

        for line in data.readlines():
            word = line.split(". ")[1].split()[0].lower().split("/")[0]
            f = open('box0/%s' % word, 'w')
            f.close()

        if not os.path.exists(TOEIC):
            url = URL_BASE + TOEIC
            self.print_msg('Downloading %s' % url)
            urllib.urlretrieve(url, TOEIC)

        data = open(TOEIC)

        for line in data.readlines():
            word = line.split(" (")[0]
            if word == 'N/A':
                continue
            f = open('box0/%s' % word, 'w')
            f.close()

    def choose_random_word(self):
        if self.box_number == 1:
           nwords = len(glob.glob('box1/*'))
           if nwords <= 3:
               msg = 'Adding'
               for i in range(30):
                   words_in_box0 = glob.glob('box0/*')
                   if len(words_in_box0) == 0:
                       break
                   src = random.choice(words_in_box0)
                   dst = src.replace('box0', 'box1')
                   msg += " '%s'" % src.replace('box0/', '')
                   shutil.move(src, dst)
               msg += ' to Box 1'
               self.print_msg(msg)

        words = glob.glob('box%d/*' % self.box_number)
        if len(words) == 0:
            return None
        
        rand = random.randrange(len(words))
        
        return words[rand].replace('box%d/' % self.box_number, '')

    def lookup(self, word):
        result = DCSCopyTextDefinition(None, word, (0, len(word)))
        self.print_msg(result.encode('utf-8'))

    def know(self, word):
        if self.box_number != 5:
            src = 'box%d/%s' % (self.box_number, word)
            dst = 'box%d/%s' % (self.box_number + 1, word)
            shutil.move(src, dst)
            self.print_msg("'%s' has been moved from Box %d to Box %d." % (word, self.box_number, self.box_number + 1))

    def dont_know(self, word):
        if self.box_number != 5:
            src = 'box%d/%s' % (self.box_number, word)
            dst = 'box1/%s' % word
            shutil.move(src, dst)
            self.print_msg("'%s' has been moved from Box %d to Box 1." % (word, self.box_number))

    def init_windows(self):
        self.win = curses.initscr()
        self.win.refresh()
        y, x = self.win.getmaxyx()
        self.ask_window = self.win.subwin(3, x, 0, 0)
        self.ask_window.box()

        self.msg_frame = self.win.subwin(y - 3, x, 3, 0)
        self.msg_frame.box()
        self.msg_frame.scrollok(True)
        self.msg_frame.refresh()

        self.msg_window = self.win.subwin(y - 5, x - 4, 4, 2)
        self.msg_window.scrollok(True)
        
    def print_ask(self, word):
        self.ask_window.erase()
        self.ask_window.box()
        self.ask_window.move(1, 2)
        self.ask_window.addstr("Do you know '")
        self.ask_window.addstr(word, curses.A_BOLD)
        self.ask_window.addstr("'? [Y/N/L/S/Q/B/H/1/2/3/4/5] ")
        self.ask_window.refresh()
        curses.echo()
        c = self.ask_window.getch()
        curses.noecho()
        return chr(c)

    def print_msg(self, message):
        self.msg_window.erase()
        self.msg_window.move(0, 0)
        self.msg_window.addstr(message)
        self.msg_window.refresh()

    def print_help(self):
        self.print_msg("""Y (Yes): You know the word. The word will be moved to the next box.
N (No): You don\'t know the word. The word will be moved to Box 1.
L (Lookup): Look up the word with the Dictionary.
S (Say): Listen the pronunciation of the word with \'say\' command.
B (Box): Print the number of words in each box.
H (Help): Print this message.
Q (Quit): Quit the Wordbook.
1-5: Move to another box.""")

    def main(self):
        main_loop_flag = True
        while main_loop_flag:
            word = self.choose_random_word()
            if not word:
                self.print_msg('Box %d is empty\nMoving back to Box 1' % self.box_number)
                self.box_number = 1
                continue
            word_loop_flag = True
            while word_loop_flag:
                c = self.print_ask(word)
                c = c.upper()
                if c == 'Y':
                    self.know(word)
                    word_loop_flag = False
                elif c == 'N':
                    self.dont_know(word)
                    word_loop_flag = False
                elif c == 'L':
                    self.lookup(word)
                elif c == 'S':
                    self.say(word)
                elif c == 'B':
                    self.print_nwords()
                elif c == 'H':
                    self.print_help()
                elif c == 'Q':
                    word_loop_flag = 0
                    main_loop_flag = 0
                elif c == '1' or c == '2' or c == '3' or c == '4' or c == '5':
                    self.box_number = int(c)
                    word_loop_flag = False

        curses.endwin()
        print 'Bye...'

    def print_nwords(self):
        msg = ''
        for i in range(6):
            nwords = len(glob.glob('box%d/*' % i))
            msg += 'Box %d: %4d words\n' % (i, nwords)
        self.print_msg(msg)

    def say(self, word):
        os.system('say %s' % word)

if __name__ == '__main__':
    wb = Wordbook(int(sys.argv[1]))
    wb.main()