LinguaLeo辞書をAnkiフラッシュカードにインポートして変換する

問題文



英語を学ぶ人はおそらく、アンキに精通しているでしょう。これは、インターバルの繰り返しを使用して単語、表現、その他の情報を記憶するプログラムです。



紹介を必要としないもう1つの人気のあるサービス-LinguaLeoでは、元のテキストを読みながら、読むためになじみのない単語をすぐに送信し、発音、画像、単語の転写、およびそれが使用されるコンテキストとともに独自の辞書に保存できます。 数年前、LinguaLeoは間隔反復システムを導入しましたが、Ankiとは異なり、反復システムはそれほど強力ではなく、カスタマイズ機能を備えていません。



ハリネズミ越えて2つのプラットフォームを利用しようとしたらどうなるでしょうか? すべてのメディアファイルと情報と共にLingua Leoから単語を取り出し、Ankiのリソースを使用してそれらを記憶します。



既製のソリューションの概要



タスクにはいくつかの既製のソリューションが既にありますが、それらはすべてユーザーフレンドリーではなく、多くの追加アクションが必要です:記録モデル、カードテンプレートの作成、CSSスタイルの追加、メディアファイルの個別アップロードなどが必要です。 など



さらに、これらはすべて別個のプログラム( LinguaGet )として、またはブラウザーへのアドオン( onetwo )として作成されますが、これもあまり便利ではありません。 理想的な状況では、ユーザーがAnki自体から直接新しいカードを取得できるようにしたかった。



楽器の選択



Anki 2.0はPython 2.7で書かれており、アドオンのシステムをサポートしています。これらはPythonの個別のモジュールです。 安定版GUIはPyQt 4.8を使用します。



したがって、タスクは1つの文で説明できます。PythonでAnkiから直接起動し、LinguaLeoからログインとパスワードを入力する以外のユーザーのアクションを必要としないアドオンを作成します。



解決策



プロセス全体は3つの部分に分けることができます。



  1. LinguaLeoからの承認と辞書の取得。
  2. データをAnkiカードに変換します。
  3. グラフィックインターフェイスの作成。


データのインポート



LinguaLeoには公式のAPIは含まれていませんが、ブラウザーのアドオンコードを詳しく調べると、2つの必要なアドレスを見つけることができます。



userDictUrl = "http://lingualeo.com/userdict/json"

loginURL = "http://api.lingualeo.com/api/login"








承認手順は複雑ではなく、Habrに関するこの記事で詳しく説明されています



JSON辞書には、各ページに100語が含まれています。 urlencodeを使用して、フィルターパラメーターをすべてに渡し、ページパラメーターで必要なページの番号を渡し、それぞれを調べて辞書を保存します。



辞書認証とインポートモジュールコード
 import json import urllib import urllib2 from cookielib import CookieJar class Lingualeo: def __init__(self, email, password): self.email = email self.password = password self.cj = CookieJar() def auth(self): url = "http://api.lingualeo.com/api/login" values = {"email": self.email, "password": self.password} return self.get_content(url, values) def get_page(self, page_number): url = 'http://lingualeo.com/ru/userdict/json' values = {'filter': 'all', 'page': page_number} return self.get_content(url, values)['userdict3'] def get_content(self, url, values): data = urllib.urlencode(values) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj)) req = opener.open(url, data) return json.loads(req.read()) def get_all_words(self): """ The JSON consists of list "userdict3" on each page Inside of each userdict there is a list of periods with names such as "October 2015". And inside of them lay our words. Returns: type == list of dictionaries """ words = [] have_periods = True page_number = 1 while have_periods: periods = self.get_page(page_number) if len(periods) > 0: for period in periods: words += period['words'] else: have_periods = False page_number += 1 return words
      
      







受信したデータをAnkiカードに変換します



Ankiのデータは次のように保存されます。 コレクションには、CSSスタイル、 フィールドのリスト、およびカード(前面と背面) 設計するためのhtmlテンプレートを含むモデルが含まれます



この場合、5つのフィールドがあります:単語自体、翻訳、文字起こし、発音ファイルへのリンク、画像ファイルへのリンク。



