Pythonでニュースをクローリング



## はじめに
どうも、こんにちは。今回はPythonでニュース記事をクローリングする方法について、簡単に書いていこうかなと思います。というのも、このクローリング部分が、私が現在大学で携わっているチーム開発での担当箇所なんですよね。
来年チームに参加してくれるであろう後輩たちのためにも、仕様書っぽいものを何か残しておいた方がいいだろうということで、書くことにしました。
業務未経験の大学生が、大学生に対して書いているものなので、温かい目で見てもらえれば幸いです(笑)

## プログラムの概要
今回、私が書いたプログラムは、ニュース記事をネット上から取得して、それをjson形式に変換してフォルダに格納するというものです。
ニュース記事を拾ってくる部分と、記事をjson形式に変換する部分でコードを分けたので、一つずつ解説していきます。

## ニュース記事を取得するプログラム
```
import feedparser


# ニュース記事のデータを取得するためのクラス
class CrawlingNews:

    def __init__(self, rss_url):
        self.rss_url = rss_url

    def crawling(self):
        # ニュース記事のRSSからタイトルと要約を取得してリストに格納する。
        data = feedparser.parse(self.rss_url)
        news = [[entry.title, entry.summary, entry.link] for entry in data.entries]

        return news

# RSSのURLを引数として渡す必要あり
URL = "https://www.news24.jp/rss/index.rdf"

news_data = CrawlingNews(URL)
news_list = news_data.crawling()
print(news_list)

```

はい、ニュース記事を取得するために必要なプログラムはこれだけです。
それでは軽く解説をしていきたいと思います。


まずは、このプログラムを動かすのに必要なライブラリを追加しましょう。
ターミナルで下記のコマンドを入力してもらえれば問題なく、追加できると思います。

```
pip install feedparser
```

このfeedparserというライブラリは何のために必要なのかというと、RSS形式のデータを取得するのに使います。「RSSとは何ぞや？？」という方に対して簡単に説明すると、ニュース記事のタイトルや要約、リンクなどを配信するデータ形式の一つです。「ニュース　RSS」とかで検索すると、大量に出てくるので一度見てもらった方がイメージしやすいと思います。

このRSS形式のデータを利用する一番のメリットは、取得するのがめっちゃ楽ということです。基本的にRSSというのは毎回同じ形式でデータを配信してくれます。これの何がいいのかというと、一度プログラムを作ってしまえば、今日も明日も明後日も企業がRSSの配信をやめない限り、データを取得することができるという点です。
例えば、通常のHTMLで書かれているニュース記事から必要なデータだけを取得しようとすると、毎回ニュースのタイトルや要約が埋め込まれている場所が違ったりするので、すごく大変です。


それではプログラムの解説をしていきます。
とは言っても、実際にこのプログラムで解説が必要なのは、この二行ぐらいだと思います。

```
data = feedparser.parse(self.rss_url)
news = [[entry.title, entry.summary, entry.link] for entry in data.entries]
```

まず、「feedparser.parse(RSSのURL)」で取得したニュースのデータを変数の中に格納してあげます。今回は、dataという変数の中に取得したデータが格納されています。

次に、この部分ですが、内包表記を通常のfor文に戻すと

```
news = [[entry.title, entry.summary, entry.link] for entry in data.entries]
```
こうなります。

```
news = []
# dataにentriesというキーを指定する
for entry in data.entries:
   sub_list = [entry.title, entry.summary, entry.link]
   news.append(sub_list)
```
別に、内包表記で書かなくても問題なく動きますが、内包表記で書けるところはできる限り内包表記で書くようにしています。というのも、リストの格納にappendを使うと結構時間がかかるんですよね。これぐらいのデータ量だとあまり差はないと思いますが、少しでもパフォーマンスを意識したコーディングをした方がいいのかな～、ということで内包表記を使っています。

ここでのポイントは先ほど取得したニュースデータを格納しているdata変数に「entries」というキーを付けて、for文で回しているということです。このentriesというキーを指定してあげることで、ニュースのタイトルや要約などの情報を取得することができます。詳細は下記の記事を参考にしてください。
また、このプログラムではニュースデータの格納された**二次元リスト**を返します。ここが、この後重要になるので覚えておいてください。

https://amateur-engineer-blog.com/feedparser-rss-reader/


