3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

遊戯王の新カード情報を自動で通知するBotをAWSサーバーレスで作った話

3
Posted at

はじめに

遊戯王OCGプレイヤーにとって、新カードの情報はいち早くチェックしたいものです。しかし、公式サイトやX(旧Twitter)を毎日何度も確認するのは手間がかかります。

そこで、公式サイトとXの公式アカウントを自動で監視し、新カード情報があればDiscordとLINEに通知するBotをAWSのサーバーレス構成で作成しました。さらに、LINE公式アカウントにカード名を送ると該当するカード情報を検索できるインタラクティブ機能も実装しています。

この記事では、システムの全体像から実装の詳細までを紹介します。

システム概要

本システムは大きく2つの機能を持っています。

  1. 定期監視モード: EventBridgeによるスケジュール実行で公式Xアカウントを監視し、新カード情報をDiscord・LINEに自動通知
  2. 対話モード: LINE公式アカウントにキーワードを送ると、該当するカード情報を検索して返信

アーキテクチャ

┌──────────────────────────────────────────────────────────┐
│                     AWS Cloud                            │
│                                                          │
│  ┌─────────────┐     ┌─────────────────────────┐        │
│  │ EventBridge  │────→│                         │        │
│  │ (定期実行)    │     │     AWS Lambda          │        │
│  └─────────────┘     │     (Python 3.12)       │        │
│                      │     Dockerコンテナ        │        │
│  ┌─────────────┐     │                         │        │
│  │ API Gateway  │────→│                         │        │
│  │ (LINE       │     └───────┬───────┬─────────┘        │
│  │  Webhook)   │             │       │                   │
│  └─────────────┘             │       │                   │
│                              │       │                   │
│                    ┌─────────▼──┐  ┌─▼──────────┐       │
│                    │  DynamoDB   │  │    EC2      │       │
│                    │ (通知履歴)   │  │ (Squidプロキシ│       │
│                    └────────────┘  │  日本IP)     │       │
│                                    └─────────────┘       │
└──────────────────────────────────────────────────────────┘
         │                       │
         ▼                       ▼
┌────────────────┐     ┌────────────────┐
│  Discord       │     │  X API v2      │
│  Webhook       │     │ (ツイート取得)   │
└────────────────┘     └────────────────┘
         │
         ▼
┌────────────────┐
│  LINE          │
│  Messaging API │
└────────────────┘

技術スタック

カテゴリ 技術
言語 Python 3.12
コンピュート AWS Lambda(Dockerコンテナ)
スケジューラ Amazon EventBridge
データベース Amazon DynamoDB
プロキシ Amazon EC2 + Squid
スクレイピング Playwright(Chromium)
HTML解析 BeautifulSoup4
通知 Discord Webhook / LINE Messaging API
SNS監視 X API v2
CI/CD GitHub Actions + OIDC認証
コンテナレジストリ Amazon ECR

実装の詳細

1. Lambda関数のエントリーポイント

Lambda関数は、イベントソースに応じて処理を振り分ける設計にしています。

def lambda_handler(event, context):
    if "httpMethod" in event or "requestContext" in event:
        return handle_api_gateway_event(event)  # LINE Webhook
    else:
        return handle_scheduled_event(event)     # 定期実行
  • EventBridgeからのイベント → Xスクレイピング → 通知
  • API Gatewayからのイベント → LINEメッセージの処理 → 返信

2. Xスクレイピング(x_scraping.py)

X API v2を使って遊戯王公式アカウント(@YuGiOh_OCG_INFO)のツイートを取得し、新カード情報をフィルタリングしています。

KEY_WORD = ["カード公開", "再録", "付録", "新カード", "カードを公開"]
NG_WORD  = ["実物", "ラッシュデュエル"]

フィルタリングのポイント:

  • キーワードに1つでも合致 かつ NGワードを含まないツイートだけを抽出
  • 公式アカウントのツイートでは◤カード名◢の形式でカード名が記載されるため、正規表現で抽出
card_name = re.search(r"◤(.*?)◢", text)

3. 公式サイトスクレイピング(scraper.py)

遊戯王公式サイト(yu-gi-oh.jp)はCloudflareによって保護されているため、通常のrequestsではアクセスできません。そこでPlaywright(ヘッドレスChromium)+ EC2プロキシという構成で突破しています。

