ChatGPTをjsonファイルで出力して英単語表を作ってみる

# はじめに
ChatGPTの対話から英単語とその日本語訳からなる英単語表を作成します。具体的にはChatGPTのjsonファイルから、Pythonで値を整理して最終的に対話の表を作成します。
### まえがき
ChatGPTはレスポンスが早く、英単語のようなリアルタイム性のない簡単な内容であればある程度正確であり、学習機能も備わっていることから英語学習に向いていると考えています。さらに、会話が記録されていること、それを複数端末から入力または閲覧できることから、私は英語学習中、わからない単語のメモとして活用しています↓。
![d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2560987/a7e857cf-bf60-d8f2-9ac0-817f5b0a7399.png)

これらのログを見やすい形で整理すれば学習の効率を上げられるのではないかと思ったのが、今回のきっかけです。
### 注意書き
プログラミング初学者が初めて記事を書くため、至らない点が多いことと思いますが、暖かい目で見守っていただくと幸いです。
### ChatGPT拡張機能
Chromeの拡張機能として、「[Superpower ChatGPT](https://chrome.google.com/webstore/detail/superpower-chatgpt/amhmeenmapldpjdedekalnfifgnpfnkc?hl=ja "Superpower ChatGPT")」を使用します。ChatGPTの標準の出力機能とは違い、会話を選んで出力できるというメリットがあります。
### 使用言語と環境
json形式のファイルを処理するため、Jupyter Notebook上でPythonを実行しました。
# 対話の部分の文字列を整列して取り出してみる
### 1.jsonファイルの内容を確認する
例として、次のような対話を出力します。極端に短い会話ですが、それでもファイルはかなり長くなります。
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2560987/66c925ac-7fc9-89e8-b781-bb5ce369fee5.png)

まず、出力されたjsonファイルの中身を確認してみます。
<details><summary>test.json(長いので折り畳み)</summary>

~~~test.json
{"archived":false,
 "create_time":1683031269.217,
 "current_node":"368ad81d-b0fa-4299-9401-08bf0355919d",
 "id":"3d9a2b19-0857-4d0d-a0a9-0b87be710648",
 "languageCode":"default",
 "mapping":{
     "01704f2c-8c30-40e9-8283-99da8166188f":{ (1)
         "children":["5640b7e3-490c-4c19-ac27-60ce8ad96365"],
         "id":"01704f2c-8c30-40e9-8283-99da8166188f",
         "message":null,
         "parent":null
     },
     "368ad81d-b0fa-4299-9401-08bf0355919d":{ (2)
         "children":[],
         "id":"368ad81d-b0fa-4299-9401-08bf0355919d",
         "message":{
             "author":{
                 "metadata":{},
                 "name":null,
                 "role":"assistant"
             },
             "content":{
                 "content_type":"text",
                 "parts":["おやすみなさい！良い夢を見てください。"]
             },
             "create_time":1683031296.493976,
             "end_turn":true,
             "id":"368ad81d-b0fa-4299-9401-08bf0355919d",
             "metadata":{
                 "finish_details":{
                     "stop":"
",
                     "type":"stop"
                 },
                 "message_type":"next",
                 "model_slug":"text-davinci-002-render-sha"
             },
             "recipient":"all",
             "update_time":null,
             "weight":1
         },
         "parent":"3b592a2e-2e20-47ed-9fd8-c17337213f00"
     },
     "3b592a2e-2e20-47ed-9fd8-c17337213f00":{ (3)
         "children":["368ad81d-b0fa-4299-9401-08bf0355919d"],
         "id":"3b592a2e-2e20-47ed-9fd8-c17337213f00",
         "message":{
             "content":{
                 "content_type":"text",
                 "parts":["おやすみ"]
             },
             "id":"3b592a2e-2e20-47ed-9fd8-c17337213f00",
             "metadata":{
                 "model_slug":"text-davinci-002-render-sha"
             },
             "role":"user"
         },
         "parent":"eab6ca6d-647b-43e0-be68-2b99d496e865"
     },
     "5640b7e3-490c-4c19-ac27-60ce8ad96365":{ (4)
         "author":{
             "metadata":{},
             "name":null,
             "role":"system"
         },
         "children":["bca69803-cb8a-4f6a-90a8-a5686e305271"],
         "content":{
             "content_type":"text",
             "parts":[""]
         },
         "create_time":1683031254.172533,
         "end_turn":true,
         "id":"5640b7e3-490c-4c19-ac27-60ce8ad96365",
         "metadata":{},
         "parent":"01704f2c-8c30-40e9-8283-99da8166188f",
         "recipient":"all",
         "update_time":null,
         "weight":1
     },
     "bca69803-cb8a-4f6a-90a8-a5686e305271":{ (5)
         "children":["eab6ca6d-647b-43e0-be68-2b99d496e865"],
         "id":"bca69803-cb8a-4f6a-90a8-a5686e305271",
         "message":{
             "author":{
                 "metadata":{},
                 "name":null,
                 "role":"user"
             },
             "content":{
                 "content_type":"text",
                 "parts":["こんにちは"]
             },
             "create_time":1683031254.172943,
             "end_turn":null,
             "id":"bca69803-cb8a-4f6a-90a8-a5686e305271",
             "metadata":{
                 "message_type":null,
                 "model_slug":"text-davinci-002-render-sha",
                 "timestamp_":"absolute"
             },
             "recipient":"all",
             "update_time":null,
             "weight":1
         },
         "parent":"5640b7e3-490c-4c19-ac27-60ce8ad96365"
     },
     "eab6ca6d-647b-43e0-be68-2b99d496e865":{ (6)
         "children":["3b592a2e-2e20-47ed-9fd8-c17337213f00"],
         "id":"eab6ca6d-647b-43e0-be68-2b99d496e865",
         "message":{
             "author":{
                 "metadata":{},
                 "name":null,
                 "role":"assistant"
             },
             "content":{
                 "content_type":"text",
                 "parts":["こんにちは！何かお手伝いできることはありますか？"]
             },
             "create_time":1683031268.994782,
             "end_turn":true,
             "id":"eab6ca6d-647b-43e0-be68-2b99d496e865",
             "metadata":{
                 "finish_details":{
                     "stop":"
",
                     "type":"stop"
                 },
                 "message_type":"next",
                 "model_slug":"text-davinci-002-render-sha"
             },
             "recipient":"all",
             "update_time":null,
             "weight":1
         },
         "parent":"bca69803-cb8a-4f6a-90a8-a5686e305271"
     }
 },
 "moderation_results":[],
 "shouldRefresh":false,
 "title":"手伝いますか？",
 "toneCode":"default",
 "writingStyleCode":"default"
}
~~~

