Claude Media
e-GovのパブリックコメントRSSをClaude Codeで週次監視し要約する

e-GovのパブリックコメントRSSをClaude Codeで週次監視し要約する

e-Govパブリック・コメントのRSSから案件番号・所管・締切を拾い、自社に関係する意見公募だけを週1回Claude Codeに要約させる手順。取得スクリプトと定期実行の選び方つき。

意見公募(パブリックコメント)は、案の公示から締切まで1か月ほどの案件が多く、出遅れると意見を出す機会が過ぎます。法令や告示になってから気づいても、もう間に合いません。

e-Govパブリック・コメントは、募集中の案件一覧をRSSで配信しています。これを週に1回取得し、自社に関係する案件だけをClaude Codeに要約させれば、締切前に気づける仕組みになります。ここでは、フィードの中身、絞り込みスクリプト、要約の書式、定期実行の順に組みます。

用意するもの

  • Python 3(標準ライブラリだけで動きます。追加のインストールは不要です)
  • Claude Code v2.1.259以降(定期実行で使う--permission-prompts noneがこのバージョンから使えます)
  • 作業用のディレクトリ。本文の名前で揃えると次の構成になります
pcm-watch/
├── pcm_watch.py      取得と絞り込みのスクリプト
├── watchlist.json    監視条件
├── run_weekly.sh     週次で呼ぶシェル
├── CLAUDE.md         メモの書式
├── state/            既読の案件番号(seen.json)
├── out/              絞り込み結果(candidates.json)
└── memos/            週次メモの出力先

state/、out/、memos/はスクリプトとClaude Codeが作るので、空のディレクトリを用意する必要はありません。

RSSで取れる項目と、取れない項目

フィードはRSS 1.0(RDF)形式で、案件ごとにitemが1つ並びます。1件あたりに入っている情報は次のとおりです。

欲しい情報フィード上の場所注意
案件の題名フィード上の場所title注意同じ題名で別案件のことがある
詳細ページフィード上の場所link注意クエリのidが案件番号
案件番号フィード上の場所linkのid=注意詳細ページの「案件番号」と一致する
公示日・締切フィード上の場所description内の「案の公示日」「受付締切日時」注意時刻は案件ごとに違う
カテゴリーフィード上の場所description内の「カテゴリー」注意「厚生」「工業」など
所管フィード上の場所description内の「問合せ先(所管省庁・部局名等)」注意専用の欄はなく、部局名と電話番号が続く

descriptionはHTMLの<br/>で区切られた「項目名:値」の並びです。XMLとしては文字列1つなので、自分で分割します。項目名の括弧は全角で、所管の欄は問合せ先(所管省庁・部局名等)という名前になります。スクリプトの辞書キーも、この全角括弧の表記に合わせています。

案件番号は専用の要素になく、linkのクエリにあります。id=495260216の案件は、詳細ページの「案件番号」も495260216でした。題名の重複排除には、この番号を使います。水産庁の告示案で、題名がまったく同じ案件が番号と締切を変えて3件並んでいたためです。

RSSで取れないものもあります。案の本文、意見公募要領、根拠法令の条項は、詳細ページ側にしかありません。フィードは「どの案件が出たか」を知るための入口で、関係の有無を最終判断する材料にはなりません。

もう1つの落とし穴はdc:dateです。2026-10-09T15:00:Zのように秒が欠けた形で、厳密なISO 8601のパーサーでは読めないことがあります。値は公示日の0時(日本時間)をUTCで書いたもので、公示日はdescriptionから取るほうが確実です。

取得して絞り込むスクリプト

標準ライブラリだけで動くPythonで書きます。監視条件はwatchlist.jsonに置き、題名のキーワード、カテゴリー、所管の3つのどれかに当たった案件だけを残します。

{
  "keywords": ["電気事業", "個人情報", "下請"],
  "categories": ["労働", "外国為替、貿易"],
  "agencies": ["経済産業省"]
}

スクリプトは3つの仕事をします。案件番号で既読を管理し、締切が近い既読案件を別枠で拾い、フィードの形式が変わったら黙って空振りせずに止まります。

import json
import sys
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import parse_qs, urlparse
 
