【個人開発】🦇コウモリの番組お知らせBOT - スクレイピングなしでTV番組を自動通知!
こんにちは、未経験からエンジニアを目指して独学中です。
動物シリーズでLINE BOTを作成しており、今回は🦇コウモリの番組お知らせをリリースしました!
開発のきっかけ
普段テレビはあまり見ないけど、
「金曜ロードショーでジブリやるらしいけど、いつだっけ?」
「好きな映画がテレビでやるの、いつも見逃しちゃう…」
と思い、作ってみようと思いました。
番組表サイトを毎日チェックするのは面倒だし、有料の番組表APIは個人開発にはハードルが高い。スクレイピングは規約的にグレーだし、サイト構造が変わると動かなくなる…。
そこで思いついたのが、「Geminiの検索機能(Grounding)を使えば、番組表を自動で調べてくれるのでは?」 というアイデアでした。
夜行性でテレビっ子なキャラクターということで、「コウモリのチロちゃん」 が誕生しました🦇
🦇 作ったもの:「TV Bat」
キーワードを登録しておくと、放送予定が見つかったときだけLINEで通知してくれるボットです。

1. TV番組検索
「ジブリやる?」「来週の金曜ロードショーは?」と話しかけると、その場でGoogle検索して教えてくれます。
2. 見たい番組リスト管理
Firestoreを使って、監視したい番組を自由に登録できます。
- 「追加: ポケモン」 → 監視リストに追加
- 「削除: ポケモン」 → 監視リストから削除
- 「リスト」 → 現在登録しているキーワードを確認
3. 自動通知
Cloud Schedulerで毎日チェックを実行し、直近1週間以内に放送予定が見つかった場合のみLINEで通知します。
📱 主な機能
Gemini Grounding(Google検索連携)
GeminiのGrounding機能を使って、リアルタイムでGoogle検索を実行します。番組表APIやスクレイピングなしで、最新の放送情報を取得できます。
Firestore(監視リスト管理)
ユーザーごとの監視キーワードをFirestoreに保存。追加・削除・一覧表示が簡単にできます。
Cloud Scheduler(定期実行)
毎日決まった時間に自動チェックを実行。放送予定が見つかったときだけ通知するので、うるさくありません。
🛠 使用技術
| 項目 | 技術 |
|---|---|
| 言語 | Python 3.10+ |
| フレームワーク | FastAPI |
| AIモデル | Gemini 2.5 Flash(Vertex AI) |
| 検索機能 | Grounding with Google Search |
| データベース | Firestore |
| 定期実行 | Cloud Scheduler |
| インフラ | Google Cloud Run |
| API | LINE Messaging API |
🏗 アーキテクチャ
💡 技術的なこだわりポイント
1. Grounding(Google検索連携)の活用
今回の肝は、GeminiのGrounding機能です。これを使うと、GeminiがリアルタイムでGoogle検索を実行して回答してくれます。
from google.genai import types
# Grounding設定
tools = [types.Tool(google_search=types.GoogleSearch())]
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"来週のテレビで「{keyword}」の放送予定はありますか?地上波優先で教えてください。",
config=types.GenerateContentConfig(
tools=tools,
temperature=0.1 # 低めに設定して安定させる
)
)
これにより、番組表APIの契約もスクレイピングも不要になりました。
2. ハルシネーション対策のプロンプト設計
LLMは「それっぽい嘘」をつくことがあります。存在しない放送予定を通知してしまうと大問題なので、プロンプトを工夫しました。
prompt = f"""
あなたはTV番組検索アシスタントです。
【質問】
来週のテレビで「{keyword}」の放送予定はありますか?
【回答ルール】
* 検索結果に明確な放送予定がある場合のみ、日時とチャンネルを回答してください
* 放送予定が見つからない場合は「見つかりませんでした」とだけ回答してください
* 推測や曖昧な情報は絶対に含めないでください
* 地上波(日テレ、テレ朝、TBS、テレ東、フジ、NHK)を優先してください
"""
「ある」と断定できた場合のみ回答させることで、誤通知を防いでいます。
3. Firestoreでユーザーごとのリスト管理
監視キーワードはFirestoreに保存しています。
from google.cloud import firestore
db = firestore.Client()
# キーワード追加
def add_keyword(user_id: str, keyword: str):
doc_ref = db.collection("bat_watchlist").document(user_id)
doc = doc_ref.get()
if doc.exists:
keywords = doc.to_dict().get("keywords", [])
if keyword not in keywords:
keywords.append(keyword)
doc_ref.update({"keywords": keywords})
else:
doc_ref.set({"keywords": [keyword]})
# キーワード一覧取得
def get_keywords(user_id: str) -> list:
doc = db.collection("bat_watchlist").document(user_id).get()
if doc.exists:
return doc.to_dict().get("keywords", [])
return []
4. Cloud Schedulerで定期実行
毎日朝8時にチェックを実行する設定です。
# Cloud Scheduler設定
URL: https://YOUR-CLOUD-RUN-URL/cron/bat_check
Method: GET
Frequency: 0 8 * * *
Timezone: Asia/Tokyo
FastAPI側のエンドポイント:
@app.get("/cron/bat_check")
async def cron_check():
# 全ユーザーの監視リストを取得
users = db.collection("bat_watchlist").stream()
for user in users:
user_id = user.id
keywords = user.to_dict().get("keywords", [])
for keyword in keywords:
# Geminiで検索
result = search_tv_schedule(keyword)
if result.found:
# 放送予定が見つかったら通知
send_line_message(user_id, result.message)
return {"status": "ok"}
5. キャラクター設定
コウモリの「チロちゃん」というキャラクターで応答します。
system_prompt = """
あなたは「チロちゃん」というテレビコウモリです。
【性格】
* 夜行性でテレビっ子
* 少し毒舌だけど親切
* 語尾は「〜モリ」「〜キキ」
【例】
「その番組、来週やるモリ!見逃さないようにキキ!」
「残念だけど、見つからなかったモリ…」
"""
😿 ハマったポイント
Grounding の実装が大変だった
Vertex AIのGrounding機能は、ライブラリのバージョンによって書き方が全然違うので苦労しました。公式ドキュメントも頻繁に更新されるので、最新の書き方を追うのが大変でした。
検索結果の精度問題
「ジブリ」で検索すると、過去の放送情報や映画館の上映情報も混ざってきます。プロンプトで「地上波優先」「来週以内」と指定することで、ある程度絞り込めるようになりました。
通知のタイミング
最初は「見つかったら即通知」にしていましたが、同じ番組を何度も通知してしまう問題が発生。通知済みフラグを管理するか、通知頻度を1日1回に制限するかで悩みました。
🌟 このボットで学んだこと
LLMの検索機能の可能性
「APIがない」「スクレイピングできない」という状況でも、LLMの検索機能で代替できることがわかりました。精度は100%ではありませんが、個人開発には十分使えます。
ハルシネーション対策の重要性
通知系のボットでは、誤った情報を送らないことが最重要です。プロンプト設計で「確実な情報のみ回答させる」工夫が必要でした。
定期実行の仕組み
Cloud Schedulerを使った定期実行の仕組みを学べました。他のボットにも応用できそうです。
さいごに
スクレイピングや有料APIなしで番組通知ができるのは、LLMならではの強みだと思います。
「好きな番組を見逃したくない」という方は、ぜひ試してみてください🦇
ここまで読んでいただきありがとうございました🦇❤️
Discussion