2020年6月21日日曜日

PythonでPDFの表からデータを抽出する(その2)

PythonでPDFの表からデータを抽出するでは、キャッシュレス決済によるポイント還元制度の公式サイトから当時公開されていた登録加盟店一覧のPDFをダウンロードし、PDF内の表からPythonでデータを抽出してみた。これは比較的簡単にできたわけだが、ときにはなかなか簡単にできないこともある。最近、厚生労働省のホームページの地域ごとの感染状況等の公表についてからダウンロードできるPDFからデータ抽出をしてみたが、これがすんなりとできなかったので、Pythonで抽出する方法をここにまとめておく。


環境


WSL(Ubuntu18.04)。



対象のPDF


今回データを抽出したのは、厚生労働省のホームページ地域ごとの感染状況等の公表についてからダウンロードできる「3.帰国者・接触者相談センターへの相談件数の推移(都道府県別・各日)・帰国者・接触者外来の受診者数の推移・うちPCR検査実施件数の推移(都道府県別・各日) 」。

このPDFには次の5つのデータが含まれる。
  • 帰国者・接触者相談センター(全相談件数)
  • 帰国者・接触者相談センター(症状等の相談件数)
  • 帰国者・接触者外来の受診者数
  • PCR検査実施件数
  • PCR検査実施件数(唾液)

全国および都道府県ごとに上記データが表にまとまっていて、ここで使用するPDFには、今年の4月1日から6月16日までの日ごとのデータが含まれる。全12ページで、全国~富山、石川~島根、岡山~沖縄の3つのブロックごとにまとまっている。


環境の準備


今回もPythonでPDFの表からデータを抽出すると同様にtabula-pyを使うので、openjdk-8-jdkとtabula-pyをインストールしておく。

すでにtabula-pyがインストール済みの場合はバージョンアップしておく。


tabula-pyのバージョンは2.1.1。


と、ここまではさくっと抽出できると思っていたが、実際にやってみるといくつか問題があった。


問題その1


tabula-pyでPDFを読み込む(read_pdf)ときにlattice=True(表の罫線でセルを判定)を指定すると一部のデータが抽出できない。実は今回読み込むPDFの表には罫線が欠けている箇所がいくつかあり、これが理由と思われる。よって今回はこのオプションは使用しない。


問題その2


最新バージョンのtabula-pyでは、デフォルトで結果はDataFrameのリストになるが、1ページ分だけ読み込んでもなぜか複数のDataFrameが含まれるリストが作成される。ただ、リストの最初の要素は必要なデータがすべて含まれているDataFrameなので、1ページごとに読み込んで、リストの最初の要素のみ使うようにする。


問題その3


取得したDataFrameに空列が含まれるページがある。なぜ空列があるのかは不明だが、そのままだと最終的にDataFrameを連結していくときに都合が悪い。空列の列名は「Unnamed: x」(xは数字)となるので、こうした列を削除する。


データ抽出


もろもろの問題を踏まえて、PDFからcsvを作成するコードを作成した。PDFでは日にちが列名になっているが、csvでは行と列を入れ替えている。

import os
import pandas as pd
import tabula

# 読み込むPDFのパス
PDF_PATH = '000641241.pdf'

# PDFの総ページ数
N_PAGE = 12

# ブロック(都道府県のまとまり)数
N_BLOCK = 3

# データの種類数
N_CATEGORY = 5

def _to_int(x):
    # データ値を整数に変換

    if pd.isnull(x):
        # NaNはfloatと判定される
        return x
    elif isinstance(x, float):
        return int(x)
    elif isinstance(x, str):
        return int(x.replace(',', ''))
    else:
        return x

def _set_column_names(df):

    # 2列目が空列の場合は削除
    if df[df.columns[1]].dropna().size == 0:
        df.drop(columns=[df.columns[1]], inplace=True)

    cols = df.columns.tolist()
    cols[0] = 'Pref'
    cols[1] = 'Category'
    df.columns = cols
    return df

def _set_index(df):
    # インデックスの設定

    df = _set_column_names(df)

    # 都道府県列のnanを補間
    df[df.columns[0]] = df[df.columns[0]].fillna(method='ffill', limit=2).fillna(method='bfill', limit=2)

    return df.set_index(['Pref', 'Category'])

def _remove_unnamed(df):
    # 空列(Unnamed: xx)の削除

    cols = df.columns.tolist()
    unnamed = [x for x in cols if x.startswith('Unnamed:')]

    return df.drop(columns=unnamed)

def main():
    l_df = []
    for page in range(1, N_PAGE+1):
        # 1ページあたり複数のDataFrameが取得されるので1ページごとに処理
        l_df_raw = tabula.read_pdf(PDF_PATH, stream=True, pandas_options={'header': 0}, pages=page)

        # 得られたリストの最初のDataFrameを使う
        l_df.append(_set_index(l_df_raw[0]))

    # 空列(Unnamed: xx)を削除
    for i, d in enumerate(l_df):
        l_df[i] = _remove_unnamed(d)

    # 全国~富山、石川~島根、岡山~沖縄のブロックごとに連結
    n_page_block = N_PAGE / N_BLOCK
    l_df_pref = []
    for i in range(N_BLOCK):
        st = int(n_page_block * i)
        ed = int(n_page_block*(i+1))
        l_df_pref.append(pd.concat(l_df[st:ed], axis=1))

    # ブロックごとのDataFrameを連結
    df = pd.concat(l_df_pref)
    
    # データ値を整数に変換
    df = df.applymap(_to_int)

    # 行(都道府県とデータの種類)と列(日にち)を入れ替え
    df = df.T

    # インデックスをDatetimeIndexに変換
    df.index = pd.to_datetime(df.index, format='%m月%d日') + pd.DateOffset(years=120)

    # csvに保存
    csvname = os.path.splitext(os.path.basename(PDF_PATH))[0] + '.csv'
    df.to_csv(csvname, encoding='shift-jis')

if __name__ == '__main__':
    main()

作成されたcsvをExcelで開くと以下のような感じになる。


0 件のコメント:

コメントを投稿