PDFMiner(Python)で応用情報試験の解答PDF取り込み

# 挨拶
初めまして、日本システム開発株式会社の鈴木です。
技術者として更なる向上を目指すためQiitaアウトプットをする取り組みを行っています。
技術者としては経験が浅く発信内容はとにかく試したものの覚書になります。
本稿は資格試験学習支援システムの作成についての内容です。

# 環境
この記事では以下の環境を用いています。
・Python3.8.1
・pdfminer.six 20220524
# 前文
資格取得勉強を行う際に資格運営団体が配布している過去問は便利なものです。しかし、過去問はPDFファイルで展開されることがあります。PDFファイルは文書の閲覧には便利なものの書き込む必要のある問題集としては使いづらい点が目立ちます。それならばシステムに取り込み簡単な出題システムを作ろうとなってもテキストファイルやcsvファイルほど読み込みやすい形式ではありません。
そこで本稿ではPythonとそのライブラリであるpdfminer.sixを用いて応用情報技術者試験の解答データを配布されているPDFから取り込みます。

# 本文
今回作成したシステムは以下のPDFを入力とし
![2021r03a_ap_am_ans_pdf.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641357/caf0f5c9-a840-89d3-a476-baf89f8a823b.png)
以下のような配列を出力とするようなシステムになります。
```出力
['エ', 'エ', 'ウ', 'ア', 'ウ', 'イ', 'ウ', 'エ', 'エ', 'イ', 'ア', 'イ', 'エ', 'ア', 'エ', 'ア', 'ア', 'ウ', 'ア', 'ア', 'ア', 'ア', 'イ', 'エ', 'ウ', 'エ', 'イ', 'イ', 'イ', 'エ', 'ア', 'ア', 'エ', 'イ', 'エ', 'イ', 'イ', 'ウ', 'ア', 'ウ', 'ア', 'ア', 'ア', 'イ', 'エ', 'イ', 'ア', 'ウ', 'ア', 'イ', 'エ', 'イ', 'イ', 'エ', 'ウ', 'イ', 'エ', 'ウ', 'イ', 'ウ', 'ア', 'ウ', 'ア', 'イ', 'イ', 'エ', 'ア', 'イ', 'エ', 'ア', 'エ', 'イ', 'ア', 'ウ', 'ア', 'ウ', 'ア', 'ア', 'エ', 'ア']
```


まずソースコードは以下になります。
```python:ap_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

#応用情報試験解答の文章を入力し問題番号がインデックスとなる解答配列を返す
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__':
    #pdfminerはバイナリモードで読み込む必要がある
    fp = open("2021r03a_ap_am_ans.pdf", '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ストリームを閉じる

```
このプログラムでは同階層にある2021r03a_ap_am_ans.pdfを読み込み問題番号がインデックスである解答配列を出力します。
処理の流れとしては
①pdfminerによりpdfからテキスト情報を読み出す
②テキストを解析して解答配列に変換
の２段階になります。
## pdfからテキスト情報を読み出す
>①pdfminerによりpdfからテキスト情報を読み出す

この処理はほとんどがpdfminerを利用するための処理になります。
まず実際にテキスト情報を読みだしているのは`iprtr.process_page(page)`です。
iprtrはPDFPageInterpreterクラスのインスタンスでPDFファイルの１ページごとに処理を行うためのインスタンスです。これのprocess_pageメソッドに解析対象のページデータを保持したPDFPageクラスのインスタンスを渡すことで解析をしてくれます。上プログラムでは`for page in PDFPage.get_pages(fp, maxpages = 3):`で解析対象のPDFPageインスタンスを生成しています。
ここでprocess_pageメソッドの引数には解析方法の指定（文章だけ取り出すのか画像を取り出すのかなど）はありません。それらはPDFPageInterpreterインスタンスを生成する際に指定します。`iprtr = PDFPageInterpreter(rmgr, txtcvt)`がそれです。生成時にPDF解析時にリソースを管理するためのPDFResourceManagerと解析方法を保持しているTextConverterのインスタンスを設定しています。
TextConverterで指定できるのはPDFResourceManager、出力先となるIO、テキスト読み込み時のパラメータ（空白は何ミリまで無視する、何ミリ以上離れたら別の行とするなど）を設定するLAParamsの３つです。
`TextConverter(rmgr, outIO, laparams = LAParams())`では出力先にPython標準の文字入出力ストリームであるStringIOのインスタンスを、パラメータは初期値のものを指定しています。
まとめると、
1. PDFリソースを管理するためのインスタンスを生成
2. 文章抽出のための情報を保持したインスタンスを生成
2. 2で作成した文章抽出ルールを設定した解析クラスのインスタンスを生成
2. 3で作成した解析クラスに解析して欲しいPDFのページデータを入力

という流れになります。

# 解答配列に変換
```python:解答配列変換部
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
```
解答配列変換部分は読み込みたいファイルに合わせて読み込んだテキストから情報を抽出します。解答PDFは文章で抽出した場合以下の様になります。
```
令和 3 年度  秋期  応用情報技術者試験  解答例 

午前試験
問番号  正解  分野
問 1  エ  Ｔ
問 2  エ  Ｔ
問 3  ウ  Ｔ
問 4  ア  Ｔ
.
.
.
```
いずれも空白区切りになっていることに注目しsplit()メソッドで配列化します。配列にすると内容が[「問」「数字」「解答」「分野」]の繰り返しになっているので今回は単純に「問」の２個後の要素を抜き出しています。

# 今後の課題
* データの使い先
→現在は配列で抽出しているだけなのでDBや他のシステム用のファイルに成型する必要がある。
* 問題文の抽出
→応用情報技術者試験の問題は文章ではなく１ページ１枚の画像になっています。画像を抽出した上で問題ごとに画像処理によって切り離す必要があります。