🖥️

日経クロストレンドのRSSフィードを自分用に生成してみた

2023/03/10に公開

きっかけ

仕事の都合があり、日経クロストレンドで情報収集をすることにしました。
https://xtrend.nikkei.com/
WEBサイトの記事はFeedlyにRSSフィードを登録してまとめて読むようにしているので、これもまとめちゃお~と思ったのですが、なんと日経クロストレンドは新着記事のRSSフィードを吐いていないようです[1]
ないなら作ればいいんじゃない?ということで、PythonもAWSも初心者の筆者がやってみたら意外とさくっとできたので、自分の整理も兼ねて記事にしようと思った次第です。

作りたいことの要件

  • 新着記事一覧のRSSファイルを生成する
  • 生成したRSSファイルはFeedlyが読みに行けるインターネット上のどこかに置かれていること
  • 上記の処理を定期的(1日に1回)に自動実行する環境を作って運用する

作ったものの処理流れ

LambdaS3で実装しました。初心者でもかんたんにできた!(と思う)

  1. Lambda新着記事一覧ページから必要箇所をスクレイピング
  2. 取得したデータからRSSファイルを生成する
  3. 生成したRSSファイルをインターネット公開設定したS3バケットにアップロード
  4. 上記1,2の処理をAWS Lambdaで1日1回定時実行する設定を登録

あとは、S3上のRSSファイルのURLをFeedlyに登録すれば、毎日快適に新着記事を読む生活がはじまります。
では以降でさっそく作ってきたいと思います。

ステップ①S3バケットの準備(公開用バケットの登録・設定)

まずはS3バケットの準備をします。

上記の通り、最終的にRSSファイルを置く場所になりますので、Feedlyから読みに行ける場所、つまりインターネットへの公開設定をしたバケットを作る必要があります。

完全に以下のページを参考にして設定しましたので、詳細はこちらを参照ください↓
https://zenn.dev/longbridge/articles/30da1524649ef9
記事内にもある通り、設定が完了したら、ダミーファイルでよいのでS3に一度アップロードしてみて、そのファイルのURLをWEBブラウザから普通に読み込みに行けるかテストをすると良いと思います。読み込めない場合はおそらくAccess Deniedって出ると思うので、設定が間違ってるということです。

ステップ②新着記事一覧のページ構造の分析

Lambdaへの設定に着手する前に、そもそもどうやったら新着記事一覧を取得できるかをきちんと分析しておきたいと思います。

新着記事一覧ページを開いて、右クリック→検証 でページ構造をみていきます。

スクレイピングする時にめちゃ便利ですよね、この機能。

さて、フムフムと見ていくと、<li class="backnumber-list-item">というHTMLタグが記事分繰り返して出現しているのが見て取れます。このタグの間に記事タイトルや概要文、記事URLも含まれているようなので、backnumber-list-itemというclassが含まれる部分をまるっと抽出できれば必要な情報は得られそうです。

また、この新着記事一覧ページはどうやら動的に生成されるページのようです。
動的に生成されるページは、HTMLのソースをただ単純に取得するだけではコンテンツは取れません(ブラウザで読みに行ってはじめてコンテンツがロードされるので)。よってこういう場合はSeleniumを使って、あたかもページを実際にロードしたかのような動きを実行し、コンテンツをロードさせてからスクレイピングをする必要があります。実際にどうコードを書くかは以降のステップ④で見ていきます!

ステップ③Lambdaの準備(環境設定)

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
Lambdaとは、要するに「やらせたい処理があるんだけど、EC2立てるほどじゃねえな…ちょっとした処理だからさくっと実装したいな…」という、まさに今回のようなタスクにうってつけの便利サービスだと個人的には理解しています!異論は認めます!

まずは環境設定ですが、こちらも例によって素晴らしい先人の知恵を完全に参考にしましたので、詳細はこちらを参照ください↓
https://dev.classmethod.jp/articles/aws-lambda-python-selenium-make-env/
記事内をすべて消化すると環境設定完了です。次のステップでLambda関数の設定をしていきますが、ここで作ったサンプルの関数を流用して作っていく流れにすれば環境設定でつまづきにくくなると思います。

なお、今回の目的の処理のためには、この記事に載っているものに追加してPythonパッケージを準備しておく必要があります。
ですので、「パッケージの用意→Seleniumのダウンロード」の節でPython3.7用のSeleniumパッケージをダウンロードしていますが、ここの作業で合わせて以下のパッケージもダウンロードしておくようにコマンドを叩いておいてください。

