プレミアムバンダイでガンプラの新商品が出たら通知する機能を追加した

 以下の記事で紹介させて頂いた twitter bot に一つ通知を追加しました。それは「プレミアムバンダイに新しく追加されたガンプラ」の通知になります。

具体的にはこちらの検索「予約・販売期間:開始前」、「キーワード:ガンプラ」に新しく出てきた商品があったらそれを通知します。

 公式ツイッターでも新商品を予約開始前にツイートしてくれますが、結構ギリギリだったりガンプラ以外の紹介もあるので結構見逃してしまうことがあったのでガンプラに特化した通知が欲しくて機能を追加しました。

 私が一番最近にプレミアムバンダイで予約したキットが以下のツイートのものですが、予約開始が 13:00 で、そのツイートされた時間が 12:41 です。流石にギリギリ過ぎますね。通知ONにしてたら予約開始前に気づくかもしれませんが、プレミアムバンダイのツイートはガンプラだけじゃないので、その全てのツイートが通知されるのも鬱陶しいです。

久しぶりに F5 連打して予約した。

 ツイートはこのようにギリギリにされますが、実はプレミアムバンダイのサイトには商品ページが、その日のもっと早い時間から用意されていました。なのでプレミアムバンダイのサイトを定期的に巡回していればいち早く情報をキャッチすることが出来ます。そこで巡回をするプログラムを作って、bot に通知するようにしました

今回追加した通知の詳細
  • プレミアムバンダイの商品検索「予約・販売期間:開始前」、「キーワード:ガンプラ」で追加されたものがあったらツイートする。
  • 巡回は午前中に10分間隔で実行する。(サイトの更新が午前中であることが多いため)
  • 追加されたキットを一つ一つツイートする。(ここは改善の余地あり)

今回の構成

プログラム

 今回作成したプログラムはこちらです。ポイントは前回からの巡回から追加された商品を見つけるために巡回の結果をどこかに保存しておく必要がありました。今回も AWS Lamdba で実行させるつもりだったので保存場所として S3Bucket を選びました。巡回結果を JSON ファイルに出力して S3Bucket に保存しています。

# coding: UTF-8
import env
import logging
import json
import re
import datetime
import time
import os
import requests
from bs4 import BeautifulSoup
import boto3
import post_twitter

logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.info('start ' + __file__)

s3 = boto3.resource('s3')


