KindleからAnkiノートを一括作成する


SQL, javascript/typescript, HTML, css を使用して、Kindle で調べた英単語のリストから Anki ノートを一括作成する方法を紹介します。

執筆時に使用した Kindle は Kindle Paperwhite (第 11 世代)、ファームウェアバージョン 5.14.3.2 です。

まず公式サイトのマニュアルを一読することをおすすめします。これも Immersion の一環です。

カードとノートの違い

Anki のノートとカードの概念がややこしいのでまとめました。

  • ノートはフィールドとカード(s)を定義する
  • カードはあるノートの表示方法をHTML, cssなどによって定義する

Kindle からノートを作る

Kindle の Vocabulary Builder からノートを作成します。Kindle で本を読んでいるときに単語を検索すると Vocabulary Builder にその単語が保存されていきます。Kindle 内でフラッシュカードを作成する機能がありますが、Anki へインポートしたほうが効率的です。Vocabulary Builder が有効になっているか以下のページで確認できます。

Vocabulary Builder

Vocabulary Builder の内容は例えば Mac の場合/Volumes/Kindle/system/vocabulary/vocab.dbに保存されているようです。
以下のコマンドでローカルにコピーします。

zsh
cp /Volumes/Kindle/system/vocabulary/vocab.db vocab.db

以下のクエリで英語の本のタイトルとその本の著者のリストが取得できます。

sql
SELECT title, authors FROM BOOK_INFO WHERE BOOK_INFO.lang = 'en'

以下のクエリで指定した本から、語幹、単語、その単語が使われている英文、タイムスタンプを取得できます。

sql
SELECT WORDS.stem, WORDS.word, LOOKUPS.usage, LOOKUPS.timestamp
FROM WORDS
JOIN LOOKUPS ON WORDS.id = LOOKUPS.word_key
JOIN BOOK_INFO ON BOOK_INFO.id = LOOKUPS.book_key
WHERE BOOK_INFO.title = :title

Anki からノートを定義します。名前は例えばkindle、フィールドは以下のように定義しました。

fields for kindle

定義したフィールドに合わせてタイプを定義します。Video, Audioフィールドは必要ないかもしれませんが、例えば難しい単語は調べた動画や音声を後で手動でノートに書き込むことがあるので、ここでは定義しました。

src/types.ts
typescript
export type Note = {
Expression: string;
IPA: string;
Image: string;
Video: string;
Audio: string;
Sentence: string;
Definition: string;
DefinitionJP: string;
Difficulty: string;
Origin: string;
Title: string;
Authors: string;
Date: string;
};

先程用意したリストから csv ファイルを出力します。以下の例では Deno を使用しています。makeNote()はジェネレーターで各本のメタデータからノートをyieldしています。Anki は HTML によってカードを表示するので、web サイトからinnerHTMLなどのプロパティでそのまま HTML コードを取得すると後で便利です。

src/main.ts
typescript
import { writeCSVObjects } from 'https://deno.land/x/csv@v0.7.5/mod.ts';
import { DB } from 'https://deno.land/x/sqlite@v3.5.0/mod.ts';
const db = new DB('vocab.db');
let title = '';
let authors = '';
// console.log(db.query('SELECT title, authors FROM BOOK_INFO WHERE BOOK_INFO.lang = 'en''));
// ... get title and authors ...
const SQL_LOOKUPS = `
SELECT WORDS.stem, WORDS.word, LOOKUPS.usage, LOOKUPS.timestamp
FROM WORDS
JOIN LOOKUPS ON WORDS.id = LOOKUPS.word_key
JOIN BOOK_INFO ON BOOK_INFO.id = LOOKUPS.book_key
WHERE BOOK_INFO.title = :title
`;
const metadata_list = db.queryEntries<{
stem: string;
word: string;
usage: string;
timestamp: number;
}>(SQL_LOOKUPS, {
title,
});
const f = await Deno.open('./notes.csv', { write: true, create: true, truncate: true });
const header = [
'Expression',
'IPA',
'Image',
'Video',
'Audio',
'Sentence',
'Definition',
'DefinitionJP',
'Difficulty',
'Origin',
'Title',
'Authors',
'Date',
];
await writeCSVObjects(f, makeNote(), { header });
f.close();
Deno.exit();
async function* makeNote() {
for (const md of metadata_list) {
// ... yield note ...
}
}

以下のコマンドで実行します。

zsh
deno run src/main.ts

Anki から csv ファイルをインポートすると以下の画面が表示されます。作成したノートとデッキを選択します。フィールドと各項目を合わせてインポートします。一括作成する場合、同じ単語のカードが既に存在していることがよくあります。その時にインポートしないこともここで設定できます。今回は重複があってもインポートする設定にしています。フィールドにHTMLを使用している場合、Allow HTML in fieldsをチェックします。

import csv

あとはカードのテンプレートを作成して完成です。

html
<!-- Front Template -->
<div id="front_body" style="opacity: 0">
<div class="front">
<div class="header">
<div class="date">{{Date}}</div>
<div class="title">{{Title}} - {{Authors}}</div>
</div>
<div class="sentence">{{edit:Sentence}}</div>
<div id="audio">{{Audio}}</div>
</div>
<hr class="separate" />
</div>
<script>
// ...
</script>
html
<!-- Back Template -->
<link href="_style.css" rel="stylesheet" />
<link href="_weblio.css" rel="stylesheet" />
<link href="_cambridge.css" rel="stylesheet" />
<!-- ... -->
<div class="front">
<div class="header">
<div class="date">{{Date}}</div>
<div class="title">{{Title}} - {{Authors}}</div>
</div>
<div class="sentence">{{edit:Sentence}}</div>
<div id="audio">{{Audio}}</div>
</div>
<hr class="separate" />
<div class="panels">
<div class="half">
<div class="photos" id="image">{{Image}}</div>
</div>
<div class="half">
<div id="definition">{{Definition}}</div>
<div id="example">{{Example}}</div>
<div id="URL"><a href="{{URL}}">{{URL}}</a></div>
<!-- ... -->
</div>
</div>
<!-- ... -->
<script>
// ...
</script>

cssは直接カードに定義するか、linkタグでHTMLから読み込み出来ます。別ファイルから読み込む場合、名前の先頭に_を付ける必要があります。1

css
/* _style.css */
.front {
font-size: 2rem !important;
text-align: center;
}
#definition,
#example,
#URL {
margin: 0.5rem !important;
border: 1px solid;
border-color: var(--border-color);
border-radius: 5px;
padding: 1rem;
}
ul {
padding-left: 1rem;
}
strong {
text-decoration: underline;
}
/* ... */

デモ

寄付のお願い

このブログやプロジェクトを継続するために寄付を頂ければ幸いです。


BTC: 1L5r7dDeJfq56UvPA96WCvQSDCfSUmwjT4
1L5r7dDeJfq56UvPA96WCvQSDCfSUmwjT4

XRP: rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh
Destination Tag: 106945877
rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh 106945877