漢字データベースを使って漢字ベン図を作問する
漢字ベン図は、QuizKnockがやっていた漢字クイズです。条件が3つ与えられるので、複数の条件に当てはまる漢字を答えていきます。
この記事では、漢字情報データベース Mojidata を活用して、漢字ベン図を作問してみようと思います。
MojidataはSQLiteというデータベースエンジンで使うことができるデータベースになっていて、情報をSQLで取得することができます。
データベースを使う準備
Mojidataを使うには、Node.jsとSQLiteをインストールしてあると楽です。
その後、ターミナルで次のコマンドを実行して、moji.dbをダウンロードし、sqlite3を起動してください。
# 作業用のディレクトリを作る mkdir kanji-venn # カレントディレクトリを変更する cd kanji-venn # npm パッケージの初期化(node_modulesを作業用ディレクトリに作成するため) npm init -y # mojidataパッケージのインストール npm install @mandel59/mojidata # SQLiteの起動 sqlite3 node_modules/@mandel59/mojidata/dist/moji.db
SQLiteが起動すると、次のようなプロンプトが表示されます。
SQLite version 3.40.0 2022-11-16 12:10:08 Enter ".help" for usage hints. sqlite>
個人的にはSQLiteはCLIからだと少し使いづらいと思うので、普段は自作のErqというツールを使っています。これは補完機能が使え、SQLより簡単に書けるErqクエリ言語を使って情報を取得できます。開発途中で、マニュアル等はないのですが、Erqを使ってみたい場合は、こちらもnpmでインストールして使うことができます。次のコマンドでErqをインストールします。
# Erqのインストール npm install github:mandel59/erq # Erqの起動 npx erq node_modules/@mandel59/mojidata/dist/moji.db
Erqを起動すると、次のようなプロンプトが表示されます。
Connected to node_modules/@mandel59/mojidata/dist/moji.db erq>
他にDuckDBを使って読み込む方法や、GUIのツールを使う方法もあります。
漢字情報を取得してみる
作問するにあたって、漢字の次のような情報が取得したいです。
- 常用漢字の一覧
- 漢字の読み
- 漢字の総画数
- 漢字の部首
- 漢字の構造
常用漢字の一覧と読みは、常用漢字表のデータを格納した joyo
テーブルに保存されています。また、総画数はMJ文字情報一覧のデータを格納した mji
テーブルから、部首は mji_rsindex
テーブルから、漢字の構造は ids
テーブルから、それぞれ取得できます。
erq> joyo limit 10;; select * from joyo limit 10 ["漢字","音訓","例","備考"] ["亜","ア","[\"亜流\",\"亜麻\",\"亜熱帯\"]",""] ["哀","アイ","[\"哀愁\",\"哀願\",\"悲哀\"]",""] ["哀","あわれ","[\"哀れ\",\"哀れな話\",\"哀れがる\"]",""] ["哀","あわれむ","[\"哀れむ\",\"哀れみ\"]",""] ["挨","アイ","[\"挨拶\"]",""] ["愛","アイ","[\"愛情\",\"愛読\",\"恋愛\"]","愛媛(えひめ)県"] ["曖","アイ","[\"曖昧\"]",""] ["悪","アク","[\"悪事\",\"悪意\",\"醜悪\"]",""] ["悪","オ","[\"悪寒\",\"好悪\",\"憎悪\"]",""] ["悪","わるい","[\"悪い\",\"悪さ\",\"悪者\"]",""] 10 rows (0.011s)
読み情報ビュー kanji_reading
を定義して、必要な情報だけを使いやすくします。
erq> view temp.kanji_reading = joyo {k: 漢字, r: 音訓};; create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading ok (0.002s) erq> kanji_reading limit 10;; select * from kanji_reading limit 10 ["k","r"] ["一","ひと"] ["一","ひとつ"] ["一","イチ"] ["一","イツ"] ["丁","チョウ"] ["丁","テイ"] ["七","なな"] ["七","ななつ"] ["七","なの"] ["七","シチ"] 10 rows (0.000s)
総画数と部首、構造を取得するビューも定義します。
総画数情報ビュー kanji_strokes
の定義
erq> view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};; create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes ok (0.000s) erq> kanji_strokes limit 10;; select * from kanji_strokes limit 10 ["k","s"] ["一",1] ["丁",2] ["七",2] ["万",3] ["丈",3] ["三",3] ["上",3] ["下",3] ["不",4] ["与",3] 10 rows (0.001s)
部首情報ビュー kanji_radical
の定義
erq> view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};; create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical ok (0.000s) erq> kanji_radical limit 10;; select * from kanji_radical limit 10 ["k","rad"] ["一","一"] ["丁","一"] ["七","一"] ["万","一"] ["丈","一"] ["三","一"] ["上","一"] ["下","一"] ["不","一"] ["与","一"] 10 rows (0.001s)
構造情報ビュー kanji_ids
の定義
erq> view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;; create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids ok (0.000s) erq> kanji_ids limit 10;; select * from kanji_ids limit 10 ["k","ids"] ["一","一"] ["丁","⿱一亅"] ["七","〾⿻乚一"] ["万","⿸丆𠃌"] ["丈","⿻𠂇乀"] ["三","三"] ["上","⿱⺊一"] ["下","⿱一卜"] ["不","⿸丆⿰丨丶"] ["不","⿻丆卜"] 10 rows (0.003s)
クイズを作問する
ここまでできれば、あとは、条件に当てはまる漢字を取得するクエリを作るだけです。先の動画の例題で言えば、「さんずい」「9画」「「せ」から始まる」といった条件は、漢字をx
とすれば、SQLとErqではそれぞれ次のように表現できます。
- さんずい
- SQL:
x in (select k from kanji_ids where ids glob '⿰氵*')
- Erq:
x in kanji_ids[ids glob '⿰氵*']{k}
- SQL:
- 9画
- SQL:
x in (select k from kanji_strokes where s = 9)
- Erq:
x in kanji_strokes[s = 9]{k}
- SQL:
- 「せ」から始まる
- SQL:
x in (select k from kanji_reading where r glob 'せ*' or r glob 'セ*')
- Erq:
x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
- SQL:
各常用漢字 x
についてそれぞれ判定し、複数の条件に当てはまるものを表示すれば作問ができそうです。Erqでクエリを作ってみます。
/* 常用漢字を x という名前で取り出す */ joyo {x: 漢字} distinct /* 各漢字について、条件を判定する */ { x, `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k}, `9画`: x in kanji_strokes[s = 9]{k}, `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k} } /* 複数の条件にあてはまる漢字のみ残す */ [`さんずい` + `9画` + `「せ」から始まる` >= 2] /* あてはまる条件でグループ化 */ { `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) } ;;
これを入力すると:
erq> /* 常用漢字を x という名前で取り出す */ joyo {x: 漢字} distinct ...> /* 各漢字について、条件を判定する */ ...> { ...> x, ...> `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k}, ...> `9画`: x in kanji_strokes[s = 9]{k}, ...> `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k} ...> } ...> /* 複数の条件にあてはまる漢字のみ残す */ ...> [`さんずい` + `9画` + `「せ」から始まる` >= 2] ...> /* あてはまる条件でグループ化 */ ...> { `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) } ...> ;; select `さんずい`, `9画`, `「せ」から始まる`, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as `さんずい`, x in (select k from kanji_strokes where (s = 9)) as `9画`, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as `「せ」から始まる` from (select distinct 漢字 as x from joyo) where (`さんずい` + `9画` + `「せ」から始まる` >= 2)) group by (`さんずい`), (`9画`), (`「せ」から始まる`) ["さんずい","9画","「せ」から始まる","group_concat(x)"] [0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"] [1,0,1,"清,潜,瀬"] [1,1,0,"洋,洞,津,洪,活,派,浄,海"] [1,1,1,"洗,浅"] 4 rows (0.017s)
コマンドラインからクエリを実行する
先ほどは手でクエリを入力していましたが、毎回同じように手で入力するのは面倒なので、次のクエリを kanji-venn.erq
ファイルに保存しておき、コマンドで実行してみます。
view temp.kanji_reading = joyo {k: 漢字, r: 音訓};; view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};; view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};; view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;; /* 常用漢字を x という名前で取り出す */ joyo {x: 漢字} distinct /* 各漢字について、条件を判定する */ { x, p1: x in kanji_ids[ids glob '⿰氵*']{k}, p2: x in kanji_strokes[s = 9]{k}, p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k} } /* 複数の条件にあてはまる漢字のみ残す */ [p1 + p2 + p3 >= 2] /* あてはまる条件でグループ化 */ {p1, p2, p3 => group_concat(x)} ;;
npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq
$ npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq Connected to node_modules/@mandel59/mojidata/dist/moji.db view temp.kanji_reading = joyo {k: 漢字, r: 音訓};; view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};; view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};; view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;; /* 常用漢字を x という名前で取り出す */ joyo {x: 漢字} distinct /* 各漢字について、条件を判定する */ { x, p1: x in kanji_ids[ids glob '⿰氵*']{k}, p2: x in kanji_strokes[s = 9]{k}, p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k} } /* 複数の条件にあてはまる漢字のみ残す */ [p1 + p2 + p3 >= 2] /* あてはまる条件でグループ化 */ {p1, p2, p3 => group_concat(x)} ;; create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading ok (0.024s) create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes ok (0.000s) create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical ok (0.000s) create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids ok (0.000s) select p1, p2, p3, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as p1, x in (select k from kanji_strokes where (s = 9)) as p2, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as p3 from (select distinct 漢字 as x from joyo) where (p1 + p2 + p3 >= 2)) group by (p1), (p2), (p3) ["p1","p2","p3","group_concat(x)"] [0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"] [1,0,1,"清,潜,瀬"] [1,1,0,"洋,洞,津,洪,活,派,浄,海"] [1,1,1,"洗,浅"] 4 rows (0.032s)