/*Google AdSense自動広告*/

2020年10月8日木曜日

Selenium+PythonでWeb操作を自動化する→実例:LINEモバイルの複数アカウントの月額料金を自動取得する

今流行のRPA−業務自動化は、在宅ワークと相まって、仕事をパソコンにまかせて酒を呑むことが最終目標となっていることでしょう。

その中でWeb操作の自動化(ウェブスクレイピング)は不安定で動作が遅く、積極的にやるべきものではありませんが、Web上でしか情報を得られない場合はやるしかありません。

今回は、Python + Seleniumで、LINEモバイルの複数アカウントの月額料金を自動取得するプログラムを作りました。

OS : Windows 10
ブラウザ:Chrome



Pythonのインストール


通常のダウンロードボタンからだと、32bit版が選択されてしまうようです。


そこで、上記ページの「Download Windows x86-64 web-based installer」を選び、実行します。
→ver.3.9.0からは64bitがデフォルトとなり、Windows7が非対応となっています。

コマンドプロンプト/PowerShellから「python」と打って、バージョン情報のあとにプロンプト「>>>」が出ればインストール成功です。終了は「exit()」です。括弧を忘れずに。

Seleniumとchromedriver-binaryのインストール

Seleniumとは、色々な言語・ブラウザでWeb操作を自動化するツールです。今回はPython+Chromeで開発を行いましたが、例えばJava+Firefox等の組み合わせも可能です。
Pythonでは、パッケージ管理ツールの「pip」からインストールできます。
python -m pip install --upgrade pip
pip install selenium
pip install chromedriver-binary

最後の行のchromedriver-binaryは、Chromeを操作するためのドライバー。本体はchromedriver.exeという実行ファイルで、これを介してブラウザを操作します。

ただし、このchromedriver.exeは、操作するブラウザのバージョンに依存します。

これが色々と厄介で、例えば会社のPCは古いバージョンのChromeが入っている、それに合わせて開発したら、自動バージョンアップされて動かなくなる、という事態がまあ頻繁に起きます。

バージョンを指定してインストールするには、まずPCに入っているChromeのバージョンを確認します。

右上の「…」ボタン → ヘルプ → Google Chromeについて をクリックすると、以下の画面が出てきます。


多くの場合、メジャーバージョンを合わせれば動作するので、小数点以下は無視して、「85」。次に、chromedriverのdownloadページから…


「If you are using Chrome version 85, please download ChromeDriver 85.0.4183.87」←これが、使用すべきchromedriverのバージョンです。

バージョン指定してインストールするコマンドは↓

pip install chromedriver-binary==85.0.4183.87

この他に、chromedriver.exeをダウンロードし、パスを直接指定する方法もありますが、どちらにしてもChromeのメジャーバージョンアップ時にはメンテが必要です。仕事で使っていると、朝から「使えないよ!!」の声がブワッと挙がり…

VSCodeのインストール(任意)


Visual Studio Code(VSCode)とは、Microsoft社製のテキストエディタ・総合開発ツールです。エディタと言うと、ちょっと昔は個人作成のフリーソフトが流行りでしたが、Google(Atom)とMicrosoft(VSCode)の2大巨頭が進出し、VSCodeがスタンダードになりつつあります。Atomは重くて不安定だったけど、今はどうだろう?

もちろん、好みのテキストエディタでコードを書き、コマンドプロンプト(今はPowerShell/Terminalが推奨されているが…)でpythonを実行しても良いのですが、一つの画面で出来たら作業がはかどります。

Visual Studio Codeダウンロードページ

インストールしたら、左のエクステンションメニュー(ブロックが積まれたようなアイコン)から、検索ボックスに「Python」と入力し、Microsoft謹製ツールをインストールします。


これで、Pythonコードのハイライトや、入力支援、プログラム実行機能が使用できます。



Selenium基本動作のクラスを作成


これで準備が整いました。あとは、コードを書いて実行するだけです。

その前に、Webページに対する「クリック」「文字入力」等の基本操作をまとめてクラス化し、別ファイルにして管理するとメンテナンスが楽になります。


SeleniumFunctions.py

import sys
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.select import Select  
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options

class SeleniumFunctions:

    def __init__(self, default_wait=30):
        self.default_wait = default_wait

    def open_chrome(self, url, is_headless, page_timeout=60):

        options = Options()
        if is_headless:
            options.add_argument('--headless')

        self.driver = webdriver.Chrome(options=options)
        self.driver.set_page_load_timeout(page_timeout)

        try:
            self.driver.get(url)
        except:
            print('Error or Timeout')

    def close_chrome(self):
        self.driver.quit()

    def click(self, xpath):
        try:
            WebDriverWait(self.driver, self.default_wait).until(EC.visibility_of_element_located((By.XPATH, xpath)))
            WebDriverWait(self.driver, self.default_wait).until(EC.element_to_be_clickable((By.XPATH, xpath)))
            element = self.driver.find_element_by_xpath(xpath)
            element.click()
            print(f'SUCCESS : {xpath} clicked')
        except:
            print(f'ERROR : {xpath} could not click. {str(sys.exc_info()[0])}')

    def input(self, xpath, input_str):
        try:
            WebDriverWait(self.driver, self.default_wait).until(EC.visibility_of_element_located((By.XPATH, xpath)))
            element =self.driver.find_element_by_xpath(xpath)
            element.send_keys(input_str)
            print(f'SUCCESS : input {input_str} to {xpath}')
        except:
            print(f'ERROR : {xpath} could not input. {str(sys.exc_info()[0])}')

    def select(self, xpath, select_val):
        try:
            WebDriverWait(self.driver, self.default_wait).until(EC.visibility_of_element_located((By.XPATH, xpath)))
            element = self.driver.find_element_by_xpath(xpath)
            Select(element).select_by_value(select_val)
            print(f'SUCCESS : select {select_val} to {xpath}')
        except:
            print(f'ERROR : {xpath} could not select. {str(sys.exc_info()[0])}')

    def get_text(self, xpath):
        try:
            WebDriverWait(self.driver, self.default_wait).until(EC.visibility_of_element_located((By.XPATH, xpath)))
            element = self.driver.find_element_by_xpath(xpath)
            print(f'SUCCESS : get text from {xpath}')
            return element.text            
        except:
            print(f'ERROR : {xpath} could not get text.  {str(sys.exc_info()[0])}')

