はじめに
AWSのアップデート、AI界隈のニュース、Terraform / UiPath / 開発者ツール … 自分の関心領域だけでも、毎日数十件のフィードが流れてきます。全部追うのは無理、けれど追わないと取り残される、という板挟みが地味にストレスでした。
「AIに毎朝読んでもらえばいいじゃん」とすぐ思いつきます。実際、AIエージェントは長文の要約や関心軸でのフィルタが得意です。
ただ、1回目の試行は失敗しました。AIエージェント(Claude Code)のスケジュールタスクで毎朝の収集を走らせていたのですが、
- 自分のマシン(Desktop App)が起動していない時間は動かない
- 平日朝の出勤直前に「あ、今日まだ立ち上げてない」で抜ける
- 結果として日次のリズムが崩れ、ニュースを読まなくなった
つまり収集をAIに任せた途端、デバイス依存が露呈した。AIの賢さ以前に、毎日確実に動くインフラがなければ運用は続かない。
そこで設計を組み直しました。
「機械的な収集」と「主観的なキュレーション」を分ける — 収集はGitHub Actionsに置き、AI(Claude Code)はその出力を朝読むだけの役割にする
これで毎朝、自分のマシンが寝ていても、PCの蓋を閉じていても、ニュースが集まった状態で1日が始まる設計になりました。本記事ではその構成と、AIに任せたい人が”前段”をどう機械化するかの考え方を共有します。
自己紹介
Qurated編集部のKです。AIを業務と生活の両方に取り込んだ実践事例を、こちらquratedlab/journalで書いています。
全体像
組んだパイプラインはこんな形です。
[GitHub Actions cron] 毎朝 19:30 UTC = 翌朝 04:30 JST
↓
[fetch_daily_info.py] Python標準ライブラリのみ・RSS 8ソースを並列に取得
↓
[整形 + スパムフィルタ] og:description優先で要約を取り直し、Markdownに整形
↓
[auto-commit + push] daily-info/YYYY-MM-DD.md として履歴を残す
↓
[Claude Code /morning] 朝、AIが当日ファイルを読んで自分用にキュレーション
前段の収集はAIを使っていません。これがキモです。RSSの取得・整形・保存は決定的な処理なので、機械の世界(GitHub Actions + Python)に閉じた方が安く・確実に回ります。AIの主観的判断は後ろに寄せます。
部品1: GitHub Actions cron
ワークフローは超シンプルです。
name: Daily Info Fetch
on:
schedule:
# 毎日 19:30 UTC = 翌朝 04:30 JST
- cron: '30 19 * * *'
workflow_dispatch: # 手動実行も可
jobs:
fetch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Fetch daily info
run: python scripts/fetch_daily_info.py
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add daily-info/
git diff --staged --quiet || git commit -m "chore: daily info $(date +%Y-%m-%d)"
git push
ポイントは3つ。
- runs-on: ubuntu-latest: 無料枠で十分。プライベートリポジトリでも個人利用なら月の無料分(2,000分)を使い切ることはまずない
- schedule + workflow_dispatch の二段構え: 自動で動くだけでなく、Actionsタブから手動でも走らせられる。設計を試行錯誤するときに地味に効く
- permissions: contents: write: GitHub Actionsから同リポジトリにcommit + pushする最小権限。これを忘れると最後のpushがコケる
GitHub Actionsの schedule は数分〜十数分のジッタがあります(公式ドキュメントのscheduleにも「保証しない」旨の記載あり)。「ピッタリ4:30」では発火しません。私は JST 4:30着を目安に、逆算で 19:30 UTC に設定して余裕を持たせています。
部品2: Python スクリプト(標準ライブラリのみ)
scripts/fetch_daily_info.py は外部依存ゼロです。requests も feedparser も使いません。
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
JST = timezone(timedelta(hours=9))
FEEDS = [
{"name": "AWS Blog", "url": "https://aws.amazon.com/blogs/aws/feed/", "emoji": "☁️"},
{"name": "AWS What's New", "url": "https://aws.amazon.com/about-aws/whats-new/recent/feed/", "emoji": "🆕"},
{"name": "Terraform Releases", "url": "https://github.com/hashicorp/terraform/releases.atom", "emoji": "🏗️"},
{"name": "UiPath Blog(公式)", "url": "https://www.uipath.com/blog/rss.xml", "emoji": "🤖"},
{"name": "Zenn トレンド", "url": "https://zenn.dev/feed", "emoji": "📝"},
{"name": "dev.to (AWS)", "url": "https://dev.to/feed/tag/aws", "emoji": "👩💻"},
{"name": "Hacker News (AI/Cloud)", "url": "https://hnrss.org/newest?q=aws+OR+terraform+OR+claude+OR+%22generative+AI%22&points=30", "emoji": "🔶"},
]
HOURS_BACK = 48 # 過去48時間以内の記事のみ
なぜ標準ライブラリ縛りか。理由は2つ。
- GitHub Actionsの起動時間が短い:
pip installのステップを省ける。1分以内に終わる - メンテが消える: 依存パッケージのバージョンアップで毎月Dependabot PRが来る世界から抜けられる
urllib.request + xml.etree.ElementTree で十分です。
og:descriptionで要約を取り直す
地味に効いたのが、RSSのdescriptionをそのまま使わず、記事ページのog:descriptionを取りに行く設計です。
def fetch_article_text(url: str, fallback: str) -> str:
"""記事URLのog:description / meta descriptionを取得。失敗時はfallback。"""
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=5) as resp:
html = resp.read().decode("utf-8", errors="ignore")
m = re.search(r'<meta[^>]+property=["\']og:description["\'][^>]+content=["\'](.*?)["\']', html, re.IGNORECASE)
# ... fallback to name="description" ...
if m:
return strip_html(m.group(1))[:200]
return fallback
RSSのdescriptionは「タイトルの言い換え」レベルで終わる供給元が多く、og:descriptionの方が一段濃い要約になっています。タイトル + og:descriptionの組み合わせで、朝に「これは深掘りすべきか」を1秒で判断できる密度になります。
壊れたRSSへの防御
実在のRSSは想像以上に汚れています。例えばUiPath公式RSSは、XML 1.0で許可されていない制御文字や、エスケープされていない & を含んで配信されることがあります。厳格な ElementTree だとそこで止まります。
_ILLEGAL_XML_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
_BARE_AMP_RE = re.compile(r"&(?!amp;|lt;|gt;|quot;|apos;|#[0-9]+;|#x[0-9a-fA-F]+;)")
def sanitize_xml(raw: bytes) -> bytes:
text = raw.decode("utf-8", errors="ignore")
text = _ILLEGAL_XML_RE.sub("", text)
text = _BARE_AMP_RE.sub("&", text)
return text.encode("utf-8")
これを通してからparseすれば、正常なフィードはそのまま、壊れたフィードだけ修復されます。
スパムフィルタ
dev.to やZennには時々、「Buy followers」「24/7 service」といった本筋と無関係な投稿が混ざります。タイトル + 本文の語彙ベースで弾きます。
SPAM_TITLE_KEYWORDS = ["buy ", "sell ", "cheap ", " price", "accounts", "followers", "views", "likes"]
SPAM_BODY_PATTERNS = ["telegram:", "discord:", "24/7", "getusasmm", "@get"]
def is_spam_like(title: str, summary: str) -> bool:
if any(kw in title.lower() for kw in SPAM_TITLE_KEYWORDS):
return True
matches = sum(1 for p in SPAM_BODY_PATTERNS if p in summary.lower())
return matches >= 2
完璧ではないですが、明らかに不要なものは消えるので朝の体験が崩れません。
部品3: auto-commit でファイル化する
整形後のMarkdownは daily-info/YYYY-MM-DD.md として保存され、ワークフローの最後で GitHub Actionsがそのままcommit + pushします。
これにより日次のニュースがGitリポジトリ自体に履歴として残るのがポイントです。
- 「あの日の発表でやりたい記事があった」を後から思い出せる
- フォーマットや収集ロジックを変えても、過去データは時点保存されている
- 別のAIエージェント(Claude / Gemini / Codex 等)から同じファイルを読み込める
DBもS3も用意していません。Gitだけで履歴管理が回ります。
GitHub Actionsから同リポジトリにcommit + pushする時は、permissions: contents: write を明示しないと最後のpushが silentにコケます。ワークフローの最初に書いておくのが安全です。
部品4: AIによる朝のキュレーション
ここで初めてAIが登場します。Claude Codeに /morning というスラッシュコマンドを定義しておき、
- 今日の
daily-info/YYYY-MM-DD.mdを読む CLAUDE.md(プロジェクトメモリ)に書いてある自分の関心テーマ・価値観を踏まえて- 「今日読むべきもの」を3〜5件に絞って提示
を任せます。
CLAUDE.md には例えば、
## 関心テーマ
- AWS / Terraform / クラウドインフラ / セキュリティ
- AI / 生成AI(Claude Code 含む)
- UiPath / RPA(業務自動化)
## 価値観
- 「人生が自分のリズムで回っている」状態を目指している
- 小さな積み重ねを大切にしたい
といった情報が書いてあります。これがあるおかげで、AIは「ただ集めたもの」から「私のためのキュレーション」へ変換してくれます。
なぜGitHub Actionsを選んだか
最初に作ったClaude CodeのMCPスケジュールタスク方式と比べて、GitHub Actionsを選んだ理由は4つあります。
| 観点 | Claude Code MCP cron | GitHub Actions cron |
|---|---|---|
| 起動環境 | 自分のマシン(Desktop App)が起動している必要 | server-side、マシン状態に依存しない |
| コスト | 無料(ただしマシン稼働が前提) | 無料枠(個人利用ならほぼ尽きない) |
| 履歴管理 | スケジュールタスクのログのみ | リポジトリのcommit履歴がそのままアーカイブ |
| 検証性 | ログを見に行く必要 | Actionsタブで成功/失敗が常に見える |
特に効いたのは1番目です。マシン非依存にすると、その瞬間に運用が楽になります。寝てる間でも、出張で別端末を使ってる時でも、ニュースは集まっている。AIに任せるための前段が、デバイスから切り離される。
効いた設計のコツ
3つあります。
1. 「収集」と「キュレーション」を別レイヤーに分ける
最初の失敗は、両方をAIエージェントに任せていたことでした。収集は決定的・キュレーションは主観的、というレイヤーの違いを意識して分けると、それぞれを別のインフラに最適化できます。
- 収集: GitHub Actions + Python (cheap, deterministic, no AI)
- キュレーション: Claude Code + CLAUDE.md (subjective, contextual, AI)
2. 依存ゼロのスクリプトにする
GitHub Actionsで動かすときは、依存を増やせば増やすほど起動が遅くなり、メンテも増えます。Pythonの標準ライブラリ(urllib, xml.etree, re, datetime)で書ける範囲なら、それで完結させた方が幸せです。
feedparser を使えば10行で書けるところを、わざわざ40行で書いています。それでも、pip install の不確実性とDependabotの通知から解放される方が遥かに勝ります。
3. 出力をファイルとして残す
daily-info/YYYY-MM-DD.md のように1日1ファイルで残すと、副次効果が大きいです。
- AIに「先週の自分」を渡せる(振り返り用途)
- フォーマット変更があっても過去データは保存されている
- 他のAIエージェントや、別端末から、同じインターフェース(ファイル)で読める
AI連携の共通インターフェースとして”ファイル”を選ぶのは安い、けれど強い設計だと思います。
何が変わったか
前回の行政MCPの記事、家賃の記事で書いた「AIに任せるのは”取りかかるまでのコスト”を消すため」という主題は、今回も同じです。
ただ、今回の学びはもう一段下のレイヤーにあります。
AIに任せたいなら、AIに依存しないインフラを下に置く。
AIエージェントは賢いし、面白いキュレーションをしてくれます。けれどそれは「データが来ていれば」の話。データを毎日確実に届けるレイヤーはむしろAIから外した方が、結局AIをうまく使えました。
まとめ
- AI(Claude Code)のスケジュールタスクで毎朝の収集を走らせたら、マシン依存で運用が崩れた
- 収集レイヤーをGitHub Actionsに移すと、マシン非依存・無料・履歴付きで走るようになる
- Python標準ライブラリのみで書く / og:descriptionで要約を取り直す / RSS sanitization / スパムフィルタ — どれも地味だが効く
- 出力をMarkdownファイルとしてgitに残すと、AI連携のインターフェースがそのまま安く実現できる
- 「機械的な収集」と「主観的なキュレーション」をレイヤー分離すると、それぞれを別インフラに最適化できる
結論
AIに任せたいなら、AIに依存しないインフラを下に置く。
毎朝AIに何かを読ませたい、要約させたい、判断させたい — もし試して続かなかった人は、収集の前段にGitHub Actionsを置いてみてください。AIの上に確実な機械の世界があるだけで、運用は驚くほど続きます。