Pythonでデータベースに解答文字列を格納

# 挨拶
初めまして、日本システム開発株式会社の鈴木です。
技術者として更なる向上を目指すためQiitaアウトプットをする取り組みを行っています。
技術者としては経験が浅く発信内容はとにかく試したものの覚書になります。
本稿は資格試験学習支援システムの作成についての内容です。
# 環境
この記事では以下の環境を用いています。
・Python3.8.1
・pdfminer.six 20220524
・MySQL Ver 8.0.29
# 概要
　[以前の記事](https://qiita.com/f-suzuki/items/d861caf8ca9232c991fe)でpdfから基本情報技術者試験午前解答を文字の配列として抽出するシステムを実装しました。
本記事では上システムに解答文字列をデータベースに格納する機能を実装します。

# 使用DB構造
　まず使用するテーブを定義します。今回はどの問題かを特定するための問題IDと解答となるカタカナ（ア,イ,ウ,エのいずれか）を保持します。
問題IDは`開催年_季節(FallまたはSpring)_FE_q問題番号`を付与することにします。以下に今回使用したテーブルの作成SQLを示します。
```sql:am_ans.sql
CREATE TABLE am_ans(
	id varchar(50) NOT NULL PRIMARY KEY,
	ans varchar(2)
);
```

# DB操作処理の実装
続いて、Pythonにより実際にDBへデータを挿入する処理を実装します。
新たに基本情報技術者試験用のデータを管理するDBに接続するクラスを定義し、そこに先ほど定義したam_ansテーブルへデータを挿入するメソッドを実装します。
```python:dbManager.py
import mysql.connector


# 基本情報技術者試験用のDB接続クラス
class dbManagerFE:
    def __init__(self):
        self.cnx = None # インスタンス変数で接続を保持
        try:
            self.cnx = mysql.connector.connect(
                user="user",  # ユーザー名
                password="pass",  # パスワード
                host="localhost",  # ホスト名(IPアドレス）
            )
        except Exception as e:
            print(f"Error Occurred: {e}")


    # 基本情報技術者試験午前問題の答えをfe_study.am_ansに挿入
    # exam_id:試験ID（文字列）
    # ans　　:解答行列(文字列の配列)
    def insertAmAns(self, exam_id, ans):

        # DBに読み込ませるSQL文とそのためのデータ組を用意
        data = []
        for cnt, e in enumerate(ans):
            data.append((exam_id + "q" + str(cnt + 1), e))  # (問題ID, 解答)のタプルを生成

        sql = """
        INSERT INTO fe_study.am_ans
            (id,ans)
        VALUES
            (%s, %s)
        """

        # DBにデータを挿入
        cursor = self.cnx.cursor()
        cursor.executemany(sql, data)
        self.cnx.commit()
        print(cursor.fetchall())

        cursor.close()


    def __del__(self):  # デコンストラクタ
        if self.cnx is not None and self.cnx.is_connected():
            self.cnx.close()

```
　生成時にMySQLへの接続を行いインスタンス変数としてその情報を保持します。ここで生成した接続はデコンストラクタで削除するようにします。
　実際にデータをDBに挿入するのは`insertAmAns(self, exam_id, ans)`メソッドです。前半部分では呼び出し元から渡された解答文字の配列ansを各解答とそれに対応するIDのタプルの配列に変換します。IDは渡された`開催年_季節(FallまたはSpring)_FE`に`_q`と配列のインデックスを付与することで生成します。実行するSQL文では配列を用いることを前提に記述します。また、配列を用いるためexecute()ではなくexecutemany()を用います。
# データ挿入処理の追加
　最後に、今回実装したdbManagerFEクラスを解答読み取りシステムに追加します。
```python:am_ans.py
from pdfminer.pdfinterp import PDFResourceManager  # PDFのテキストや画像を管理するクラス
from pdfminer.layout import LAParams  # テキスト抽出に必要なパラメータを設定するクラス
from pdfminer.converter import TextConverter  # PDFのテキストを抽出するためのクラス
from pdfminer.pdfpage import PDFPage  # PDFの1ページごとの情報を管理するクラス
from pdfminer.pdfinterp import PDFPageInterpreter  # PDFpageから必要な処理を行うクラス
from io import StringIO
import dbManager

# 応用情報試験解答の文章を入力し問題番号がインデックスとなる解答配列を返す
def extract_ap_ans(pdf_str):
    ans = []
    pdf_str_arry = pdf_str.split()  # 解答はスペース区切りになっているためスペースで分割
    for idx in range(len(pdf_str_arry)):
        if pdf_str_arry[idx] == "問":
            ans.append(pdf_str_arry[idx + 2])  # 「問 番号 解答選択肢」のセットなため「問」の２要素後ろを抽出

    return ans


if __name__ == "__main__":
    FILENAME = "2021r03a_ap_am_ans.pdf"
    EXAMID = "2021_Fall_FE"

    # pdfminerはバイナリモードで読み込む必要がある
    fp = open(FILENAME, "rb")

    # 読みだすテキストの出力先（今回は文字ストリーム）
    outIO = StringIO()

    # PDFminerオブジェクトを生成　ここから
    rmgr = PDFResourceManager()  # PDFResourceManagerオブジェクトの取得
    txtcvt = TextConverter(
        rmgr, outIO, laparams=LAParams()
    )  # TextConverterにリソースマネージャと出力先ストリームを指定
    iprtr = PDFPageInterpreter(
        rmgr, txtcvt
    )  # PDFPageInterpreterにリソースマネージャと使用するテキストコンバータを指定
    # PDFminerオブジェクトを生成　ここまで

    # PDFファイルから1ページずつ解析(テキスト抽出)処理する
    for page in PDFPage.get_pages(fp, maxpages=3):
        iprtr.process_page(page)
        ans = extract_ap_ans(outIO.getvalue())
        print(ans)

    outIO.close()  # 出力用I/Oストリームを閉じる
    txtcvt.close()  # TextConverterオブジェクトの解放
    fp.close()  # Fileストリームを閉じる

    # DBへの挿入処理
    dbManagerFE = dbManager.dbManagerFE()
    dbManagerFE.insertAmAns(EXAMID, ans)
```

追加したのは最後の２行になります。以前の処理で解答文字の配列`ans`を生成しているためそれを先ほどのdbManagerFE.insertAmAns()に渡します。

# 実行結果
　am_ans.pyを実行した後のfe_study.am_ansテーブルが以下になります。
```
mysql> SELECT * FROM fe_study.am_ans;
+-----------------+------+
| id              | ans  |
+-----------------+------+
| 2021_Fall_FEq1  | エ   |
| 2021_Fall_FEq10 | イ   |
| 2021_Fall_FEq11 | ア   |
| 2021_Fall_FEq12 | イ   |
| 2021_Fall_FEq13 | エ   |
| 2021_Fall_FEq14 | ア   |
| 2021_Fall_FEq15 | エ   |
| 2021_Fall_FEq16 | ア   |
| 2021_Fall_FEq17 | ア   |
| 2021_Fall_FEq18 | ウ   |
| 2021_Fall_FEq19 | ア   |
| 2021_Fall_FEq2  | エ   |
| 2021_Fall_FEq20 | ア   |
| 2021_Fall_FEq21 | ア   |
| 2021_Fall_FEq22 | ア   |
| 2021_Fall_FEq23 | イ   |
| 2021_Fall_FEq24 | エ   |
| 2021_Fall_FEq25 | ウ   |
| 2021_Fall_FEq26 | エ   |
| 2021_Fall_FEq27 | イ   |
| 2021_Fall_FEq28 | イ   |
| 2021_Fall_FEq29 | イ   |
| 2021_Fall_FEq3  | ウ   |
| 2021_Fall_FEq30 | エ   |
| 2021_Fall_FEq31 | ア   |
| 2021_Fall_FEq32 | ア   |
| 2021_Fall_FEq33 | エ   |
| 2021_Fall_FEq34 | イ   |
| 2021_Fall_FEq35 | エ   |
| 2021_Fall_FEq36 | イ   |
| 2021_Fall_FEq37 | イ   |
| 2021_Fall_FEq38 | ウ   |
| 2021_Fall_FEq39 | ア   |
| 2021_Fall_FEq4  | ア   |
| 2021_Fall_FEq40 | ウ   |
| 2021_Fall_FEq41 | ア   |
| 2021_Fall_FEq42 | ア   |
| 2021_Fall_FEq43 | ア   |
| 2021_Fall_FEq44 | イ   |
| 2021_Fall_FEq45 | エ   |
| 2021_Fall_FEq46 | イ   |
| 2021_Fall_FEq47 | ア   |
| 2021_Fall_FEq48 | ウ   |
| 2021_Fall_FEq49 | ア   |
| 2021_Fall_FEq5  | ウ   |
| 2021_Fall_FEq50 | イ   |
| 2021_Fall_FEq51 | エ   |
| 2021_Fall_FEq52 | イ   |
| 2021_Fall_FEq53 | イ   |
| 2021_Fall_FEq54 | エ   |
| 2021_Fall_FEq55 | ウ   |
| 2021_Fall_FEq56 | イ   |
| 2021_Fall_FEq57 | エ   |
| 2021_Fall_FEq58 | ウ   |
| 2021_Fall_FEq59 | イ   |
| 2021_Fall_FEq6  | イ   |
| 2021_Fall_FEq60 | ウ   |
| 2021_Fall_FEq61 | ア   |
| 2021_Fall_FEq62 | ウ   |
| 2021_Fall_FEq63 | ア   |
| 2021_Fall_FEq64 | イ   |
| 2021_Fall_FEq65 | イ   |
| 2021_Fall_FEq66 | エ   |
| 2021_Fall_FEq67 | ア   |
| 2021_Fall_FEq68 | イ   |
| 2021_Fall_FEq69 | エ   |
| 2021_Fall_FEq7  | ウ   |
| 2021_Fall_FEq70 | ア   |
| 2021_Fall_FEq71 | エ   |
| 2021_Fall_FEq72 | イ   |
| 2021_Fall_FEq73 | ア   |
| 2021_Fall_FEq74 | ウ   |
| 2021_Fall_FEq75 | ア   |
| 2021_Fall_FEq76 | ウ   |
| 2021_Fall_FEq77 | ア   |
| 2021_Fall_FEq78 | ア   |
| 2021_Fall_FEq79 | エ   |
| 2021_Fall_FEq8  | エ   |
| 2021_Fall_FEq80 | ア   |
| 2021_Fall_FEq9  | エ   |
+-----------------+------+
```
解答が格納されていることがわかります。