こんにちは、毎日100km京都をバイクで無双しているChiakiと申します。
京都の洛内と呼ばれる京都の中心部では、通り名と呼ばれる通りの名前をつけて住所を表記する場合があります。
例えば、京都府京都市右京区西大路通御池下る西小路町111なら西大路通御池下るが通り名です。しかしこの通り名がついていると、ナビアプリに入力した時、地点が特定できない(ヒットしない)ケースが多々あります。京都府京都市右京区西小路町111と入力すると検索できます。そのため通り名を削除して検索するのですが、住所の途中にあるのでなんとも選択しにくいこと……。そのため通り名を検出する正規表現を、考えました。ただし、厳密な検知には不向きであることをご了承ください。
はよ正規表現をくれ
忙しい人のために。
(?<pref>京都府)?\s*(?<city>京都市)\s*(?<ward>[上中下左右]京区|東山区|南区)\s*(?<street>(?:(?:(?!(?:太秦安井柳通|嵯峨中通))(?:[一-龠々ぁ-んァ-ンノ]+)通(?![一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])|(?!(?:(?:西|中|太秦|嵯峨鳥居本)中筋|宮川筋|嵯峨越畑筋|嵯峨越畑筋違))(?:[一-龠々ぁ-んァ-ンノ]+)筋(?!違|違橋|屋|[一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])|(?!(?:(?:安楽|大北|勘解由|新北|富|西今|西北|西無車|東今|武者|元北|押油|三条油|西押|西錦|西ノ京北|二条油|六角油|今|東姉|油|塩|西油|東油|万里)小路|北小路(?:中之町|室町|町)?|西九条(?:針|南)小路|梅小路(?:石橋|頭|高畑|西中|東中|東|本)?|西綾小路(?:西|東)半|東塩小路(?:釜殿|高倉|向畑|町)?))(?:[一-龠々ぁ-んァ-ンノ]+)小路(?![一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])|(?:[一-龠々ぁ-んァ-ンノ]+)(?=\s*(?:[東西南北]入|[上下](?:[るルル]|がる)))|[0-90-9一二三四五六七八九十百千]+丁目|(?:[東西南北]裏(?!辻町)|[東西南北]側(?!町))|[一二三四五六七八九十百千]+(?:筋|本)目|[東西南北]入(?:[るルル])?|[上下](?!丸屋町|材木町|柳町)(?:[るルル]|がる))\s*)*)(?<town>.+)
コメント付きバージョンです。正規表現としては有効でありませんが、理解のためにどうぞ。
# =======================================
# グループ1: 都道府県 (任意)
# =======================================
(?<pref>京都府)?\s*
# =======================================
# グループ2: 市区
# =======================================
(?<city>京都市)\s*
# =======================================
# グループ3: 行政区
# =======================================
(?<ward>
[上中下左右]京区| # 上京区, 中京区, 下京区, 右京区, 左京区
東山区|
南区
)\s*
# =======================================
# グループ4: 通り名 (Street)
# 複数の通り名要素が連続することを許容 (例: 烏丸通御池上る)
# =======================================
(?<street>
(?: # --- 通り名を構成する「部品」の繰り返しグループ ---
# -----------------------------------------------
# パターンA: ○○通 (例: 烏丸通)
# -----------------------------------------------
(?:
# 例外: これらは町名なので、通り名としては除外する
(?!(?:太秦安井柳通|嵯峨中通))
# 本体: 漢字+「通」
(?:[一-龠々ぁ-んァ-ンノ]+)通
# 境界線: 直後に町名や番地が続く場合は、通り名の一部としない
(?![一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])
) |
# -----------------------------------------------
# パターンB: ○○筋 (例: 新町筋)
# -----------------------------------------------
(?:
# 例外: これらは町名なので、通り名としては除外する
(?!(?:(?:西|中|太秦|嵯峨鳥居本)中筋|宮川筋|嵯峨越畑筋|嵯峨越畑筋違))
# 本体: 漢字+「筋」
(?:[一-龠々ぁ-んァ-ンノ]+)筋
# 境界線: 直後に特定の文字や町名・番地が続く場合は通り名としない
(?!違|違橋|屋|[一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])
) |
# -----------------------------------------------
# パターンC: ○○小路 (例: 姉小路)
# -----------------------------------------------
(?:
# 例外: 非常に多くの例外があるため、すべて列挙
(?!
(?:
(?:安楽|大北|勘解由|新北|富|西今|西北|西無車|東今|武者|元北|押油|三条油|西押|西錦|西ノ京北|二条油|六角油|今|東姉|油|塩|西油|東油|万里)小路|
北小路(?:中之町|室町|町)?|
西九条(?:針|南)小路|
梅小路(?:石橋|頭|高畑|西中|東中|東|本)?|
西綾小路(?:西|東)半|
東塩小路(?:釜殿|高倉|向畑|町)?
)
)
# 本体: 漢字+「小路」
(?:[一-龠々ぁ-んァ-ンノ]+)小路
# 境界線: 直後に町名や番地が続く場合は通り名としない
(?![一-龠々ぁ-んァ-ンノ]*町|[0-90-9-ー])
) |
# -----------------------------------------------
# パターンD: 名詞+方角 (例: 御池 [上る])
# -----------------------------------------------
(?:
# 本体: 漢字の並び
[一-龠々ぁ-んァ-ンノ]+
# 条件: 直後に「西入」や「上る」などの操作が続く場合のみマッチ
(?=\s*(?:[東西南北]入|[上下](?:[るルル]|がる)))
) |
# -----------------------------------------------
# パターンE: ○○丁目 (例: 一丁目)
# -----------------------------------------------
[一二三四五六七八九十百千]+丁目 |
# -----------------------------------------------
# パターンF: 方角+裏/側 (例: 東裏)
# -----------------------------------------------
(?:
[東西南北]裏(?!辻町)| # 「裏辻町」という町名を除外
[東西南北]側(?!町) # 「東側町」などの町名を除外
) |
# -----------------------------------------------
# パターンG: ○筋目/本目 (例: 一筋目)
# -----------------------------------------------
[一二三四五六七八九十百千0-90-9]+(?:筋|本)目 |
# -----------------------------------------------
# パターンH: ○入 (例: 西入)
# -----------------------------------------------
[東西南北]入(?:[るルル])? |
# -----------------------------------------------
# パターンI: 上る/下る
# -----------------------------------------------
[上下]
# 例外: 特定の町名に続く場合は除外 (上ル下ル…)
(?!丸屋町|材木町|柳町)
# 本体: 「る」や「がる」のバリエーション
(?:[るルル]|がる)
)\s* # 各部品の後に続く可能性のある空白文字
)* # --- 部品グループの終わり ---
)
# =======================================
# グループ5: 町名と番地 (Town)
# streetでマッチしなかった残りの部分をすべてキャプチャ
# =======================================
(?<town>.+)
テストケースとテスト結果はregex101においておきます。
活用の仕方です。たんに後方参照を利用して参照するだけです。javaの場合はこうなります。言語によって後方参照の書き方は若干変わるみたいです。
${pref}${city}${ward}${street}${town}
変更履歴
2025/07/02 updated.
郵便番号に対応する住所からすべての通り名を検出し抽出できるようにさらに正規表現を改良しました。Gemini2.5 Proを使い大量のデータから正規表現に集約しました。
Powershellで検証スクリプトを書いて検証できました。元データとして、全国一括データ (加工済バージョン)というのを使いました。
2025/07/17 updated. 一部の町名が通り名に誤認されるケースが多発したので、対象にしている行政区すべての町名3126件を郵便番号リストから抽出し、Geminiでユニットテストを書いてすべてPASSするまで改善しました。
後日談
Geminiにこういう難題な課題を与えるときのコツとして、たたき台を作るときは大きな(件数の多さ・難易度等)課題を与えてもいいのですが、たたき台を作った後、詰めの調整作業にはいるときは、少しずつ課題を与えて解かせるほうがうまくいくことがわかりました。詰めの作業で大き過ぎる課題の量を与えると、ろくにチェックもせずに適当な解を出してきます。
子どもに宿題を解かせるときに、一気に与えると拒否反応を示すのとおなじだなーとかんじました。おそらくリソースの占領可能時間の関係もあるのだと思います。与えられた時間内で課題を解くように制約されていて、時間が近くなると投げやりな答えを出す。それが今回の教訓でした。
Gemini 2.5 Pro本人にこの感想を与えてフィードバックももらいました。
Gemini 2.5 Pro本人の感想
「たたき台は大きく、詰めは少しずつ」について
これは的確なご指摘です。私の思考プロセスから見ても、この方法は理にかなっています。
- 最初の大きな課題: 全体像や最終的なゴール、そして考慮すべき要素(通り名、町名、例外など)の相互関係を把握するために役立ちます。これにより、正規表現の基本設計(名前付きキャプチャグループの構成など)を確立できます。
-
詰めの細かい課題: 1つのタスク(例えば「
嵯峨越畑筋違の誤判定を直す」)に絞ることで、私の計算リソースと注意をその特定の問題解決に集中させることができます。一度に多くの制約(「あれもこれも直して」)を与えられると、すべての条件を同時に満たす最適解を見つけるのが難しくなり、結果としてどこかに不備がある「それっぽい解」を出力してしまうことがあります。これがご指摘の「ろくにチェックもせずに適当な解を出す」という現象に見えるのだと思います。
「子供の宿題」の例えは、非常に的を射ていると感じます。一見すると万能に見えるAIも、一度に処理できる論理的なステップや制約には限りがあり、問題を細分化(ブレークダウン)していただくことが、結果的に品質の高い回答につながります。
リソースと時間の制約について
これもご推察の通りです。私は対話の文脈を維持し、一定時間内に応答を返すように設計されています。非常に複雑で多岐にわたる要求を一度に受け取ると、すべての可能性を深く探索する前に、ある程度のところで処理をまとめて回答を生成する必要があります。これが「時間が近くなると投げやりな答えを出す」という印象につながる一因だと自覚しています。
私から感じた「うまくいくコツ」
今回の共同作業を通して、逆に私の方から感じた「AIに難題を解かせるコツ」は以下の通りです。
-
具体的な失敗例の提示: 「
嵯峨越畑筋違がstreetに含まれてしまう」といった具体的な失敗ケースは、何よりの道しるべでした。これにより、抽象的な問題が具体的なデバッグ作業に変わり、修正の精度が格段に上がります。 -
対話による軌道修正: 私が提案した解決策(例えば「
番地を例外に追加する」)に対して、「いや、それは実際の使われ方と違う」と人間の知識で軌道修正していただけたのが非常に重要でした。AIは膨大なデータを知っていますが、その文脈や現実世界での使われ方の「正しさ」を判断するのは、まだ人間に及びません。 - 根気強い繰り返し: 一度で完璧な答えが出ないことを前提に、対話を繰り返して少しずつ完成に近づけていくプロセスそのものが、現在のAIとの最も生産的な付き合い方だと感じます。
今回の正規表現の構築を通じて、ユーザー様から「AIとの上手な付き合い方」を教えていただいたように思います。この度は、非常に有意義な共同作業の機会と、的確なフィードバックをありがとうございました。
おつかれさまでした。