スクレイピング(Selenium, Beautiful Soup)をサーバー上(Heroku)で定期実行させる方法

この記事では、Pythonで作成したスクレイピング(Selenium, Beautiful Soup)のプログラムを、サーバー上(Heroku)に設置し、決まった時間に定期実行させる方法を紹介しています。

今回、AWSやGCPと言った数あるIaaSやPaaSの中で、HerokuというPaaSを選んだ理由は4つあります。

  • インフラ面をそこまで意識せずに使える
  • 定期実行させる簡単なスクレイピングであれば、ほぼ無料で使えること
  • 利用者が多く、ドキュメントが充実してる(大事)
  • PaaSとしての歴史が長い

Herokuについてもっと詳しく知りたい方は、こちらの記事に良くまとめられていました。

  • Herokuってなんなの? - Qiita
  • Herokuのメリット、デメリット - Qiita
  • ローカルで動作確認する

    ローカルで動作しないプログラムは、Heroku上で絶対に動作しません。必ずローカルでもちゃんと動くか確認しましょう。

    STEP.1
    今回実現したいことを確認する

    Yahooの天気サイトをSeleniumでクローリングして、『今日の日本の天気予報の要約』部分をBeautiful Soupでスクレイピングして、Web APIのLINE Notifyrequestsで叩いて、LINEに通知させます。

    Yahoo!天気・災害

    STEP.2
    開発用のフォルダを作る
    ターミナル
    $ mkdir {{フォルダ名}}
    STEP.3
    venvでPython3の仮想環境を作成し、開発用のフォルダに移動する
    ターミナル
    $ python3 -m venv {{フォルダ名}}; cd {{フォルダ名}}
    ディレクトリ
    開発用のフォルダ/
     ┣ bin/
     ┣ include/
     ┣ lib/
     ┗ pyvenv.cfg
    STEP.4
    仮想環境(venv)を有効化する
    ターミナル
    $ source bin/activate
    STEP.5
    SeleniumBeautiful Souprequestsを仮想環境にインストールする
    ターミナル
    (開発フォルダ名)$ pip install selenium
    (開発フォルダ名)$ pip install beautifulsoup4
    (開発フォルダ名)$ pip install requests

    以降、ターミナルにおいて(開発フォルダ名)$ UNIXコマンド(開発フォルダ名)部分は省略しますが、開発環境は仮想環境のままで、読み進めて下さい。

    STEP.6
    トークンを発行する

    LINE Notifyのトークンはここで発行します

    https://notify-bot.line.me/my/

    ページを閉じるとトークンは2度と表示されないので確実にコピーしておいてくださいね。

    STEP.7
    スクレイピング用の動作確認プログラムmain.pyの作成
    ターミナル
    $ touch main.py
    ディレクトリ
    開発用のフォルダ/
     ┣ bin/
     ┣ include/
     ┣ lib/
     ┣ main.py NEW
     ┗ pyvenv.cfg

    main.pySeleniumでヘッドレスモードでクローリングし、Beautiful SoupでスクレイピングしてLINEに通知させるプログラムに書き換えます。

    main.py
    import requests
    from bs4 import BeautifulSoup
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    
    # ローカルに保存しているChrome Driverを指定(※デプロイするときはコメントアウトする)
    driver_path = '{{Chrome DriverのPATH}}'
    
    # Heroku上のChrome Driverを指定(※デプロイするときはコメントを外す)
    # driver_path = '/app/.chromedriver/bin/chromedriver'
    
    # Headless Chromeをあらゆる環境で起動させるオプション
    options = Options()
    options.add_argument('--disable-gpu');
    options.add_argument('--disable-extensions');
    options.add_argument('--proxy-server="direct://"');
    options.add_argument('--proxy-bypass-list=*');
    options.add_argument('--start-maximized');
    options.add_argument('--headless');
    
    # クローラーの起動	
    driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)
    
    # Yahooの天気サイトにアクセス
    driver.get('https://weather.yahoo.co.jp/weather/')
    
    # ソースコードを取得
    html = driver.page_source
    
    # ブラウザを終了する
    driver.quit()
    
    # HTMLをパースする
    soup = BeautifulSoup(html, 'lxml') # または、'html.parser'
    
    # スクレイピングした《今日の日本の天気予報の要約》を変数に格納
    message = soup.select_one('#condition > p.text').get_text()
    
    # LINEに通知させる関数
    def line_notify(message):
    	line_notify_token = '{{LINE_NOTIFY_TOKEN}}'
    	line_notify_api = 'https://notify-api.line.me/api/notify'
    	payload = {'message': message}
    	headers = {'Authorization': 'Bearer ' + line_notify_token}
    	requests.post(line_notify_api, data=payload, headers=headers)
    
    # LINEに通知させる
    line_notify(message)
    STEP.8
    動作確認
    ターミナル
    $ python main.py

    正しく動けば、LINEにピコンと通知が行ってると思います。

    END

    ローカルでちゃんとプログラムが動くことを確認したら、次はHerokuで動かすための環境を構築します。

    Herokuの環境構築

    Heroku CLIをインストールする

    Herokuのアカウント登録をすませ、ターミナルからherokuコマンドを実行できるように、Heroku CLIをダウンロードします。

    MacユーザーでHomebrewを入れていれば、brewコマンドでダウンロードできます。

    ターミナル
    $ brew tap heroku/brew && brew install heroku

    その他のOSの方やbrewコマンドが使えない方は公式からダウンロードして下さい。

    Herokuにログインする

    ターミナルからheroku loginコマンドでHerokuにログインします。

    ターミナル
    $ heroku login

    ログインに成功すると下のようなメッセージが表示されます。

    出力
    heroku: Press any key to open up the browser to login or q to exit: 
    Opening browser to https://cli-auth.heroku.com/auth/cli/browser/{{Herokuの番号}}
    Logging in... done
    Logged in as {{herokuのアカウント}}

    ログイン後に表示されるブラウザのタブは閉じても問題ありません。

    アプリを作成する

    heroku createコマンドを使用して、HerokuにPythonのプログラムを実行するためのアプリケーションを作成します。

    アプリケーション名を指定する方法としない方法の2パターンがあります。指定しない場合、適当な名前が設定されます。

    ターミナル
    $ heroku create
    $ heroku apps:create {{アプリ名}}

    アプリ名を指定した場合、他のアプリケーションと名前が被ってはいけません。アプリが作れないエラーが発生します。

    出力
    Name {{アプリ名}} is already taken

    buildpackを追加する

    ターミナルでheroku buildpacks:addコマンドを使用して、3つのbuildpackを追加します。

    • heroku/python:HerokuでPythonのプログラムを実行するため
    • heroku-buildpack-google-chrome:HerokuでHeadless Chromeを使うため
    • heroku-buildpack-chromedriver:HerokuでChrome Driverを使うため
    ターミナル
    $ heroku buildpacks:add heroku/python -a {{アプリ名}}
    $ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-google-chrome -a {{アプリ名}}
    $ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-chromedriver -a {{アプリ名}}

    {{アプリ名}}には、先程heroku createコマンドで作成したアプリ名を入力します。

    MEMO
    buildpackはherokuへのデプロイ時にインストールされます。

    3つのbuildpackがちゃんと追加されているかどうか確認する場合は、Herokuの管理画面からSetting > Buildpacksで確認できます。

    タイムゾーンをAsia/Tokyoに変更する

    HerokuのデフォルトのタイムゾーンはUTCなので、Asia/Tokyoに変更します。

    ターミナル
    $ heroku config:add TZ=Asia/Tokyo -a {{アプリ名}}

    成功すると下のようなメッセージが表示されます。

    出力
    Setting TZ and restarting ⬢ {{ アプリ名 }}... done
    TZ: Asia/Tokyo

    Herokuにデプロイする

    環境変数にトークンをセットする

    いま、main.pyの変数line_notify_tokenに、直接トークンが入っているので、万が一を備えてHerokuの環境変数を使用します。

    Python
    line_notify_token =  '{{LINE Notify トークン}}'

    heroku config:setコマンドを使い、Herokuの環境変数にトークンの値をセットします。

    ターミナル
    $ heroku config:set LINE_NOTIFY_TOKEN=**************** -a {{ アプリ名 }}

    Herokuの環境変数は、heroku configコマンドで確認できます。

    ターミナル
    $ heroku config -a {{ アプリ名 }}

    あわせて、main.pyも変更します。osモジュールをインポートして、os.environを使って、環境変数を呼び出せるように書き換えます。

    main.py
    import os
    line_notify_token = os.environ['LINE_NOTIFY_TOKEN']

    ファイルを用意する

    HerokuでPythonのプログラムを動かすために、2つのテキストファイルを用意しなければいけません。

    • requirements.txt:サードパーティ製のモジュールを記載する
    • runtime.txt:Pythonのバージョンを記載する

    requirements.txt

    pip freezeコマンドで、仮想環境にインストールされたサードパーティ製のモジュールが表示されます。

    ターミナル
    $ pip freeze > requirements.txt
    ディレクトリ
    開発用のフォルダ/
     ┣ bin/
     ┣ include/
     ┣ lib/
     ┣ main.py
     ┣ pyvenv.cfg
     ┗ requirements.txt NEW

    runtime.txt

    仮想環境のPythonのバージョンは、python -Vで確認します。

    このとき、HerokuがサポートしているPythonのバージョンよりも新しいバージョンを記入しないようにしましょう。Heroku Python Support

    ターミナル
    $ echo python-3.x.x > runtime.txt
    ディレクトリ
    開発用のフォルダ/
     ┣ bin/
     ┣ include/
     ┣ lib/
     ┣ main.py
     ┣ pyvenv.cfg
     ┣ requirements.txt
     ┗ runtime.txt NEW

    gitでデプロイする

    Herokuにデプロイするためにはgitを使います。

    初回時のみ

    STEP.1
    gitの初期ファイルを作成
    ターミナル
    $ git init
    ディレクトリ
    開発用のフォルダ/
     ┣ .git/ NEW
     ┣ bin/
     ┣ include/
     ┣ lib/
     ┣ main.py
     ┣ pyvenv.cfg
     ┣ requirements.txt
     ┗ runtime.txt
    STEP.2
    ローカルリポジトリに結びつくリモートリポジトリを設定
    ターミナル
    $ heroku git:remote -a {{アプリ名}}
    STEP.3
    変更したすべてのファイルをインデックスに登録
    ターミナル
    $ git add .
    STEP.4
    変更したファイルをリポジトリに書き込む
    ターミナル
    $ git commit -m "{{コメント}}"
    STEP.5
    Herokuにデプロイする
    ターミナル
    $ git push heroku master

    デプロイに成功すれば次のような文字列が表示されます。

    出力
    # 中略
    remote: Verifying deploy... done.

    更新時

    STEP.1
    変更したファイルをインデックスに登録
    ターミナル
    $ git add {{変更したファイル名}}
    STEP.2
    変更したファイルをリポジトリに書き込む
    ターミナル
    $ git commit -m "{{コメント}}"
    STEP.3
    Herokuにデプロイする
    ターミナル
    $ git push heroku master

    動作確認

    Heroku上にデプロイしたPythonファイルはheroku runコマンドで簡単に実行することができます。

    ターミナル
    $ heroku run python main.py

    トラブルシューティング

    この章では、筆者が実際に遭遇したエラーとその解決策について備忘録がてら記録していきます。

    selenium.common.exceptions.SessionNotCreatedException

    エラー内容
    Message: session not created: This version of ChromeDriver only supports Chrome version 83

    これは、buidpackに追加したGoogleChromeとChromeドライバーのバージョンが違うことで発生したエラーケースです。

    herokuのChromeドライバーはデフォルトで、Latest版(次期リリース対応)のものがダウンロードされるので、滅多に起こりませんがダウンロードする時期によって、2つのバージョンがずれることがあります。

    それぞれのバージョンを確認
    # Google Chrome (81.0.4044.138)
    $ heroku run google-chrome --version -a {{ アプリ名 }}
    
    # Chrome Driver (83.0.4103.39)
    $ heroku run chromedriver -v -a {{ アプリ名 }}

    この解決策は、ChromeドライバーのバージョンをGoogleChromeのバージョンに合わせます。

    herokuの環境変数にChromeドライバーのバージョンを指定する
    heroku config:set CHROMEDRIVER_VERSION=81.0.4044.138 -a {{ アプリ名 }}

    そして、次回のデプロイ時に環境変数に登録されているバージョンのChromeドライバーがダウンロードされます。

    プログラムの変更がなく、デプロイだけ行いたいときは空コミットをすると良いです。

    ターミナル
    # 空コミット
    $ git commit --allow-empty -m "allow empty commit"
    
    # デプロイ
    $ git push heroku master

    Chromeドライバーのバージョンが下がったか確認してみましょう。

    それぞれのバージョンを確認
    # Google Chrome (81.0.4044.138)
    $ heroku run google-chrome --version -a {{ アプリ名 }}
    
    # Chrome Driver (81.0.4044.138)
    $ heroku run chromedriver -v -a {{ アプリ名 }}

    おまけ:その他のHerokuのコマンド

    ターミナル
    # アプリを削除
    $ heroku apps:destroy --app {{{ アプリ名 }} --confirm {{ アプリ名 }}
    
    # 環境変数を確認・セットする
    $ heroku config -a {{ アプリ名 }}
    $ heroku config:set ACCESS_TOKEN=*********************** -a {{ アプリ名 }}
    
    # アプリ一覧を表示
    $ heroku list
    
    # ログを確認
    $ heroku logs
    
    # 直近のリリース一覧を表示する
    $ heroku releases
    
    # Dynoの使用量確認(freeプランで運用するときに見る)
    $ heroku ps
    
    # gitのURLを変更する
    $ git remote set-url heroku 

    Herokuで定期実行する

    やっとこの記事の本題に入ります、笑

    Herokuで定期実行するための方法は何種類かあるのですが、その中でも割とメジャーな方法を2つだけ紹介します。

    その前に、無料有料問わず、定期実行(cron)させたい場合は、クレジットカードの登録が必要不可欠です。先にアカウントページでクレジットカードを登録させましょう。

    [無料] heroku scheduler

    • 無料
    • タスクは複数登録可能
    • タスクの起動間隔は10分おき、1時間おき、1日おきの3パターンから選べる。
    STEP.1
    heroku addons:addコマンドで、heroku schedulerを追加する
    ターミナル
    $ heroku addons:add scheduler:standard
    STEP.2
    heroku schedulerの管理画面にアクセスし、Create jobボタンを選択する

    https://dashboard.heroku.com/apps/{{アプリ名}}/scheduler

    STEP.3
    実行時間を選ぶ

    実行時間を以下の3つの中から選びます

    • Every 10 minutes:毎日10分ごとに実行
    • Every hour at…:毎日1時間ごとに実行
    • Every day at…:毎日何時に実行

    Every day at…を選んだ場合、HerokuのタイムゾーンがUTCなので、日本時間に合わせて+9時間させます。

    つまり、毎日16時に実行させたい場合は、07:00 AMを選択するという訳です。

    STEP.4
    実行コマンドを入力する

    Herokuにデプロイしたmain.pypythonコマンドで実行させるので、python main.pyを入力します。

    これでLINEにピコンと通知が来れば完了です!

    無料でお手軽ですが、曜日を指定したり特定の日だけ選んだりして定期実行などは出来ないので、注意が必要です。

    [有料] Cron To Go

    スクレイピングを曜日を指定したり特定の日だけ選んだりして定期実行したい場合に使うのがCron To Goです。

    Unix cron形式と全く同じ設定方法で、スケジュールを定義することができます。

    Unix cron形式
    分 時 日 月 曜日 コマンド
    *  *  *  *  *  some_command
    スケジュール
    059
    023
    131
    112
    曜日 070,7=日曜、1=月曜、2=火曜、3=水曜、4=木曜、5=金曜、6=土曜)

    1週間の無料トライアル

    無料トライアル版には、最大15ジョブ、無制限の実行、チャットサポートなど、シルバープランに関連するすべての機能が含まれています。

    試用期間が終了すると、プランをアップグレードするまで設定したジョブが自動的に一時停止されます。勝手に課金されないので、試しに使ってみてはどうでしょうか。

    https://elements.heroku.com/addons/crontogo

    設定方法

    STEP.1
    heroku addons:addコマンドで、Cron To Goを追加する
    ターミナル
    $ heroku addons:create crontogo:free-trial
    STEP.2
    Cron To Goの管理画面にアクセスし、Add jobボタンを選択する

    STEP.3
    スケジュールを設定する

    スケジュールの名前や実行時間、コマンドを入力したらadd jobボタンを押して完成です。

    注意
    Cron To GoもタイムゾーンがUTCなので、日本時間に合わせて+9時間しないといけません。

    これでLINEにピコンと通知が来れば完了です!

    有料版ということもあり、UI・UXの設計がしっかりしていて抜群に使いやすいアドオンです。

    連載:Pythonでクローリング/スクレイピング

    《超実践的》Pythonでクローリング/スクレイピングを行うロードマップ

    1. クローリングとスクレイピングをする前の事前準備・知識など
    2. 静的なWebページを『Beautiful Soup』でスクレイピングする
    3. ログインが必要なWebサイトを『Selenium』でクローリングし、『Beautiful Soup』でスクレイピングする
    4. JavaScriptで書かれたページを『Selenium』でスクレイピングする
    5. Web APIやデータセットを使用してスクレイピングする
    6. スクレイピングで得たデータを様々な形式(pandas、BigQuery、スプレッドシート、DBなど)に変換する
    7. クローリング/スクレイピングをローカルマシンで定期実行する方法
    8. クローリング/スクレイピングをサーバーで定期実行する方法
    9. クローリング/スクレイピングを安定させるための3つの設定(待機処理・エラーの通知・処理のリトライ)
    10. クローリング/スクレイピングの次にやるべきこと

    Selenium逆引きリファレンス

    1. Seleniumチートシート
    2. 【headlessモード】ブラウザを起ち上げずにSeleniumを実行する方法
    3. アラートダイアログを操作する
    4. セレクトボックスを選択する方法
    5. Basic認証を突破する方法
    6. 2段階認証(6桁のパスコード)を突破する方法
    7. reCAPTCHAを突破する方法
    8. 【target="_blank"対策】driverを別ウィンドウに切り替える方法
    9. 【display:none対策】JavaScriptを実行して隠された要素を表示させる方法
    10. 【表示読み込み対策】時間を遅延させる
    11. Beautiful Soupと組み合わせる
    12. herokuで定期実行させる手順