Pythonと外部ライブラリであるpypdfを利用し、PDF内の表から文字データを自動取得する
※正規表現の部分で誤りがあったため訂正しました(2024-07-14)
pdfからpypdfを利用して文字データを抽出・取得します。(現時点で、pypdf2ではなくpypdfの方が推奨されるようです。)
文章をそのまま抜き出すのではなく、表の中に入っている文字列(特に数字)を取得し、意図する形式に再構成します。
表が入ることにより文字データの抽出はややこしくなります。やってみると簡単なようで意外と難しいです。
正規表現を用いて、うまく当該データのみを取得する方法を考えてみます。
今回は千葉県における教員採用選考(過去3年間)のデータを抽出し、別表にまとめてみます。教員採用の傾向などが見えてくるかもしれません。
通常モードでテキストを抽出
まず、PDF表の一部ですが、以下のようになっています。
それに対し、下記のような処理を実行します。
PdfReaderに対してpdfファイルのパスを渡すことで当該ファイルの解析を行いそれをdataに代入します。
次に(※このPDFは1メージだけのpdfですが)data.pages[0]で先頭ページ(つまり1ページ目)のオブジェクトを取り出してpageに代入します。
そして、pageに対してextract_text()メソッドを実行することで、通常モードでテキストデータを抽出しtextに代入します。
printで出力した結果を水色部分のコメント文として記載しました。
さて、問題が発生しました。このテキストの状態では表のレイアウトは崩れており、どの数字がどの項目に対応するのかが全くわからなくなっています。
普通の文章を抽出する場合は通常モードで問題ないのですが、表の場合はレイアウトモードで抽出する必要があります。
import pypdf
data = pypdf.PdfReader("r3sennkoukekka.pdf")
page = data.pages[0]
text = page.extract_text()
print(text)
"""
募集人員<名> 志願者数<名> 1次選考合格者数<名> 2次選考合格者数<名>
約640 1,579 1,163 778
中学校 25 18 12
298 216 83
551 306 99
<以下略>
"""
テキストの抽出モードの変更(レイアウトモードによるテキスト抽出)
extraction_mode="layout"とすることで、テキストのレイアウトが維持されたまま抽出可能です。
ただし、縦書きの文字列が表中に含まれているなど、やっかいなPDFも存在します。うまくいくかどうかは元々の表次第です。
例によってコメント文に抽出結果を載せていますが、概ね問題はなさそうです。
ここで問題が発生した場合も、あとは正規表現による力技で抽出するしかありません(たぶん)。ここからが人間の頭の使い所です。
import pypdf
data = pypdf.PdfReader("r3sennkoukekka.pdf")
page = data.pages[0]
text = page.extract_text(extraction_mode="layout")
print(text)
"""
校種・教科・科目 募集人員<名> 志願者数<名> 1次選考合格者数<名> 2次選考合格者数<名>
小学校 約640 1,579 1,163 778
中学校 技 術 25 18 12
国 語 298 216 83
社 会 551 306 99
<以下略>
"""
正規表現を使う
さて、考え方としてはこうです。
まず小学校の場合で考えていきます。
"小学校"というキーワードの後から"中学校"というキーワードのまで間に4つの数字が入ります。それぞれ "約640" "1,579" "1,163" "778"です。
それらは表に照らし合わせると順に "募集人員" "志願者数" "1次合格者" "2次合格者"になります。
つまり、"小学校"、"中学校"の間のテキストを抽出し、その間にある数字を順に並べればよいわけです。
募集人員に関しては"約"で始まります。また、数字の間には空白があります。
最初と最後のテキスト部分に注目してその部分を引っこ抜けばいいわけです。
ここでは正規表現についてはあまり解説しませんが(私自身苦手です^^)、結構難しいので習得するよりも必要な部分だけを使うと割り切って、ChatGPTやCopilotに聞くのもありかもしれません。
正規表現を使うため、import reします。
re.findall()に、正規表現のパターン、対象テキスト、re.DOTALL(改行を含む場合)を指定し、抽出します。allということは最初に発見したパターンの後に再度マッチするパターンを見つけた場合それも返します。
re.search()は最初のパターンのみ返します。よってre.saerch()を使います。
返却値はオブジェクトになり(余談ですがなぜかキーは[0]で中身を出力できるようです。そのせいで.groupの存在をわかっていませんでした)、print時は.group()とすることでテキストに出力できます。
一応これで、小学校-中学校まで抽出できましたが、この部分をさらに分割していかなければなりません。
import pypdf
import re
data = pypdf.PdfReader("r3sennkoukekka.pdf")
page = data.pages[0]
text = page.extract_text(extraction_mode="layout")
shougakkou_chuugakkou = re.search(r'小学校.*?中学校',text,re.DOTALL)
print(shougakkou_chuugakkou.group())
"""
小学校 約640 1,579 1,163 778
中学校
"""
正規表現による抽出と置換を繰り返し、目的の部分だけを抽出する
ここからやや複雑になります。
まず"約640"の部分を抽出します。"約"の部分があるので、さらに"\d+"で数字の連続をパターンとして抽出します。
この部分が志願者の人数になりますね。
全体であるtextから、志願者の部分をre.subで除外していきます。その除外したものを一時変数tmpに格納しておくわけですが、この除外したものをもとに、数値の連続パターン(カンマ区切り)を抽出すると、"約640"はもう除外済みなので、その次の1,579が見つかるわけです。
このようにひとつひとつ除外しながらテキストを抽出しています。
ちなみにカンマも文字列として処理されるので、3つの数字+カンマという認識でいないとうまく数字を抽出できません。
import pypdf
import re
data = pypdf.PdfReader("r3sennkoukekka.pdf")
page = data.pages[0]
text = page.extract_text(extraction_mode="layout")
# textをもとに、小学校-中学校 にマッチするテキストをなるべく短い範囲で抽出する
shougakkou_chuugakkou = re.search(r'小学校.*?中学校',text,re.DOTALL)
# 小学校 約640 1,579 1,163 778
# 中学校
# shougakkou_chuugakkouをもとに、約-連続する数字 にマッチするテキストを抽出する
boshuu = re.search(r'約\d+',shougakkou_chuugakkou.group(),re.DOTALL)
# 約640
# shougakkou_chuhugakkou から boshuuにマッチする部分を除外して、shiganshaに代入する
shigansha = re.sub(boshuu.group(),"",shougakkou_chuugakkou.group())
# 一時変数に格納
tmp = shigansha
# カンマ区切りの3桁の数字にマッチする部分を抽出する
shigansha = re.search(r'\d{1,3}(,\d{3})*',shigansha,re.DOTALL)
# tmpからshiganshaにマッチする部分を除外して、ichiji_goukakuに代入する
ichiji_goukaku = re.sub(shigansha.group(),"",tmp)
tmp = ichiji_goukaku
# カンマ区切りの3桁の数字にマッチする部分を抽出する
ichiji_goukaku = re.search(r'\d{1,3}(,\d{3})*',ichiji_goukaku,re.DOTALL)
# 割愛
niji_goukaku = ""
print("小学校の募集人員:",boshuu.group())
print("小学校の志願者数:",shigansha.group())
print("小学校の一次合格者:",ichiji_goukaku.group())
# 小学校の募集人員 約640
# 小学校の志願者数 1,579
# 小学校の一次合格者 1,163
まとめ
「ある文字列の次に必ずこの文字列が来る」そういうタイプの定型文からは、正規表現で文字列を引き抜ける可能性が高いです。パターンを抽出してもとの文を加工しながら、最終的には別表にまとめたり、グラフにすることも考えられます。
公的機関は公開データのフォーマットをあまり変えてこない場合が多いと思いますので、わりと公開情報の分析は誰でもできることだったりします。