ChatGPTやClaude Codeに「値下がりしたらLINEに通知する価格監視スクリプト」を頼むと、今でもLINE Notify宛ての requests.post が出てくることがあります。LINE Notifyは2025年3月31日に終了しているので、このコードはエラーも出さずに通知だけが届かない状態になりがちです。この記事では、AIにどう指示すればLINE Messaging APIのpushメッセージかDiscord Webhookへ書き直させられるかを、指示文の実物つきで説明します。そのうえで、毎日の値下がり通知を止めずに動かす場所までまとめます。

AIが今も書く「LINE Notify+requests.post」の見分け方(2025年3月31日終了)

生成AIは学習時点のコードを手本にして書きます。LINE Notifyは「トークン1つで自分のLINEに通知できる」手軽さから、価格監視やスクレイピングの通知先として長く使われてきました。そのためAIが手本にした例の中では、今も定番の書き方として残っています。

AIが出しがちなコード(そのまま使ってはいけない例)

「Amazonっぽい商品ページの価格を毎日見て、安くなったらLINEに通知して」と頼んだとき、典型的に出てくるのは次のような形です。

import requests
from bs4 import BeautifulSoup

TOKEN = "ここにトークンを貼る"
URLS = ["https://example.com/items/1", "https://example.com/items/2"]

for url in URLS:
    try:
        html = requests.get(url).text
        soup = BeautifulSoup(html, "html.parser")
        price = int(soup.find("span", class_="price").text.replace(",", "").replace("円", ""))
        if price < 10000:
            requests.post(
                "https://notify-api.line.me/api/notify",
                headers={"Authorization": f"Bearer {TOKEN}"},
                data={"message": f"値下がり: {price}円"},
            )
    except:
        pass

短くて読みやすく、一見すると問題なさそうに見えます。ですが、毎日動かす前提で見ると危ない箇所が4つあります。

読者が自分で確認できる4つのチェック箇所

コードを読み慣れていなくても、次の文字列を探せば見つけられます。エディタの検索(Ctrl+F/Cmd+F)で順に当ててください。

探す文字列

何がまずいか

直す方向

notify-api.line.me

2025年3月31日に終了したLINE Notifyの宛先。通知は届かない

Messaging APIの https://api.line.me/v2/bot/message/push か Discord Webhook に変える

TOKEN = " のような直書き

APIキー・トークンがファイルに残る。共有やGitへの登録で漏れる

環境変数から読む。未設定なら起動時に止める

requests.get(url) に timeout が無い/time.sleep が無い

相手の応答が無いと固まり続ける。複数ページを間隔なしで連続取得する

timeout= を付け、1件ごとに数秒あける

except: の直後に pass

失敗をすべて黙って握りつぶす。通知が届かない理由が一切わからない

失敗を記録して通知し、終了コードを0以外にする

特に厄介なのが1つ目と4つ目の組み合わせです。終了したサービスへの送信が失敗しても、requests.post の結果を誰も見ていません。さらに except: pass があるので、価格の取得に失敗しても何も起きません。「毎朝動いているのに、通知が来ないのは値下がりしていないからだ」と思い込んだまま何週間も過ぎる、というのがこのコードの一番の怖さです。

IPAの「情報セキュリティ10大脅威2026」では「AIの利用をめぐるサイバーリスク」が3位に初めて入りました。また、AI生成コードには人が書いたものより多くの問題が含まれるというレポートも出ています。AIの出力は「動いたから正しい」ではなく、上の4か所を自分の目で確かめてから使う、という前提で扱うのが安全です。

最初の指示と、直させる指示文の実物(ChatGPT・Claude Code共通)

上のようなコードが出てくる原因の多くは、最初の指示に「通知先」と「動かし方」の条件が無いことです。AIは条件が無いと、自分が一番よく見た書き方を選びます。

最初の指示(よくある書き方)

商品ページの価格を毎日チェックして、値下がりしたらLINEに通知する
Pythonスクリプトを書いてください。

