【SeleniumBasic】地図のオプションを指定して距離と時間を取得する【Excel】

以前書いたこの記事について質問をいただきました。

これだと高速道路を使うかどうかにかかわらず、一番早い距離と時間だけが取得できてしまうのですが、高速道路を使った場合と使わない場合の距離と時間を両方取得したい、どうすればよいかというご質問です。

Googleマップのルート検索。赤い枠の中がオプション。Googleマップの利用規約によれば、地図の部分は著作権の問題があるようなのでこの図ではモザイクをかけてあります。

コード

まずは私が考えたコードを載せます。

Option Explicit
'ワークシートに表示する情報のヘッダ
Enum e
    出発地 = 1
    目的地
    高速時間
    高速距離
    下道時間
    下道距離
    TheEnd = 下道距離
End Enum
'
'作成:野口香
'作成:2023/10/07
'
Public Sub GetMap()
    '作業用シート
    Dim sh As Worksheet
    Set sh = ThisWorkbook.Worksheets(1)
    
    'Seleniumを用意
    Dim driver As Object
    Set driver = CreateObject("Selenium.WebDriver")
    
    'スクレイピングのお作法・・・wait時間(単位:ms)
    Const clSHORT_WAIT As Long = 500  '0.5sec。
    Const clLONG_WAIT As Long = 5000  '5sec。
   
    
    'Chromeをスタート
    driver.Start "Chrome"
    
    'ブラウザを最大サイズに
    driver.Window.Maximize
    
    '前回までの情報をクリアしておく
    Const clHeader As Long = 2  'Excelシートのデータのある表の列見出しのある行の行番号
    Const cstStart As Long = clHeader + 1      'Excelシートのデータが入っている行の1番目の行番号。clHeaderの次の行なのでプラス1
    sh.Range(sh.Cells(cstStart, e.高速時間), sh.Cells(sh.Rows.Count, e.TheEnd)).ClearContents
    
    'GoogleMapのURL
    Const cstUrl As String = "https://www.google.co.jp/maps"
    driver.Get cstUrl
    driver.Wait clSHORT_WAIT
    
    'ルート検索ボタンを押す
    driver.FindElementById("hArJGc").Click
    driver.Wait clSHORT_WAIT

    'Loop開始前、準備
    Dim lRow As Long
    lRow = sh.Cells(cstStart, e.目的地).End(xlDown).Row
    If lRow = sh.Rows.Count Then
        lRow = cstStart
    End If
    
    'ループ内で使うオブジェクト群の宣言
    Dim el As Object
    Dim Keys As Object      'SendKeysで使うSeleniumのキーボード操作用オブジェクト
    Set Keys = CreateObject("Selenium.Keys")
    Dim myBy As Object      'IsElementPresentで使う。IsElementPresentは要素が存在するか調べるメソッドで、引数にSlenium.Byオブジェクトを使う。
    Set myBy = CreateObject("Selenium.By")
    Dim ii As Long
    
    For ii = cstStart To lRow
        
        '出発地
        Set el = driver.FindElementByXPath("//*[@id=""sb_ifc50""]/input")
        el.Clear
        driver.Wait clSHORT_WAIT
        el.SendKeys sh.Cells(ii, e.出発地).Value
        driver.Wait clSHORT_WAIT
        
        '目的地
        Set el = driver.FindElementByXPath("//*[@id=""sb_ifc51""]/input")
        el.Clear
        driver.Wait clSHORT_WAIT
        el.SendKeys sh.Cells(ii, e.目的地).Value
        driver.Wait clSHORT_WAIT
        
        ' 検索ボタンをクリック
        driver.FindElementByXPath("//*[@id=""directions-searchbox-1""]/button[1]").Click
        driver.Wait clLONG_WAIT '少し長めに待つ
        
        '高速道路のオプションの要素が存在するかどうか
        If driver.IsElementPresent(myBy.XPath("//*[@id=""pane.directions-options-avoid-highways""]")) Then
            '既にオプションが表示されている
        Else
            'オプションが表示されていない
            '→オプションを表示する
            driver.FindElementByXPath("//*[@id=""QA0Szd""]/div/div/div[1]/div[2]/div/div[1]/div/div/div[2]/button").Click
            driver.Wait clSHORT_WAIT
        End If
        
        '高速道路を使わないにチェック
        driver.FindElementByXPath("//*[@id=""pane.directions-options-avoid-highways""]").SendKeys Keys.Space
        
        '下道時間を取得
        sh.Cells(ii, e.下道時間).Value = driver.FindElementByXPath("//*[@id=""section-directions-trip-0""]/div[1]/div/div[1]/div[1]").Text
        driver.Wait clSHORT_WAIT
        
        '下道距離を取得
        sh.Cells(ii, e.下道距離).Value = driver.FindElementByXPath("//*[@id=""section-directions-trip-0""]/div[1]/div/div[1]/div[2]/div").Text
        driver.Wait clSHORT_WAIT

        '高速道路を使用しないのチェックを外す(高速道路を使う)
        driver.FindElementByXPath("//*[@id=""pane.directions-options-avoid-highways""]").SendKeys Keys.Space
    
        '高速時間を取得
        sh.Cells(ii, e.高速時間).Value = driver.FindElementByXPath("//*[@id=""section-directions-trip-0""]/div[1]/div/div[1]/div[1]").Text
        driver.Wait clSHORT_WAIT
        
        '高速距離を取得
        sh.Cells(ii, e.高速距離).Value = driver.FindElementByXPath("//*[@id=""section-directions-trip-0""]/div[1]/div/div[1]/div[2]/div").Text
        driver.Wait clSHORT_WAIT
    Next ii
    
    '終了
    driver.Close
    
    'オブジェクト解放
    Set sh = Nothing
    Set driver = Nothing
    Set myBy = Nothing
    Set Keys = Nothing
    Set el = Nothing
    
    '終了メッセージ
    MsgBox "終了"
    
