카카오톡 봇 만들기 ( python, pc 카톡, 비활성 ) - 4편
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편에서는 채팅방의 채팅 내용을 가져와 지정한 키워드가 나왔는지 체크한 후 기능을 실행하는
키워드 명령어로 동작하는 카톡 봇을 구현해보려 한다.
이번 글을 좀 자세히 쓸까 하다가 그냥 코드 위주로 설명하려고 합니다.
이유는 글 마지막에 씀
동작 방법
- 채팅 목록에서 채팅창을 선택해 열고
- 채팅 내용을 클립보드에 저장
- 지정한 키워드가 나와있는지 체크
- 새로운 채팅인지 확인 방법은, 마지막 채팅 인덱스와 채팅 내용으로 구분
- (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 제대로 지원해주는 텔레그램, 디스코드, 슬랙
이런 거 쓰세요 그냥..
단순 알림 봇은 '파이썬으로 가능한 것들은 다 된다' 라는 장점이 있어 그래도 쓸 만 하지만,
위에 억지로 만든 명령어 인식 카톡 봇은 문제점이 여러 가지가 있는데
무한 요청으로 계속 확인 작업을 해야 하는 것도 있고
채팅방의 내용이 쌓일수록 처리에 부담이 가고 속도도 떨어지는 등
꽤 많은 문제점이 있어서 장시간 사용 및 채팅이 많은 곳에서 사용하기에 다소 힘들 겁니다.
꼼수를 굳이 부리자면 채팅창을 닫았다가 다시 초기 설정(함수) 해주면 좀 나아지는 정도..