【完全自動】Apple StoreのiPhone在庫を10分ごとに監視して通知する方法【 Python ×Playwright】

iPhone

※本記事は転売を推奨するものではありません。
 昨今品薄のiPhoneをお店でピックアップ購入したい方向けの内容になります。

Apple Storeで人気モデルのiPhoneを購入したいと思っても、在庫がなかなか復活しないことがあります。

特に人気のカラーや容量は、気づいた時にはすでに在庫切れになっていることも珍しくありません。

そんなときに便利なのが、

Apple Storeの商品ページを定期的に監視し、在庫状況を自動でチェックする仕組み

です。

この記事では、ローカルPC上で動作する Python プログラムを使って、

  • Apple StoreのiPhone在庫状況を自動確認
  • 10分ごとにチェック
  • 在庫が復活したら通知

という仕組みを作る方法を、実際のコード付きで詳しく解説します。

しかも今回は、GitHub Actions やクラウド環境ではなく、

自宅PC(ローカルPC)で安定して動作させる方法

に絞って紹介します。

Apple Storeの在庫確認は、通常のHTTPリクエストでは取得できないケースがあり、少しコツが必要です。

その点も含めて、実際にハマりやすいポイントや、動く構成までまとめました。


なぜ Apple Store の在庫を自動監視したいのか

Apple Store では、店舗受け取り在庫や配送予定日がリアルタイムで変動します。

特定の機種・容量・カラーが欲しい場合、次のような悩みが出てきます。

  • 何度もページを手動で開くのが面倒
  • 仕事中や就寝中に在庫復活しても気づけない
  • 人気モデルは復活してもすぐ売れてしまう
  • 店舗受け取り在庫を即座に知りたい

特に、新モデル発売直後や品薄モデルでは、

「いつ在庫が戻るのか分からない」

という状態になりがちです。

そのため、

定期的にページを監視し、在庫が出たら通知する仕組み

を自分で作る価値があります。


今回作るシステムの全体像

今回の記事で作る仕組みは次の通りです。

※本記事は転売を推奨するものではありません。品薄だけどiPhoneがほしい方に向けた記事になります。

今回はいつもと指向を変えて、iPhoneの在庫があった時に自動で判定をしてくれるツール作成方法をお伝えします。

ローカルPC

Pythonスクリプト

PlaywrightでApple商品ページを開く

ブラウザ文脈で在庫APIにアクセス

在庫状況を解析

在庫があれば通知

通知先は Discord を想定していますが、仕組み自体はメールやLINE Notify系にも応用可能です。

今回のポイントは、

通常の requests や curl ではなく、Playwright を使って「ブラウザ経由」で取得する

ことです。


なぜ requests ではなく Playwright を使うのか

最初に多くの人が考えるのは、Python の requests ライブラリで Apple の在庫APIを直接叩く方法です。

しかし、Apple Store の在庫情報は、単純な HTTP リクエストではうまく取得できないことがあります。

実際に試すと、

  • ブラウザで URL を開くと JSON が返る
  • curl だと 541 エラー
  • requests でも 541 エラー
  • HTML の Page Not Found が返る

という挙動になることがあります。

つまり、

URLが正しくても、ブラウザ以外のクライアントはブロックされることがある

ということです。

これは Apple 側が、単に URL だけでなく、

  • 通信の指紋
  • ブラウザ文脈
  • 自動化判定
  • ヘッダや Cookie

などを見て判定しているためと考えられます。

そのため、ローカルPC上で安定して動かすには、

本物のブラウザを起動して、その中からAPIを呼ぶ

という方法が有効です。

これを実現できるのが Playwright です。


この記事で使う環境

今回の想定環境は次の通りです。

  • macOS または Windows
  • Python 3.10 以上推奨
  • Playwright
  • requests

筆者の検証では、ローカルの通常ブラウザ環境からは Apple の在庫APIが正常に返る一方で、クラウド環境や単純なHTTPクライアントではうまく取得できないケースがありました。

そのため、今回の実装方針は

ローカルPC × Playwright

です。


必要なもの

まずは必要なライブラリをインストールします。

Pythonライブラリのインストール

pip install playwright requests

次に、Playwright 用の Chromium をインストールします。

python -m playwright install chromium

これでブラウザ実行環境の準備は完了です。


監視対象のモデル情報を調べる

Apple Store の在庫監視では、単に商品ページURLだけではなく、実際に使われている商品識別子を確認する必要があります。

例えば、iPhone 17 Pro Max 256GB シルバーの場合、Apple の内部パラメータとして

MFY84J/A

のような型番が使われます。

また、店舗も内部的には店舗コードで管理されています。

今回の例では、名古屋栄が

R005

でした。

このような情報は、Apple Store の商品ページを開き、ブラウザの開発者ツールの Network タブで確認できます。


