なるべく短い正規表現で住所を「都道府県/市区町村/それ以降」に分けるエクストリームスポーツ(Pythonで挑戦)

住所データを整形する用事があったので、調べてみたらなるべく短い正規表現で住所を「都道府県/市区町村/それ以降」に分けるエクストリームスポーツという広く参照されている記事があるようだった。もちろんそのコードをまるっといただくのが早いが、正規表現に触れたことがなかったので、いい勉強になると思いPythonで挑戦してみた。

1.テストデータの準備

郵便局のHPから、かの有名(悪名高い)なKEN_ALL.csvをダウンロードしてくればよい。「かの有名」というのはググってみると色々出てくる*1

今回はKEN_ALLの仕様は関係ないので、Pythonで読み込んで都道府県/市区町村/それ以降を結合し、これを再び分割することで適切に処理できているかの答え合わせをする。

import csv, re
with open('KEN_ALL.csv', encoding='sjis') as f:
    reader = csv.reader(f)
    data = [row for row in reader]
address_list = []
for i in range(len(data)):
    address_list += [[data[i][6] + data[i][7] + data[i][8], data[i][6], data[i][7], data[i][8]]]

2.まずは都道府県

都道府県については簡単である。reモジュールの使い方は[Python]正規表現のみで住所を「都道府県/市区町村/その他」に分割する方法を参考にした。正規表現は今回の挑戦を通して基本的な正規表現一覧を死ぬほど見返した。

pattern = '''(...??[都道府県])'''

match_count = 0
for i in range(len(data)):
    result = re.match(pattern, address_list[i][0])
    if  result.group(1) == address_list[i][1]:
        match_count += 1
    else:
        print(address_list[i][1], result.group(1))
print('データ数=', len(data),'マッチ数=', match_count, 'アンマッチ数=', len(data)-match_count, 'マッチ率=', match_count/len(data))

結果:データ数= 124556 マッチ数= 124556 アンマッチ数= 0 マッチ率= 1.0

結果は100%で問題なし。注意点は少し書き方を間違えると京「都」府が「京都」でマッチングするくらい。

3.次は市区町村

同様に市区町村で実験してみる。

単純

まずは普通に市区町村。

pattern = '''(...??[都道府県])(.+?[市区町村])'''

match_count = 0
for i in range(len(data)):
    result = re.match(pattern, address_list[i][0])
    if result.group(2) == address_list[i][2]:
        match_count += 1
    else:
        if i % 50 == 0:
            print(address_list[i][1], address_list[i][2], result.group(2))
print('データ数=', len(data),'マッチ数=', match_count, 'アンマッチ数=', len(data)-match_count, 'マッチ率=', match_count/len(data))

結果:データ数= 124556 マッチ数= 106216 アンマッチ数= 18340 マッチ率= 0.8527569928385625

結構マッチングできないものがある。なおアンマッチのものの一部を抽出しているのは、基本的に隣合ったデータは市区町村以下が微妙に違うだけなので、確認の際に見やすくするため。

~市~区

次はアンマッチのサンプルから~市~区(札幌市中央区など)となっているものが多く見受けられたので、その分を処理する(pattern以外は同じなので略)。

pattern = '''(...??[都道府県])(.+?市.+?区|.+?[市区町村])'''

結果:データ数= 124556 マッチ数= 122151 アンマッチ数= 2405 マッチ率= 0.9806914159093099

マッチ数が急速に上昇した。どうやら政令指定都市のデータが多く拾えたことが理由らしい。考えてみると人口が多いので郵便番号も多いからか。

~郡~町・村/~区/~市

~郡~町・村(西村山郡河北町など)が多く見られたのでその点と、新宿区市谷が新宿区市になるのでその点と、~市は先に拾わないと~村~市(武蔵村山市など)を拾えないので優先対応。

pattern = '''(...??[都道府県])(.+?郡.+?[町村]|.+?市.+?区|.+?[区]|.+?[市]|.+?[町村])'''

データ数= 124556 マッチ数= 123006 アンマッチ数= 1550 マッチ率= 0.9875557981951893

郡が混在するところ(大和郡山市など)と、政令指定都市ではないけれども~区が存在する市(旭川市4区など)でうまく抽出できていないものが残っていた。

