Python

[파이썬] 공공데이터 API 활용 XML 파싱 - 기상청 시간 데이터 추출

weweGH 2025. 3. 16. 20:20
반응형

공공데이터 XML 파싱
공공데이터 XML 파싱


공공데이터 API 활용 XML 파싱 - 기상청 시간 데이터 추출


OUTLINE


  • 공공데이터 활용
  • 패키지 불러오기
  • 분석지역
  • xml 파싱 step1. totalCount 조회
  • xml 파싱 step2. 데이터프레임 생성
  • 전체 지역 데이터 추출 코드

공공데이터 활용


데이터 분석을 하다 보면, 공공데이터를 활용할 때가 종종 생긴다. 이런 경우, 하나하나 클릭하며 데이터를 다운로드하는 것보다 파이썬 패키지들을 활용하면 편리하게 다운로드할 수 있다. 이 글에서 사용할 데이터는 기상청의 종관 기상 관측 데이터로 전국의 1시간 단위 기상에 대한 자료이다. 파이썬의 requests, bs4(beautifulsoup), xmltodict 패키지들을 활용하여 http 요청부터 xml을 파싱 하여 데이터프레임으로 생성하는 단계까지 진행한다.


패키지 불러오기


import requests
import pprint
import pandas as pd
import bs4
import datetime
import xmltodict
import numpy as np
import warnings
warnings.filterwarnings('ignore')

분석지역


분석할 지역은 총 95개이며, 아래 링크의 워드파일에 각 지역에 대한 설명이 첨부되어 있다. 전체 지역 데이터를 추출하기 전에, 서울 지역에 대한 데이터 추출로 코드를 진행한다.
** 전체 지역에 대한 데이터 추출은 글의 마지막 전체 코드 참고

# 전국 지역 총 95개
area_id = ['90','93','95','98','99','100','101','102','104','105',
             '106','108','112','114','115','119','121','127','129',
             '130','131','133','135','136','137','138','140','143',
             '146','152','155','156','159','162','165','168','169',
             '170','172','174','175','177','184','185','188','189',
             '192','201','202','203','211','212','216','217','221',
             '226','232','235','236','238','243','244','245','247',
             '248','251','252','253','254','255','257','258','259',
             '260','261','262','263','264','266','268','271','272',
             '273','276','277','278','279','281','283','284','285',
             '288','289','294','295']
print(len(area_id))

xml 파싱 step1. totalCount 조회


데이터를 추출하기 전에 데이터의 크기를 알아내기 위해 totalCount를 통해 pageno 변수를 생성한다.

예를 들어, 2023.1.1 00시부터 2023.1.31 23시까지 서울(area_id:108)의 데이터를 추출한다고 하면, 아래와 같이 pageno는 2가 된다.

url = 'http://apis.data.go.kr/1360000/AsosHourlyInfoService/getWthrDataList'
st_date = '20230101' # 시작 날짜
ed_date = '20230131' # 종료 날짜

# totalcnt 조회를 위한 params
params ={'serviceKey' : '개인서비스키',
        'pageNo' : '1', 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
        'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
        'stnIds' : 108}
response = requests.get(url, params=params)
dic = xmltodict.parse(response.content)
totalcnt = dic['response']['body']['totalCount']
pageno = int(np.ceil(int(totalcnt)/500))

* 위의 코드를 상세하게 살펴보면,

먼저, url과 시작날짜, 마지막날짜 변수를 생성한다. 그리고 설정한 params와 url을 통해 http요청을 한다. params의 각 변수들의 정의는 다음과 같다. 

  • serviceKey : 개인이 발급받은 서비스키
  • numOfRows : 한 페이지 결과 수
  • dataType : 응답자료형식(xml, json 등이 있다)
  • dataCd : 자료코드
  • dateCd : 날짜코드
  • startDt/Hh : 시작일/시
  • endDt/Hh : 종료일/시
  • stnIds : 지점번호
url = 'http://apis.data.go.kr/1360000/AsosHourlyInfoService/getWthrDataList'
st_date = '20230101' # 시작 날짜
ed_date = '20230131' # 종료 날짜

# totalcnt 조회를 위한 params
params ={'serviceKey' : '개인서비스키',
        'pageNo' : '1', 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
        'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
        'stnIds' : 108}
response = requests.get(url, params=params)

