Pythonプログラミング：BeautifulSoup4を使ってlivedoor NEWSからキーワード検索結果のニュースを取得（スクレイピング）してみた

# はじめに

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

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

今回は、たくさんのニュースに対して、キーワード検索した結果の取得を目指します。
WebブラウザでTop画面を表示した際の、左肩にあるテキストエリアにキーワードを入力して、検索した画面の画面です。
↓↓↓キーワード「**緊急事態宣言 解除**」での検索結果の例↓↓↓
![検索結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/234490ac-016d-b9fb-9101-f0b47a884517.png)

というわけで、Pythonプログラムで取得（スクレイピング）する処理を実現してみようと思います。

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

## 本稿で紹介すること

- キーワード検索結果のニュース（livedoor NEWS）の取得

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

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

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

# サンプルコード

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

## 1. キーワードの指定
キーワードをURLエンコードして、URLに付与します。
その際、文字コードも指定しており、livedoor NEWSだと「euc-jp」です。

## 2. CSV出力は追記モードでヘッダなし
検索結果の（恐らく）複数ページを対象にして、pandas.DataFrameをCSVファイルへ出力します。
が、検索キーワード単位でファイル出力したかったので、追記モードにしています。
更に、ヘッダありで都度追記してファイル途中にヘッダが何度も登場してしまう、、、というのを避けるべく、ヘッダなしにしています。

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

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

## 5. タグ要素の指定
各ページ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 os
import urllib.parse
import sys
import time

# 保管先フォルダの名称
OUTPUT_DIR = 'livedoor_news'
# ニュース検索（ライブドアニュース）のURL
URL = 'https://news.livedoor.com/search/article/?p={1:}&word={0:}'
# ニュースID抽出用の正規表現（名前付きグループ）
REG_URL = r'(?P<L1>(https?://[^/]+/))(?P<L2>([^/]+))/(?P<L3>([^/]+))/(?P<L4>([^/]+))/'
# 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_paging(query=None, page=None):
    print(URL.format(urllib.parse.quote_plus(query, encoding='euc-jp'), page))
    try:
        # HTTPリクエストヘッダーにUser Agentを設定
        req = Request(URL.format(urllib.parse.quote_plus(query, encoding='euc-jp'), page), data=None, headers={'User-Agent': USER_AGENT})
        with urlopen(req) as res:
            # HTMLドキュメントからBeautifulSoupを初期化
            soup = BeautifulSoup(res.read().decode('euc_jp', 'ignore'), 'html.parser')
            # 存在しないページに辿り着いたら、中断（すべての結果を取得済み）
            if soup.find('div', id='content').find('p', class_='empty'):
                #print(soup.find('div', id='content').find('p', class_='empty').text)
                return True
            # 検索結果のニュース部分を検索し、全てを取得
            soupNewsQuery = soup.find('ul', class_='articleList').find_all('li')

            articles = []
            for idx, soupNews in enumerate(soupNewsQuery):
                # 詳細ページURLをHTMLタグの属性から抽出
                url = soupNews.a.get('href')
                # NewsBodyを検索し取得
                soupNewsBody = soupNews.find('div', class_='articleListBody')
                # NewsBodyから各種データを抽出
                article = {
                    'url': url,
                    # ニュースIDを詳細ページURLから抽出
                    'id': re.search(REG_URL, url).groupdict()['L4'],
                    # タイトル／サマリ／提供元／公開日時をHTMLタグの本文（公開日時だけ属性）から抽出
                    'title': soupNewsBody.find('h3', class_='articleListTtl').text,
                    'summary': soupNewsBody.find('p', class_='articleListSummary').text,
                    'vender': soupNewsBody.find('p', class_='articleListVender').text,
                    'datetime': soupNewsBody.find('time', class_='articleListDate').get('datetime')
                }
                articles.append(article)
                #print('%2d: %s' %(idx + 1, soupNewsBody.find('h3', class_='articleListTtl').text))

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

def get_links(query=None):
    try:
        for page in range(1, sys.maxsize):
            isFinish = get_links_paging(query, page)
            if isFinish:
                return
            time.sleep(5)
    except Exception as e:
        # エラー概要
        print('Exception: ', e)
        print('=====')
        # エラー詳細（スタックトレース）
        print(traceback.format_exc().rstrip())
        print('=====')
```

こんな風に使います。キーワードをスペースでつないで引数にします。

```python:
create_folder()
get_links('緊急事態宣言 解除')
```

正常に実行できると、以下のようになります。
![実行例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/69d6546b-097e-0062-6bef-801deb2297a2.png)

ちなみに、本稿執筆時点（2021/10/08）だと、緊急事態宣言が解除されたり首相が交代したりの時期なので、妥当な結果ですね。
![CSV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/ebef3e76-6665-9a68-d967-c78dc1c39dcb.png)

と思ったけど、国民的アイドル「**嵐**」に関するニュースがHitしてますね。

[櫻井＆相葉　あらゆる分野での功績がある嵐だからこそのW結婚発表](https://news.livedoor.com/article/detail/20997159/)の記事全文を眺めると、ばっちりキーワード「**緊急事態宣言 解除**」が登場していました。はい、妥当ですね。

# まとめ
BeautifulSoup4を使ってキーワード検索結果のニュースを取得（スクレイピング）する方法を紹介。