트위터로 trpg/게임용 봇을 만들어보자 [2.3] 봇 출력 텍스트 만들기

오터넛·2022년 5월 7일
0

💡 이 글은 [2] 자동 응답하기 에서 이어지는 프로그래밍 초심자를 위한 봇 출력 텍스트 튜토리얼 페이지입니다. 코드 사용에 대한 간략한 설명을 바라시면 소숫점이 붙은 목차는 건너뛰고 [3] 구글 시트 연동하기로 이동하시면 됩니다.

1. 단순 답변 보내기

트위터 api v1은 봇을 이용하여 트윗을 작성하기 위해 다음의 코드 도구를 제공합니다.

API.update_status(status, *, in_reply_to_status_id, auto_populate_reply_metadata, exclude_reply_user_ids, attachment_url, media_ids, possibly_sensitive, lat, long, place_id, display_coordinates, trim_user, card_uri)
출처: https://docs.tweepy.org/ko/v4.8.0/api.html#tweepy.API.update_status

이와 같은 api 문서는 다음의 구성으로 이루어져있습니다.

object.function(function parameters)

위의 구조와 비교하면 api 는 object, update_status 는 funcion, 괄호 안은 function parameters 라고 이해하면 됩니다. object는 우리가 만든 client/api와 같이 어떤 명령을 보내기 위해 만들어 놓은 계정 같은 개념입니다. 이 계정이 들고 있는 잡다한 기능들, '코드 도구'라고 말한 모든 것들을 프로그래밍 용어로 function 이라고 부릅니다.

function을 사용하기 위해서는 그 function을 소유한 계정/object 와 function이 필요로 하는 재료들이 필요합니다. 재료는 괄호 안에 들어있는 function parameters 입니다.

콤마로 구분해 놓은 하나하나가 전부 재료입니다. 하지만 저걸 전부 넣으려면 머리가 아프겠죠? 안심하세요. 전부 필요한 값은 아닙니다.

링크를 통해 문서에 들어가면 각각의 재료(우리는 이걸 '함수값(파라미터)'이라고 부릅니다.) 어떤 파라미터가 필수인지 아닌지 그리고 어떤 값을 넣어야하는지를 상세히 설명하고 있습니다.

우리의 목표는 봇에게 특정한 [키워드]를 보낸 멘션에 답을 주는 것이므로, 괄호 안에서 필요한 값은 status(트윗 내용)와 in_reply_to_status_id(답글을 보낼 트윗 id)입니다.

💡 요약!

  • api 를 통해 봇의 동작을 하기 위해서는 api와 api가 들고 있는 기능(=function), 기능을 위한 재료(=function parameter)가 필요하다.
  • function에는 . 을 통해 접근하고(ex. api.update_statue)
  • function parameter는 괄호로 넘겨준다.
  • 이를 모두 종합하면 아래와 같은 코드가 나온다.
  • api.update_status(status, in_reply_to_status_id)

가장 간단하게 아래의 명령어를 수행하면 봇은 타임라인에 누구도 태그하지 않고 혼잣말을 합니다.

api.update_status(status="오늘 저녁 뭐 먹지?")

그리고 텍스트에 @user_id를 추가하면 해당 유저를 태그하는 트윗이 됩니다. 여기에 구체적으로 '어떤 트윗'에 답글을 할 것인지를 포함하면 해당 트윗에 대한 '답글'이 됩니다.

예시코드

api.update_status(status="@twitter_bot 마라탕 먹자", in_reply_to_status_id=1293043275618)

한 가지 귀찮은 점은, api v1에서는 in_reply_to_status_id 와 @user_id 태그가 동시에 이루어져야 한다는 것입니다.

V2에서는 답변할 멘션의 아이디만 추가해도 답변이 가도록 function을 개선하였을 뿐만 아니라, 타래로 멘션을 이어갈 수 있는 기능(v1에서는 봇이 자신에게 답한 멘션에는 답을 할 수 없습니다.) 까지 생겼지만 우리는 v1을 사용합니다. 왜냐하면 v2로는 이미지 업로드가 불가능하기 때문입니다.

물론 v2를 사용해서 이미지를 올릴 수 있는 방법은 있지만 그렇게하면 코드가 너무 더러워집니다. 따라서 트위터에서 v2 이미지 업로드 기능을 추가해주기 전까지는 v1을 사용할 예정입니다. (혹은 요청이 많아지면 진지하게 고려해보겠습니다.)

💡 주의 사항: 답글을 보내기 위해선 텍스트 가장 앞에 반드시 @user_id 가 포함되어야한다.
+) 현재 봇에는 자신에게 온 답글에 답을 다는 기능이 구현되지 않았다. (트위터잘못이다.)

