Pythonプログラミング：BeautifulSoup4を使ってlivedoor NEWSから話題のニュースを取得（スクレイピング）してみた

# はじめに

前回記事に続き、データ収集に関する投稿です。

1. [ニュースアクセスランキング編](https://qiita.com/Blaster36/items/163333b81398b5bd4af0)
2. [今日のニュース編](https://qiita.com/Blaster36/items/3d02a050f61218679ae0)
3. 話題のニュース編　★本稿
4. [キーワード検索結果のニュース編](https://qiita.com/Blaster36/items/680000a7479e3a90857f)

今回は、[話題のニュース](https://news.livedoor.com/social_reaction/)が対象です。
というわけで、Pythonプログラムで取得（スクレイピング）する処理を実現してみようと思います。

※本稿執筆時点（2021/10/07）の情報に基づき、Code紹介と実行例を示します。

## 本稿で紹介すること

- 話題のニュース（livedoor NEWS）の取得

尚、筆者は以下のVersionで動作確認をしています。
  - Python: 3.6.10
  - BeautifulSoup4: 4.9.3

## 本稿で紹介しないこと

- Pythonライブラリのインストール方法および使い方
  - urllib.request
  - BeautifulSoup4
  - re
  - pandas
  - csv
  - traceback
  - os

# サンプルコード

Code量も多くないので、全体のCodeを紹介。
ポイントは4つ。

## 1. 日付の指定
話題のニュースなのですが、Webブラウザから見える範囲だと当日含め直近１週間分です。
が、URLに日付（YYYY-MM-DD）を付与することにより、過去日に遡及して取得可能です。
ただし、それでも限界はあって、ざっくり１年半前までが取得可能という仕組みです。

## 2. User-Agentの設定
HTTPリクエストヘッダーにUserAgentを設定するのがベター。
自宅PC（個人環境）なので不要ですが、会社PC（企業環境）であればProxyの設定も同じようにヘッダーに設定するのがベター。

## 3. 名前付きグループの正規表現
後々、ニュースIDをKeyに記事全文を取得することも視野に、ニュースIDも抽出しておくのがベター。

## 4. タグ要素の指定
各ページSourceを眺めて、タグ構成を鑑みて要素を指定し、BeautifulSoup4で情報を取得する必要あり。
多くの場合、タグに付与されたclass属性を指定し、目的のタグ（およびその内部のText）を取得する処理を実装することになります。
WebブラウザでページのSourceを眺めて、HTMLタグの構造を把握するのは簡単です。

## Codeを紹介
以下、全体のCodeです。

```python
from urllib.request import Request, urlopen
from bs4 import BeautifulSoup
import re
import pandas as pd
import csv
import traceback
import datetime
import os

# 保管先フォルダの名称
OUTPUT_DIR = 'livedoor_news'
# 話題のニュース（ライブドアニュース）のURL
URL = 'https://news.livedoor.com/social_reaction/%s/'
# ニュースID抽出用の正規表現（名前付きグループ）
REG_URL = r'(?P<L1>(https?://[^/]+/))(?P<L2>([^/]+))/(?P<L3>([^/]+))/(?P<L4>([^/]+))/'
# ニュースアーカイブの遡及可能な日数
DAYS_AGO_MAX = 570
# UserAgent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36'

def create_folder():
    if not os.path.exists(OUTPUT_DIR):
        # フォルダが存在しなければ、フォルダを作成
        os.makedirs(OUTPUT_DIR)

def get_links(delta=None):
    # 基準日を実行時点で初期化
    target = datetime.date.today()
    #print(target.strftime('%Y-%m-%d'))
    if delta in list(range(-DAYS_AGO_MAX, 1)):
        # delta日前を求めるtimedeltaを作成
        td = datetime.timedelta(days=delta)
        # 基準日のdelta日前を計算して取得
        target = target + td
    #print(target.strftime('%Y-%m-%d'))
    try:
        print(URL %target.strftime('%Y-%m-%d'))
        # HTTPリクエストヘッダーにUser Agentを設定
        req = Request(URL %target.strftime('%Y-%m-%d'), data=None, headers={'User-Agent': USER_AGENT})
        with urlopen(req) as res:
            # HTMLドキュメントからBeautifulSoupを初期化
            soup = BeautifulSoup(res.read().decode('euc_jp', 'ignore'), 'html.parser')
            # 話題のニュース部分を検索し、全てを取得
            soupNewsTopic = soup.find('ol', class_='socialReactionList').find_all('li')

            articles = []
            for idx, soupNews in enumerate(soupNewsTopic):
                # ICON(FacebookとTwitter)を除外
                if len(soupNews.contents) == 0 or soupNews.contents[0].name == 'i':
                    continue
                # 詳細ページURLをHTMLタグの属性から抽出
                #url = soupNews.a.get('href')
                url = soupNews.find('a')['href']
                # NewsBodyを検索し取得
                soupNewsBody = soupNews.find('div', class_='socialReactionBody')
                # NewsBodyから各種データを抽出
                article = {
                    'url': url,
                    # ニュースIDを詳細ページURLから抽出
                    'id': re.search(REG_URL, url).groupdict()['L4'],
                    # タイトル／サマリをHTMLタグの本文から抽出
                    'title': soupNewsBody.find('h3', class_='socialReactionTtl').text,
                    'summary': soupNewsBody.find('p', class_='socialReactionDesc').text
                }
                articles.append(article)
                #print('%2d: %s' %(idx + 1, soupNewsBody.find('h3', class_='socialReactionTtl').text))

            df = pd.DataFrame(articles)
            # DataFrameからCSVファイルを生成
            # encoding='sjis'だとJupyterLab（CSVTable）上で表示不可なことに注意
            df.to_csv('%s/livedoor_news_topic_%s.csv' %(OUTPUT_DIR, target.strftime('%Y-%m-%d')), encoding='utf-8', index=False, quotechar='"', quoting=csv.QUOTE_ALL)
    except Exception as e:
        # エラー概要
        print('Exception: ', e)
        print('=====')
        # エラー詳細（スタックトレース）
        print(traceback.format_exc().rstrip())
        print('=====')
```

正常に実行できると、以下のようになります。
![実行例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/9cb92ccc-fda8-9970-c50e-e5b29c610e7a.png)

ちなみに、2020年4月の緊急事態宣言が発令された翌日だと、以下のような記事があり、社会情勢を反映していますね。
![実行例2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/aa156652-0bac-3df2-a5e5-859deba34a1e.png)


# まとめ
BeautifulSoup4を使って[話題のニュース](https://news.livedoor.com/social_reaction/)を取得（スクレイピング）する方法を紹介。