英単語リストからAnkiノートを一括作成する


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

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

カードとノートの違い

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

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

単語リストからノートを作る

txt形式の単語リストからcsv形式のノートリストを作ります。

まず単語リストを用意します。

txt形式などで保存しておきます。\nを区切り文字としました。

words.txt
text
test
hello

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

fields for anki_words

定義したフィールドに合わせてタイプを定義します。

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

用意した単語リストから csv ファイルを出力します。以下の例では Deno を使用しています。Anki は HTML によってカードを表示するので、innerHTMLなどのプロパティでそのまま HTML コードを取得すると後で便利です。以下のコードでは辞書ソースに Cambridge DictionaryWeblio 英和辞典 を使用しました。画像ソースは Google 画像検索を使用しました。今回は省略しますが、例えばVoiceTubeYouGlishでその単語が使われている動画や英文を検索してノートを作成しても良いと思います。

src/main.ts
typescript
import { writeCSVObjects } from 'https://deno.land/x/csv@v0.7.5/mod.ts';
import { Cambridge } from './Cambridge.ts';
import { GoogleImages } from './GoogleImages.ts';
import { Note } from './types.ts';
import { Weblio } from './Weblio.ts';
const word_list = Deno.readTextFileSync('./words.txt').split('\n').filter(Boolean);
const cambridge = new Cambridge();
const weblio = new Weblio();
const google = new GoogleImages();
const notes: Note[] = [];
for (const query of word_list) {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
const entries = await cambridge.find(query);
const image = await google.find(query);
const jp = await weblio.find(query);
const { expression, difficulty, origin, reading } = entries[0];
const note: Note = {
Expression: expression,
IPA: reading,
Image: image ?? '',
Video: '',
Audio: '',
Definition: entries.map((note) => note.definitions.join('')).join(''),
DefinitionJP: jp ?? '',
Difficulty: difficulty,
Title: '',
URL: '',
Origin: origin,
Date: new Date().toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'long',
hourCycle: 'h23',
}),
};
notes.push(note);
} catch (err) {
console.log(err);
}
}
const f = await Deno.open('./notes.csv', { write: true, create: true, truncate: true });
const header = Object.keys(notes[0]);
await writeCSVObjects(f, notes, { header });
f.close();
Deno.exit();

細かい実装は省略しますが、例えば以下のコードで Google Images 検索の結果から画像をHTML形式で取得することが出来ます。

src/GoogleImages.ts
typescript
import { JSDOM } from 'https://jspm.dev/jsdom';
export class GoogleImages {
max_example: number;
img_height: number;
constructor() {
this.max_example = 4;
this.img_height = 150;
}
element(path: string) {
return `<img src="${path}" decoding="async" style="height: ${this.img_height}px;">`;
}
async find(word: string) {
const query = encodeURIComponent(word);
const url = `https://www.google.com/search?tbm=isch&q="${query}"&safe=active&gl=us&pws=0`;
const text = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36',
},
}).then((res) => res.text());
const document: Document = new JSDOM(text).window.document;
const images = document.querySelectorAll<HTMLImageElement>('.rg_i');
return [...images]
.map((image) => image.getAttribute('data-src') ?? '')
.filter(Boolean)
.slice(0, this.max_example)
.map((src) => this.element(src))
.join('');
}
}

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

zsh
deno run src/main.ts

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

import csv

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

html
<!-- Front Template -->
<div id="expression">{{Expression}}</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 id="back_body" style="opacity: 0">
<div class="front">
<div class="header">
<div class="date">{{Date}}</div>
<div class="title"><a href="{{URL}}">{{Title}}</a></div>
</div>
<div id="expression">{{Expression}}</div>
<div id="audio">{{Audio}}</div>
</div>
<hr class="separate" />
<div class="panels">
<div class="half">
{{#Video}}
<video src="{{Video}}" controls autoplay></video>
{{/Video}}
<div class="photos" id="image">{{Image}}</div>
</div>
<div class="half" id="word_meaning">
<div id="head">
<div class="">
<span class="" id="expression">{{Expression}}</span>
<span class="" id="extrainfo">{{Difficulty}}</span>
</div>
<div class="head__item">
<div id="ipa">{{IPA}}</div>
<div class="tab">
<div class="button tab_links" id="tab_eng" onclick="open_tab(event, 'Eng')">ENG</div>
<div class="button tab_links" id="tab_jpn" onclick="open_tab(event, 'Jpn')">JPN</div>
</div>
</div>
</div>
<div id="meaning">
<div id="Eng" class="tab_content" style="display: none">{{edit:Definition}}</div>
<div id="Jpn" class="tab_content" style="display: none">{{edit:DefinitionJP}}</div>
<div id="origin-wrapper">{{edit:Origin}}</div>
</div>
<div id="buttons">
<!-- ... -->
</div>
</div>
</div>
</div>
<script>
// ...
</script>

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

css
/* _style.css */
#expression {
font-size: 2rem;
margin: 5px;
padding: 5px;
font-family: var(--serif);
display: inline-block;
}
/* ... */

デモ

寄付のお願い

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


BTC: 1L5r7dDeJfq56UvPA96WCvQSDCfSUmwjT4
1L5r7dDeJfq56UvPA96WCvQSDCfSUmwjT4

XRP: rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh
Destination Tag: 106945877
rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh 106945877