COTOHAを使ってテキストから穴埋め問題を生成する【自習応援】

# 自分だけの穴埋め問題を作りたい！
勉強するときに練習問題を解くのは大事です。
でも都合よく練習問題が手に入るとは限りません。
そこで任意のテキストから穴埋め問題を作成するコードを書いてみました！



## 例
### 日本史
例えば[Wikipediaの記事「鎌倉時代」](https://ja.wikipedia.org/wiki/%E9%8E%8C%E5%80%89%E6%99%82%E4%BB%A3)の「概要」を使うと次のような感じ（全文は載せられないので一部分）。

問題
> 12世紀末に、（　1　）が（　2　）として（　3　）の頂点に立ち、全国に守護を置いて、（　4　）を開いた。京都の（　5　）と地方の荘園・（　6　）はそのままで、地方支配に地頭等の形で（　3　）が割り込む二元的な支配構造ができあがった。（　7　）は「（　2　）」頼朝の私的家政機関として設立されており、公的機関ではない。したがって基本的に（　4　）が支配下に置いたのは（　2　）の知行国および主従関係を結んだ（　3　）（（　8　））であり、守護の設置などで諸国の治安維持等を担当したものの、全国の（　3　）を完全な支配下に治めたわけではない。（　9　）が（　5　）に入り込み、（　5　）を通じて支配を試みたのとは対照的である。
・・・・
*[Wikipedia　鎌倉時代]((https://ja.wikipedia.org/wiki/%E9%8E%8C%E5%80%89%E6%99%82%E4%BB%A3))を一部改変*

解答

```answer.csv
番号,答え
1,源頼朝
2,鎌倉殿
3,武士
4,鎌倉幕府
5,朝廷
6,公領
7,幕府
8,御家人
9,平氏政権
・・・・
```

### 世界史
[Wikipediaの記事「盛期ルネサンス」](https://ja.wikipedia.org/wiki/%E7%9B%9B%E6%9C%9F%E3%83%AB%E3%83%8D%E3%82%B5%E3%83%B3%E3%82%B9)を使うと次のような感じ。

問題
> 盛期ルネサンス（せいきるねさんす、英語：HighRenaissance）は、（　1　）において、イタリアのルネサンス芸術の最盛期（1450年〜1527年）を指す言葉である。前期は（　2　）が支配する（　3　）による（　3　）派、後期は（　4　）教皇ユリウス2世による芸術家たちをパトロンとした時期で、活動の中心は、それまでの（　3　）から（　4　）に移った。このユリウス2世や、後の（　2　）出身の教皇レオ10世などは（　5　）と呼ばれる。一般に、（　6　）は1490年代後半に現れたとされている。（　7　）が（　8　）で『最後の晩餐』を描いていた時期である。
・・・・
*[Wikipedia 盛期ルネサンス](https://ja.wikipedia.org/wiki/%E7%9B%9B%E6%9C%9F%E3%83%AB%E3%83%8D%E3%82%B5%E3%83%B3%E3%82%B9)を一部改変*

解答

```answer.csv
番号,答え
1,美術史
2,メディチ家
3,フィレンツェ
4,ローマ
5,ルネサンス教皇
6,盛期ルネサンス絵画
7,レオナルド・ダ・ヴィンチ
8,ミラノ
・・・・
```

### 生物
[Wikipediaの記事「生態系」](https://ja.wikipedia.org/wiki/%E7%94%9F%E6%85%8B%E7%B3%BB)の「生態系の成り立ち」を使うと次のような感じ。

問題
> 生態系の生物部分は大きく、（　1　）、消費者、（　2　）に区分される。植物（（　1　））が太陽光から系にエネルギーを取り込み、これを動物などが利用していく（消費者）。遺体や排泄物などは主に微生物によって利用され、さらにこれを食べる生物が存在する（（　2　））。これらの過程を通じて（　1　）が取り込んだエネルギーは消費されていき、生物体を構成していた物質は無機化されていく。それらは再び植物や微生物を起点に食物連鎖に取り込まれる。これを物質循環という。ある地域の生物を見たとき、そこには動物、植物、菌類その他、様々な生物が生息している。これを生物群集というが、その種の組み合わせは、でたらめなものではなく、同じような環境ならば、ある程度共通な組み合わせが存在する。それらの間には捕食被食、競争、共生、寄生、その他様々な関係がある。捕食-被食関係のような生物間のエネルギーの流れを食物連鎖と呼ぶが、近年ではその複雑さを強調して（　3　）[3]が用いられることが多い。
・・・・
*[Wikipedia　生態系](https://ja.wikipedia.org/wiki/%E7%94%9F%E6%85%8B%E7%B3%BB)を一部改変*

解答

```answer.csv
番号,答え
1,生産者
2,分解者
3,食物網
・・・・
```

### ラーメン
[Wikipediaの記事「ラーメン」](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3)の「概要」を使うとこんな感じ。

問題
> （　1　）は江戸時代末に開港した横浜、神戸、（　2　）、（　3　）などに、明治時代になると誕生した（　4　）（当時は南京町と呼ばれた）で食べられていた（　5　）の（　6　）をルーツとするものである[8]。ただし（　2　）では、開港以前から相当数の華僑が定住しており、（　7　）も存在していた可能性がある。1910年（明治43年）、（　8　）に初めて（　9　）人経営者尾崎貫一が（　10　）から招いた（　5　）人料理人（　11　）を雇って（　9　）人向けの（　7　）「（　12　）」を開店し、大人気となった。
・・・・
*[Wikipedia　ラーメン](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3)を一部改変*

解答

```answer.csv
番号,答え
1,ラーメン
2,長崎
3,函館
4,中華街
5,中国
6,麺料理
7,中華料理店
8,東京府東京市浅草区
9,日本
10,横浜中華街
11,12名
12,来々軒
・・・・
```

### エヴァンゲリオン

[Wikipediaの記事「新世紀エヴァンゲリオン」](https://ja.wikipedia.org/wiki/%E6%96%B0%E4%B8%96%E7%B4%80%E3%82%A8%E3%83%B4%E3%82%A1%E3%83%B3%E3%82%B2%E3%83%AA%E3%82%AA%E3%83%B3)の「ストーリー」を使うとこんな感じ。

問題　
> 物語の舞台は西暦2000年9月13日に起きた大災害セカンドインパクトによって世界人口の（　1　）が失われた世界。その15年後の西暦2015年、主人公である14歳の少年碇（　2　）は、別居していた父、国連直属の非公開組織・特務機関NERV（ネルフ）の総司令である（　3　）から突然（　4　）に呼び出され、巨大な汎用人型決戦兵器エヴァンゲリオン（EVA）初号機の（　5　）となって（　4　）に襲来する謎の敵「使徒」と戦うことを命じられる[注4]。当初は（　6　）の命令で、そしてEVA零号機の（　5　）である少女綾波レイの負傷を目の当たりにしたため仕方なくEVAに乗っていた（　2　）だが、使徒との戦い、そして戦闘指揮官であり保護者役となった（　7　）、同級生（　8　）・相田ケンスケらとの交流によって次第に自らの意思でEVAで戦うようになる。
・・・・
*[Wikipedia　新世紀エヴァンゲリオン](https://ja.wikipedia.org/wiki/%E6%96%B0%E4%B8%96%E7%B4%80%E3%82%A8%E3%83%B4%E3%82%A1%E3%83%B3%E3%82%B2%E3%83%AA%E3%82%AA%E3%83%B3)を一部改変*

解答

```answer.csv
番号,答え
1,半数
2,シンジ
3,碇ゲンドウ
4,第3新東京市
5,パイロット
6,ゲンドウ
7,葛城ミサト
8,鈴原トウジ
・・・・
```

このように分野にかかわらずいい感じの問題が作成できます。
今回は都合でWikipediaの記事を使いましたが、同じくらいの長さならどのようなテキストでも問題を作成可能だと思います。

# 仕組みとコード
## 仕組み
難しいことは何もなくて、[COTOHAの固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)と[COTOHAのキーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)で共通して抽出される単語を重要単語とみなして、その単語を空欄にしているだけです。
固有表現抽出だけでは「日本」や「ヨーロッパ」等の問題文の中ではあまり重要でない単語も抽出されています。逆にキーワード抽出のみでは「安定」、「発展」等の概念的な言葉も抽出されてしまします。
だったら共通部分を出せば文中の重要単語だけを拾えるだろうというアイデアです。

![dif_st.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535720/8fbabff0-61c9-3521-d16d-765c970478a3.png)
「生態系」の場合の例（一部のみ）

@zakiyama2918さんの[COTOHAでクイズ自動生成](https://qiita.com/zakiyama2918/items/0329e54ef23978a60574)の方では構文解析が使われていたので、テキストから問題を作る方法はこの他にもたくさんあるのかもしれません。

## コード
がんばってPythonで書いたほうがいいかなと思ったのですが、今回は慣れ親しんでいるRで書きました。
でもRを触ったことがない方でもCOTOHAの登録さえできればあまり苦労せずに使えると思います。

<details><summary>コードは以下です（クリックで展開）。</summary><div>

```R
# 準備
## 既存のオブジェクトを削除
rm(list = ls())

## パッケージの読み込み
library(tidyverse)
library(magrittr)
library(httr)
library(jsonlite)

## 開始時間の記録
start.time <- Sys.time()

## エンドポイントの読み込み
source("endpoint.r", encoding = "utf-8")

## テキストの読み込み
text <- scan("text.txt", what = character(), fileEncoding = "utf-8") %>% paste0(collapse = "")
paste0("Text length: ", nchar(text)) %>% print()

# COTOHAによる処理
## アクセストークンの取得
config <- read_json("config.json") # リクエストボディのロード
token <- POST(url.token, body = config, encode = "json", content_type = "application/json", verbose()) # リクエストをPOST
access.token <- content(token) %>% magrittr::extract2("access_token") # レスポンスからアクセストークンを取得


## 固有表現抽出
status <- 500 # ステータスコードの初期化
while (status == 500) { # ステータスコードが500の間はリトライ
    body.ne <- list(sentence = text) # リクエストボディを作成
    result.ne.tmp <- POST(url.ne, body = body.ne, encode = "json", content_type = "application/json;charset=UTF-8", add_headers(Authorization = paste("Bearer", access.token)), verbose()) # リクエストをポスト
    status <- status_code(result.ne.tmp) # レスポンスのステータスコードを取得
    Sys.sleep(20) # 20秒遅延
    if (as.numeric(difftime(Sys.time(), start.time, units = "mins")) > 3) {break()} # 総経過時間が3分を超えていれば処理を中断
}

result.ne <- result.ne.tmp %>% content() %>% magrittr::extract2("result") %>% bind_rows() %>% bind_rows() # レスポンスをtibbleにまとめる

## キーワード抽出
status <- 500 # ステータスコードを初期化
while (status == 500) { # ステータスコードが500の間はリトライ
    body.key <- list(document = text, type = "default", max_keyword_num = 100) # リクエストボディを作成
    result.key.tmp <- POST(url.keyword, body = body.key, encode = "json", content_type = "application/json;charset=UTF-8", add_headers(Authorization = paste("Bearer", access.token)), verbose()) # リクエストをポスト
    status <- status_code(result.key.tmp) # ステータスコードを取得
    Sys.sleep(20) # 20秒遅延
    if (as.numeric(difftime(Sys.time(), start.time, units = "mins")) > 3) {break()} # 総経過時間が3分を超えていれば処理を中断
}

result.key <- result.key.tmp %>% content() %>% magrittr::extract2("result") %>% bind_rows() %>% bind_rows() # レスポンスをtibbleにまとめる


# 結果の統合
full.list.tmp <- inner_join(result.ne, result.key, by = "form", keep = TRUE) %>% distinct(form) # 共通部分を残して結合し、重複を削除
if (nrow(full.list.tmp) == 0) { # キーワード抽出がうまく行かなかった場合の処理
    full.list <- result.ne %>% distinct(form) # 固有表現の結果のみを使用
}

full.list <- full.list.tmp %>% mutate(num = row_number(), index = as.character(formatC(row_number(), width = 6, flag = "0"))) # 行番号と行のインデックスを取得


# 問題文と解答の作成
quiz.tmp <- NULL # オブジェクトを初期化
for (j in full.list$num) { # 該当する単語をインデックスコードに置換
    if (j == 1) {
        quiz.tmp <- text %>% str_replace_all(pattern = full.list$form[j], replacement = full.list$index[j])
    } else {
        quiz.tmp <- quiz.tmp %>% str_replace_all(pattern = full.list$form[j], replacement = full.list$index[j])
    }
}

order.list <- str_extract_all(quiz.tmp, pattern="[0-9]{6}") %>% unlist() %>% unique() %>% tibble(index =., order = 1:length(.)) # 問題文のなかでの順番を取得

quiz <- NULL # オブジェクトを初期化
for (k in 1:nrow(order.list)) { # インデックスコードを番号に置換
    if (k == 1) {
        quiz <- quiz.tmp %>% str_replace_all(pattern = order.list$index[k], replacement = paste0("（　", order.list$order[k], "　）"))
    } else {
        quiz <- quiz %>% str_replace_all(pattern = order.list$index[k], replacement = paste0("（　", order.list$order[k], "　）"))
    }
}

## ファイルネームの設定
file.name <- start.time %>% format( "%Y-%m-%d_%H_%M" )

## 問題文の保存
quiz %>% write(paste0("quiz_", file.name, ".txt"))

## 解答の作成と保存
answer <- inner_join(full.list, order.list, by = "index") %>% arrange(order) %>% transmute(`番号` = order, `答え` = form) # 番号と解答をひもづけ
answer %>% write_csv(paste0("answer_", file.name, ".txt")) # 解答を保存
```
</div></details>

コードはGit Hubにもおいてあります[https://github.com/ocean-v/quiz_maker](https://github.com/ocean-v/quiz_maker)。

# 使い方
## ***注意***
**今回のコードでできるのはあくまでも「穴埋め問題みたいなもの」を作成することだけです。その単元で中で重要だから空欄になっているわけではありませんし、テストや受験で出る単語をカバーしているわけでもない点にご注意ください。メインの勉強はちゃんとした練習問題などで行ってもらって、空いた時間にでも使ってみていただければと思います。
また使用するテキストの著作権にも十分配慮していただき、作成した問題も個人利用にとどめていただければと思います。**
内容によって生じた損害等の一切の責任を負いかねますのでごめんなさい。


## 環境
Ubuntu18.04、Windows10で動作確認しました。
ただ、Windows10では長い文が処理できないようなので注意（接続が不安定？要確認）。

## 準備
### Rの準備

- Rのインストール
Rを入れていない場合はRをインストールします。
方法はその他のサイトに任せますが、パッケージの依存関係を満たせない場合があるのでできるだけ最新版を入れてください。

- Rのパッケージのインストール
Rで以下のコマンドから必要パッケージをインストールします。
tidyverseなどがすでにインストールされている場合は依存関係が壊れないように注意してください。

```R
install.packages("tidyverse")
install.packages("magrittr")
install.packages("httr")
install.packages("jsonlite")
```

### コードのダウンロード
[こちらのページ](https://github.com/ocean-v/quiz_maker)からファイルをダウンロード（あるいはレポジトリをクローン）して、任意のフォルダにファイルを保存します。
保存できたら`quiz_maker.r`があるフォルダを開いておきます。
ファイルのエンコードはUTF-8になっているので注意してください。


### COTOHAの準備
- COTOHA for Developpersへの登録
登録していない場合は[COTOHAのページ](https://api.ce-cotoha.com/contents/index.html)から無料のfor Developpersに登録します。

- アカウントホームの確認
各種情報を確認しておきます。[以前書いた記事](https://qiita.com/ocean_f/items/91e47786a6d2367da037#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97)も参照。

- `config.json`の編集
フォルダ内の`config.json`をメモ帳等のエディタで開いて、[client id]と[client secret]を先程確認した自分のものに書き換えます。

- `endpoint.r`の編集
フォルダ内の`endpoint.r`をメモ帳等のエディタで開いて、[Access Token Publish URL]、[API Base URL]を確認したものに書き換えます。


### テキストの準備
問題に使いたい文章を`text.txt`に書き込みます。エンコードをUTF-8にしないとうまく行かないと思うので注意してください。
基準がよくわからないのですがUbuntuだと2500文字、Windowsだと500文字くらいを超えるとうまく行かないみたいなので、それ以下の文字数のテキストを使いましょう。


## 実行
上記の準備ができたらRを起動し、`quiz_maker.r`があるフォルダを作業ディレクトリに指定します。
[以前書いた記事](https://qiita.com/ocean_f/items/1a4ba2d3ff085785581c)も参考。

その後`source("quiz_maker.r", encoding = "utf-8")`をRから実行すると処理が開始されます。
エラーが起きた場合はテキストを短くするか、時間をおいてやり直すとうまく行くかもしれません（ステータスコードで500が連続で出るなら短くしたほうがいいかも）。

※1 リクエストのポストは最低20秒間隔になるようにしています。また過剰な繰り返しを防ぐため、処理開始から3分経過した時点で処理を中断するようにしています。
※2 for Developpersの上限は「各API、1,000コール/日」なので注意してください。

## 出力
上記コードを実行すると処理中にはリクエストの結果が表示されます。
うまく行った場合には問題文が`quiz_[日時].txt`の中に、解答が`answer_[日時].csv`の中に保存されます。

コードはもっとスマートに書けるかもしれないので自分で書き換えてみてください。

学ぶ機会をみんなが失わないように祈ります！

# その他参考にしたサイト
[自然言語処理を簡単に扱えると噂のCOTOHA APIをPythonで使ってみた](https://qiita.com/gossy5454/items/83072418fb0c5f3e269f)
[［R］テキストファイル（改行有り）の中身を簡単に読み込んでベクトルにする](http://goldenstate.cocolog-nifty.com/blog/2017/10/r-b926.html)
[R で文字列の切り出しや置換などの文字列処理を行う方法](https://stats.biopapyrus.jp/r/basic/string.html)
[stringrを使って文字列処理をやってみる](https://kazutan.github.io/kazutanR/stringr-intro.html#%E5%8A%A0%E5%B7%A5)
[基本的な正規表現一覧](https://murashun.jp/blog/20190215-01.html#chapter-1)
[Data Transformation with dplyr : : CHEAT SHEET](https://github.com/rstudio/cheatsheets/blob/master/data-transformation.pdf)
[【R】dplyrで連番を追加する](https://qiita.com/weda_654/items/c480d6d45344fd947e6b)
[行の選択 - filter関数](https://kazutan.github.io/kazutanR/hands_on_170730/filter.html#%E6%96%87%E5%AD%97%E5%88%97%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF)
[[翻訳] httr vignette: httrはやわかり](https://qiita.com/nakamichi/items/14f9952445089927c38e#%E3%83%9C%E3%83%87%E3%82%A3)
[httrを使ってRからREST APIを叩く](https://qiita.com/komde/items/22c8a4f809e46cdd70f8)
[httr POST authentication error](https://stackoverflow.com/questions/41809707/httr-post-authentication-error)
[R unique()で重複のない値を取り出す](https://qiita.com/ymmy02/items/368c280f30bd5cd8d179)
[日付・時間関数Tips大全](http://www.okadajp.org/RWiki/?%E6%97%A5%E4%BB%98%E3%80%81%E6%99%82%E9%96%93%E9%96%A2%E6%95%B0Tips%E5%A4%A7%E5%85%A8)
[数値の頭に0を詰めて桁を揃える](https://teramonagi.hatenablog.com/entry/20100902/1283429021)
[RプログラミングTips大全](http://www.okadajp.org/RWiki/?R%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Tips%E5%A4%A7%E5%85%A8)