【ExcelVBA】VBAからFirefoxをSeleniumなしでスクレイピング

VBAからSeleniumを使わずFirefoxでスクレイピングする方法について調べましたので記事にします。

元ネタ

参考にしたのはこちらのサイトです。

Excel VBAでSeleniumBasicを使わずにスクレイピングする

https://qiita.com/uezo/items/66e20b064ffd5f239b9a

↑こちらのサイトではChromeとEdgeを対象としていましたので、私はFirefox用にカスタマイズしました。

事前準備

JsonConverter

JsonConverter.basが必要です。

これも元ネタサイトで紹介してありますので、そちらからダウンロードしてください。

念のためこちらでもリンクを貼っておきます。

https://github.com/VBA-tools/VBA-JSON/releases/tag/v2.3.1

「Source code」をダウンロードして解凍するとJsonConverter.basが入っていますので、これをVBAのプロジェクトでインポートすればOKです。

インポートしたJsonConverter.bas

Webdriver

Firefox用のWebdriverが必要です。

FirefoxのWebdriverはgeckodriver.exeといいまして、こちら↓からダウンロードできます。

https://github.com/mozilla/geckodriver/releases/tag/v0.32.1

この記事執筆当時の最新版がv0.32.1でした。

Firefoxのバージョンが109です。

Webdriverとブラウザはバージョンを合わせる必要がありまして、バージョンの対応表はこちらで確認できます。

https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html

https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html

ESRと言うのが何の意味が分からないのですけど、geckodriverが0.32.1で、Firefoxの最小が102、最大がn/a(該当なし)なので、109が使えるという理解でいます(あってる?)。

参照設定

ExcelVBAにおいて、私は普段ほとんど参照設定を使っていないのですけど(人に配布するときに参照設定を設定してもらうのが困難だから)、今回は設定しました。

これも元ネタサイトに紹介があります。Microsofrt Scripting Runtimeです。

Microsoft Scripting Runtimeの参照設定

Firefox用のコード

最初に、元ネタサイトにあったChromeでGoogleを開いてcatを検索する一連のコードを、私がFirefoxバージョンに修正したものを上げておきます。

Option Explicit

Sub Main()
    'WebDriverの起動。デフォルトで4444番ポートを監視(元ネタは9515ポート)
    Shell "C:\Users\hogehoge\AppData\Local\SeleniumBasic\geckodriver.exe", vbMinimizedNoFocus

    'ブラウザ起動パラメータの作成
    Dim params As New Dictionary
    params.Add "capabilities", New Dictionary
    params.Add "desiredCapabilities", Nothing

    'HTTPクライアントの起動
    Dim client As Object
    Set client = CreateObject("MSXML2.ServerXMLHTTP")
    
    'ブラウザ起動
    Dim oBuf As Object
    Set oBuf = SendRequest(client, "POST", "http://localhost:4444/session", params)
    
    'ブラウザ起動処理の戻り値からSessionIdを取得
    Dim sessionId As String
    sessionId = oBuf("value")("sessionId")

    'URL遷移用のパラメータを定義
    Dim navparams As New Dictionary
    navparams.Add "url", "https://www.google.co.jp/?q=cat"

    '遷移
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/url", navparams
    
    '検索テキストボックスを取得するためのパラメータを準備(name属性がq)
    Dim elmparams As New Dictionary
    elmparams.Add "using", "css selector"
    elmparams.Add "value", "[name=""q""]"

    '検索テキストボックスを取得して`elementId`に控えておく
    Dim elementId As String
    elementId = SendRequest(client, "POST", "http://localhost:4444/session/" + sessionId + "/element", elmparams)("value")("element-6066-11e4-a52e-4f735466cecf")

    '取得結果を表示
    Debug.Print elementId
    
    Dim searchValue As String
    searchValue = SendRequest(client, "GET", "http://localhost:4444/session/" + sessionId + "/element/" + elementId + "/attribute/value")("value")
    Debug.Print searchValue
    
    Dim text As String
    text = "猫 サバトラ白"

    '1文字ずつに区切る
    Dim chars() As String
    ReDim chars(Len(text) - 1)
    Dim i As Long
    For i = LBound(chars) To UBound(chars)
        chars(i) = Mid(text, i + 1, 1)
    Next i

    '値入力用のパラメータを準備
    Dim valparams As New Dictionary
    valparams.Add "text", text
    valparams.Add "value", chars

    '既に入力されているcatを消す
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/element/" + elementId + "/clear", New Dictionary

    '値入力の指示
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/element/" + elementId + "/value", valparams
    
    '検索ボタン取得のパラメータの準備(name属性がbtnK)
    Dim btnelmparams As New Dictionary
    btnelmparams.Add "using", "css selector"
    btnelmparams.Add "value", "[name=""btnK""]"

    '検索ボタンを取得して`elementId`に控えておく
    Dim btnElementId As String
    btnElementId = SendRequest(client, "POST", "http://localhost:4444/session/" + sessionId + "/element", btnelmparams)("value")("element-6066-11e4-a52e-4f735466cecf")

    Debug.Print "btnElementId=" & btnElementId
    
    '検索ボタンをクリック
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/element/" + btnElementId + "/click", New Dictionary
   
    '終了処理(メモリ解放などの後始末)
    Set params = Nothing
    Set client = Nothing
    Set oBuf = Nothing
    Set navparams = Nothing
    Set elmparams = Nothing
    Set valparams = Nothing
    Set btnelmparams = Nothing
    
