MENU

【Python】Raspberry PiからSeleniumでWeb画面をキャプチャしてLINE Botで通知

はじめに

クレジットカードのWeb利用明細画面を毎日画面キャプチャしてLINE Botで妻とのLINEグループに通知するということをやりたかったので自宅のRaspberry Piに実装してみました。
先人たちの記事を参考(というか丸パクリ)にしつつそれらを寄せ集めて簡単に実現出来たので、自分で工夫して実装した箇所はゼロなのでほぼ参照先のリンク集でしかありませんが備忘としてまとめます。

背景

妻の持つ家族カードの利用明細画面では家族カードの利用分しか見れないため、カード支払い金額の全体が見えない。
締め日を過ぎて確定した金額はメール通知から取得してLINE Botで通知させてるけど、金額が確定する前から日々どれぐらいカード利用があるのかを通知させたかった。

方針

利用明細ページをスクレイピングして金額の部分を取得し、それを通知させてもいいけど、ログイン認証した先のページだし、第2パスワード入力とかもあるからスクレイピングで認証を通すのは難しそうだと思いました。
それよりはSeleniumを使ってブラウザでの手動操作をそのままプログラムに落とし込んでキャプチャを撮って送るほうが楽かなーと思い、Seleniumを使うことにしました。
普段愛用しているHerokuでSeleniumを動かせるもんなのかよく分からなかった(全然調べてない)のでとりあえず自宅に置いてあるRaspberryPi上で動かすことにしました。

Raspberry PiSeleniumを使用

以下の記事を参考にさせていただきました。
いろいろ調べてみるとRaspnerryPiでChromiumドライバをインストールするのは大変っぽい記事が結構出てくるのですが最近はかんたんに導入できるようになったみたいです。

irukanobox.blogspot.com

手順は↑の記事そのままです。

# 環境
 $ lsb_release -dr
Description:    Raspbian GNU/Linux 10 (buster)
Release:    10

# Seleniumのインストール
$ pip3 install selenium

# Chromiumドライバのインストール
$ sudo apt install chromium-chromedriver

サンプルも↑の記事の通りに書いてみて動作確認が取れればOKです。

Dropbox SDKを使用してアップロード、共有リンク取得

LINE Botでキャプチャを送るためには、LINE Messaging APIで画像メッセージを送る形になりますが、そのためには画像のURLが必要になります。
developers.line.biz

今回は自宅のLAN内に置いてあるRaspberry Piで動作させるため、グローバルから接続できるURLは作れません。
そのため一度取得したキャプチャをDropboxにアップロードして共有リンクを取得することにしました。

Dropboxアクセストークンの取得

DropboxのDevelopper Consoleへ接続します。
www.dropbox.com

右上のApp consoleを開きます。
f:id:ti_taka:20211028224805p:plain

「Create app」を選択 f:id:ti_taka:20211028225222j:plain

必要事項を入力してAppを作成します。
f:id:ti_taka:20211028225514p:plain

Appの設定画面に来るので、「Permission」を選択します。
f:id:ti_taka:20211028225810j:plain

必要な権限を付与します。(今回はローカルで動かすシステムに組み込むだけなのでファイル・フォルダに対しての操作権限とコラボレーションに関しての権限をすべて付与しました。)
f:id:ti_taka:20211028225943p:plain

「Settings」メニューに戻り、下の方にあるOAuth 2のメニューでアクセストークンを生成します。
今回は毎回ログインしてトークン取得するのではなく永続的にシステム埋め込んでしまうので、「Access token expiration」→「No Expiration」を選択肢、「Generated access token」の「Generate」をクリックし、アクセストークンを生成します。

f:id:ti_taka:20211028231216j:plainf:id:ti_taka:20211028231355j:plain

Dropbox SDKの使い方は↓を参考にしました。
qiita.com

コードはほぼそのままです。

画像をLINE Botで送信

LINEのMessaging APIのPythonSDKの使い方はリポジトリに記載されています。
github.com

画像をLINEで送信するのはUsageを参考に以下のように実装しました。

import os
import sys
from os.path import join
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
    SourceUser, SourceGroup, SourceRoom,
    TemplateSendMessage, ConfirmTemplate, MessageTemplateAction,
    ButtonsTemplate, ImageCarouselTemplate, ImageCarouselColumn, URITemplateAction,
    PostbackTemplateAction, DatetimePickerTemplateAction,
    CarouselTemplate, CarouselColumn, PostbackEvent,
    StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage,
    ImageMessage, ImageSendMessage, VideoMessage, VideoSendMessage, AudioMessage, FileMessage,
    UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent
)

load_dotenv(verbose=True)

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