def fetch_html_playwright(url: str) -> str:
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            proxy={"server": PROXY_SERVER},
            args=["--no-sandbox", "--disable-setuid-sandbox", ...]
        )
        page = browser.new_page()
        page.set_extra_http_headers({
            "User-Agent": "Mozilla/5.0 ...",
            "Accept-Language": "ja,en-US;q=0.9,en;q=0.8"
        })
        page.goto(url, timeout=60000)
        html = page.content()
        browser.close()
        return html

なぜEC2プロキシが必要か:

  • 公式サイトは日本国外からのアクセスを制限している場合がある
  • AWS LambdaのIPは日本リージョンでも海外IP扱いされることがある
  • EC2上のSquidプロキシ(日本IP)を経由することでこの問題を回避

取得したHTMLはBeautifulSoupで解析し、「収録」「再録」「付録」といったキーワードを含む新着情報のみを抽出します。

4. 重複通知の防止(dynamodb.py)

同じ情報を何度も通知しないよう、DynamoDBで通知履歴を管理しています。

def is_notified(item_id: str) -> bool:
    response = table.get_item(Key={"id": item_id})
    return "Item" in response

def mark_as_notified(item_id: str):
    today = datetime.now()
    timestamp = int(datetime.timestamp(today))
    table.put_item(Item={"id": item_id, "time": timestamp})

テーブル内のレコードは最大20件に制限し、古いものから自動的に削除するようにしています。これにより、DynamoDBのコストを最小限に抑えつつ、直近の通知履歴を保持できます。

5. 通知システム(notifier.py)

通知先はDiscordとLINEの2チャンネルです。

Discord通知:

def notify_discord(scraping_results: list[str], hook_url: str):
    for message in scraping_results:
        payload = {"content": message}
        requests.post(hook_url, json=payload, timeout=10)

LINE Bot通知(Push / Reply):

# プッシュ通知(定期実行時)
def notify_linebot(scraping_results: list[str], url: str):
    for message in scraping_results:
        payload = {
            "to": USER_ID,
            "messages": [{"type": "text", "text": message}]
        }
        requests.post(url, headers=headers, data=json.dumps(payload))

# リプライ(ユーザーのメッセージに返信)
def reply_linebot(reply_token: str, messages: list[str]):
    line_messages = [{"type": "text", "text": msg[:5000]} for msg in messages[:5]]
    payload = {"replyToken": reply_token, "messages": line_messages}
    requests.post("https://api.line.me/v2/bot/message/reply", ...)

LINE Webhookの署名検証も実装しており、不正なリクエストを拒否します。

def verify_line_signature(body: str, signature: str) -> bool:
    hash_value = hmac.new(
        LINE_CHANNEL_SECRET.encode("utf-8"),
        body.encode("utf-8"),
        hashlib.sha256
    ).digest()
    expected = base64.b64encode(hash_value).decode("utf-8")
    return hmac.compare_digest(signature, expected)

6. LINEによるカード検索機能

LINE公式アカウントにカード名を送ると、Xの公式アカウントの投稿から該当するカード情報を検索して返信します。

def handle_api_gateway_event(event):
    # 署名検証
    if not verify_line_signature(body, signature):
        return {"statusCode": 403, "body": "Invalid signature"}

    for line_event in events:
        keyword = line_event["message"]["text"]
        results = search_tweets([keyword])
        if results:
            messages = [f"{r['card_name']}\n{r['url']}" for r in results]
            reply_linebot(reply_token, messages)
        else:
            reply_linebot(reply_token, [f"「{keyword}」に該当するカードは見つかりませんでした。"])

Dockerコンテナの構成

Lambda上でPlaywrightを動かすため、Dockerコンテナイメージを使用しています。

FROM public.ecr.aws/lambda/python:3.12

# Chromiumの依存ライブラリをインストール
RUN dnf install -y \
    libXcomposite libXcursor libXdamage libXext libXi libXtst \
    nss nss-tools atk cups-libs libdrm pango alsa-lib \
    at-spi2-atk gtk3 mesa-libgbm libxkbcommon \
    xorg-x11-server-Xvfb wget unzip \
    && dnf clean all

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Playwrightとブラウザのインストール
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN mkdir -p /ms-playwright
RUN pip install playwright
RUN playwright install chromium
RUN chmod -R 755 /ms-playwright

