지오코딩으로 주소를 좌표로 바꾸기 (api 3종 비교)

2024. 8. 7. 11:57R Python GIS

지오코딩은 인간의 문장으로 된 주소(Address)를 컴퓨터가 인식할 수 있는 경위도 좌표(Coordinates)로 변환하는 작업입니다.
컴퓨터는 바보라서 주소를 말해줘도 알아먹지를 않습니다. 따라서 인터넷에 주소를 검색해서 좌표를 따 와야 하는데, 일일히 구글 지도에 복붙해 가며 검색할 수도 없는 노릇입니다. 이럴 땐 파이썬으로 데이터프레임 안의 주소를 자동으로 인터넷에 검색하여 좌표를 받아적는 반복작업을 맡길 수 있습니다.

주소를 검색하는 인터넷 서비스를 응용프로그램 개발 과정에서 사용할 수 있도록 만들어놓은 API가 몇 가지 있는데, 이번 포스트에서는 그 중 대표적인 3개를 소개해볼까 합니다.

노미나팀(Nominatim)

오픈스트리프맵(OSM)을 기반으로 한 지오코딩 어플리케이션과 API입니다. API 키가 필요없어 사용이 간편하다는 장점이 있지만, 주소를 그 나라 말로 적지 않으면 좌표를 찾지 못합니다. 1초에 1개로 속도가 제한되어 있습니다. 주소 600개를 지오코딩하려면 10분이 걸리는 셈입니다. 파이썬에서는 geopy 패키지로 제공됩니다. Nominatim(user_agent = '<유저명>').geocode(<대상>).latitudeNominatim(user_agent = '<유저명>').geocode(<대상>).longitude로 각각 위도와 경도를 반환합니다.

카카오 API

카카오에서 제공하는 지도 API로, 한국어로 된 한국 지명만 사용 가능하지만 놀라운 속도와 정확성을 보여줍니다. request 모델로 카카오 서버에 http 요청을 보내 좌표를 받아오는 방식으로 사용합니다. requests.get("https://dapi.kakao.com/v2/local/search/address.json", headers="Authorization": "KakaoAK <API 키>", params={"query": <대상>}).json()[documents][0][x]와 같이 약간 복잡한 방식으로 위도를 받아옵니다. (마지막 x를 y로 바꾸면 경도입니다.) 또한 한 번의 요청 뒤에는 response.raise_for_status()를 통해 응답 상태를 반환시켜줘야 합니다. https://developers.kakao.com/console/app에서 애플리케이션을 등록하고 '앱 키' 에서 'REST API 키'를 복사해서 입력해줘야 합니다.

구글 API

구글에서 제공하는 지도 API로, 카카오와 마찬가지로 https://console.cloud.google.com/에 접속하여 Geocoding API를 신청하고 'API 및 서비스' -> '사용자 인증 정보'에 들어가 API 키를 받아와야 합니다. 모든 지역의 모든 언어로 된 주소를 빠르게 받아오지만 200만건이 지나면 요금이 부과됩니다. 학부 수준에서는 이정도까지 지오코딩 할 일은 없을 테니 구글 API를 사용하도록 합시다. googlemaps 패키지로 제공되며, googlemaps.Client(key=<API 키>).geocode(<대상>)[0].get('geometry')['locaion']['lat']으로 위도를 받아옵니다. (마찬가지로 마지막 lat를 lng로 바꾸면 경도입니다.)

예시 코드

한국어로 된 한국 내 주소 (서울시청), 영어로 된 한국 내 주소 (부산시청), 현지 언어로 된 현지 주소 (에펠탑), 한국어로 된 외국 주소 (주일본대사관)의 네 가지 주소로 검증해 보았습니다.

#시험용 데이터셋 생성

import pandas as pd

df = pd.DataFrame({'Name': ['서울시청', '부산시청', '에펠탑', '주일본대한민국대사관'],
        'Address': ['서울특별시 중구 세종대로 110', '1001, Joongangdae-ro, Yeonje-gu, Busan', 'Av. Gustave Eiffel, 75007 Paris', '도쿄도 미나토구 미나미아자부 1-7-32']})

df
  Name Address
0 서울시청 서울특별시 중구 세종대로 110
1 부산시청 1001, Joongangdae-ro, Yeonje-gu, Busan
2 에펠탑 Av. Gustave Eiffel, 75007 Paris
3 주일본대한민국대사관 도쿄도 미나토구 미나미아자부 1-7-32
#노미나팀

import time
from geopy.geocoders import Nominatim