これだけだと「LINEに通知」はLINE Notifyと解釈されやすく、エラー処理やアクセス間隔も省かれます。

直させる指示(そのままコピーして使える形)

出てきたコードに対して、次のように終了の事実と置き換え先を具体的に書いて直させます。AIは「LINE Notifyが終わった」ことを知らない場合があるので、日付まで書いて伝えるのがコツです。

このコードを以下の条件で書き直してください。

1. LINE Notify は2025年3月31日に終了しています。通知は LINE Messaging API の
   pushメッセージ(POST https://api.line.me/v2/bot/message/push)に置き換えてください。
   SDKは使わず requests で直接呼び出してください。
2. チャネルアクセストークンと宛先のユーザーIDはコードに書かず、
   環境変数 LINE_CHANNEL_ACCESS_TOKEN と LINE_USER_ID から読んでください。
   未設定なら、何が足りないかを表示して起動時に終了してください。
3. 通知の送信結果は raise_for_status() で確認し、失敗を握りつぶさないでください。
4. 「10000円未満」の固定しきい値ではなく、前回取得した価格より下がったときだけ
   通知してください。前回の価格はスクリプトと同じフォルダのJSONファイルに保存し、
   パスは pathlib でスクリプトの場所を基準に組み立ててください(cronから実行するため)。
5. requests.get には timeout を付け、商品ページを1件取得するごとに5秒あけてください。
6. bare except と pass は使わないでください。取得に失敗した商品は一覧にまとめて
   1通だけ通知し、そのうえで終了コード1で終わってください。
7. 価格の要素が見つからない場合(サイトのHTMLが変わった場合)も失敗として扱ってください。
8. 各関数に、何をしているかを日本語のコメントで1〜2行付けてください。

ポイントは「何を使うか」だけでなく「何をしてはいけないか」(bare except、直書き、固定しきい値)を名指しすることです。禁止を書かないと、AIは直したつもりで同じ癖を残すことがあります。直ったコードが返ってきたら、もう一度前の表の4か所を検索して、本当に消えているかを確認してください。

同じ「古い書き方を名指しで直させる」やり方は、pandasの仕様変更でも効きます。pandas date_format移行とAI指示|26年10月でも、指示文の書き方を具体例つきで紹介しています。

直った後のコード:Messaging APIのpush_messageとDiscord Webhook

実行環境と準備

  • Python:3.9以降を想定しています。手元のバージョンは python3 -V で確認してください。
  • パッケージ:pip install requests beautifulsoup4。特別なバージョン指定は不要です。入った版は pip show requests beautifulsoup4 で確認できます。
  • LINE側の準備:LINE Developersコンソールで Messaging API のチャネルを用意し、チャネルアクセストークン(長期)を発行します。自分宛てに送るときの宛先は、チャネルの基本設定にある「あなたのユーザーID」です。作った公式アカウントを自分のLINEで友だち追加しておかないと届きません。

LINE Notifyとの大きな違いは、Messaging APIのpushメッセージが公式アカウントの月間メッセージ数に数えられることです。無料で送れる通数や料金は、LINE公式アカウントの料金プランのページで最新の値を確認してください。だからこそ、下のコードは「値下がりしたときだけ」「複数件は1通にまとめて」送る作りにしています。

完成したコード(price_watch.py)

import json
import os
import sys
import time
from pathlib import Path

import requests
from bs4 import BeautifulSoup

# スクリプトの場所を基準にする(cronはカレントディレクトリが違うため)
BASE_DIR = Path(__file__).resolve().parent
STATE_FILE = BASE_DIR / "last_prices.json"

TARGETS = [
    {"name": "商品A", "url": "https://example.com/items/1"},
    {"name": "商品B", "url": "https://example.com/items/2"},
]
PRICE_SELECTOR = "span.price"   # 価格が入っている要素(サイトに合わせて変える)
INTERVAL_SEC = 5                # 1件ごとにあける秒数
HEADERS = {"User-Agent": "price-watch/1.0 (personal use)"}
LINE_PUSH_URL = "https://api.line.me/v2/bot/message/push"


def load_config():
    # トークンと宛先は環境変数から読む。足りなければここで止める
    token = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN")
    user_id = os.environ.get("LINE_USER_ID")
    if not token or not user_id:
        sys.exit("環境変数 LINE_CHANNEL_ACCESS_TOKEN と LINE_USER_ID を設定してください")
    return token, user_id


def push_line(token, user_id, text):
    # Messaging API の push で自分に1通送る。失敗は例外にして気づけるようにする
    resp = requests.post(
        LINE_PUSH_URL,
        headers={"Authorization": f"Bearer {token}"},
        json={"to": user_id, "messages": [{"type": "text", "text": text}]},
        timeout=10,
    )
    resp.raise_for_status()


def fetch_price(url):
    # 商品ページを取得して価格を整数で返す。要素が無ければ構造変更とみなす
    resp = requests.get(url, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    tag = soup.select_one(PRICE_SELECTOR)
    if tag is None:
        raise ValueError("価格の要素が見つかりません(ページの構造が変わった可能性)")
    digits = "".join(ch for ch in tag.get_text() if ch.isdigit())
    if not digits:
        raise ValueError("価格を数値として読めません")
    return int(digits)


def load_state():
    # 前回の価格を読む。初回はファイルが無いので空で始める
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text(encoding="utf-8"))
    return {}


def save_state(state):
    # 途中で落ちても壊れないよう、一時ファイルに書いてから置き換える
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(STATE_FILE)


def main():
    token, user_id = load_config()
    state = load_state()
    drops, errors = [], []

    for i, target in enumerate(TARGETS):
        if i:
            time.sleep(INTERVAL_SEC)
        try:
            price = fetch_price(target["url"])
        except (requests.RequestException, ValueError) as e:
            errors.append(f"{target['name']}: {e}")
            continue

        prev = state.get(target["url"])
        if prev is not None and price < prev:
            drops.append(f"{target['name']} {prev:,}円 → {price:,}円\n{target['url']}")
        state[target["url"]] = price

    save_state(state)

    if drops:
        push_line(token, user_id, "値下がりしました\n\n" + "\n\n".join(drops))
    if errors:
        push_line(token, user_id, "価格の取得に失敗しました\n" + "\n".join(errors))
        sys.exit(1)


if __name__ == "__main__":
    main()

ブロックごとの説明

  1. 冒頭の定数:BASE_DIR はスクリプト自身の場所です。cronから動かすと「今いるフォルダ」がホームディレクトリなどになるため、相対パスのままだと保存ファイルが別の場所にできてしまいます。それを防ぐために、ここを基準にしています。監視する商品・セレクタ・間隔は、ここだけ書き換えれば済むようにまとめました。
  2. load_config():トークンを環境変数から読みます。設定し忘れたまま動かすと、送信の段階で分かりにくい失敗になります。そこで、何が足りないかを最初に表示して止めます。
  3. push_line():LINE Notifyの data={"message": ...} とは形が違います。Messaging APIは宛先 to とメッセージの配列 messages をJSONで送ります。raise_for_status() で、送信が失敗したら例外として表に出します。
  4. fetch_price():timeout を付けて取得し、価格の要素が無ければ「構造が変わった」という失敗として扱います。AIのコードではここが except: pass に吸い込まれ、サイト改修後も黙って動き続けていました。数字以外を落とす処理は「1,980円」「¥1,980(税込)」のような表記揺れへの備えです。
  5. load_state()/save_state():前回の価格をJSONで持ちます。固定しきい値ではなく「前回より下がったか」で判定するので、同じ値段のまま毎日通知が来ることはありません。
  6. main():1件ごとに5秒あけて取得し、値下がりと失敗をそれぞれ1通にまとめて送ります。失敗があれば終了コード1で終わるので、cronやsystemdのログにも「失敗した日」として残ります。

LINEではなくDiscord Webhookにする場合

公式アカウントを作るのが面倒なら、Discordのサーバーにチャンネルを作り、連携サービスからWebhook URLを発行する方法もあります。こちらは送信数の上限を気にせずに済むことが多く、仕組みも単純です。指示文の1〜2を「Discord Webhookに送る。URLは環境変数 DISCORD_WEBHOOK_URL から読む」に変えれば、AIは次のような関数を出します。

def push_discord(webhook_url, text):
    # Webhook URL に content を送るだけ。失敗は例外にする
    resp = requests.post(webhook_url, json={"content": text}, timeout=10)
    resp.raise_for_status()

Webhook URLは、それ自体がトークンと同じ扱いです。URLを知っている人は誰でもそのチャンネルに投稿できるので、コードに直書きしないのはLINEと同じです。また、1通に載せられる文字数には上限があります。監視する商品が多い場合は、Discordの公式ドキュメントで制限を確認して分割してください。

比較

LINE Messaging API(push)

Discord Webhook

準備

チャネル作成・トークン発行・友だち追加

チャンネルでWebhook URLを発行するだけ

送信数

公式アカウントの月間メッセージ数に数えられる(料金プランで確認)

短時間に連投するとレート制限に当たる

環境変数

LINE_CHANNEL_ACCESS_TOKEN・LINE_USER_ID

DISCORD_WEBHOOK_URL

向いている人

普段LINEしか見ない人

通知をたくさん受けたい人・複数人で共有したい人

取得先サイトへの配慮:robots.txt・利用規約・1日1回の間隔

スクレイピング自体は日本の法律で禁止されていません。ただし、サーバーへの過度な負荷、ログイン制限の突破、利用規約違反は、損害賠償などの責任を問われる可能性があります。価格監視は毎日動かすものなので、最初にここを確認しておきます。

  • robots.txt を見る:https://example.com/robots.txt のように、サイトの直下に置かれています。監視したいパスが Disallow に入っていれば対象から外します。
  • 利用規約を見る:「自動化された手段による取得を禁止」と書かれているサイトは対象にしません。公式のAPIやアフィリエイト向けの商品情報APIがあれば、そちらを使う方が安定します。APIはHTMLの改修で壊れることも少なくなります。
  • 間隔を空ける:価格は分単位で追う必要がありません。1日1〜2回、1件ごとに数秒あけるで十分です。コードの INTERVAL_SEC を縮めたり、並列で取得したりはしないでください。
  • 名乗る:User-Agent に用途が分かる文字列を入れておくと、相手側がアクセスを判別できます。ブラウザになりすます書き方はしません。
  • 止められたら止める:アクセスを拒否されたり、確認画面が出るようになったりしたら、それはサイト側の意思表示です。回避する方法を探すのではなく、監視対象から外してください。AIに「ブロックされないようにして」と頼むと回避策を出してくることがありますが、使わないでください。

毎日止めずに動かす場所:手元PC・レンタルサーバーのcron・VPSのsystemd timer

コードが直っても、動かす場所を間違えると「通知が来ない=値下がりしていない」の思い込みに戻ってしまいます。それぞれの場所には、止まる条件がはっきりあります。

場所

向いている条件

止まる条件

手元のPC(cron・タスクスケジューラ)

試運転、PCを毎日決まった時間に開いている人

スリープ・電源オフの間は実行されない。閉じたままの日はその日の分が丸ごと飛ぶ

共有レンタルサーバーのcron

このスクリプトのように requests と BeautifulSoup だけで済むもの

プランでcronやPythonが使えない、Pythonのバージョンが古い、実行時間やプロセス数の制限。ブラウザを起動するSelenium・Playwrightは動かせないことが多い

VPS(systemd timer)

常時稼働させたい、ブラウザ操作も含めて動かしたい

OSを更新して再起動したあとにタイマーが有効になっていない、ディスクの空き不足、自分で監視しない限り誰も気づかない

GitHub Actionsの定期実行

サーバーを持ちたくない人

指定時刻どおりに起動するとは限らない。リポジトリに動きが無いと定期実行が止まることがある

GitHub Actionsで動かす場合の書き方と注意点は、GitHub Actions cronスクレイピング|26年10月にまとめています。

cronで動かす場合(レンタルサーバー・VPS共通)

cronは、普段使っているシェルの設定ファイルを読みません。そのため環境変数を別ファイルに置き、実行のたびに読み込ませます。ファイルは自分だけが読めるように chmod 600 にしておきます。

# /home/username/price-watch/.env(chmod 600)
LINE_CHANNEL_ACCESS_TOKEN=発行したトークン
LINE_USER_ID=あなたのユーザーID
# crontab -e で追加(毎朝8:00)
0 8 * * * cd /home/username/price-watch && set -a && . ./.env && set +a && /home/username/price-watch/.venv/bin/python price_watch.py >> /home/username/price-watch/cron.log 2>&1

python は必ずフルパスで書きます。cronからは仮想環境(.venv)が見えないので、python3 とだけ書くと、requestsが入っていない別のPythonで動いて即座に失敗します。set -a は、読み込んだ値を環境変数として子プロセスに渡すための指定です。最後の >> ... 2>&1 で、エラーも含めてログに残します。

VPSならsystemd timerにする

VPSで動かすなら、cronよりsystemd timerの方が向いています。理由は2つです。1つは Persistent=true を付けると、止まっていた間に逃した回を起動時に実行してくれること。もう1つは、失敗した回を journalctl で後から追えることです。

# ~/.config/systemd/user/price-watch.service
[Unit]
Description=price watch

[Service]
Type=oneshot
WorkingDirectory=/home/username/price-watch
EnvironmentFile=/home/username/price-watch/.env
ExecStart=/home/username/price-watch/.venv/bin/python price_watch.py
# ~/.config/systemd/user/price-watch.timer
[Unit]
Description=price watch daily

[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now price-watch.timer
loginctl enable-linger username      # ログアウトしても動かし続ける
systemctl --user list-timers         # 次回の実行時刻が出ていれば登録できている
journalctl --user -u price-watch.service   # 実行結果を見る

enable-linger を忘れると、SSHから抜けた時点でユーザー単位のタイマーが止まります。また、サーバーを再起動したあとは list-timers にタイマーが残っているかを一度確認してください。登録が外れていても、エラーは何も出ません。

「通知が来ない」を異常として気づける形にする

どこで動かす場合でも、最後に残る弱点は「スクリプト自体が起動しなかった日」です。この日は失敗通知も届きません。値下がりは毎日起きるものではないので、通知が無い日が続いても普通に見えてしまいます。対策として、たとえば週に1回だけ「今週も○件を監視しました」と送る行をAIに追加させるとよいでしょう。そうすれば、その通知が止まったことで異常に気づけます。追加するときも、指示文に「送るのは週1回だけ。曜日は月曜」と回数を明記してください。書かないと、AIが毎回送る形にすることがあります。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

まとめ

ChatGPTやClaude Codeが書く価格監視スクリプトには、今も2025年3月31日に終了したLINE Notifyの notify-api.line.me 宛ての requests.post が混ざることがあります。except: pass と組み合わさると、エラーも出さずに通知だけが消えます。直すときは、AIに終了の日付と置き換え先を伝えます。置き換え先は、Messaging APIの /v2/bot/message/push かDiscord Webhookです。あわせて「直書き禁止・timeoutと間隔・握りつぶし禁止・構造変更を失敗として扱う」を名指しで指示してください。返ってきたコードは、4つの文字列を検索して自分の目で確かめます。取得先のrobots.txtと利用規約を確認し、1日1〜2回に抑えます。動かす場所は、試運転なら手元のPC、軽い取得なら共有サーバーのcron、常時稼働ならVPSのsystemd timerが目安です。それぞれ止まる条件を知ったうえで、週1回の生存通知で「来ない」に気づける形にしておきましょう。