COPY . ${LAMBDA_TASK_ROOT}
CMD ["lambda_function.lambda_handler"]

ポイント:

  • Lambda公式のPython 3.12ベースイメージを使用
  • Chromiumの動作に必要なシステムライブラリを手動でインストール
  • PLAYWRIGHT_BROWSERS_PATHでブラウザの格納先を明示的に指定

CI/CDパイプライン

GitHub ActionsでOIDC認証を使った自動デプロイを実現しています。AWSのアクセスキーをGitHubに保存する必要がなく、セキュアな運用が可能です。

name: Deploy to Lambda via OIDC

on:
  push:
    branches:
      - scheduled_by_aws_Xscraping

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/ygo_scraper_OIDC
          aws-region: ap-northeast-1

      - name: Build and push Docker image
        run: |
          docker build -f ./src/Dockerfile -t $ECR_REGISTRY/$ECR_REPOSITORY:latest ./src
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: Update Lambda function
        run: |
          aws lambda update-function-code \
            --function-name new_card_bot_Xscraping \
            --image-uri $ECR_REGISTRY/$ECR_REPOSITORY:latest

デプロイフロー:

  1. 対象ブランチにpush
  2. OIDC認証でAWSにログイン
  3. DockerイメージをビルドしてECRにpush
  4. Lambda関数のイメージを更新

設計上の工夫と苦労した点

Cloudflare対策

公式サイトはCloudflareで保護されており、requestsやcloudscraperでは安定してアクセスできませんでした。最終的にPlaywright(ヘッドレスChromium)を採用することで、JavaScript実行を含むChallenge Pageを突破できるようになりました。ただし、Lambdaでブラウザを動かすためにはDockerコンテナイメージが必須で、イメージサイズやコールドスタートのトレードオフがあります。

DynamoDBによる状態管理

Lambdaはステートレスなため、「どの情報を既に通知したか」を外部に保持する必要があります。DynamoDBはサーバーレスと相性が良く、無料枠の範囲内で運用できています。レコード数を20件に制限することで、コストをほぼゼロに抑えています。

OIDC認証によるセキュアなCI/CD

GitHub ActionsからAWSへの認証にOIDCを使うことで、長期間有効なアクセスキーをGitHub Secretsに保存する必要がなくなりました。IAMロールに必要最小限の権限を付与することで、セキュリティリスクを最小化しています。

LINE Botの双方向通信

LINE Messaging APIのPush APIとReply APIを使い分けることで、定期通知と対話型検索の両方を実現しています。Webhook署名検証(HMAC-SHA256)により、不正なリクエストからBotを保護しています。

全体のファイル構成

new_card_bot/
├── .github/
│   └── workflows/
│       └── deploy.yml          # GitHub Actions デプロイ定義
├── src/
│   ├── lambda_function.py      # Lambdaエントリーポイント
│   ├── scraper.py              # 公式サイトスクレイピング
│   ├── x_scraping.py           # X API スクレイピング
│   ├── notifier.py             # Discord / LINE 通知
│   ├── dynamodb.py             # DynamoDB 操作
│   ├── ec2.py                  # EC2 プロキシ制御
│   ├── Dockerfile              # 本番用コンテナ定義
│   ├── Dockerfile_test         # テスト用コンテナ定義
│   └── requirements.txt        # Python依存パッケージ
├── requirements.txt
├── .gitignore
└── README.md

まとめ

本記事では、遊戯王の新カード情報を自動で収集・通知するBotの設計と実装について紹介しました。

システムのポイント:

  • AWSサーバーレス構成(Lambda + EventBridge + DynamoDB)で低コスト運用
  • Playwright + EC2プロキシでCloudflare保護サイトのスクレイピングを実現
  • X API v2で公式アカウントの投稿をリアルタイム監視
  • Discord / LINE の2チャンネルで通知を受け取れる
  • LINE公式アカウントからカード情報のインタラクティブ検索が可能
  • GitHub Actions + OIDC認証でセキュアなCI/CDパイプラインを構築

AWSの無料枠を活用すればほぼ無料で運用でき、趣味のプロジェクトとして非常にコスパの良いシステムになっています。同じように特定サイトの更新情報を自動で追いたい方の参考になれば幸いです。

作成した公式LINE

https://line.me/R/ti/p/@898txacv

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?