2. 이미지 첨부하여 답글 보내기

그렇다면 이미지를 첨부하려면 어떻게 해야할까요? v2에서는 하나의 function에 parameter 로 image를 넘겨주는 것으로 간단하게 해결이 가능하지만 v1에서는 다음의 새로운 function을 사용해야합니다.

API.update_status_with_media(status, filename, *, file, possibly_sensitive, in_reply_to_status_id, lat, long, place_id, display_coordinates)
출처: https://docs.tweepy.org/ko/v4.8.0/api.html#tweepy.API.update_status_with_media

여기서 우리가 사용해야할 parameter 는 위에서 사용했던 status, in_reply_to_status_id 에 더해 filename입니다.

앞의 두 값은 1번에서와 동일하게 작성하면 됩니다. 중요한 건 filename 인데, 이 filename 은 이전 Authentification 문서에서 다뤘던 경로와 동일하게 작성하면 됩니다.

저는 이 프로젝트에서 src/tweetBot/images에 이미지들을 모두 저장하고 있기 때문에 예를 들어 lottery.png 라는 이미지를 업로드하기 위해서는 다음의 filename을 사용할 것입니다.

filename="src/tweetBot/images/lottery.png"

위의 변수까지 포함하여 코드를 작성하면 아래와 같은 코드가 완성됩니다.

api.update_status_with_media(
	status="이번주 로또번호는?",
    in_reply_to_status_id=1293043275618,
    filename="src/tweetBot/images/lottery.png"
)

💡 요약

  • v1에서는 update_status_with_media 함수(function)를 통해 이미지가 포함된 트윗을 작성할 수 있다.
  • filename 은 프로젝트 root로부터의 경로이다.

이제 다음으로 랜덤한 값을 생성하는 트윗을 작성해봅시다.

3. 답변으로 랜덤한 값 보내기

이에 대한 예시로 [로또뽑기] 기능과 '오늘의운세' 의 구현을 설명하겠습니다.

2.1 [로또뽑기]

로또뽑기는 다음의 기능을 수행합니다.

  1. 1에서 45까지 랜덤한 값을 생성한다.
  2. 이 값을 유저에게 이미지와 함께 보낸다.

2의 내용은 위에서 설명한 것들이니 1의 코드를 작성해봅시다.

파이썬에서는 랜덤한 숫자를 뽑는 도구(function)을 제공해주고 있습니다. 그렇지만 이 도구들을 사용하기 위해서는 특정한 라이브러리가 필요합니다. [1] Authentification에서 설명했던 라이브러리에 대해서 기억하나요? 이 라이브러리를 불러오는 코드는 아래와 같습니다.

from random import sample

해석하면 'random'이라는 라이브러리로부터 sample이라는 함수를 꺼내오겠단 뜻입니다. 이 sample 함수는 특정한 범위에 대해서 몇 개의 숫자를 꺼낼 것인지를 parameter로 필요합니다.

random.sample 참고: https://docs.python.org/ko/3/library/random.html#functions-for-sequences

이때 범위는 range(start, end) 로 표기하며, start 이상 end 미만의 수를 범위로 가집니다. 그러니 우리가 원하는 1과 45사이의 숫자는 아래와 같이 표기할 수 있습니다.

randoms = sample(range(1, 46), 10)

이렇게 만든 random 은 [1, 2, 3, 4, ... , 10] 과 같은 형태로 숫자를 저장합니다. 그리고 이것은 숫자가 아닙니다. list라는 자료형으로, 간단히 설명하면 숫자들을 넣어둔 상자와 같습니다.

그렇다면 마지막으로 이렇게 뽑은 숫자를 텍스트로 바꾸려면 어떻게 해야할까요?
파이썬에서는 join 이라는 함수를 통해 list의 숫자들을 텍스트로 옮길 수 있습니다. 다음으 그 코드입니다.

number_script = ', '.join(str(random) for random in randoms)

해석하면 randoms 안에 있는 random한 숫자를 차례대로 ", " 로 구분하여 나열하겠다. 라는 뜻입니다.

number_script를 출력하면 아래와 같습니다.

"1, 2, 3, 4, 5, 6, 7, 8, 9, 10"

", " 대신 "/" 를 사용하면 숫자는 아래와 같이 /로 구분됩니다.

"1/2/3/4/5/6/7/8/9/10"

이제 답글로 보내줄 텍스트 작성을 완료했으니 위에서 사용했던 update_status 함수를 이용하여 답글을 보내봅시다. 모두 합치면 아래와 같은 코드가 됩니다.

