VPoTの岩本 (iwamot) です。
先日、コーポレート部門の同僚から「弊社Webページの新着情報をSlackに通知したい」と相談を受けました。こういう相談を受けるのも、ぼくの業務のひとつです。腕の見せどころで、やる気が高まります。
見てみると、そのページはクライアントサイドで外部APIを呼び出し、取得した新着情報を動的に表示していました。RSSやAtomフィードは提供されていません。外部APIの詳細も不明です。
今回の記事では、この相談にどう考えて対応したか書いてみます。正解はないと思うので「自分ならこうする」目線でお読みいただければ幸いです。
最終的な構成
結論から書くと、最終的には下記の構成に落ち着きました。
- 動的ページのスクレイピング
- Dockerイメージ(Playwrightでスクレイピングし、新着情報RSSを出力)
- Docker Build Cloud(Dockerイメージのビルド)
- AWS CodeBuild(Dockerイメージを使ってRSSを出力し、Amazon S3に保存)
- Amazon EventBridge(定期的にCodeBuildのビルドを実行)
- RSSの配信
- Amazon CloudFront(S3に保存したRSSを配信)
- Slackへの通知
- IFTTT(RSSの新着アイテムをSlackに通知)
- ビルドエラーの監視
- CodeBuildのビルド通知(エラーをSlackに通知)
ここに至るまでに、いくつか考えたポイントがあります。
スクレイピングに何を使うか
まず、スクレイピングに何を使うかです。PlaywrightやPuppeteerなど、いろんな選択肢が考えられます。
今回は、Playwrightにしました。以前から「Puppeteerの上位互換」のような評価を目にしていて、試したかったためです。目的さえ達成できれば、なんでもかまいません。
また、言語はPythonとしました。単純に慣れているからで、深い理由はありません。
その他、パッケージ管理をどうするかなどの話も、些末なので省きます。
Dockerイメージでどこまで処理するか
次に、Dockerイメージでどこまで処理するかです。RSSを出力し、S3に保存するところまで任せてよいかもしれません。RSSは使わず、新着情報をSlackに直接ポストしてもよいでしょう。
今回は、RSSを標準出力に出力するだけにしました。RSS化すれば、新着情報を場面で活用できます。出力するRSSのS3への保存は、イメージの利用者がすべきだと考え、対象外としました。
Dockerイメージをどこで実行するか
今回いちばん悩んだのが、Dockerイメージをどこで実行するかです。CodeBuildのほかにも、AWS Lambda、Amazon ECSといった選択肢があります。
結局CodeBuildにしたのは、AWS LambdaではPlaywightがうまく動かせず、また、ECSのためだけにVPCを作るのは手間だったからでした。CodeBuildだと、ローカルと同じようにイメージがすんなり実行できました。
Dockerイメージをどうビルドするか
Dockerイメージをどうビルドするかについても、少し悩みました。Docker Build Cloudでなくても、CodeBuildでも、GitHub Actionsでも、なんでもよいといえばよいのです。
先んじて、チームメンバーの深堀さんがDocker Build Cloudを検証されていたので、今回はそれに乗っかってみました。実際、マルチアーキテクチャなイメージがさくっと作れたので、今後も積極的に使うつもりです。
SlackにRSSをどう連携するか
最後に、SlackにRSSをどう連携するかです。IFTTTを使わずとも、SlackのRSSアプリでも通知できます。
今回、IFTTTを選んだのは、通知メッセージをカスタマイズしたいとのリクエストがあったためでした。また、すでに有料プランを契約している背景もありました。
おわりに
以上、同僚からの「弊社Webページの新着情報をSlackに通知したい」との相談に、ぼくがどのように対応したかご紹介しました。
ちょっとした相談でしたが、PlaywrightやDocker Build Cloudといった未経験の技術にふれるきっかけとなってよかったです。
VPoTのぼくだからではなく、ENECHANGEでは誰でも裁量広く動けるので、試してみたい技術を気軽に試せます。
広い裁量で学びつつ仕事したい方、ENECHANGEの採用サイトをぜひご覧ください。
engineer-recruit.enechange.co.jp
おまけ:サンプルコード
せっかくなので、サンプルコードを載せておきます。いずれもMITライセンスです。ご利用になる場合は、当然ながら自己責任でお願いします。
Dockerfile
ARG SCRIPT_DIR="/script" ARG PLAYWRIGHT_BROWSERS_PATH=${SCRIPT_DIR}/browsers FROM python:3.12 as build-image ARG SCRIPT_DIR ARG PLAYWRIGHT_BROWSERS_PATH ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH} WORKDIR ${SCRIPT_DIR} COPY requirements.txt ${SCRIPT_DIR} RUN pip install --target ${SCRIPT_DIR} -r requirements.txt --no-cache-dir RUN python -m playwright install chromium FROM python:3.12-slim ARG SCRIPT_DIR ARG PLAYWRIGHT_BROWSERS_PATH ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH} RUN apt-get update && apt-get install -y --no-install-recommends \ libglib2.0-0 \ libnss3 \ libnspr4 \ libdbus-1-3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libcups2 \ libdrm2 \ libatspi2.0-0 \ libx11-6 \ libxcomposite1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libxcb1 \ libxkbcommon0 \ libpango-1.0-0 \ libcairo2 \ libasound2 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR ${SCRIPT_DIR} COPY --from=build-image ${SCRIPT_DIR} ${SCRIPT_DIR} COPY generate_rss.py ${SCRIPT_DIR} CMD [ "/usr/local/bin/python", "generate_rss.py" ]
generate_rss.py
import os from bs4 import BeautifulSoup from datetime import datetime from feedgen.feed import FeedGenerator from playwright.sync_api import sync_playwright def get_page_content(url): with sync_playwright() as playwright: with playwright.chromium.launch(headless=True) as browser: with browser.new_page() as page: page.goto(url) content = page.content() return content def generate_rss(title, description, url, content): fg = FeedGenerator() fg.title(title) fg.link(href=url, rel="alternate") fg.description(description) soup = BeautifulSoup(content, "html.parser") ul = soup.find("ul", class_="news") for li in ul.find_all("li", recursive=False): title = li.find("span", class_="title").text link = li.find("a")["href"] category = li.find("span", class_="category").text pub_date_str = li.find("span", class_="date").text pub_date = datetime.strptime(pub_date_str + "+0900", "%Y/%m/%d%z") fe = fg.add_entry() fe.title(title) fe.link(href=link) fe.category({"term": category}) fe.pubDate(pub_date) return fg.rss_str(pretty=True).decode("utf-8") if __name__ == "__main__": target_url = os.environ["TARGET_URL"] content = get_page_content(target_url) rss_title = os.environ["RSS_TITLE"] rss_description = os.environ["RSS_DESCRIPTION"] rss = generate_rss(rss_title, rss_description, target_url, content) print(rss)
requirements.txt
beautifulsoup4 feedgen playwright