Python-适用于硬件测试的小工具
文末附源码
一、监控器核心功能亮点
这个系统资源监控器没有复杂的依赖,核心功能聚焦“实用”和“轻量”,主要亮点如下:
- 多资源覆盖:支持CPU使用率、内存使用率监控,若有NVIDIA显卡,还能自动识别并监控GPU使用率。
- 日志持久化:所有监控数据以CSV格式保存,包含时间戳,方便后续分析(比如用Excel或Python画趋势图)。
- 实时可视化:UI界面实时预览最新数据,自动保留最近15条记录,避免信息过载。
- 友好交互:支持自定义日志保存路径,按钮状态动态切换(开始后禁用“开始”按钮,防止重复点击),操作逻辑清晰。
- 跨平台兼容:适配Windows和Linux系统的中文字体,避免UI乱码问题。
二、核心代码解析:关键模块如何实现?
整个项目代码约300行,核心分为UI构建、资源监控、数据记录三大模块,下面逐一拆解关键技术点。
1. 基础环境与依赖准备
首先要明确依赖库,项目用到3个核心库,功能各有分工:
tkinter
:Python自带的GUI库,用于搭建界面(无需额外安装)。psutil
:跨平台系统监控库,获取CPU、内存等硬件信息(需安装:pip install psutil
)。pynvml
:NVIDIA显卡监控库,仅用于NVIDIA GPU信息获取(需安装:pip install nvidia-ml-py3
,AMD显卡暂不支持)。
代码开头的“依赖检测”逻辑很重要,能避免因缺少库导致程序崩溃:
# 尝试导入GPU监控库
try:import pynvml # NVIDIA GPU监控has_nvml = True
except ImportError:has_nvml = False
2. UI构建:兼顾美观与实用性
UI部分用Tkinter实现,核心是“分区域布局”,让界面清晰不杂乱。主要分为5个区域:
- 标题区:显示“系统资源监控器”,用加粗字体突出。
- GPU信息区:自动检测GPU状态(如“检测到GPU: NVIDIA GeForce RTX 3060”),无GPU时显示友好提示。
- 路径选择区:支持浏览选择CSV日志保存路径,默认文件名含时间戳(如
system_monitor_20240520_143000.csv
)。 - 控制按钮区:“开始记录”(绿色)和“停止记录”(红色),状态动态切换。
- 数据预览区:用Text组件显示实时数据,带滚动条,仅保留最近15行。
其中“中文字体适配”是容易踩坑的点,代码专门做了跨平台处理:
def setup_fonts(self):"""设置中文字体支持,避免乱码"""if sys.platform.startswith('win'):default_font = ('Microsoft YaHei UI', 9)title_font = ('Microsoft YaHei UI', 10, 'bold')else:default_font = ('SimHei', 9)title_font = ('SimHei', 10, 'bold')self.root.option_add("*Font", default_font)self.title_font = title_font
3. 资源监控:如何获取CPU/内存/GPU数据?
资源数据的获取是监控器的核心,代码用简洁的逻辑实现了多硬件支持:
(1)CPU与内存数据:基于psutil
psutil
库封装了底层系统调用,几行代码就能获取关键数据:
- CPU使用率:
psutil.cpu_percent(interval=0.1)
,interval
是采样间隔(0.1秒兼顾实时性和性能)。 - 内存使用率:
psutil.virtual_memory().percent
,直接返回内存占用百分比。
(2)GPU数据:基于pynvml
NVIDIA GPU监控需要先初始化库,再获取设备句柄,最后读取使用率:
def init_gpu_monitoring(self):if has_nvml:try:pynvml.nvmlInit() # 初始化库device_count = pynvml.nvmlDeviceGetCount() # 检测GPU数量if device_count > 0:self.gpu_handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 获取第1块GPU句柄self.gpu_available = Truegpu_name = pynvml.nvmlDeviceGetName(self.gpu_handle).decode('utf-8') # 转中文self.gpu_info = f"检测到GPU: {gpu_name}"except Exception as e:self.gpu_info = f"GPU初始化失败: {str(e)}"
获取GPU使用率时,用pynvml.nvmlDeviceGetUtilizationRates()
,返回的gpu
字段就是使用率百分比。
4. 数据记录:线程安全与日志持久化
如果直接在主线程循环记录数据,会导致UI卡顿(Tkinter是单线程GUI库)。因此代码用子线程处理数据记录,同时保证线程安全:
(1)线程创建与管理
- 开始记录时,创建daemon子线程(
daemon=True
,主线程退出时子线程自动关闭)。 - 停止记录时,通过
self.is_recording = False
控制循环退出,避免线程残留。
(2)CSV日志写入
- 首次创建日志文件时,自动写入表头(“时间”“CPU使用率(%)”“内存使用率(%)”“GPU使用率(%)”)。
- 后续记录以“追加模式”写入,避免覆盖历史数据,编码用
utf-8
防止中文乱码。
关键代码片段:
def record_data(self):while self.is_recording:# 1. 获取各类资源数据(CPU、内存、GPU)current_time = time.strftime("%Y-%m-%d %H:%M:%S")cpu_usage = psutil.cpu_percent(interval=0.1)mem_usage = psutil.virtual_memory().percentgpu_usage = self.get_gpu_usage() # 自定义GPU获取函数# 2. 写入CSVwith open(self.log_path.get(), 'a', newline='', encoding='utf-8') as f:writer = csv.writer(f)row_data = [current_time, cpu_usage, mem_usage]if self.gpu_available:row_data.append(gpu_usage)writer.writerow(row_data)# 3. 更新UI预览(需用root.after确保主线程更新)self.update_preview(current_time, cpu_usage, mem_usage, gpu_usage)time.sleep(0.9) # 控制记录间隔约1秒(加前面的0.1秒采样,共1秒)
5. 实时预览:控制数据显示规模
数据预览区用Text
组件实现,但如果记录太久,文本会无限变长。代码加入了“行数控制”,仅保留最近15行:
def update_preview(self, time_str, cpu, mem, gpu):def update():self.data_text.config(state=tk.NORMAL)# 超过15行时,删除第一行line_count = int(self.data_text.index('end-1c').split('.')[0])if line_count > 15:self.data_text.delete('1.0', '2.0')# 追加新数据line = f"{time_str} - CPU: {cpu}% 内存: {mem}%"if self.gpu_available:line += f" GPU: {gpu}%"self.data_text.insert(tk.END, line + "\n")self.data_text.see(tk.END) # 自动滚动到最后一行self.data_text.config(state=tk.DISABLED)self.root.after(0, update) # 确保UI操作在主线程执行
三、实战使用:3步上手监控系统资源
看完代码解析,我们来实际运行监控器,步骤非常简单:
1. 安装依赖
打开命令行,执行以下命令安装所需库:
pip install psutil nvidia-ml-py3
(如果没有NVIDIA显卡,可跳过nvidia-ml-py3
,程序会自动忽略GPU监控)
2. 运行程序
将完整代码保存为system_monitor.py
,然后用Python运行:
python system_monitor.py
3. 开始监控与查看日志
- 点击“浏览…”选择日志保存路径(默认会生成带时间戳的CSV文件名)。
- 点击“开始记录”,状态会变为“正在记录…”,数据预览区实时显示CPU/内存/GPU数据。
- 监控完成后点击“停止记录”,程序会弹出提示“日志已保存至xxx”。
- 找到保存的CSV文件,用Excel或Notepad打开,就能看到所有时间点的资源数据,方便后续分析。
四、总结与扩展方向
这个轻量级监控器虽然简单,但覆盖了“监控-记录-分析”的完整流程,适合个人日常使用。如果想进一步优化,可以考虑这些方向:
- 支持多GPU监控:当前只监控第1块GPU,可扩展为选择多块GPU。
- 添加磁盘使用率监控:用
psutil.disk_usage('/')
获取磁盘占用,丰富监控维度。 - 数据可视化:在UI中加入Matplotlib图表,实时显示资源变化趋势。
- 定时任务:支持设置监控时长(如“监控1小时后自动停止”)。
如果你需要快速监控系统资源,又不想安装复杂的工具(如Task Manager、nvidia-smi),这个Python监控器会是不错的选择。赶紧试试吧!
资源监控器源码:
import tkinter as tk
from tkinter import filedialog, messagebox
import psutil
import time
import csv
import threading
import os
import sys# 尝试导入GPU监控库
try:import pynvml # NVIDIA GPU监控has_nvml = True
except ImportError:has_nvml = Falseclass SystemMonitor:def __init__(self, root):self.root = rootself.root.title("系统资源监控器")self.root.geometry("600x400")self.root.resizable(True, True)# 设置中文字体支持self.setup_fonts()# 变量初始化self.log_path = tk.StringVar()self.is_recording = Falseself.recording_thread = Noneself.gpu_available = Falseself.gpu_handle = None# 初始化GPU监控self.init_gpu_monitoring()# 创建UI组件self.create_widgets()def setup_fonts(self):"""设置中文字体支持"""if sys.platform.startswith('win'):default_font = ('Microsoft YaHei UI', 9)title_font = ('Microsoft YaHei UI', 10, 'bold')else:default_font = ('SimHei', 9)title_font = ('SimHei', 10, 'bold')self.root.option_add("*Font", default_font)self.title_font = title_fontdef init_gpu_monitoring(self):"""初始化GPU监控"""if has_nvml:try:pynvml.nvmlInit()device_count = pynvml.nvmlDeviceGetCount()if device_count > 0:self.gpu_handle = pynvml.nvmlDeviceGetHandleByIndex(0)self.gpu_available = Truegpu_name = pynvml.nvmlDeviceGetName(self.gpu_handle)if isinstance(gpu_name, bytes):gpu_name = gpu_name.decode('utf-8')self.gpu_info = f"检测到GPU: {gpu_name}"else:self.gpu_info = "未检测到GPU设备"except Exception as e:self.gpu_info = f"GPU初始化失败: {str(e)}"else:self.gpu_info = "未安装NVIDIA GPU监控库(pynvml)"def create_widgets(self):"""创建界面组件"""# 标题标签title_label = tk.Label(self.root, text="系统资源监控器", font=self.title_font)title_label.pack(pady=10)# GPU信息标签gpu_label = tk.Label(self.root, text=self.gpu_info, fg="gray")gpu_label.pack(pady=5)# 路径选择区域path_frame = tk.Frame(self.root)path_frame.pack(pady=10, fill=tk.X, padx=20)tk.Label(path_frame, text="日志文件路径:").pack(side=tk.LEFT)tk.Entry(path_frame, textvariable=self.log_path, width=50).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)tk.Button(path_frame, text="浏览...", command=self.browse_path).pack(side=tk.LEFT, padx=5)# 控制按钮区域btn_frame = tk.Frame(self.root)btn_frame.pack(pady=15)self.start_btn = tk.Button(btn_frame, text="开始记录", command=self.start_recording, width=15, bg="#4CAF50", fg="white")self.start_btn.pack(side=tk.LEFT, padx=10)self.stop_btn = tk.Button(btn_frame, text="停止记录", command=self.stop_recording, width=15, bg="#f44336", fg="white", state=tk.DISABLED)self.stop_btn.pack(side=tk.LEFT, padx=10)# 状态区域status_frame = tk.Frame(self.root)status_frame.pack(pady=5, fill=tk.X, padx=20)tk.Label(status_frame, text="状态:").pack(side=tk.LEFT)self.status_var = tk.StringVar(value="就绪")tk.Label(status_frame, textvariable=self.status_var, fg="blue").pack(side=tk.LEFT, padx=5)# 数据显示区域tk.Label(self.root, text="监控数据预览:").pack(anchor=tk.W, padx=20, pady=(10, 5))self.data_text = tk.Text(self.root, height=10, wrap=tk.WORD)self.data_text.pack(pady=5, padx=20, fill=tk.BOTH, expand=True)self.data_text.config(state=tk.DISABLED)# 添加滚动条scrollbar = tk.Scrollbar(self.data_text)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.data_text.config(yscrollcommand=scrollbar.set)scrollbar.config(command=self.data_text.yview)def browse_path(self):"""浏览并选择日志文件保存路径"""default_filename = f"system_monitor_{time.strftime('%Y%m%d_%H%M%S')}.csv"filename = filedialog.asksaveasfilename(defaultextension=".csv",filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],title="选择日志文件保存位置",initialfile=default_filename)if filename:self.log_path.set(filename)def start_recording(self):"""开始记录系统资源使用情况"""if not self.log_path.get():messagebox.showwarning("警告", "请先选择日志文件保存路径")returnself.is_recording = Trueself.start_btn.config(state=tk.DISABLED)self.stop_btn.config(state=tk.NORMAL)self.status_var.set("正在记录...")# 检查文件是否存在,如果不存在则创建并写入表头if not os.path.exists(self.log_path.get()):try:with open(self.log_path.get(), 'w', newline='', encoding='utf-8') as f:writer = csv.writer(f)headers = ["时间", "CPU使用率(%)", "内存使用率(%)"]if self.gpu_available:headers.append("GPU使用率(%)")writer.writerow(headers)except Exception as e:messagebox.showerror("错误", f"无法创建日志文件: {str(e)}")self.reset_buttons()return# 启动记录线程self.recording_thread = threading.Thread(target=self.record_data)self.recording_thread.daemon = Trueself.recording_thread.start()def stop_recording(self):"""停止记录系统资源使用情况"""self.is_recording = Falseself.reset_buttons()self.status_var.set("已停止")messagebox.showinfo("信息", f"日志已保存至: {self.log_path.get()}")def reset_buttons(self):"""重置按钮状态"""self.start_btn.config(state=tk.NORMAL)self.stop_btn.config(state=tk.DISABLED)def record_data(self):"""记录系统资源数据的线程函数"""while self.is_recording:try:# 获取当前时间(精确到秒)current_time = time.strftime("%Y-%m-%d %H:%M:%S")# 获取CPU使用率cpu_usage = psutil.cpu_percent(interval=0.1)# 获取内存使用率mem = psutil.virtual_memory()mem_usage = mem.percent# 获取GPU使用率(如果可用)gpu_usage = Noneif self.gpu_available and has_nvml:try:gpu_util = pynvml.nvmlDeviceGetUtilizationRates(self.gpu_handle)gpu_usage = gpu_util.gpuexcept:gpu_usage = "N/A"# 写入日志文件with open(self.log_path.get(), 'a', newline='', encoding='utf-8') as f:writer = csv.writer(f)row_data = [current_time, cpu_usage, mem_usage]if self.gpu_available and gpu_usage is not None:row_data.append(gpu_usage)writer.writerow(row_data)# 在UI上显示最新数据self.update_preview(current_time, cpu_usage, mem_usage, gpu_usage)except Exception as e:error_msg = f"记录数据时出错: {str(e)}"self.status_var.set(error_msg)self.is_recording = Falseself.root.after(0, self.reset_buttons)self.root.after(0, lambda: messagebox.showerror("错误", error_msg))break# 等待1秒(减去前面操作已用的时间)time.sleep(0.9)def update_preview(self, time_str, cpu, mem, gpu):"""更新预览区域显示最新数据"""# 在UI线程中更新文本框def update():self.data_text.config(state=tk.NORMAL)# 保持只显示最后15行数据line_count = int(self.data_text.index('end-1c').split('.')[0])if line_count > 15:self.data_text.delete('1.0', '2.0')# 构建显示行line = f"{time_str} - CPU: {cpu}% 内存: {mem}%"if self.gpu_available and gpu is not None:line += f" GPU: {gpu}%"self.data_text.insert(tk.END, line + "\n")self.data_text.see(tk.END) # 滚动到最后一行self.data_text.config(state=tk.DISABLED)# 确保在主线程中更新UIself.root.after(0, update)if __name__ == "__main__":try:root = tk.Tk()# 设置窗口图标(可选)try:root.iconbitmap(default="")except:passapp = SystemMonitor(root)root.mainloop()except Exception as e:messagebox.showerror("程序错误", f"程序运行出错: {str(e)}")