郵便局のutf_ken_all.csvの「都道府県名、市区町村名、町域名」をつなげた住所を元の列に分解する正規表現の紹介です。
下記のようにすると100%分解できました(2026-06-30現在)。
CITY = ( # 市名が「区」で終われない等の例外
"旭川市|伊達市|田村市|南相馬市|上越市|富山市|"
"東村山市|武蔵村山市|羽村市|十日町市|那須塩原市|"
"姫路市|大町市|大村市|野々市市|四日市市|廿日市市"
)
GUN = ( # 郡部の例外
"芳賀郡市貝町|佐波郡玉村町|中新川郡上市町|西八代郡市川三郷町|"
"神崎郡市川町|柴田郡村田町|田村郡..町|吉野郡下市町|杵島郡大町町|"
"余市郡.{2,3}[町村]|高市郡.{2,3}[町村]|[東西北]村山郡.{2,3}町"
)
pattern = (
"^(東京都|北海道|(?:京都|大阪)府|.{2,3}県)" # 都道府県名
f"({CITY}|{GUN}|.+?市.+?区|.+?[市区町村])" # 市区町村名
"(.+)$" # 町域名
)
市区町村名の変更があっても修正すれば対応できそうです。
確認方法
郵便番号データダウンロードから、utf_ken_all.csvをダウンロードしてください。
下記をmain.pyで保存して、uv run main.pyを実行してください。一致率が表示されます。
# /// script
# dependencies = ["polars==1.42.0"]
# ///
import polars as pl
CITY = ( # 市名が「区」で終われない等の例外
"旭川市|伊達市|田村市|南相馬市|上越市|富山市|"
"東村山市|武蔵村山市|羽村市|十日町市|那須塩原市|"
"姫路市|大町市|大村市|野々市市|四日市市|廿日市市"
)
GUN = ( # 郡部の例外
"芳賀郡市貝町|佐波郡玉村町|中新川郡上市町|西八代郡市川三郷町|"
"神崎郡市川町|柴田郡村田町|田村郡..町|吉野郡下市町|杵島郡大町町|"
"余市郡.{2,3}[町村]|高市郡.{2,3}[町村]|[東西北]村山郡.{2,3}町"
)
pattern = (
"^(東京都|北海道|(?:京都|大阪)府|.{2,3}県)" # 都道府県名
f"({CITY}|{GUN}|.+?市.+?区|.+?[市区町村])" # 市区町村名
"(.+)$" # 町域名
)
df = pl.read_csv("utf_ken_all.csv", has_header=False)[:, 6:9]
df.columns = ["Prefecture", "City", "Town"]
df = (
df.filter(pl.col("Town") != "以下に掲載がない場合")
.with_columns(pl.col("Town").str.split("(").list[0])
.with_columns(
Address=pl.col("Prefecture") + pl.col("City") + pl.col("Town")
)
.unique(pl.col("Address"), maintain_order=True)
)
df_fail = df.with_columns(
pl.col("Address").str.extract_groups(pattern).struct.unnest()
)
assert df_fail.filter(pl.any_horizontal(pl.all().is_null())).height == 0
df_fail = df_fail.filter(
(pl.col("Prefecture") != pl.col("1"))
| (pl.col("City") != pl.col("2"))
| (pl.col("Town") != pl.col("3"))
)
print(f"一致率 = {1 - df_fail.height / df.height:.4%}")
参考
以上