End Sub

Private Function SendRequest(ByRef client As Object, method As String, url As String, Optional data As Dictionary = Nothing) As Dictionary

    'メソッドに応じてリクエスト送信
    client.Open method, url
    If method = "POST" Or method = "PUT" Then
        client.setRequestHeader "Content-Type", "application/json"
        client.Send JsonConverter.ConvertToJson(data)
    Else
        client.Send
    End If

    '送信完了待ち
    Do While client.readyState < 4
        DoEvents
    Loop

    'レスポンスをDictionaryに変換してリターン
    Dim Json As Object
    Set Json = JsonConverter.ParseJson(client.responseText)
    Set SendRequest = Json
    
    'メモリ解放
    Set Json = Nothing
End Function

冒頭部分のgeckodriver.exeへのパスは実際の環境に合わせてください。

SendRequestの引数にclientを追加したのは、サブで毎回Objectを生成し解放するというのがどうにもメモリ操作的にうるさく感じたからです。

これくらいの小さな処理なら別に元ネタのままでもよかったのですが、私のこだわりで変えました。

本題 – FirefoxでYahoo!にログイン

ここからが本題です。

今回の調査のきっかけが、こちらの質問でして、

https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q13274674623

自身のYAHOO IDでログインをし、添付のようにYAHOO JAPANトップから 赤枠のニュース蘭をカテゴリ別にシートをVBAにて自動生成して それぞれのシートに右図のようなニュース文字列を表示し、さらにリンクを付けて クリックしたらそれぞれのYAHOOページに飛ぶようなマクロを FIREFOXブラウザ(※IEじゃないです)にて行いたいです。

https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q13274674623

まずはYahoo!にログインをしないといけません。

ということで、元ネタサイトを参考に、Yahoo!にログインを試みたのがこちらのコードです。

Option Explicit

