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

これらの記事↓の続きです。

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

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

今回の質問者さんの要望が、「ExcelVBAでFirefoxを使う」「Yahoo!のトップページにあるカテゴリごとにワークシートを作り記事の一覧を作ってリンクを貼る」というものでした。

完成

最初に完成したコードを提示しておきます。

Option Explicit

Sub YahooTopからカテゴリ別にシートを自動生成しリンクを貼る()

    'Webdriverの起動。Firefoxはデフォルトで4444番ポートを使用
    Shell "C:\drivers\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")
    
    'ブラウザ起動
    Const cstLocalhost As String = "http://localhost:4444/session"
    Dim oBuf As Object
    Set oBuf = SendRequest(client, "POST", cstLocalhost, params)
    
    'SessionIdを取得
    Dim sessionId As String
    sessionId = oBuf("value")("sessionId")

    'URL遷移用のパラメータの作成
    Set params = New Dictionary
    Const cstYahooUrl As String = "https://www.yahoo.co.jp/"
    params.Add "url", cstYahooUrl

    '遷移
    SendRequest client, "POST", cstLocalhost & "/" + sessionId + "/url", params
    
    'tabtopicsを探すためのパラメータを準備
    Set params = New Dictionary
    params.Add "using", "css selector"
    params.Add "value", "[role=""tab""]"
    
    'tabtopicsのエレメント群を取得
    Dim elementId As String
    Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/elements", params)
    
    'エレメント群が格納されているコレクションを取得するための変数を用意
    Dim tabtopics As Collection
    Set tabtopics = New Collection
    Set tabtopics = oBuf("value")
    
    'tabtopicsの件数
    Dim ltabTopicsCnt As Long
    ltabTopicsCnt = tabtopics.Count
    
    'エレメントIDを示すキー文字列
    Const cstE As String = "element-6066-11e4-a52e-4f735466cecf"
    
    'ループ内で使う変数の宣言
    Dim ii As Long, jj As Long, kk As Long, mm As Long
    
    'ワークシート操作関連の変数
    Dim lRow As Long
    Dim stText As String, stId As String
    Dim stShName As String
    Dim Bk As Workbook, tSh As Worksheet
    Set Bk = ThisWorkbook
    
    'articleタグ用
    Dim artElements As Collection
    Dim artId As String
    Dim lartCnt As Long
    
    'ulタグ用
    Dim ulElements As Collection
    Dim ulId As String
    Dim lulCnt As Long
    
    'aタグ用
    Dim aElements As Collection
    Dim aId As String
    Dim laCnt As Long
    
    For ii = 1 To ltabTopicsCnt
        
        'tabtopicsのタブ名を取得(例:ニュース、経済、エンタメ、スポーツ、国内、国際、IT・科学、地域
        elementId = tabtopics(ii)(cstE)
        stShName = SendRequest(client, "GET", cstLocalhost & "/" + sessionId + "/element/" + elementId + "/text")("value")
        
        'シートを追加しリネーム
        If IsExistThatsName(stShName) = True Then
            'すでに同名のシートが存在する
            Set tSh = Bk.Worksheets(stShName)
            'シートのセルの内容をクリアコンテンツする
            tSh.Cells.ClearContents
        Else
            'シート追加
            Set tSh = Bk.Worksheets.Add(After:=Bk.Worksheets(Bk.Worksheets.Count))
            'シート名変更
            tSh.Name = stShName
        End If
        
        'tabをクリックする(1以外)
        If ii = 1 Then
            'Nothing
        Else
            SendRequest client, "POST", cstLocalhost & "/" + sessionId + "/element/" + elementId + "/click", New Dictionary
        End If
        
        'シートのセルに書き込むための行番号を初期化
        lRow = 2
        
        'tabtopicsのtabごとに記事とリンクを把握する
        '記事一覧のことをtabpaneltopicsと言う。

        'tabpaneltopicsを取得するためのパラメータを作成
        Set params = New Dictionary
        params.Add "using", "css selector"
        params.Add "value", "#tabpanelTopics" & ii
        
        'tabpaneltopicsのエレメントを取得
        Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/element", params)
        stId = oBuf("value")(cstE)
        
        'tabpaneltopicsの下にある記事一つ一つを取得する
        
        'tabpaneltopicsの下にあるulを取得する
        Set params = New Dictionary
        params.Add "using", "tag name"
        params.Add "value", "ul"
        Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/element/" + stId + "/elements", params)
        Set ulElements = New Collection
        Set ulElements = oBuf("value")
        'ulの件数
        lulCnt = ulElements.Count
        
       'ulの配下にあるarticleタグを探す
        For jj = 1 To lulCnt
                    
            'ulタグのエレメントIDを取得
            ulId = ulElements(jj)(cstE)
            
            'articleを取得する
            Set params = New Dictionary
            params.Add "using", "tag name"
            params.Add "value", "article"
            Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/element/" + ulId + "/elements", params)
            Set artElements = New Collection
            Set artElements = oBuf("value")
            'articleの件数
            lartCnt = artElements.Count
            If ii = 7 Then
                'IT・科学のときだけ
                If jj = 1 And lartCnt > 0 Then
                    With tSh.Cells(lRow, 2)
                        'IT
                        .Value = "IT"
                        .ClearHyperlinks
                        With .Font
                            .Underline = xlUnderlineStyleNone
                            .ColorIndex = xlAutomatic
                        End With
                    End With
                    lRow = lRow + 1
                ElseIf jj = 2 And lartCnt = 0 Then
                    With tSh.Cells(lRow, 2)
                        '科学
                        .Value = "科学"
                        .ClearHyperlinks
                        With .Font
                            .Underline = xlUnderlineStyleNone
                            .ColorIndex = xlAutomatic
                        End With
                    End With
                    lRow = lRow + 1
                End If
            End If
                    
            'articleの配下にあるaタグを探して取得する
            For kk = 1 To lartCnt
                'articleタグのエレメントIDを取得
                artId = artElements(kk)(cstE)
                
                'aタグを取得
                Set params = New Dictionary
                params.Add "using", "tag name"
                params.Add "value", "a"
                Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/element/" + artId + "/elements", params)
                Set aElements = New Collection
                Set aElements = oBuf("value")
                laCnt = aElements.Count
                
                For mm = 1 To laCnt
                    
                    'aタグのエレメントIDを取得
                    aId = aElements(mm)(cstE)
                    
                    'aタグの中にあるh1タグを取得
                    Set params = New Dictionary
                    params.Add "using", "tag name"
                    params.Add "value", "h1"
                    Set oBuf = SendRequest(client, "POST", cstLocalhost & "/" + sessionId + "/element/" + aId + "/element", params)
                    
                    'h1タグのエレメントIDを取得
                    stId = oBuf("value")(cstE)
                    
                    'h1タグのテキストを取得
                    stText = SendRequest(client, "GET", cstLocalhost & "/" + sessionId + "/element/" + stId + "/text")("value")
                    'セルに書き込む
                    tSh.Cells(lRow, 2).Value = stText
                    
                    'リンクアドレスを取得
                    stText = SendRequest(client, "GET", cstLocalhost & "/" + sessionId + "/element/" + aId + "/property/href")("value")
                    'ハイパーリンクを設定
                    tSh.Hyperlinks.Add Anchor:=tSh.Cells(lRow, 2), Address:=stText
                    
                    '文字折り返しなしに設定
                    tSh.Cells(lRow, 2).WrapText = False
                    
                    '行番号操作用変数インクリメント
                    lRow = lRow + 1
                Next mm
            Next kk
        Next jj
    Next ii
    
    '終了処理
    EndProc
    
    'メモリ解放などの後始末
    Set params = Nothing
    Set client = Nothing
    Set oBuf = Nothing
    Set tabtopics = Nothing: Set artElements = Nothing
    Set ulElements = Nothing: Set aElements = Nothing
    Set Bk = Nothing: Set tSh = Nothing