FEED = "https://public-comment.e-gov.go.jp/rss/pcm_list.xml"
RSS = "{http://purl.org/rss/1.0/}"
SEEN = Path("state/seen.json")
OUT = Path("out/candidates.json")
SOON_DAYS = 10
 
 
def fetch_items():
    req = urllib.request.Request(FEED, headers={"User-Agent": "pcm-watch/1.0"})
    root = ET.fromstring(urllib.request.urlopen(req, timeout=30).read())
    items = []
    for it in root.iter(RSS + "item"):
        link = it.findtext(RSS + "link")
        desc = it.findtext(RSS + "description") or ""
        fields = {}
        for seg in desc.split("<br/>"):
            if ":" in seg:
                k, v = seg.split(":", 1)
                fields[k.strip()] = " ".join(v.split())
        for key in ("案の公示日", "受付締切日時", "カテゴリー"):
            if key not in fields:
                sys.exit(f"descriptionの形式が変わった可能性: {key}が無い: {link}")
        items.append({
            "id": parse_qs(urlparse(link).query)["id"][0],
            "title": it.findtext(RSS + "title"),
            "link": link,
            "published": fields["案の公示日"],
            "deadline": fields["受付締切日時"],
            "category": fields["カテゴリー"],
            "contact": fields.get("問合せ先(所管省庁・部局名等)", ""),
        })
    if not items:
        sys.exit("itemが0件: フィードの構造が変わった可能性")
    return items
 
 
def reasons(item, watch):
    hit = [f"題名:{k}" for k in watch.get("keywords", []) if k in item["title"]]
    if item["category"] in watch.get("categories", []):
        hit.append(f"カテゴリー:{item['category']}")
    hit += [f"所管:{a}" for a in watch.get("agencies", []) if a in item["contact"]]
    return hit
 
 
def main():
    if len(sys.argv) < 2:
        sys.exit("使い方: python3 pcm_watch.py watchlist.json | --mark-seen")
    if sys.argv[1] == "--mark-seen":
        pending = json.loads(OUT.read_text())["new_ids"]
        seen = set(json.loads(SEEN.read_text())) if SEEN.exists() else set()
        SEEN.parent.mkdir(exist_ok=True)
        SEEN.write_text(json.dumps(sorted(seen | set(pending))))
        return
    watch = json.loads(Path(sys.argv[1]).read_text())
    seen = set(json.loads(SEEN.read_text())) if SEEN.exists() else set()
    now = datetime.now()
    new, soon = [], []
    for item in fetch_items():
        item["matched"] = reasons(item, watch)
        if not item["matched"]:
            continue
        due = datetime.strptime(item["deadline"], "%Y/%m/%d %H:%M")
        if due < now:
            continue
        if item["id"] not in seen:
            new.append(item)
        elif due - now <= timedelta(days=SOON_DAYS):
            soon.append(item)
    OUT.parent.mkdir(exist_ok=True)
    OUT.write_text(json.dumps(
        {"new": new, "closing_soon": soon, "new_ids": [i["id"] for i in new]},
        ensure_ascii=False, indent=2))
    print(f"new={len(new)} closing_soon={len(soon)}")
    if not new and not soon:
        sys.exit(3)
 
 
main()

既読にするのを別コマンド(--mark-seen)に分けているのは、要約が失敗した週の案件を取りこぼさないためです。取得の時点で既読にすると、Claude Codeの実行が途中で落ちたとき、その案件は二度と出てきません。

監視リストで個人情報やAIのような短い語を使うと、題名の部分一致で関係のない案件も拾います。キーワードは法令名や制度名など、固有の語にします。

動かして出力を確かめる

最初は、定期実行に載せる前に手で通します。

python3 pcm_watch.py watchlist.json
head -n 20 out/candidates.json

標準出力にnew=4 closing_soon=0のような件数が出て、out/candidates.jsonに案件が入れば取得は成功です。candidates.jsonの各案件には、どの条件に当たったかがmatchedに残ります。

{
  "id": "595320038",
  "title": "貿易関係貿易外取引等に関する省令第十条第三項の規定に基づく…意見公募",
  "deadline": "2026/11/07 16:30",
  "category": "外国為替、貿易",
  "contact": "経済産業省 貿易経済安全保障局 経済安全保障政策課 技術調査・流出対策室",
  "matched": ["カテゴリー:外国為替、貿易", "所管:経済産業省"]
}

2回目の実行前に--mark-seenを通すと、同じ案件はnewから外れ、締切が10日以内のものだけがclosing_soonに出ます。該当が1件もないときは終了コード3で止まるので、後段のシェルで「今週は報告なし」と分岐できます。

CLAUDE.mdで要約の書式を固定する