lat = []
lng = []
for i in range(len(df['Address'])):
    try:
        geo = Nominatim(user_agent = 'Anyeong Haseyo').geocode(df['Address'][i])
        crd = (geo.latitude, geo.longitude)
        lat.append(crd[0])
        lng.append(crd[1])
        print("[" + str(df['Address'][i]) + "] 위도:" + str(lat[i]) + " / 경도:" + str(lng[i]))
        time.sleep(0.2)
    except:
        lat.append('')
        lng.append('')
        print("[" + str(df['Address'][i]) + "]에서 오류!")
df['lat.nominatim'] = lat
df['lng.nominatim'] = lng
df
[서울특별시 중구 세종대로 110] 위도:37.56635025 / 경도:126.97830900276097
[1001, Joongangdae-ro, Yeonje-gu, Busan]에서 오류!
[Av. Gustave Eiffel, 75007 Paris] 위도:48.8580382 / 경도:2.2955342
[도쿄도 미나토구 미나미아자부 1-7-32]에서 오류!
  Name Address lat.nominatim lng.nominatim
0 서울시청 서울특별시 중구 세종대로 110 37.56635 126.978309
1 부산시청 1001, Joongangdae-ro, Yeonje-gu, Busan    
2 에펠탑 Av. Gustave Eiffel, 75007 Paris 48.858038 2.295534
3 주일본대한민국대사관 도쿄도 미나토구 미나미아자부 1-7-32    
#카카오

import time
import requests

lat = []
lng = []
api_url = "https://dapi.kakao.com/v2/local/search/address.json"
API_KEY = "여기에 API 키 입력"
headers = {"Authorization": f"KakaoAK {API_KEY}"}
for i in range(len(df['Address'])):
    try:
        response = requests.get(api_url, headers=headers, params={"query": df['Address'][i]})
        response.raise_for_status()
        result = response.json()
        if "documents" in result and len(result["documents"]) > 0:
            lat.append(result["documents"][0]["y"])
            lng.append(result["documents"][0]["x"])
            print("[" + str(df['Address'][i]) + "] 위도:" + str(lat[i]) + " / 경도:" + str(lng[i]))
            time.sleep(0.2)
        else:
            lat.append('')
            lng.append('')
            print(df['Address'][i]+"에서 오류!")
    except requests.exceptions.RequestException as e:
        lat.append('')
        lng.append('')
        print(df['Address'][i]+"에서 오류!")
df['lat.kakao'] = lat
df['lng.kakao'] = lng
df
[서울특별시 중구 세종대로 110] 위도:37.5663174209601 / 경도:126.977829174031
1001, Joongangdae-ro, Yeonje-gu, Busan에서 오류!
Av. Gustave Eiffel, 75007 Paris에서 오류!
도쿄도 미나토구 미나미아자부 1-7-32에서 오류!
  Name Address lat.nominatim lng.nominatim lat.kakao lng.kakao
0 서울시청 서울특별시 중구 세종대로 110 37.56635 126.978309 37.5663174209601 126.977829174031
1 부산시청 1001, Joongangdae-ro, Yeonje-gu, Busan        
2 에펠탑 Av. Gustave Eiffel, 75007 Paris 48.858038 2.295534    
3 주일본대한민국대사관 도쿄도 미나토구 미나미아자부 1-7-32        
#구글

import time
import googlemaps

lat = []
lng = []
API_KEY = "여기에 API 키 입력"
for i in range(len(df['Address'])):
    try:
        geo = googlemaps.Client(key=API_KEY).geocode(df['Address'][i])[0].get('geometry')
        lat.append(geo['location']['lat'])
        lng.append(geo['location']['lng'])
        print("[" + str(df['Address'][i]) + "] 위도:" + str(lat[i]) + " / 경도:" + str(lng[i]))
        time.sleep(0.2)
    except:
        lat.append('')
        lng.append('')
        print("[" + str(df['Address'][i]) + "]에서 오류!")
df['lat.google'] = lat
df['lng.google'] = lng
df
[서울특별시 중구 세종대로 110] 위도:37.5665851 / 경도:126.9782038
[1001, Joongangdae-ro, Yeonje-gu, Busan] 위도:35.18240460000001 / 경도:129.0829635
[Av. Gustave Eiffel, 75007 Paris] 위도:48.8578065 / 경도:2.295188
[도쿄도 미나토구 미나미아자부 1-7-32] 위도:35.650363 / 경도:139.7373209
  Name Address lat.nominatim lng.nominatim lat.kakao lng.kakao lat.google lng.google
0 서울시청 서울특별시 중구 세종대로 110 37.56635 126.978309 37.5663174209601 126.977829174031 37.566585 126.978204
1 부산시청 1001, Joongangdae-ro, Yeonje-gu, Busan         35.182405 129.082964
2 에펠탑 Av. Gustave Eiffel, 75007 Paris 48.858038 2.295534     48.857807 2.295188
3 주일본대한민국대사관 도쿄도 미나토구 미나미아자부 1-7-32         35.650363 139.737321