[python][ChatGPT]ChatGTPで臨床検査技師国家試験演習Webサイトを作成

ChatGPT

経緯

ひょんなことから臨床検査学科で授業を持つことになり、いい機会だと思って臨床検査技師の国家試験を勉強→せっかくなので問題演習をできるサイトを作成。

結果

臨床検査技師国家試験演習
臨床検査技師国家試験のためのオンライン演習サイト。過去の試験問題を使用した試験のシミュレーションを提供し、あなたの試験対策をサポートします。

デザインもクソもない無骨な感じになってしまったが、なんとか形にはできた。4年分の過去問題・解答・解説を掲載。後から気がついたけれど、同じようなサイトはいくらでもある感じもしてきた・・・。

経緯(詳細)

ChatGPTで一瞬で出来ました、と言いたいところだけど、結構大変だった。備忘録代わりにある程度つまんで経緯を残しておきたいと言うのが本音。

はじまり

こちらが一番はじめの対話ログ。素人丸出し感。

一番最初のログ。

とりあえず最初のhtml, css, javascriptは動く。

PDFから文章をスクレイピング

臨床検査技師国家試験は厚生労働省で全部公開されている。https://www.mhlw.go.jp/seisakunitsuite/bunya/kenkou_iryou/iryou/topics/tp190415-07.html

しっかりとpdfに文章も埋め込まれているので、これをPythonコードでテキスト化。最初はPyPDF2を使っていたが、自分の環境だと日本語の精度が今ひとつだったので、pdfplumberを採用。

スクレイピングに関しては以前に色々調べておいて、ある程度役に立った。
PythonによるWebスクレイピング
PythonでのスクレイピングとCSVファイルへ書き込み

また、pdfからのテキスト抽出に関しても、色々調べておいて敷居が下がっていたのも幸い。
PythonでPDFからテキストを抽出する方法

抽出コードはこちら。

import pdfplumber
import re
import json

# Extract text from pdf
def extract_text_from_pdf(pdf_path, skip_first_pages=True, start_page_number=5):
    with pdfplumber.open(pdf_path) as pdf:
        extracted_text = ""

        start_page = start_page_number - 1 if skip_first_pages else 0
        for page in pdf.pages[start_page:]:
            extracted_text += page.extract_text()

    return extracted_text

# read pdf file
pdf_path = "/Users/hogehoge/Documents/LabDepartment/problem/065/065_01.pdf"
extracted_text_01 = extract_text_from_pdf(pdf_path)
pdf_path = "/Users/hogehoge/Documents/LabDepartment/problem/065/065_02.pdf"
extracted_text_02 = extract_text_from_pdf(pdf_path)

# check extracted text
print(extracted_text_01)
print(extracted_text_02)

# remove page info
def remove_page_info(text):
    pattern = r"DKIX07(午前|午後)H\.smd Page \d+ 21/12/22 15:41 v4\.00"
    cleaned_text = re.sub(pattern, '', text)
    return cleaned_text

# replace cid numbers
def replace_cid_numbers(text):
    def replace_cid(match):
        cid_number = int(match.group(1))
        if cid_number == 139:
            return "("
        elif cid_number == 140:
            return ")"
        else:
            return str(cid_number)

    pattern = r'\(cid:(\d+)\)'
    replaced_text = re.sub(pattern, replace_cid, text)
    return replaced_text

# replace text with parentheses
def replace_text_with_parentheses(text):
    def replace_parentheses(match):
        if match.group(1).isalpha():
            return f"{match.group(1)}({match.group(2)})"
        else:
            return match.group(0)

    pattern = r'(\w)0(.*?)2'
    replaced_text = re.sub(pattern, replace_parentheses, text)
    return replaced_text

# remove single digit lines
def remove_single_digit_lines(text):
    pattern = r'^\d+\n'
    cleaned_text = re.sub(pattern, '', text, flags=re.MULTILINE)
    return cleaned_text

# remove dkix lines
def remove_dkix_lines(text):
    lines = text.split("\n")
    filtered_lines = [line for line in lines if "DKIX" not in line]
    return "\n".join(filtered_lines)