End Sub

今回ご質問をいただいた際にも言及がありましたが、Googleマップのルート検索のXPath(HTMLタグ)については修正が入っており、以前書いた記事にあるコードでは動きませんでした。このような修正はサイレントに行われるので、使う側は気が付いたらその都度修正しないといけないです。私が今回書いたこのコードも、いずれ使えなくなる時がくると思います。

解説

チェックボックス

Googleマップの「オプションを表示」をクリックすると、下記のようなチェックボックスが現れます。

チェックボックス

一見するとチェックボックスのように見えますが、F12で中身を見てみると、inputタグを使っていました。

inputタグにはSendKeysで値をセットすればよいのですが、今回の場合は1とかTRUEとか、具体的な値を入れるのではないようです(試してみたけど動かなかったので)。

チェックボックスの上でスペースキーを押すとチェックボックスにチェックが入ることが動作確認できている(Enterキーを押下でも動いた)ので、それならばSendKeysでキーボードのスペースキーを送ってやればよいと考えました。

そこで、次のようなコードとなります。

driver.FindElementByXPath("//*[@id=""pane.directions-options-avoid-highways""]").SendKeys Keys.Space

スペースキーをinputタグに送るときには、Keys.Spaceとします。

Keysというのは、Selenium.Keysオブジェクトのことで、あらかじめDimで宣言しておく必要があります。

Dim Keys As Object      'SendKeysで使うSeleniumのキーボード操作用オブジェクト

私はObject型で宣言しましたが、事前にSelenium Type Libraryを参照設定してあるなら、次のような記述をすることも可能です。

Dim Keys As Keys

私の場合はコードをWebに公開しており、私の記事を見た人が参照設定をするかどうか保障はないので、なるべく参照設定が必要のない記述を心がけています。そのためAs Objectとしました。

オブジェクトを宣言したら、初期化が必要です。初期化は次のように書くことができます。

Set Keys = CreateObject("Selenium.Keys")

これも、Selenium Type Libraryの参照設定がしてあるなら、次のような書き方をすることもできます。

Set Keys = New Keys
参照設定(画像の一部を加工してあります)

ところで、KeysはDictionaryでも使います。ほかにもKeysを使うクラスはたくさんあります。

単にNew Keysと書いてあるより、CreateObject(“Selenium.Keys”)と書いてある方が、どこの所属のKeysか分かって分かりやすいと思いませんか?