http 요청 후에 xml을 딕셔너리 자료형으로 변환한다. 간략하게 데이터를 확인할 수 있다. 이미지 아래쪽의 'numOfRows':'900', 'totalCount':'744'를 통해 해당 페이지의 데이터의 크기를 알아낼 수 있다. 예를 들어, 만약 totalcount가 64이고, numOfRows가 10이면 pageNo를 1부터 7까지 요청해야 한다. 

dic = xmltodict.parse(response.content)
dic

xml 딕셔너리 자료형 변환
xml 딕셔너리 자료형 변환


현재 페이지는 totalCount가 numOfRows보다 작으므로 pageNo가 1이면 전체 데이터를 불러올 수 있다. 하지만 나중에 자동화를 위해 totalcnt를 통해 pageno를 추출하는 방법을 코드로 작성했다.

totalcnt = dic['response']['body']['totalCount']
pageno = int(np.ceil(int(totalcnt)/900))
pageno # 1
반응형

xml 파싱 step2. 데이터프레임 생성


데이터의 크기를 확인했다면, 이제 xml을 파싱하여 분석할 수 있도록 데이터프레임을 생성한다. 

params ={'serviceKey' : '개인서비스키',
        'pageNo' : 1, 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
        'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
        'stnIds' : 108}
response = requests.get(url, params)
xml_obj = bs4.BeautifulSoup(response.text,'lxml-xml')
rows = xml_obj.findAll('item')
#
tmp1 = pd.DataFrame()
for i in range(0, len(rows)):
    tmp = pd.DataFrame({
            rows[i].find('tm').name:[rows[i].find('tm').text],
            rows[i].find('rnum').name:[rows[i].find('rnum').text],
            rows[i].find('stnId').name:[rows[i].find('stnId').text],
            rows[i].find('stnNm').name:[rows[i].find('stnNm').text],
            rows[i].find('ta').name:[rows[i].find('ta').text],
            rows[i].find('rn').name:[rows[i].find('rn').text],
            rows[i].find('ws').name:[rows[i].find('ws').text],
            rows[i].find('wd').name:[rows[i].find('wd').text],
            rows[i].find('hm').name:[rows[i].find('hm').text],
            rows[i].find('pv').name:[rows[i].find('pv').text],
            rows[i].find('td').name:[rows[i].find('td').text],
            rows[i].find('pa').name:[rows[i].find('pa').text],
            rows[i].find('ps').name:[rows[i].find('ps').text],
            rows[i].find('ss').name:[rows[i].find('ss').text],
            rows[i].find('icsr').name:[rows[i].find('icsr').text],
            rows[i].find('dsnw').name:[rows[i].find('dsnw').text],
            rows[i].find('hr3Fhsc').name:[rows[i].find('hr3Fhsc').text],
            rows[i].find('dc10Tca').name:[rows[i].find('dc10Tca').text],
            rows[i].find('dc10LmcsCa').name:[rows[i].find('dc10LmcsCa').text],
            rows[i].find('clfmAbbrCd').name:[rows[i].find('clfmAbbrCd').text],
            rows[i].find('lcsCh').name:[rows[i].find('lcsCh').text],
            rows[i].find('vs').name:[rows[i].find('vs').text],
            rows[i].find('gndSttCd').name:[rows[i].find('gndSttCd').text],
            rows[i].find('dmstMtphNo').name:[rows[i].find('dmstMtphNo').text],
            rows[i].find('ts').name:[rows[i].find('ts').text],
            rows[i].find('m005Te').name:[rows[i].find('m005Te').text],
            rows[i].find('m01Te').name:[rows[i].find('m01Te').text],
            rows[i].find('m02Te').name:[rows[i].find('m02Te').text],
            rows[i].find('m03Te').name:[rows[i].find('m03Te').text]})
    tmp1 = pd.concat([tmp,tmp1])
    tmp1.to_csv('종관기상관측_108.csv',encoding='euc-kr',index=False)

* 위의 코드를 상세하게 살펴보면,

위에서 totalcnt를 요청했던대로 동일한 params를 통해 html을 요청한다. 그리고 html 문서를 response.txt를 활용하여 읽고, lxml 파서를 통해 beautifulsoup 객체를 생성한다. 코드를 실행하면, xml_obj는 다음과 같이 데이터를 xml 형식이다.

