【python】PDFをスクレイピングして中身をCSVへ出力する(前編)
陸上競技場が個人向けに開放されている日を調べるために、毎回サイトへ訪問して、PDFリンクをクリックして、今日が空いているのか、を確認するのって億劫ですよね。
例えばこんな感じです。
そこで、サイト内に存在するPDFをスクレイピングして中身をCSVへ保存するプログラムを作ろうと考えました。
今回は、プログラム作成における戦略と戦術をまとめていきたいと思います。
本記事で作るプログラム
長すぎるので二つの記事に分けたいと思います。
本記事では、URLからHTMLを取得し、PDFファイルをスクレイピングしてローカルへ保存するまでを扱いたいと考えています。
そして、出来上がるのは下記プログラムになります。
from bs4 import BeautifulSoup
from selenium import webdriver
import chromedriver_binary
import re
import urllib
import pandas as pd
import tabula
import os
base_url = 'https://www.city.kawasaki.jp/nakahara'
driver = webdriver.Chrome()
driver.get("https://www.city.kawasaki.jp/nakahara/page/0000088519.html")
content = driver.page_source
soup = BeautifulSoup(content, "html.parser")
save_dir = 'track_pdfs'
if save_dir not in os.listdir("./"):
os.mkdir(save_dir)
file_names = []
for element in soup.findAll(attrs={"class": "mol_attachfileblock"}):
names = element.findAll('a')
for name in names:
pdf_relative_path = name.get('href')
pdf_path = re.sub(r'^.', base_url, pdf_relative_path)
file_name = pdf_relative_path.split("/")[-1]
print(pdf_path)
urllib.request.urlretrieve(pdf_path, os.path.join(save_dir, file_name))
driver.quit
色々書いてありますが、一緒に見ていきましょう。
pre 戦略
スクレイピング技術を使ってPDFファイルの中身を読み取る?それとも、通知でPDFファイルの内容をLINEに通知する?どちらかかなと考えました。
どうやらpythonにはPDFファイルの中身を表にして書き出すライブラリが用意されているようなので、活用していきたいと思います。
戦略
多分下記の通りやったらいけるのではないかと空想しています。
- クローリング&スクレイピングでPDFのURLを取得 ←本記事で扱います。
- PDFをローカルにダウンロード ←本記事で扱います。
- PDFの中身を取得 ←本記事で扱います。
- PDFの中身をcsvに出力
戦術
開発環境
- PDFのURLを取得
必要なライブラリをインストールしておきましょう。
スクレイピングで使用するライブラリは下記の通りになります。
- re
- urllib
- pandas
- tabula
- os
すでにv3.7以上のpython、pipをインストール済みであれば、下記コマンドでダウンロードできます。
pip install re
pip install urllib
pip install pandas
pip install tabula
pip install os
取得したいPDFがあるページへアクセスする
今回は、等々力競技場(川崎フロンターレの本拠地)の競技場利用案内サイトからPDFを取得していきたいと思います。11月の個人開放日時を取得したいので、下記画像でいう令和4年11月分になります。
from bs4 import BeautifulSoup
from selenium import webdriver
import chromedriver_binary
import pandas as pd
import os
base_url = 'https://www.city.kawasaki.jp/nakahara'
driver = webdriver.Chrome()
driver.get("https://www.city.kawasaki.jp/nakahara/page/0000088519.html")
content = driver.page_source
soup = BeautifulSoup(content, "html.parser")
chrome driverを使っています。
chrome driverの使い方は(こちら)[https://zenn.dev/subaru_hello/articles/38624760719abf] に詳しく書いてありますので、参考にしていただければと思います。
簡単に概要を説明いたしますと、下記のような流れになってます。
- PDFが存在するページをbase_url変数に格納。
- chromedriverを使ってクローリング。
- 取得したページの中身をスクレイプするために、BeautifulSoupの第一引数に入れる。
これで、指定したURLのHTMLを解析して中身をスクレイピングすることができます。
保存先を作成する
save_dir = 'track_pdfs'
if save_dir not in os.listdir("./"):
os.mkdir(save_dir)
pdfを出力するディレクトリを作成しておきましょうか。
既にディレクトリがあるというエラーが出てしまうので、条件分岐の記述も追加しています。
osライブラリは、開発環境の操作のできるsysをimportしています。
linux操作をpythonでもできるようにしたライブラリでしょうか。
詳しくみてみると面白いです。
cpython/os.py at 92b531b8589b733c4e44e291f08271fa34947400 · python/cpython
スクレイピングしたHTMLからPDFのある要素を抽出する
for element in soup.findAll(attrs={"class": "mol_attachfileblock"}):
names = element.findAll('a')
for name in names:
mol_attachfileblockというクラス名を持つ要素をスクレイピングしています。
beautiful soupに用意されているfindAllというメソッドを使い、指定したattrsに該当する箇所を全て取得していますね。
本来、findAllで取得した要素は一次元配列となって帰ってくるのですが、下記画像で分かる通り、ulの中にliがネストしてしまっているので、さらに配列操作が必要になっています。
要は、該当する要素を最初の一つを取得するか、全て取得するかの違いです。
RailsでActiveModelに対してクエリーを投げるときにfindを使うかselectの違いに近しいものを感じますね。
# find('li')だと<li>きのこの山</li>しか取得できないが、
# findAll('li')だと<li>やまいもの谷</li>まで取得できる。欲張りたいですよね。
<ul>
<li>きのこの山</li>
<li>たけのこの里</li>
<li>フジツボの丘</li>
<li>やまいもの谷</li>
</ul>
そして、getリクエストの引数にhrefを入れ、pdfファイルへのパスを取得します。
pdf_relative_path = name.get('href')
PDFを絶対パスに置換する
今のままだと下記のようになってしまうので、正規表現を使って先頭の文字を川崎市のホームページURLに変換します。
./cmsfiles/contents/0000088/88519/korona.pdf
./cmsfiles/contents/0000088/88519/jouken202204.pdf
./cmsfiles/contents/0000088/88519/1010kara.pdf
./cmsfiles/contents/0000088/88519/todorokiyakan2.pdf
./cmsfiles/contents/0000088/88519/202210.pdf
./cmsfiles/contents/0000088/88519/202211.pdf
./cmsfiles/contents/0000088/88519/riyouannnai202204.pdf
./cmsfiles/contents/0000088/88519/rule.pdf
./cmsfiles/contents/0000088/88519/satuei.pdf
./cmsfiles/contents/0000088/88519/sennyou.pdf
./cmsfiles/contents/0000088/88519/zikiru-pu.pdf
./cmsfiles/contents/0000088/88519/akusesu.pdf
re(regexの略)というライブラリを使用します。第一引数に正規表現、第二引数に変換後の文字列、第三引数に変換前の文字列を入れます。
import re
re.sub(r'正規表現',BaseUrl, pdf_name)
今回は下記のように使用しています。
pdf_path = re.sub(r'^.', base_url, pdf_relative_path)
見事置換をすることができました。置換おめでとうございます。
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/korona.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/jouken202204.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/1010kara.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/todorokiyakan2.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/202210.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/202211.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/riyouannnai202204.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/rule.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/satuei.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/sennyou.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/zikiru-pu.pdf
https://www.city.kawasaki.jp/nakahara/cmsfiles/contents/0000088/88519/akusesu.pdf
置換したURLのパス名をファイル名にする
file_name = pdf_relative_path.split("/")[-1]
pdfを保存する際の名前を用意します。URLのパス名の最後をファイル名にしていきます。
先ほど置換したファイルを/単位で字句分解し、最後の塊をfile_nameとしています。
PDFに出力する
urllib.request.urlretrieve(pdf_path, os.path.join(save_dir, file_name))
urllibというライブラリを使ってpdfに落とし込んでいきます。
urllib内にあるrequestメソッドはHTTPリクエストをしてくれるライブラリのようです。
そして、urlretirieveというメソッドに取得したいURL、取得したURLを出力するローカルパス名を引数として渡します。
cpython/request.py at 92b531b8589b733c4e44e291f08271fa34947400 · python/cpython
driver.quit()
最後に、driver.quit()を使って処理の終わりを明示することも忘れないようにしましょう。
保存を確認
ここまでの処理をvimで行なっていた方は、GUIで確認してみましょう。11月の競技場予定表を取得できていますね。どうやら高校サッカーの準決勝があるみたいですね。是非我が母校の桐蔭学園には全国へと駒を進めていただきたいものです。
次回はcsvへ出力する処理に関して執筆していきたいと思います。
参考文献
Python If with NOT Operator
[Python]正規表現で文字列の先頭を置換するには?
urllib.request:保存先ディレクトリの指定方法
Discussion