프로젝트

Python과 thecampy로 육군훈련소 인편 자동화하기

moon44432 2022. 10. 9. 00:51

기획의도

입대해 본 분은 아시겠지만 훈련소에서는 저녁에 틀어주는 뉴스 외에는 바깥 소식을 접할 기회가 많이 부족합니다. 따라서 매일 받는 인편 한 통이 큰 위안이 됩니다. 입대를 앞둔 필자는 훈련소에서도 최신 정보와 금융 시세를 놓치고 싶지 않았습니다. 하지만 매일 지인에게 뉴스를 스크랩해서 보내 달라고 할 수는 없는 노릇이니 무언가 자동화할 방법이 필요했습니다. 이에 입대 며칠 전, Python으로 인편 전송을 자동화하여 분야별 일일 뉴스 헤드라인과 가상화폐 시세를 받아볼 수 있도록 기획 및 구현해 보았습니다. 결론부터 말하자면 결과는 매우 성공적이었고, 매일 빠짐없이 고급(?) 정보를 챙겨볼 수 있었습니다. 다음은 실제 전송된 인편 사진입니다.

뉴스 헤드라인
가상화폐 시세 정보

본 게시글에서는 이를 어떻게 구현했는지 설명할 것입니다. 시작하기 전, 본 프로젝트는 저와 비슷한 아이디어를 구현한 https://github.com/SyphonArch/news-relayhttps://github.com/calofmijuck/letter를 참고했음을 미리 밝힙니다.

thecampy로 기본적인 Sender 만들기

인편을 전송하기 위해 선택한 라이브러리는 thecampy였습니다. thecampy는 국군 온라인 소통 앱인 더 캠프의 파이썬 라이브러리로, 앱을 실행하지 않고도 인터넷 편지를 보낼 수 있도록 해 줍니다. 편지를 보내기 위한 코드는 다음과 같습니다.

import thecampy

my_soldier = thecampy.Soldier('이름')
msg = thecampy.Message('test', 'test')

tc = thecampy.Client(email, pw)
tc.get_soldier(my_soldier) # returns soldier_code
tc.send_message(my_soldier, msg)

Client로는 카카오톡 소셜 로그인 계정이 아닌 이메일을 사용하는 계정을 요구했는데, 간단하게 어머니의 계정을 활용했습니다.[각주:1]

다음은 thecampy를 이용한 편지 Sender입니다. string을 받아, 내용이 너무 길거나 줄 바꿈이 너무 많은 경우에는 자동으로 분할 전송하도록 구현되어 있습니다.[각주:2]

from datetime import datetime
import thecampy

SOLDIER = thecampy.Soldier(
    'XXX', 200XXXXX, 202XXXXX, '육군훈련소'
)


def send(subject, content):
    content += '\n[FINISH]'
    msg_num = 0
    char_count, enter_count = 0, 0
    buffer = []
    for i, char in enumerate(content):
        buffer.append(char)
        char_count += 1
        if char == '\n':
            enter_count += 1

        if char_count > 1490 or enter_count > 22 or i == len(content) - 1:
            _send(subject + f" - {msg_num}", ''.join(buffer))

            char_count, enter_count = 0, 0
            buffer = []

            msg_num += 1


def _send(subject, content):
    try:
        message = thecampy.Message(subject, content)
        client = thecampy.client()
        client.login('john@doe.com', '1q2w3e4r')
        client.get_soldier(SOLDIER)
        client.send_message(SOLDIER, message)
        print(f"[+] {datetime.now()} - Successfully sent")
    except:
        print(f"[-] {datetime.now()} - Failed to send")

네이버 뉴스 크롤링해서 전송하기

인편에 담을 뉴스 내용은 네이버에서 크롤링해 오기로 결정했습니다. BeautifulSoup 라이브러리를 활용하여 정치, 경제, 사회, IT/과학 섹션에서 각각 몇 개의 기사를 불러온 후, 제목과 내용을 문자열로 추출하여 약간의 클리닝을 거쳤습니다. 마음 같아서는 기사 전문을 보내고 싶었으나, 인편을 출력하는 종이를 아끼기 위해 한 기사당 수백 글자 정도로 제한을 둘 수밖에 없었습니다. 다음은 네이버 헤드라인 뉴스와 기사 내용을 크롤링하여 전송하는 코드입니다.

from datetime import datetime
from sender import send
from bs4 import BeautifulSoup
import re
import requests