def get_list_from_web(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser", from_encoding="shift_jis")
    search_result = soup.find("div",class_="pbFluid-p-card-list")

    uls = search_result.find_all("ul",class_="pbFluid-p-card")
    """
    <ul class="pbFluid-p-card">
        <li class="pbFluid-p-card__image">
            <a href="/item/item-1000162659/?source=csearch&medium=pc">
                <img height="150" id="" src="//bandai-a.akamaihd.net/bc/img/model/b/1000162659_1.jpg" width="150"/>
            </a>
        </li>
        <li class="pbFluid-p-card__label">
            <img class="sRL_ItemIcon_20160229" src="https://bandai-a.akamaihd.net/bc/img/icon/ITEM_RESERVE_BEFORE.gif"/>
            <img class="sRL_ItemIcon_20160229" src="https://bandai-a.akamaihd.net/bc/img/icon/RESERVE_202202.gif"/>
        </li>
        <li class="pbFluid-p-card__description">
            <a href="/item/item-1000162659/?source=csearch&medium=pc">MG 1/100 エクリプスガンダム【2022年2月発送】</a>
        </li>
        <li class="pbFluid-p-card__tooltip">
            <a class="pbFluid-p-card__tooltip__detail" href="/item/item-1000162659/?source=csearch&medium=pc">MG 1/100 エクリプスガンダム【2022年2月発送】</a>
        </li>
    </ul>
    """
    list = []
    for ul in uls:
        label_imgs = ul.find("li",class_="pbFluid-p-card__label").find_all("img")
        labels = []
        for label_img in label_imgs:
            label = re.sub('https://bandai-a.akamaihd.net/bc/img/icon/', '', label_img['src'])
            labels.append(label)

        item_img = ul.find("li",class_="pbFluid-p-card__image").find("img")['src']
        img_url  = 'https:' + item_img

        tooltip = ul.find("li",class_="pbFluid-p-card__tooltip")
        name = tooltip.text
        link = "https://p-bandai.jp" + re.search('/item/item-[0-9]*/', tooltip.a['href']).group()

        list.append({"name":name, "url":link, "label":labels, "img":img_url})

    new_list = json.dumps({"p-bandai_release":list}, ensure_ascii=False, indent=2)
    logger.info('got relese list from p-bandai.')
    return list

def search_update(old_list, new_list):
    updates = []
    old_names = [d['name'] for d in old_list]
    for kit in new_list:
        if kit['name'] not in old_names:
            updates.append(kit)
    return updates

def get_preorder_open(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser", from_encoding="shift_jis")
    article_details_shop = soup.find("div",class_="article_details_shop")
    return preorder_open

#if __name__ == '__main__':
def handler(event, context):
    try:
        now = datetime.datetime.now()

        # 最新の開始前販売・予約リストをプレミアムバンダイから取得
        url = "https://p-bandai.jp/chara/c0010/?q=%83K%83%93%83v%83%89&page=0&n=60&C5=30"
        new_list = get_list_from_web(url)
        #new_list = json.load(open('new_list.json', 'r'))

        # 前回取得したリストを S3Bucket から取得
        s3.meta.client.download_file(env.s3_buchket, 'gunpla.json', '/tmp/gunpla.json')
        logger.info('download previous list from s3bucket.')
        old_list = json.load(open('/tmp/gunpla.json', 'r'))
        #old_list = json.load(open('old_list.json', 'r'))

        # 前回との差分を抽出
        update_list = search_update(old_list['gunplas'], new_list)

        if len(update_list) == 0:
            logger.info('no update')
        else:
            for update in update_list:
                kit_name  = update['name']
                kit_url   = update['url']
                labels = update['label']
                label_jpn = ""
                for label in labels:
                    if label == "ITEM_SALE_BEFORE.gif":
                        label_jpn = "【販売開始前】"
                    elif label == "ITEM_RESERVE_BEFORE.gif":
                        label_jpn = "【予約開始前】"
                    elif "RESERVE_" in label:
                        ym = re.search('[0-9]+', label).group()
                        y  = ym[:4]
                        m  = ym[4:]
                        label_jpn = label_jpn + "【" + y + "年" + m + "月発送】" #【20yy年mm月発送】
                    else:
                        logger.warning('unexpected label ' + label)

                text = '【' + now.strftime("%m/%d") + '更新】' + label_jpn + '\n' + kit_name + 'がプレミアムバンダイに新しく追加されました。\n' + kit_url
                post_twitter.post(env.access_token,env.access_token_secret,env.api_key,env.api_secret,text)
                logger.info('posted that updated p-bandai to twitter.')

        # 最新のリストを S3Bucket にアップロード
        with open('/tmp/new_gunpla.json', 'w') as f:
            json.dump({'data':now.strftime("%Y-%m-%d"), 'gunplas':new_list}, f, ensure_ascii=False, indent=2)
        s3.meta.client.upload_file('/tmp/new_gunpla.json', env.s3_buchket, 'gunpla.json')
        logger.info('uploaded new list to s3bucket.')

    except Exception as e:
        logger.exception(f'{e}')

実行環境

 実行環境は AWS の以下のサービスで構成されています。サーバレスですね。

  • Lambda Function
  • Cloudwatch Events
  • S3 Bucket

以下が tf ファイルです。

resource "aws_ecr_repository" "get_new_reserve_pbandai" {
  name = "get_new_reserve_pbandai"
}

resource "aws_ecr_lifecycle_policy" "get_new_reserve_pbandai" {
  repository = aws_ecr_repository.get_new_reserve_pbandai.name

  policy = <<EOF
{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Expire images older than 3 days",
            "selection": {
                "tagStatus": "untagged",
                "countType": "sinceImagePushed",
                "countUnit": "days",
                "countNumber": 3
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}
EOF
}

resource "aws_iam_role" "get_new_reserve_pbandai" {
  name                = "get_new_reserve_pbandai"
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      },
    ]
  })

  inline_policy {
    name = "inline_policy"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action = [
            "s3:GetObject",
            "s3:PutObject",
            "s3:HeadObject"
          ]
          Effect = "Allow"
          Resource = [
            "${aws_s3_bucket.get_new_reserve_pbandai.arn}",
            "${aws_s3_bucket.get_new_reserve_pbandai.arn}/*"
          ]
        },
      ]
    })
  }
}

resource "aws_lambda_function" "get_new_reserve_pbandai" {
  function_name = "get_new_reserve_pbandai"
  package_type  = "Image"
  memory_size   = 128
  timeout       = 180
  role          = aws_iam_role.get_new_reserve_pbandai.arn
  image_uri     = "653376028258.dkr.ecr.ap-northeast-1.amazonaws.com/get_new_reserve_pbandai@sha256:5567050109ec704e66dcf6df1b6f37a4dcb7be50653684317160cec973d69b8d"
}

resource "aws_cloudwatch_log_group" "get_new_reserve_pbandai" {
  name              = "/aws/lambda/get_new_reserve_pbandai"
  retention_in_days = 7
}

resource "aws_cloudwatch_event_rule" "get_new_reserve_pbandai" {
  name                = "get_new_reserve_pbandai"
  description         = "Run Lambda get_new_reserve_pbandai"
  schedule_expression = "cron(0/10 0-3 * * ? *)" #daily09:00-12:00JSTに10分おきに実行
}

resource "aws_cloudwatch_event_target" "get_new_reserve_pbandai" {
  rule = aws_cloudwatch_event_rule.get_new_reserve_pbandai.name
  arn  = aws_lambda_function.get_new_reserve_pbandai.arn
}

resource "aws_lambda_permission" "allow_cloudwatch_to_run_lambda" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.get_new_reserve_pbandai.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.get_new_reserve_pbandai.arn
}

resource "aws_s3_bucket" "get_new_reserve_pbandai" {
  bucket = "get-new-reserve-pbandai"
}