ENECHANGE Developer Blog

ENECHANGE開発者ブログ

動的Webページの新着情報をどうSlackに通知するか

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を検証されていたので、今回はそれに乗っかってみました。実際、マルチアーキテクチャなイメージがさくっと作れたので、今後も積極的に使うつもりです。

tech.enechange.co.jp

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