私はそう思うので、あえて参照設定は使わず、CreateObject(“Selenium.Keys”)で初期化しています。

1点注意があります。

今回、次のコードは全く同じです。

        '高速道路を使わないにチェック
        driver.FindElementByXPath("//*[@id=""pane.directions-options-avoid-highways""]").SendKeys Keys.Space
        '高速道路を使用しないのチェックを外す(高速道路を使う)
        driver.FindElementByXPath("//*[@id=""pane.directions-options-avoid-highways""]").SendKeys Keys.Space

違うのは、スペースキーを押すタイミングです。

何回か実験してみたところ、Googleマップのルート検索のオプションを表示のチェックボックスは、デフォルトではチェックが外れていることが確認できました。

そこで、最初にチェック無→チェック有にすることで、下道の時間と距離を把握します。

次に、チェック有→チェック無にすることで、高速道路を使った時間と距離を把握しています。

オプションを表示

ループの処理の中で困ったのが、「オプションを表示する」がすでに表示されているかどうかを把握することでした。

今回、それを判定するために、高速道路のチェックボックス(実際にはinputタグ)の要素が画面上に表示されているかどうかを判断することにしました。

判断するためには、IsElementPresentを使います。

IsElementPresentについては、以前記事を書いたことがありますので、よろしかったらそちらをご参照ください。

この記事の中で、Selenium.Byの使い方に言及しています。

Byの使い方もKeysと同様です。

最初に宣言(Dim)、次に初期化です。

Enumを使うのはなぜか

私のコードの冒頭ではEnumを宣言しています。

ワークシートのヘッダ行をEnumで宣言

これはなぜかという質問も、今回同時にいただきました。

これは可読性を増すためと、作業効率を上げるためです。

例えば、sh.Cells(ii, e.目的地) を sh.Cells(ii,2) のように書いてもいいはずです。

しかし、それだと「2」という数字の意味をどこかにコメントで書いておかないと、自分以外の誰かが見たときに分かりません。

このような生の数字をコードに書くと、「マジックナンバー」と言って、プログラムの世界では嫌われます。

マジックナンバーの意味は、Wikiを見てください。

https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%B8%E3%83%83%E3%82%AF%E3%83%8A%E3%83%B3%E3%83%90%E3%83%BC_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0)

また、Enum化するとVBEのインテリセンス機能が使えるようになります。

インテリセンスというのは、次のような選択肢の表示です。

Enumしておいたメンバが、「e.」と入力したときに表示されて便利。

これが使えると、いちいちワークシートに戻って、「あれ?高速道路って左から何番目だったっけ?」などとしなくて済みます。

もう一つ、可読性を上げる要素として強調しておきたいのは、コメントがいらないということです。

例えば、本来ちゃんとしたシステムであれば日本語の2バイト文字(漢字やひらがな)をプログラムコードになんて書けません。全部英数字で書かないといけないのです。

下のコードは全部英文字で書いてみたEnumです。いちいちコメントを添えないといけません。

Enum f
    START_POS = 1  '出発地
    END_POS         '目的地
    HIGHWAY_TIME    '高速時間
    HIGHWAY_DURATION '高速距離
    LOCAL_TIME      '下道時間
    LOCAL_DURATION  '下道距離
    TheEnd = LOCAL_DURATION
End Enum

ところが、VBAでは2バイト文字も変数に使えるので、私はあえて使っています。そうすることで、コメントがいらない、シンプルなコードに近づけると思うからです。

Enum e
    出発地 = 1
    目的地
    高速時間
    高速距離
    下道時間
    下道距離
    TheEnd = 下道距離
End Enum

上のようなEnumだと、2バイト文字(漢字ひらがな)で定数名を宣言しつつ、コメントも兼ねており、少なくとも日本語を読む人が目で見て一目で分かるすっきりしたコードとなっています。

ヘッダ行をEnum化するテクニックについては、以前記事にしていますので、こちらの記事も参考にしてみてください。

おまけ

今回作成したマクロブックをZIPにして置いておきます。

https://kn-sharoushi.com/wp-content/uploads/2023/10/20231007getGoogleMap.zip

自由に改変してかまいませんが、著作権は放棄していません。