Apple認定整備済製品の販売状況をLINE経由で通知するシステムを作ってみた

<h1>はじめに</h1>
明けましておめでとうございます。<br>
Appleの正月セールが終わりましたね。<br>

今後定価未満でApple製品を買うとなれば、
認定整備済製品を狙う方が一定数いらっしゃるかと思います。
[Appleの認定整備済製品](https://www.apple.com/jp/shop/refurbished?afid=p238%7Csk80EB8IN-dc_mtid_18707vxu38484_pcrid_642391970787_pgrid_13140807261_pntwk_g_pchan__pexid__ptid_kwd-323661845015_&cid=aos-jp-kwgo-brand--slid---product-)

私が狙っているのはiPad Airなので、
下記のWebページからWebスクレイピングを行います。
[iPadの認定整備済製品](https://www.apple.com/jp/shop/refurbished/ipad)

<h1>紹介する内容</h1>
iPadの認定整備済製品をWebスクレイピング<br>
↓<br>
LINE MessagingAPIを使って、取得した情報を自身のLINEアカウントに送信<br>
↓<br>
cronを使って定期実行（Unix系OS限定）<br>

<h1>開発環境や使用ライブラリ</h1>

- PC: Macbook Air M2
- OS: macOS Sequoia
- 言語: Python 3.9.6
- ライブラリ：
    - beautifulsoup4 4.12.3
    - requests 2.32.3

<h1>実装までの過程</h1>

最初は、下記リンクを参考にして実装しようとしました。
[Python+SeleniumでスクレイピングしてApple整備済製品をカートに入れ、ラインで通知する](https://qiita.com/kobayashi-masayuki/items/dd7051f4aabbff28a149)

ところが、上記サイトで使われているLINE Notifyは2025年3月31日をもって終了するとのことでした。
[LINE Developersからのお知らせ](https://developers.line.biz/ja/news/2024/10/07/line-notify-will-be-discontinued/)

今後は、LINE Messaging APIの利用を推奨するとのことでした。
LINE Messaging APIを利用している記事を探したところ、下記リンクを見つけました。
[Python+Lambdaで天気予報をLINEから通知するアプリを簡単に作ってみた](https://www.heiyoshilog-business.com/python-lambda-tenki/)

リンク先のソースコードを参考に、Webスクレイピングをしてみました。
LINE_ACCESS_TOKENは以下のリンクを参考に取得して下さい。
[LINEチャネルアクセストークンの取得・確認方法※2023年最新](https://programming-zero.net/line-api-channel-access-token/)

```WebScraiping_Apple.py
#未インストールの場合は、pipを使うなどして各自インストールして下さい。
import requests
from bs4 import BeautifulSoup
import json
import datetime

#LINEのチャネルアクセストークン
LINE_ACCESS_TOKEN="各自で取得して下さい"

# 定数
ENDPOINT = "https://api.line.me/v2/bot/message/broadcast"
APPLE_REFURBISHED_URL = "https://www.apple.com/jp/shop/refurbished/ipad"
TARGET_PRODUCT = "iPad Air"  # 検索対象のモデルを指定 (部分一致でOK)

def check_ipad_air():
    """Apple認定整備済ページをチェックし、iPad Airの掲載状況を返す"""
    try:
        response = requests.get(APPLE_REFURBISHED_URL)
        response.raise_for_status()
        soup = BeautifulSoup(response.content, "html.parser")

        script_tags = soup.find_all("script", type="application/ld+json")
        found_products = [] # 複数のiPad Air情報を格納するリスト

        for script_tag in script_tags:
            try:
                json_data = json.loads(script_tag.string)
                if json_data.get("@type") == "Product":
                    product_name = json_data.get("name")
                    product_url = json_data.get("url")
                    if product_name and product_url and TARGET_PRODUCT in product_name:
                        found_products.append({"name": product_name, "url": product_url}) # リストに追加

            except json.JSONDecodeError as e:
                print(f"JSONデコードエラー: {e}")
                continue

        return found_products # リストを返す

    except requests.exceptions.RequestException as e:
        print(f"Webサイトへのアクセスエラー: {e}")
        return None
    except AttributeError as e:
        print(f"HTML解析エラー: {e}")
        return None

def send_message(message):
    """LINEにメッセージを送信する"""
    headers = {
        "Authorization": f"Bearer {LINE_ACCESS_TOKEN}",
        "Content-Type": "application/json"
    }
    data = {"messages": [{"type": "text", "text": message}]}
    try:
        response = requests.post(ENDPOINT, headers=headers, json=data)
        response.raise_for_status()  # LINE APIのエラーもチェック
        print("LINEメッセージ送信成功")
        return True
    except requests.exceptions.RequestException as e:
        print(f"LINEメッセージ送信エラー: {e}")
        return False
    except json.JSONDecodeError as e:
        print(f"JSONデコードエラー: {e}")
        return False
def main():
    found_products = check_ipad_air()

    if found_products:
        now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        message = f"{now} 以下のiPad Airが掲載されています！\n"
        for product in found_products: # 複数の製品情報をメッセージに連結
            message += f"- {product['name']}\n{product['url']}\n"
        if send_message(message):
            print("通知完了")
        else:
            print("LINEへの通知に失敗しました。")
    else:
        now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        print(f"{now} {TARGET_PRODUCT} は掲載されていません。")

if __name__ == "__main__":
    main()
```
結果、このように取得できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325458/91c88be4-0c0e-00ea-56a2-b923cee8cb11.png)

実行してから数秒でLINEに通知が来ます。
ちなみに、LINE Messaging APIを無課金で使う場合、1ヶ月あたりのメッセージ送信上限は200件となっています（2025/1/7時点）。
メッセージ送信先に参加しているユーザ数の分カウントされるようなので、200名参加しているLINEグループに通知を送ると一発で使い切ることになります。
ご注意ください。

ここからは定期実行についてです。
上記のコードを毎回手動で実行するなら、都度認定整備済製品を確認したほうが早いため、定期実行を設定します。

AWS LambdaやGoogle Cloud Run functionsを使っても良いのですが、
意図しない課金の発生が怖い＆アカウント作成などが面倒だったため、cronを使って定期実行します。

参考になる記事を貼っておきます。
[ローカル環境でcronを使ってpythonを自動実行させる方法](https://note.com/shiki_3/n/ne1a1ba1e7d40)
上手く実行できない方は、「cron python」で検索してみて下さい。
参考になる記事が多数ヒットするかと思います。

私は毎日17:30に通知を受け取りたかったので、下記の通り設定しました。
標準出力やエラー出力は「output.log」に吐き出されます。
```
30 17 * * * /usr/bin/python3 /Users/（ユーザ名）/Desktop/SourceCode/WebScraiping_Apple.py >> /Users/（ユーザ名）/Desktop/SourceCode/output.log 2>&1
```


<h1>おわりに</h1>
私は正月セールでiPad Airを買ったため、<br>
ここで紹介した内容は全然活用できてません、、、<br>
誰かの参考になれば非常に嬉しいです！

今度Apple製品を買う機会が来たら、
新たな認定整備済製品が入荷された際に通知を送るよう改修しようと思います。

<h1>追記(2025/1/12)</h1>

スリープ中にも定期実行できるように改良しました。
[コチラ](https://qiita.com/sanmo3/items/b21942f58430310482e6)を参考にさせていただきました。
1日1回の定期実行で良いなら、macのシステム設定上でもスリープ解除できるそうです。
```
28 17 * * * pmset repeat wakeorpoweron MTWRFSU 17:29:00
30 17 * * * /usr/bin/python3 /Users/（ユーザ名）/Desktop/SourceCode/WebScraiping_Apple.py >> /UserS/（ユーザ名）/Desktop/SourceCode/hoge.log 2>&1
31 17 * * * pmset repeat sleep MTWRFSU 17:32:00
```