params ={'serviceKey' : '개인서비스키',
        'pageNo' : 1, 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
        'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
        'stnIds' : 108}
response = requests.get(url, params)
xml_obj = bs4.BeautifulSoup(response.text,'lxml-xml')
xml_obj

xml_obj
xml_obj


xml_obj는 페이지의 전체 내용이 포함되어 있기 때문에, 데이터프레임을 생성하기 위해 findAll을 활용하여 item의 하위에 있는 요소들만 찾아 리스트로 변환한다. 데이터만 추출해야 한다. rows 변수를 확인해 보면, 필요한 칼럼명과 데이터 값만 추출할 수 있다.

rows = xml_obj.findAll('item')
rows[0]

rows[0] 일부 캡처
rows[0] 일부 캡처


rows 변수를 통해 데이터프레임에 필요한 컬럼명과 데이터 값을 추출했다면, 데이터프레임 생성 단계만 거치면 된다. rows에는 전체 데이터가 들어있기 때문에 반복문을 활용하여, 각 행마다 데이터를 읽어 concat 하면 데이터프레임이 완성된다. 지역별로 csv를 저장하기 위해 to_csv를 통해 지역별로 저장까지 하면 완성이다.

tmp1 = pd.DataFrame()
for i in range(0, len(rows)):
    tmp = pd.DataFrame({
            rows[i].find('tm').name:[rows[i].find('tm').text],
            rows[i].find('rnum').name:[rows[i].find('rnum').text],
            rows[i].find('stnId').name:[rows[i].find('stnId').text],
            rows[i].find('stnNm').name:[rows[i].find('stnNm').text],
            rows[i].find('ta').name:[rows[i].find('ta').text],
            rows[i].find('rn').name:[rows[i].find('rn').text],
            rows[i].find('ws').name:[rows[i].find('ws').text],
            rows[i].find('wd').name:[rows[i].find('wd').text],
            rows[i].find('hm').name:[rows[i].find('hm').text],
            rows[i].find('pv').name:[rows[i].find('pv').text],
            rows[i].find('td').name:[rows[i].find('td').text],
            rows[i].find('pa').name:[rows[i].find('pa').text],
            rows[i].find('ps').name:[rows[i].find('ps').text],
            rows[i].find('ss').name:[rows[i].find('ss').text],
            rows[i].find('icsr').name:[rows[i].find('icsr').text],
            rows[i].find('dsnw').name:[rows[i].find('dsnw').text],
            rows[i].find('hr3Fhsc').name:[rows[i].find('hr3Fhsc').text],
            rows[i].find('dc10Tca').name:[rows[i].find('dc10Tca').text],
            rows[i].find('dc10LmcsCa').name:[rows[i].find('dc10LmcsCa').text],
            rows[i].find('clfmAbbrCd').name:[rows[i].find('clfmAbbrCd').text],
            rows[i].find('lcsCh').name:[rows[i].find('lcsCh').text],
            rows[i].find('vs').name:[rows[i].find('vs').text],
            rows[i].find('gndSttCd').name:[rows[i].find('gndSttCd').text],
            rows[i].find('dmstMtphNo').name:[rows[i].find('dmstMtphNo').text],
            rows[i].find('ts').name:[rows[i].find('ts').text],
            rows[i].find('m005Te').name:[rows[i].find('m005Te').text],
            rows[i].find('m01Te').name:[rows[i].find('m01Te').text],
            rows[i].find('m02Te').name:[rows[i].find('m02Te').text],
            rows[i].find('m03Te').name:[rows[i].find('m03Te').text]})
    tmp1 = pd.concat([tmp,tmp1])
    tmp1.to_csv('종관기상관측_108.csv',encoding='euc-kr',index=False)

전체 지역 데이터 추출 코드


위에서 각 파트별로 실행한 코드는 서울 지역 데이터만 추출한 결과다. 전체 지역에 대한 데이터 추출 코드는 다음과 같다.

import requests
import pprint
import pandas as pd
import bs4
import datetime
import xmltodict
import numpy as np
import warnings
warnings.filterwarnings('ignore')

# 전국 지역 총 95개
area_id = ['90','93','95','98','99','100','101','102','104','105',
             '106','108','112','114','115','119','121','127','129',
             '130','131','133','135','136','137','138','140','143',
             '146','152','155','156','159','162','165','168','169',
             '170','172','174','175','177','184','185','188','189',
             '192','201','202','203','211','212','216','217','221',
             '226','232','235','236','238','243','244','245','247',
             '248','251','252','253','254','255','257','258','259',
             '260','261','262','263','264','266','268','271','272',
             '273','276','277','278','279','281','283','284','285',
             '288','289','294','295']