</details>
jsonファイルについては、こちらの記事を参考にさせていただきました。
ページの冒頭部分を読むだけでも理解が深まると思います。


https://qiita.com/Morio/items/7538a939cc441367070d

内容の全てを理解することは難しいですが、"mapping"の各要素は識別用のコードと思われる文字列を持ち、さらにその中に様々な要素を含んでいるようです。
今回使いたい入力された文字列とChatGPTの回答の文字列は、
"mapping"->識別コード(id)->"message"->"content"->"parts"という深いネストで辿ることができ、これで十分だとわかります。

今後の説明のために、mappingの各要素に(1)~(6)の番号を振ります(test.json参照)。

### 2.対話部分の文字列を出力する

ひとまず、文字列を出力するpythonのコードを実行してみます。

~~~sample1.py
import json

with open('test.json', 'rb') as f:
    data = f.read().decode('utf-8')

json_data = json.loads(data)

val=json_data['mapping']
for i in val.keys():
    try:
        a=val[i]['message']['content']['parts']
        print(a)
    except TypeError:
        continue
    except KeyError:
        continue
~~~
val=json_data['mapping']では"mapping"以下の要素を持ちますが、
・(1)のように"message"がnullであるためTypeError
・(4)のように"message"という要素がないためKeyError
 がそれぞれ出るため、例外の出た要素をcontinueでスキップさせています。
  
~~~出力結果1.
['おやすみなさい！良い夢を見てください。']
['おやすみ']
['こんにちは']
['こんにちは！何かお手伝いできることはありますか？']
~~~

以上より、文字列をjsonのデータの順番通りに出力できました。しかし、質問とそれに対する回答の順番としては不自然ですので、これから整理していきます。

### 3.整列された文字列の組をつくる
今回使用する要素の名前を記述します(test.json参照)。
#### create_time
jsonファイルの要素は先ほど述べたように時系列で整列されていません。
create_timeは生成時間を表す要素であると推測できます。

