見出し画像

[SeleniumVBA]テーブルの行と列の情報からセルの位置を特定する


1 はじめに

WEBスクレイピングをしていて、ExcelのVlookup関数のように、テーブルの行と列の情報からテーブル内のセルの位置を特定してクリックしたいと思うことはないでしょうか?私は頻繁にそう思いました。

インストール不要でありながら高機能なSeleniumVBAでは、ExcelVBAを利用して、テーブルの情報を二次元配列に格納することができますので、これを利用したいと思います。

2 やりたいこと

事例として、競馬ラボ様の競馬データベースをお借りしまして、まずコードの紹介を行ったあと、SeleniumVBAを実際に動作させ、初心者の方でも実感していただきたいと思います。

やりたいことは、画面中のテーブルのうち「順位」が5位の「騎手名」のセルを特定して、その騎手名のリンクをクリックすることです。

何だ、そんなもの見ればすぐ分かるじゃないか、と思われるもしれませんが、メンテのことも考え汎用性を持たせるとなると結構大変なことが分かってくるかと思います。

3 コード紹介

コード全体は以下のとおりです。コード冒頭で、見出しに「騎手名」があるテーブルのうち、「順位」が「5」の行の「騎手名」の列を拾うという内容の初期設定していることが分かるかと思います。理由はもちろん、後で変更できるようにするためです。

With driver
    '====================================================
    '【初期設定】
    '(適用条件は見出しが横並びでセル結合がないこと)
    '/他のテーブルにないユニークな見出し名
     Const uniqueTitle As String = "騎手名"
    '/検索キーの見出し名
     Const keyTitle    As String = "順位"
    '/検索キーの値
     Const keyValue    As String = "5"
    '/抽出列の見出し名
     Const resultTitle As String = "騎手名"
    '====================================================
    'URLの指定
    .NavigateTo "https://www.keibalab.jp/db/"
    
    '「1 テーブルの特定」
    Dim cnt As Long
    Dim elmtable As WebElement
    Dim elmTitle As WebElement
    Dim arrTable As Variant
    
    '見出し名の上位のタグが<tbody>なのか<thead>なのか
    If .IsPresent(By.XPath, "//th//*[text()='" & uniqueTitle & "']/ancestor::tbody", 100, , elmTitle) = True Then
    ElseIf .IsPresent(By.XPath, "//th//*[text()='" & uniqueTitle & "']/ancestor::thead", 100, , elmTitle) = True Then
    Else: Stop
    End If
    If .IsPresent(By.XPath, "//th//*[text()='" & uniqueTitle & "']/ancestor::table", 2000, elmTitle, elmtable) = False Then Stop
    Select Case elmTitle.GetTagName
        Case "tbody": arrTable = elmtable.TableToArray(skipHeader:=True)
        Case "thead": arrTable = elmtable.TableToArray
    End Select
    
    '結合セル有無のチェック(最初の行のみチェック)
    Dim htmlDoc As New HTMLDocument
    Dim htmlTbl As htmlTable
    Dim cell    As HTMLTableCell
    Dim cntLast As Long, cntTh As Long
    
    Set htmlTbl = htmlDoc.createElement("table")
    htmlTbl.innerHTML = elmtable.GetInnerHTML
    If elmTitle.GetTagName = "tbody" Then htmlTbl.deleteTHead
    cntLast = elmTitle.FindElements(By.XPath, ".//th/..").Count
    For cnt = 0 To cntLast
        For Each cell In htmlTbl.Rows(cnt).Cells
            If cell.rowSpan > 1 Or cell.colSpan > 1 Then
                AppActivate Application.caption
                MsgBox "結合セルがありますので処理を終了します": Exit Sub
            End If
            '明細行の1列目がthであった場合thの数をカウント
            If cnt = cntLast And cell.tagName = "TH" Then cntTh = cntTh + 1
        Next
    Next
                
    '「2 列番号/行番号の取得」
    '(1) 列番号の取得
    Dim rowTitle  As Long
    Dim colResult As Long
    Dim flg       As Boolean
    
    '抽出列の見出し名の列番号を取得する
    For rowTitle = LBound(arrTable, 1) To UBound(arrTable, 1)
        For colResult = LBound(arrTable, 2) To UBound(arrTable, 2)
            If arrTable(rowTitle, colResult) = resultTitle Then flg = True: Exit For
        Next
        If flg = True Then Exit For
    Next
    
    '検索キーのある見出し名の列番号を取得する
    Dim colKey As Long
    For colKey = LBound(arrTable, 2) To UBound(arrTable, 2)
        If arrTable(rowTitle, colKey) = keyTitle Then Exit For
    Next
    
    '(2) 行番号の取得
    Dim rowKey As Long
    
    '検索キーの値に該当する行番号を取得する
    For rowKey = LBound(arrTable, 1) To UBound(arrTable, 1)
       '======================================================================
        '【例外処理】Top3の順位は空白のため、画像リンクの情報をもとに順位を追記
        Dim elmtop3 As WebElement
        If arrTable(rowKey, colKey) = "" Then
            If .IsPresent(By.XPath, ".//tr[" & rowKey & "]/td[" & colKey & "]/img", 2000, elmtable, elmtop3) = False Then Stop
            arrTable(rowKey, colKey) = Left(Right(elmtop3.GetAttribute("src"), 5), 1)
        End If
       '======================================================================
        '検索に該当すれば抜ける
        If Replace(arrTable(rowKey, colKey), vbCrLf, "") = keyValue Then Exit For
    Next
    
    '「3 セルの特定及びクリック」
    Dim elmfound   As WebElement
    Dim rowMainasu As Long: rowMainasu = 0
    '見出しが<thead>に含まれている場合はthが含まれるtrの数をマイナス
    If elmTitle.GetTagName = "thead" Then rowMainasu = elmTitle.FindElements(By.XPath, ".//th/..").Count
    '明細列に<th>に含まれている場合はthが含まれるtrの数をマイナス
    If .IsPresent(By.XPath, ".//tbody/tr[" & rowKey - rowMainasu & "]/td[" & colResult - cntTh & "]//a", 2000, elmtable, elmfound) = False Then Stop
    '条件を満たす要素をクリック
    elmfound.Click

