
本文详解如何正确读取多行 json 格式的日志文件(如每行一个 json 对象),解析其内容,并实时更新 tkinter 界面,重点解决因重复序列化导致的字符串嵌套问题及 gui 线程安全更新技巧。
本文详解如何正确读取多行 json 格式的日志文件(如每行一个 json 对象),解析其内容,并实时更新 tkinter 界面,重点解决因重复序列化导致的字符串嵌套问题及 gui 线程安全更新技巧。
在构建客户端-服务器硬件监控系统时,一个常见但易被忽视的问题是:JSON 数据在传输与持久化过程中被多次 json.dumps() 序列化,导致最终写入文件的内容并非标准 JSON 对象,而是“JSON 字符串的字符串”——即双重转义的嵌套结构。这正是您 Export.json 中出现类似 ""{\"Name\":...}"" 的根本原因。若直接用 json.load() 尝试解析整文件,会因格式非法而失败;而仅取最后一行并 json.loads() 也常因未处理外层引号而报错。
✅ 正确处理流程:避免双重序列化
1. 服务端:只序列化一次,且发送原始 JSON 字符串
# ✅ 正确:构造 dict → dumps 一次 → 入队
load_dict = {
"Name": self.sens_name,
"Sensor Type": self.sens_type,
"Value": self.cpu_temps, # 注意:原文本中此处误用了 self.cpu_temps,实际应为 self.cpu_load
"Timestamp": time.strftime("%H:%M:%S")
}
json_str = json.dumps(load_dict) # ← 唯一一次 dumps
queue_2.put(json_str) # 直接放入队列
# ✅ 正确:出队后直接发送,不再 dumps
q_data = queue_2.get() # q_data 是 str 类型的 JSON 字符串
self.sock.sendto(q_data.encode("utf-8"), (self.UDP_IP, self.UDP_PORT)) # 直接 encode 发送
⚠️ 错误示例(原代码):s_data = json.dumps(q_data) —— 此时 q_data 已是字符串,再 dumps 会生成 ""{...}"",造成嵌套。
2. 客户端:接收后立即解析,写入文件时保持单层 JSON 行格式
# ✅ 正确:接收字节 → decode → loads → 写入单行 JSON
data, addr = self.sock.recvfrom(4096)
json_str = data.decode("utf-8") # 得到 '"{...}"' 或 '{...}'
try:
obj = json.loads(json_str) # 解析为 dict(自动处理外层引号)
except json.JSONDecodeError:
# 兜底:若仍含多余引号,先 strip 再解析
cleaned = json_str.strip('"').replace('\"', '"')
obj = json.loads(cleaned)
# 写入文件:每行一个合法 JSON 对象(NDJSON 格式)
with open("Export.json", "a", encoding="utf-8") as f:
f.write(json.dumps(obj, ensure_ascii=False) + "
")
这样生成的 Export.json 将是标准的 NDJSON(Newline-Delimited JSON) 格式:
{"Name": "CPU Total", "Sensor Type": "Load", "Value": 39.5, "Timestamp": "14:22:05"}
{"Name": "CPU Total", "Sensor Type": "Load", "Value": 39.75, "Timestamp": "14:22:06"}
✅ Tkinter GUI 安全读取与刷新
Tkinter 主循环必须运行在主线程,所有 UI 更新(如 Text.insert)不可在子线程中直接调用。您的 ClientGUI 类继承 threading.Thread 并在 run() 中启动 GUI,这是危险的反模式。应改为:
- GUI 初始化和 mainloop() 在主线程;
- 数据加载与刷新逻辑通过 after() 方法周期性触发(线程安全);
- 使用 queue.Queue 在后台线程与 GUI 线程间安全传递数据。
推荐重构的 GUI 核心逻辑:
class ClientGUI:
def __init__(self, master):
self.master = master
master.title("Hardware Monitor Client")
master.geometry('600x400')
self.txt_cpu = Text(master, height=8, width=60, font=("Consolas", 10))
self.txt_cpu.pack(padx=10, pady=10)
self.close_button = Button(master, text="Exit", command=master.destroy)
self.close_button.pack(pady=5)
# 启动后台监控线程(非 GUI 线程)
self.data_queue = queue.Queue()
self.monitor_thread = threading.Thread(target=self._monitor_json_file, daemon=True)
self.monitor_thread.start()
# 启动 GUI 刷新循环
self.master.after(1000, self._update_display) # 每秒检查一次
def _monitor_json_file(self):
"""后台线程:持续监听文件末尾新增行"""
last_size = 0
while True:
try:
with open("Export.json", "r", encoding="utf-8") as f:
f.seek(0, 2) # 移动到文件末尾
current_size = f.tell()
if current_size > last_size:
f.seek(last_size)
new_lines = f.readlines()
for line in new_lines:
line = line.strip()
if line:
try:
data = json.loads(line)
self.data_queue.put(data)
except json.JSONDecodeError:
continue
last_size = current_size
time.sleep(0.5)
except FileNotFoundError:
time.sleep(1)
def _update_display(self):
"""主线程:安全地从队列取数据并更新界面"""
while not self.data_queue.empty():
try:
data = self.data_queue.get_nowait()
# 清空并插入最新数据(可按需格式化)
self.txt_cpu.delete("1.0", tk.END)
self.txt_cpu.insert("1.0", f"CPU Load: {data.get('Value', 'N/A')}% "
f"(at {data.get('Timestamp', 'N/A')})
")
self.txt_cpu.insert("2.0", f"Sensor: {data.get('Name', 'N/A')} "
f"[{data.get('Sensor Type', 'N/A')}]
")
except queue.Empty:
break
self.master.after(1000, self._update_display) # 继续轮询
? 关键注意事项总结
- 序列化原则:dict → dumps → string → transmit/store;string → loads → dict → use。严禁对已序列化的字符串再次 dumps。
- 文件格式统一:采用 NDJSON(每行一个 JSON 对象),便于流式读取与增量解析。
- GUI 线程安全:所有 Tkinter 组件操作必须在主线程,使用 queue.Queue + after() 实现跨线程通信。
- 错误防御:json.loads() 前对原始字符串做 strip('"') 和转义清理,提升鲁棒性。
- 资源管理:使用 with open(...) 确保文件句柄及时释放;网络 socket 建议添加异常关闭逻辑。
通过以上修正,您的客户端将能稳定、准确地从 Export.json 提取实时硬件数据,并在 Tkinter 界面中清晰、动态地呈现 CPU 负载与温度信息。