絞り込み済みのJSONなら、Claude Codeに渡す情報は少量で済みます。役割は、題名と所管の文面から「自社にどう関係しそうか」を短く書くことです。

プロジェクトのCLAUDE.mdには、書式と、やってはいけないことを書いておきます。

# パブコメ週次メモの書式
 
out/candidates.json を読み、memos/YYYY-MM-DD.md に書く。
 
- 見出しは「新着」と「締切が近い」の2つ。該当なしの節は「なし」と書く
- 1件ごとに、案件番号・題名・所管・締切・詳細ページのURLを必ず載せる
- 締切は candidates.json の文字列をそのまま写す。曜日や残り日数は計算しない
- 「関係しそうな理由」は matched の条件と題名から書き、1〜2文にとどめる
- 題名と所管から分からないことは「詳細ページで要確認」と書く。案の内容は推測しない
- 意見を出すかどうかの判断は書かない

締切を写すだけにしているのは、時刻が案件ごとに違うからです。フィードで見えた締切の時刻は、09:00、23:59、16:30、12:00、00:00などばらばらでした。00:00の案件は、前日の終わりまでなのか、その日の始まりまでなのか、フィードだけでは読めません。Claude Codeに計算させず、原文の文字列を詳細ページと見比べられる形で残します。

シェルで束ねる

取得、要約、既読化を1本のシェルにまとめます。

#!/bin/sh
python3 pcm_watch.py watchlist.json
code=$?
[ "$code" -eq 3 ] && exit 0
[ "$code" -ne 0 ] && exit 1
 
claude -p "CLAUDE.mdの書式に従い、out/candidates.jsonからメモを作る" \
  --allowedTools "Read,Write" \
  --permission-prompts none || exit 1
 
python3 pcm_watch.py --mark-seen

claude -pは非対話で1回だけ実行するモードで、成功すれば終了コード0、失敗すれば0以外で終わります。そのため、||で受けて既読化を止められます。--allowedToolsでReadとWriteだけを事前承認しているので、確認待ちで止まることはありません。--permission-prompts noneは、誰も確認に答えられない定期実行向けのフラグで、Claude Code v2.1.259以降が必要です。それより古いバージョンでは「不明なオプション」で失敗します。

手元のcronやlaunchdから回す場合、--bareは付けないでおきます。--bareはCLAUDE.mdを読み込まないうえ、サブスクリプションのログインを使わずANTHROPIC_API_KEYが要るためです。この手順はCLAUDE.mdを前提にしているので噛み合いません。claude -pの基本はClaude Code -pモードにまとめています。

関係ありそうな案件は詳細ページで確かめる

メモに載った案件のうち、実際に意見を出す候補は、詳細ページで確かめます。詳細ページには、RSSにない項目が並んでいます。

  • 根拠法令条項(例: 感染症法の第53条の9と第53条の13)
  • 行政手続法に基づく手続かどうか
  • 受付開始日時と受付締切日時
  • 意見提出が30日未満の場合の理由
  • 意見公募要領と命令などの案(確認した案件ではWord形式)
  • 問合せ先

案の本文まで要約させるなら、添付ファイルを取得してテキストにしてから、Claude Codeに読ませます。この段階は、毎週自動で回すより、メモで拾った数件を人が選んで依頼するほうが向いています。全件の添付を毎週取ると、読むべきでない案件の分まで時間とトークンを使います。

依頼の例は次のとおりです。

案件番号 620340014 の意見公募要領と命令などの案を attachments/ に置いた。
改正前後で変わる義務と、施行時期を条番号つきで表にして。
書かれていない点は「記載なし」とする。

読み終えたあと、条番号や施行時期は原文と突き合わせます。要約はCLAUDE.mdで縛っても、条文の取り違えが混ざる余地は残ります。

週1回の定期実行は何で回すか

上のシェルを週に1回動かす方法は、3つあります。

Routines(クラウド)Desktop手元のcron
実行場所Routines(クラウド)クラウドDesktop自分のマシン手元のcron自分のマシン
マシンの電源Routines(クラウド)不要Desktop必要手元のcron必要
最短間隔Routines(クラウド)1時間Desktop1分手元のcronOSの仕様による
ファイルの扱いRoutines(クラウド)毎回リポジトリを新しくクローンDesktopローカルのファイルを使える手元のcronローカルのファイルを使える

/loopは選びません。繰り返しのタスクは作成から7日で期限切れになり、週1回では数回しか動かないためです。

