漢字データベースを使って漢字ベン図を作問する

漢字ベン図は、QuizKnockがやっていた漢字クイズです。条件が3つ与えられるので、複数の条件に当てはまる漢字を答えていきます。

www.youtube.com

この記事では、漢字情報データベース Mojidata を活用して、漢字ベン図を作問してみようと思います。

github.com

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> 

個人的にはSQLiteCLIからだと少し使いづらいと思うので、普段は自作の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}
  • 9画
    • SQL: x in (select k from kanji_strokes where s = 9)
    • Erq: x in kanji_strokes[s = 9]{k}
  • 「せ」から始まる
    • 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}

常用漢字 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)