【python】PDFをスクレイピングして中身をCSVへ出力する(後編)
陸上競技場のサイトからPDFを取得してローカルに保存することを前編では取り扱いました。
本記事では、前編 で取得したPDFの中身をcsvへ出力するプログラムを作成していきたいと思います。
また、別途必要になる下記ライブラリのインストール方法については本記事では取り扱いません。
- pandas
- tabula
本記事で作るプログラム
外部サイトからPDFを取得してcsvへ出力する際の戦略は下記の通りです。
1~2は前編で取り扱いました。今回は3~4に関して取り扱っていきたいと思います。
- クローリング&スクレイピングでPDFのURLを取得
- PDFをローカルにダウンロード
- PDFの中身を取得 ←本記事で取り扱います。
- 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の保存されているディレクトリを指定し、その他オプションを設定することで様々な処理を行うことができます。
読み込まれた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の良さを知るいいきっかけになるかもしれません。
Discussion