End With

1 テーブルの特定

競馬ラボ様の該当ページには、複数のテーブルが存在し、テーブル自体を特定する必要がありますが、テーブルのクラス名が同一のものが複数あり、特定が困難でした。

そのため、Xpathを駆使して、テーブルの見出し名に「騎手名」が含まれるテーブルを特定して、SeleniumVBAの機能であるTableToArrayにより、下図の色のついた部分が二次元配列に格納されます。

配列の格納範囲

1位から3位までの順位の表記が空白になっていますが、これはのちほどお話したいと思います。

なお、該当ページのテーブルには<thead>内に「○○○○年リーディングジョッキー」がありましたが、セル結合されており、検索にも必要がないので、skipHeader:=Trueとして取り込みませんでした。

(備考)「skipHeader:=True」の仕様は、<thead>タグ内のテキストの取り込みをスキップする仕様です。もし<thead>が見出し名である場合は、「skipHeader:=False」または省略して、見出しを取り込みます

tableタグ内の構造

2 列番号/行番号の取得

処理の順番として列番号の取得→行番号の取得という流れになります。列番号は、抽出列と検索列の両方の取得が必要となります。

取得においては、二次元配列内の情報を利用しているため、サーバの負担もなく、処理スピードも速いです。

For~Nextループ処理の部分は、コードが冗長になりがちなので、実務では汎用関数化してスッキリさせています。

(1)列番号の取得

Vlookupの列番号に相当する「騎手名」の列番号の取得をしています。

合わせてVlookupの「検索値」に相当するのは「順位」であるため、検索値が存在する検索列の列番号も取得しています。Vlookupのように検索列が左端であると決め打ちはしませんでした。

(2)行番号の取得

順位が5であるので、「検索値」=5に相当する行番号を取得しています。

ただし、表の作り方に問題があって、Top3は王冠の画像のみで順位の情報がありませんでしたので、例外処理により順位の取得が必要となりました。

’1位の場合のHTMLコード
<img src="https://www-f.keibalab.jp/img/db/ico_rank_01.png" class="rankImg" style="width:30px;height:30px;">

考えた末、上記のHTMLをみて、imgタグ中のsrc属性に手がかりとなる「~rank_01.png」という情報がありました。

'VBAコード抜粋
If .IsPresent(By.XPath, ".//tr[" & rowKey & "]/td[" & colKey & "]/img", 2000, elmTable, elmtop3) = False Then Stop
arrTable(rowJuni, colJuni) = Left(Right(elmtop3.GetAttribute("src"), 5), 1)

そこで、上のように、Xpathを駆使して該当セルのimgタグの要素の取得、そしてGetAttributeによりsrc属性の値を取得して、文字列操作関数Rightにより後ろから5文字を拾ってから、Leftにより始めの1文字を拾って配列に格納しました。