pip3 install beautifulsoup4 -t ./python/lib/python3.7/site-packages
pip3 install lxml -t ./python/lib/python3.7/site-packages
pip3 install boto3 -t ./python/lib/python3.7/site-packages
pip3 install feedgenerator -t ./python/lib/python3.7/site-packages

ステップ④Lambda関数の設定

お待たせしました。いよいよ本丸であるLambda関数の設定をしていきます!
ステップ③で作ったサンプルのLambda関数を開くかコピーして作っていくとよいと思います。

コードがやや長くなってしまったので、各ブロックごとに分けて書いていきます。「コピペしたいだけだから先に全部見せろ」という方は以下をクリックしてください!

コード全体はこちら(クリックで開きます)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import feedgenerator
import boto3
import time
import os

def lambda_handler(event, context):
    # Seleniumの設定
    options = Options()
    options.add_argument('--headless')
    options.add_argument('--single-process')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument("--no-sandbox")
    options.binary_location = "/opt/headless/headless-chromium"
    
    # Selenium起動
    driver = webdriver.Chrome(executable_path = "/opt/headless/chromedriver", options=options)
    driver.get("https://xtrend.nikkei.com/atcl/contents/new/")
    time.sleep(10) #ページにコンテンツがロードされるまで少し待つ
    
    # 取得したコンテンツを保存して、Seleniumを終了
    html = driver.page_source
    driver.quit()
    
    # HTMLをパースして新着記事が含まれるパートのみに絞り込み
    soup = BeautifulSoup(html,'lxml')
    elems = soup.select('.backnumber-list-item')
    
    # RSS Feedの生成
    feed = feedgenerator.Rss201rev2Feed(
        title="※お好きなフィード名を入力",
        link="※お好きなURLを入力",
        description="※お好きな文章を入力",
        language="ja")
    
    # 各記事ごとに記事名・概要・URLの情報を抽出
    for elem in elems:
        title = elem.select(".backnumber-list-title")[0].text.replace("\u3000"," ")
        desc = elem.select(".backnumber-list-text")[0].text.replace("\u3000"," ")
        url = "https://xtrend.nikkei.com" + elem.select("a")[0].get("href")
    
	# RSS feedに記事ごとの情報を追加
        feed.add_item(
            title=title,
            link=url,
            description=desc)
    
    # RSS feedをtmp領域に一旦保存
    with open('/tmp/feed_xtrend.rss', 'w') as fp:
        feed.write(fp, 'utf-8')
        
    # 保存したRSS FeedをS3にアップロード
    client = boto3.client(
        's3',
        aws_access_key_id='※アクセスキーID',
        aws_secret_access_key='※シークレットアクセスキー',
        region_name='※リージョン'
    )
    
    Filename = '/tmp/feed_xtrend.rss'
    Bucket = '※S3バケット名'
    Key = 'feed/feed_xtrend.rss'
    client.upload_file(Filename, Bucket, Key)

    #最後にtmp領域のRSSフィードファイルを削除(次回実行時に再利用されるのを防ぐため)
    os.remove('/tmp/feed_xtrend.rss')

    return

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import feedgenerator
import boto3
import time
import os

まずは必要モジュールのインポートです。ここは説明省略します。

def lambda_handler(event, context):
    # Seleniumの設定
    options = Options()
    options.add_argument('--headless')
    options.add_argument('--single-process')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument("--no-sandbox")
    options.binary_location = "/opt/headless/headless-chromium"
    
    # Selenium起動
    driver = webdriver.Chrome(executable_path = "/opt/headless/chromedriver", options=options)
    driver.get("https://xtrend.nikkei.com/atcl/contents/new/")
    time.sleep(10) #ページにコンテンツがロードされるまで少し待つ
    
    # 取得したコンテンツを保存して、Seleniumを終了
    html = driver.page_source
    driver.quit()

続いて、lambda関数の中身を書いていきます。まずはSeleniumを動かすパートです。
普通にSeleniumを起動するとブラウザ(chrome)が立ち上がるのですが、それだとLambda上では動かないので、ヘッドレスモードというブラウザが立ち上がらないモードに設定をしておきます。
options.binary_location = "/opt/headless/headless-chromium"driver = webdriver.Chrome(executable_path = "/opt/headless/chromedriver", options=options)にあるとおり、レイヤーから読み込んだファイルは/opt/配下に解凍されて利用される仕様になっているとのことで、こんな感じに書くと動きました。

