【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です。
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
ESRと言うのが何の意味が分からないのですけど、geckodriverが0.32.1で、Firefoxの最小が102、最大がn/a(該当なし)なので、109が使えるという理解でいます(あってる?)。
参照設定
ExcelVBAにおいて、私は普段ほとんど参照設定を使っていないのですけど(人に配布するときに参照設定を設定してもらうのが困難だから)、今回は設定しました。
これも元ネタサイトに紹介があります。Microsofrt 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で説明します。
検索窓に「次へ」と入力します。これは「次へ」ボタンのタグを調べようとしているところです。
すると、「次へ」があるタグが見つかりました。
マウスをタグに合わせてみるとブラウザのログイン画面の「次へ」ボタンがフォーカスされます。
それでこれがこのボタンのタグで間違いないことが確認できましたので、あとはボタンのタグのところで右クリック→コピー→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は私の試行錯誤の記録です。
-
前の記事
【ExcelVBA】VBAでFirefoxを使うには 2023.02.05
-
次の記事
【ExcelVBA】Web情報でシートを作成しリンクを貼る【Seleniumなしでスクレイピング】【Firefox】Part1 2023.02.07