例外処理なのに詳しく説明する理由は、実務でも文字列操作を必要とする場面に直面することが数多くあるからです。
ようやく、これで晴れて1位から3位までの検索もできるようになりました。

3 セルの特定及びクリック

上記によりセルの行番号と列番号が特定できました。あとは以下のようにXPathを駆使して、tbodyタグを基点としてtrには行番号、tdには列番号を指定してあげれば、セルの要素が取得できるので、それをクリックしています。

'テーブル内におけるクリックするセル位置の特定
If .IsPresent(By.XPath, ".//tbody/tr[" & rowKey & "]/td[" & colResult & "]/a", 2000, elmTable, elmfound) = False Then Stop
elmfound.Click

(備考1)同一階層に複数の同じタグがある場合には、角括弧を使って1から始まる順番を指定して特定します。上記の例の場合は<tbody>タグを基点として<tr>タグが複数あり、さらに<tr>タグ内に<td>タグが複数あります。

trとtdの関係(tbodyタグに見出し名がある場合)

(備考2)見出し名が<thead>タグの中にあり、<tbody>タグの中にない場合は、trの行番号を1差し引きます。

'見出し名が<thead>タグの中にある場合
If .IsPresent(By.XPath, ".//tbody/tr[" & rowKey - 1 & "]/td[" & colResult & "]/a", 2000, elmTable, elmfound) = False Then Stop
elmfound.Click
trとtdの関係(tbodyタグに見出し名がない場合)

4 実際に動作させる

では、いよいよ実際に動作させていきます。GitHubから下図赤枠部分をクリックして、SeleniumVBA.xlsmをダウンロードします。(注)会社の環境ではセキュリティソフトの設定で赤枠がクリックできないかもしれません。

GitHub ダウンロード画面

ダウンロードしたSeleniumVBA.xlsmのアイコンを右クリックープロパティにより表示されるセキュリティを、以下のとおりに設定します。

アイコンを右クリックしてプロパティを選択した画面

標準モジュール「test_Tables」を開きます。そして「3 コード紹介」のコード全部をコピー(右上にコピーボタンがあります)して下図の赤枠部分に貼り付けて書き換えます。またEdgeの御利用の場合は「driver.StartEdge」に書き換えます。実行は画面上部の▶ボタンで行います。

SeleniumVBA.xlsm  標準モジュール「test_Tables」

初回実行時はWebDriverの自動ダウンロードを行うため時間がかかるかもしれません。(注)SelenimVBAはWebDriverの自動更新機能が標準で組み込まれています。

上記実行後、コード冒頭の初期設定で指定したとおりのページが表示された状態で止まっていればOKです。リンク元のページはブラウザの「戻る」ボタンで確認できます。

【推奨】エラートラップの設定

エラー発生時の対応を行いやすくするため、VBEの「ツール」「オプション」「全般」のエラートラップの設定は、下記のとおりとすることを強力にお勧めします。

VBEの「ツール」ー「オプション」の画面
  • 「エラー発生時に中断」にしない理由は、SeleniumVBAのプログラムは至るところに「On Error GoTo 〜」が使用されており、その箇所で中断してしまうため。

  • 「クラスモジュールで中断」にしない理由は、独自に実装するプログラムは標準モジュールで行うことが想定され、クラスモジュールで中断してしまうとデバッグが行いづらくなるため。

5 おわりに

このコードは変更に強い構造とすることを意識しました。
例えば競馬ラボ様が列の追加や列の並び順の変更をしても、列名が変わらなければ動作します。このように決めうちする箇所を極力少なくして安定した運用を目指しているのが特徴です。

他のテーブルでお試しになりたい場合も、例外処理のコード箇所は削除して、見出しが横並びで結合セルがなければ、初期設定を変えるだけて対応できるようにしてあります。

結合セル(例:colspan="2" rowspan="2"などで設定されているセル)が対応できない理由は、<tr><td>タグの数が行によって変わってしまい、行番号や列番号による位置の特定が困難で、汎用化は不可能と判断したからです。

結合セルの有無は見た目での判断が困難な場合もあり、ExcelVBA側で結合セル有無のチェックを行い、存在すればメッセージボックスを出すようにしています。(セル結合は本当に御勘弁いただきたいですね)

今回の事例ではXpathをフル活用しましたが、あまり詳しく説明できませんでした。以下の記事に詳しい説明がありますので御参照いただければ、と思います。

この記事が気に入ったらサポートをしてみませんか?