api.update_status_with_media(
	status="@user_id"+number_script,
    in_reply_to_status_id=1293043275618,
    filename="src/tweetBot/images/lottery.png"
)

3.2 오늘의 운세 출력하기

다음은 오늘의 운세를 출력해봅시다.
오늘의 운세에 대한 동작은 아래와 같습니다.

  1. 1~1000사이에 랜덤한 숫자를 하나 뽑는다.
  2. 이때 숫자가 1이면 [극악의 운], 2~49이면 [대흉], 50~199 이면 [중흉], 200~499이면 [흉], 500~799이면 [길], 800~949 이면 [중길] 그 이상이면 [대길]을 출력합니다.
  3. 길의 종류와 함께 짧막한 텍스트를 함께 넣어줍니다.

1은 위에서 사용한 random 라이브러리에서 randint 라는 함수로 구현했습니다. randint는 랜덤한 범위에 대해서 랜덤한 정수 하나를 추출해줍니다. 범위는 sample 함수와 다르게 range(start, end) 에 대해 start 초과, end 이하의 숫자를 추출하므로 코드는 아래와 같습니다.

value = randint(0, 1000)

다음으로 이렇게 뽑은 숫자가 어느 범위에 속하는지를 걸러내는 코드가 필요한데, python 에서는 if 와 elif 문을 사용하여 이를 해소합니다.

if 다음에 나오는 문장이 참이면 그 아래를 수행하고, 아니면 넘어갑니다. elif 는 else if 의 줄임말로 if 문이 거짓일 때 수행되며 똑같이 그 다음 문장이 참이면 수행하고 아니면 넘어갑니다.

이를 이용해서 우리가 짜려는 코드를 구성하면 아래와 같은 논리가 됩니다.

  1. value 가 1이면 [대흉] 축하합니다!!! 0.01%의 확률을 뚫은 불운!!! 을 출력
  2. 그게 아니고 2~49 의 숫자이면 [대흉] 대흉과 대길은 확률이 같습니다. 힘내세요 ㅠ^ㅠ 을 출력
  3. 그것도 아니고 50~199 사이의 숫자라면 "[중흉] 오늘은 조금 주의해야겠어요."을 출력
  4. 이와 같은 방법으로 대길까지의 텍스트를 작성한다.

이를 코드로 옮기면 아래와 같이 됩니다.

        if value == 1:
            reply_text= "축하합니다!!! 0.01%의 확률을 뚫은 불운!!!"
        elif value < 50:
            reply_text= "[대흉] 대흉과 대길은 확률이 같습니다. 힘내세요 ㅠ^ㅠ"
        elif value < 200:
            reply_text= "[중흉] 오늘은 조금 주의해야겠어요."
        elif value < 500:
            reply_text= "[흉] 이 정도면 나쁘지 않은 운이네요"
        elif value < 800:
            reply_text= "[길] 상쾌한 하루가 되겠네요!"
        elif value < 950:
            reply_text= "[중길] 0.5% 확률을 뚫은 행운아!"
        else:
            reply_text "[대길] 축하합니다!! 로또보단 낮지만 어쨌든 엄청난 확률을 손에 쥐셨습니다!"

이렇게 넘겨받은 텍스트는 이미지가 없으므로, update_status 함수로 트윗을 남기면 됩니다.

api.update_status(status="@user_id" + reply_text, in_reply_to_status_id=1293043275618)

이제 마지막으로 두 개의 기능을 모두 합쳐 [로또뽑기] 와 오늘의 운세에 모두 반응하는 코드를 작성해봅시다.

4. 기능 합치기

기능을 합치는 것은 간단합니다. 이전 글에서 유저가 보낸 트윗을 분석해서 그로부터 키워드를 가져왔던 코드를 기억하나요? 해당 코드는 아래와 같았습니다.

task_name = ""
for keyword in keywords:
    if keyword in tweet.text.lower():
         task_name = keyword

이제 task_name이 무엇인지를 확인해서 어떤 기능을 수행할지를 결정하면 됩니다.
이때 사용할 명령어는 위에서 썼던 if 입니다. 논리절차는 아래와 같습니다.

  1. task_name 이 "[로또뽑기]"이면 [로또뽑기]를 출력하고
  2. 그렇지 않고 task_name 이 "오늘의운세" 라면 오늘의 운세를 출력한다.

이를 코드로 나타내면 아래와 같습니다.

if task_name == "[로또뽑기]":
	randoms = sample(range(1, 46), 10)
    number_script = ', '.join(str(random) for random in randoms)
    
    api.update_status_with_media(
	status="@user_id"+number_script,
    in_reply_to_status_id=1293043275618,
    filename="src/tweetBot/images/lottery.png"
)

