【Python】WebスクレイピングでYahoo!ニュースのコメントを取得する

# はじめに
Yahoo!ニュースのコメントをたまに見るのですが、見ても5ページくらいですね。
返信コメントがあってもクリックしないと見れないので、見逃してしまうことがあります。

この前、「[【Python】Youtube Data Apiを使ってコメントを全取得する](https://qiita.com/yaju/items/3bec88dbd544502e1343)」を書いたのですが、
それならと今度はYahoo!ニュースのコメントも一括取得に挑戦しようと考えました。

【2022/06/25追記】
Yahoo!側の仕様変更で、記事のソースコードでは取得できなくなっています。
2022/06/24にPythonではなくExcelで取得できるようにしましたが、Yahoo!ニュースのコメントはWebスプレイピングが禁止されていることもあり、技術的なポイントだけを書きました。

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

# 調査
Yahoo!ニュースのコメントの取得については、Youtube Data Apiのような提供しているAPIがありませんでした。
そこでネットで検索してみると下記の2記事を見つけました。

* [ヤフーニュースのコメントをスクレイピングで自動取得する方法](https://gaaaon.jp/blog/yahoo_scraping)
* [selenium で yahooニュースのコメントをスクレイピング－python](http://ailaby.com/scrape_ya_comment/)

どうやらWebスクレイピングするしかコメント取得の方法はないようです。

# 環境
今回は誰でも手軽にコメント取得が出来ればいいなと思って、Googleアカウントさえあれば直ぐに使える「Google Colaboratory」を使ってやりたかったので、プログラム言語としてはPythonにしたかった。

## Webスクレイピング方法
PhantomJSブラウザの開発が中断しているため、下記を参考にSeleniumにてGoogle Chromeをヘッドレスモードを使用します。
[静岡のGoToEat公式サイトをスクレイピング、伊豆のキャンペーン対象店をリスト化する](https://qiita.com/andy2017/items/21e52492eaca9f27404c)

Pythonのスクレイピングライブラリに「Beautifulsoup4」がありますが、これだとJavaScriptの実行ができません。
返信のコメントを取得するには返信ボタンをクリックしてコメントデータを取得することになるので、JavaScriptの実行ができる「Selenium」を使用します。

## 環境準備
Google Colaboratory にて、Selenium と ChromeDriver をインストールします。
下記を実行すれば動かせます。ただし、90分と12時間ルールで初期化されるので、その際にはインストールし直します。

```
!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
!pip install selenium
```

>Google Colaboratoryでは以下の条件を満たす場合、実行中のプログラムがあってもインスタンスの状態がすべてリセットされていまいます。
* 【12時間ルール】新しいインスタンスを起動してから12時間経過
* 【90分ルール】ノートブックのセッションが切れてから90分経過

[【簡単】GoogleColabの制限とは？９０分と１２時間ルール](https://kenko-keep.com/12hour-90minit/)

# Yahoo!ニュースのコメント仕様
Yahoo!ニュースは1ページに10件のコメントが表示されます。１ページ目に認証されたユーザーのコメントがあった場合はその分だけ多くなります。
「返信」ボタンの横に0以外の数値があれば、「返信」ボタンをクリックするとコメントが10件取得されて表示されます。10件以上あれば「もっと見る」のリンクが付き、それをクリックすれば次のコメントが10件単位で取得され表示されます。

認証されたユーザーがコメントした場合、「返信」ボタンではなく「参考になった」ボタンが表示されます。この場合には返信はできません。

## 掲載期間
>Yahoo!ニュースに掲載されている記事の掲載期間は、情報提供元により異なります。掲載期間を過ぎると、記事は削除されて読めなくなります。
トピックス一覧では、ニュース編集部がピックアップした話題を確認できますが、掲載開始から1週間以上経過したトピックスは読めません。また、掲載から1週間以内であっても、情報提供元により記事詳細を表示できない場合もあります。
[記事の掲載期間 - Yahoo! JAPANヘルプセンター](https://support.yahoo-net.jp/PccNews/s/article/H000006479)

記事の掲載期間が終了した場合、コメントも削除されます。

# 仕様
タブ区切りでコメントの改行は半角空白に置換しています。
親連番は4桁、子の連番は3桁にしているので桁数を超えるコメントを取得したい場合は表示する桁数を変更増やすといいでしょう。

```
000X    (コメント)  （グッド数/参考になった数）　（バッド数）   （ユーザー名）   （日時)   （返信数)
000X-00X    (子コメント)   （グッド数）　（バッド数）  （ユーザー名）   （日時)
```

# 使用方法
Google Colaboratoryを開いて上記サイトの「getYahooNewsComments.py」をコピペで貼り付け、指定URLを書き換えて実行します。
環境準備のところに記載しましたが、使用前にSelenium と ChromeDriver をインストールしてください。

## 指定URL
「[漫画家の水島新司さんが引退表明　野球漫画「ドカベン」など](https://news.yahoo.co.jp/articles/c3fe0c9976b8a84b89ffa07bbd27890f944369fc/comments?order=recommended)」の場合、アドレスバーのURLを指定します。

```python
URL = "https://news.yahoo.co.jp/articles/c3fe0c9976b8a84b89ffa07bbd27890f944369fc/comments?order=recommended"
```

ページ番号の「&page=2」の部分は、プログラムで付け加えているので外してセットしてください。

## ページ範囲
プログラム上では開始(start)１ページ〜終了(end)2ページとしています。当然、終了ページを増やせば取得できます。
返信ボタンやもっと見るリンクをクリックする際に2秒待機していますので、ページ数を増やせば多少は時間がかかります。

```python
start = 1
end = 2
```

# プログラム
```python:getYahooNewsComments.py
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# ブラウザをバックグラウンド実行
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
# ブラウザ起動
driver = webdriver.Chrome('chromedriver',options=options)

URL = 'URLを入力'

# 対象要素のテキスト取得
def getItem(element, name, name2):
    result = ""
    elem = element.find_elements_by_class_name(name)
    if len(elem) > 0:
        if name2 == "":
            result = elem[0].text.strip()
        else:
            result = elem[0].find_element_by_class_name(name2).text.strip()

    return result;

# 認証者コメント出力
def print_authorComment(no):
    comment_boxes = driver.find_elements_by_css_selector('li[id^="authorcomment-"]')
    for comment_box in comment_boxes:
        #コメント取得
        elem_comment = comment_box.find_element_by_class_name("comment")
        comment = elem_comment.text.strip().rstrip('...もっと見る')
        comment += comment_box.find_element_by_class_name("hideAthrCmtText").get_attribute("textContent")
        #ユーザー名取得
        name = getItem(comment_box, "rapidnofollow", "")
        #日付取得
        date = getItem(comment_box, "date", "")
        #参考になった数取得
        refcnt = comment_box.find_elements_by_css_selector('li.reference a em')[0].text

        no += 1
        print('{:0=4}\t{}\t{}\t{}\t{}\t{}\t{}'.format(no,comment.replace('\n', ' '), refcnt, "0", name, date, "0"))
    
    return no

# 一般者コメント出力
def print_generalComment(no):
    comment_boxes = driver.find_elements_by_css_selector('li[id^="comment-"]')
    for comment_box in comment_boxes:
        #コメント取得
        comment = getItem(comment_box, "cmtBody", "")
        #ユーザー名取得
        name = getItem(comment_box, "rapidnofollow", "")
        #日付取得
        date = getItem(comment_box, "date", "")
        #good数取得
        agree = getItem(comment_box, "good", "userNum")
        #bad数取得
        disagree = getItem(comment_box, "bad", "userNum")
        #返信数
        reply = int(getItem(comment_box, "reply", "num") or "0")

        no += 1
        print('{:0=4}\t{}\t{}\t{}\t{}\t{}\t{}'.format(no, comment.replace('\n', ' '), agree, disagree, name, date, reply))

        if reply == 0:
            continue

        #返信出力
        print_reply(comment_box, reply, no)

    return no

# 返信コメント出力
def print_reply(element, reply, no):
    # 「返信」 リンクを click
    rep_links = element.find_elements_by_css_selector('a.btnView.expandBtn')
    for rep_link in rep_links:
        rep_link.click()
        time.sleep(2)

    # 「もっと見る」 リンクを click
    response_boxes = element.find_elements_by_class_name("response")
    for i in range(int(reply/10)):
        if len(response_boxes) > 0 and (reply % 10) > 0:
            rep_links = response_boxes[0].find_elements_by_css_selector('a.moreReplyCommentList')
            for rep_link in rep_links:
                rep_link.click()
                time.sleep(2)

    # 返信コメント 取り出し
    replys = response_boxes[0].find_elements_by_css_selector('li[id^="reply-"]')
    cno = 1
    for reply in replys:
        cmtBodies = reply.find_elements_by_css_selector('div.action article p span.cmtBody')
        if len(cmtBodies) == 0:
            continue
        #コメント取得
        comment = cmtBodies[0].text.strip()
        #ユーザー名取得
        name = getItem(reply, "rapidnofollow", "")
        #日付取得
        date = getItem(reply, "date", "")
        #good数取得
        agree = getItem(reply, "good", "userNum")
        #bad数取得
        disagree = getItem(reply, "bad", "userNum")

        print('{:0=4}-{:0=3}\t{}\t{}\t{}\t{}\t{}'.format(no, cno, comment.replace('\n', ' '), agree, disagree, name, date))
        cno += 1

# コメント取り出し
start = 1
end = 2

no = 0
for page in range(start, end + 1):
    driver.get(URL + "&page={}".format(page))

    iframe = driver.find_element_by_class_name("news-comment-plguin-iframe")
    driver.switch_to.frame(iframe)

    #認証者コメント
    if page == 1:
        no = print_authorComment(no)
    #一般者コメント
    no = print_generalComment(no)
```

## プログラムの説明
Pythonは、たまにしか記述しないのでプログラム的にはベタに書いています。その方が他の言語に移植しやすいかな。
返信コメントは「もっと見る」がなくなるまでクリックしてから、一気にコメントを取得しています。
改行を半角スペースに置換してるので読みやすさは特に考慮してません、Excelに貼り付けて折り返して見ればいい。

「...もっと見る」の後のテキストがあるのに属性が「display:none」になっていることで取得できなくて困っていたのですが、下記サイトの方法で解決できました。ありがとうございます。
[Seleniumでタグに囲まれた文字列を.textで取得すると空文字が返ってくる](https://qiita.com/riikunn1004/items/68c7621baaa54cf27230)

```python
#コメント取得
elem_comment = comment_box.find_element_by_class_name("comment")
comment = elem_comment.text.strip().rstrip('...もっと見る')
comment += comment_box.find_element_by_class_name("hideAthrCmtText").get_attribute("textContent")
```

# 実行結果
「[漫画家の水島新司さんが引退表明　野球漫画「ドカベン」など](https://news.yahoo.co.jp/articles/c3fe0c9976b8a84b89ffa07bbd27890f944369fc/comments?order=recommended)」のYahoo!ニュースのコメントを取得すると、下記のようになります。

```bath:コメント取得例
0001	10年近く前、・・・ます。	4792	0	菊地高弘	11時間前	0
0002	いつかは・・・言えるでしょう。	2938	0	河村鳴紘	11時間前	0
0003	10年ほど前、・・・あります。	1282	0	阿佐智	10時間前	0
0004	ユニークな・・・お疲れ様でした。	5978	649	dck*****	12時間前	61
0004-001	YahooTOP記事・・・お過ごしください。	101	0	p*******	12時間前
0004-002	水島先生・・・思います。	63	0	gtj*****	11時間前
```

# 最後に
Advent Calendarが始まりました。今年も「[Visual Basic Advent Calendar 2020](https://qiita.com/advent-calendar/2020/vb)」を記事を募集しています。
これでPythonではなく Visual BasicやExcelでやってみるなどのネタができそうです。