CHANNEL_SECRET = os.environ.get('LINE_CHANNEL_SECRET', None)
CHANNEL_ACCESS_TOKEN = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN', None)
MY_GROUP_ID = os.environ.get('MY_GROUP_ID', None)

if CHANNEL_SECRET is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if CHANNEL_ACCESS_TOKEN is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

message = ImageSendMessage(
    original_content_url=url, preview_image_url=url
)
line_bot_api.push_message(
    MY_GROUP_ID,
    message
)

実装したもの

最終的に以下の感じに実装しました。
(実際のURLやパスワード入力などはやや伏せてます)

import os
import datetime
import sys
from os.path import join, dirname
from dotenv import load_dotenv
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
import dropbox
from dropbox.files import WriteMode
from dropbox.exceptions import ApiError, AuthError
from dotenv import load_dotenv

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
    SourceUser, SourceGroup, SourceRoom,
    TemplateSendMessage, ConfirmTemplate, MessageTemplateAction,
    ButtonsTemplate, ImageCarouselTemplate, ImageCarouselColumn, URITemplateAction,
    PostbackTemplateAction, DatetimePickerTemplateAction,
    CarouselTemplate, CarouselColumn, PostbackEvent,
    StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage,
    ImageMessage, ImageSendMessage, VideoMessage, VideoSendMessage, AudioMessage, FileMessage,
    UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent
)


load_dotenv(verbose=True)

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

ID = os.environ.get("ID")
PASSWORD = os.environ.get("PASSWORD")

CHANNEL_SECRET = os.environ.get('LINE_CHANNEL_SECRET', None)
CHANNEL_ACCESS_TOKEN = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN', None)

MY_GROUP_ID = os.environ.get('MY_GROUP_ID', None)
DROPBOX_API_TOKEN = os.environ.get('DROPBOX_API_TOKEN', None)

if CHANNEL_SECRET is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if CHANNEL_ACCESS_TOKEN is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)
if DROPBOX_API_TOKEN is None:
    print('Specify DROPBOX_API_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

dbx = dropbox.Dropbox(DROPBOX_API_TOKEN)

dir_path = os.path.dirname(os.path.abspath(__file__))

if __name__ == '__main__':
    today = datetime.date.today()
    today_str = today.strftime("%Y%m%d")

    options = ChromeOptions()
    options.add_argument('-headless')
    options.add_argument('--hide-scrollbars')
    driver = Chrome(executable_path='/usr/bin/chromedriver', options=options)

    driver.set_window_size(1024, 768)
    driver.get('ログインURL')

    driver.implicitly_wait(10)

    form_username = driver.find_element_by_name('u')
    form_username.clear()
    form_username.send_keys(ID)
    form_password = driver.find_element_by_name('p')
    form_password.clear()
    form_password.send_keys(PASSWORD)
    wait = WebDriverWait(driver, 10)

    input = wait.until(expected_conditions.element_to_be_clickable((By.XPATH, "//input[@value='ログイン']")))

    if input.get_attribute('type') == 'submit':
        input.click()
    else:
        print('Failed to find submit button.')

    driver.get('キャプチャを撮るURL')
    file_name = today_str + '.png'
    file_path = dir_path + '/' + file_name
    driver.save_screenshot(file_path)
    # dropboxへ保存&共有リンク取得
    dbx_file_name = '/' + file_name
    with open(file_path, 'rb') as f:
        dbx.files_upload(f.read(), dbx_file_name, mode=WriteMode('overwrite'))
        setting = dropbox.sharing.SharedLinkSettings(requested_visibility=dropbox.sharing.RequestedVisibility.public)
        link = dbx.sharing_create_shared_link_with_settings(path=dbx_file_name, settings=setting)

        links = dbx.sharing_list_shared_links(path=dbx_file_name, direct_only=True).links

        if links is not None:
            for link in links:
                url = link.url
                url = url.replace('www.dropbox','dl.dropboxusercontent').replace('?dl=0','')
                print(url)
                message = ImageSendMessage(
                    original_content_url=url, preview_image_url=url
                )
                line_bot_api.push_message(
                    MY_GROUP_ID,
                    message
                )

    driver.close()

このスクリプトをラズパイのCronに設定しておきます。

0 10 * * * /usr/bin/python3 /home/pi/Develop/get_bill.py >> ~/cron.log 2>&1

これで毎日10時にスクショを送ってくれるようになります。

f:id:ti_taka:20211029231129j:plain

まとめ

実装しようと思って色々調べたらやはりすでに実施されてる先人方の記事を見ながらサクッと実装出来たので助かりました。
Seleniumを使えばこれ以外にも夫婦間で情報共有するのに便利な事ができそうなので色々試してみようと思います。