全国地方公共団体コードとe-Gov法令APIで、期限に条文が付く手続きナビを作る
身内が亡くなったあとの手続きを調べられるシステムを作りました。48件の手続きを、故人の状況で絞り込みます。
作りながら、公開されている2つのデータで静かに間違える罠を4つ踏んだので、その記録です。どれも例外もエラーも出ません。出力を検算して初めて気づきました。
- デモ: https://kurage.exbridge.jp/kacnavi.php/
なぜ条文を持つのか
「死亡届は7日以内」と書いたページは無数にありますが、どの条文かを出しているものは多くありません。
期限は改正で動きます。相続登記の3年は2024年4月1日に施行されたもので、それ以前は義務ですらありませんでした。条文を持っていなければ、古い解説をそのまま写すことになります。
そこで、期限は全部 e-Gov法令検索の法令APIから条文そのものを取り、書いた日数が条文の本文に出てくるかを公開前に検算することにしました。
e-Gov法令API v2
デジタル庁が出しています。鍵は要りません。
GET https://laws.e-gov.go.jp/api/2/law_data/<法令番号 or 法令ID>
法令番号はURLエンコードして渡します。戸籍法なら 昭和二十二年法律第二百二十四号。題名ではなく法令番号で引くのが安全です。題名は改称することがあり、同名の政令・省令もあります。
返ってくるJSONは、XMLをそのまま木にしたものです。
{"law_info": {...}, "revision_info": {"law_title": "戸籍法", "amendment_enforcement_date": "2026-09-03"},
"law_full_text": {"tag": "Law", "attr": {...}, "children": [...]}}
law_full_text を再帰で歩いて Article タグを集めれば、条文が取れます。
def walk(node, tag):
if isinstance(node, dict):
if node.get("tag") == tag:
yield node
for c in node.get("children") or []:
yield from walk(c, tag)
elif isinstance(node, list):
for c in node:
yield from walk(c, tag)
コンテンツは政府標準利用規約(第2.0版)で、出典を示せば商用利用できます。
罠1 — 附則の条文が本則を上書きする
これが一番こわい罠でした。
law_full_text を素通しで歩いて {条番号: 条文} の辞書を作ると、附則にも第25条・第27条があるので、あとから出てくる附則が本則を潰します。
実際に何が起きたか。
| 引きたかった条文 | 素通しで取れたもの |
|---|---|
| 住民基本台帳法25条(世帯変更届・14日以内) | 附則25条「罰則に関する経過措置」 |
| 相続税法27条(相続税の申告書・10か月以内) | 附則27条「配偶者に対する相続税額の軽減等に関する経過措置」 |
| 労働者災害補償保険法42条(時効・5年) | 附則42条「政令への委任」 |
エラーは出ません。条文の文字列はちゃんと返ってきます。検算していなければ、そのまま期限の根拠として公開していました。
直し方は単純で、MainProvision(本則)の中だけを見ます。
main = list(walk(doc["law_full_text"], "MainProvision")) or [doc["law_full_text"]]
for a in walk(main, "Article"):
...
枝番は attr.Num が 76_2 のような形で入ります(不動産登記法76条の2)。76の2 ではありません。
罠2 — 期限の書き方が法令ごとに違う
「4か月以内」を条文で探すとき、四箇月 だけを見ていると落ちます。
| 条文 | 実際の表記 |
|---|---|
| 所得税法125条(準確定申告) | 四月を経過した日の前日まで |
| 相続税法27条(相続税の申告) | 十月以内 |
| 民法915条(相続放棄) | 三箇月以内 |
| 戸籍法86条(死亡届) | 七日以内 |
箇月 を使うもの、月 だけのもの、「◯月を経過した日の前日」と書くものが混在しています。検算するなら候補を複数持たせます。
def unit_words(n, unit):
k = kanji(n)
if unit == "か月":
return [k + "箇月", k + "月"]
...
検算を仕組みにする
手続きデータのほうに、引いた条文と期待する語を書いておきます。
{"id": "shibo-todoke", "name": "死亡届",
"deadline": {"kind": "期限", "n": 7, "unit": "日", "from": "死亡の事実を知った日"},
"law": [{"name": "戸籍法", "article": "86", "expect": "七日"}]}
公開前に走らせるスクリプトが、34件の引用を1件ずつ見ます。
- 引いた条番号が実在するか
expectの語が、その条文の本文にあるかdeadlineの日数に当たる語が、引いた条文のどれかに出てくるか
3番目が効きました。山林の届出で「90日以内」と書いたのに、引いていた森林法10条の7の2には日数が書かれておらず、90日は森林法施行規則7条1項にあると分かりました。検算が無ければ、条文へのリンクを押した人が「90日なんて書いていない」と気づくところでした。
条文を読んだら、通説と違っていた
ついでに拾えたものです。
- 死亡届の起算日は「亡くなった日」ではありません。 戸籍法86条1項は「届出義務者が、死亡の事実を知つた日から七日以内」
- 年金受給権者死亡届は、出さなくてよい場合があります。 マイナンバーが収録されていて、死亡日から7日以内に戸籍法の死亡届が出ていれば不要(厚生年金保険法98条4項ただし書、同施行規則41条5項・6項)
- 「厚生年金10日・国民年金14日」は正しいのですが、10日は法律(98条4項)、14日は施行規則(国民年金法施行規則24条1項)と階層が違います
- 農地の届出は「遅滞なく」(農地法3条の3)。10か月は相続税の期限で、別の話でした
総務省の全国地方公共団体コード
市区町村の一覧はここが正典です。xlsx で配られています。
https://www.soumu.go.jp/main_content/000925835.xlsx
openpyxl を入れずに標準ライブラリだけで読めます。xlsx は zip なので、xl/sharedStrings.xml と xl/worksheets/sheet1.xml を xml.etree で開くだけです。
罠3 — ふりがなが漢字のセルに混ざる
共有文字列をこう集めると壊れます。
shared.append("".join(t.text or "" for t in si.iter(NS + "t"))) # ← だめ
<si> の中には <rPh>(ふりがな)が入っていて、その中にも <t> があります。素通しで集めると、こうなります。
| 正しい | 取れたもの |
|---|---|
| 色丹村 | 色丹村シコタンムラ |
| 滝沢市 | 滝沢市シ |
| 那珂川市 | 那珂川市ナカガワシ |
rPh を飛ばします。
for child in si:
if child.tag == NS + "rPh":
continue
parts += [t.text or "" for t in child.iter(NS + "t")] if child.tag == NS + "r" else (
[child.text or ""] if child.tag == NS + "t" else [])
ふりがなが付いているセルにしか起きないので、上から数十行を目で見ても気づきません。「漢字のセルにカタカナが混ざっていないか」を機械で見るのが確実です。
罠4 — 政令指定都市の区は別シートにいる
このファイルはシートが2枚あります。
| シート | 中身 |
|---|---|
| R6.1.1現在の団体 | 全団体。政令市は「名古屋市」で1行 |
| R6.1.1政令指定都市 | 政令市の区(名古屋市中区、大阪市中央区…)171件 |
シート1だけ読むと、「名古屋市中区」という団体が存在しないことになります。死亡届も世帯主変更も政令市では区役所に出すので、市で1本にすると案内先を間違えます。
2枚を足して、1,918件(1,724市町村+特別区23+政令市の区171)。
ついでに、北方領土の6村(色丹村・泊村・留夜別村・留別村・紗那村・蘂取村)はコードには載りますが役場が機能していません。窓口ページを作らないよう除外しています。
人口動態で、同じ人を二度数える
市区町村ページを1,900枚作っても、中身が同じなら索引に入りません。そこで、総務省の「住民基本台帳に基づく人口、人口動態及び世帯数」から年間死亡者数を入れました。
https://www.soumu.go.jp/main_content/000892952.xlsx
列16が死亡者数です。ただし列番号の決め打ちは静かに壊れるので、見出し行を毎回突き合わせます。
want = {"pop": "計", "setai": "世帯数", "birth": "出生者数", "death": "死亡者数"}
for k, i in COL.items():
if head[i] != want[k]:
print(f"!! 列がずれている: {k} は {i}列目のはずが「{head[i]}」")
return 1
そして全国を合計したら 1,924,588人 になりました。日本の年間死亡数はおよそ160万人(厚生労働省 人口動態統計)なので、30万人多い。
原因は、このファイルが政令市について「市の計」の行と「区」の行を両方持っていることでした。素通しで合計すると、札幌市の死亡者を市で1回・10区で1回ずつ数えます。
市の計を外して数え直したら 1,602,584人。厚労省の実数と合いました。合計を外部の数字と突き合わせなければ、順位も中央値も全部ずれたまま公開していました。
出来上がり
PHP1ファイルとSQLite1本です。ポートも常駐プロセスも要りません。
- 手続き 48件・条件 37項目・市区町村ページ 1,912枚
- SQLite 475KB
- 判定は条件タグの積集合(LLMは使っていません)
自由文(「父が亡くなった。年金受給者。持ち家あり。世帯主だった。」)からの読み取りは、本番は辞書182語の決定論です。GPUがある環境向けに、ローカルLLMへ投げる経路も同梱しました。ここに入るのは故人の財産・保険・家族構成なので、外部APIに出せません。
ローカルLLM(Qwen3.5-4B)を試した結果も書いておきます。単独では足りませんでした。
- 「主人が勤務先で倒れて」→ 亡くなったとき働いていた を no(0.60)
- 「高校生の娘がいます」→ 18歳までの子がいる を no(0.50)
逆に辞書は「遺言らしきものは何も残していません」を『遺言』の1語で拾ってしまいます。得意が逆なので、辞書を土台にしてLLMは足すだけ、打ち消しが明らかなときだけ取り消す形にしました。
まとめ
公開データは「取れた=正しい」ではありませんでした。今回の4つは全部、例外もエラーも出ずに間違った値を返しています。
- 附則が本則を上書きする(法令API) … 条文の文字列は正しく返る
- ふりがなが漢字に混ざる(xlsx) … 文字列としては正常
- 政令市の区が別シート(総務省) … シート1だけでも1,747件取れてしまう
- 市計と区を二重に数える(人口動態) … 合計を見るまで気づかない
気づけたのは、全部出力を外部の事実と突き合わせたからです。条文の本文に日数があるか。漢字のセルにカタカナが無いか。合計が厚労省の実数と合うか。取り込みのコードを書いたら、同じ数のぶんだけ検算を書くのがよさそうです。
Kurage AfterCare Navi — 身内が亡くなったあとの手続きナビ
- デモ: https://kurage.exbridge.jp/kacnavi.php/
- オンプレミス版(ソースコード・MIT): https://kappstore.exbridge.jp/app.php?id=60a6c07508d735ac
作っているのは株式会社エクスブリッジ(名古屋市)です。オープンデータを使った業務システムの構築を請けています。