안녕하십니까.
하프 라고 합니다 😊
키보드 녹화 프로그램을 만들어보겠습니다.
설계 구성
구상 방법 1.
제가 처음으로 구상하던 방법은 동영상 녹화처럼 생각을 했습니다.
하지만 이 방법은 입력된 시간 비례해서 저장되는 용량이 압도적으로 높을 것 입니다.
저장 방식 또한 애매하겠지요..
또한.. 만든다 하여도.. 동영상 처럼 ms단위로 재생하는 것에는 무리가 있을 것 같았습니다.
결과적으로는 .. 기각되었습니다.
구상 방법 2.
두 번째로 생각해본 방법은, 키 입력 순번으로 Json을 저장하는 것 입니다.
그럼 다음 과 같이 저장이 되었다고 할 때 `Ctrl + a`의 조합키가 완성이 됩니다.
시작한 시간과 입력된 시간만 알 수 있다면
`입력된 시간 - 시작한 시간` 만큼 계산해 준다면 delay 시간을 알 수 있으니 키 입력시간까지 정확히 알 수 있을것 입니다.
따라서 두 번째 방법으로 제작을 진행하기로 했습니다.
코드 구성
1. 라이브러리
제가 사용한 라이브러리는 다음과 같습니다.
[키보드 관련]
pynput
keyboard
classDD
[기타 라이브러리]
time
threading
json
os
ctypes
signal
보통의 키보드 입력이라면 keyboard 선에서 전부 해결이 될 것입니다.하지만.. 보안이 조금 있는곳이라면 software 입력이 안되는 경우가 매우 크겠지요,,
하드웨어 입력으로 변환시켜주기 위해 저는 classDD 라이브러리를 사용했습니다.classDD는 원래 중국에서 인증을 받고 다운을 받아야 하지만은.. 많이 널려있기에 쉽게 구하실 수 있을겁니다.
키보드 녹화에서 사용된 베이스 코드는 다음과 같습니다.
from pynput import keyboard
# 기록된 키 이벤트를 저장할 리스트
recorded_events = []
# 키를 누를 때 호출되는 콜백 함수
def on_press(key):
try:
# 키보드 입력을 문자열로 변환하여 리스트에 추가
recorded_events.append(str(key.char))
except AttributeError:
# 특수 키의 경우, 그냥 문자열로 추가
recorded_events.append(str(key))
# 키를 떼었을 때 호출되는 콜백 함수
def on_release(key):
if key == keyboard.Key.esc:
# 'esc' 키를 누르면 녹화 종료
return False
# 키보드 리스너 생성
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
# 리스너가 실행되는 동안 대기
listener.join()
# 녹화된 키 이벤트 출력
print("Recorded Events:")
print(recorded_events)
2. 시행착오
구성 계획을 바탕으로 만들어 보았지만은.. 조금 독특한 상황이 있긴 했네요 🫠그러한 상황이라믄.. Ctrl 조합키의 경우 이상한 값으로 나온다는 점 인데,,(\x01, \x02 과 같은 16진수 값)찾아보니 아스키 제어 문자라고 합니다.
+-------+---------+---------------------------+
| ASCII | 16진수 | 의미 |
+-------+---------+---------------------------+
| ^A | \x01 | Start of Header (SOH) |
| ^B | \x02 | Start of Text (STX) |
| ^C | \x03 | End of Text (ETX) |
| ^D | \x04 | End of Transmission (EOT) |
| ^E | \x05 | Enquiry (ENQ) |
| ^F | \x06 | Acknowledgement (ACK) |
| ^G | \x07 | Bell (BEL) |
| ^H | \x08 | Backspace (BS) |
| ^I | \x09 | Horizontal Tab (HT) |
| ^J | \x0A | Line Feed / New Line (LF) |
| ^K | \x0B | Vertical Tab (VT) |
| ^L | \x0C | Form Feed (FF) |
| ^M | \x0D | Carriage Return (CR) |
| ^N | \x0E | Shift Out (SO) |
| ^O | \x0F | Shift In (SI) |
| ^P | \x10 | Data Link Escape (DLE) |
+-------+---------+---------------------------+
처음에는 문자열로 매핑하려 하니 잘 안되었길래 타입명을 확인해보니 keyboard의 enum 으로 되어 있더군요.
이거를 어떻게 해결은 하였나 하면 Ctrl을 눌렀을 경우만 특별하게 매핑을 시켜주었습니다.
ctrl_mapping = {
keyboard.KeyCode.from_char('\x01'): keyboard.KeyCode.from_char('a'),
keyboard.KeyCode.from_char('\x02'): keyboard.KeyCode.from_char('b'),
keyboard.KeyCode.from_char('\x03'): keyboard.KeyCode.from_char('c'),
keyboard.KeyCode.from_char('\x04'): keyboard.KeyCode.from_char('d'),
keyboard.KeyCode.from_char('\x05'): keyboard.KeyCode.from_char('e'),
keyboard.KeyCode.from_char('\x06'): keyboard.KeyCode.from_char('f'),
keyboard.KeyCode.from_char('\x07'): keyboard.KeyCode.from_char('g'),
keyboard.KeyCode.from_char('\x08'): keyboard.KeyCode.from_char('h'),
keyboard.KeyCode.from_char('\x09'): keyboard.KeyCode.from_char('i'),
keyboard.KeyCode.from_char('\x0A'): keyboard.KeyCode.from_char('j'),
keyboard.KeyCode.from_char('\x0B'): keyboard.KeyCode.from_char('k'),
keyboard.KeyCode.from_char('\x0C'): keyboard.KeyCode.from_char('l'),
keyboard.KeyCode.from_char('\x0D'): keyboard.KeyCode.from_char('m'),
keyboard.KeyCode.from_char('\x0E'): keyboard.KeyCode.from_char('n'),
keyboard.KeyCode.from_char('\x0F'): keyboard.KeyCode.from_char('o'),
keyboard.KeyCode.from_char('\x10'): keyboard.KeyCode.from_char('p'),
keyboard.KeyCode.from_char('\x11'): keyboard.KeyCode.from_char('q'),
keyboard.KeyCode.from_char('\x12'): keyboard.KeyCode.from_char('r'),
keyboard.KeyCode.from_char('\x13'): keyboard.KeyCode.from_char('s'),
keyboard.KeyCode.from_char('\x14'): keyboard.KeyCode.from_char('t'),
keyboard.KeyCode.from_char('\x15'): keyboard.KeyCode.from_char('u'),
keyboard.KeyCode.from_char('\x16'): keyboard.KeyCode.from_char('v'),
keyboard.KeyCode.from_char('\x17'): keyboard.KeyCode.from_char('w'),
keyboard.KeyCode.from_char('\x18'): keyboard.KeyCode.from_char('x'),
keyboard.KeyCode.from_char('\x19'): keyboard.KeyCode.from_char('y'),
keyboard.KeyCode.from_char('\x1A'): keyboard.KeyCode.from_char('z')
}
3. 코드
다음 작업으로는 Record.py 에 대한 부분입니다.
on_press와 on_release의 대한 이벤트를 핸들링을 해주었습니다.
해당 키 코드의 값을 mapping을 통해 따로 치환 해주는 작업을 거친 후, 시간 값과 타입을 json으로 저장할 수 있도록 했습니다.
별거 없는 코드지만 도움이 되셨다면 좋겠네요 😅
def on_press(key):
... [생략]
try:
#1. 키가 계속 눌려져 있을경우 처리
cur_key_code = key
#2. Ctrl 조합키를 고려
if key in ctrl_mapping:
cur_key_code = ctrl_mapping[key]
if cur_key_code == "<21>": #한글키 변경
cur_key_code = "hangul"
recoding[recoding_number] = {
"key": str(cur_key_code) if type(cur_key_code) is Key else cur_key_code.char.lower(),
"time": time.time(),
"type": "press"
}
recoding_number += 1
except AttributeError:
print("Special Key Press : {0}".format(key))
... [생략]
def on_release(key):
global isRecord
global recoding_number
ctrl_mapping = keyMapping.ctrl_mapping
if isRecord == True and key != Key.f1:
cur_key_code = key
if key in ctrl_mapping:
cur_key_code = ctrl_mapping[key]
recoding[recoding_number] = {
"key": str(cur_key_code) if type(cur_key_code) is Key else cur_key_code.char.lower(),
"time": time.time(),
"type": "release"
}
recoding_number += 1
... [생략]
녹화가 끝나면, json으로 저장할 수 있도록 코드를 구현했습니다.
그냥.. 파일이 있으면 cnt 를 증가시켜준거 밖에 없습니다 😅
def save_dict_to_file(my_dict, base_filename='recoding'):
# 기본 파일 이름에 번호를 붙여가며 중복을 피함
filename = f"{base_filename}.json"
count = 1
while os.path.exists(filename):
filename = f"{base_filename}_{count}.json"
count += 1
# 딕셔너리를 JSON 형식으로 변환하여 파일에 쓰기
with open(filename, 'w') as json_file:
json.dump(my_dict, json_file)
print(f"Dictionary saved to file: {filename}")
소스 코드
1. Record.py
다음은 Play.py 부분 입니다.
녹화가 되어 저장된 json을 재생하기란 어려울 것은 없습니다.
json을 순서대로 읽어와서 그대로 뿌려주면 되겠습니다.
하지만!!.. 순차적으로 진행되는건 아직 구성해보지는 않았습니다.
조금 무식한 방법을 사용 했습니다 ㅎㅎ,,
무슨 방법이냐면,,
모든 json의 순서를 읽고, Thread로 전부 보내버립니다.
이 방법의 단점은 아무래도, 입력이 많고 길어지면 길어질수록 RAM을 많이 차지한다는 단점이 있을겁니다.
저도 최대치가 어느정도까지는 알지는 못하나.. 최대치를 넘어가버릴경우 메모리 오버플로우가 발생해 강제 중지될 확률이 높을 것 같습니다.
그래도 쉽게 구성한 방법이다 판단하고 구현은 해봤습니다.
다음은 쓰레드 코드 입니다.
Keyboard 라이브러리와 ClassDD 라이브러릴 주석으로 구분해 놓았으니, 각각 풀어서 확인해보시면 될 것 같습니다.
... [생략]
class Worker(threading.Thread):
def __init__(self, start_time, name, key, time, type):
super().__init__()
self.start_time = start_time
self.name = name # thread 이름 지정
self.key = key
self.time = time
self.type = type
def run(self):
delay = round(max(0, self.time - self.start_time), 4)
classDD_mapping = keyMapping.classDD_mapping
time.sleep(delay)
try:
input_key = self.key
# 키보드 라이브러리 사용
#if input_key in key_mapping:
# input_key = key_mapping[self.key]
# ClassDD 라이브러리 사용
if input_key in classDD_mapping: #다시한번 매핑(임시 테스트용)
#if len(input_key) == 1:
# input_key = input_key.lower()
input_key = classDD_mapping[input_key]
if self.type == "press":
# 키보드 라이브러리 사용
#pyautogui.keyDown(input_key)
# ClassDD 라이브러리 사용
DD.DD_key(input_key, 1)
else:
# 키보드 라이브러리 사용
#pyautogui.keyUp(input_key)
# ClassDD 라이브러리 사용
DD.DD_key(input_key, 2)
except Exception as ex:
print("오류 : ", ex)
return
소스 코드
1. Play.py
Github 프로젝트
Link to Github
실행 결과
1. 메모장 테스트
실행 시켜놓고 키보드에서 손을 놓아도 잘 동작 되는걸 확인할 수 있습니다 🫠
키보드 입력 기록 파일은 깃허브에 있으니 참고 바랍니다 : )
2. 인게임 테스트
코드 분리 이전에 찍어놓은거기는 하지만, 그 갓 겜 메이플스토리 에서 동작이 되는지 궁금하여.. 찍어놓았슴다..
잘되는거 보고 저도 깜짝 놀랐네요 🤔..
이걸로 키보드 녹화 시켜서 사냥도 조~...금 해봤는데
바로 거짓말 탐지기 뜨고, 룬 해제 하려고 하니 화살표가 빙글빙글 돌아서 ㅋㅋ .. 관두었습니다
어차피 공부용으로 만든거니 여기까지만 하고 그만두렵니다!
참고만 하시고 악용하는것은 절대로!! 안됩니다

'개발 > 파이썬' 카테고리의 다른 글
[파이썬] 코인 자동매매 만들기 #1 - API Key 생성 (0) | 2024.06.07 |
---|