## json形式への変換＋フォルダへの保存
```
import json


# クローリングしたニュースのデータをjson形式にしてフォルダーに格納するクラス
class ConvertNewsData:

    def __init__(self, news_data, file_path):
        self.news_data = news_data
        self.file_path = file_path

    def convert(self):
        news_dict = {}

        # 引数とした渡されたニュースデータの二次元配列の順番とデータを取得
        for num, data in enumerate(self.news_data):

            # 対象のニュースが何番目かという情報を辞書のキーとして利用
            filename = f"news{num}"

            # 空の辞書にニュースのタイトル、要約、リンクを追加していく
            news_dict[filename] = dict((["title", data[0]],["summary", data[1]],["link", data[2]]))

        # 辞書型のデータをjson形式に変換して、指定されたパスに格納する
        with open(self.file_path , "w", encoding="utf-8") as f:
            json.dump(news_dict, f, indent=4, ensure_ascii=False)


```
次に、先ほど取得したニュース記事のデータを、任意のフォルダに格納するプログラムの解説です。このプログラムは引数として、先ほどのプログラムで作成した**二次元リスト**を引数として渡してあげる必要があります。
下記のようにニュースデータを取得するためのクラスの実行結果を、変数に格納してあげるのが、一番簡単かなと思います。


```
# プログラムが書かれているファイルと実行ファイルを分ける場合は、importする必要があります。
from crawling import CrawlingNews
from convert_news_data import ConvertNewsData



# ニュースデータを取得するためのクラス
news_data = CrawlingNews(URL)
# 実行結果をnews_list変数に格納する。news_list変数には二次元リストが格納されている状態
news_list = news_data.crawling()

# 適当なファイルパスを指定する
file_path = "./news_folder/news.json"

# ニュースデータをjson形式に変換してフォルダに格納するためのクラス
# ニュースデータが格納された二次元リストと、保存先のファイルパスを引数として渡す
json_data = ConvertNewsData(news_list, file_path)
json_data.convert()

```
また、もう一つの引数として保存先のファイルパスを指定してあげる必要があるのですが、任意のフォルダに格納したい場合は、そのフォルダを作成してから実行する必要があります。jsonファイルは自動で生成されますが、フォルダーは生成されないため、仮にnews_folderというフォルダに格納したい場合は、事前にフォルダを作ったうえでパスを指定してあげてください。
　　
それではプログラムの解説をしていきます。このプログラムで重要になるのは、下記の部分かなと思います。

```
 def convert(self):
        news_dict = {}

        # 引数とした渡されたニュースデータの二次元配列の順番とデータを取得
        for num, data in enumerate(self.news_data):

            # 対象のニュースが何番目かという情報を辞書のキーとして利用
            filename = f"news{num}"

            # 空の辞書にニュースのタイトル、要約、リンクを追加していく
            news_dict[filename] = dict((["title", data[0]],["summary", data[1]],["link", data[2]]))

        # 辞書型のデータをjson形式に変換して、指定されたパスに格納する
        with open(self.file_path , "w", encoding="utf-8") as f:
            json.dump(news_dict, f, indent=4, ensure_ascii=False)

```

やっていることは非常にシンプルで、今回は辞書にニュース記事を格納していって、最後にjson形式に変換しています。ちなみに、jsonとpythonの辞書型の違いについては下記の記事がわかりやすいので、一度目を通してみることをお勧めします。

https://qiita.com/shungiku/items/0e87fbd144319926475c

最初のfor文ではenumerate関数を使って、いまfor文で回しているデータが何番目のデータか、という情報も取得して、その数字を辞書のキー名として利用している形になります。

そして最後に、この部分でjson形式に変換するわけですが、私はここで少し躓きました。ここで大事になるのは「encoding="utf-8"」と「ensure_ascii=False」の部分。「encoding="utf-8"」でしっかりと文字コードを指定しないと文字化けします。「ensure_ascii」はデフォルトではTrueなので、Falseだと明記してあげないと、ascii文字列以外のものはすべて「\uXXXX」みたいな文字列に変換して、よくわからない文字列が出来上がります。

```
# 辞書型のデータをjson形式に変換して、指定されたパスに格納する
with open(self.file_path , "w", encoding="utf-8") as f:
    json.dump(news_dict, f, indent=4, ensure_ascii=False)
```


## 最後に
以上でPythonでニュースをクローリングする方法の紹介は終わりになります。分かりにくい部分もあったと思いますが、少しでも役に立てたのなら幸いです。また、別の記事で今回紹介したプログラムをAWSを使って定期実行する方法についても書きますので、そちらの方も見ていただけたら嬉しいです！
それではまた。