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]教員としてはよほどの事情がない限りかんべんしてほしいです。
Hugo で構築されています。
テーマ StackJimmy によって設計されています。