# final process
def process_text(text):
    text_without_page_info = remove_page_info(text)
    tex_without_cid = replace_cid_numbers(text_without_page_info)
    text_without_parentheses = replace_text_with_parentheses(tex_without_cid)
    text_no_single_digit_lines = remove_single_digit_lines(text_without_parentheses)
    final_text = remove_dkix_lines(text_no_single_digit_lines)
    return final_text

processed_text_01 = process_text(extracted_text_01)
processed_text_02 = process_text(extracted_text_02)

# check text after removal
print(processed_text_01)
print(processed_text_02)

# extract for JSON files
def extract_questions_and_choices(text):
    lines = text.split("\n")
    questions = []
    question_text = ""
    choices = []

    for line in lines:
        # 問題番号を検出
        if re.match(r"\d+ ", line):
            if question_text:
                questions.append({
                    "number": int(question_number),
                    "text": question_text.strip(),
                    "choices": choices
                })
                choices = []
            question_number, question_text = line.split(" ", 1)

        # 選択肢を検出
        elif re.match(r"\d+.", line):
            choice = re.sub(r"\d+.", "", line).strip()
            choices.append(choice)

        # 問題文が複数行に渡る場合
        elif question_text and not choices:
            question_text += " " + line.strip()

    # 最後の問題を追加
    if question_text:
        questions.append({
            "number": int(question_number),
            "text": question_text.strip(),
            "choices": choices
        })

    return questions

json_data_01 = extract_questions_and_choices(processed_text_01)
json_data_02 = extract_questions_and_choices(processed_text_02)

json_data_01 = sorted(json_data_01, key = lambda x:x["number"])
json_data_02 = sorted(json_data_02, key = lambda x:x["number"])

# write to JSON
with open("/Users/hogehoge/Documents/LabDepartment/problem/065/questions_065_01.json", "w", encoding="utf-8") as outfile:
    json.dump(json_data_01, outfile, indent=2, ensure_ascii=False)
with open("/Users/hogehoge/Documents/LabDepartment/problem/065/questions_065_02.json", "w", encoding="utf-8") as outfile:
    json.dump(json_data_02, outfile, indent=2, ensure_ascii=False)

一気にJSONファイルにしようとするよりは、抽出テキストを一つ一つ見ながら体裁を整えていくという形になった。解答の抽出に関しては省略。

ローカル環境でのテスト

ここまででhtmlとcss, javascriptと問題のJSONファイルができたので、ローカル環境などで表示の仕上がりを確認。最初はGitHub Pagesで作成しようとしたところ、なかなかうまく行かない。具体的には、javascriptがうまく動かない。GitHub pagesではfetch APIを利用してJSONなどのローカルファイルを取得しようとすると、CORS (Cross-Origin Resouce Sharing)の制約によりエラーが発生することがあるとのこと。であるが、ローカル環境でもうまく動かないので、そもそもjavascriptを使わない方がいいのではと考え始める(ここが一番時間がかかって大変だった)。

Flaskの利用

色々調べているうちに、FlaskがPythonの軽量なウェブフレームワークということがわかり、これを採用。いままでのjavascriptを捨て、手慣れたpythonに変更。routingが便利!

とりあえずある年度の午前・午後の問題だけでも形にできれば、後は同じだと思い、ChatGPTちゃんと何回か対話しながら作っていく。人間と違って、何回聞いても怒らないし、わからないところも嫌がらずに教えてくれる。

将来AIが知能を持った時のために、丁寧語で会話を重ねる。

ちなみに、GTP4とGTP3.5は能力が桁違いなので、お金を払ってでもGPT4を利用した方がいい。

Webサイト立ち上げ

ある年度の問題、解答が出来てきたので、Webサイトを作成し公開。使い慣れたXserverで(このWebサイトもXserverで運営)。参考になったのは、ChatGTPよりも以下のふたつのWebサイト。
https://dattesar.com/xserver-pip-flask/
https://codeaid.jp/webapp-xserver/

