英単語本をパースしてAnkiに一括登録

   · ☕ 5 min read

anki.jpg

英単語の学習のため、Anki を使用している。今回はTOEFL テスト英単語 3800の登録を行った。その過程をまとめる。



Anki のインポート機能

Anki という暗記学習に特化したアプリが存在する。詳細は割愛するが、とても便利。
このアプリへのデータ登録は、基本的には手入力で行う。しかし Anki にはインポート機能がデフォルトで存在し、特定のファイルから一括でデータを読み込むことができる。この機能を使用して、電子書籍のデータを流し込んでいく。

はじめに Anki のインポート機能を確認する。Anki 日本語マニュアル Wikiによると、以下の形式のファイルを通して、データの読込が可能なようだ。

  • txt
  • html
  • 表計算シート
  • メディア(イメージ)

最後のメディアは、暗記カード内に画像を埋め込む際に使用する。今回はテキストデータのみのため使用しない。また形式は、最も手間の少なそうな txt 形式を用いる。

txt 形式の読み込みには、いくつかの注意点がある。
大きくは以下の 3 点。

  • プレーンテキスト ファイル
  • コンマ・セミコロン・タブ のいずれかによってフィールドが区切る
  • 文字コードは UTF-8

注意すべきは、下記 1 点。

  • ファイルの最初の行を元にフィールドの個数が判断される
    • このフィールドの個数に合和ない行に関しては、登録されない

フィールドの書き出しを誤ると、正しくデータがインポートされないようだ。そのため、読み込み元の電子書籍をよく確認し、データ構造を定義する必要がある。

読み込む本

データの定義

今回読み込む本はTOEFL テスト英単語 3800

この単語本には 3800 の英単語が収録されており、それぞれの単語は下記の判例に則り記載されている。

引用: 当該書籍より「単語リストの掲載内容」

この仕様に則り、それぞれの単語を以下のような形で、仮想の単語カードにまとめるものとする。

  • カード_おもて
    • インデックス(番号)
    • 英単語
    • 発音記号
    • 例文
  • カード_うら
    • 英単語_日本語訳
    • 例文_日本語訳

注意点; パターンの把握が困難

単語には、下記のようないくつかのパターンが存在した。

  • 発音の記載がない単語
  • 例文の記載がない単語
  • 複数の例文が記載された単語
  • 複数の日本語訳が記載された単語

これらを網羅した上で実装をする必要がある。
このパターンの把握が最も時間がかかるものであった。

パターン網羅の複雑さと、工数の削減を考慮した結果、最も効率の良い方法は実装と同時に適宜確認を行い、都度、テストケースを修正する方法と仮定し、実装を進めた。

実装の流れ

テストケースを丁寧に

実装においては、テストケースの用意を丁寧に行うよう意識した。
以下の流れで行った。

    1. テストケースを作成
    1. テストを満たすように実装
    1. 1 と 2 を繰り返し
    1. 実装が進んだら実データを投入
    1. 調整、テストの繰り返し。目視で異常確認し修正

実データの投入後には、少しだけ新たなパターンが見つかった(英単語ではなく英熟語であったり)。しかし概ねすんなりと実装できた。

Anki登録結果画面。全3800英単語を登録できた。図の上部に`1024枚`とあるが、これは複数のAnkiデッキに分けたため。

実装の詳細

メインのファイルから ReadWriter クラスを呼び出し、パースを行ったテキストファイルを書き出すように実装した。

1
2
3
4
5
6
7
8
9
// index.js
const ReadWriter = require("./readWriter");
const fileName = "./assets/rank4.txt";
const textCode = "utf8";
const crlf = "\n";
const output = "./assets/output4.txt";
const readWriter = new ReadWriter(fileName, textCode, crlf);

readWriter.writeFile(output);

ファイルのパースの実装には Parser クラスを設け、これを ReadWriter クラスに埋め込む形とした。
ReadWriter クラスでは読み込んだ元データをパースし、chunkプロパティに配列形式へ保持する。このchunkwriteFileメソッドによって書きだすようにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ReadWriter.js
const fs = require("fs");
const Parser = require("./parser");
class ReadWriter {
  constructor(fileName, textCode, crlf) {
    ...
    // パースしたデータを保持
    this.chunk = [];
    // マスターデータを読み込み
    this.masterText = this.readFile();
    // パース用のクラスを埋め込み
    this.parser = new Parser(this.masterText);
    ...
  }
  ...
  // マスターデータを読み込み
  readFile(){...}
  // 書き出し
  writeFile(fileName) {...}
  ...
}

Parser クラスには、下記のようなマッチャーを作成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Parser.js
...
matchIndex(val) {
    const regexp = /^\d{4}$/;
    return regexp.test(val);
  }
matchWord(val) {
    const regexp = /^[a-zA-Z][\w\s-]+[\w]$/
    return regexp.test(val)
}
...

マッチャ―のテストは下記のようなものを作成した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// parser.test.js
...
  test("matchPronaun", () => {
    expect(parser.matchPronaun("[123]")).toBeTruthy();
    expect(parser.matchPronaun("[abc]")).toBeTruthy();
    expect(parser.matchPronaun("[]")).toBeFalsy();
    expect(parser.matchPronaun("abc")).toBeFalsy();
    expect(parser.matchPronaun("[abc")).toBeFalsy();
    expect(parser.matchPronaun("abc]")).toBeFalsy();
    expect(parser.matchPronaun("]abc[")).toBeFalsy();
  });
...

メインとなるテストケースは複数のパターンを用意し、定義したデータ構造と等しくなるようパースされるかどうかをテストした。下記のような記述となった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
expect(parser.parse()).toEqual([
      [
        ["1028"],
        ["confidential"],
        ["[kɑ̀(ː)nfɪdénʃəl]"],
        ["■ This document should be kept strictly confidential."],
        ["形 内密の,守秘義務のある"],
        ["■ この書類は極秘にしておかなければならない。"],
      ],
      [
        ["1029"],
        [...],
        ...
      ],
      ...
])

おわりに

仕様の把握などを含め、1 日作業となった。
パターンマッチの箇所で if 文が乱立する汚いも実装となってしまった点が残念。しかしこの作業にこれ以上の時間はかけられない。

追記

  • パースに使用したソースコードはプライベートリポジトリとした。詳細が必要な方はご連絡ください。
Share on

whasse
WRITTEN BY
whasse
Web Developer