elif task_name =="오늘의운세":
        if value == 1:
            reply_text= "축하합니다!!! 0.01%의 확률을 뚫은 불운!!!"
        elif value < 50:
            reply_text= "[대흉] 대흉과 대길은 확률이 같습니다. 힘내세요 ㅠ^ㅠ"
        elif value < 200:
            reply_text= "[중흉] 오늘은 조금 주의해야겠어요."
        elif value < 500:
            reply_text= "[흉] 이 정도면 나쁘지 않은 운이네요"
        elif value < 800:
            reply_text= "[길] 상쾌한 하루가 되겠네요!"
        elif value < 950:
            reply_text= "[중길] 0.5% 확률을 뚫은 행운아!"
        else:
            reply_text "[대길] 축하합니다!! 로또보단 낮지만 어쨌든 엄청난 확률을 손에 쥐셨습니다!"
            
       api.update_status(status="@user_id" + reply_text, in_reply_to_status_id=1293043275618)
    

여기서 좀더 깔끔한 코드를 쓰기 위해서는 아래와 같은 고찰이 필요합니다.

  1. 위의 과정에서 겹치는 동작이 있는가?
  2. 특정 동작을 하나로 묶어서 이름을 붙이면 더 이해하기 쉬운 코드가 되지 않을까?

위의 고찰들을 자세히 설명하고 이를 이해하기 위해서는 좀더 많은 코드 경험이 필요하며 여기서는 자세히 다루지 않을 것이지만 이를 간단히 설명하면,

우리가 라이브러리로부터 특정 도구(함수)들을 불러왔듯이 우리도 직접 함수를 만들고 이를 이름으로 붙일 수 있습니다.

함수의 생성 목표는 '공통된 동작'을 일반화된 이름으로 묶어서 코드의 길이를 줄이고 정갈된 형태로 코드를 작성하는 것. 이고 또 이렇게 만들어진 함수들 중 비슷한 역할을 하는 함수들을 하나의 class 라는 도구함에 넣어 라이브러리처럼 사용하는 것으로 코드를 더 높은 단계로 개선시킬 수 있습니다.

제 코드는 위의 과정들을 통해 만들어진 코드들로 지금까지 설명한 내용은
src/tweetBot의 activities.py 와 update_tweet.py 에 분산되어 작성되었습니다.

이에 대해 좀더 자세히 이해하고 싶다면 Python function 과 class 에 대해 공부해보시기 바랍니다!

Q. 그럼 프로젝트에 새로운 기능을 추가하고 싶다면 어떻게 해야하나요?
이제부터 그 방법을 설명해드리겠습니다.

5. 프로젝트에 새 기능 추가하기

제 프로젝트는 위에서 말했듯 각각의 기능들이 세분화되어있습니다. 하지만 이러한 세분화는 코드의 개선을 위해서 뿐만이 아니라 제 프로젝트를 사용하는 이들의 편의를 위해서이기도 합니다.

간단히 말하면, 프로젝트의 동작 방식을 설명하기 위해 위에서 길게 트윗을 보내는 방법에 대해 설명했지만 제 프로젝트에 '로또뽑기'라는 새로운 기능을 추가하기 위해서는 아래의 코드만 작성하면 됩니다.

elif task_name == "[로또뽑기]":
    randoms = sample(range(1, 46), 10)
    number_script = ', '.join(str(random) for random in randoms)
    reply_image = "lottery.png"
    reply_comment = "@%s " % user_id + number_script

모든 봇의 동작은 '보낼 트윗 내용'과 '이미지'를 생성하여 함수에 보내는 것이므로, 저는 elif로 새로운 task_name을 감지하고 해당 키워드를 받았을 때 출력할 텍스트를 제가 미리 지정해둔 'reply_comment'라는 곳(변수)에 입력합니다. 이미지는 'reply_image'에 이름을 확장자명과 함께 적습니다.

경로는 제가 위에서 이미 따로 선언을 해두었기 때문에 적을 필요 없고 새로운 이미지를 src/tweetBot/images에 넣은 후 이름만 적어넣어두면 코드는 정상 작동합니다.

여기까지 배웠다면 여러분은 랜덤한 트윗을 마음대로 작성할 수 있습니다. 하지만 랜덤한 내용을 코드에 덕지덕지 적었다가는 분명 나중에 머리가 아플 것입니다. 다채로운 문장을 구사할 수도 없고요.

어떻게 하면 진짜 게임처럼 다양한 문장들을 봇이 구사할 수 있게 할 수 있을까요?

이에 대한 방법이 다음부터 설명할 '구글 스프레드 시트'와의 연동입니다.😎

profile
당신의 친절한 오타쿠

0개의 댓글