파이썬 예제 (COVID-19 시도별 현황)

오늘은 공공데이터포털에서 제공하는 OPEN API 를 이용해, 코로나 바이러스 시도별 감염현황을 모니터링하는 프로그램을 만들어 보겠습니다.

Python언어로 PyQt5 + Matplotlib + reqeusts 모듈을 활용했으며, 완성된 모습은 아래와 같습니다.

[COVID-19 모니터링 프로그램]

 

대한민국 정부에서 제공하는 공공데이터 서버에 필요한 데이터를 Https Request 방식으로 정해진 쿼리를 요청하고 응답 데이터(XML or JSON)를 받아서 구성한 예제 입니다.

 

기본적인 개발 진행 순서는 다음을 참조 바랍니다.

1. 공공데이터 포털 회원가입 (정부공개자료에서 정보를 받아오는 방식)


2. 공공데이터 중 관심 분야 검색 ("코로나")


3. 정보 활용신청 후 서비스키 확보


4. OPEN API 서비스 키를 이용해 요청신호 전송 후 응답 (XML)처리

OPEN API 대부분이 요청신호를 보내고, XML, JSON 형태를 응답받는 Http Request 방식으로 사용됩니다.

[REQUEST, 요청신호 샘플]

http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19SidoInfStateJson?serviceKey=서비스키&pageNo=1&numOfRows=10&startCreateDt=20200831&endCreateDt=20200831

[RESPONSE, 응답신호 샘플]

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><response><header><resultCode>00</resultCode><resultMsg>NORMAL SERVICE.</resultMsg></header><body><items><item>생략...</item></items><numOfRows>10</numOfRows><pageNo>1</pageNo><totalCount>19</totalCount></body></response>

 

소스코드

그럼 소스 코드를 한번 살펴보겠습니다.

전체 소스는 main.py, openapi_function.py 2개의 파이썬 파일로 구성되어 있습니다.

main.py는 화면 구성, openapi_function.py는 OPEN API 신호를 요청하고, 응답받은 XML 데이터를 파싱하는 역할을 담당합니다.

코드양을 줄이기위해 Qt Designer를 활용해 UI를 구성하였으므로, 프로젝트 구성 시 *.py 파일과 같은 경로에 form.ui 파일이 존재해야 실행 가능합니다. (클릭하면 다운 가능)

*.ui 파일은 코드에서 직접 컨트롤을 생성 하는 방식이 아닌 Qt Designer를 통해 QML로 구현한 UI 파일이라고 생각하면 됩니다.

main.py 소스코드

from PyQt5.QtWidgets import QApplication, QWidget, QHeaderView, QAbstractItemView, QTableWidgetItem, QVBoxLayout, QMessageBox
from PyQt5.QtCore import Qt, QDate 
from PyQt5.uic import loadUi
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import sys
import datetime
import openapi_function as func

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)    

class Form(QWidget):
    def __init__(self):
        super().__init__()
        loadUi('form.ui', self)

        self.setWindowTitle('Ocean Coding School')

        # 기준일 컨트롤 초기화
        dt = datetime.datetime.now()
        self.date_std.setDate(QDate(dt.year, dt.month, dt.day))

        # 챠트 컨트롤 설정
        self.fig = plt.Figure()
        self.canvas = FigureCanvasQTAgg(self.fig)
        vbox = QVBoxLayout()
        vbox.addWidget(self.canvas)
        self.chartbox.setLayout(vbox)        

        # 테이블 초기화
        self.initTable()        

        # Signal, Slot
        self.btn_update.clicked.connect(self.clickUpdateBtn)

    def clickUpdateBtn(self):
        self.table.clear()
        self.initTable()        

    def initTable(self):
        # 기준일 문자열 얻기 (ex.20200828)
        date = self.date_std.date().toString('yyyyMMdd')        

        # COVID 기본 현황 얻기
        lst, _query, _response = func.getCovidInfo(date)         
        
        if not lst:
            #delete QTableWidget, chart
            self.table.setRowCount(0)
            self.table.setColumnCount(0)
            self.fig.clear()
            self.canvas.draw()
            QMessageBox.information(self, 'Error', '당일 데이터가 오후에 갱신되므로, 조회 기준일을 하루전으로 돌려 시도바랍니다')
            return

        row = len(lst)
        col = len(lst[0])

        # QTableWidget 초기화
        self.table.setRowCount(row)
        self.table.setColumnCount(col)

        label = ('등록일시', '사망자수', '확진자수', '시도명(한글)',  '전일대비 증감수', '격리 해제수','격리중 환자수', '지역발생수', '해외 유입수', '10만명당 발생률')
        self.table.setHorizontalHeaderLabels(label)        

        # QTableWidget 쓰기
        for r in range(row):
            for c in range(col):                
                self.table.setItem(r, c, QTableWidgetItem(lst[r][c]))
        
        # QTableWidget 옵션
        self.table.setAlternatingRowColors(True)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)        

        header = self.table.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)       
        header.setSectionResizeMode(0, QHeaderView.Stretch)
        
        # 챠트 초기화
        data = {}
        col_city = 3
        col_num = 4

        for r in range(row-1):
            key = lst[r][col_city]
            val = lst[r][col_num]
            data[key] = val

        self.initChart('시도별 확진자', data)

    def initChart(self, title, data):
        self.fig.clear()
        #matplotlib 한글 폰트
        path = 'C:/Windows/Fonts/malgun.ttf'
        font = fm.FontProperties(fname=path, size=8).get_name()
        plt.rc('font', family=font)

        key = list(data.keys())
        val = list(data.values())
        val = [int(i) for i in val]
        ax = self.fig.subplots()         
        ax.bar(key, val)        
        ax.grid(True, which='major', axis='y', color='y', linestyle='--')
        self.fig.suptitle(title)
        self.canvas.draw()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Form()
    w.show()
    sys.exit(app.exec_())