実際に使うURL

今回の在庫確認では、以下のような URL を使用します。

https://www.apple.com/jp/shop/fulfillment-messages?fae=true&pl=true&mts.0=regular&mts.1=compact&cppart=UNLOCKED_JP&parts.0=MFY84J/A&searchNearby=true&store=R005

ただし、これを requests で直接呼んでもうまくいかない場合があります。

そのため、今回のコードでは ブラウザの中から fetch() で呼ぶ 方法を採用します。


Apple Store の在庫監視フルコード

以下が、ローカルPCで動かす apple_stock_check.py になります。
型番や店を変更する際は先ほど説明したパラメータを変更してください

from playwright.sync_api import sync_playwright
import json
import os
from datetime import datetime, timedelta
import requests

# =========================
# 設定
# =========================
MODEL = "MFY84J/A"   # 例: iPhone 17 Pro Max 256GB シルバー
STORE = "R005"       # 名古屋栄
STORE_NAME = "名古屋栄"
WEBHOOK = os.getenv("DISCORD_WEBHOOK")

PRODUCT_URL = "https://www.apple.com/jp/shop/buy-iphone/iphone-17-pro"
API_URL = (
    "https://www.apple.com/jp/shop/fulfillment-messages"
    "?fae=true"
    "&pl=true"
    "&mts.0=regular"
    "&mts.1=compact"
    "&cppart=UNLOCKED_JP"
    f"&parts.0={MODEL}"
    "&searchNearby=true"
    f"&store={STORE}"
)

def now_jst_str() -> str:
    now_jst = datetime.utcnow() + timedelta(hours=9)
    return now_jst.strftime("%Y-%m-%d %H:%M:%S")

def send_discord(msg: str) -> None:
    if not WEBHOOK:
        print("DISCORD_WEBHOOK is not set")
        return

    try:
        r = requests.post(
            WEBHOOK,
            json={"content": msg[:1900]},
            timeout=10
        )
        r.raise_for_status()
        print("Discord notification sent.")
    except Exception as e:
        print(f"Discord send failed: {e}")

def check_stock() -> None:
    time_str = now_jst_str()
    print(f"[{time_str}] Start stock check")

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-blink-features=AutomationControlled",
            ],
        )

        context = browser.new_context(
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/122.0.0.0 Safari/537.36"
            ),
            locale="ja-JP",
            viewport={"width": 1280, "height": 800},
        )

        page = context.new_page()

        try:
            print(f"[{time_str}] Opening product page...")
            page.goto(PRODUCT_URL, wait_until="networkidle", timeout=60000)
            page.wait_for_timeout(3000)

            print(f"[{time_str}] Calling API from browser context...")
            result = page.evaluate(
                """
                async (apiUrl) => {
                    const r = await fetch(apiUrl, {
                        method: "GET",
                        headers: {
                            "x-requested-with": "XMLHttpRequest"
                        },
                        credentials: "include"
                    });

                    const text = await r.text();

                    return {
                        status: r.status,
                        ok: r.ok,
                        contentType: r.headers.get("content-type"),
                        text: text
                    };
                }
                """,
                API_URL,
            )

            print(f"[{time_str}] API Status: {result['status']}")
            print(f"[{time_str}] Content-Type: {result['contentType']}")

            text = result["text"].lstrip()

            if result["status"] != 200:
                print("Non-200 response:")
                print(result["text"][:1000])
                return

            if not text.startswith("{"):
                print("API returned non-JSON response:")
                print(result["text"][:1000])
                return

            data = json.loads(result["text"])

            stores = (
                data.get("body", {})
                .get("content", {})
                .get("pickupMessage", {})
                .get("stores", [])
            )

            if not stores:
                print("No stores found in response.")
                return

            target_store = next(
                (s for s in stores if s.get("storeNumber") == STORE),
                None
            )

            if not target_store:
                print(f"Target store not found: {STORE}")
                print("Available stores:", [s.get("storeNumber") for s in stores])
                return

            part_info = target_store.get("partsAvailability", {}).get(MODEL, {})
            status = part_info.get("pickupDisplay", "unknown")
            quote = part_info.get("pickupSearchQuote", "")
            store_name = target_store.get("storeName", STORE_NAME)

            print(f"Result: {store_name} - {status} - {quote}")

            if status != "unavailable":
                message = (
                    f"🚨 **在庫あり!**\n"
                    f"店舗: {store_name}\n"
                    f"状態: {status}\n"
                    f"詳細: {quote}\n"
                    f"確認: {time_str}\n"
                    f"{PRODUCT_URL}"
                )
                send_discord(message)
            else:
                print("No stock for now.")

        except Exception as e:
            print(f"Unexpected error: {e}")
            raise
        finally:
            browser.close()
            print(f"[{time_str}] Browser closed")

