🏃

【python】PDFをスクレイピングして中身をCSVへ出力する(後編)

2022/11/19に公開

陸上競技場のサイトからPDFを取得してローカルに保存することを前編では取り扱いました。

本記事では、前編 で取得したPDFの中身をcsvへ出力するプログラムを作成していきたいと思います。
また、別途必要になる下記ライブラリのインストール方法については本記事では取り扱いません。

  • pandas
  • tabula

本記事で作るプログラム

外部サイトからPDFを取得してcsvへ出力する際の戦略は下記の通りです。
1~2は前編で取り扱いました。今回は3~4に関して取り扱っていきたいと思います。

  1. クローリング&スクレイピングでPDFのURLを取得
  2. PDFをローカルにダウンロード
  3. PDFの中身を取得 ←本記事で取り扱います。
  4. PDFの中身をcsvに出力 ←本記事で取り扱います。

開発環境

pip python chromedriver
22.3 3.9.10 102.0.5005.27

取得したPDFの中身をCSVに落とし込む

まずは作業をするファイルを作成しましょう。
write_tracks_to_csv.pyという名前のpython fileを作り、プログラムを書いていきたいと思います。

全体像

まずは全体像を見てみましょう。意外とシンプルなので拍子抜けするかもしれません。

#write_tracks_to_csv.py

import csv
import pandas as pd
import tabula
import os

file_name_list = os.listdir("./track_pdfs")

