본문 바로가기

카테고리 없음

아리봇 코로나19 기능 정비한 이야기

def corona_parser(today):
    API_KEY = unquote('발급받은 API 키')
    url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
    params = {'serviceKey': API_KEY,
              'pageNo': '1',
              'numOfRows': '1',
              'startCreateDt': '{}'.format(today-7), <=== 문제의 부분
              'endCreateDt': '{}'.format(today)}

처음 디스코드 봇 아리봇에서 코로나19 정보 검색 기능을 만들었을때는 질병관리청 코로나19 정보센터 사이트에서 크롤링해오는 방식을 사용했습니다.

 

 

코로나바이러스감염증-19(COVID-19)

코로나바이러스감염증-19 정식 홈페이지로 발생현황, 국내발생현황, 국외발생현황, 시도별발생현황, 대상별 유의사항, 생활 속 거리 두기, 공적마스크 공급현황, 피해지원정책, 홍보자료, FAQ, 관

ncov.mohw.go.kr

그리고 그게 벌써 2년 전이 되었고 얼마 전 확인해보니 기능이 작동하지 않는 것을 확인했습니다.

2년 사이 상황이 많이 바뀌었고 사이트도 많이 개편되어서 제가 만들어둔 크롤링 코드가 제대로 작동하지 않았기 때문입니다.

 

그래서 다른 방법을 찾아보니 공공 데이터 포털에서 코로나19 확진자 정보를 받아올 수 있었습니다.

크롤링보다 만들기 훨씬 쉬울 것 같기도 하고 xml을 받아 사용하는 건 처음 해보는 일이라 경험삼아 해보기로 했습니다.

 

제가 만들것은 아리봇에게 명령어가 입력되었을 때 오늘의 신규와 누적 확진, 사망 건수를 보여주고, 오늘로부터 일주일 사이의 신규 확진자 추이를 함께 보여주는 기능입니다.

 

공공 데이터 포털 SERVICE_KET_IS_NOT_REGISTERED

 

공공데이터활용지원센터_보건복지부 코로나19 감염 현황

