
Summary
この文書の要点
- AIは収集設定の生成にのみ関与させ、日次の実行は宣言的設定だけで決定論的に動かします。
- 文字コードはHTTPヘッダ、meta charset、UTF-8フォールバックの多段判定にし、レガシーな日本語エンコーディングを前提から外しません。
- 地域単位のゲートと二段階のコスト上限で、収集範囲の拡大を人が制御できる速度に保ちます。
- 公的機関の告知文という題材でも、出典明記と収集エチケットは技術設計の一部として扱います。
数千サイトの公開データを、同じ形で拾い続けることの難しさ
自治体はそれぞれ独立したWebサイトを持ち、CMS製品も更新頻度もページ構造もばらばらです。ある自治体は表形式で公告一覧を出し、別の自治体はPDFへのリンクだけを並べ、また別の自治体はJavaScriptでページを組み立てます。これを1サイトずつ人手で確認しながら収集ルールを書くのは、対象が数十サイトなら成立しても、全国規模になった瞬間に破綻します。
一方で、収集ルールをすべてAIにその場で判断させる設計も長続きしません。LLMは同じ入力でも出力が揺れますし、費用もかかります。毎日の巡回のたびにページ構造の解釈をAIに委ねると、ある日は取得できていた項目が次の日は取れない、という再現性のない状態になり、下流でその情報を使う処理(通知やダッシュボード)が信頼できなくなります。
この題材で扱うのは、全国の自治体サイトから公告や会議の議事録のような、性質上公開されている定型情報を横断的に集め、必要な人が検索・購読できる形にする収集パイプラインです。個別の自治体名や利用組織には触れず、収集の設計と、AIをどこまで・どのように使うかという判断軸だけを扱います。
対象は静的HTML/PDFに絞り、収集エチケットを先に決めます
設計の出発点として、対象を静的なHTMLとPDFに限定しました。JavaScriptの実行が前提のSPAサイトは対象外です。ヘッドレスブラウザを使えば技術的には対応できますが、数千サイト規模で毎日安定して動かすには、起動コストと失敗率の高さが見合いません。JS必須サイトは初期スコープから外し、静的なページで大部分をカバーできることを優先しました。
収集エチケットも、実装の詳細ではなく設計の前提として先に固定しました。各サイトの`robots.txt`を尊重すること、同一ホストへのリクエストには一定間隔を空けること、深夜から早朝の負荷の低い時間帯にまとめて実行することです。相手は多くの場合、大規模なアクセスを想定していない自治体の公式サイトであり、収集側の都合で相手のサービスに負荷をかけないことを技術要件として扱いました。
もう一つの前提は、収集した情報の性質です。公的機関が発する告知や議事録は、日本の著作権法上、単なる事実の伝達に過ぎない報道や、法令・告示のように保護対象から外れるものが少なくありません。ただし「保護されないから何をしてもよい」わけではなく、出典(自治体名と元URLへのリンク)を必ず併記することを、法務上の配慮としてだけでなく、情報の信頼性を担保する設計要件として組み込みました。
- 対象は静的HTML/PDFのみ。JS必須サイトは初期スコープから外す。
- robots.txt遵守、同一ホストへのアクセス間隔、深夜帯実行を収集エチケットとして固定する。
- 出典(発信元名と元URL)の併記を、法務要件かつ品質要件として扱う。
技術スタック
収集・解析基盤はTypeScript/Bunで統一し、APIフレームワークにHono、ORMにDrizzleを使ってPostgreSQL上にスキーマを構築しています。HTML解析はcheerio、PDFからのテキスト抽出は専用のパーサーライブラリ、レガシーな日本語文字コードのデコードにはiconv-liteを採用しました。埋め込みベクトルの近似検索にはpgvectorを使い、収集したテキストの類似度による事前絞り込みに使っています。
AIの利用箇所は2つに限定しています。ひとつは収集設定そのものの生成支援(後述)、もうひとつは収集したテキストが利用者にとって関心のある話題かどうかを判定する構造化レビューです。どちらも、生成AIの出力をそのまま信用するのではなく、スキーマ検証やドライラン実行という機械的なチェックを必ず挟む設計にしています。
AIは設定を作る側に置き、実行系からは締め出します
この設計の中心にある判断は、「AIが収集設定を生成することは許すが、日次の収集実行そのものにAIを介在させない」という境界です。収集設定は、CSS選択子・正規表現・辞書マッピングだけで構成される宣言的なスキーマ(zodなどで型検証されたオブジェクト)として定義し、実行エンジンはこの設定を解釈するだけの決定論的なコードにしました。
この分離をしない場合、日次実行のたびにLLMへページ構造を渡して抽出方法を推論させることになります。それは一見柔軟に見えますが、実際には「今日は正しく取れたが昨日は取れなかった」という説明のつかない揺らぎと、数千サイト×毎日という呼び出し回数に比例した費用増加を同時に招きます。設定生成と実行を分けることで、実行系は完全に再現可能になり、費用は「新しい設定を作るとき」だけに発生します。
AIの役割を「提案者」に限定するという考え方は、収集パイプラインに限らず、決定的に検証できない処理をAIに任せるときに繰り返し有効な境界の引き方です。AIの出力は常に、人が承認するか、機械的な検証(ドライラン、スキーマ検証)を通過するかのどちらかを経てから初めて実行系に反映されます。
テンプレートと個別サイト差分を2層に分け、変更を追跡可能にします
収集設定は、共有テンプレートと個別サイトの差分(overrides)の2層構造にしています。多くの自治体サイトは少数のCMS製品のいずれかを使っているため、CMS単位・レイアウトパターン単位の共通テンプレートを用意し、個別サイト固有の差分だけを上書き設定として持たせることで、似た構造のサイトに対する設定作成コストを大きく下げられます。
設定は上書きではなく追記(リビジョン)で管理し、過去のリビジョンを参照可能な状態で残します。ある日から収集結果が変わった場合に、サイト側のレイアウト変更が原因なのか、設定変更が原因なのかを後から切り分けられるようにするためです。設定変更の履歴が残らない収集基盤は、障害調査のたびに「いつから」「なぜ」壊れたのかを推測に頼ることになります。
- 共有テンプレート(CMS/レイアウト単位のプリセット)+個別サイトoverridesの2層構成。
- 設定は上書きせず追記型のリビジョンで管理し、過去の設定を参照可能にする。
文字コード判定は、思い込みを捨てて多段フォールバックにします
自治体サイトには、UTF-8だけでなく、Shift_JISやEUC-JPといったレガシーな日本語文字コードで配信されているものが今でも一定数残っています。最初は実行環境の標準的な文字列デコード機構だけで十分だろうという前提で設計しましたが、これらのレガシーエンコーディングには標準機構が対応しておらず、文字化けという形で問題が顕在化しました。
最終的には、HTTPレスポンスヘッダのcharset指定を最優先とし、それが無ければHTML内の`<meta charset>`を覗き見し、それでも判定できない場合はUTF-8にフォールバックするという優先順位を実装し、レガシーエンコーディングのデコードには専用ライブラリ(iconv-lite)を挟む構成に落ち着きました。「今どきUTF-8以外はもう無いだろう」という前提は、全国規模の公共サイトを相手にする限り成立しません。
この種の落とし穴は、少数のサイトで検証している間は表面化しません。対象サイト数を絞ったパイロット段階では気づかず、収集範囲を広げた段階で初めて発覚する、というのは収集基盤全般に共通するリスクです。文字コードのような「枯れた」領域こそ、対象母数が大きくなった時に例外が顕在化しやすいと考えておくべきです。
AI支援の設定生成は、3段階でハルシネーションと暴走を防ぎます
新しいサイトの収集設定をゼロから人手で書くのは時間がかかるため、AIに設定生成を支援させる仕組みを用意しています。ただし、AIに自由にページを読ませて設定を作らせると、実在しないURLやセレクタを生成する(ハルシネーションする)リスクがあるため、生成プロセスを3段階に分け、各段階で機械的なガードレールを設けています。
1段階目は、対象サイトのトップページをクロールし、リンク候補をキーワードで事前フィルタしたうえで、その中からLLMに「公告一覧や議事録一覧に該当しそうなURL」を選ばせます。ここで重要なのは、LLMに新しいURLを生成させるのではなく、実際に存在するリンクの中から選ばせることです。これだけで、存在しないパスを設定に組み込んでしまう典型的な失敗を防げます。
2段階目は、対象ページのDOM構造をそのままLLMに渡すのではなく、クラス名やタグ構造だけを残してテキストを間引いた骨格情報に変換してから渡します。トークン消費を抑える目的もありますが、それ以上に、本文のノイズに引きずられずに構造的なパターンだけを見て選択子を提案させる狙いがあります。ここで提案された内容は、スキーマ検証済みの宣言的設定オブジェクトとして組み立てられます。
3段階目は、生成された設定を使って即座にドライラン(実際の抽出)を行い、実際に項目を抽出できたものだけを「提案」として残します。抽出に失敗した設定は破棄され、人に提示すらされません。さらに、生成された設定を自動的に本番投入することはせず、既定では承認フローを経て人が確認してから有効化する運用にしています。
- ステップ1: 実在するリンクの中からのみ候補ページを選ばせ、URLのハルシネーションを防ぐ。
- ステップ2: DOM骨格だけを渡し、トークンを節約しつつノイズに引きずられない選択子生成にする。
- ステップ3: 生成直後にドライラン検証し、抽出できた設定だけを提案として残す。
- 自動適用は既定でオフにし、人の承認を経てから有効化する。
地域単位のゲートで、収集範囲を段階的に広げます
全国規模の収集を最初から一括で有効化すると、想定外の失敗や、思ったより重い負荷が同時多発的に発生し、原因の切り分けが難しくなります。そこで、日次収集の対象を「収集を許可した地域」だけに絞るグローバルな許可リストを用意し、新しい地域を有効化するたびに段階的に対象を広げる運用にしています。
このゲートは、性能上のボトルネックではなく、意図的な安全弁です。設定の品質や失敗率を見ながら対象範囲を広げられるようにすることが目的で、既定の日次スケジュールではこのゲートの外にある地域は収集対象に入りません。手動での明示的なトリガーだけがこのゲートを一時的にバイパスできる、という非対称性を持たせています。
コスト上限は、呼び出し前の予約で止めます
AI呼び出しを伴う機能(設定生成支援や内容レビュー)には、テナント単位の日次上限とシステム全体の日次上限という二段階のクォータを設けています。片方だけでは、特定のテナントが集中的に使い切ってしまう問題も、全体としての予算超過も防げないため、両方を独立してチェックする構成にしました。
クォータの判定は、呼び出し後の実費を集計してから制限するのではなく、呼び出し前に「これから使う分」を仮に確保し、確保できない場合は実行しないという事前予約の形にしています。使ってから制限すると「気づいた時には超過している」状態になりやすいため、先に枠を押さえる設計のほうが運用上安全です。
収集したテキストを、埋め込み類似検索と構造化AIレビューで絞り込みます
収集した議事録や公告の全文を、そのまま毎回LLMにレビューさせるのはコストと精度の両面で非効率です。そこで、まずベクトル埋め込みによる類似検索で「関心のあるトピックに近そうな候補」を事前に絞り込み、その候補だけをLLMの構造化出力レビューにかける、という二段構えにしています。
埋め込みの生成自体もコストがかかるため、全地域・全テナント分をあらかじめ一括生成するのではなく、あるテナントがその地域を分析対象として選択した場合にのみオンデマンドで生成する設計にしました。使われるかどうか分からないデータに事前投資しない、という考え方です。
日本の行政文書には和暦(令和・平成・昭和など)による日付表記が混在するため、収集したテキストを扱う前段で、和暦を含む日付表記を統一フォーマットへ正規化する処理を挟んでいます。ロケール固有の表記揺れは、収集基盤の中では地味ですが見落とすと下流の並び替えやフィルタリングが静かに壊れる領域です。
内部運用機能は、テナントから見えない場所に切り離します
収集設定の承認やコスト上限の調整といった運用機能は、一般利用者向けのアプリケーションとは別の、社内運用者専用のアプリケーションに分離しています。単に画面上でメニューを隠すのではなく、そもそもテナント側のアプリケーション一覧に登録しない、という形で構成レベルから見えなくしています。
権限チェックを画面表示の条件分岐だけに頼ると、実装ミスひとつで意図しない利用者に運用機能が見えてしまうリスクが残ります。アプリケーションそのものを分け、システムの構成要素として最初から利用者の目に触れない場所に置くほうが、事故の起きようがないという意味で堅牢です。
検証観点: ドライラン、承認フロー、段階的拡大
この基盤の検証は、単体テストだけでは足りません。生成された収集設定が実際のサイトから正しく項目を抽出できるかというドライラン検証、承認前の設定が本番の日次収集に混入しないことの確認、そして地域ゲートを広げた直後に失敗率が跳ね上がらないかの監視が、日常的な検証観点になります。
新しい地域を有効化する際は、対象サイト数が少ないうちに数日分の収集結果を確認し、抽出漏れや文字化けが無いことを確かめてから、対象を広げるという運用を徹底しています。全件を一度に有効化して事後的に問題を発見するより、小さく確認しながら広げるほうが、結果的に手戻りが少なくなります。
著作権と収集エチケットという、技術だけでは決まらない制約
公的機関が発信する告知や議事録の多くは、著作権法上、単なる事実の伝達や法令・告示に類するものとして保護の対象から外れる場合があります。ただし、これは「収集して自由に加工・再配布してよい」ことを意味しません。出典(発信元の名称と元のURL)を必ず表示することを、法務上の要請であると同時に、情報の一次ソースを利用者が確認できるようにするための品質要件として、収集パイプラインの出力仕様に組み込んでいます。
robots.txtの遵守や同一ホストへのアクセス間隔の制御も、技術的な実装の細部ではなく、相手のサイトに迷惑をかけないという運用上の責任として、収集エンジンの必須要件に位置づけています。収集対象が公共性の高い情報だからこそ、収集する側の振る舞いにも一定の節度が求められると考えています。
この方式が向かない条件
この設計は、静的なHTML・PDFで構成された、更新頻度が日次程度の定型的な公開情報の収集に向いています。逆に、JavaScriptでの動的レンダリングが必須のサイトや、リアルタイム性が求められる通知(数分単位での即時反映など)には向きません。日次バッチという前提そのものが、速報性を犠牲にしてコストと安定性を優先する選択だからです。
また、対象サイトの構造が極端に頻繁に変わる場合は、設定のメンテナンスコストがAI支援をもってしても高止まりします。この方式が効くのは、対象サイトの母数は多いが、個々のサイトの構造変化自体はそれほど頻繁ではない、という前提が成り立つ領域です。
まとめ: AIは提案者、実行は決定論的に
数千サイト規模の公開データ収集を、費用と再現性の両方を保ちながら毎日回し続けるための鍵は、AIをどこまで賢くするかではなく、AIの関与を「設定を提案する」役割に限定し、実際の実行系を非AIの決定論的な仕組みに固定することでした。設定生成は揺らいでよいが、実行は揺らいではいけない、という境界の引き方です。
この考え方は、収集基盤に限らず、AIを業務システムに組み込むときに繰り返し使える判断軸だと考えています。AIの出力を最終判断としてそのまま実行系に流し込むのではなく、スキーマ検証やドライランのような機械的な検証点を必ず挟み、AIが間違えても実行系全体が揺らがない構造にしておくことが、AI活用を長く安定して運用するための現実的な設計です。


