DH分野のツール情報を追いかける
デジタル人文学(DH)の分野では、OCR、IIIF、テキスト翻刻といった領域で新しいツールが継続的に開発されています。NDL(国立国会図書館)の ndl-lab や CODH(人文学オープンデータ共同利用センター)などの機関がGitHub上でツールを公開しており、研究者個人による開発も活発です。
こうした情報を体系的に収集し、カレントアウェアネスのように定期的にまとめる仕組みが欲しいと考え、自動収集・記事生成のシステムを構築しました。
収集対象
情報源は3種類です。
X (Twitter) では、DH分野で活発にツールを開発・公開している研究者や機関のアカウントを対象にしています。
RSSフィードでは、カレントアウェアネス・ポータル(国立国会図書館)の https://current.ndl.go.jp/rss.xml を取得しています。
GitHub では、DH関連のツールを公開している組織・個人の公開リポジトリの更新情報を GitHub API 経由で取得しています。
方法の検討
X投稿の取得
X の投稿取得にはいくつかの方法を検討しました。
| 方法 | 費用 | 結果 |
|---|---|---|
| X API (Basic) | $100/月 | 確実だが高コストのため不採用 |
| Web検索(site:x.com/xxx) | 無料 | インデックスされたごく一部しか取れず不採用 |
| RSSHub(セルフホスト) | 無料 | Xの内部API経由で全ツイート取得可能。Docker運用が必要で、GitHub Actions内での一時起動も検討したが、Playwright案のほうがシンプルなため不採用 |
| Playwright(ログインなし) | 無料 | 一部アカウントで0件。ログインウォールに阻まれ不採用 |
| Playwright(Cookie認証) | 無料 | 全アカウントで取得成功。毎日の取得であれば取りこぼしも少ない。採用 |
最終的に、Playwright で Cookie 認証を使う方法を採用しました。DevTools から auth_token と ct0 を手動で取得し、.x_cookies.json に保存する形です。GitHub Actions 上では Secret TWITTER_COOKIE から環境変数として渡しています。
Playwright による自動ログインも試みましたが、X のボット検出で失敗したため、手動取得に落ち着きました。Cookie は数ヶ月で期限切れになるため、定期的な更新が必要です。
AI要約・記事生成
| 方法 | 費用 | 結果 |
|---|---|---|
| Claude Code 内で手動実行 | プラン内 | 自動化できないためテスト用のみ |
| Claude API (Anthropic直接) | 従量課金 | 動作するが独自のAPI Key管理が必要で不採用 |
| OpenRouter | 従量課金 | 複数モデル選択可能、1回$0.05程度で採用 |
RSSフィード
カレントアウェアネス・ポータルでは /feed エンドポイントがアイテム0件で、/rss.xml に30件のアイテムがあることを確認し、後者を使用しています。CODH のサイトはメンテナンス中でRSS取得ができなかったため、X 投稿で代替しています。
アーキテクチャ
システムは日次収集と週次記事生成の2段階で動作します。
GitHub Actions(毎日 JST 7:00)
├── Playwright + Cookie で X 投稿を取得(3アカウント)
├── RSS で カレントアウェアネス-R を取得
├── GitHub API でリポジトリ更新情報を取得
└── data/daily/YYYY-MM-DD.json に蓄積・コミット
GitHub Actions(毎週日曜 JST 9:00)
├── その週の daily/*.json を集約
├── OpenRouter 経由の AI で記事生成(テーマ: 新規ツール開発に限定)
├── Zenn記事 + Hugo記事を生成
└── PR を作成 → レビュー後にマージ
実装の詳細
日次収集 (collect_daily.py)
collect_daily.py は3つの情報源からデータを収集し、data/daily/YYYY-MM-DD.json に保存します。
X の取得では、Playwright で各アカウントのページにアクセスし、Cookie 認証でタイムラインを表示します。5回スクロールして複数件を読み込んだ後、article[data-testid="tweet"] セレクタでツイート要素を取得し、テキスト・URL・日時を抽出しています。
tweet_articles = page.query_selector_all('article[data-testid="tweet"]')
for article in tweet_articles:
text_el = article.query_selector('[data-testid="tweetText"]')
text = text_el.inner_text() if text_el else ""
カレントアウェアネスは標準ライブラリの xml.etree.ElementTree でRSSをパースしています。content:encoded があればそちらを優先し、なければ description を使用します。
GitHub は公開イベントAPI(/users/{owner}/events/public)を使用しています。認証なしの場合、PushEvent の commits が省略されるケースがあるため、その場合はブランチ名だけを記録するようにしています。
外部ライブラリへの依存は Playwright のみです。HTTP通信やXMLパースは標準ライブラリの urllib と xml.etree.ElementTree で処理しています。
週間記事生成 (generate_weekly.py)
generate_weekly.py は1週間分の日次データを集約し、OpenRouter API 経由で記事を生成します。
OpenRouter API は OpenAI 互換のインターフェースを持つため、urllib で直接呼び出しています。モデルは anthropic/claude-sonnet-4 を指定しています。
payload = json.dumps({
"model": model,
"max_tokens": 4096,
"messages": [{"role": "user", "content": prompt}],
}).encode("utf-8")
プロンプトでは「新規ツール開発・公開」にテーマを厳密に限定しています。デジタルアーカイブの公開・リニューアルはツール開発ではないため除外対象です。Git push だけの情報では記事にせず、具体的な機能追加がわかる場合のみ取り上げるよう指示しています。ライティング規約(ですます調、太字禁止、断定を避ける等)もプロンプトに組み込んでいます。
重複除去はURLベースで行い、同一URLが複数日にわたって収集された場合も1件として扱います。
Hugo への同期 (sync_to_hugo.py)
sync_to_hugo.py は Zenn 記事の frontmatter を Hugo 形式に変換し、content/ja/posts/ に出力します。ファイル名から日付を抽出し、DH-Weekly カテゴリと dh-weekly タグを付与しています。Zenn 側で published: true になっている記事のみが同期対象です。
Hugo 0.157 で schema_json.html の safeJS パイプラインがマルチバイト文字でエラーを起こす問題がありました。.Plain | truncate 5000 | jsonify に修正することで解消しています。
プロンプトチューニングの過程
AI による記事生成では、プロンプトの調整が重要でした。
初回の生成では、デジタルアーカイブのリニューアルなど、テーマ外のトピックが混入しました。これに対して以下の改善を行いました。
- 除外条件を明確化し、「既存サービスのリニューアル」「メタデータサービスの変更」を除外対象として明示
- 「推測されます」等の曖昧表現を禁止し、情報不足なら取り上げないよう指示
- Git push だけでは記事にせず、具体的な機能変更がわかる場合のみ取り上げるよう指示
これらの改善により、6記事中テーマ外のトピックの混入が0件になりました。
費用
| 項目 | 費用 |
|---|---|
| X投稿取得(Playwright + Cookie) | 無料 |
| カレントアウェアネス(RSS) | 無料 |
| GitHub(公開API) | 無料 |
| AI記事生成(OpenRouter経由) | 1回約$0.05(月$0.20程度) |
| GitHub Actions | 無料枠内(月2,000分、1回約2分) |