Sub Main()
    'WebDriverの起動。デフォルトで4444番ポートを監視
    Shell "C:\Users\hogehoge\AppData\Local\SeleniumBasic\geckodriver.exe", vbMinimizedNoFocus

    'ブラウザ起動パラメータの作成
    Dim params As New Dictionary
    params.Add "capabilities", New Dictionary
    params.Add "desiredCapabilities", Nothing

    'HTTPクライアントの起動
    Dim client As Object
    Set client = CreateObject("MSXML2.ServerXMLHTTP")
    
    'ブラウザ起動
    Dim oBuf As Object
    Set oBuf = SendRequest(client, "POST", "http://localhost:4444/session", params)
    
    'ブラウザ起動処理の戻り値からSessionIdを取得
    Dim sessionId As String
    sessionId = oBuf("value")("sessionId")

    'URL遷移用のパラメータを定義
    Dim navparams As New Dictionary
    navparams.Add "url", "https://login.yahoo.co.jp/config/login"

    '遷移
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/url", navparams
    
    'ログインテキストボックスを取得するためのパラメータを準備(id属性がlogin_handle)
    Dim elmparams As New Dictionary
    elmparams.Add "using", "css selector"
    elmparams.Add "value", "[id=""login_handle""]"

    'ログインテキストボックスを取得して`elementId`に控えておく
    Dim elementId As String
    elementId = SendRequest(client, "POST", "http://localhost:4444/session/" + sessionId + "/element", elmparams)("value")("element-6066-11e4-a52e-4f735466cecf")

    Dim text As String
    text = "hogehoge"   'ログインユーザID

    '1文字ずつに区切る
    Dim chars() As String
    ReDim chars(Len(text) - 1)
    Dim i As Long
    For i = LBound(chars) To UBound(chars)
        chars(i) = Mid(text, i + 1, 1)
    Next i

    '値入力用のパラメータを準備
    Dim valparams As New Dictionary
    valparams.Add "text", text
    valparams.Add "value", chars

    '値入力の指示
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/element/" + elementId + "/value", valparams
    
    '次へボタン取得のパラメータの準備(F12でcss selectorを調べる)
    Dim nextBtnparams As New Dictionary
    nextBtnparams.Add "using", "css selector"
    nextBtnparams.Add "value", "div.ar-button_medium_1i9SB:nth-child(1) > button:nth-child(1)"  '---①F12でcss selectorを調べる

    '次へボタンを取得して`elementId`に控えておく
    Dim btnElementId As String
    Set oBuf = SendRequest(client, "POST", "http://localhost:4444/session/" + sessionId + "/element", nextBtnparams)
    btnElementId = oBuf("value")("element-6066-11e4-a52e-4f735466cecf")

    Debug.Print "btnElementId=" & btnElementId  '---②ここにブレイクポイントを置いておく
    
    '次へボタンをクリック
    SendRequest client, "POST", "http://localhost:4444/session/" + sessionId + "/element/" + btnElementId + "/click", New Dictionary
   
    '終了処理(メモリ解放などの後始末)
    Set params = Nothing
    Set client = Nothing
    Set oBuf = Nothing
    Set navparams = Nothing
    Set elmparams = Nothing
    Set valparams = Nothing
    Set nextBtnparams = Nothing
      
End Sub

Private Function SendRequest(ByRef client As Object, method As String, url As String, Optional data As Dictionary = Nothing) As Dictionary

    'メソッドに応じてリクエスト送信
    client.Open method, url
    If method = "POST" Or method = "PUT" Then
        client.setRequestHeader "Content-Type", "application/json"
        client.Send JsonConverter.ConvertToJson(data)
    Else
        client.Send
    End If

    '送信完了待ち
    Do While client.readyState < 4
        DoEvents
    Loop

    ' レスポンスをDictionaryに変換してリターン
    Dim Json As Object
    Set Json = JsonConverter.ParseJson(client.responseText)
    Set SendRequest = Json
    
    'メモリ解放
    Set Json = Nothing
End Function

これを実行すると、まずはこのサイトが開き

コードにべた書きしたログインユーザIDを入力して次へボタンを押してくれます。

が、ここで問題が。

次の画面遷移でコードは終わります。これはなぜかというと、ここで行き詰ったからです(倫理的に)。

