ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 카카오톡 봇 만들기 ( python, pc 카톡, 비활성 ) - 4편
    python/매크로 2020. 5. 1. 18:54
    반응형

     

    3줄 선 요약

    • 플러스친구 아님
    • 모바일 어플을 활용한 방법 아님
    • pc카톡, python 으로 pc 환경에서 봇을 제작함

    2020/04/21 - [python/매크로] - 카카오톡 봇 만들기 ( python, pc 카톡, 비활성 ) - 1편

    2020/04/24 - [python/매크로] - 카카오톡 봇 만들기 ( python, pc 카톡, 비활성 ) - 2편

    2020/04/27 - [python/매크로] - 카카오톡 봇 만들기 ( python, pc 카톡, 비활성 ) - 3편

     

    1편 에서는 카톡 봇의 기능설명과 메시지 전송,

    2편 에서는 친구 목록 or 채팅방 목록을 선택해서 입장하는 방법,

    3편 에서는 1~2편의 전송 기능을 활용해 간단한 네이버 실시간 검색어 크롤링 알림봇 을 만들어봤다.

     

    이번 4편에서는 채팅방의 채팅 내용을 가져와 지정한 키워드가 나왔는지 체크한 후 기능을 실행하는

    키워드 명령어로 동작하는 카톡 봇을 구현해보려 한다.

     

    이번 글을 좀 자세히 쓸까 하다가 그냥 코드 위주로 설명하려고 합니다.

    이유는 글 마지막에 씀

     

     

    동작 방법

    1. 채팅 목록에서 채팅창을 선택해 열고
    2. 채팅 내용을 클립보드에 저장
    3. 지정한 키워드가 나와있는지 체크
    4. 새로운 채팅인지 확인 방법은, 마지막 채팅 인덱스와 채팅 내용으로 구분
    5. (4)를 위해 초기 저장 필요

     

    채팅 내용을 클립보드로 가져오는 코드

    import time, win32con, win32api, win32gui, ctypes
    from pywinauto import clipboard # 채팅창내용 가져오기 위해
    
    # # 카톡창 이름, (활성화 상태의 열려있는 창)
    kakao_opentalk_name = '메모장'
    chat_command = '실검 알려줘'  # 테스트용..
    
    
    PBYTE256 = ctypes.c_ubyte * 256
    _user32 = ctypes.WinDLL("user32")
    GetKeyboardState = _user32.GetKeyboardState
    SetKeyboardState = _user32.SetKeyboardState
    PostMessage = win32api.PostMessage
    SendMessage = win32gui.SendMessage
    FindWindow = win32gui.FindWindow
    IsWindow = win32gui.IsWindow
    GetCurrentThreadId = win32api.GetCurrentThreadId
    GetWindowThreadProcessId = _user32.GetWindowThreadProcessId
    AttachThreadInput = _user32.AttachThreadInput
    
    MapVirtualKeyA = _user32.MapVirtualKeyA
    MapVirtualKeyW = _user32.MapVirtualKeyW
    
    MakeLong = win32api.MAKELONG
    w = win32con
    
    
    # 조합키 쓰기 위해
    def PostKeyEx(hwnd, key, shift, specialkey):
        if IsWindow(hwnd):
    
            ThreadId = GetWindowThreadProcessId(hwnd, None)
    
            lparam = MakeLong(0, MapVirtualKeyA(key, 0))
            msg_down = w.WM_KEYDOWN
            msg_up = w.WM_KEYUP
    
            if specialkey:
                lparam = lparam | 0x1000000
    
            if len(shift) > 0:  # Если есть модификаторы - используем PostMessage и AttachThreadInput
                pKeyBuffers = PBYTE256()
                pKeyBuffers_old = PBYTE256()
    
                SendMessage(hwnd, w.WM_ACTIVATE, w.WA_ACTIVE, 0)
                AttachThreadInput(GetCurrentThreadId(), ThreadId, True)
                GetKeyboardState(ctypes.byref(pKeyBuffers_old))
    
                for modkey in shift:
                    if modkey == w.VK_MENU:
                        lparam = lparam | 0x20000000
                        msg_down = w.WM_SYSKEYDOWN
                        msg_up = w.WM_SYSKEYUP
                    pKeyBuffers[modkey] |= 128
    
                SetKeyboardState(ctypes.byref(pKeyBuffers))
                time.sleep(0.01)
                PostMessage(hwnd, msg_down, key, lparam)
                time.sleep(0.01)
                PostMessage(hwnd, msg_up, key, lparam | 0xC0000000)
                time.sleep(0.01)
                SetKeyboardState(ctypes.byref(pKeyBuffers_old))
                time.sleep(0.01)
                AttachThreadInput(GetCurrentThreadId(), ThreadId, False)
    
            else:  # Если нету модификаторов - используем SendMessage
                SendMessage(hwnd, msg_down, key, lparam)
                SendMessage(hwnd, msg_up, key, lparam | 0xC0000000)
    
    
    def main():
    
        # # 핸들 _ 채팅방
        hwndMain = win32gui.FindWindow( None, kakao_opentalk_name)
        hwndListControl = win32gui.FindWindowEx(hwndMain, None, "EVA_VH_ListControl_Dblclk", None)
    
        # #조합키, 본문을 클립보드에 복사 ( ctl + A , C )
        PostKeyEx(hwndListControl, ord('A'), [w.VK_CONTROL], False)
        time.sleep(1)
        PostKeyEx(hwndListControl, ord('C'), [w.VK_CONTROL], False)
        ctext = clipboard.GetData()
        print(ctext)	# 내용 확인
    
    
    if __name__ == '__main__':
        main()

     

    EVA_VH_ListControl_Dblclk

    이게 채팅내용 적히는 곳의 클래스 이름이고, 이걸로 핸들을 가져온다

     

    이후 PostKeyEx 함수로 ctr + a , c 를 비활성으로 클립보드에 가져온다

    ..가져온 내용 확인

     

     

     

    1~3편 코드 + 키워드(명령어) 로 동작하는 봇 코드 ( 예시 )

    import time, win32con, win32api, win32gui, ctypes
    import requests
    from bs4 import BeautifulSoup
    from apscheduler.schedulers.background import BackgroundScheduler
    from pywinauto import clipboard # 채팅창내용 가져오기 위해
    import pandas as pd # 가져온 채팅내용 DF로 쓸거라서
    
    
    # # 카톡창 이름, (활성화 상태의 열려있는 창)
    kakao_opentalk_name = '메모장'
    chat_command = '실검 알려줘'  # 테스트용..
    
    PBYTE256 = ctypes.c_ubyte * 256
    _user32 = ctypes.WinDLL("user32")
    GetKeyboardState = _user32.GetKeyboardState
    SetKeyboardState = _user32.SetKeyboardState
    PostMessage = win32api.PostMessage
    SendMessage = win32gui.SendMessage
    FindWindow = win32gui.FindWindow
    IsWindow = win32gui.IsWindow
    GetCurrentThreadId = win32api.GetCurrentThreadId
    GetWindowThreadProcessId = _user32.GetWindowThreadProcessId
    AttachThreadInput = _user32.AttachThreadInput
    
    MapVirtualKeyA = _user32.MapVirtualKeyA
    MapVirtualKeyW = _user32.MapVirtualKeyW
    
    MakeLong = win32api.MAKELONG
    w = win32con
    
    
    # # 채팅방에 메시지 전송
    def kakao_sendtext(chatroom_name, text):
        # # 핸들 _ 채팅방
        hwndMain = win32gui.FindWindow( None, chatroom_name)
        hwndEdit = win32gui.FindWindowEx( hwndMain, None, "RichEdit20W", None)
    
        win32api.SendMessage(hwndEdit, win32con.WM_SETTEXT, 0, text)
        SendReturn(hwndEdit)
    
    # # 채팅내용 가져오기
    def copy_chatroom(chatroom_name):
        # # 핸들 _ 채팅방
        hwndMain = win32gui.FindWindow( None, chatroom_name)
        hwndListControl = win32gui.FindWindowEx(hwndMain, None, "EVA_VH_ListControl_Dblclk", None)
    
        # #조합키, 본문을 클립보드에 복사 ( ctl + c , v )
        PostKeyEx(hwndListControl, ord('A'), [w.VK_CONTROL], False)
        time.sleep(1)
        PostKeyEx(hwndListControl, ord('C'), [w.VK_CONTROL], False)
        ctext = clipboard.GetData()
        # print(ctext)
        return ctext
    
    
    # 조합키 쓰기 위해
    def PostKeyEx(hwnd, key, shift, specialkey):
        if IsWindow(hwnd):
    
            ThreadId = GetWindowThreadProcessId(hwnd, None)
    
            lparam = MakeLong(0, MapVirtualKeyA(key, 0))
            msg_down = w.WM_KEYDOWN
            msg_up = w.WM_KEYUP
    
            if specialkey:
                lparam = lparam | 0x1000000
    
            if len(shift) > 0:
                pKeyBuffers = PBYTE256()
                pKeyBuffers_old = PBYTE256()
    
                SendMessage(hwnd, w.WM_ACTIVATE, w.WA_ACTIVE, 0)
                AttachThreadInput(GetCurrentThreadId(), ThreadId, True)
                GetKeyboardState(ctypes.byref(pKeyBuffers_old))
    
                for modkey in shift:
                    if modkey == w.VK_MENU:
                        lparam = lparam | 0x20000000
                        msg_down = w.WM_SYSKEYDOWN
                        msg_up = w.WM_SYSKEYUP
                    pKeyBuffers[modkey] |= 128
    
                SetKeyboardState(ctypes.byref(pKeyBuffers))
                time.sleep(0.01)
                PostMessage(hwnd, msg_down, key, lparam)
                time.sleep(0.01)
                PostMessage(hwnd, msg_up, key, lparam | 0xC0000000)
                time.sleep(0.01)
                SetKeyboardState(ctypes.byref(pKeyBuffers_old))
                time.sleep(0.01)
                AttachThreadInput(GetCurrentThreadId(), ThreadId, False)
    
            else:
                SendMessage(hwnd, msg_down, key, lparam)
                SendMessage(hwnd, msg_up, key, lparam | 0xC0000000)
    
    
    # # 엔터
    def SendReturn(hwnd):
        win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)
        time.sleep(0.01)
        win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_RETURN, 0)
    
    
    # # 채팅방 열기
    def open_chatroom(chatroom_name):
        # # # 채팅방 목록 검색하는 Edit (채팅방이 열려있지 않아도 전송 가능하기 위하여)
        hwndkakao = win32gui.FindWindow(None, "카카오톡")
        hwndkakao_edit1 = win32gui.FindWindowEx( hwndkakao, None, "EVA_ChildWindow", None)
        hwndkakao_edit2_1 = win32gui.FindWindowEx( hwndkakao_edit1, None, "EVA_Window", None)
        hwndkakao_edit2_2 = win32gui.FindWindowEx( hwndkakao_edit1, hwndkakao_edit2_1, "EVA_Window", None)    # ㄴ시작핸들을 첫번째 자식 핸들(친구목록) 을 줌(hwndkakao_edit2_1)
        hwndkakao_edit3 = win32gui.FindWindowEx( hwndkakao_edit2_2, None, "Edit", None)
    
        # # Edit에 검색 _ 입력되어있는 텍스트가 있어도 덮어쓰기됨
        win32api.SendMessage(hwndkakao_edit3, win32con.WM_SETTEXT, 0, chatroom_name)
        time.sleep(1)   # 안정성 위해 필요
        SendReturn(hwndkakao_edit3)
        time.sleep(1)
    
    
    # # 채팅내용 초기 저장 _ 마지막 채팅
    def chat_last_save():
        open_chatroom(kakao_opentalk_name)  # 채팅방 열기
        ttext = copy_chatroom(kakao_opentalk_name)  # 채팅내용 가져오기
    
        a = ttext.split('\r\n')   # \r\n 으로 스플릿 __ 대화내용 인용의 경우 \r 때문에 해당안됨
        df = pd.DataFrame(a)    # DF 으로 바꾸기
    
        df[0] = df[0].str.replace('\[([\S\s]+)\] \[(오전|오후)([0-9:\s]+)\] ', '')  # 정규식으로 채팅내용만 남기기
    
        return df.index[-2], df.iloc[-2, 0]
    
    # # 채팅방 커멘드 체크
    def chat_chek_command(cls, clst):
        open_chatroom(kakao_opentalk_name)  # 채팅방 열기
        ttext = copy_chatroom(kakao_opentalk_name)  # 채팅내용 가져오기
    
        a = ttext.split('\r\n')  # \r\n 으로 스플릿 __ 대화내용 인용의 경우 \r 때문에 해당안됨
        df = pd.DataFrame(a)  # DF 으로 바꾸기
    
        df[0] = df[0].str.replace('\[([\S\s]+)\] \[(오전|오후)([0-9:\s]+)\] ', '')  # 정규식으로 채팅내용만 남기기
    
        if df.iloc[-2, 0] == clst:
            print("채팅 없었음..")
            return df.index[-2], df.iloc[-2, 0]
        else:
            print("채팅 있었음")
    
            df1 = df.iloc[cls+1 : , 0]   # 최근 채팅내용만 남김
    
            found = df1[ df1.str.contains(chat_command) ]    # 챗 카운트
    
            if 1 <= int(found.count()):
                print("-------커멘드 확인!")
                p_time_ymd_hms = \
                    f"{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday} / " \
                    f"{time.localtime().tm_hour}:{time.localtime().tm_min}:{time.localtime().tm_sec}"
                realtimeList = naver_realtimeList()  # 네이버 실시간 검색어 상위 20개
                kakao_sendtext(kakao_opentalk_name, f"{p_time_ymd_hms}\n{realtimeList}")  # 메시지 전송, time/실검
    
                # 명령어 여러개 쓸경우 리턴값으로 각각 빼서 쓰면 될듯. 일단 테스트용으로 위에 하드코딩 해둠
                return df.index[-2], df.iloc[-2, 0]
    
            else:
                print("커멘드 미확인")
                return df.index[-2], df.iloc[-2, 0]
    
    
    # # 네이버 실검 상위 20개, 리턴
    def naver_realtimeList():
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
    
        url = 'https://datalab.naver.com/keyword/realtimeList.naver?where=main'
        res = requests.get(url, headers = headers)
        soup = BeautifulSoup(res.content, 'html.parser')
        data = soup.findAll('span','item_title')
    
        a = []
        for item in data:
            a.append(item.get_text())
    
        s = "\n".join(a)
        return s
    
    
    # # 스케줄러 job_1
    def job_1():
        p_time_ymd_hms = \
            f"{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday} / " \
            f"{time.localtime().tm_hour}:{time.localtime().tm_min}:{time.localtime().tm_sec}"
    
        open_chatroom(kakao_opentalk_name)  # 채팅방 열기
        realtimeList = naver_realtimeList()  # 네이버 실시간 검색어 상위 20개
        kakao_sendtext(kakao_opentalk_name, f"{p_time_ymd_hms}\n{realtimeList}")  # 메시지 전송, time/실검
    
    
    def main():
    
        # sched = BackgroundScheduler()
        # sched.start()
    
        cls, clst = chat_last_save()  # 초기설정 _ 마지막채팅 저장
    
        # # 매 분 5초마다 job_1 실행
        # sched.add_job(job_1, 'cron', second='*/5', id="test_1")
    
        while True:
            print("실행중.................")
            cls, clst = chat_chek_command(cls, clst)  # 커멘드 체크
            time.sleep(5)
    
    
    if __name__ == '__main__':
        main()

     

    사용법은

    kakao_opentalk_name = '메모장'

    에서 '메모장' 부분을 원하는 채팅방 이름으로 바꾸고


    chat_command = '실검 알려줘'

    '실검 알려줘' 를 기능에 맞게 원하는 걸로 바꾸면 됨

     

     

    위에 쓴 코드들은 테스트 용도로 하드코딩해둔 거라서

    여러 명령어를 쓸 경우엔 손좀 봐야 함

    지금은 3편에서 만들었던 네이버 실시간 검색어 크롤링 기능을 그대로 씀

     

    잘 짠 것도 아니라서 "가능은 하구나".. 정도만 이해하시면 되고,

    더 좋게 짜실 분은 참고하는 느낌으로 보시면 됩니다.

     


    일단 카카오톡, pc 카톡 봇 만들기 포스팅을 마무리 짓긴 해야 하기 때문에

    클립보드도 사용하는 등,,, 억지로 4편을 만들긴 했지만

     

    명령어 인식 같은 건 이왕이면 API 제대로 지원해주는 텔레그램, 디스코드, 슬랙
    이런 거 쓰세요 그냥..

     

    단순 알림 봇은 '파이썬으로 가능한 것들은 다 된다' 라는 장점이 있어 그래도 쓸 만 하지만,

     

    위에 억지로 만든 명령어 인식 카톡 봇은 문제점이 여러 가지가 있는데

    무한 요청으로 계속 확인 작업을 해야 하는 것도 있고

    채팅방의 내용이 쌓일수록 처리에 부담이 가고 속도도 떨어지는 등

     

    꽤 많은 문제점이 있어서 장시간 사용 및 채팅이 많은 곳에서 사용하기에 다소 힘들 겁니다.

    꼼수를 굳이 부리자면 채팅창을 닫았다가 다시 초기 설정(함수) 해주면 좀 나아지는 정도..

     

    반응형

    댓글

Designed by Tistory.