Python + pipenv 環境に Selenium + ChromeDriver + BeautifulSoup4 でクローリング・スクレイピングしてみる
前回紹介した pipenv を使って、Python おなじみのクローリング・スクレイピングを行ってみる。
今回作成したプロジェクトの全量は以下の GitHub リポジトリに上げたのでドウゾ。
目次
pipenv の準備
各プロジェクトディレクトリの配下の .venv/
ディレクトリ配下に仮想環境のファイルを配置するようにするため、環境変数で以下のように設定しておく。
export PIPENV_VENV_IN_PROJECT=1
Windows の場合も PIPENV_VENV_IN_PROJECT
というキーに true
などの値を設定して環境変数を設定しておくこと。
プロジェクトの作成
プロジェクトの作成手順はこんな感じ。
# Pipfile を生成する
$ pipenv --python 3.7
# クローリング用の Selenium をインストールする
$ pipenv install selenium
# OS にインストールしてあるChrome のバージョンに合わせてインストールする
$ pipenv install chromedriver-binary~=77.0
# HTML のパース・スクレイピングを行う BeautifulSoup4 をインストールする
$ pipenv install beautifulsoup4
chromedriver-binary
というのは、ChromeDriver のパスを自動判定してくれるモノ。後述するコードがシンプルになり、環境依存が減らせる期待があるので入れてみた次第。
クローリングを行うスクリプト
まずは指定の URL にアクセスして HTML ソースを取得する、いわゆる「クローリング」を行うスクリプトを作ってみる。
この辺については、Node.js で似たようなことをやったことがあるので、Node.js 民はそちらも参考にしていただけると理解がしやすいかと。
- Node.js で selenium-webdriver と chromedriver を使って Chrome ブラウザを自動操作してみる
example_crawl.py
… 完全版は以下
from pathlib import Path
import platform
import chromedriver_binary
from selenium import webdriver
def main():
prepareHtmlDirectory()
driver = createDriver()
pageSource = getPageSource(driver, 'http://google.com/')
writePageSource(pageSource, 'google.html')
driver.quit()
# Chrome WebDriver を生成する
def createDriver():
chromeOptions = webdriver.ChromeOptions()
chromeOptions.add_argument('--headless')
chromeOptions.add_argument('--disable-gpu')
chromeOptions.add_argument('--no-sandbox')
driver = webdriver.Chrome(options = chromeOptions)
return driver
def prepareHtmlDirectory():
htmlDir = Path(Path.cwd()).joinpath('html')
if not htmlDir.exists():
htmlDir.mkdir()
def getPageSource(driver, url):
driver.get(url)
print(f'{url} : {driver.title}')
return driver.page_source
def writePageSource(pageSource, fileName):
file = Path(Path.cwd()).joinpath('html').joinpath(fileName)
file.write_text(pageSource, encoding = 'utf-8', newline = '')
if __name__ == '__main__':
main()
chromedriver_binary
を import
すると、webdriver.Chrome()
で ChromeDriver をセットアップする時に executable_path
引数の指定が要らなくなるのが特徴。
しかし試したところ、MacOS ではこの chromedriver-binary で Chrome を上手く認識できなかったのと、ヘッドレスモードで起動できなかったので、MacOS で動かす場合は以下のように修正した。
# MacOS では executable_path で指定しないと上手く読み込めなかったのでコメントアウトしておく
# import chromedriver_binary
# Chrome WebDriver を生成する
def createDriver():
# MacOS では ChromeDriver のパスを指定しておく
executablePath = '/usr/local/bin/chromedriver'
chromeOptions = webdriver.ChromeOptions()
# MacOS では Headless モードにすると上手く起動しなかったので避ける
if platform.system() != 'Darwin':
chromeOptions.add_argument('--headless')
chromeOptions.add_argument('--disable-gpu')
chromeOptions.add_argument('--no-sandbox')
driver = webdriver.Chrome(executable_path = executablePath, options = chromeOptions)
return driver
ChromeDriver 周りは、実行環境によってどうしてもムリが出てくるので、OS をまたいだクロスブラウザテストをする、みたいな要件がないのであれば、Docker とかに寄せて OS 環境から統一させた方が良いと思う。
あとは pathlib
という組み込みモジュールを使って、Node.js でいうところの fs
みたいなことを色々やった。特にファイル書き込み write_text()
などは、改行コードやエンコードが未指定の時は OS に依存して勝手に Shift-JIS とか CRLF にされたりするので、しっかり明示しておくと環境差異が出なくなる。
あとファイル末尾に if __name__ == '__main__':
と書いているのは、Python でよくあるイディオム。このファイルを別のファイルが import
した時に、勝手に main()
関数が実行されないようにするやり方。Node.js で再現するとしたら if(!module.parent)
とかで判定すればいいかな。
このスクリプトを実行する時は以下のようにする。
$ pipenv run python example_crawl.py
./html/google.html
ファイルが生成されて、中身が書かれていれば OK。
スクレイピングを行うスクリプト
続いて、HTML ソースから必要な情報を抜き取る、「スクレイピング」処理を行うスクリプトを作る。
example_scrape.py
… 完全版は以下
import json
from pathlib import Path
from bs4 import BeautifulSoup
def main():
prepareJsonDirectory()
html = getHtml('google.html')
soup = parseHtml(html)
scrapedDict = scrape(soup)
writeJson(scrapedDict, 'google.json')
print('End')
def prepareJsonDirectory():
jsonDir = Path(Path.cwd()).joinpath('json')
if not jsonDir.exists():
jsonDir.mkdir()
def getHtml(fileName):
file = Path(Path.cwd()).joinpath('html').joinpath(fileName)
html = file.read_text(encoding = 'utf-8')
return html
# BeautifulSoup でパースする
def parseHtml(html):
soup = BeautifulSoup(html, 'html.parser')
return soup
# スクレイピングして結果を Dict で返す
def scrape(soup):
# 辞書 (Dict) を用意する
scrapedDict = {}
# 一つ要素を取得する
print('my_text')
scrapedDict['my_text'] = soup.select('a.gb_e')[0].string
print(' ' + scrapedDict['my_text'])
# p 要素を全て取得してリストに詰める
print('a_elements')
elements = soup.find_all('a')
elementList = []
for index in range(len(elements)):
element = elements[index]
paragraphString = element.string
print(f' [{index}] : {paragraphString}')
elementList.append(paragraphString)
# 辞書に格納する
scrapedDict['a_elements'] = elementList
return scrapedDict
def writeJson(scrapedDict, fileName):
jsonFile = Path(Path.cwd()).joinpath('json').joinpath(fileName)
with jsonFile.open('w', encoding = 'utf-8') as file:
# Unicode 出力しないようにする
json.dump(scrapedDict, file, indent = 2, ensure_ascii = False)
if __name__ == '__main__':
main()
先程のコードで取得した ./html/google.html
が存在する前提でコードを書いている。
HTML ファイルを読み込み、BeautifulSoup でパースし、jQuery 的にセレクタを使って要素を特定できる形式にする。
Node.js では、Cheerio という npm パッケージが BeautifulSoup と似たようなことをしてくれるので、HTML パーサなんだなーと思えていれば OK。
そして scrape()
関数の中で、実際に要素を特定してテキストを取得、Dict (辞書・JS でいう連想配列) に詰めている。
この Dict という Python の型は、組み込みモジュール json
を使うとすぐに JSON 形式に変換できるので、そのまま ./json/google.json
というファイルで書き出している。
このスクリプトの実行方法はこんな感じ。
$ pipenv run python example_scrape.py
./json/google.json
ができていれば OK。
以上
まだまだ Python 素人なので、Python らしくない書き方が散見されるかもしれないが、とりあえず Python を使ってクローリングしたい、スクレイピングしたい、というところはコレで始められるかなと思う。