関数の解説


driver = webdriver.Chrome(options=options)でChromeを起動し(ヘッドレスモード=非表示も可能)、driver.get(URL)でURLのページを開きます。

WebDriverWaitは、HTML上に現れた(visibility_of_element_located)、クリック可能になった(element_to_be_clickable)等の状態まで、タイムアウト時間を指定して待つものです。

プログラムはページ読み込みと関係なく進むので、例えばボタンが表示される前に押そうとするため、Waitは必須です。このあたりは、ページ構成によって何を待てばうまく動作するか、試行錯誤しなければなりません。


XPATHによる指定


HTMLの要素を特定するには、class、name、idなどがありますが、ここではXPATHによる指定で統一しました。select_by_id、select_by_name…とたくさん関数が並ぶのは嫌でしょう?

XPATHはChromeで調べることができます。調べたい要素を右クリック→検証



上記の様なデベロッパーウィンドウが出てきます。対象の要素がハイライトされない場合は、もう一度右クリック→検証すると、ソースがハイライトされます。このようにして、まずは手動でページを遷移しながら、要素のXPATHを調べるのです。

LINEモバイルマイページにログインし、月額料金を取得するPythonプログラム

先のSeleniumFunctions.pyは、C:\myCodes\Selenium配下に置いて、sys.path.appendでフォルダを追加した後にimportします。これで本体はだいぶスッキリしますね。

import sys
import time
import re
from os import path
sys.path.append('C:/myCodes/Selenium')
from SeleniumFunctions import SeleniumFunctions

ini_file = path.join(path.dirname(__file__), 'line_accounts.ini')

with open(ini_file, 'r', encoding='utf-8') as f:
    iniLines = f.readlines()

result_text = ''
for iniLine in iniLines:
    currentId = iniLine.strip().split(",")[0]
    currentPass = iniLine.strip().split(",")[1]

    sf = SeleniumFunctions()
    sf.open_chrome('https://mobile.line.me/mypage/bill/', True)
                
    sf.input('//input[@name="loginAccount"]', currentId)
    sf.input('//input[@name="loginPassword"]', currentPass)
    sf.click('//input[@id="FnNextBtn"]')

    result_text += currentId + '\n' + sf.get_text('//div[@data-index="1"]') + '\n'

    sf.close_chrome()

print(result_text)
output_file = path.join(path.dirname(__file__), re.search(r'\d+年\d+月', result_text).group() + '_LINE月額料金.txt')

with open(output_file, 'w', encoding='utf-8') as f:
    f.writelines(result_text)      


同フォルダにline_accounts.iniを置き、ここに

line id1,pass1
line id2,pass2
....

のように複数アカウントのIDとパスの組み合わせをコンマ区切りで記載します。ファイルの読み込みはreadlinesで一発ですが、改行が含まれるのでstrip()で取り除きます。ここ、忘れがちで、改行が入っているためのエラーはありがちです。

LINEモバイルのページの料金明細ですが、左のナビゲーター(navタグ)にある「料金明細」がクリックできない問題が…。clickableでないのか?javascriptで直接クリックすればいいのか?迷いましたが、最初から料金明細のURL(https://mobile.line.me/mypage/bill/)に飛んで、ログイン画面へ自動遷移される動作を利用しました。Seleniumを使う自動化の場合、入力パスの保存や履歴は継承されず、まっさらなブラウザが立ち上がります。そのため、必ずログイン画面へ飛ぶのです。

ログイン画面でinput boxにIDとパスを入力します。要素にidがあればそれを使うのがベストです。今回は一部nameを使用しています。

料金明細はテーブルやテキストボックスでなく、divの中に羅列されるだけなので、div内のテキストを取得するしかありません。正規表現検索re.searchで△年◆月の文字列を抽出し、ファイル名に使用しています。必要ならば、このテキストを分析してCSVの綺麗な表にするのもいいと思います。


こんな感じで、複数アカウントの月額料金を羅列したテキストファイルが出来上がります。


line_id1
2020年10月
¥586
消費税 10%対象分:
¥1,322
課税対象外など:
¥-736
LINEフリープラン 1GB / 音声通話SIM
¥1,200
....
line_id2
....
line_id3
....

何度もログイン→ログアウトを繰り返すのは面倒ですよね。特に家族全員LINEモバイルで、支払いと家計管理者が一人の場合。

いや、LINEモバイルが複数アカウントの料金明細一覧表示に対応すればいい話だけど…問い合わせたら、無理だそうなので、まあ頭の体操ついでに自動化した次第です。お気に召したらご賞味ください。



0 件のコメント:

コメントを投稿