state/seen.jsonをリポジトリに置くなら、Routinesが扱いやすい選択です。ただしクラウドの実行は毎回デフォルトブランチをクローンし、変更はclaude/で始まるブランチに積まれます。既読の記録をデフォルトブランチへ戻さないと、毎週同じ案件が「新着」に出ます。

戻し方は、Routineのプロンプトに書きます。Claudeはclaude/接頭辞のブランチへプッシュするのが既定ですが、プロンプトで別のブランチを指示すればそちらへプッシュします。たとえば、run_weekly.shのあとに次のように続けます。

run_weekly.sh を実行し、成功したら state/seen.json をコミットして
デフォルトブランチ(main)へプッシュする。プッシュが拒否されたら、その旨を報告して止まる。

デフォルトブランチへプッシュできるかどうかは、GitHub側のブランチ保護ルールで決まります。保護を掛けているリポジトリでは拒否されることがあります。この手順でプッシュが通るかは、使うリポジトリの設定ごとに確かめてください。履歴を汚したくなければ、状態だけを置く専用のリポジトリか専用ブランチを用意する形もあります。

どうしても戻せないときは、Desktopか手元のcronに切り替えます。この2つはローカルのstate/seen.jsonをそのまま読み書きできるので、戻す作業そのものが要りません。

Routinesの既定のネットワーク設定(Trusted)では、許可リストにないホストへの通信は403とx-deny-reason: host_not_allowedで失敗します。public-comment.e-gov.go.jpを使うには、環境のネットワークアクセスをCustomにして、許可ドメインに足します。設計の全体はRoutines完全ガイドに、手元で完結させる場合はDesktopのスケジュールタスクにあります。

週次の結果を月曜の朝に読みたければ、日曜の夜に動かします。時刻は、ちょうどの時刻を避けて数分ずらします。

手元のcronなら、crontab -eに1行足すだけです。日曜(0)の21時37分にrun_weekly.shを呼ぶ例です。

37 21 * * 0 cd /path/to/pcm-watch && ./run_weekly.sh

cronはログインシェルの環境変数を引き継がないので、python3とclaudeにはPATHが通っているかを先に確かめます。通っていなければ、run_weekly.shの先頭でPATHを指定するか、コマンドをフルパスで書きます。run_weekly.shにはchmod +xで実行権限も付けます。時刻は端末のタイムゾーンで解釈されるため、日本時間で動かしたいときは、マシン側が日本時間になっているかも見ておきます。macOSでlaunchdを使う場合も、同じ曜日と時刻をStartCalendarIntervalで指定すれば、構成は変わりません。

つまずきやすい点

  • フィードの形式変更: スクリプトは、必須の項目が欠けたときとitemが0件のときに終了コード1で止まります。黙って「該当なし」になるのを避けるためです
  • 締切の時刻: 日付だけでなく時刻も案件ごとに違います。締切当日の午後に気づいた案件は、詳細ページで時刻を見ます
  • 題名の重複: 同じ題名で別案件が並ぶことがあるため、既読の管理は題名でなく案件番号で行います
  • キーワードの粒度: 広い語を入れると週ごとの件数が増え、メモが読まれなくなります。最初は1〜2件に絞り、足りなければ足します
  • 監視リストに当たらない案件: 条件に合わなかった案件は、メモにも出ません。取りこぼしが気になる間は、月に1回だけ一覧ページを目視で見比べます

意見公募の前後の工程

意見公募の段階を追う作業は、法令になってからの追跡と対になります。公布済みの改正の施行日や条文差分は、e-Gov法令APIとClaude Codeの週次追跡で扱っています。パブリックコメントの案は、そこで追う改正の手前にあたります。

意見を出す側ではなく、集まった意見を整理する側の作業は、Claudeでパブリックコメントの意見を整理する手順にあります。

まとめ

パブリックコメントのRSSは、案件番号をlinkのidから、公示日・締切・カテゴリー・所管をdescriptionから取れます。案の本文や根拠条項は詳細ページにあり、RSSからは取れません。

自動化の役割分担は、絞り込みと既読管理をスクリプトに、関係しそうな理由の下書きをClaude Codeに任せる形です。意見を出すかどうかの判断と、案文の読み込みは人が持ちます。週1回の実行は、リポジトリに状態を置くならRoutines、手元で完結させるならDesktopかcronが候補になります。

この記事を共有:XはてブLinkedIn