写代码的日子里,你是不是经常遇到这种情况:
- 接口调试拿到一大串 JSON 数据,嵌套结构看得你头昏脑涨;
- 用 Notepad++ 或 VSCode 打开,缩进很乱,还容易漏字段;
- 想看某个字段的值,还得一层层展开手动找;
- 想提取某个字段,还得去写一段
json.loads()
然后调试。
有没有一种可能,让我们把这些麻烦都变得简单一点?
我用Trae 做了一个有意思的Agent 「Python工程师」。 点击 s.trae.com.cn/a/7c5aa9 立即复刻,一起来玩吧!
答案当然是:写一个图形化的 JSON 数据解析器。今天我们就来实现这样一个小工具 ------ 使用 PyQt5 打造一款干净、简洁、实用的 JSON Viewer,它能快速格式化、展示、查找、导出 JSON 数据。
这个工具不光适合自己平时调试接口用,还可以打包给团队成员使用,让测试、产品也能直观查看 JSON 内容,再也不用你帮他们"翻译数据"。

工具目标:让 JSON 一目了然
这款工具的核心目标很简单:
- 解析任意 JSON 字符串或文件
- 以树形结构展示嵌套字段
- 支持字段搜索、高亮定位
- 支持复制节点路径和值
- 支持格式化 / 压缩显示切换
- 支持导出为 .json、.txt 文件
- 支持拖拽 JSON 文件直接打开
说白了,它就是程序员调接口、做数据分析时的左膀右臂。

关键功能拆解
1. JSON 导入与格式校验
用户可以通过三种方式导入 JSON:
- 粘贴到文本框内(手动输入)
- 选择本地
.json
文件打开 - 拖拽 JSON 文件到窗口
程序会实时校验输入是否合法,并提示错误位置。错误提示可以用 PyQt 的 QMessageBox
显示。
2. 树形结构展示(QTreeWidget)
将 JSON 数据递归解析成树形节点,是这个工具的核心。比如这样一段:
json
{ "id": 123, "name": "Alice", "roles": "admin" }
我们可以通过 QTreeWidgetItem
的递归构造来实现这种嵌套展示。

3. 字段搜索与高亮
你是否也有过"我记得那个字段叫 token,但忘了在哪"的时刻?加一个"搜索字段"功能,可以极大提升效率:
- 输入关键词,匹配所有包含该关键词的字段名或字段值
- 匹配成功的节点自动展开,并高亮显示
- 支持上下跳转下一个匹配项
这个功能可以用 QTreeWidget
自带的遍历能力配合搜索框实现。
4. 节点右键菜单:复制路径 & 复制值
每个字段都可能是你调试用的关键数据点。右键节点时弹出菜单:
- 复制字段路径(如
user.roles[1]
) - 复制字段值(如
editor
) - 展开/折叠当前节点
路径构造可以通过记录递归过程中的键名拼接实现。
5. 格式化/压缩切换
有些时候我们希望看"结构化"的 JSON,有时候则希望它更紧凑一些。加一个"格式化 / 紧凑"切换按钮即可:
- 格式化显示:带缩进、换行,美观可读
- 紧凑显示:一行 JSON,方便复制传参
这部分可以直接切换显示区的 JSON 字符串视图内容。
6. 导出功能
将当前 JSON 数据导出为 .json
、.txt
文件,可以方便和其他人共享结果,或者做调试日志保存。
使用标准 QFileDialog
弹出保存窗口即可完成导出路径选择。

