- Cloud Scheduler: 毎日指定時刻(8:00 / 16:00 JST)にバッチを定期起動(HTTP POST / OAuth認証)
- Cloud Run Jobs (
batch): バッチ処理本体を実行するコンテナ基盤- 記事収集 (
sources.py): 5つのWebメディアから某SaaS関連記事を取得(RSS / HTML解析) - 時間フィルタリング (
digest.py): 直近一定時間内(標準24時間)の新着記事のみを抽出 - AI要約生成 (
summarizer.py): Anthropic APIを叩き、記事ごとの詳細要約と、全体傾向の要約(4パターン)を生成 - メール組み立て (
digest.py): プレーンテキストのメール本文・件名を整形 - メール配信 (
mailer.py): Gmail API経由で指定のGmailアドレス宛に送信
- 記事収集 (
- Cloud Scheduler: 毎日指定時刻(8:00 / 16:00 JST)にバッチを定期起動(HTTP POST / OAuth認証)
- Cloud Run Jobs (
batch): バッチ処理本体を実行するコンテナ基盤- 記事収集 (
sources.py): 5つのWebメディアから某SaaS関連記事を取得(RSS / HTML解析) - 時間フィルタリング (
digest.py): 直近一定時間内(標準24時間)の新着記事のみを抽出 - AI要約生成 (
summarizer.py): Anthropic APIを叩き、記事ごとの詳細要約と、全体傾向の要約(4パターン)を生成 - メール組み立て (
digest.py): プレーンテキストのメール本文・件名を整形 - メール配信 (
mailer.py): Gmail API経由で指定のGmailアドレス宛に送信
- 記事収集 (
- HTML直接スキャンの対応 (TechWave Keywords)
キーワードページのようにRSSが提供されていないメディアについては、requests+BeautifulSoup4でリンクパターン(/(articles|news)/\d{4}/\d{2}/news\d+\.html$)を正規表現判定し、DOM構造を最大4階層遡って日付テキストを取得するヒューリスティックなスクレイピングを実装しました。 - マルチソースのキーワードフィルタ (Enterprise Cloud Insider)
「CRM」のような汎用カテゴリフィードの場合、他製品の記事も混入します。そのため、タイトルや本文概要に対して目的のサービス名を表すキーワード(正規表現によるパターンマッチ)でポストフィルタを適用しています。 - 障害耐性
いずれかのWebサイトが落ちていたりHTML構造が変わってエラーを出しても、1つのソースの失敗でバッチ全体が停止しないよう、個別例外をキャッチして次のソースにスキップする設計にしています。 - 各記事に対して、発表内容や変更点に加えて「背景やビジネスへの影響」も含めた日本語3〜4文(250文字程度)で生成。
- 英語記事もAIが直接日本語に要約するため、別途翻訳APIを通す必要がありません。
- 収集された全記事の中から特にインパクトの大きい変化にフォーカスし、140文字以内の要約を切り口を変えて4パターン生成させます。
- メールの冒頭でサクッと全体の潮流を把握するのに重宝します。
- GCPの Secret Manager に認証情報を保管。
- バッチ実行時に
google.oauth2.credentials.Credentialsを使ってアクセストークンを動的に取得し、users.messages.sendエンドポイントを叩きます。 - 現象: デプロイ後の初回答作確認で
ValueError: invalid literal for int() with base 10: ''が発生し、ジョブが異常終了(Exit Code 1)。 - 原因: GitHub Actionsのワークフロー(
deploy.yml)で--set-env-varsを指定する際、未設定の任意変数LOOKBACK_HOURSが「空文字列("")」として環境変数に注入されていました。int(os.environ.get('LOOKBACK_HOURS', '24'))では、getのデフォルト値(24)が評価されず、空文字列をint()に渡してクラッシュしていました。 - 対策:
config.pyに空文字列チェックを行うヘルパー関数_optional()を追加。Noneの場合だけでなく空文字列""の場合もデフォルト値(24)へフォールバックするように修正しました。 - 静的解析・フォーマッタ:
ruff(Line length: 100, Python 3.12) - 型チェック:
mypy(disallow_untyped_defs = true) - 単体テスト:
pytest - 日々の情報収集が超効率化: 毎朝8時と夕方16月に、必要な情報だけがAI要約付きでメールに届くため、情報検索の時間を大幅に削減。
- 英語ニュースの心理的ハードル解消: 最新機能やグローバルでの動向は英語で一次発信されることが多いですが、AIが自動で日本語化してくれるため見落としが無くなりました。
- サーバーレスで運用が楽: サーバーの管理が不要で、ログや実行結果は Cloud Console や
gcloudコマンドで一括確認できます。
業務で利用している某SaaSサービスに関する最新ニュースや導入事例、機能アップデートなどの情報を日々追うのは非常に大変ですよね。日本語メディアだけでなく海外の専門サイトまでチェックしようとすると、それだけでかなりの時間が削られてしまいます。
そこで今回、Google Cloud Platform(GCP) と AI(Anthropic API / Claude)、Gmail API を組み合わせて、指定した時間に某SaaS関連ニュースを自動収集し、AI要約付きのダイジェストメールとして配信するニュースバッチシステム を構築しました!
完全に技術寄りの解説として、設計から実装、CI/CD、テスト、本番トラブル対応まで一通りまとめましたので、同じような自動情報収集バッチを作りたい方の参考になれば幸いです。
💡 システムの概要と全体像
構築したシステムの構成は非常にシンプルなサーバーレスアーキテクチャです。
Cloud Scheduler (毎日 8:00 / 16:00 JST)
│ HTTP POST (OAuth認証)
▼
Cloud Run Jobs "batch"
│
├─ 1. 5サイトから記事一覧を収集 (sources.py)
├─ 2. 直近LOOKBACK_HOURS時間以内の記事に絞り込み (digest.py)
├─ 3. Anthropic APIで記事要約 & 全体要約4パターン生成 (summarizer.py)
├─ 4. メール件名・本文を組み立て (digest.py)
└─ 5. Gmail APIで個人アドレスへ送信 (mailer.py)
│
▼
指定Gmailアドレスにダイジェストメール着信
🌐 収集対象ソースと取得戦略
海外メディアも含め、対象の某SaaSに特化した最新情報を効率的に集めるため、ありそうでなさそうな以下の5つのソースを対象に設定しました。
| No. | ソース名 | 種別 | 取得方式 |
|---|---|---|---|
| 1 | クラウドBizプレス(公式ニュース) | 公式 | RSS |
| 2 | TechWave Keywords「某SaaS」 | 国内ITメディア | HTML直接スキャン (※RSS非提供) |
| 3 | Global SaaS Post (English) | 海外メディア | RSS (英語) |
| 4 | Enterprise Cloud Insider (CRMカテゴリ) | 海外専門誌 | RSS + キーワードフィルタ |
| 5 | SaaS-Hacks.io | 海外テックブログ | RSS |
🛠️ 工夫したポイント・泥臭い実装
🤖 Anthropic API(Claude)による2段構えの要約処理
要約エンジンには Anthropic API(モデル: claude-haiku-4-5-20251001)を採用しました。速さとコストパフォーマンスが高く、ニュース要約には最適です。
要約は2段階で実行されます。
1. 記事ごとの詳細要約
2. 全体傾向の要約(切り口を変えた4パターン)
⚠️ プロンプトエンジニアリングと出力クレンジング
プロンプト側で「Markdown記法や見出しを使わないで」と指示していても、LLMの出力には稀に # SaaSニュース分析 のようなMarkdown見出し行が入ってしまうことがあります。
メールの見た目を崩さないため、コード側で保険として以下のような後処理関数(_strip_heading_lines())を噛ませています。
def _strip_heading_lines(text: str) -> str:
"""Markdown見出し(#で始まる行)を除去し、複数行を1行に結合する"""
lines = [line.strip() for line in text.splitlines() if line.strip()]
filtered_lines = [line for line in lines if not line.startswith('#')]
return " ".join(filtered_lines)
📩 Gmail APIによるメール送信
個人開発や社内自動化において「SMTPサーバーを立てる」あるいは「SendGrid等の外部メールサービスを契約する」のはハードルがあります。
今回は既有の個人Gmailアカウントから安全かつ確実に送信するため、Gmail API(OAuth 2.0 リフレッシュトークン認証)を採用しました。
🏗️ ディレクトリ構造とコード設計
バッチ部分のディレクトリ構成です。モジュールを明確に分割し、保守性を高めています。
services/batch/
├── job/
│ ├── __init__.py
│ ├── main.py # エントリポイント。パイプライン制御
│ ├── config.py # 環境変数・Secret読み込みとバリデーション
│ ├── sources.py # 5サイトからの記事収集ロジック
│ ├── digest.py # フィルタリングおよびメール本文整形
│ ├── summarizer.py # Anthropic API呼び出し・クレンジング
│ └── mailer.py # Gmail APIによる送信処理
├── scripts/
│ └── get_gmail_refresh_token.py # ローカルで初期リフレッシュトークンを取得するCLIスクリプト
├── tests/
│ ├── test_config.py
│ ├── test_sources.py
│ ├── test_digest.py
│ ├── test_summarizer.py
│ └── test_main.py
├── Dockerfile
├── pyproject.toml # ruff, mypy, pytest の設定
└── requirements.txt
🐛 本番デプロイで遭遇したトラブルと解決策
開発完了後、GitHub Actions 経由で Cloud Run Jobs にデプロイし、gcloud run jobs execute で本番テストを行った際に発生したトラブルと対策を共有します。
1. GitHub Variablesの空文字列(””)による ValueError 発生
def _optional(env_name: str, default: str) -> str:
val = os.environ.get(env_name, "").strip()
return val if val else default
2. –dry-run オプションの重宝さ
本番環境でメール送信テストを何度も行うとAPIコストが無駄にかかる問題や送信テストの不都合があります。
エントリポイント(main.py)に --dry-run フラグを実装し、メール送信を行わずに標準出力(コンソール)へ組み立て結果を表示するモード を用意することで、ローカルやCloud Shell上での動作検証が非常にスムーズになりました。
# ローカルでの動作確認用コマンド例
python -m job.main --dry-run
🧪 テスト・品質管理
品質向上とデグレ防止のため、静的解析ツールと自動テストを導入しています。
テストコード(pytest)は36ケースを作成し、全ソースの解析・パース例外・AI要約のフォールバック動作・見出し除去ロジック・直近時間フィルタ・設定エラーケースなどを網羅しました。
$ pytest
========================== 36 passed in 1.38s ==========================
🚀 まとめ
Cloud Run Jobs と Anthropic API、Gmail API を連携させることで、サーバーレスかつ運用コストほぼゼロで「某SaaS最新ニュースダイジェストバッチ」を構築することができました!
構築して良かったポイント
同じようなバッチを作りたい方や、クラウド×AIによる自動化に興味がある方の参考になれば幸いです!

コメント