ページを読み込んだのち、コンテンツがロードされるまで少し時間がかかるのでtime.sleep(10)で10秒動きを止めています。3秒で何も取得できず空振りしたことがあったので、やや長めにしてあります。

その後取得したページの内容をhtmlというオブジェクトに移して、Seleniumさんはお仕事終了です。

    # HTMLをパースして新着記事が含まれるパートのみに絞り込み
    soup = BeautifulSoup(html,'lxml')
    elems = soup.select('.backnumber-list-item')
    
    # RSS Feedの生成
    feed = feedgenerator.Rss201rev2Feed(
        title="※お好きなフィード名を入力",
        link="※お好きなURLを入力",
        description="※お好きな文章を入力",
        language="ja")
    
    # 各記事ごとに記事名・概要・URLの情報を抽出
    for elem in elems:
        title = elem.select(".backnumber-list-title")[0].text.replace("\u3000"," ")
        desc = elem.select(".backnumber-list-text")[0].text.replace("\u3000"," ")
        url = "https://xtrend.nikkei.com" + elem.select("a")[0].get("href")
    
	# RSS feedに記事ごとの情報を追加
        feed.add_item(
            title=title,
            link=url,
            description=desc)

ここからは取得したページについてBeautifulSoupで必要要素を抽出していきます。

ステップ②で見た通り、.backnumber-list-itemというクラスが設定されているところだけあればよさそうなので、まずそこだけに絞っておきます。

次に、feedオブジェクトを作り、各記事の記事名・概要・URLにあたる部分を抽出して追加していきます。このfeedオブジェクトが最終的に作りたいRSSファイルになります。
forループの中で必要要素を抽出していますが、もうちょっとスマートな書き方がある気がするもののとりあえず動いたのでそのままにしています。。詳しい方コメント欄で教えてください!

    # RSS feedをtmp領域に一旦保存
    with open('/tmp/feed_xtrend.rss', 'w') as fp:
        feed.write(fp, 'utf-8')
        
    # 保存したRSS FeedをS3にアップロード
    client = boto3.client(
        's3',
        aws_access_key_id='※アクセスキーID',
        aws_secret_access_key='※シークレットアクセスキー',
        region_name='※リージョン'
    )
    
    Filename = '/tmp/feed_xtrend.rss'
    Bucket = '※S3バケット名'
    Key = 'feed_xtrend.rss'
    client.upload_file(Filename, Bucket, Key)

    #最後にtmp領域のRSSフィードファイルを削除(次回実行時に再利用されるのを防ぐため)
    os.remove('/tmp/feed_xtrend.rss')

    return

前のブロックで作ったfeedオブジェクトをRSSファイルとして一旦保存します。
一旦保存しないやり方もありそうでしたが今回は保存するやり方で実装してみました。。/tmp/配下が保存領域として使えるようなので、そこに保存します。

あとはこのファイルをboto3を使ってS3にアップロードするだけです。

ちなみに最後の処理ですが、この/tmp/領域は次回以降実行時に再利用されることがあるらしいので、次回実行時に悪さをしないように最後にファイル削除をしておくようにしました。

これでLambda関数の設定は終了です。
最後に画面上のDeployを押し、完了したらTestを押してエラーなく通るかテストします。エラーが出ていたら、がんばって解消してください。。

うまくできていたらS3にRSSファイルが置かれているはずなので、そちらのファイルを確認しにいって「お、うまくできてるな」とニヤニヤしましょう。

ステップ⑤定時実行の設定

うまく動くものができたら、定期的に実行できるように設定をしておきましょう。

ここからトリガーを追加をクリックして、

EventBridge (ClodwWatch Events)を選択します。

ルール名はなんでもよく、ルールタイプスケジュール式に、cron(0 22 * * ? *)というクセがすごい式を書きます。これはcron式と言われる記法らしく、この場合「毎日UTC22時(JST7時)ちょうどに実行しますよ」という意味になるようです。毎日だいたい朝5時か6時あたりには記事が更新されていそうだったのでこのような設定にしています。

感想

例によって環境設定がわりと面倒で初心者にとってはハマりポイントですが、そこを乗り越えてしまえば、この程度のライトな処理であればわりと簡単に実装できるんだな~というのが率直な感想です。
あとLambdaめっちゃ便利ですね!(今更感)
引き続き勉強していろんなものを作っていきたいと思います。長文にも関わらず最後までお読みいただきありがとうございました!

脚注
  1. ポッドキャストのみRSSフィードが吐かれている。なんでや。 https://xtrend.nikkei.com/info/podcast/19/ ↩︎

Discussion