特にパーミッションの設定をしっかり行わないと、エラーがでて動かない(index.cgiなどを書き換えてファイルサーバーソフトで再アップロードしたときは、設定が変更されてしまい正しいコードでも動かなくなるときが多い)。

また、注意としては作成したドメイン(この場合だと”lab-exam-app.com”)の”public_html”直下を色々変更するということ。ここにある”.htaccess”や”index.cgi”、あるいは”index.html”などを書き換え・あるいは置き換えていく。

解説の作成

せっかく問題と解答があるので、解説も出力したい。最初はJSONファイルは

{
    "number": 1,
    "text": "内部精度管理法で管理血清を用いるのはどれか。<b>2つ選べ。</b>",
    "choices": [
      "|R/<span style= 'text-decoration: overline;'>X</span>|管理法",
      "<span style= 'text-decoration: overline;'>X</span>-R管理図法",
      "項目間チェック法",
      "デルタチェック法",
      "マルチルール管理図法"
    ] 
 },

という形式だったが、解説も追加。

{
    "number": 1,
    "text": "内部精度管理法で管理血清を用いるのはどれか。<b>2つ選べ。</b>",
    "choices": [
      "|R/<span style= 'text-decoration: overline;'>X</span>|管理法",
      "<span style= 'text-decoration: overline;'>X</span>-R管理図法",
      "項目間チェック法",
      "デルタチェック法",
      "マルチルール管理図法"
    ],
    "explanation": "<br>選択肢2のX-R管理図法と選択肢5のマルチルール管理図法は、管理血清を使用する内部精度管理法です。<br><br>2. X-R管理図法:これは内部品質管理に一般的に使用される方法で、平均(X)と範囲(R)をプロットしたグラフを使用します。管理血清は、一定の期間にわたって繰り返し測定され、その結果は管理図に記録されます。制御限界は統計的に計算され、測定結果が制御限界内に収まるかどうかを確認します。これにより、分析システムの精度と精度が維持されているかどうかがモニタリングされます。<br><br>5. マルチルール管理図法:これは「ウェスガードルール」とも呼ばれ、複数の統計的ルールを用いて管理血清の結果を解釈します。具体的なルールには、1 2s、2 2s、R 4s、4 1s、10 Xなどがあり、これらは異常な結果を示すために用いられます。これらのルールは、システムの偏りやランダムなエラーを特定するのに有効です。この方法でも、管理血清が定期的に分析され、その結果はモニタリングと評価のために使用されます。<br>"
  },

一つ一つ解説を打ち込むのは現実的でないので、ここでもChatGPTちゃんにご登場いただく。APIで一気に解説を出力しようと思い、実際に出来たが、どうもGTP3.5だと解説の精度が悪い・・・。改行せず1行で出力しろ、という命令やhtmlの<br>タグを適宜挿入しろ、という命令も守ってくれない。一応GTP4のAPIも申し込んだけれど、未だに連絡はない。仕方がないので、以下のようなpromptを作成し、一つ一つ解説を作成。

一応医学的に誤りがあるとまずいので、内容は確認。

ここらへんは、問題数も1年で200問程度(医師国家試験だと自分が受験した時はAからHブロックまであり、3日間で合計500問くらい出題されていたと思う。今は日程2日になったらしい)で、しこしこと空いた時間に入力していく。GTP4は3時間で25回の利用制限があるので、そんなに一気には進められなかった。

感想

やってみるのこういうのは面白かった。色々と勉強(臨床検査技師国家試験の問題だけでなく、Webサイトの作り方とかも)になることも多かった。JSONとかpdfからの文章抽出も、まったく0からではなくて1回でも触れたことがあるというのが大きかったと思う。後、臨床検査技師国家試験は毎年何番から何番までが公衆衛生の問題、などというように出題分野が固定されているので、分野別の出題も作成したいと思っている。

*注:上記Webサイトは個人の作成したものであり、所属する組織とは一切関係ありません。

関連リンク・関連記事

https://lab-exam-app.com/
JSONファイルの読み込みについて

コメント

タイトルとURLをコピーしました