공공데이터포털( data.go.kr )에서 제공하는 국토교통부 오피스텔 전월세 실거래가 자료 API를 수집하는 방법을 알아본다.
개요
오피스텔 전월세 실거래가 데이터는 국토교통부에서 제공하는 데이터로, 관련 데이터는 오피스텔 매매 실거래가 자료가 있다.
API 사용
API를 사용하기 위해서는 해당 API 키(key) 발급, API 호출을 위한 URL 그리고 호출 시 필요한 파라미터를 알아야 한다.
API Key 발급
API를 사용하기 위해서 [✏️활용신청] 버튼을 클릭하여 신청을 해야 한다. 신청 페이지에서 활용 목적을 적당히 기입하고 신청을 하면 해당 API를 사용할 수 있게 된다.
사이트 우상단의 [마이페이지]를 클릭하면 다음과 같은 화면을 볼 수 있다.
API 키는 두 종류(Encoding, Decoding)이 발급되며 상황에 맞게 적절한 키를 사용해야 한다. 그리고 신청하는 API에 관계 없이 여기서는 하나의 API 키를 사용할 수 있다.
마이페이지에서 [API 신청] 메뉴를 눌러보면 확실하게 API를 사용할 수 있게 되었는지 그 목록을 확인할 수 있으며 경우에 따라 신청 즉시 사용할 수 없는 API도 있으니 참고하도록 한다.
API 기초 정보 확인
API를 호출하기 위해 사용되는 기본 주소는 일반적으로 엔드 포인트(End Point)라고 하며 이 API의 엔드 포인트는 다음과 같다.
http://apis.data.go.kr/1613000/RTMSDataSvcOffiRent
해당 주소 뒤에 여러 정보를 붙여서 API를 호출할 수 있다.
간단하게 확인하려면 다음과 같이 [미리보기] 메뉴 아래의 [확인]버튼을 눌러보면 나오는 다음과 같은 화면에서 확인할 수 있으며
상세 정보를 보려면 다음과 같이 API 문서를 확인해야 한다.
API 최초 요청
공공데이터 포털에서의 API 요청은 직접 코드를 사용하는 것도 좋지만 초심자라면 API 페이지에서 제공하는 미리보기를 우선적으로 사용해보는 것을 권장한다.
앞에서 살펴본 미리보기 메뉴에서 “serviceKey”옆의 인증키(Encoding)를 입력한 후 아래의 [미리보기] 버튼을 누르면 사용 중인 웹브라우저에서 별도의 창(또는 탭)이 뜨면서 API 호출 결과를 확인할 수 있다.
상기 내용을 기반으로 호출을 한 결과는 다음과 같다.
파이썬으로 API를 호출하게 되면 상기와 같은 XML 형식의 데이터를 받게 되며 이를 정제하는 코드를 작성해야 한다.
그리고 API 호출 시 새로 열린 웹브라우저의 탭의 주소를 가져오면 다음과 같다.
https://apis.data.go.kr/1613000/RTMSDataSvcOffiRent/getRTMSDataSvcOffiRent?serviceKey=qSd6eaxxxxxxxxxxxxxxxXPn0SgGfh0YsRJEr%2FOr7wi%2BA%3D%3D&LAWD_CD=11110&DEAL_YMD=201512
※ API 키는 일부 지워서 동작하지는 않음
“?” 이전 부분이 이 API를 호출하는 기본 주소이며 “?” 이후의 부분이 파라미터를 나타낸다. 파라미터는 “&”로 구분되며 각각의 파라미터는 “key=value” 형태로 되어 있다.
이제 파이썬으로 API를 호출하는 코드를 작성해보자. 참고로 아래 코드의 “url” 객체에는 앞에서 확인한 웹브라우저의 탭의 주소를 입력해야 하며 URL의 “https”를 “http”로 바꾸어야 정상동작한다. 만약 제대로 하지 않았다면 “SSL” 에러가 발생한다.
※ “SSL”에러 관련 내용은 Py) API(공공) SSL 에러 게시글 참고
1 | import requests |
이제 API 응답을 확인해보자.
1 | res.content |
뭔가 굉장히 많지만 아무튼 뭔가 정보가 있는 것 같다. 그런데 만약 올바르지 않은 인증키를 입력했다면 다음과 같은 에러메세지를 볼 수 있다.
1 | res.content |
상기 인증키 에러를 포함하여 다양한 에러에 대해서는 Py) API(공공) 에러 게시글을 참고하도록 하자.
API 결과 상세
거래 상세 내용이 있는 “items”태그의 내용이 많기 때문에 해당 내용을 접어서 보면 다음과 같다.
“header”에는 결과에 대한 대략적인 정보가 있으며 “resultCode”가 000이고 “resultMsg”가 “OK”인 것으로 보아 정상 요청과 응답이 실시되었다는 것을 알 수 있다.
그리고 “body”에는 거래 상세내용이 있는 “items”와 더불어 “numOfRows”, “pageNo”, “totalCount” 등의 정보가 있다. “numOfRows”는 한 페이지에 보여지는 거래 내용의 개수이며 “pageNo”는 현재 페이지 번호이다. “totalCount”는 전체 거래 내용의 개수이다.
즉, 해석하자면 2015년 12월의 종로구(11110)의 오피스텔 전월세 거래는 총 45건이 있으며 한 페이지에 10건씩 보여지고 현재 1페이지를 보고 있다는 것이다.
다음은 “items” 태그 안에 있는 내용에 대한 표이다.
※ 이 표는 API 문서의 내용을 옮겨온 것으로 되도록이면 해당 문서를 참고하도록 하자.
컬럼명 | 항목명 |
---|---|
sggCd | 지역코드 |
sggNm | 시군구 |
umdNm | 법정동 |
jibun | 지번 |
offiname | 단지명 |
excluUseAr | 전용면적 |
dealYear | 계약년도 |
dealMonth | 계약월 |
dealDay | 계약일 |
deposit | 보증금액 |
monthlyRent | 월세금액 |
floor | 층 |
buildYear | 건축년도 |
contractTerm | 계약기간 |
contractType | 계약구분 |
useRRRight | 갱신요구권사용 |
preDeposit | 종전계약보증금 |
preMonthlyRent | 종전계약월세 |
이제 “BeautifulSoup”이 있는 bs4라이브러리를 사용하여 XML 데이터를 파싱해보자.
1 | bs_res = bs(res.content, features = "xml") |
파라미터의 변경
이 API는 크게 지역코드와 거래시점을 바꾸는 것이 주된 파라미터 변경이다. 그리고 앞의 코드를 조금 업그레이드 해보자.
다음의 코드를 입력하는데 대신 “key” 객체에는 본인이 발급받은 “Encoding” 키를 할당해야 한다.
1 | url_base = "http://apis.data.go.kr/1613000/RTMSDataSvcOffiRent/getRTMSDataSvcOffiRent" |
이렇게 코드를 바꾸게 되면 반복문을 사용하여 쉽게 데이터를 가져올 수 있게 된다. “val_deal_ymd” 객체에 202501
를 입력하여 25년도 1월 데이터를 가져오도록 하자.
1 | url_base = "http://apis.data.go.kr/1613000/RTMSDataSvcOffiRent/getRTMSDataSvcOffiRent" |
데이터 정제
이제 종로구의 2025년도 1월의 오피스텔의 전월세 데이터를 데이터프레임으로 정제해보자. 앞에서 생성한 “bs_res” 객체를 기준으로 핸들링 하겠다.
1 | bs_items = bs_res.select("item") |
최초에 10개 데이터를 가져왔기 때문에 “bs_items” 객체의 길이가 10인 것을 확인할 수 있다. 그럼 첫 번째 원소도 확인해보자.
1 | bs_items[0] |
연지동 “더넥스트 종로” 11층의 거래 상세 내용이다. 이제 이 내용을 데이터프레임으로 정제해보자. 간단하게 초급수준의 코드를 작성하면 다음과 같다.
1 | bs_item = bs_items[0] |
buildYear | contractTerm | contractType | dealDay | dealMonth | dealYear | deposit | excluUseAr | |
---|---|---|---|---|---|---|---|---|
0 | 2022 | 8 | 1 | 2025 | 23,000 | 18.66 |
모든 데이터는 아니지만 이런식으로 데이터를 하나하나 .select_one()
으로 가져와서 데이터프레임으로 정제할 수 있다. 그리고 concat()
함수로 각각의 데이터프레임을 이어붙일 수 있다.
1 | df_item_bind_sample = pd.DataFrame() |
buildYear | contractTerm | contractType | dealDay | dealMonth | dealYear | deposit | excluUseAr | |
---|---|---|---|---|---|---|---|---|
0 | 2022 | 8 | 1 | 2025 | 23,000 | 18.66 | ||
0 | 2020 | 25.02~27.02 | 신규 | 18 | 1 | 2025 | 26,900 | 24.47 |
0 | 2004 | 25.03~26.03 | 갱신 | 13 | 1 | 2025 | 27,000 | 37.56 |
0 | 2004 | 25.02~26.02 | 신규 | 9 | 1 | 2025 | 5,000 | 28.24 |
0 | 2022 | 25.03~26.03 | 신규 | 20 | 1 | 2025 | 2,000 | 21.14 |
0 | 2020 | 25.03~27.03 | 신규 | 21 | 1 | 2025 | 5,000 | 27.18 |
0 | 2022 | 25.02~26.02 | 신규 | 8 | 1 | 2025 | 1,000 | 24.21 |
0 | 2004 | 25.02~26.02 | 신규 | 10 | 1 | 2025 | 1,000 | 28.19 |
0 | 2022 | 25.03~26.02 | 신규 | 18 | 1 | 2025 | 1,000 | 24.21 |
0 | 2004 | 25.03~26.03 | 갱신 | 12 | 1 | 2025 | 1,906 | 17.2 |
나머지 항목의 데이터를 추가하게 되면 손쉽게 상기 코드를 활용해서 1회 호출분의 결과를 하나의 데이터프레임으로 일괄 취합할 수 있다.
본인이 원하는 항목만 가져오고자 한다면 상기의 방법이 괜찮긴 하지만 전체 데이터를 가져오고자 한다면 더 간단한 방법이 있다. 바로 내포(comprehension)를 사용하는 것이다. 우선 .find_all()
메서드를 사용하면 모든 하위 태그를 뽑을 수 있다.
1 | bs_item = bs_items[0] |
그리고 .name
어트리뷰트와 .text
어트리뷰트를 사용하면 태그의 이름과 내용을 가져올 수 있다.
1 | bs_item.find_all()[0].name |
상기 내용을 합쳐서 반복문으로 우선 만들어보자.
1 | bs_item_tags = bs_item.find_all() |
buildYear | contractTerm | contractType | dealDay | dealMonth | dealYear | deposit | excluUseAr | |
---|---|---|---|---|---|---|---|---|
0 | 2022 | 8 | 1 | 2025 | 23,000 | 18.66 |
이제 한 단계 더 나아가서 10개 거래 내역의 모든 항목 데이터를 취합해보자.
1 | df_item_bind = pd.DataFrame() |
buildYear | contractTerm | contractType | dealDay | dealMonth | dealYear | deposit | excluUseAr | |
---|---|---|---|---|---|---|---|---|
0 | 2022 | 8 | 1 | 2025 | 23,000 | 18.66 | ||
0 | 2020 | 25.02~27.02 | 신규 | 18 | 1 | 2025 | 26,900 | 24.47 |
0 | 2004 | 25.03~26.03 | 갱신 | 13 | 1 | 2025 | 27,000 | 37.56 |
0 | 2004 | 25.02~26.02 | 신규 | 9 | 1 | 2025 | 5,000 | 28.24 |
0 | 2022 | 25.03~26.03 | 신규 | 20 | 1 | 2025 | 2,000 | 21.14 |
0 | 2020 | 25.03~27.03 | 신규 | 21 | 1 | 2025 | 5,000 | 27.18 |
0 | 2022 | 25.02~26.02 | 신규 | 8 | 1 | 2025 | 1,000 | 24.21 |
0 | 2004 | 25.02~26.02 | 신규 | 10 | 1 | 2025 | 1,000 | 28.19 |
0 | 2022 | 25.03~26.02 | 신규 | 18 | 1 | 2025 | 1,000 | 24.21 |
0 | 2004 | 25.03~26.03 | 갱신 | 12 | 1 | 2025 | 1,906 | 17.2 |
이렇게 한 번의 호출 결과 전체를 데이터프레임 하나로 정제할 수 있다. 그럼 이제 반복문을 해봤으니 이를 내포(comprehension)로 바꿔보자.
1 | ls_items = [{tag.name: tag.text for tag in bs_item.find_all()} for bs_item in bs_items] |
buildYear | contractTerm | contractType | dealDay | dealMonth | dealYear | deposit | excluUseAr | |
---|---|---|---|---|---|---|---|---|
0 | 2022 | 8 | 1 | 2025 | 23,000 | 18.66 | ||
1 | 2020 | 25.02~27.02 | 신규 | 18 | 1 | 2025 | 26,900 | 24.47 |
2 | 2004 | 25.03~26.03 | 갱신 | 13 | 1 | 2025 | 27,000 | 37.56 |
3 | 2004 | 25.02~26.02 | 신규 | 9 | 1 | 2025 | 5,000 | 28.24 |
4 | 2022 | 25.03~26.03 | 신규 | 20 | 1 | 2025 | 2,000 | 21.14 |
5 | 2020 | 25.03~27.03 | 신규 | 21 | 1 | 2025 | 5,000 | 27.18 |
6 | 2022 | 25.02~26.02 | 신규 | 8 | 1 | 2025 | 1,000 | 24.21 |
7 | 2004 | 25.02~26.02 | 신규 | 10 | 1 | 2025 | 1,000 | 28.19 |
8 | 2022 | 25.03~26.02 | 신규 | 18 | 1 | 2025 | 1,000 | 24.21 |
9 | 2004 | 25.03~26.03 | 갱신 | 12 | 1 | 2025 | 1,906 | 17.2 |
13줄의 코드가 두 줄의 코드로 줄었으며 조금 더 길게 쓴다면 한 줄의 코드로도 치환이 가능하다.
이는 dictionary comprehension과 list comprehension을 사용한 것이다. 이렇게 코드를 작성하면 더 간결하고 빠르지만, 코드 작성 난이도가 높고 가독성이 떨어질 수 있으므로 상황에 맞게 사용하도록 하자.
API 사용 실무
실제로 API를 활용하여 원하는 데이터를 모두 수집하기 위해서는 날짜와 지역을 변환하는 것과 더불어 API를 호출할 때 발생할 수 있는 예외 상황까지 대처할 수 있도록 강인한(robust) 코드를 작성해야 한다.
검토 사항
API마다 설계가 다르게 되어있기 때문에 API 사용 전에 해당 API의 문서 확인을 권장하며 대표적으로 고려해야 하는 사항은 다음과 같다.
- 제약사항: 일일 호출 횟수, 호출 시간 제한 등
- 결과처리: 호출 결과 형식(JSON, XML 등)과 정제 방법
- 수집범위: 데이터 수집이 필요한 범위 및 API의 제공 범위
- 예외처리: API 호출 시 발생할 수 있는 예외 상황에 대한 대처 방안
- 기타사항: 복잡한 보안/인증 절차 등 기타사항으로 인한 API 사용 제한
앞에서도 언급했듯이 신청 즉시 사용할 수 없는 API도 있는데 다른 모든 것이 정상이라도 공공데이터포털의 내부 시스템이나 연계된 시스템에서 API 사용 신청을 실시간으로 반영해주지 못하는 경우도 있다. 경험상 최대 1영업일 까지 소요되는데 필요하면 해당 API를 관리하는 담당자에게 연락해보도록 하자.
수집 범위에 따른 검토 사항
이 API는 법정동별, 년월별, 페이지별 데이터를 제공하기 떄문에 대량 수집을 하는 경우 모든 데이터를 하나의 데이터프레임으로 합친 후 저장하기 보다는 법정동 또는 년월별로 데이터를 합쳐서 저장하는 것을 권장한다.
충분히 검토가 된 코드라면 관계없지만, 중간에 수집코드가 오류 등 다양한 사유로 멈출 경우 그 중간부터 시작하도록 하는 것이 꽤나 귀찮고 번거롭기 때문에 코드를 완벽하게 작성하는 것도 있겠지만 되도록이면 중간까지의 수집 내용을 분실하지 않기 위해 별도의 파일로 저장하는 것을 권장한다.
최종 코드
이제 최종적으로 API를 활용하여 데이터를 수집하는 코드를 작성해보자. 이 코드는 2024년도 1월부터 12월까지의 모든 지역의 데이터를 수집하는 코드이다. 만약, 다른 년도 데이터 수집이 필요하다면 “val_year” 변수에 원하는 년도를 입력하면 된다.
그리고 앞에서 언급하지 않은 “pageNo”와 “numOfRows”도 추가되어 있다. 이는 API 문서에 명시되어 있으니 확인해보면 된다.
앞에서 사용한 API키는 Encoding Key이나 다음의 코드에서는 Decoding Key임으로 주의하도록 하자. 관련 내용은 Py) 공공데이터포털 API Key 사용 게시글에서 좀 더 자세하게 설명하고 있다.
1 | import os |