すべてのフィールドに入力すると、 レコードが取得されます。 次に、レコードから2 枚のカードを作成できます。「英語-ロシア語」と「ロシア語-英語」です。



フラッシュカード



カードはデッキにありますコシュチェイの卵の死とeggの卵 )。



上記に加えて、音声と画像をダウンロードする機能が必要です。



モデル、レコードなどを作成するためのユーティリティを備えたモジュールコード
 import os from random import randint from urllib2 import urlopen from aqt import mw from anki import notes from lingualeo import styles fields = ['en', 'transcription', 'ru', 'picture_name', 'sound_name', 'context'] def create_templates(collection): template_eng = collection.models.newTemplate('en -> ru') template_eng['qfmt'] = styles.en_question template_eng['afmt'] = styles.en_answer template_ru = collection.models.newTemplate('ru -> en') template_ru['qfmt'] = styles.ru_question template_ru['afmt'] = styles.ru_answer return (template_eng, template_ru) def create_new_model(collection, fields, model_css): model = collection.models.new("LinguaLeo_model") model['tags'].append("LinguaLeo") model['css'] = model_css for field in fields: collection.models.addField(model, collection.models.newField(field)) template_eng, template_ru = create_templates(collection) collection.models.addTemplate(model, template_eng) collection.models.addTemplate(model, template_ru) model['id'] = randint(100000, 1000000) # Essential for upgrade detection collection.models.update(model) return model def is_model_exist(collection, fields): name_exist = 'LinguaLeo_model' in collection.models.allNames() if name_exist: fields_ok = collection.models.fieldNames(collection.models.byName( 'LinguaLeo_model')) == fields else: fields_ok = False return (name_exist and fields_ok) def prepare_model(collection, fields, model_css): """ Returns a model for our future notes. Creates a deck to keep them. """ if is_model_exist(collection, fields): model = collection.models.byName('LinguaLeo_model') else: model = create_new_model(collection, fields, model_css) # Create a deck "LinguaLeo" and write id to deck_id model['did'] = collection.decks.id('LinguaLeo') collection.models.setCurrent(model) collection.models.save(model) return model def download_media_file(url): destination_folder = mw.col.media.dir() name = url.split('/')[-1] abs_path = os.path.join(destination_folder, name) resp = urlopen(url) media_file = resp.read() binfile = open(abs_path, "wb") binfile.write(media_file) binfile.close() def send_to_download(word): picture_url = word.get('picture_url') if picture_url: picture_url = 'http:' + picture_url download_media_file(picture_url) sound_url = word.get('sound_url') if sound_url: download_media_file(sound_url) def fill_note(word, note): note['en'] = word['word_value'] note['ru'] = word['user_translates'][0]['translate_value'] if word.get('transcription'): note['transcription'] = '[' + word.get('transcription') + ']' if word.get('context'): note['context'] = word.get('context') picture_url = word.get('picture_url') if picture_url: picture_name = picture_url.split('/')[-1] note['picture_name'] = '<img src="%s" />' % picture_name sound_url = word.get('sound_url') if sound_url: sound_name = sound_url.split('/')[-1] note['sound_name'] = '[sound:%s]' % sound_name return note def add_word(word, model): collection = mw.col note = notes.Note(collection, model) note = fill_note(word, note) collection.addNote(note)
      
      







GUI作成



アドオンはミニマリズムを目指しているため、必要なものは次のとおりです。





画像