def send_news():
    base_address = 'https://news.naver.com/main/list.naver?mode=LSD&mid=sec&sid1=10'

    def get(url):
        response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})
        html = response.text
        soup = BeautifulSoup(html, 'html.parser')
        return soup

    def get_body_text(article, chars=400):
        html = str(article.find('div', {'id': 'dic_area'}))
        edited_html = re.sub('<br/>', '\n', html)
        content = BeautifulSoup(edited_html, 'html.parser').text
        formatted = re.sub('\n', ' ', content)
        formatted = re.sub('\t', '', formatted)
        formatted = re.sub(' +', ' ', formatted)

        return formatted[:chars].strip()


    output = []

    for i in [0, 1, 2, 5]:
        soup = get(base_address + str(i))
        section_name = soup.find('title').text.split(' ')[0]
        print(section_name)
        output.append(f"[[{section_name}]]")
        articles = soup.find_all('dt', {'class': ''})

        article_cnt = 0
        for item in articles:
            try:
                item = item.find('a', {'class': 'nclicks(fls.list)'})
                title = re.sub('\t', '', item.text)
                url = re.sub('\t', '', item.attrs['href'])
                
                article = get(url)
                output.append(title)
                output.append(get_body_text(article))

                article_cnt += 1
            except:
                continue
            if article_cnt is 5:
                break

    output = '\n'.join(output)

    now = '{:%Y-%m-%d_%H}H'.format(datetime.now())
    send(f'[NEWS] NEWS OVERVIEW ({now})', output)

전송되는 내용은 대략 다음과 같습니다.

[[정치]]

 與 “이재명, ‘불법리스크’ 감추려 물타기…野 선동 속지 않을 것”

“이 대표의 반일선동은 ‘죽창가 시즌2’ 文정권 북한바라기, 탄도미사일만 남겨” 국민의힘은 8일 더불어민주당 이재명 대표
......

가상화폐 시세와 지갑 잔고 받아와서 전송하기 - Upbit와 Coinone

뉴스 이외에도, 주가 지수나 개별 주가, 가상화폐 시세 등 금융 시세를 받아오는 기능도 추가하기로 했습니다. 필자의 경우, 주식에 대해서는 다른 경로로도 정보를 얻을 수 있었기 때문에 가상화폐 시세와 지갑 잔고 정도만 받아오도록 구현했습니다. 가상화폐 시세를 받아오는 것 자체는 업비트에서 제공하는 REST API를 사용하여 원하는 티커(예: KRW-BTC)를 집어넣는 것이 전부여서 매우 단순했습니다.

def get_from_upbit(ticker):
    urlTicker = urllib.request.urlopen('https://api.upbit.com/v1/ticker?markets='+ticker)
    readTicker = urlTicker.read()
    jsonTicker = json.loads(readTicker)

    data = []

    data.append(float(jsonTicker[0]['opening_price']))
    data.append(float(jsonTicker[0]['high_price']))
    data.append(float(jsonTicker[0]['low_price']))
    data.append(float(jsonTicker[0]['trade_price']))
    data.append(float(jsonTicker[0]['signed_change_rate']))

    return data

코인원에서 시세를 받아오는 것 또한 업비트의 경우와 거의 같았습니다. 데이터를 파싱하는 방법이 조금 다른 것 이외에는 특별한 차이가 없습니다.

def get_from_coinone(ticker):
    urlTicker = urllib.request.urlopen('https://api.coinone.co.kr/ticker/?currency='+ticker)
    readTicker = urlTicker.read()
    jsonTicker = json.loads(readTicker)

    data = []

    l = float(jsonTicker['last'])
    yc = float(jsonTicker['yesterday_last'])
    data.append(float(jsonTicker['first']))
    data.append(float(jsonTicker['high']))
    data.append(float(jsonTicker['low']))
    data.append(l)
    data.append(float((l - yc) / yc))

    return data

다만 pyupbit 라이브러리로 업비트에서 지갑 잔고를 받아오는 과정은 조금 복잡했습니다. 우선 업비트 사이트에서 API 사용 신청을 한 후 Access와 Secret 코드를 발급받아야 했습니다. API를 호출할 컴퓨터의 IP 주소까지 요구했는데, 홈 서버의 IP를 넣자니 인편 하나를 위해 컴퓨터를 몇 달 동안 켜 둘 수는 없었으므로, 예전부터 돌아가고 있던 무료 AWS 인스턴스(EC2)를 사용하기로 했습니다.