End Sub

Private Function SendRequest(ByRef client As Object, method As String, url As String, Optional data As Dictionary = Nothing) As Dictionary
    Dim stBuf As String
    'メソッドに応じてリクエスト送信
    client.Open method, url
    If method = "POST" Or method = "PUT" Then
        client.setRequestHeader "Content-Type", "application/json"
        stBuf = JsonConverter.ConvertToJson(data)
        client.Send stBuf
    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

'同一シート名が存在しないか調査
'存在するならTrueを返す
Private Function IsExistThatsName(ByVal stName As String) As Boolean
    Dim sh As Worksheet
    Dim Flg As Boolean
    
    Flg = False
    
    For Each sh In ThisWorkbook.Worksheets
        If sh.Name = stName Then
            Flg = True
            Exit For
        End If
    Next sh
    
   IsExistThatsName = Flg
    
End Function

'開いているWebdriverを終了させる
Private Sub EndProc()
    Dim Wd As Object
    
    Set Wd = CreateObject("Word.Application")
    If Wd.Tasks.Exists("geckodriver.exe") Then
        Wd.Tasks("geckodriver.exe").Close
    End If
    Wd.Quit
    Set Wd = Nothing
    
End Sub

実行結果

上記のコードを実行するとこんな感じになります。

解説

ループの入れ子

記事一覧をループの入れ子で取得しています。

CSS セレクタで一気に取得といきたいところでしたが、IT・科学のところで他のタブと異なるセレクタだったので挫折しました。

IT・科学のシート。ITと科学に分かれている。

地道にループで回して細かく拾っていく方式にしましたので、冗長なコードとなっています。

IT・科学

ITと科学の部分をどうやって制御するか悩みました。

正直言ってCSSセレクタやTagなどの情報だけで制御することが私には難しかったので、ループカウンタが一定の値をとった瞬間で制御することにしました。

ITと科学を制御している個所のコード

つまり全く汎用性も弾力性も可塑性もないです。面目ない。

同名シートの有無チェック

すでに同名シートが存在していた場合はシートを追加せず、そのシートの内容をいったんクリアコンテンツするようにしました。

同名シートが存在するかチェックするFunction

同名シートが存在するおそれが全くないなら不要な制御だと思いますので、適宜削除してください。

私は何度もデバッグするにあたり、必要となったので作りました。

ドライバを終了させる

shellで起動したgeckodriver.exeをどうやって終了させるか迷ったのですが、OfficeTANAKAさんのこちらの記事を参考に、WordのTasksで終了させることにしました。

APIを使わないで実行中のタスク一覧を調べる

あまりお行儀のよい終わらせ方だとは思えないので(そもそもWordがインストールされているとは限らない)、ちゃんと終了させたいなら、こういった記事↓を参考に、TaskIDからWindowハンドルをゲットしてしかるべき手順で終了させるようにしてください(私はそこまでやる根気がなかった)。

VBAのShell関数で起動したアプリの終了方法について

https://teratail.com/questions/36610

蛇足

一応これで質問者さんの質問に答えたことになりますが(Yahoo!IDでログインするは実装していませんが)、蛇足でどうしても言っておきたいことがあります。

これって、使い方によってはYahoo!のサーバに迷惑をかけるので、多用はしないでください。

例えば、1日1回だけ使うならまだしも、1時間の間に何回も何回も何回も起動して情報をとってくるのは、サーバに対するDOS攻撃みたいなことにならないか心配しています。

利用規約を守って、快適なWebライフを!