GUI
 class PluginWindow(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.initUI() def initUI(self): self.setWindowTitle('Import From LinguaLeo') # Window Icon if platform.system() == 'Windows': path = os.path.join(os.path.dirname(__file__), 'favicon.ico') loc = locale.getdefaultlocale()[1] path = unicode(path, loc) self.setWindowIcon(QIcon(path)) # Buttons and fields self.importButton = QPushButton("Import", self) self.cancelButton = QPushButton("Cancel", self) self.importButton.clicked.connect(self.importButtonClicked) self.cancelButton.clicked.connect(self.cancelButtonClicked) loginLabel = QLabel('Your LinguaLeo Login:') self.loginField = QLineEdit() passLabel = QLabel('Your LinguaLeo Password:') self.passField = QLineEdit() self.passField.setEchoMode(QLineEdit.Password) self.progressLabel = QLabel('Downloading Progress:') self.progressBar = QProgressBar() self.checkBox = QCheckBox() self.checkBoxLabel = QLabel('Unstudied only?') # Main layout - vertical box vbox = QVBoxLayout() # Form layout fbox = QFormLayout() fbox.setMargin(10) fbox.addRow(loginLabel, self.loginField) fbox.addRow(passLabel, self.passField) fbox.addRow(self.progressLabel, self.progressBar) fbox.addRow(self.checkBoxLabel, self.checkBox) self.progressLabel.hide() self.progressBar.hide() # Horizontal layout for buttons hbox = QHBoxLayout() hbox.setMargin(10) hbox.addStretch() hbox.addWidget(self.importButton) hbox.addWidget(self.cancelButton) hbox.addStretch() # Add form layout, then stretch and then buttons in main layout vbox.addLayout(fbox) vbox.addStretch(2) vbox.addLayout(hbox) # Set main layout self.setLayout(vbox) # Set focus for typing from the keyboard # You have to do it after creating all widgets self.loginField.setFocus() self.show()
      
      







アドベンチャーは、示されているものに加えて、ダウンロードプロセス中にアドオンが死んだとユーザーが思わないようにプログレスバーを作成するときに開始します。



GUIをフリーズせずにバーの進行状況を機能させるには、(QThreadを使用して)別のスレッドを作成し、すべてのデータをロードしてカードを作成し、進行状況カウンターのみをグラフィカルインターフェイスに送信する必要があります。 ここで、トラブルが待っています-Ankiの情報はSQliteデータベースに保存され、プログラムはメインスレッドの外部からの変更を許可しません。 解決策:主な費用のかかるタスクは、メディアファイルをダウンロードし、2番目のストリームのプログレスバーにデータを転送することです。一方、主なフィールドに入力してレコードを保存します。 したがって、凍結されたインターフェイスのない作業プログレスバーが表示されます。



すべてのGUIモジュールコード
 # -*- coding: utf-8 -*- import locale import os import platform import socket import urllib2 from anki import notes from aqt import mw from aqt.utils import showInfo from PyQt4.QtGui import (QDialog, QIcon, QPushButton, QHBoxLayout, QVBoxLayout, QLineEdit, QFormLayout, QLabel, QProgressBar, QCheckBox) from PyQt4.QtCore import QThread, SIGNAL from lingualeo import connect from lingualeo import utils from lingualeo import styles class PluginWindow(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.initUI() def initUI(self): self.setWindowTitle('Import From LinguaLeo') # Window Icon if platform.system() == 'Windows': path = os.path.join(os.path.dirname(__file__), 'favicon.ico') loc = locale.getdefaultlocale()[1] path = unicode(path, loc) self.setWindowIcon(QIcon(path)) # Buttons and fields self.importButton = QPushButton("Import", self) self.cancelButton = QPushButton("Cancel", self) self.importButton.clicked.connect(self.importButtonClicked) self.cancelButton.clicked.connect(self.cancelButtonClicked) loginLabel = QLabel('Your LinguaLeo Login:') self.loginField = QLineEdit() passLabel = QLabel('Your LinguaLeo Password:') self.passField = QLineEdit() self.passField.setEchoMode(QLineEdit.Password) self.progressLabel = QLabel('Downloading Progress:') self.progressBar = QProgressBar() self.checkBox = QCheckBox() self.checkBoxLabel = QLabel('Unstudied only?') # Main layout - vertical box vbox = QVBoxLayout() # Form layout fbox = QFormLayout() fbox.setMargin(10) fbox.addRow(loginLabel, self.loginField) fbox.addRow(passLabel, self.passField) fbox.addRow(self.progressLabel, self.progressBar) fbox.addRow(self.checkBoxLabel, self.checkBox) self.progressLabel.hide() self.progressBar.hide() # Horizontal layout for buttons hbox = QHBoxLayout() hbox.setMargin(10) hbox.addStretch() hbox.addWidget(self.importButton) hbox.addWidget(self.cancelButton) hbox.addStretch() # Add form layout, then stretch and then buttons in main layout vbox.addLayout(fbox) vbox.addStretch(2) vbox.addLayout(hbox) # Set main layout self.setLayout(vbox) # Set focus for typing from the keyboard # You have to do it after creating all widgets self.loginField.setFocus() self.show() def importButtonClicked(self): login = self.loginField.text() password = self.passField.text() unstudied = self.checkBox.checkState() self.importButton.setEnabled(False) self.checkBox.setEnabled(False) self.progressLabel.show() self.progressBar.show() self.progressBar.setValue(0) self.threadclass = Download(login, password, unstudied) self.threadclass.start() self.connect(self.threadclass, SIGNAL('Length'), self.progressBar.setMaximum) self.setModel() self.connect(self.threadclass, SIGNAL('Word'), self.addWord) self.connect(self.threadclass, SIGNAL('Counter'), self.progressBar.setValue) self.connect(self.threadclass, SIGNAL('FinalCounter'), self.setFinalCount) self.connect(self.threadclass, SIGNAL('Error'), self.setErrorMessage) self.threadclass.finished.connect(self.downloadFinished) def setModel(self): self.model = utils.prepare_model(mw.col, utils.fields, styles.model_css) def addWord(self, word): """ Note is an SQLite object in Anki so you need to fill it out inside the main thread """ utils.add_word(word, self.model) def cancelButtonClicked(self): if hasattr(self, 'threadclass') and not self.threadclass.isFinished(): self.threadclass.terminate() mw.reset() self.close() def setFinalCount(self, counter): self.wordsFinalCount = counter def setErrorMessage(self, msg): self.errorMessage = msg def downloadFinished(self): if hasattr(self, 'wordsFinalCount'): showInfo("You have %d new words" % self.wordsFinalCount) if hasattr(self, 'errorMessage'): showInfo(self.errorMessage) mw.reset() self.close() class Download(QThread): def __init__(self, login, password, unstudied, parent=None): QThread.__init__(self, parent) self.login = login self.password = password self.unstudied = unstudied def run(self): words = self.get_words_to_add() if words: self.emit(SIGNAL('Length'), len(words)) self.add_separately(words) def get_words_to_add(self): leo = connect.Lingualeo(self.login, self.password) try: status = leo.auth() words = leo.get_all_words() except urllib2.URLError: self.msg = "Can't download words. Check your internet connection." except ValueError: try: self.msg = status['error_msg'] except: self.msg = "There's been an unexpected error. Sorry about that!" if hasattr(self, 'msg'): self.emit(SIGNAL('Error'), self.msg) return None if self.unstudied: words = [word for word in words if word.get('progress_percent') < 100] return words def add_separately(self, words): """ Divides downloading and filling note to different threads because you cannot create SQLite objects outside the main thread in Anki. Also you cannot download files in the main thread because it will freeze GUI """ counter = 0 problem_words = [] for word in words: self.emit(SIGNAL('Word'), word) try: utils.send_to_download(word) except (urllib2.URLError, socket.error): problem_words.append(word.get('word_value')) counter += 1 self.emit(SIGNAL('Counter'), counter) self.emit(SIGNAL('FinalCounter'), counter) if problem_words: self.problem_words_msg(problem_words) def problem_words_msg(self, problem_words): error_msg = ("We weren't able to download media for these " "words because of broken links in LinguaLeo " "or problems with an internet connection: ") for problem_word in problem_words[:-1]: error_msg += problem_word + ', ' error_msg += problem_words[-1] + '.' self.emit(SIGNAL('Error'), error_msg)
      
      







エラー処理を追加するだけです。 アドオンの準備が整いました。



まだベータ版の新しいAnkaのプラグインを3番目のpythonに書き換え、メディアファイルの非同期読み込みを使用して作業を高速化する計画です。



BitBucketソースコード。

Ankiアドオンフォーラムのプラグインページ。



All Articles