업무 중, 가입 회원들의 주소지 분포를 알아야 할 일이 생겼다.
모든 주소지에 대한 위/경도를 얻은 후, 점지도(dot map) 또는 열지도(heat map)을 찍을 수 있다.
하지만, 신규 회원이 생길 때마다 해당 주소지의 위/경도 값을 구하여 저장하지 않는다면 주기적으로 작업을 통해 데이터를 갱신해야 한다.
당장 회원 정보에 위/경도 칼럼을 둘 계획이 없었기 때문에 전국 우편번호 별 대표 위/경도가 있었으면 좋겠다고 생각했다.
회원 정보에는 주소지와 우편번호 값을 나타내는 칼럼을 이미 가지고 있었기 때문이다.
우편번호 별 위/경도 값을 갖는 테이블을 유지한다면, 가입 주소지 분포 지도를 구하기 수월할 것으로 생각했다.
구글링을 해 봐도 마음에 드는 데이터를 찾지는 못했다.
위/경도는 점(x, y)으로 표현되는데, 우편번호는 면(x1, x2, y1, y2)으로 표현된다.
우편번호를 대표하는 위/경도가 뭐냐고 묻는다면, 애매하다.
효율적으로 일하기 위해서, 특정 우편번호를 대표하는 위/경도를 찾고자 고민하지 않고 우편번호에 속한 여러 지번주소 중 하나를 택하여 표시하는 것으로 결정했다.
1) 우편번호 데이터 구하기
1) URL 위치 : https://www.epost.go.kr/search/zipcode/areacdAddressDown.jsp
2) 다운로드 파일 중, 아래 파일을 사용했음
2) 실행 환경 및 소스
- 읽을 파일 (20221006_지번 범위.txt) 위치와 결과 파일 위치는 환경에 맞게 수정 필요
- python 3.9.9 사용
- mac OS에서 실행
- 네이버 MAP 라이브러리를 사용했음
''' zip code (우편번호) 별, 위/경도 구합니다. 1) 우편번호 데이터를 다운 받습니다. (https://www.epost.go.kr/search/zipcode/areacdAddressDown.jsp) 활용 파일 - 20221006_지번범위.txt 2) 지번범위.txt 파일을 파싱하여, 위경도 조회를 시도합니다. 2-1) 우편번호별, 1개의 주소를 취합니다. 2-2) 해당 주소의 위/경도 조회를 하여 값이 나오면, 해당 우편번호의 대표 위/경도로 설정합니다. 해당 주소의 위/경도 조회가 되지 않는 경우, 동일 우편번호에 대한 다음 주소에 대해 위/경도 조회를 시도하여 설정합니다. 2-3) 주소는, 시도 + 시군구 + 읍면동 + 리명 또는 행정동 + 산여부 값이 1 인 경우, '산-' + 시작주번지 + '-' 시작부번지 예1) 25627|강원도|Gangwon-do|강릉시|Gangneung-si|강동면|Gangdong-myeon|모전리|0||2|3|880|0 => '강원도 강릉시 강동면 모전리 2-3' 예) 25620|강원도|Gangwon-do|강릉시|Gangneung-si|강동면|Gangdong-myeon|상시동리|1||20|3|199|0 => '강원도 강릉시 강동면 상시동리 산 20-3 ''' import argparse import json from time import sleep from urllib.request import urlopen from urllib import parse from urllib.request import Request from urllib.error import HTTPError class LineOfLotAddress: def __init__(self, in_list): self.zip_code = self._get_zipcode(in_list) self.addr = self._get_address(in_list) def _get_zipcode(self, in_list): return in_list[0].strip() def _get_address(self, in_list): addr1 = in_list[1] # 시도 addr2 = in_list[3] # 시군구 addr3 = in_list[5] # 읍면동 addr4 = in_list[7] or in_list[9] # 7 - 리명, 9 - 행정동 is_mountain = True if in_list[8] == '1' else False addr5 = in_list[10] if in_list[10] and in_list[10] != '0' else '' addr6 = in_list[11] if addr5 and in_list[11] and in_list[11] != '0' else '' return f"{addr1.strip()} {addr2.strip()} {addr3.strip()} {addr4.strip()}" + \ f" {'산' if is_mountain else ''} {addr5 if addr5 else ''} {'-'+addr6 if addr6 else ''}" class NaverGeocording: API_URL = 'https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode?query=' def __init__(self, client_id, client_secret): self.id = client_id self.secret = client_secret def request(self, address): url = NaverGeocording.API_URL + parse.quote(address) request = Request(url) request.add_header('X-NCP-APIGW-API-KEY-ID', self.id) request.add_header('X-NCP-APIGW-API-KEY', self.secret) try: response = urlopen(request) except Exception as e: print(f"request 실패 {address} - HTTP Error {e}") sleep(3) return (0.0, 0.0) else: resp_code = response.getcode() if resp_code == 200: resp_data = response.read().decode('utf-8') j_data = json.loads(resp_data) if not j_data['addresses']: return (0.0, 0.0) return (j_data['addresses'][0]['y'],j_data['addresses'][0]['x']) else: print(f"Response Status [{resp_code}] {address}") return (0.0, 0.0) def get_addr_file(ref_file): with open(ref_file, 'r', encoding='utf-8') as r_fd: line = r_fd.readline() # 우편번호|시도|시도영문|시군구|시군구영문|읍면동|읍면동영문|리명|산여부|행정동|시작주번지|시작부번지|끝주번지|끝부번지 while True: line = r_fd.readline() if not line: break in_list = line.split('|') oLine = LineOfLotAddress(in_list) yield oLine def load_zip_dict(json_file_name): try: with open(json_file_name, 'r') as r_fd: zip_dict = json.load(r_fd) return zip_dict except FileNotFoundError: return {} def out_lat_lng_by_zip(out_file, ref_file, client_id, client_secret): '''zip 코드 기반의 위경도 값을 구하여, 파일에 저장합니다.''' zip_dict = load_zip_dict(out_file) geo = NaverGeocording(client_id, client_secret) for oLine in get_addr_file(ref_file): if oLine.zip_code in zip_dict: continue latitude, longitude = geo.request(oLine.addr) if not latitude or not longitude: continue zip_dict[oLine.zip_code] = {'lat':latitude, 'lng':longitude, 'addr':oLine.addr} print(f"-- DEB: {oLine.zip_code}, {latitude}, {longitude}, {oLine.addr}") with open(out_file, 'w', encoding='utf-8') as w_fd: json.dump(zip_dict, w_fd, ensure_ascii=False, indent=4) def proc_parser(): "입력값 파싱" parser = argparse.ArgumentParser() parser.add_argument('--ref_file', '-ref', help='지번범위 파일', default='/Users/jhchoi/Project/map/data/20221006_지번범위.txt') parser.add_argument('--out_file', '-out', help='출력 파일명', default='/Users/jhchoi/Project/map/data/20221006_latlng_by_zip.txt') parser.add_argument('--client_id', '-id', help='네이버 MAP 서비스 클라이언트 id') parser.add_argument('--client_secret', '-secret', help='네이버 MAP 서비스 클라이언트 secret') return parser.parse_args() if __name__ == '__main__': PARAMS = proc_parser() out_lat_lng_by_zip(PARAMS.out_file, PARAMS.ref_file, PARAMS.client_id, PARAMS.client_secret) # 위/경도 값을 얻는 과정에서 누락된 값이 있을 수 있어 한번 더 실행합니다. out_lat_lng_by_zip(PARAMS.out_file, PARAMS.ref_file, PARAMS.client_id, PARAMS.client_secret)
3) 결과 파일 (22년 10월 29일 결과 파일)
'프로그래밍' 카테고리의 다른 글
python 제너레이터(generator) 를 이용한 여러개 파일 쓰기 (1) | 2022.11.02 |
---|---|
프로그래밍 숙련 단계 (0) | 2021.10.20 |