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)で順に当ててください。
探す文字列 | 何がまずいか | 直す方向 |
|---|---|---|
| 2025年3月31日に終了したLINE Notifyの宛先。通知は届かない | Messaging APIの |
| APIキー・トークンがファイルに残る。共有やGitへの登録で漏れる | 環境変数から読む。未設定なら起動時に止める |
| 相手の応答が無いと固まり続ける。複数ページを間隔なしで連続取得する |
|
| 失敗をすべて黙って握りつぶす。通知が届かない理由が一切わからない | 失敗を記録して通知し、終了コードを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()
ブロックごとの説明
- 冒頭の定数:
BASE_DIRはスクリプト自身の場所です。cronから動かすと「今いるフォルダ」がホームディレクトリなどになるため、相対パスのままだと保存ファイルが別の場所にできてしまいます。それを防ぐために、ここを基準にしています。監視する商品・セレクタ・間隔は、ここだけ書き換えれば済むようにまとめました。 load_config():トークンを環境変数から読みます。設定し忘れたまま動かすと、送信の段階で分かりにくい失敗になります。そこで、何が足りないかを最初に表示して止めます。push_line():LINE Notifyのdata={"message": ...}とは形が違います。Messaging APIは宛先toとメッセージの配列messagesをJSONで送ります。raise_for_status()で、送信が失敗したら例外として表に出します。fetch_price():timeoutを付けて取得し、価格の要素が無ければ「構造が変わった」という失敗として扱います。AIのコードではここがexcept: passに吸い込まれ、サイト改修後も黙って動き続けていました。数字以外を落とす処理は「1,980円」「¥1,980(税込)」のような表記揺れへの備えです。load_state()/save_state():前回の価格をJSONで持ちます。固定しきい値ではなく「前回より下がったか」で判定するので、同じ値段のまま毎日通知が来ることはありません。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しか見ない人 | 通知をたくさん受けたい人・複数人で共有したい人 |
取得先サイトへの配慮: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回の生存通知で「来ない」に気づける形にしておきましょう。