#### role
"mapping"->識別コード(id)->"message"->"author"の要素の中には"role"というものがあり、
ChatGPT側の返答にあたる(2),(6)では"assistant",
~~~
 (2)"message":{
             "author":{
                 "metadata":{},
                 "name":null,
                 "role":"assistant"
             },
~~~
入力に当たる(5)では"user"としているようです。
~~~
(6)"message":{
             "author":{
                 "metadata":{},
                 "name":null,
                 "role":"user"
             },
~~~
しかし、最後の入力に当たるパッセージにあたる要素(3)のみ"author"や"create_time"といった要素が存在しません("role"自体は"message"の直接の要素としてあります)。


#### parentとchildren
質問と回答の一対一の対応関係を表すパラメータとしてもう一つ、"mapping"の直接の要素を表す識別コードに注目してみます。識別コードの名前はその直後の要素である"id"と同じであり、さらに"mapping"には別のidらしきものが格納されている"children"や"parent"といった要素を持っています。

  回答(2)と質問(3),質問(5)と回答(6)をそれぞれ見ると、
・質問の"children"と回答のid,
・回答の"parent"と質問のid
　が一致していることが分かります。よって、質問が親、回答が子、の関係があり、この性質を使うと目的を達成できそうです。


~~~sample2.py
import json

with open('test.json', 'rb') as f:
    data = f.read().decode('utf-8')

json_data = json.loads(data)

val=json_data['mapping']
dict={}

for i in val.keys():
    try:
        if(str(val[i]['message']['author']['role'])=='assistant'):
            d=val[i]['message']['create_time']
            question_id=str(val[i]['parent'])
            answer_str=str(val[i]['message']['content']['parts'])[2:-2]
            question_str=str(val[question_id]['message']['content']['parts'])[2:-2]
            dict[d]=[question_str,answer_str]
    except TypeError:
        continue
    except KeyError:
        continue
sortedDict = sorted(dict.items())
print(sortedDict)
~~~
最後の質問の要素は他と形式が異なるものの、回答の要素の形式は統一されています。したがって、"role"='assist'となる要素のみ取り出せば、親である質問の要素にもアクセスすることができます。
keyにcreate_timeを、valueに質問と回答の文字列を含むlistをもつ辞書型のデータdictを作成し、格納します。そのあと、dictをsortすると時系列順に文字列の組が並びます。
~~~test.py
answer_str=str(val[i]['message']['content']['parts'])[2:-2]
question_str=str(val[question_id]['message']['content']['parts'])[2:-2]
~~~
また、出力結果1のように、文字列には両端の[大カッコ]と'シングルクォーテーション'まで含まれるため、[2,-2]の部分で除外しています。
以下は、sample2.pyの実行結果です。
~~~出力結果2.
[(1683031268.994782, ['こんにちは', 'こんにちは！何かお手伝いできることはありますか？']), (1683031296.493976, ['おやすみ', 'おやすみなさい！良い夢を見てください。'])]
~~~
keyに質問文を,valueに回答をもつdictを作成して一対一の関係で保存したい場合、次のコードを追加して改めてmydictに格納します。
~~~sample3.py
mydict={}
for k,v in sortedDict:
    mydict[v[0]]=v[1]
print(mydict)
~~~
~~~出力結果3.
{'こんにちは': 'こんにちは！何かお手伝いできることはありますか？', 'おやすみ': 'おやすみなさい！良い夢を見てください。'}
~~~
これで、時系列に質問文と回答文を1対1対応したデータを作ることができました。
# 英単語表を作ってみる
次のような対話から、英単語とその日本語訳による表を作成します。
![c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2560987/81b429f8-506f-7412-7c22-8a2613440928.png)
指示文は出力させたくないため、文頭に'*'をつけています。この操作は任意です。

pandasのDataFrameを使って、表を作成します。
~~~sample4.py
import json
import pandas as pd #ライブラリのインポート

with open('ewords.json', 'rb') as f:
    data = f.read().decode("utf-8")

json_data = json.loads(data)

val=json_data['mapping']
dict={}

for i in val.keys():
    try:
        if(str(val[i]['message']['author']['role'])=='assistant'):
            d=val[i]['message']['create_time']
            question_id=str(val[i]['parent'])
            answer_str=str(val[i]['message']['content']['parts'])[2:-2]
            question_str=str(val[question_id]['message']['content']['parts'])[2:-2]
            if(question_str[0]!='*'): #文頭が*であれば追加しない
                dict[d]=[question_str,answer_str]
    except TypeError:
        continue
    except KeyError:
        continue
sortedDict = sorted(dict.items())

mydict={}
for k,v in sortedDict:
    mydict[v[0]]=v[1]

~~~
ここまでは先ほどのsample2.pyとsample3.pyのコードとほとんど変わりません。pandasのインポートと、質問文の文頭に*がついた場合の分岐の追加、不要なprint()の削除のみ行いました。
更に、次のコードを実行します。
~~~sample5.py
ind=[]
lst=[]
for k,v in mydict.items():
    ind.append(k)
    lst.append(v)
df=pd.DataFrame(lst,index=ind,columns=["意味"])
print(df)
~~~
mydictのkey,すなわち質問文をインデックスに、valueである回答文を値にとるDataFrameを作成し、表示させました。
~~~出力結果4.
                                 意味
clock         clockは「時計」という意味になります。
tower          towerは「塔」という意味になります。
ghost         ghostは「幽霊」という意味になります。
head            headは「頭」という意味になります。
silent  silentは「静かな、無言の」という意味になります。
hill            hillは「丘」という意味になります。
~~~
このままだと使いづらいので、csvファイルに出力します。
~~~sample6.py
df.to_csv("test.csv",encoding="shift-jis")
~~~
![スクリーンショット 2023-05-03 101644.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2560987/237647d0-bc14-5540-349f-d3b042df59d7.png)
このように、英単語表が作成できました。

# おわりに
英単語表を作るという目的であれば、テキスト形式で出力して体裁を自動で整えればもっと楽に実装できると書きながら気づきました。それでもテキストにはないパラメーターを理解することができたので、まだ工夫のしようがありそうです。また時間のある時に触ってみようと思います。

最後まで読んでいただきありがとうございました。問題点や改善点があれば指摘していただけますと幸いです。