はじめに
以前(2020年12月01日)に下記の記事を書きました。
最近になり、「ヤフーコメントのHTMLの変化で使用できなくなってしまったので対応してもらうことは可能でしょうか?」というコメントを頂きました。
最後に
Pythonではなく Visual BasicやExcelでやってみるなどのネタができそうです。
当時、SeleniumBasicでやろうとするも知識が疎かったせいか、例外エラーが解消できずにAdventCalendarの期日も過ぎたのでそのままやめてしまいました。
時は経ち、TinySeleniumVBAで簡単にWebスプレイピングする方法が分かり、PythonではなくてExcelでやってみようと思った次第です。
環境
Mac環境のExcelでTinySeleniumVBAを使用しました。
Mac Book Pro(2.3 GHz 8コアIntel Core i9) Monterey 12.2.1
Microsoft Office Home and Student 2019
実行結果
大谷、奪三振ショー!8回メジャー最多13K無失点投球で6勝目、打では3試合連続安打 エ軍連敗ストップ
認証されたユーザーのコメントが複数あり、ユーザーコメントの返信コメントも10件以上あってテストするのには丁度よかった。
Webスプレイピング禁止
ものは出来たので、記事を書く準備をしてネット調査をしていたところ下記サイトを見つけました。
どうやら自分の記事を実行しようとしたがエラーになるという質問でした。
問題はその回答ですね。
Disallow: /articles/*/comments
https://news.yahoo.co.jp/robots.txtYahooNewsのRobots.txtです。
コメントを取得することは禁止されているようなので、避けた方が良いかと思います。
Yahooファイナンスのスクレイピングが禁止されていることは有名で知っていました。
Yahoo!ファイナンス掲載情報の自動取得(スクレイピング)は禁止しています
技術的ポイント
認識が甘かったため全部のソースコードの公開はやめることにしました、ただTinySeleniumVBAで作成した時の技術的ポイントなら公開しても問題ないだろうとして、記事を書くことにしました。
XPathの学習
XPath PlaygroundサイトがXPathを検証するのに便利だった。
以前と違いYahoo!ニュースのコメントのHTMLが変わったことで、classなどにランダムな値が付与されるようになっているため取得が面倒になった。文字列操作のstarts-with
を使って文字列始まりで検索して取得している。
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側で操作する。
' 値取得
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させます
'「返信」ボタンを 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つの「もっと見る」リンクをクリックするだけでいいです。
データを取得する際の待機として1秒取っています。
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
認証者かユーザーかの判別処理
1ページ目の最初の何件かはYahooで認証されたユーザーが表示されて「参考になった」ボタンのみとなり、返信することも出来ません。
ユーザーコメントは、PタグのclassName属性にUserComment
が含まれているかで判断しています。
' オーサー 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要素がセットされます。
' 親の親要素を取得
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
のような文字列で取得されるので、数字のみ抽出して格納します。
' 参考になった数取得
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=
とする必要があります。
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でも指定したミリ秒で待機できます。
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出力処理を再利用しました。
' コメント出力
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には、親要素を指定します。
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!ニュースのコメントは、近ごろは誹謗中傷など問題になっています。ビュー数を延ばすために過激なタイトルや記事内容になったりしていましたからね。
Webスプレイピングも業務妨害になるほど大量に取得するのではなく、数ページ分を取得するくらいで手動でクリックするよりは少しの手間で済む感じなら問題にはならないと思うんですがね。機械学習用の自然言語データを集める上で、生きたコメントとして活用できる。
まー所詮は、趣味の範囲レベルですよ。