print(len(area_id)) # 95

url = 'http://apis.data.go.kr/1360000/AsosHourlyInfoService/getWthrDataList'

for k in range(0,len(area_id)):
    print(area_id[k],'시작시간 : ', datetime.datetime.now())
    # 시작, 종료 날짜
    st_date = '20230101'
    ed_date = '20230131'
    #
    # totalcnt 조회 위한 params
    params ={'serviceKey' : '개인서비스키',
            'pageNo' : '1', 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
            'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
            'stnIds' : area_id[k]}
    response = requests.get(url, params=params)
    dic = xmltodict.parse(response.content)
    totalcnt = dic['response']['body']['totalCount']
    pageno = int(np.ceil(int(totalcnt)/900))
    # 
    for p in range(1,pageno+1) :
            params ={'serviceKey' : '개인서비스키',
                    'pageNo' : p, 'numOfRows' : '900', 'dataType' : 'XML', 'dataCd' : 'ASOS', 'dateCd' : 'HR', 
                    'startDt' : st_date, 'startHh' : '00', 'endDt' : ed_date, 'endHh' : '23',
                    'stnIds' : area_id[k]}
            response = requests.get(url, params)
            xml_obj = bs4.BeautifulSoup(response.text,'lxml-xml')
            rows = xml_obj.findAll('item')
            #
            tmp1 = pd.DataFrame()
            for i in range(0, len(rows)):
                tmp = pd.DataFrame({
                        rows[i].find('tm').name:[rows[i].find('tm').text],
                        rows[i].find('rnum').name:[rows[i].find('rnum').text],
                        rows[i].find('stnId').name:[rows[i].find('stnId').text],
                        rows[i].find('stnNm').name:[rows[i].find('stnNm').text],
                        rows[i].find('ta').name:[rows[i].find('ta').text],
                        rows[i].find('rn').name:[rows[i].find('rn').text],
                        rows[i].find('ws').name:[rows[i].find('ws').text],
                        rows[i].find('wd').name:[rows[i].find('wd').text],
                        rows[i].find('hm').name:[rows[i].find('hm').text],
                        rows[i].find('pv').name:[rows[i].find('pv').text],
                        rows[i].find('td').name:[rows[i].find('td').text],
                        rows[i].find('pa').name:[rows[i].find('pa').text],
                        rows[i].find('ps').name:[rows[i].find('ps').text],
                        rows[i].find('ss').name:[rows[i].find('ss').text],
                        rows[i].find('icsr').name:[rows[i].find('icsr').text],
                        rows[i].find('dsnw').name:[rows[i].find('dsnw').text],
                        rows[i].find('hr3Fhsc').name:[rows[i].find('hr3Fhsc').text],
                        rows[i].find('dc10Tca').name:[rows[i].find('dc10Tca').text],
                        rows[i].find('dc10LmcsCa').name:[rows[i].find('dc10LmcsCa').text],
                        rows[i].find('clfmAbbrCd').name:[rows[i].find('clfmAbbrCd').text],
                        rows[i].find('lcsCh').name:[rows[i].find('lcsCh').text],
                        rows[i].find('vs').name:[rows[i].find('vs').text],
                        rows[i].find('gndSttCd').name:[rows[i].find('gndSttCd').text],
                        rows[i].find('dmstMtphNo').name:[rows[i].find('dmstMtphNo').text],
                        rows[i].find('ts').name:[rows[i].find('ts').text],
                        rows[i].find('m005Te').name:[rows[i].find('m005Te').text],
                        rows[i].find('m01Te').name:[rows[i].find('m01Te').text],
                        rows[i].find('m02Te').name:[rows[i].find('m02Te').text],
                        rows[i].find('m03Te').name:[rows[i].find('m03Te').text]})
                tmp1 = pd.concat([tmp,tmp1])
            print(len(tmp1) == len(rows))
            print(area_id[k],'완료시간 : ', datetime.datetime.now())
            tmp1.to_csv('종관기상관측_'+area_id[k]+'.csv',encoding='euc-kr',index=False)

반응형