PythonでTyranoBuilderの問題を生成する
やりたいこと:エクセルで問題を作って、jsonにする。
タブ区切りテキストで作成した問題文を、Tyranoのシーン用に変換追加する。 他のツールにいれたくなることも考えて、まずは読み込んだテキストをjsonに変換して、改めてTyrano用に変換することにする。
動機
授業の復習をしてもらうために、ドリルがあるとよいなと感じる。ただし、そのために専用のアプリをインストールするのは学生にとってハードルが高い。 ハードルが少しでも高いと、学生はとりくまないだろう。
そこでWebサイトでの公開を前提とした仕組みを模索した。TyranoBuilderであれば、この要求を満たせる。ついでに、グラフィックや音楽に凝りたければ、それも可能だ。
しかし、問題がある。それは、TyranoBuilderで問題を追加するのはとても面倒だ。 一つ作成してコピーすればよいのだが、問題の出し方を全体的に修正したくなるともう大変だ。 そして、問題そのものの修正も少しめんどうだし、問題全体の一覧性も悪い。
問題をjson形式とかyaml形式にすれば処理面では楽だが、書くほうは大変だ。せめてタブ区切りぐらいにしておきたい。
問題をテキストファイルで作成して、Webでの公開はTyranoBuilderという形にできれば理想だ。
というわけで、今回のとりくみとなる。
計画
入力:タブ区切りテキスト形式の問題
出力:TyranoBuilderのks(シーン)ファイル形式
実験編
どういうことができるか、まずは、実験をここでおこなう。
Pythonでjsonを読んでパースする。
test.jsonに簡単なjsonファイルを作成して、読み込む。
参考サイトをそのままトレース。
import json
f = open('test.json', 'r')
jdict = json.load(f)
print('book1:{}'.format(jdict['book1']))
print('book3:{}'.format(jdict['book3']['page']))
for x in jdict:
book_page = jdict[x]['page']
print('{0}:{1} <- {2}'.format(jdict[x]['title'], jdict[x]['year'],x))
book1:{'title': 'Python Beginners', 'year': 2005, 'page': 399}
book3:344
Python Beginners:2005 <- book1
Python Developers:2006 <- book2
Python cookbook:2002 <- book3
Python Dictionary:2012 <- book4
なるほど、こうなるのか。
そのまま辞書型として読み込んでくれるので、扱いも楽だ。 jsonって便利だけど、手で書くのはちょい厳しい。でも使われるのは、処理が楽だからなんだろうな。 その気になれば、読めるし。
ヒアドキュメントによる登録
import json
jstr='''
{
"Q1":{
"question":"What is 1+1?",
"choice":{
"c1":{
"title":"2",
"correct":"1",
"description":"OK. Easy?"
},
"c2":{
"title":"3",
"correct":"0",
"description":"Unbelievable!"
}
}
},
"Q2":{
"question":"What is 1+3?",
"choice":{
"c1":{
"title":"2",
"correct":"0",
"description":"Oh! No!"
},
"c2":{
"title":"4",
"correct":"1",
"description":"Good"
},
"c3":{
"title":"5",
"correct":"0",
"description":"Almost!"
}
}
}
}
'''
jtest = json.loads(jstr)
jsonに値を追加
Q3を追加することにしよう。 ついでに、日本語が使えるかも試しておく。
※ 職場のMacはPython2だったので日本語の扱いが難しかったようだ。 すべての文字列の前にuがついていた。 Python3では問題ない。
jtest['Q3']={'question':'Who am I?', 'choice':{'c1':{'title':'Yusuke', 'correct':0, 'description':'おしいですね!'}}}
print(jtest)
{'Q1': {'question': 'What is 1+1?', 'choice': {'c1': {'title': '2', 'correct': '1', 'description': 'OK. Easy?'}, 'c2': {'title': '3', 'correct': '0', 'description': 'Unbelievable!'}}}, 'Q2': {'question': 'What is 1+3?', 'choice': {'c1': {'title': '2', 'correct': '0', 'description': 'Oh! No!'}, 'c2': {'title': '4', 'correct': '1', 'description': 'Good'}, 'c3': {'title': '5', 'correct': '0', 'description': 'Almost!'}}}, 'Q3': {'question': 'Who am I?', 'choice': {'c1': {'title': 'Yusuke', 'correct': 0, 'description': 'おしいですね!'}}}}
問題にIDを追加する
QIDとCIDを作って問題と解答の一覧を表示させてみる。 簡単な処理だけど、慣れるように、練習練習。
qn=1
for x in jtest:
print('Q{0}: {1}'.format(qn,jtest[x]['question']))
cn=1
for c in jtest[x]['choice']:
print('C{0}:{1}'.format(cn, jtest[x]['choice'][c]['title']))
cn += 1
qn +=1
Q1: What is 1+1?
C1:2
C2:3
Q2: What is 1+3?
C1:2
C2:4
C3:5
Q3: Who am I?
C1:Yusuke
テキストからの変換
jsonの読み込みとキーの作成はだいたい分かった。
次はタブ区切りテキストをjsonに変換する実験をする。
最終的にどんなフォーマットにするのがいいのか迷う。可変長のデータになってしまう。CSVだと意外と難しいなあ、これ。
次はタブ区切りデータが良さそうだが、使えるのかな。読んでみよう。 まずは普通に読んでだらだらと表示。
ポイントはdelimiterの指定。
import csv
tsv_file = 'csvtest.txt'
with open(tsv_file, 'r') as f:
reader = csv.reader(f, delimiter='\t')
for line in reader:
print(line)
['問題', '坂田は今どこにいますか']
['選択肢', '0', 'くまもと']
['', 'それは自宅ですね。今はそこにはいません。']
['選択肢', '0', '福岡']
['', '職場です。']
['選択肢', '1', 'デンマーク']
['', '正解!よく知ってましたね。']
['選択肢', '0', '東京']
['', 'あー、それはないですね。']
['', 'まあ、べつにいいですけど。']
['問題', '持続可能な開発の事例としてふさわしい事項を一つ選びなさい。']
['選択肢', '0', '毎年2%の経済成長を目指して目標を立てる']
['', '持続可能性を考えたばあい、経済成長だけの目標は不適切です']
['選択肢', '1', '地域で利用できる技術を使用したプロジェクトを計画する']
['', '適正技術と呼ばれる方法です。']
['選択肢', '0', '原油を使えるだけ使って生産を継続する']
['', 'あきらかに不適切です']
['', 'これはむしろ資源の持続可能でない使いかたといえます']
['', 'こういうアプローチをとるばあいは、代替資源の開発にめどが立っているばあいにのみ許されます']
なるほど、配列に読みこまれるのか。
問題文を読んで、選択肢だけを表示してみよう。
import csv
tsv_file = 'csvtest.txt'
with open(tsv_file, 'r') as f:
reader = csv.reader(f, delimiter='\t')
for line in reader:
if (line[0] == '問題'):
print('問題:<{0}>'.format(line[1]))
elif (line[0] == '選択肢'):
print(' 選択肢:{0}'.format(line[1]))
else:
print(' {0}'.format(line[1]))
坂田は今どこにいますか
現在の住居を答えなさい
0
それは自宅ですね。今はそこにはいません。
0
職場です。
1
正解!よく知ってましたね。
0
あー、それはないですね。
まあ、べつにいいですけど。
持続可能な開発の事例としてふさわしい事項を一つ選びなさい。
0
持続可能性を考えたばあい、経済成長だけの目標は不適切です
1
適正技術と呼ばれる方法です。
0
あきらかに不適切です
これはむしろ資源の持続可能でない使いかたといえます
こういうアプローチをとるばあいは、代替資源の開発にめどが立っているばあいにのみ許されます
論理思考を勉強するためにいらないことってなんでしょうか。
1
はい、そのとおり!
食べたいけど、なくてもいいですよね。
0
はずれー
論理思考を学ぶためには、ちゃんと考える姿勢、必要です。
1
正解!
論理思考を学ぶためには、それ、違いますよね。
1
正解!
論理思考を学ぶためには、字はまあ関係ないですよね。
坂○先生の字も読めないって学生にしょっちゅう言われています。
0
はずれ
論理思考を学ぶためには、大切です。
どうやったら図解できるかも、ここで学びましょう。
大学で楽しく勉強するためにはなにが必要でしょうか。
1
おっしゃるとおり!
すばらしすぎる解答です。もしかして、魔女好き?
0
はずれー
学食もありますよ。
0
そんなことありません。
偏差値がなんで存在してるか分かってますかね?
0
まあ、そうですね!
友達いなくても別にいいけど、いたらいいと思います
0
はずれ
保護者が大学についてくる必要はありません。
教員としてはよほどの事情がない限りかんべんしてほしいです。
オッケー
まとめる
じゃあ、次にこれをさきほどのプログラムに追加して、読み込んだファイルをjsonに追加できるようにする。
あ、その前に出力用の関数つくっておこう。 読み込んでそのたびに画面に表示では、邪魔だから、読み込んだあと、それをフォーマットして表示するようにしよう。
後々はこれを改造して、TyranoBuilder用のデータを出力する関数にしていく。
def print_json(jtext):
res = ''
qn=1
for x in jtext:
qitem = jtext[x]
print('Q{0}: {1}'.format(qn,qitem['question']))
cn=1
for c in qitem['choice']:
item = qitem['choice'][c]
print(' 選択肢{0}({1}):{2}'.format(cn, item['correct'],item['title']))
print(' 説明 :{0}'.format(item['description']))
cn += 1
qn +=1
さて、本題。
import csv
def read_json(fname):
def concat_text(body, newitem):
return(newitem if (body == '') else body + '[p]' + newitem)
res = {}
qid = 1
# qfは、問題文を処理中かどうかを示すフラグ
qf = False
with open(tsv_file, 'r') as f:
reader = csv.reader(f, delimiter='\t')
for line in reader:
if (line[0] == QFlag):
qname='Q'+str(qid)
res[qname]={'question':line[1],'choice':{}}
# 次から、選択肢の処理にうつる準備。実際には問題文が2行以上あるばあいは、2行目以降の処理に移る
qid += 1
cnum = 1
# 問題文の処理を始める
qf = True
elif (line[0] == CFlag):
cname = 'c' + str(cnum)
res[qname]['choice'][cname]={'title':line[2], 'correct':line[1], 'description':''}
cnum += 1
# 選択肢の処理を行なっていることを示す
qf = False
else:
if (qf):
res[qname].update([('question',concat_text(res[qname]['question'],line[1]))])
else:
res[qname]['choice'][cname].update([('description' , concat_text(res[qname]['choice'][cname]['description'], line[1]))])
return res
QFlag = 'Q'
CFlag = 'C'
tsv_file = 'csvtest.txt'
jtest = read_json(tsv_file)
# print_json(jtest)
Q1: 坂田は今どこにいますか[p]]現在の住居を答えなさい
選択肢1(0):くまもと
説明 :それは自宅ですね。今はそこにはいません。
選択肢2(0):福岡
説明 :職場です。
選択肢3(1):デンマーク
説明 :正解!よく知ってましたね。
選択肢4(0):東京
説明 :あー、それはないですね。[p]]まあ、べつにいいですけど。
Q2: 持続可能な開発の事例としてふさわしい事項を一つ選びなさい。
選択肢1(0):毎年2%の経済成長を目指して目標を立てる
説明 :持続可能性を考えたばあい、経済成長だけの目標は不適切です
選択肢2(1):地域で利用できる技術を使用したプロジェクトを計画する
説明 :適正技術と呼ばれる方法です。
選択肢3(0):原油を使えるだけ使って生産を継続する
説明 :あきらかに不適切です[p]]これはむしろ資源の持続可能でない使いかたといえます[p]]こういうアプローチをとるばあいは、代替資源の開発にめどが立っているばあいにのみ許されます
Q3: 論理思考を勉強するためにいらないことってなんでしょうか。
選択肢1(1):おやつを用意する
説明 :はい、そのとおり![p]]食べたいけど、なくてもいいですよね。
選択肢2(0):じっくり考える姿勢
説明 :はずれー[p]]論理思考を学ぶためには、ちゃんと考える姿勢、必要です。
選択肢3(1):天才であること
説明 :正解![p]]論理思考を学ぶためには、それ、違いますよね。
選択肢4(1):字がきれいなこと
説明 :正解![p]]論理思考を学ぶためには、字はまあ関係ないですよね。[p]]坂○先生の字も読めないって学生にしょっちゅう言われています。
選択肢5(0):図で書く、イメージする力
説明 :はずれ[p]]論理思考を学ぶためには、大切です。[p]]どうやったら図解できるかも、ここで学びましょう。
Q4: 大学で楽しく勉強するためにはなにが必要でしょうか。
選択肢1(1):図書館を好きになる
説明 :おっしゃるとおり![p]]すばらしすぎる解答です。もしかして、魔女好き?
選択肢2(0):お弁当がおいしいこと
説明 :はずれー[p]]学食もありますよ。
選択肢3(0):天才であること
説明 :そんなことありません。[p]]偏差値がなんで存在してるか分かってますかね?
選択肢4(0):友達をつくること
説明 :まあ、そうですね![p]]友達いなくても別にいいけど、いたらいいと思います
選択肢5(0):保護者がついてくること
説明 :はずれ[p]]保護者が大学についてくる必要はありません。[p]]教員としてはよほどの事情がない限りかんべんしてほしいです。
Q5: 次のうち、ストックにあてはまるものを答えなさい
選択肢1(0):国内総生産
説明 :フローです。[p]]ある国で1年間に生産された付加価値の総額を言います。
選択肢2(1):日本の資産総額
説明 :資産の総額は、年間で変動します。[p]]けれども、現在の総額を言うばあいは、今この瞬間の量を言いますので、ストックです。
選択肢3(0):あなたの家の年間収入総額
説明 :これもフローです
選択肢4(0):お風呂にお湯が貯まる速さ
説明 :フローとストックの説明で利用した事例です[p]]今、貯まっている量はストックでしたね
キーワードをみつけて、置換する実験
これで、タブ区切りデータが辞書形式になって、手元にある。
次はこれをTyranoBuilderで使うシーンの形式にあわせる。 これは、準備したテンプレートにあてはめてエクスポートするだけなので、そんなに難しくない。
置き換えるキーワードは«abc»のような形式にした。これをテンプレートにうめこんで、置換していく。
読み込んだファイルはこんな感じですね。
f = open('csource.txt')
choicedata = f.read() # ファイル終端まで全て読んだデータを返す
f.close()
print(choicedata)
これを変換します。
cbody = 'はい、そのとおり![p]食べたいけど、なくてもいいですよね。[p]'
choicedata = choicedata.replace('<<qid>>','1').replace('<<cid>>','1').replace('<<cbody>>',cbody)
print(choicedata)
*Q<<qid>>C<<cid>>
[tb_start_text mode=1 ]
<<cbody>>
[_tb_end_text]
<<correct>>
[l ]
[jump storage="Q1.ks" target="*Q1common" ]
*Q1C1
[tb_start_text mode=1 ]
はい、そのとおり![p]食べたいけど、なくてもいいですよね。[p]
[_tb_end_text]
<<correct>>
[l ]
[jump storage="Q1.ks" target="*Q1common" ]
ということで、だいたい実験終了。
実装
実験が終わったので、実装していく。
問題ファイルの形式を決める
jsonでイメージすると、こんなかんじ。
- 問題ファイル
- 問題文
- グループタグ
- レベル
- 選択肢
- 選択肢文
- 正解フラグ(これはポイントも兼ねている。)
- 解説
正解フラグは1以上で正解。 ポイント制にするばあいは、ポイントを記入。 選択肢がいくつかあるかは不明。
なお、レベルとグループは将来の対応
当面のフォーマット
問題問題文 問題文続き 選択肢解答フラグ選択肢文 説明文 説明文続き
改行はtyranoscriptにあわせて、[p]をいれることにする。
これが最終的なスクリプト
最初にread_jsonを呼んで、jsonを作っておく。
一番外側から。
scene_source.txtが一番外側。理由はとくにないけれど、関数の外で定義。 シーンまるごと関数で作成するのではなく、シーンにいれこむ問題データのみを関数に生成させる。
問題データは、qsource.txtに埋め込んでいく。 選択肢群は、clinksに埋め込む。 そのために、clistに必要な数だけリンクを追加しておく。その際、Y座標を変えるために、yvalueを変えていく。
解答は、解説と正答の確認を行う。 ここは、choicesにcbodyを埋め込む。 cbodyにはcsource.txtを利用して、問題の解説と正答チェックをいれる。
def get_data(fname):
f = open(fname)
fdata = f.read() # ファイル終端まで全て読んだデータを返す
f.close()
return(fdata)
def tyrano_q(jtext):
qn = 1
qs = get_data('qsource.txt')
cs = get_data('csource.txt')
cl = get_data('clinks.txt')
correct = get_data('correct.txt')
cg = get_data('cgiveup.txt')
ascene = ''
for x in jtext:
qitem = jtext[x]
cn=1
clist = ''
cbody = ''
yvalue = 150
for c in qitem['choice']:
item = qitem['choice'][c]
clist = clist + cl.replace('<<qid>>', str(qn)).replace('<<cid>>',str(cn)).replace('<<ctitle>>', item['title']).replace('<<Y>>',str(yvalue))
cortext = correct if (int(item['correct'])>0) else ''
cbody = cbody + cs.replace('<<qid>>', str(qn)).replace('<<cid>>',str(cn)).replace('<<cbody>>', item['description']).replace('<<correct>>',cortext)
yvalue += 50
cn += 1
# giveupを追加
clist += cg.replace('<<Y>>',str(yvalue))
ascene += qs.replace('<<qid>>', str(qn)).replace('<<qtext>>', qitem['question']).replace('<<clinks>>', clist).replace('<<choices>>', cbody)
qn +=1
return(ascene, qn)
QFlag = 'Q'
CFlag = 'C'
newscene, qn = tyrano_q(read_json('csvtest.txt'))
q1scene = get_data('scene_source.txt').replace('<<questions>>', newscene).replace('<<qs>>', str(qn-1))
## 保存しとこう
with open('Q1.txt', mode='w') as f:
f.write(q1scene)
選択肢のシャッフル機能を追加
これは実験中
import csv
def read_json(fname):
def concat_text(body, newitem):
return(newitem if (body == '') else body + '[p]' + newitem)
res = {}
with open(tsv_file, 'r') as f:
qid = 1
cnum = 1
qf = False
reader = csv.reader(f, delimiter='\t')
for line in reader:
if (line[0] == QFlag):
qname='Q'+str(qid)
res[qname]={'question':line[1],'choice':{}}
cnum = 1
qid += 1
qf = True
elif (line[0] == CFlag):
qf = False
choice = {'title':line[2], 'correct':line[1], 'description':''}
cname = 'c' + str(cnum)
cnum += 1
else:
if (qf):
res[qname].update([('question',concat_text(res[qname]['question'],line[1]))])
else:
choice.update([('description' , concat_text(choice['description'], line[1]))])
if (line[0] in (QFlag, CFlag)) and (cnum > 1) and (qid > 1):
res[qname]['choice'][cname]=choice
res[qname]['choice'][cname]=choice
return res
QFlag = 'Q'
CFlag = 'C'
tsv_file = 'csvtest.txt'
jtest = read_json(tsv_file)
print_json(jtest)
Q1: 坂田は今どこにいますか[p]現在の住居を答えなさい
選択肢1(0):くまもと
説明 :それは自宅ですね。今はそこにはいません。
選択肢2(0):福岡
説明 :職場です。
選択肢3(1):デンマーク
説明 :正解!よく知ってましたね。
選択肢4(0):東京
説明 :あー、それはないですね。[p]まあ、べつにいいですけど。
Q2: 持続可能な開発の事例としてふさわしい事項を一つ選びなさい。
選択肢1(0):毎年2%の経済成長を目指して目標を立てる
説明 :持続可能性を考えたばあい、経済成長だけの目標は不適切です
選択肢2(1):地域で利用できる技術を使用したプロジェクトを計画する
説明 :適正技術と呼ばれる方法です。
選択肢3(0):原油を使えるだけ使って生産を継続する
説明 :あきらかに不適切です[p]これはむしろ資源の持続可能でない使いかたといえます[p]こういうアプローチをとるばあいは、代替資源の開発にめどが立っているばあいにのみ許されます
Q3: 論理思考を勉強するためにいらないことってなんでしょうか。
選択肢1(1):おやつを用意する
説明 :はい、そのとおり![p]食べたいけど、なくてもいいですよね。
選択肢2(0):じっくり考える姿勢
説明 :はずれー[p]論理思考を学ぶためには、ちゃんと考える姿勢、必要です。
選択肢3(1):天才であること
説明 :正解![p]論理思考を学ぶためには、それ、違いますよね。
選択肢4(1):字がきれいなこと
説明 :正解![p]論理思考を学ぶためには、字はまあ関係ないですよね。[p]坂○先生の字も読めないって学生にしょっちゅう言われています。
選択肢5(0):図で書く、イメージする力
説明 :はずれ[p]論理思考を学ぶためには、大切です。[p]どうやったら図解できるかも、ここで学びましょう。
Q4: 大学で楽しく勉強するためにはなにが必要でしょうか。
選択肢1(1):図書館を好きになる
説明 :おっしゃるとおり![p]すばらしすぎる解答です。もしかして、魔女好き?
選択肢2(0):お弁当がおいしいこと
説明 :はずれー[p]学食もありますよ。
選択肢3(0):天才であること
説明 :そんなことありません。[p]偏差値がなんで存在してるか分かってますかね?
選択肢4(0):友達をつくること
説明 :まあ、そうですね![p]友達いなくても別にいいけど、いたらいいと思います
選択肢5(0):保護者がついてくること
説明 :はずれ[p]保護者が大学についてくる必要はありません。[p]教員としてはよほどの事情がない限りかんべんしてほしいです。