応用情報技術者で出題される単語を、自分の言葉で説明できるようにする

## 挨拶

こんにちは！現在、社内SEをしている20代社会人です。
今回が初投稿となりますので、色々と内容が疎かだったり、見づらかったするかもしれませんが、ご了承ください。

今回私が作成したのは、タイトルにもある通り、「応用情報技術者試験」の午前の４択問題で出題されるワードを自分の言葉で説明できるようにするためのWEBアプリです。

このWebアプリを作ろうとした背景ですが、去年の１２月に「基本情報技術者」に合格しまして、その勢いで応用情報技術者もついでに取得を目指すことにしました。
午前問題の対策として、よく出題されるワードを自分の言葉で説明できればスムーズに回答できて得点アップにも繋がり、その後の午後問題や、更にその上のネスペなど資格取得に役立つかなと思い作ってみた次第です。

~~本当は10月中旬に初更新をしたかったのですが、初めての一人暮らし(引っ越し、家具の選定、役所の手続き等…)や、基本情報技術者の勉強、同時期に自作パソコンの故障(hdd,ssd,マザーボード,電源の4つが同時に壊れるという異常事態が発生)など諸々の事情が重なりこの時期になりました…~~


## 具体的な構造
1\. [IT用語辞典](https://e-words.jp/p/s-ranking.html "IT用語辞典")のアクセスランキングの単語を全て取得し、sqliteに保存。
2\. 単語をランダムで一つ呼び出し、その単語に対する説明をユーザーが入力して、openAIに噛ませる。
3\. openAIによる結果を、下記3つの観点から判定して返す
- 正確性
- 不足している点や改善点について具体的の説明
- 総合的な評価コメント

## 具体的なコード

※ やっている内容を大まかに記載していますので、参考にしてください


 #### [IT用語辞典](https://e-words.jp/p/s-ranking.html "IT用語辞典")から単語一覧を取り出す
 

- bs4でhtmlを解析し、その中のaタグに囲まれた単語を取得
- ap_question_dbというDBの中に、it_wordテーブルが存在しており、その中に番号と単語を追加
 
```python
import httpx
from bs4 import BeautifulSoup
import sqlite3

class GetItDictionary:
    def __init__(self):
        self.url = 'https://e-words.jp/p/s-ranking.html'

    
    def get_it_word(self) -> dict:
        try:
            response = httpx.get(self.url, follow_redirects=True)
            response.raise_for_status()

            soup = BeautifulSoup(response.text, 'html.parser')
            title_dict = {}
            for index, title in enumerate(soup.find_all('a', title=True), start=1):
                title_dict[index] = title['title']
            print(title_dict)

        except httpx.RequestError as e:
            print(f"リクエストエラー: {e}")
        except httpx.HTTPStatusError as e:
            print(f"HTTPエラー: {e}")
                
        return title_dict


    def save_to_db(self, it_word):
        conn = sqlite3.connect(r'/your/path/to/ap_question_db.db')
        cursor = conn.cursor()

        cursor.execute('''
                CREATE TABLE IF NOT EXISTS it_word (
                    id INTEGER PRIMARY KEY,
                    word TEXT UNIQUE
                )
            ''')


        cursor.executemany('''
            INSERT OR REPLACE INTO it_word (id, word) VALUES (?, ?)
        ''', [(key, value) for key, value in it_word.items()])


        # コミットして接続を閉じる
        conn.commit()
        conn.close()
        print("データベースへの挿入が完了しました！")

        return


def main():
    get_it_dictionary = GetItDictionary()
    it_word = get_it_dictionary.get_it_word()
    get_it_dictionary.save_to_db(it_word)
    return

if __name__ == "__main__":
    main()
```

---------------------------------------

#### DBからランダムで一つ単語を受け取り、ユーザーが単語を回答。その単語と内容をopenAIに渡して結果を受け取る

- dbから全ての単語を受け取り、その中から一つの単語をランダムに一つ取り出す
- 単語をユーザーに渡して単語の回答を受け取る
- 単語と回答をopenAIに渡して、その結果を返す


※ inputの記述がありますが、reactとFlaskを使用し、UIで直接データのやりとりをしているため、実際には使用していません…

```python
import openai
import sqlite3
import os
import random

from dotenv import load_dotenv

load_dotenv()

class QuestionGenerator:
    def __init__(self):
        self.ai_api_key = os.getenv('openai_api_key')
        pass

    def get_random_word_from_db(self):
        try:
            conn = sqlite3.connect(r'/your/path/to/ap_question_db.db')
            cursor = conn.cursor()
            cursor.execute('SELECT id from it_word')
            all_ids = [row[0] for row in cursor.fetchall()]

            if not all_ids:
                return None
            
            random_id = random.choice(all_ids)

            cursor.execute("SELECT word FROM it_word WHERE id = ?", (random_id,))
            result = cursor.fetchall()
            print(result[0][0])

            return result[0][0] if result else None

        except sqlite3.Error as e:
            print(f"データベースエラー: {e}")
            return None
        
        finally:
            conn.close()


    def check_word_definition(self, question_word):
        text = input(f'次の単語について説明しなさい : {question_word} => ')
        print(text)
        return question_word, text

    def check_explanation_accuracy(self, question_word, user_answer):
        openai.api_key = self.ai_api_key

        prompt = f'''
        あなたは応用情報技術者試験の資格を持った講師です。
        以下の単語に対する生徒の回答を評価してください。
        問題: {question_word}
        生徒の回答: {user_answer}

        評価基準:
        1. 正確性（0〜100%でスコアリングしてください）。
        2. 不足している点や改善点について具体的に説明してください。
        3. 最終的に、総合的な評価コメントを与えてください。
        '''
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": "あなたは試験官です。"},
                {"role": "user", "content": prompt}
            ]
        )
        print(response.choices[0].message.content)

        return response.choices[0].message.content



    def main(self):
        while True:
            random_word = self.get_random_word_from_db()
            if not random_word:
                print("データベースから単語を取得できませんでした。")
                continue

            question_word, user_answer = self.check_word_definition(random_word)
            if user_answer is exit:
                print("終了します。")
                break

            self.check_explanation_accuracy(question_word, user_answer)
        return

if __name__ == "__main__":
    question_generator = QuestionGenerator()
    question_generator.main()
```
---------------------------------------

#### Flaskで単語のデータやユーザーが入力した回答、openAIの評価などを受け渡しする。


- 「QuestionGenerator」Classを呼び出し、get_random_word_from_dbを使ってランダムな単語を取得する
- 「QuestionGenerator」Classを呼び出し、check_explanation_accuracyを使って単語の意味をopenAIに解説させる

```python
from flask import Flask, jsonify, request
from flask_cors import CORS
import sqlite3
import random
from create_ap_puestion import QuestionGenerator

app = Flask(__name__)
CORS(app)


@app.route("/api/get-word", methods=["GET"])
def get_random_word():
    question_generator = QuestionGenerator()
    word = question_generator.get_random_word_from_db()
    if word:
        return jsonify({"word": word})
    else:
        return jsonify({"word": "データなし"}), 404
    
    
@app.route("/api/save-word", methods=["POST"])
def save_word():
    data = request.get_json()
    word = data.get("word")
    description = data.get("description")

    if not word:
        return jsonify({"message": "単語が提供されていません"}), 400
    print(f"受信した単語: {word}")
    print(f"ユーザーの説明: {description}")

    question_generator = QuestionGenerator()
    result = question_generator.check_explanation_accuracy(word, description)
    
    response_data = {
        "processed_word": word,
        "processed_description": result
    }

    return jsonify(response_data), 200


if __name__ == "__main__":
    app.run(debug=True, port=5000)

```
---------------------------------------

#### Reactを用いて実際のUi画面を表示させる


```react
import { useState } from "react";

function App() {
  const [word, setWord] = useState<string>("");
  const [processedWord, setProcessedWord] = useState<string>(""); // Flaskからの処理結果
  const [processedDescription, setProcessedDescription] = useState<string>("");

  const fetchWord = async () => {
    try {
      const response = await fetch("http://localhost:5000/api/get-word");

      if (!response.ok) {
        throw new Error(`HTTPエラー: ${response.status}`);
      }

      const data = await response.json();
      console.log("取得した単語:", data);
      setWord(data.word);
    } catch (error) {
      console.error("単語の取得に失敗しました", error);
      setWord("エラーが発生しました");
    }
  };

  const sendWord = async (userInput: string) => {
    if (!word) {
      alert("単語を取得してください");
      return;
    }

    if (!userInput) {
      alert("入力内容がありません");
      return;
    }

    try {
      const response = await fetch("http://127.0.0.1:5000/api/save-word", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ word, description: userInput })
      });

      const data = await response.json();
      console.log("Flaskからのレスポンス:", data);

      // データが存在する場合のみ state を更新
      setProcessedWord(data.processed_word || "データなし");
      setProcessedDescription(data.processed_description || "データなし");

    } catch (error) {
      console.error("単語の送信に失敗しました", error);
    }
  };

  return (
    <div style={{ textAlign: "center", marginTop: "50px" }}>
      <h1>ITワード説明学習ツール</h1>
      <button onClick={fetchWord} style={{ padding: "10px 20px", fontSize: "16px", cursor: "pointer" }}>
        単語を取得
      </button>
      <p>あなたが説明する単語 : {word || "まだありません"}</p>

      {/* FreeTextInput に word と sendWord を渡す */}
      <FreeTextInput word={word} onSendWord={sendWord} />

      {processedWord && (
        <div style={{ marginTop: "20px", padding: "10px", border: "1px solid gray" }}>
          <h2>Open AIからの処理結果</h2>
          <p>処理された単語: {processedWord}</p>
          <p>処理された説明: {processedDescription}</p>
        </div>
      )}
    </div>
  );
}

// FreeTextInputのPropsに word と onSendWord を追加
type FreeTextInputProps = {
  word: string;
  onSendWord: (inputText: string) => void;
};

const FreeTextInput: React.FC<FreeTextInputProps> = ({ word, onSendWord }) => {
  const [text, setText] = useState("");

  return (
    <div style={{ marginTop: "20px" }}>
      <label>回答記入欄</label>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="自由に入力してください"
        style={{ marginLeft: "10px", padding: "5px" }}
      />
      <p>入力内容: {text}</p>

      {/* 送信ボタンで受信単語とユーザー入力を一緒に送信 */}
      <button onClick={() => onSendWord(text)} style={{ padding: "10px 20px", fontSize: "16px", cursor: "pointer" }}>
        単語を送信
      </button>
    </div>
  );
};

export default App;


```


---------------------------------------

## 改善点


- 回答を送信してから、応答されるまでの時間が長い
- ボタンを押した際、実際に動いているか視覚的にわからない
- 画面に画面リセットボタンがない
- IT辞典ではなく、[応用状技術者ドットコム](https://www.ap-siken.com/s/keyword/ "応用状技術者ドットコム")に、主要な単語がまとめてあったのを、上司から教えてもらったので、そっちを使って単語収集をする


## 総括
作ってみた感じ、思いの外いい感じに動作している。多分gpt3.5でも良さげな感じがしている(そっちのほうが利用料金安いしね！)
あとは、改善点を修正すればだいぶいい感じに動作すると思うから、もう少し頑張ってみる(´ε｀ )