日経クロストレンドのRSSフィードを自分用に生成してみた
きっかけ
仕事の都合があり、日経クロストレンドで情報収集をすることにしました。[1]。
ないなら作ればいいんじゃない?ということで、PythonもAWSも初心者の筆者がやってみたら意外とさくっとできたので、自分の整理も兼ねて記事にしようと思った次第です。
作りたいことの要件
- 新着記事一覧のRSSファイルを生成する
- 生成したRSSファイルはFeedlyが読みに行けるインターネット上のどこかに置かれていること
- 上記の処理を定期的(1日に1回)に自動実行する環境を作って運用する
作ったものの処理流れ
Lambda
とS3
で実装しました。初心者でもかんたんにできた!(と思う)
-
Lambda
で新着記事一覧ページから必要箇所をスクレイピング - 取得したデータからRSSファイルを生成する
- 生成したRSSファイルをインターネット公開設定した
S3
バケットにアップロード - 上記1,2の処理を
AWS Lambda
で1日1回定時実行する設定を登録
あとは、S3上のRSSファイルのURLをFeedlyに登録すれば、毎日快適に新着記事を読む生活がはじまります。
では以降でさっそく作ってきたいと思います。
ステップ①S3バケットの準備(公開用バケットの登録・設定)
まずはS3バケットの準備をします。
上記の通り、最終的にRSSファイルを置く場所になりますので、Feedlyから読みに行ける場所、つまりインターネットへの公開設定をしたバケットを作る必要があります。
完全に以下のページを参考にして設定しましたので、詳細はこちらを参照ください↓Access Denied
って出ると思うので、設定が間違ってるということです。
ステップ②新着記事一覧のページ構造の分析
Lambdaへの設定に着手する前に、そもそもどうやったら新着記事一覧を取得できるかをきちんと分析しておきたいと思います。
新着記事一覧ページを開いて、右クリック→検証 でページ構造をみていきます。
スクレイピングする時にめちゃ便利ですよね、この機能。
さて、フムフムと見ていくと、<li class="backnumber-list-item">
というHTMLタグが記事分繰り返して出現しているのが見て取れます。このタグの間に記事タイトルや概要文、記事URLも含まれているようなので、backnumber-list-item
というclassが含まれる部分をまるっと抽出できれば必要な情報は得られそうです。
また、この新着記事一覧ページはどうやら動的に生成されるページのようです。
動的に生成されるページは、HTMLのソースをただ単純に取得するだけではコンテンツは取れません(ブラウザで読みに行ってはじめてコンテンツがロードされるので)。よってこういう場合はSelenium
を使って、あたかもページを実際にロードしたかのような動きを実行し、コンテンツをロードさせてからスクレイピングをする必要があります。実際にどうコードを書くかは以降のステップ④で見ていきます!
ステップ③Lambdaの準備(環境設定)
Lambda
とは、要するに「やらせたい処理があるんだけど、EC2立てるほどじゃねえな…ちょっとした処理だからさくっと実装したいな…」という、まさに今回のようなタスクにうってつけの便利サービスだと個人的には理解しています!異論は認めます!
まずは環境設定ですが、こちらも例によって素晴らしい先人の知恵を完全に参考にしましたので、詳細はこちらを参照ください↓
記事内をすべて消化すると環境設定完了です。次のステップで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めっちゃ便利ですね!(今更感)
引き続き勉強していろんなものを作っていきたいと思います。長文にも関わらず最後までお読みいただきありがとうございました!
-
ポッドキャストのみRSSフィードが吐かれている。なんでや。 https://xtrend.nikkei.com/info/podcast/19/ ↩︎
Discussion