- 행정구역 표 = 시도 17(짧은 이름) + 전국 · 시도별 시군구 229(자치 227 + 제주 행정시 2 · 2026-07-01 인천 제물포 · 영종 · 검단 반영) · 옛 이름(강원도 · 전라북도 · 청원 · 마산 · 진해 · 연기 · 인천 남구 · 중구 · 동구 · 경북 군위) · 별칭(영종도 · 용유도 · 세종시 · 제주도) - 풀이 master_region.split — 시도만 = 시군구 「전체」 · 시군구만 = 시도 찾음 · 읍면 = 시군구(읍면) · 여러 시도 = 쉼표 · 제외 조건 = 끝에 「 제외」 · 조합 이름 = 비고 · 잘린 끝 낱말 = 하나뿐인 시군구(동두 → 동두천 · 금 → 금천) - 지역 글 242 종 모두 풀림(못 푼 것 0) · 옛 이름 바꾼 줄 648 · 줄 · 키 · 값 그대로(24,027 · 비고만 조합 121 줄) - 쓰는 곳: 지역 글 = material_merge.where(시도 + 시군구) — M01 찾기(label · 검색어 · 자재지역) · 표 찾기 · 후보 글 · 불러오기(M02_Mat_Place · Hopyo_Match · Hopyo_Material — 가져온 지역 낱말도 짧은 이름으로 · 시군구가 있으면 그것으로 맞댐) - M01 화면: 시도 = 고르기(목록 밖 값 「대전,충남」 은 끝에) · 시군구 = 글 · 새 줄 - · - - 로직 1,520 시험 계산 전후 다름 0 · check_master 틀 · 본문 · 로직 · 조합 · 목차 0 · 단위 27 · 배 7(전과 같음) - 빌더 adopt 가 로직 장 파일 「차례」 를 하나씩 미는 것은 이번 일 밖 — 되돌림(로직 파일 손대지 않음) - 새 지킴 test_830_11_material_sido · 낡은 시험 고침: test_830_10 · test_830_2(열 · 지역 기대를 두 칸으로 · 830-11 새 겹침 38 묶음 = 청원 → 청주 4 · 조합 비고 34) · test_m02_hopyo_match(강원도(강릉시) → 강원 강릉) - 틀 문서 _틀.md 11 절 행정구역 표 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
223 lines
8.6 KiB
Python
223 lines
8.6 KiB
Python
"""지역 글 → 시도 · 시군구 두 칸(요구 842 · PLAN 830-11) — 표 = `_행정구역.json` · 시험 = test_830_11_material_sido.
|
||
|
||
`split(글)` → (시도, 시군구, 조합 글 | None)
|
||
- 없음(-) = (-, -) · 전국 = (전국, 전체) · 시도만 = (시도, 전체) · 「전지역」 은 전체
|
||
- 시군구만 = 표로 시도를 찾음 · 시도 + 시군구 = 둘 다 · 여러 시군구 · 시도 = 쉼표(적힌 차례)
|
||
- 읍면 = 「시군구(읍면)」 꼴 · 「… 제외」 = 시군구 칸 끝에 「 제외」
|
||
- 옛 이름 · 긴 이름 · 「…지역」 = 지금 짧은 이름 · 조합 이름(「…(협)」) = 셋째(비고 몫)
|
||
- 30 자 잘린 끝 낱말 = 같은 시도 안 앞글자가 하나뿐인 시군구로 채움
|
||
- 못 풀면 ValueError(빌더가 멈춤).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
|
||
DB = Path(__file__).resolve().parents[1] / "_행정구역.json"
|
||
NONE = "-"
|
||
NATION = "전국"
|
||
WHOLE = "전체"
|
||
EXCEPT = "제외"
|
||
_UNION = re.compile(r",?\s*[가-힣]+\(협\)")
|
||
_CUT = ",.()및 "
|
||
|
||
|
||
def short(name: str) -> str:
|
||
"""짧은 이름 — 끝 시 · 군 · 구를 뗌(남는 글이 두 자 넘을 때만) · 「송파구」 → 송파 · 「중구」 그대로."""
|
||
return name[:-1] if len(name) > 2 and name[-1] in "시군구" else name
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def table() -> dict:
|
||
"""{시도: [시군구 짧은 이름]} · 낱말 사전 {글: [뜻]} · 옛 이름 글 묶음."""
|
||
db = json.loads(DB.read_text(encoding="utf-8"))
|
||
sido = {s["이름"]: [short(g) for g in s["시군구"]] for s in db["시도"]}
|
||
words: dict[str, list[tuple]] = {} # 뜻 끝 「옛」 = 옛 이름으로 풂
|
||
|
||
def put(text: str, meaning: tuple) -> None:
|
||
words.setdefault(text, [])
|
||
if meaning not in words[text]:
|
||
words[text].append(meaning)
|
||
|
||
for s in db["시도"]:
|
||
for text in (s["이름"], s["정식"], s["정식"].removesuffix("도")):
|
||
put(text, ("시도", s["이름"]))
|
||
for g in s["시군구"]:
|
||
for text in {g, short(g)}:
|
||
put(text, ("시군구", s["이름"], short(g)))
|
||
put(NATION, ("시도", NATION))
|
||
for text in ("전지역", WHOLE):
|
||
put(text, ("전체",))
|
||
put(EXCEPT, ("제외",))
|
||
for kind, book in (("옛", db["옛이름"]), ("별", db["별칭"])):
|
||
for src, dst in book.items():
|
||
to, src_name = dst.split(), src.split()[-1]
|
||
mark = ("옛",) if kind == "옛" and short(src_name) != short(to[-1]) else ()
|
||
meaning = ("시도", to[0]) if len(to) == 1 else ("시군구", to[0], short(to[1]))
|
||
for text in {src_name, short(src_name)}:
|
||
put(text, meaning + mark)
|
||
return {"시도": sido, "낱말": words}
|
||
|
||
|
||
def _tokens(text: str) -> list[tuple]:
|
||
"""(종류, 뜻 | 글) 줄 — 사전 낱말은 가장 긴 것 · 시군구 뒤 괄호 · 띄움 글 = 읍면(그대로 글)."""
|
||
words = table()["낱말"]
|
||
out, i, n = [], 0, len(text)
|
||
longest = max(map(len, words))
|
||
while i < n:
|
||
ch = text[i]
|
||
if ch in "()":
|
||
out.append((ch, None))
|
||
i += 1
|
||
continue
|
||
if ch in ",.및 ":
|
||
out.append(("띄움" if ch == " " else "끊음", None))
|
||
i += 1
|
||
continue
|
||
hit = next(
|
||
(
|
||
text[i : i + k]
|
||
for k in range(min(longest, n - i), 0, -1)
|
||
if text[i : i + k] in words
|
||
),
|
||
None,
|
||
)
|
||
if hit and text[i + len(hit) :].startswith("지역"): # 충남지역 · 제주지역
|
||
out.append(("낱말", hit, words[hit]))
|
||
i += len(hit) + 2
|
||
continue
|
||
if hit:
|
||
out.append(("낱말", hit, words[hit]))
|
||
i += len(hit)
|
||
if words[hit][0][0] == "시군구" and i < n:
|
||
emd = _emd(text, i)
|
||
if emd:
|
||
out.append(("읍면", emd[0]))
|
||
i = emd[1]
|
||
continue
|
||
j = i
|
||
while j < n and text[j] not in _CUT:
|
||
j += 1
|
||
out.append(("글", text[i:j]))
|
||
i = j
|
||
return out
|
||
|
||
|
||
def _emd(text: str, i: int) -> tuple[str, int] | None:
|
||
"""시군구 바로 뒤 읍면 글 — 「(미원면 제외)」 · 「 내면」 → (글, 다음 자리) · 아니면 None."""
|
||
words = table()["낱말"]
|
||
if text[i] == "(":
|
||
depth, j = 0, i
|
||
while j < len(text):
|
||
depth += {"(": 1, ")": -1}.get(text[j], 0)
|
||
if depth == 0:
|
||
break
|
||
j += 1
|
||
return text[i + 1 : j], j + 1
|
||
if text[i] == " ":
|
||
j = i + 1
|
||
while j < len(text) and text[j] not in ",)":
|
||
j += 1
|
||
word = text[i + 1 : j].strip()
|
||
if word and word not in words and not any(w in words for w in word.split()):
|
||
return word, j
|
||
return None
|
||
|
||
|
||
def split(text) -> tuple[str, str, str | None]:
|
||
"""지역 글 → (시도, 시군구, 조합 글 | None) — 맨 위 설명."""
|
||
return _parse(text)[:3]
|
||
|
||
|
||
def is_old(text) -> bool:
|
||
"""지역 글을 옛 이름(개편 전 이름)으로 풀었나 — 보고 셈용."""
|
||
return _parse(text)[3]
|
||
|
||
|
||
def _parse(text) -> tuple[str, str, str | None, bool]:
|
||
raw = str(text or "").strip()
|
||
if raw in ("", NONE):
|
||
return NONE, NONE, None, False
|
||
unions = [u.lstrip(", ") for u in _UNION.findall(raw)]
|
||
body = _UNION.sub("", raw).strip(", ")
|
||
sidos: list[str] = []
|
||
towns: list[tuple[str, str]] = [] # (시도, 짧은 이름(읍면))
|
||
cur, cut, old, i = None, False, False, 0
|
||
tokens = _tokens(body)
|
||
while i < len(tokens):
|
||
tok = tokens[i]
|
||
if tok[0] == "낱말":
|
||
meanings = tok[2]
|
||
kinds = {m[0] for m in meanings}
|
||
if kinds == {"전체"}:
|
||
pass
|
||
elif kinds == {"제외"}:
|
||
cut = True
|
||
elif "시도" in kinds and (
|
||
cur is None or "시군구" not in kinds or not _inside(tokens, i)
|
||
):
|
||
got = next(m for m in meanings if m[0] == "시도")
|
||
cur, old = got[1], old or got[-1] == "옛"
|
||
if cur not in sidos:
|
||
sidos.append(cur)
|
||
else:
|
||
got = _town(meanings, cur, sidos, raw)
|
||
towns.append(got[1:3])
|
||
old = old or got[-1] == "옛"
|
||
elif tok[0] == "읍면" and towns:
|
||
s, g = towns[-1]
|
||
towns[-1] = (s, f"{g}({tok[1]})")
|
||
elif tok[0] == "글":
|
||
if i != len(tokens) - 1 or body.count("(") <= body.count(")"):
|
||
raise ValueError(f"지역 글 못 풂 — 「{tok[1]}」 ({raw})")
|
||
towns.append(_complete(tok[1], cur, sidos, raw))
|
||
i += 1
|
||
for s, _ in towns:
|
||
if s not in sidos:
|
||
sidos.append(s)
|
||
if not sidos:
|
||
raise ValueError(f"지역 글 못 풂 — 시도 없음 ({raw})")
|
||
names = list(dict.fromkeys(g for _, g in towns))
|
||
sgg = ",".join(names) + (f" {EXCEPT}" if cut else "") if names else WHOLE
|
||
sido = ",".join(sidos)
|
||
return sido, (WHOLE if sido == NATION else sgg), " · ".join(unions) or None, old
|
||
|
||
|
||
def _inside(tokens: list[tuple], i: int) -> bool:
|
||
"""i 번 낱말이 괄호 안인지."""
|
||
depth = 0
|
||
for t in tokens[:i]:
|
||
depth += {"(": 1, ")": -1}.get(t[0], 0)
|
||
return depth > 0
|
||
|
||
|
||
def _town(meanings: list[tuple], cur, sidos: list[str], raw: str) -> tuple:
|
||
"""시군구 낱말 → 뜻(「시군구」, 시도, 짧은 이름[, 「옛」]) — 지금 시도 › 적힌 시도 › 전국에서 하나뿐인 것."""
|
||
towns = [m for m in meanings if m[0] == "시군구"]
|
||
for pool in ([cur], sidos, None):
|
||
got = [m for m in towns if pool is None or m[1] in pool]
|
||
if len({(m[1], m[2]) for m in got}) == 1:
|
||
return got[0]
|
||
raise ValueError(f"시군구 소속 못 정함 — {[m[1:] for m in towns]} ({raw})")
|
||
|
||
|
||
def _complete(head: str, cur, sidos: list[str], raw: str) -> tuple[str, str]:
|
||
"""잘린 끝 낱말 → 앞글자가 하나뿐인 시군구(지금 시도 › 적힌 시도 › 전국)."""
|
||
sido = table()["시도"]
|
||
for pool in ([cur] if cur else [], sidos, list(sido)):
|
||
got = [(s, g) for s in pool for g in sido.get(s, []) if g.startswith(head)]
|
||
if len(got) == 1:
|
||
return got[0]
|
||
raise ValueError(f"잘린 지역 글 못 풂 — 「{head}」 ({raw})")
|
||
|
||
|
||
def valid_sido(value) -> bool:
|
||
"""시도 칸 값 — 「-」 · 「전국」 · 시도 짧은 이름(쉼표로 여럿)."""
|
||
if value in (NONE, NATION):
|
||
return True
|
||
parts = str(value or "").split(",")
|
||
return all(p in table()["시도"] for p in parts) and len(set(parts)) == len(parts)
|