私は2段階認証を設定していまして、私のログインIDでは携帯に送られてきた確認コードを毎回手入力しないとログインできないのでした。

だめじゃん!

という訳で、自動でログインするのは挫折。

ていうか、自動ログインはサイバー攻撃につながるからやってはいけないのではなかったっけ?

慌ててYahoo!の利用規約を読みましたところ、スクレイピングは禁止されていないものの、「合理的に必要相当な数を超える利用、乱用などを行い、または、ソフトウエアの使用について当社が定める指示などを順守しない用法でソフトウエアを利用すること」は禁止されていました。

この自動ログインはどうなんだ・・・?合理的に必要相当な数を超える利用は(今のところ)していないけど・・・・?

ということで、このコードをこれ以上作りこむのはやめました。

もし続きをやりたい方がいるのなら、自己責任でお願いします。

どうやってコードを作っていったかを次に軽く説明しておきます。

解説

コードの基本的な解説は元ネタサイトさんで済んでいますので、私が変えたところだけここでは説明します。

①「次へ」のボタンのcssセレクタ

スクレイピングでやっかいなのはタグとかIDとか、Webページの要素をどうやって特定するかってことです。

今回のケースではこの「次へ」のボタンが意外にも特徴のないタグでしたので、特定するのがやっかいでした。

といってもそんなに難しくないです。

ブラウザで、該当ページを表示してある状態で、F12を押します。すると、たいていのブラウザでは開発者向けコンソールが表示されます。

FirefoxでF12を押したところ
ChromeでF12を押したところ

たいていのブラウザで開発者向けのツールは似たような機能が用意されているので、困らないと思います。ここではFirefoxで説明します。

インスペクターの検索窓で「次へ」と入力したところ

検索窓に「次へ」と入力します。これは「次へ」ボタンのタグを調べようとしているところです。

すると、「次へ」があるタグが見つかりました。

マウスをタグに合わせてみるとブラウザのログイン画面の「次へ」ボタンがフォーカスされます。

それでこれがこのボタンのタグで間違いないことが確認できましたので、あとはボタンのタグのところで右クリック→コピー→CSSセクレターを選びます。

これでCSSセレクタ用の文字列がゲットできました。

あとはこれをVBEに戻ってPOSTするときの引数、Dictionaryの”value”の値として貼り付ければOKです。

動的部品の場合はもう少し工夫が必要なんですが、今回は静的部品だったので楽でした。

②ボタンをクリックする前にブレイクポイント

実験中になんどかボタンをクリックするステップがうまくいかない(何も起きない)ことがありました。

これは通信をつかっている以上、仕方がないことでして、タイムラグがあるからです。

そこで②の個所にブレイクポイントを置いて時間稼ぎをして実験していました。

こういう点からも、自動ログインはちょっと難しいなと思います。

まとめ

以上、FirefoxでSeleniumを使わないでYahoo!にログインする方法(未完成)でした。

結局、二段階認証しない普通のログイン(ユーザIDとパスワードだけ)ができるテスト環境を用意することができなかったので、自動ログインはできていませんし、今後も完成させるつもりはありません。

興味のある方はご自身でどうぞ。ただし利用規約違反にならないようご注意ください。

20230211追記

この質問者さんのご要望のうち、自動ログインは実装できませんでしたが、そのほかの部分は実装しましたので、それを紹介している記事のリンクを貼っておきます。

【ExcelVBA】Web情報でシートを作成しリンクを貼る【Seleniumなしでスクレイピング】【Firefox】Part1

【ExcelVBA】Web情報でシートを作成しリンクを貼る【Seleniumなしでスクレイピング】【Firefox】Part2

【ExcelVBA】Web情報でシートを作成しリンクを貼る【Seleniumなしでスクレイピング】【Firefox】Part3

最終的なコードだけ見たい方は、Part3だけ見ればよいです。

Part1とPart2は私の試行錯誤の記録です。