【Excel】TinySeleniumVBAでYahoo!ニュースのコメントを取得するもソース非公開

# はじめに
以前（2020年12月01日）に下記の記事を書きました。
最近になり、「ヤフーコメントのHTMLの変化で使用できなくなってしまったので対応してもらうことは可能でしょうか?」というコメントを頂きました。

https://qiita.com/yaju/items/501874c39d569fd53f91

>最後に
>Pythonではなく Visual BasicやExcelでやってみるなどのネタができそうです。

当時、SeleniumBasicでやろうとするも知識が疎かったせいか、例外エラーが解消できずにAdventCalendarの期日も過ぎたのでそのままやめてしまいました。
時は経ち、TinySeleniumVBAで簡単にWebスプレイピングする方法が分かり、PythonではなくてExcelでやってみようと思った次第です。

https://qiita.com/yaju/items/9d1f189a5d2118a3f4d3

# 環境
Mac環境のExcelでTinySeleniumVBAを使用しました。

Mac Book Pro(2.3 GHz 8コアIntel Core i9) Monterey 12.2.1
Microsoft Office Home and Student 2019

https://qiita.com/yaju/items/caa29af1c133cd8c02e1

# 実行結果
[大谷、奪三振ショー！8回メジャー最多13K無失点投球で6勝目、打では3試合連続安打　エ軍連敗ストップ](https://news.yahoo.co.jp/articles/94b2c9038a922c29134c20b27d5b67c5d569a5c4/comments)

認証されたユーザーのコメントが複数あり、ユーザーコメントの返信コメントも10件以上あってテストするのには丁度よかった。

![スクリーンショット 2022-06-24 23.43.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/85be72cd-9134-a5df-d945-74a65d17ae68.png)

# Webスプレイピング禁止
ものは出来たので、記事を書く準備をしてネット調査をしていたところ下記サイトを見つけました。
どうやら自分の記事を実行しようとしたがエラーになるという質問でした。

https://teratail.com/questions/342838

問題はその回答ですね。

>Disallow: /articles/*/comments
>[https://news.yahoo.co.jp/robots.txt](https://news.yahoo.co.jp/robots.txt)
>
>YahooNewsのRobots.txtです。
>コメントを取得することは禁止されているようなので、避けた方が良いかと思います。

Yahooファイナンスのスクレイピングが禁止されていることは有名で知っていました。
[Yahoo!ファイナンス掲載情報の自動取得（スクレイピング）は禁止しています](https://support.yahoo-net.jp/PccFinance/s/article/H000011276)

https://kajimublog.com/python-scrape-ban/

# 技術的ポイント
認識が甘かったため全部のソースコードの公開はやめることにしました、ただTinySeleniumVBAで作成した時の技術的ポイントなら公開しても問題ないだろうとして、記事を書くことにしました。

## XPathの学習
https://zenn.dev/kanoekakihito/articles/8da2966e8951a6

XPath PlaygroundサイトがXPathを検証するのに便利だった。

https://scrapinghub.github.io/xpath-playground/


以前と違いYahoo!ニュースのコメントのHTMLが変わったことで、classなどにランダムな値が付与されるようになっているため取得が面倒になった。文字列操作の`starts-with`を使って文字列始まりで検索して取得している。

```vb
Const TAG_P = "p"
Const TAG_H2 = "h2"
Const TAG_TIME = "time"
Const TAG_BUTTON = "button"

articles = driver.FindElements(By.XPath, "//div[@class=""viewableWrap""]//ul//li//article[starts-with(@class,""sc-"")]")
    
For Each article In articles
    'コメント取得
    comment = GetValue(article, TAG_P, 1)
```

## 値の取得
TinySeleniumVBAでは、GetValueやGetTextなど最小限しかないので、innerTextなどの属性を取りたい場合は、`ExecuteScript`を使用してJavaScript側で操作する。

```vb
' 値取得
Private Function GetValue(ByVal element, ByVal tag As String, Optional ByVal idx As Integer = 0, Optional ByVal attr As String = "innerText")
    GetValue = driver.ExecuteScript("return arguments[0].getElementsByTagName(""" & tag & """).item(" & idx & ")." & attr, Array(element))
End Function
```

## ボタン要素のクリック
認証されたユーザーのコメントが長い場合、「…もっと見る」リンクが付与され、リンクすると全文が取得されます。
「返信」ボタンをクリックすると10件表示され、「もっと見る」リンクをクリックすると次の10件が取得されます。
この3つに共通しているのは、buttonタグになっていることです。
button要素をclickさせます

```vb
'「返信」ボタンを click
ButtonClick article, TAG_BUTTON, 1

' ボタン要素をClick
Private Function ButtonClick(ByVal element, ByVal tag As String, Optional ByVal idx As Integer = 0, Optional ByVal attr As String = "")
    driver.ExecuteScript "arguments[0].getElementsByTagName(""" & tag & """).item(" & idx & ")." & attr & "click()", Array(element)
End Function
```

「返信」ボタン後の「もっと見る」リンクをクリックすると次の10件が取得されるようになっており、先に全部の返信コメントを表示させてから返信コメントを取得するようにしています。
前10件の「もっと見る」リンクは表示される度に消えるので、何10件あっても１つの「もっと見る」リンクをクリックするだけでいいです。
データを取得する際の待機として1秒取っています。

```vb
If reply > 10 Then
    For i = 1 To Fix(reply / 10)
        driver.FindElement(By.XPath, "//button[starts-with(@class,""MoreButton__CommentMoreButton"")]").Click
        TimerWait 1000
    Next
End If
```
## 認証者かユーザーかの判別処理
１ページ目の最初の何件かはYahooで認証されたユーザーが表示されて「参考になった」ボタンのみとなり、返信することも出来ません。
ユーザーコメントは、PタグのclassName属性に`UserComment`が含まれているかで判断しています。

```vb
' オーサー or ユーザー判定取得
className = GetValue(article, TAG_P, 1, "className")
If InStr(className, "UserComment") > 0 Then
    ' 返信数
    reply = GetValue(article, TAG_BUTTON, 1)
    reply = ExtractNumbers(reply)
```

## 親の親要素の取得
返信コメントの取得する際に、親の親要素（`parentNode.parentNode`）を取得するようにしています。
elementには、親側のarticle要素がセットされます。

```vb
' 親の親要素を取得
replyList = driver.ExecuteScript("return arguments[0].parentNode.parentNode.getElementsByTagName(""article"");", Array(element))
cno = 0
For Each article In replyList
    If cno > 0 Then
        ' コメント取得
        comment = GetValue(article, TAG_P)
```

## 数字のみ抽出処理
Macだと正規表現を使用したくてもActiveXが使えないので`CreateObject("VBScript.RegExp")`はエラーになります。そこで似た機能として`Like演算子`を使います。

`参考になった数 7841`のような文字列で取得されるので、数字のみ抽出して格納します。

```vb
' 参考になった数取得
agree = GetValue(article, TAG_BUTTON)
agree = ExtractNumbers(agree)

' 数字のみを抽出
Private Function ExtractNumbers(ByVal s As String) As String
    Dim i As Integer
    Dim num As String
    Dim result As String
 
    For i = 1 To Len(s)
        num = Mid(s, i, 1)
        If num Like "[0-9]" Then
            result = result & num
        End If
    Next
    
    ExtractNumbers = IIf(result = "", 0, result)

End Function
```
## ページ処理
おすすめ順と新着順があります。デフォルトはおすすめ順です。
新着順の場合、`?order=newer`が付きます。おすすめ順の場合、無しか`?order=recommended`を付けます。
下記コードでは、おすすめ順のままなので先頭は「?」の`?page=`にしていますが、並び順を指定するなら先頭は「&」の`&page=`とする必要があります。

```vb
For page = page_start To page_end
    driver.Navigate NEWS_URL & "?page=" & page
        
    Call GetComment(row, no, ActiveSheet)
Next
```

## Sleep処理
MacだとWindows APIの`Sleep`関数が使用できないので、SeleniumVBAのあった`TimerWait`を中身を採用しました。これなら、WindowsやMacでも指定したミリ秒で待機できます。

```vb
Private Sub TimerWait(ByVal durationMS As Long)
    'pause in milliseconds
    Dim startTime As Single, endTime As Single, nowTime As Single, elapsedTime As Single
    startTime = VBA.Timer()
    endTime = startTime + durationMS / 1000#
    Do While nowTime < endTime
        nowTime = VBA.Timer()
        If nowTime < startTime Then 'oops - someone is burning the midnight oil!
            endTime = endTime - elapsedTime
            startTime = 0
        End If
        elapsedTime = nowTime - startTime
        DoEvents    'yield to other processes.
    Loop
End Sub
```

## Excel出力処理
以前に下記の記事にて作成したExcel出力処理を再利用しました。

https://qiita.com/yaju/items/eb47e20446d7e43d8e55

```vb
' コメント出力
values = Array(no, "", comment, agree, disagree, user_name, post_date, reply)
Call Output(row, sh, values)

' Excelシートに出力
Private Sub Output(ByRef row As Integer, ByVal sh As Worksheet, ByVal values As Variant)
    Dim i As Integer

    For i = 0 To UBound(values)
        sh.Cells(row, i + 1).value = values(i)
    Next
    row = row + 1
End Sub
```

## 子要素のアクセス
TinySeleniumVBAではFindElementかFindElementsしか無いので、XPathを使用して子要素まで取得する方法もあるのですが、速度面的には毎回先頭から探索するので遅くなります。
今回はFindElementsのXPathで親要素まで取得し、ExecuteScriptでJavaScriptを使って子要素を取得しています。

elementには、親要素を指定します。

```vb
driver.ExecuteScript("return arguments[0].getElementsByTagName('p').innerText", Array(element))
Or
driver.ExecuteScript("return arguments[0].getElementsByTagName('button').item(1).innerText", Array(element))
Or
driver.ExecuteScript("return arguments[0].getElementsByTagName('p').item(1).children[0].innerText", Array(element))
```

# 最後に
Yahoo!ニュースのコメントは、近ごろは誹謗中傷など問題になっています。ビュー数を延ばすために過激なタイトルや記事内容になったりしていましたからね。

https://news.yahoo.co.jp/articles/963b4f1ec1093aa051026c4e03b4b401eda4d4ab

Webスプレイピングも業務妨害になるほど大量に取得するのではなく、数ページ分を取得するくらいで手動でクリックするよりは少しの手間で済む感じなら問題にはならないと思うんですがね。機械学習用の自然言語データを集める上で、生きたコメントとして活用できる。

まー所詮は、趣味の範囲レベルですよ。