[라인 1~9]

  • 필요한 모듈, 패키지를 불러오기

[라인 14~35]

  • 위젯 클래스 생성자 함수, Qt designer에서 생성한 *.ui 파일을 로드해 UI를 구성
  • QDateEdit 컨트롤 오늘 날짜로 초기화, matplotlib 차트를 붙이기 위한 FigureCanvasQTAgg 객체 생성
  • initTable() 함수를 호출해 QTableWidget을 초기화



[라인 37~39]

  •  새로 고침 버튼을 누르면 호출되는 슬롯함수

[라인 41~90]

  • QDateEdit 에서 오늘 날짜를 읽어와 문자열 변환 (Ex 20200831)  

  • openapi_function.py에 선언된 getCovidInfo() 함수 호출, XML로 얻어낸 COVID-19 정보를 가공된 리스트 형태로 리턴 (자세한 정보는 OPEN API의 참조문서 참조)

<OPEN API에서 수신된 원본 XML 데이터>

<원본 XML을 파싱 처리한 2차원 리스트>

  •  가공 처리된 2차원리스트 정보를 QTableWidet에 쓰기

  • 차트에 쓰기 위해 리스트 정보를 딕셔너리로 가공( {제주:6, 경남:1, 경북2 ...} 등)
  •  InitChart() 함수에 {지역명:확진자수} 딕셔니리를 넘겨 챠트 그리기

 [라인 92~106]

  •  matplitlib 한글 폰트 설정
  • 딕셔너리 이용, Bar 차트로 지역별 확진자 수 그리기


 

openapi_function.py 소스코드

import requests
import datetime
import xml.etree.ElementTree as et

def getCovidInfo(date):   

    # make query
    BASE = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19SidoInfStateJson?'
    KEY = 'serviceKey=서비스키'
    PAGE = '&pageNo=1'
    NUMROW = '&numOfRows=10'
    DATE = f'&startCreateDt={date}&endCreateDt={date}'      
    
    # send query and get response
    URL = BASE + KEY + PAGE + NUMROW + DATE
    response = requests.get(URL)    

    _query = str(URL)
    _response = response.content.decode()    

    # xml parsing
    lst = xmlParcer(response.content.decode(), 'item')    

    return lst, _query, _response


def xmlParcer(xmlstr, targetTag=''):
    lst = []    
    root = et.fromstring(xmlstr)
    for tag in root.iter(tag=targetTag):
        temp = []
        for k, subtag in enumerate(tag.iter()):
            if tag!=subtag and k!=5 and k!=6 and k!=13 and k!=14and k!=15:                
                #temp.append(dict({subtag.tag:subtag.text}))
                temp.append(subtag.text)
        lst.append(temp)
                
    return lst
[라인 5~24]
  • OPEN API QUERY 데이터 생성
  • requests 모듈로 query 전송 후 request.content에 수신된 XML 응답데이터 저장

  • xml 파싱을 위해 바이트->문자열 변환(decode) 후 xmlParcer() 함수 호출

 [라인 27~38]

  • XML 응답 데이터 중 필요한 부분의 Tag만 추출해 2차원 리스트에 저장
  • XML 응답 데이터 중 필요없는 데이터는 Tag:gubunEn(영문도시명), Tag:gunbunCn(중문도시명), Tag:seq(게시물 번호), Tag:stdDay(기준일) Tag:updateDb(수정일시)이며 순서상 5, 6, 13, 14, 15번

 

이상으로 소스코드 분석을 마칩니다.

감사합니다.


[개발환경]

  • Windows 10 pro, Python 3.7(64bit)
  • MS Visual Studio 2017
  • PyQt5 5.14.2, Matplotlib 3.2.0, requests 2.22.0

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)