实用工具也是练手范例
python
import sys
import json
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTreeWidget, QTreeWidgetItem, QPushButton, QFileDialog, QLabel,
QTextEdit, QLineEdit, QMenu) # 补充QLineEdit和QMenu导入
from PyQt5.QtGui import QBrush, QColor # 新增颜色相关导入
import json
import logging
from datetime import datetime
from PyQt5.QtCore import Qt
class JsonViewer(QMainWindow):
def __init__(self):
super().__init__()
self.setup_logger()
self.initUI()
def setup_logger(self):
logging.basicConfig(
filename=f'json_parser_{datetime.now().strftime("%Y%m%d")}.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
def initUI(self):
self.setWindowTitle('JSON解析工具')
self.setGeometry(300, 300, 800, 600)
# 创建主部件和布局
main_widget = QWidget()
self.setCentralWidget(main_widget)
layout = QVBoxLayout(main_widget)
# 顶部按钮区域
btn_layout = QHBoxLayout()
self.open_btn = QPushButton('打开文件', self)
self.open_btn.clicked.connect(self.open_file)
btn_layout.addWidget(self.open_btn)
self.parse_btn = QPushButton('解析输入', self) # 新增解析按钮
self.parse_btn.clicked.connect(self.parse_input)
btn_layout.addWidget(self.parse_btn)
self.status_label = QLabel('未选择文件')
btn_layout.addWidget(self.status_label)
layout.addLayout(btn_layout)
# 新增文本编辑区域
self.edit_area = QTextEdit()
self.edit_area.setPlaceholderText("在此粘贴或编辑JSON数据...")
layout.addWidget(self.edit_area)
# 树形展示区域
self.tree = QTreeWidget()
self.tree.setHeaderLabels(['键', '值', '类型'])
layout.addWidget(self.tree)
# 搜索功能组件
search_layout = QHBoxLayout()
self.search_field = QLineEdit()
self.search_field.setPlaceholderText('输入搜索内容...')
self.search_btn = QPushButton('搜索', self)
self.search_btn.clicked.connect(self.highlight_matches)
search_layout.addWidget(self.search_field)
search_layout.addWidget(self.search_btn)
layout.addLayout(search_layout)
# 导出功能组件
export_btn = QPushButton('导出', self)
export_btn.clicked.connect(self.export_data)
btn_layout.addWidget(export_btn)
# 启用拖拽功能
self.setAcceptDrops(True)
def open_file(self):
self.logger.info('用户点击打开文件按钮')
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(self,
"打开JSON文件",
"",
"JSON Files (*.json)",
options=options)
if file_path:
self.logger.debug(f'选择文件路径: {file_path}')
self.status_label.setText(f'已选择文件: {file_path}')
self.parse_json(file_path)
def parse_json(self, file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
self.logger.info(f'开始解析文件: {file_path}')
data = json.load(f)
self.build_tree(data, self.tree)
self.tree.expandAll()
self.logger.info('文件解析成功')
except Exception as e:
error_msg = f'解析错误: {str(e)}'
self.logger.error(error_msg, exc_info=True)
self.status_label.setText(error_msg)
def parse_input(self):
self.logger.info('用户点击解析输入按钮')
json_str = self.edit_area.toPlainText()
if not json_str:
self.logger.warning('检测到空输入')
self.status_label.setText('请输入JSON数据')
return
try:
self.logger.debug('开始解析输入内容')
data = json.loads(json_str)
self.tree.clear()
self.build_tree(data, self.tree)
self.tree.expandAll()
self.status_label.setText('解析成功')
self.logger.info('输入内容解析成功')
except Exception as e:
error_msg = f'解析错误: {str(e)}'
self.logger.error(error_msg, exc_info=True)
self.status_label.setText(error_msg)
def build_tree(self, data, parent):
if isinstance(data, dict):
for key, value in data.items():
node = QTreeWidgetItem([str(key), '', 'dict'])
# 添加子节点时根据父节点类型选择正确的方法
if isinstance(parent, QTreeWidget):
parent.addTopLevelItem(node)
else:
parent.addChild(node)
self.build_tree(value, node)
elif isinstance(data, list):
for index, value in enumerate(data):
node = QTreeWidgetItem([str(index), '', 'list'])
parent.addTopLevelItem(node) # Changed from addChild
self.build_tree(value, node)
else:
parent.setText(1, str(data))
parent.setText(2, type(data).__name__)
# 新增拖拽事件处理
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
files = [u.toLocalFile() for u in event.mimeData().urls()]
if files and files[0].endswith('.json'):
self.parse_json(files[0])
# 新增搜索高亮方法
def highlight_matches(self):
query = self.search_field.text().lower()
for i in range(self.tree.topLevelItemCount()):
self._search_items(self.tree.topLevelItem(i), query)
def _search_items(self, item, query):
for col in range(item.columnCount()):
if query in item.text(col).lower():
item.setBackground(col, QBrush(QColor('#FFF9C4')))
item.setExpanded(True)
else:
item.setBackground(col, QBrush())
for i in range(item.childCount()):
self._search_items(item.child(i), query)
# 新增显示模式切换
def toggle_view_mode(self, checked):
if checked:
self.format_btn.setText('展开模式')
self.tree.collapseAll()
else:
self.format_btn.setText('紧凑模式')
self.tree.expandAll()
# 新增右键菜单
def show_context_menu(self, position):
menu = QMenu()
copy_path = menu.addAction('复制路径')
copy_value = menu.addAction('复制值')
action = menu.exec_(self.tree.viewport().mapToGlobal(position))
item = self.tree.itemAt(position)
if action == copy_path:
self.copy_json_path(item)
elif action == copy_value:
self.copy_item_value(item)
def copy_json_path(self, item):
path = []
while item is not None:
path.append(item.text(0))
item = item.parent()
path_str = '.'.join(reversed(path))
QApplication.clipboard().setText(f'${path_str}')
def copy_item_value(self, item):
QApplication.clipboard().setText(item.text(1))
# 完善导出功能
def export_data(self):
options = QFileDialog.Options()
path, _ = QFileDialog.getSaveFileName(self,
"导出文件",
"",
"JSON Files (*.json);;Text Files (*.txt)",
options=options)
if path:
try:
if path.endswith('.json'):
with open(path, 'w') as f:
json.dump(self.current_data, f, indent=2)
else:
with open(path, 'w') as f:
self._export_text(f)
self.status_label.setText(f'成功导出到: {path}')
except Exception as e:
self.logger.error(f'导出失败: {str(e)}')
def _export_text(self, file):
stack = [(item, '') for item in self.tree.invisibleRootItem().takeChildren()]
while stack:
item, path = stack.pop()
current_path = f'{path}.{item.text(0)}' if path else item.text(0)
file.write(f'{current_path}: {item.text(1)}\n')
for i in range(item.childCount()):
stack.append((item.child(i), current_path))
if __name__ == '__main__':
app = QApplication(sys.argv)
viewer = JsonViewer()
viewer.show()
sys.exit(app.exec_())
这个 JSON 可视化工具,虽然小巧,但几乎集成了 PyQt5 开发中所有重要知识点:
- 事件绑定
- 树形结构
- 文件处理
- 样式美化
- 异常处理
- 多窗口交互
它既能帮你解决日常开发中的"小痛点",也能作为个人作品展示你的项目组织能力与用户体验意识。有了它,你再也不用苦哈哈对着 json.loads() 打断点了。