코로나19감염증으로 인한 일별 확진자,완치자,지료중인환자,사망자등에 대한 현황자료(이 제공자료는 관련 발생 상황에 대한 정보를 신속 투명하게 공개하기 위한 것으로, 추가적인 역학조사

www.data.go.kr

여기서는 많은 데이터들을 열람하고 사용할 수 있도록 API들이 제공됩니다. 데이터들은 xml로 제공되므로 쉽게 받아다가 사용할 수 있을 것 같습니다.

 

사용하기 위해서는 활용 신청을 해야합니다.

 

활용 신청을 하고 나면 다음과 같이 바로 API 키가 발급됩니다.

하지만 여기서 인증키를 입력하고 아래의 미리보기에서 검색을 해보면 검색이 되지 않습니다.

 

에러코드 99, SERVICE_KET_IS_NOT_REGISTERED 에러가 발생하게 됩니다.

 

처음에는 API키 인코딩 문제라고 생각했습니다.

import requests

def corona_parser(today):
    url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
    params = {'serviceKey': '발급받은 키',
              'pageNo': '1',
              'numOfRows': '1',
              'startCreateDt': '{}'.format(today-7),
              'endCreateDt': '{}'.format(today)}
    
    req = requests.get(url, params=params)
    soup = BeautifulSoup(req.content, "html.parser")

 

위 코드가 파싱해오기 위한 코드였는데, 인코딩 된 걸 넣든 디코딩 된 걸 넣든 계속 에러가 발생했습니다.

그래서 좀 검색하다보니 urllib의 unquote를 사용해서 인코딩을 건너뛰고 requests에서 요청할 수 있도록 처리해야 한다는 것을 알게 되었습니다. 자세한 건 모르겠음 ㅎ

 

import requests
from urllib.parse import unquote

def corona_parser(today):
    API_KEY = unquote('발급받은 API 키')
    url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
    params = {'serviceKey': API_KEY,
              'pageNo': '1',
              'numOfRows': '1',
              'startCreateDt': '{}'.format(today-7),
              'endCreateDt': '{}'.format(today)}
              
    req = requests.get(url, params=params)
    soup = BeautifulSoup(req.content, "html.parser")

 

암튼 이래도 안 됐는데 사실 알고보니 서버에 제가 발급받은 키가 아직 등록되지 않아서 생기는 오류라고 합니다.

신청할 때 안내문에는 1~2시간 정도 지나면 등록 된다고 해서 산책도 다녀왔었는데 저는 하루정도 지나고 나니 정상적으로 작동했습니다.

 

 

데이터 받아오기

정상적으로 파싱해오면 다음과 같은 결과를 받게 됩니다.

<response>
    <header>
        <resultCode>00</resultCode>
        <resultMsg>NORMAL SERVICE.</resultMsg>
    </header>
    <body>
        <items>
            <item>
            <createDt>2022-02-28 08:56:05.34</createDt>
            <deathCnt>8058</deathCnt>
            <decideCnt>3134456</decideCnt>
            <seq>803</seq>
            <stateDt>20220228</stateDt>
            <stateTime>00:00</stateTime>
            <updateDt>null</updateDt>
            </item>
        </items>
        <numOfRows>1</numOfRows>
        <pageNo>1</pageNo>
        <totalCount>1</totalCount>
    </body>
</response>

여기서 제가 필요한 것은 stateDt, decideCnt, deathCnt 세 개 입니다.

 

라이엇API를 사용해서 최근 게임 검색을 만들 때 느꼈던 것은 그 때 그 때 데이터를 파싱해다가 보여주려고 하면 너무 오래 걸린다는 것 이었습니다. API마다 일일 검색 제한이 있기도 하고요.

 

그래서 csv 파일에다가 7일치 데이터를 미리 다운받아놓고 요청이 들어왔을 때는 그냥 거기서 꺼내다가 보여주는 식으로 만들기로 했습니다.

 

def corona_parser(today):
    API_KEY = unquote('발급받은 API 키')
    url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
    params = {'serviceKey': API_KEY,
              'pageNo': '1',
              'numOfRows': '1',
              'startCreateDt': '{}'.format(today-7),
              'endCreateDt': '{}'.format(today)}
    print("Retrieving new COVID-19 data from MOHW")
    try:
        req = requests.get(url, params=params)  # 데이터 받아오기
        soup = BeautifulSoup(req.content, "html.parser")
        corona_data = []

        for i in soup.find_all('item'):  # 날짜, 확진 수, 사망 수 받아오기
            corona_data.append([i.find('statedt').string, i.find('decidecnt').string, i.find('deathcnt').string])

        for i in range(len(corona_data)-1):  # 일별 신규 확진자 계산
            corona_data[i].append(int(corona_data[i][1])-int(corona_data[i+1][1]))
        del corona_data[-1]

        f = open('coronadata.csv', 'w', encoding='utf-8', newline='')  # csv 파일에 쓰기
        wr = csv.writer(f)

        for row in corona_data:
            wr.writerow(row)

        f.close()
        return True
    except Exception as e: 
    	print(e)
        return False

Xml을 받게 되면 이제 거기서 find_all을 이용하여 item 항목들에 있는 것들을 모두 리스트에 넣어주었습니다.

item에 들어있는 것은 저 위에 제가 올려놓은 xml 코드와 동일한 덩어리들 입니다.

 

암튼 저기서 item들을 리스트에 넣고 for문을 이용하여 각 item 들에 들어있는 statedt, decidecnt, deathcnt들을 또 다시 다른 리스트를 만들어 넣어주었습니다. 여기서 대문자를 사용하지 않은 이유는 stateDt로 썼을 때 오류가 났기 때문인데, xml은 대소문자를 구분하지 않는다고 들었는데 대소문자를 구분하더라고요.

 

그 다음에 시행되는 for문은 일별 신규 확진자를 계산하기 위한 것입니다.

맨 위의 결과를 보면 확진 건수가 299만건으로 전체 확진자 수를 보여주는 것을 알 수 있습니다.

 

그래서 전날의 확진 건수에서 오늘의 확진 건수를 빼서 신규 확진 건수를 계산해야 합니다.

처음에 보면 7개의 데이터가 아닌 8개의 데이터를 받아왔는데, 이것도 신규 확진 건수를 계산하기 위한 것입니다.

7개만 받아오면 7일 전의 데이터는 신규 확진건수를 계산할 수 없기 때문입니다.

 

for문에서 리스트에다가 range(7)을 쓰지 않고 range(len(corona_data)-1)을 사용한 이유는 가끔 결과가 6개만 검색되기 때문입니다. 예를 들어 2월 28일 오전 1시에 검색을 했다면 today에 들어가는 것은 20220228일 것입니다.

하지만 보건복지부 데이터에는 아직 20220228이 없기 때문에 처음 요청문에서 startCreatDt를 20220221로, endCreatDt를 20220228로 하더라도 실제 받아온 데이터는 2월 21일부터 2월 27일까지밖에 없습니다.

 

그래서 리스트 길이로 했음.

 

뒤에는 csv에다가 날별로 넣어주는 코드.

 

그래서 csv에 넣으면 다음과 같이 저장됩니다.

 

csv에 담겨있는 일주일치 데이터

데이터 가공

이제 저한테 필요한 것은 아리봇이 디스코드 채팅에 보기 좋게 받아온 데이터들을 보내주는 것입니다.

그러기 위해서는 받아온 데이터들을 잘 깔끔하고 센스있게 텍스트로 만들어주어야 합니다.

 

우선 csv에 써있는 데이터들을 읽어와야 합니다.

def corona_data_csv_reader():
    corona_data_csv_read = open('coronadata.csv', 'r', encoding='UTF-8')
    corona_data_csv = csv.reader(corona_data_csv_read)

    corona_data_table_temp = []

    for line in corona_data_csv:
        corona_data_table_temp.append(line)

    corona_data_csv_read.close()
    print('COVID-19 data successfully read')
    return corona_data_table_temp

읽어 왔습니다.

 

다음은 이 데이터들을 텍스트로 만들고 디스코드 embed에 넣어서 봇으로 보내주는 함수를 만들어야 합니다.

 

def corona_answerer(today):
    corona_data_table = corona_data_csv_reader()

    def embed_maker():
        today_description = '{}년 {}월 {}일 기준 확진자 데이터'.format(corona_data_table[0][0][:4], corona_data_table[0][0][4:6],
                                                            corona_data_table[0][0][6:])
        week_decide_description = '지난 일주일 평균: {:,}명```\n'.format((int(corona_data_table[0][1])-int(corona_data_table[-1][1]))//len(corona_data_table))
        days = 0
        for i in corona_data_table:
            week_decide_description += ' {}-{}-{} (D-{}): {:,}명 \n'.format(i[0][:4], i[0][4:6], i[0][6:], days, int(i[3]))
            days += 1
        week_decide_description += '```'

        corona_embed = discord.Embed(title='코로나19 확진자 데이터', description=today_description, color=0x00FF00)
        corona_embed.add_field(name='신규 확진', value='{:,} 명'.format(int(corona_data_table[0][3])), inline=True)
        corona_embed.add_field(name='누적 확진', value='{:,} 명'.format(int(corona_data_table[0][1])), inline=True)
        corona_embed.add_field(name='누적 사망', value='{:,} 명'.format(int(corona_data_table[0][2])), inline=True)
        corona_embed.add_field(name='최근 7일 신규 확진 수', value=week_decide_description, inline=False)
        corona_embed.set_thumbnail(url='https://cdn.discordapp.com/attachments/667808218910556167/947581730422919328/face-mask.png')
        return corona_embed
        
     ...

 

먼저 데이터를 csv에서 읽어오고

 

함수 안에 또 함수를 만들어서 디스코드 embed를 만드는 기능을 넣었습니다.

 

맨 위의 xml에서 제공되는 확진자 데이터를 보면 3134459 이런 식으로 보기 어렵게 작성되어 있으므로 천 단위 쉼표를 넣어주기 위해서 숫자를 int형식으로 바꾸고 '{:,}'.format(숫자)를 이용해서 천 단위 쉼표를 넣어주었습니다.

 

데이터가 함수를 통해 가공되면 다음과 같이 출력됩니다.

 

버튼은 여기서 만드는 거 아님

다음으로 이제 오늘 날짜를 입력 받았을 때 지금 아리가 가지고 있는 데이터가 오늘 데이터인지 아니거나 없으면 새로 받아오는 작업을 해야 합니다.

 

def corona_answerer(today):
    corona_data_table = corona_data_csv_reader()

    def embed_maker():
        ...

    try:
        if corona_data_table[0][0] == str(today):
            return embed_maker()
        else:
            print("코로나19 데이터와 현재 날짜가 일치하지 않음.\n")
            corona_parser(today)
            corona_data_table = corona_data_csv_reader()
            return embed_maker()

    except IndexError:
        print("코로나19 데이터가 존재하지 않음\n")
        corona_parser(today)
        corona_data_table = corona_data_csv_reader()
        return embed_maker()

    except Exception as e:
        print(e)
        embed = discord.Embed(title="코로나19 데이터 다운로드 실패\n")
        return embed

먼저 가지고 있는 corona_data_table의 0,0에 들어있는 오늘 날짜와 입력된 오늘 날짜를 확인합니다.

일치하면 바로 embed를 만들어 줍니다.

 

그리고 다음으로 데이터가 현재 날짜와 일치하지 않으면 다시 파싱해오고, 데이터를 다시 읽어온 후 embed를 만들어 줍니다.

 

현재 읽어온 데이터가 없을 경우 (csv가 비어있을 경우) IndexError가 뜨게 되므로 이 경우에도 오늘 날짜를 넣고 다시 파싱해온 후 embed를 만들어 줍니다.

 

만약 다 실패하고 뭔가 오류가 뜨면 그냥 다운로드에 실패했다는 메세지가 담긴 embed를 내보내 줍니다.

 

오류 메세지

 

봇에서 명령어 처리

마지막으로 봇에서 명령어를 받고 오늘 날짜를 함수로 넘겨주는 과정이 필요합니다.

from datetime import datetime, timezone, timedelta
from discord.ext import commands
from discord_buttons_plugin import *
from parsers import corona_answerer

@client.command(name='코로나19', pass_context=True)
async def corona_info(ctx):
    today = int(datetime.now(tz=timezone(timedelta(hours=9))).strftime('%Y%m%d'))
    await buttons.send(
        embed=corona_answerer(today),
        channel=ctx.channel.id,
        components=[
            ActionRow([
                Button(
                    label="질병관리청 정보센터",
                    style=ButtonType().Link,
                    url='http://ncov.mohw.go.kr/'
                )
            ])
        ]
    )

"아리야 코로나19"라는 명령어가 입력 되면 먼저 오늘 날짜를 확인합니다.

봇이 구동중인 헤로쿠 서버가 어딨는지는 모르니까 타임존을 설정해줍니다. 아마 미국에 있던 것 같은데 암튼 우린 미국에 없으니까.

타임존은 요즘은 무슨 zoneinfo를 사용하면 지역만 입력해도 된다고 하더라고요. (https://docs.python.org/3/library/zoneinfo.html) 근데 저는 그냥 타임 델타로 했음.

 

버튼은 누르면 질병관리청 정보센터로 이동할 수 있도록 해주겠습니다.

그리고 아까 만들어둔 함수에 오늘 날짜를 넣어서 호출하고 embed를 받아와서 메세지에 박아주면 끝납니다.

 

결과

적용 결과

테스트 봇에서 테스트도 해보고 잘 작동해서 아리봇에 옮겨주었습니다.

 

초기 버전

처음에는 이렇게 나왔었는데 좀 쉼표도 넣고 마크다운도 쓰고 하다보니 꽤 볼만해진 것 같습니다.

오랜만에 아리봇을 사용중이신 서버가 760곳이던데 사용해주셔서 감사합니다.

 


그리고 3월이 되어서 오류가 발생했습니다. 명령어 사용시 제대로 된 값을 보여주지 않고 비어있는 데이터가 파싱되었습니다.

def corona_parser(today):
    API_KEY = unquote('발급받은 API 키')
    url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
    params = {'serviceKey': API_KEY,
              'pageNo': '1',
              'numOfRows': '1',
              'startCreateDt': '{}'.format(today-7), <=== 문제의 부분
              'endCreateDt': '{}'.format(today)}

이유는 대학교 5학년이 했다기엔 정말 유치한 이유였는데 20220301에서 7을 빼면 20220221이 아니라 20220294죠

안타깝게도 지구에 2월 94일은 없기 때문에 비어있는 데이터가 파싱되었고 제대로 작동하지 않아 계속해서 인덱스 에러 부분이 실행되었습니다.

 

그래서 봇 부분에서 datetime으로 7일 차이를 계산하고 아예 parameter를 두 개 넘겨주는 식으로 처리하는 식으로 수정해주었습니다.

 

@client.command(name='코로나19', pass_context=True)
async def corona_info(ctx):
    today = datetime.now(tz=timezone(timedelta(hours=9)))
    startdate = (today - timedelta(days=7)).strftime('%Y%m%d')
    enddate = today.strftime('%Y%m%d')
    await buttons.send(
        embed=corona_answerer(startdate, enddate),
        channel=ctx.channel.id,
        components=[
            ActionRow([
                Button(
                    label="질병관리청 정보센터",
                    style=ButtonType().Link,
                    url='http://ncov.mohw.go.kr/'
                )
            ])
        ]
    )