for pdf_name in file_name_list:
  pdf = './track_pdfs/' + pdf_name
  df = tabula.read_pdf(pdf, lattice=True, pages ='all')
  csv_path = "./csvs/{0}.csv".format(pdf_name)
  
  if type(df) is list:
    for deep_df in df:
      deep_df.to_csv(csv_path, index=False, encoding="utf-8")
  else:
    df.to_csv(csv_path, index=False

ディレクトリ構成

前編 でpdfを既に取得していることを前提としてお話しさせていただきます。
前編での作業が順調に進んでいた場合、恐らく下記のようなディレクトリ構造になっているのでは無いかと思います。

.

├── read_pdf.py
├── track_pdfs
│   ├── 1010kara.pdf
│   ├── 202210.pdf
│   ├── 202211.pdf
│   ├── akusesu.pdf
│   ├── jouken202204.pdf
│   ├── korona.pdf
│   ├── riyouannnai202204.pdf
│   ├── rule.pdf
│   ├── satuei.pdf
│   ├── sennyou.pdf
│   ├── todorokiyakan2.pdf
│   └── zikiru-pu.pdf

データフレームワークを作成する


import tabula
import os

file_name_list = os.listdir("./track_pdfs")

for pdf_name in file_name_list:
  pdf = './track_pdfs/' + pdf_name
  df = tabula.read_pdf(pdf, pages ='all')
  print(df)
file_name_list = os.listdir("./track_pdfs")

先ほど、スクレイピングしたpdfファイルたちはtrack_pdfsディレクトリへ保存されています。

なので、相対パスを使ってディレクトリを参照した変数を用意します。

osというpythonでLinux操作ができるようにしたライブラリを使って特定のディレクトリにあるファイル名を全て取得しています。

listdirメソッドは、指定したパス配下にあるファイルの名前を全て取得するメソッドです。

この状態でfile_name_listを出力すると下記のようになります。202211.pdfが今回取得したかったpdfになります。11月分の、陸上競技場が個人向けに開放されているかどうかに関する情報が入っています。

['riyouannnai202204.pdf', 
'202210.pdf',
'202211.pdf',
'zikiru-pu.pdf', 
'todorokiyakan2.pdf', 
'korona.pdf', 
'1010kara.pdf', 
'akusesu.pdf']

先程外部サイトから取得したpdfがtrack_pdfsに格納されているので読み込みます。

辞書型のオブジェクトになっているのでfor文を使って繰り返し処理を施しましょう。

tabula

for pdf_name in file_name_list:
  pdf = './track_pdfs/' + pdf_name
  df = tabula.read_pdf(pdf, pages ='all')
  csv_path = "./csvs/{0}.csv".format(pdf_name)

tabulaというライブラリを使ってpdfファイルの解析を行うことができます。

tabulaのread_pdfメソッドは、第一引数にpdfの保存されているディレクトリを指定し、その他オプションを設定することで様々な処理を行うことができます。
https://tabula-py.readthedocs.io/en/latest/getting_started.html

読み込まれたPDFの中身をprintを使って確認してみましょう。
PDFの中身と見比べてみると、空白がNaNとなっていることや、Unnamedとなっていたりしている部分があります。

[       日    曜            大会名(専用利用) 個人利用 Unnamed: 0
0    NaN  NaN                  NaN   午前         午後
1    1.0    土   AM障害者アスリート/PM陸協練習会    ×        NaN
2    2.0    日            中体連総合体育大会    ×        NaN
3    3.0    月                  休場日  NaN        NaN
4    4.0    火                  NaN    ○        NaN
5    5.0    水                  NaN    ○        NaN
6    6.0    木                  NaN    ○        NaN
7    7.0    金           PMフロンターレ準備    〇          ×
8    8.0    土                  清水戦    ×        NaN
9    9.0    日          中体連陸上指導者講習会    ×        NaN
10  10.0    月          川崎市スポーツフェスタ    ×        NaN
11  11.0    火                  休場日  NaN        NaN
12  12.0    水               京都サンガ戦    ×        NaN
13  13.0    木                  NaN    ○        NaN
14  14.0    金                  NaN    ○        NaN
15  15.0    土          U-18プレミアリーグ    ×        NaN
16  16.0    日                中原区民祭    ×        NaN
17  17.0    月                  休場日  NaN        NaN
18  18.0    火                  NaN    ○        NaN
19  19.0    水                  NaN    ○        NaN
20  20.0    木                  NaN    ○        NaN
21  21.0    金                  NaN    ○        NaN
22  22.0    土               秋季市民陸上    ×        NaN
23  23.0    日               秋季市民陸上    ×        NaN
24  24.0    月                  休場日  NaN        NaN
25  25.0    火                  NaN    ○        NaN
26  26.0    水                  NaN    ○        NaN
27  27.0    木                  NaN    ○        NaN
28  28.0    金  AM中学校駅伝大会PMフロンターレ準備    ×        NaN
29  29.0    土                  神戸戦    ×        NaN
  • PDFで表示されている内容

csvの出力先を指定するために、csvを拡張子に持つファイルを作成しています。{}で囲んだ部分に任意の文字を入れることのできるformatという組み込みメソッドを使用して、先ほど作成したpdf_nameという変数名をcsvファイルのファイル名にしてみましょう。

  csv_path = "./csvs/{0}.csv".format(pdf_name)

取得したデータが二次元配列となっている場合

今回、配列の中に配列が入ってしまっていたみたいで、list型のオブジェクトに対してto_csvを実行する時に下記のようなエラーが出てしまいました。

Traceback (most recent call last):
  File "/Users/subaru/dev/scraping/write_tracks_to_pdf.py", line 18, in <module>
    df.to_csv(csv_path, index=False, encoding="utf-8")
AttributeError: 'list' object has no attribute 'to_csv'
二次元配列のイメージ
[

‘競技場情報その1,

[’競技場情報その2,  ‘競技場情報その2],

‘競技場情報その3]

読み込もうとしたpdfファイルの中身が二次元配列となっていたので、配列処理を2回回さないといけないようです。

データフレームワークを作成する

import pandas as pd

--省略--
 
  if type(df) is list:
    for deep_df in df:
      deep_df.to_csv(csv_path, index=False, encoding="utf-8")
  else:
    df.to_csv(csv_path, index=False

pandasというライブラリを使ってcsvへ出力していきたいと思います。

type()で変数を囲むと、変数の型名が出力されます。動的型付けだと事前にどんな型が変数に入るのかが分からないのが辛いところですね。

今回は、辞書型だった場合にbooleanが返るような条件分岐を書きたいので、if type(df) is list:と書いています。

pandasで取得したデータフレームの型が、辞書型だった場合、さらに配列からデータを取り出す処理を追加します。

辞書型でなかった場合は、そのままcsvに出力しましょう。

CSVを確認する

出力されたCSVファイルを見てみましょう。

ここまでVimを使ってきた方は、一旦GUIのIDEを使って確認してみるといいかもしれません。自動でcsvファイルが作成されていますね。

番外編

Unnamed: がつくindex名を任意の文字列へ変換する

出来上がったCSVファイルのindexを見ると、Unnnamed: 0 という文字が入り込んでしまっています。

少し気持ちが悪いので、任意の文字へ変換していきましょう。

本来であれば下記のようになって欲しいはずでした。

そこで、columnの役割を明示し、表を見やすくするために、下記の方針で変換していきます。

  • 個人利用→個人利用a.m
  • Unnamed: 0 →個人利用p.m

pandasライブラリは、ファイルを読み込んで任意の形にデータをフォーマットしてくれるライブラリでした。

そのため、index名を変換することももちろん可能です。

今回は、renameという関数を使用していきます。

pandas.DataFrame.rename - pandas 1.5.1 documentation

def rename_index(df):
    return df.rename(columns={"個人利用": "個人利用a.m", "Unnamed: 0": "個人利用p.m"})

プログラムが肥大化してきたので、別メソッドとして切り出しました。

データフレームを引数にとっています。

columnのindex名が「個人利用」の場合は、個人利用a.mへ。

そして、Unnnamed: 0の場合は個人利用p.mへ変換しています。

「データフレームを作成する」で作成したコードに、rename_indexメソッドを挟み込んでみましょう。

    if type(df) is list:
      for deep_df in df:
        renamed_df = rename_index(deep_df)
        renamed_df.to_csv(csv_path, index=False, encoding="utf-8")

    else:
      renamed_df = rename_index(df)
      renamed_df.to_csv(csv_path, index=False, encoding="utf-8")

プログラムを実行して出来上がったcsvファイルを見てみます。置換できていますね。

完成コード

完成したコードを見ていきましょう。ひとまず目的を達成することができました。この状態から機能的凝集、データ結合へ持っていくためにリファクタリングを施すことも時間があればやっていきたいですね。

import csv
import pandas as pd
import tabula
import os


file_name_list = os.listdir("./track_pdfs")

def rename_index(df):
    return df.rename(columns={"個人利用": "個人利用a.m", "Unnamed: 0": "個人利用p.m"})

def main():
  for pdf_name in file_name_list:
    pdf = './track_pdfs/' + pdf_name
    df = tabula.read_pdf(pdf, lattice=True, pages ='all')
    csv_path = "./csvs/{0}.csv".format(pdf_name)

    if type(df) is list:
      for deep_df in df:
        renamed_df = rename_index(deep_df)
        renamed_df.to_csv(csv_path, index=False, encoding="utf-8")
        print(renamed_df)
    else:
      renamed_df = rename_index(df)
      renamed_df.to_csv(csv_path, index=False, encoding="utf-8")
      print(renamed_df)

main()

後述

あとは、csvファイルの中身をお好きなDBへ保存し、お好きなサーバーサイド言語を用いてAPIを作成するだけですね。
せっかくpythonを使ったので、RailsではなくてDjangoを用いて競技場情報を返すAPIを作成してみようかなと思います。
どうやらDjangoもオブジェクト指向で作られているみたいなので、Ruby on Railsの良さを知るいいきっかけになるかもしれません。

[Python]正規表現で文字列の先頭を置換するには?

【Python・tabula】PDFファイルから表テーブルテキストを抽出する方法

urllib.request:保存先ディレクトリの指定方法

Python If with NOT Operator
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.rename.html
https://hashikake.com/beginner

Discussion