~市~区を再考する

ざっくりいけるここからさらに減らしていこうとしたときに、ほかの人の方法では対象市区町村を個別に例外処理することが多いようだった。ただ、そのやり方だと例外に上げている基準がいまいちわかりづらい。

そもそも~市~区をどう処理するかの基準は、政令指定都市は区まで拾い、それ以外は市まで拾うということなので、今後も変化があまりないであろう政令指定都市をベースにした方が理解しやすいのではということで試してみる。

pattern = '''(...??[都道府県])((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|.+?郡.+?[町村]|.+?[市]|.+?[区町村])'''

データ数= 124556 マッチ数= 123836 アンマッチ数= 720 マッチ率= 0.9942194675487331

ビンゴ。結構減った。あとは細かい例外処理*2

市区町村が混ざるものをやっつける

鈴鹿市郡山町/長浜市小谷郡上町/長浜市小谷郡上町/高槻市郡家新町など、市で切れればいいものを拾うために、市が入っていないといけないものだけ例外処理する。また、玉村は佐波郡玉村町という村と町が混在したもの。そして市の前に市が入る野々市市四日市市廿日市市を抽出する。

pattern = '''(...??[都道府県])((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|(?:余市|高市|[^市]+?)郡(?:玉村|.+?)[町村]|[^市]+?[区]|(?:野々市|四日市|廿日市|.+?)[市]|.+?[町村])'''

データ数= 124556 マッチ数= 124397 アンマッチ数= 159 マッチ率= 0.9987234657503452

残りもあと一息になる。

郡が混ざるものをやっつける

これで残りは蒲郡市大和郡山市杵島郡大町町の郡が混在する3つになる。大町町とはややこしい・・・

pattern = '''(...??[都道府県])((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|(?:蒲郡|大和郡山)市|(?:余市|高市|杵島|[^市]+?)郡(?:玉村|大町|.+?)[町村]|[^市]+?[区]|(?:野々市|四日市|廿日市|.+?)[市]|.+?[町村])(.+)'''

データ数= 124556 マッチ数= 124556 アンマッチ数= 0 マッチ率= 1.0

マッチ率100%で完了!

3.挑戦の成果

文字数

先人の成果

(...??[都道府県])((?:旭川|伊達|石狩|盛岡|奥州|田村|南相馬|那須塩原|東村山|武蔵村山|羽村|十日町|上越|富山|野々市|大町|蒲郡|四日市|姫路|大和郡山|廿日市|下松|岩国|田川|大村)市|.+?郡(?:玉村|大町|.+?)[町村]|.+?市.+?区|.+?[市区町村])(.+)

151文字

挑戦の成果

(...??[都道府県])((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|(?:蒲郡|大和郡山)市|(?:余市|高市|杵島|[^市]+?)郡(?:玉村|大町|.+?)[町村]|[^市]+?[区]|(?:野々市|四日市|廿日市|.+?)[市]|.+?[町村])(.+)

183文字

・・・全然だめやん(笑)

再挑戦の成果(最後のあがき)

よく考えたら市での例外処理が減ったので、余市高市をいじらなくてよくなった。

(...??[都道府県])((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|(?:蒲郡|大和郡山)市|.+?郡(?:玉村|大町|.+?)[町村]|[^市]+?[区]|(?:野々市|四日市|廿日市|.+?)[市]|.+?[町村])(.+)

167文字

・・・結局だめやん(笑)

読みやすさ

文字数では全然だめだが、設計の意図は読み取りやすいようになっていると思う。ただ、結局政令指定都市とそれ以外で例外処理を2種類やっているのが長さの要因で、読みやすさと長さはトレードオフになっているということで・・・

4.最後に

そもそも本編のエクストリームはここから先の話なので、私はエクストリームに至るまでに力尽きました。

*1:私も過去に使えないか試してみたが、複数行に分かれているとか以下に掲載がない場合とか飛び地があるとか、尋常じゃなく深い闇だったので諦めたことがある。

*2:なお、?:は括弧内を切り分けるかを決めるもの。入れないものを試してみると意味がわかる。 参考:後方参照が不要なグループ化「(?: )」