다음은 가상화폐 시세와 업비트 잔고 정보를 함께 전송하는 코드입니다.

from datetime import datetime
from sender import send
from get_price import *
import pyupbit

access = ""
secret = ""
upbit = pyupbit.Upbit(access, secret)


def get_data(market, ticker):
    tk = [ticker]
    if market == 'coinone':
        return tk + get_from_coinone(ticker)
    if market == 'upbit':
        return tk + get_from_upbit(ticker)

def send_crypto_info():
    crypt = []

    crypt.append(get_data('upbit', 'KRW-BTC'))
    crypt.append(get_data('upbit', 'KRW-ETH'))
    crypt.append(get_data('upbit', 'KRW-ETC'))
    crypt.append(get_data('upbit', 'KRW-XRP'))
    crypt.append(get_data('upbit', 'KRW-DOGE'))
    crypt.append(get_data('coinone', 'klay'))
    crypt.append(get_data('coinone', 'ksp'))

    output = ""

    now = '{:%Y-%m-%d %H:%M}'.format(datetime.now())
    output += ('<p>' + now + " 기준</p>")
    output += (f'<p>{"종목명":>8} | {"시초가":>8} | {"고가":>9} | {"저가":>9} | {"현재가":>8} | {"전일대비":>3}</p>')

    for curr in crypt:
        output += (f'<p>{curr[0]:>10} | {curr[1]:>10} | {curr[2]:>10} | {curr[3]:>10} | {curr[4]:>10} | {(curr[5] * 100):.2}%</p>')

    krw = upbit.get_balance("KRW")
    eth = upbit.get_balance("KRW-ETH")
    output += ('KRW:' + str(krw) + ', ETH: ' + str(eth) + '\n')
    output += (' Total(KRW): ' + str(krw + eth * pyupbit.get_current_price('KRW-ETH')) + '\n')

    send(f'CRYPTO OVERVIEW ({now})', output)

매일 정해진 시각에 전송하도록 하기

매일 오전 8시 59분에 인편을 한꺼번에 전송하도록 간단한 반복문을 만들어 두었습니다.

if __name__ == '__main__':
    sent = False
    while True:
        try:
            now = datetime.now()
            if now.hour == 8 and now.minute == 59:
                if not sent:
                    send_news()
                    send_crypto_info()
                    sent = True
            else:
                sent = False
        except Exception as e:
            print(e)

 

업로드 및 실행, 그 후

우선 코드를 GitHub에 올려두고, AWS에 가입한 뒤 프리 티어로 생성할 수 있는 리눅스 인스턴스를 하나 만들었습니다. 필자의 경우 최소 사양인 t2.micro로도 충분했습니다. 인스턴스에 연결한 뒤 Git으로 코드를 받아오고 백그라운드에서 실행시켰습니다. 더 이상 무언가를 수정할 시간도 없었고, 테스트도 불가능했기 때문에 제가 할 수 있는 일은 여기까지였습니다. 입대 후 인편이 오기까지만 손꼽아 기다렸는데, 정작 편지함이 개설된 지 며칠간은 프로그램이 보낸 인편이 오지 않아 역시 잘못되었구나 생각했습니다. 다행히도 무슨 이유인지는 모르겠으나 그 이후부터는 문제없이 매일 인편을 받아볼 수 있었습니다.

전역을 앞둔 지금, 입대 전의 기억을 더듬어 최대한 자세히 작성하려고 노력해 보았습니다. 지금은 육군훈련소에서 휴대폰을 불출해 주므로 굳이 이런 일을 할 필요가 없다고 하던데, 아직 휴대폰 사용이 안 되는 사단 신교대로 입대하는 분이라면 여전히 유용할 것입니다. 이 글이 곧 입대하는 분들에게 많은 도움이 되길 바랍니다.

  1. 참고로, 필자가 본 프로그램을 개발하던 때와는 달리, thecampy의 최신 버전은 더 캠프 시스템 상의 변화로 인해 훈련병 카페 가입을 수동으로 미리 해 주어야 한다고 합니다. 조금은 번거로워질 수 있겠으나, 가족이나 친구의 계정을 이용할 경우 어차피 상대방이 훈련병 등록을 할 것이니 큰 지장은 없을 것입니다. [본문으로]
  2. 구버전 thecampy 기준으로 작성된 코드입니다. [본문으로]