기획의도
입대해 본 분은 아시겠지만 훈련소에서는 저녁에 틀어주는 뉴스 외에는 바깥 소식을 접할 기회가 많이 부족합니다. 따라서 매일 받는 인편 한 통이 큰 위안이 됩니다. 입대를 앞둔 필자는 훈련소에서도 최신 정보와 금융 시세를 놓치고 싶지 않았습니다. 하지만 매일 지인에게 뉴스를 스크랩해서 보내 달라고 할 수는 없는 노릇이니 무언가 자동화할 방법이 필요했습니다. 이에 입대 며칠 전, Python으로 인편 전송을 자동화하여 분야별 일일 뉴스 헤드라인과 가상화폐 시세를 받아볼 수 있도록 기획 및 구현해 보았습니다. 결론부터 말하자면 결과는 매우 성공적이었고, 매일 빠짐없이 고급(?) 정보를 챙겨볼 수 있었습니다. 다음은 실제 전송된 인편 사진입니다.
본 게시글에서는 이를 어떻게 구현했는지 설명할 것입니다. 시작하기 전, 본 프로젝트는 저와 비슷한 아이디어를 구현한 https://github.com/SyphonArch/news-relay와 https://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으로 코드를 받아오고 백그라운드에서 실행시켰습니다. 더 이상 무언가를 수정할 시간도 없었고, 테스트도 불가능했기 때문에 제가 할 수 있는 일은 여기까지였습니다. 입대 후 인편이 오기까지만 손꼽아 기다렸는데, 정작 편지함이 개설된 지 며칠간은 프로그램이 보낸 인편이 오지 않아 역시 잘못되었구나 생각했습니다. 다행히도 무슨 이유인지는 모르겠으나 그 이후부터는 문제없이 매일 인편을 받아볼 수 있었습니다.
전역을 앞둔 지금, 입대 전의 기억을 더듬어 최대한 자세히 작성하려고 노력해 보았습니다. 지금은 육군훈련소에서 휴대폰을 불출해 주므로 굳이 이런 일을 할 필요가 없다고 하던데, 아직 휴대폰 사용이 안 되는 사단 신교대로 입대하는 분이라면 여전히 유용할 것입니다. 이 글이 곧 입대하는 분들에게 많은 도움이 되길 바랍니다.
'프로젝트' 카테고리의 다른 글
renderlapse: Create timelapse video from Minecraft world backups (0) | 2022.10.09 |
---|