if __name__ == "__main__":
    check_stock()

コードのポイント解説

このコードで重要なのは以下の部分です。

1. 商品ページを先に開く

page.goto(PRODUCT_URL, wait_until="networkidle", timeout=60000)
page.wait_for_timeout(3000)

いきなり在庫APIを叩くのではなく、まず Apple の商品ページを開いています。

これにより、ブラウザ内の文脈や Cookie が整った状態になります。

2. ブラウザ内で fetch を実行

result = page.evaluate(
    """
    async (apiUrl) => {
        const r = await fetch(apiUrl, {
            method: "GET",
            headers: {
                "x-requested-with": "XMLHttpRequest"
            },
            credentials: "include"
        });

Python側の requests ではなく、ページ内JavaScriptとして fetch() を実行しています。

これにより、通常ブラウザに近い形でAPIを取得できます。

3. 店舗コードで対象店舗を絞る

target_store = next(
    (s for s in stores if s.get("storeNumber") == STORE),
    None
)

Appleのレスポンスには複数店舗が含まれることがあります。

そのため、対象店舗だけを抽出しています。

4. Discord通知

在庫が復活した場合のみ、Discordへ通知を送ります。


実行方法

次のように実行します。

python apple_stock_check.py

環境変数に Discord Webhook を入れておくと、在庫があったときに通知されます。

Mac の場合の例です。

export DISCORD_WEBHOOK="https://discord.com/api/webhooks/xxxx/xxxx"
python apple_stock_check.py

実行結果の例

在庫がない場合は、次のような出力になります。

[2026-03-11 16:25:45] Start stock check
[2026-03-11 16:25:45] Opening product page...
[2026-03-11 16:25:45] Calling API from browser context...
[2026-03-11 16:25:45] API Status: 200
[2026-03-11 16:25:45] Content-Type: application/json
Result: 名古屋栄 - unavailable - 現在ご利用いただけません
No stock for now.
[2026-03-11 16:25:45] Browser closed

在庫がある場合は Discord 通知が飛ぶようにしています。

10分ごとに自動実行する方法

ローカルPCで10分ごとに実行したい場合、Mac や Linux では cron が便利です。

cron 設定

ターミナルで次を実行します。

crontab -e

そして次の1行を追加します。

*/10 * * * * cd /Users/あなたのユーザー名/Downloads && /usr/bin/python3 apple_stock_check.py >> apple_stock_check.log 2>&1

これで 10分ごとに自動実行されます。

補足

Playwright を使う場合、環境によっては Python のパスや Playwright のインストール先に注意が必要です。

うまく動かない場合は、絶対パスで指定すると安定します。

GitHub Actions ではなくローカルPCをおすすめする理由

今回の検証では、GitHub Actions や Cloud Run、通常の requests / curl ベースでは、Apple Store の在庫API取得に失敗するケースがありました。

一方で、

  • ローカルPCの通常ブラウザでは JSON が取得できる
  • ローカルPC上の Playwright では比較的成功しやすい

という結果でした。

そのため、Apple Store の在庫監視を安定させたいなら、

ローカルPCまたは Raspberry Pi で実行する構成

が現実的です。


Raspberry Piでも応用可能

この仕組みは Raspberry Pi にも移植可能です。

ただし Playwright / Chromium を動かすため、多少セットアップは必要です。

メリットは、PCを常時つけっぱなしにしなくてもよいことです。

もし常時監視したい場合は、

  • Raspberry Pi
  • cron
  • このスクリプト

という構成にすると、低消費電力で24時間監視できます。


注意点

Apple Store 側の仕様は変更される可能性があります。

そのため、将来的に

  • 商品識別子(MODEL)
  • 店舗コード
  • APIパラメータ
  • レスポンス形式

が変わることがあります。

また、アクセス頻度を短くしすぎると望ましくないため、実運用では

10分〜30分程度の間隔

がおすすめです。


まとめ

今回は、Apple Store の iPhone 在庫をローカルPCで10分ごとに監視する方法を紹介しました。

ポイントをまとめると次の通りです。

  • Apple Store の在庫APIは単純な requests では取れないことがある
  • Playwright を使ってブラウザ文脈で fetch するのが有効
  • ローカルPC実行の方がクラウドより安定しやすい
  • cron を使えば10分ごとの自動監視ができる
  • Discord通知を組み合わせれば実用性が高い

「人気のiPhoneモデルをできるだけ早く確保したい」

「手動更新が面倒なので自動化したい」

という人にとって、かなり実用的な仕組みになるはずです。

shota_py

メーカー勤務のエンジニアです。 自分の趣味である、「電気回路」、「ガジェット」「株式投資」、「Python」に関する記事をつらつらと書いています

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA