pyxllib 0.3.197__py3-none-any.whl → 0.3.200__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxllib/__init__.py +21 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +541 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +149 -149
- pyxllib/algo/unitlib.py +66 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +852 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +240 -240
- pyxllib/data/jsonlib.py +89 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1127 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +246 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/old.py +663 -663
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +497 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +827 -827
- pyxllib/ext/utools.py +351 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +88 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1105 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +761 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +148 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +426 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +685 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2829 -2829
- pyxllib/file/xlsxlib.py +3131 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +64 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1197 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +391 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +108 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +39 -39
- pyxllib/text/airscript.js +744 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +32 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +747 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
- pyxllib-0.3.200.dist-info/RECORD +126 -0
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
pyxllib/ext/JLineViewer.py
CHANGED
@@ -1,505 +1,505 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
# @Author : 陈坤泽
|
4
|
-
# @Email : 877362867@qq.com
|
5
|
-
# @Date : 2023/08/02 14:05
|
6
|
-
|
7
|
-
import os
|
8
|
-
import random
|
9
|
-
import re
|
10
|
-
import sys
|
11
|
-
import json
|
12
|
-
import time
|
13
|
-
from types import SimpleNamespace
|
14
|
-
|
15
|
-
import warnings
|
16
|
-
|
17
|
-
warnings.filterwarnings('ignore')
|
18
|
-
|
19
|
-
import logging
|
20
|
-
|
21
|
-
logging.disable(logging.CRITICAL)
|
22
|
-
|
23
|
-
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QFileDialog, QListWidget, QLineEdit, QVBoxLayout, \
|
24
|
-
QSplitter, QTreeView, QPlainTextEdit, QPushButton, QLabel, QHBoxLayout, QSizePolicy, QWidget, QStatusBar, \
|
25
|
-
QAbstractItemView, QHeaderView, QMessageBox
|
26
|
-
from PyQt5.QtWidgets import QItemDelegate, QTextEdit
|
27
|
-
from PyQt5.QtWidgets import QItemDelegate, QDialog, QVBoxLayout, QTextEdit, QPushButton
|
28
|
-
|
29
|
-
from PyQt5.QtGui import QTextOption, QStandardItemModel, QStandardItem
|
30
|
-
from PyQt5.QtCore import Qt, QModelIndex, QSettings, QFileInfo
|
31
|
-
|
32
|
-
from pyxllib.file.specialist import XlPath
|
33
|
-
|
34
|
-
# 一个专门存储大字符串的命名空间
|
35
|
-
LargeStrings = SimpleNamespace()
|
36
|
-
|
37
|
-
# 命名并存储QTreeView的样式(旧的格式配置,在新版中已经不起作用)
|
38
|
-
LargeStrings.treeViewStyles = """
|
39
|
-
QTreeView::item { /* 设置网格线 */
|
40
|
-
border: 1px solid black;
|
41
|
-
}
|
42
|
-
|
43
|
-
QTreeView::item:selected {
|
44
|
-
background: black;
|
45
|
-
color: white;
|
46
|
-
}
|
47
|
-
|
48
|
-
QTreeView::item:selected:active {
|
49
|
-
background: black;
|
50
|
-
color: white;
|
51
|
-
}
|
52
|
-
|
53
|
-
QTreeView::item:selected:!active {
|
54
|
-
background: black;
|
55
|
-
color: white;
|
56
|
-
}
|
57
|
-
""".strip()
|
58
|
-
|
59
|
-
|
60
|
-
class MyTreeView(QTreeView):
|
61
|
-
def __init__(self, parent=None):
|
62
|
-
super().__init__(parent)
|
63
|
-
|
64
|
-
def edit(self, index, trigger, event):
|
65
|
-
if trigger == QAbstractItemView.DoubleClicked:
|
66
|
-
return False
|
67
|
-
# 如果是第 0 列 (keys),则禁止编辑
|
68
|
-
if index.column() == 0:
|
69
|
-
return False
|
70
|
-
return super().edit(index, trigger, event)
|
71
|
-
|
72
|
-
|
73
|
-
class KeyStandardItem(QStandardItem):
|
74
|
-
def data(self, role=None):
|
75
|
-
if role == Qt.TextAlignmentRole:
|
76
|
-
return Qt.AlignLeft | Qt.AlignVCenter
|
77
|
-
return super().data(role)
|
78
|
-
|
79
|
-
|
80
|
-
class ExpandedTextEditDelegate(QItemDelegate):
|
81
|
-
def createEditor(self, parent, option, index):
|
82
|
-
return QTextEdit(parent)
|
83
|
-
|
84
|
-
def sizeHint(self, option, index):
|
85
|
-
size = super().sizeHint(option, index)
|
86
|
-
return size
|
87
|
-
|
88
|
-
def setEditorData(self, editor, index):
|
89
|
-
value = index.model().data(index, Qt.EditRole)
|
90
|
-
editor.setPlainText(value)
|
91
|
-
|
92
|
-
def setModelData(self, editor, model, index):
|
93
|
-
value = editor.toPlainText()
|
94
|
-
model.setData(index, value, Qt.EditRole)
|
95
|
-
|
96
|
-
def updateEditorGeometry(self, editor, option, index):
|
97
|
-
editor.setGeometry(option.rect)
|
98
|
-
|
99
|
-
|
100
|
-
class CompactTextEditDelegate(ExpandedTextEditDelegate):
|
101
|
-
def sizeHint(self, option, index):
|
102
|
-
size = super().sizeHint(option, index)
|
103
|
-
# todo 如果一个4k的屏幕使用2k的时候,这个缩放比例不会自动兼容,阁下又当如何应对
|
104
|
-
size.setHeight(20) # 限制最大高度为20像素,限定每个条目只展示一行
|
105
|
-
return size
|
106
|
-
|
107
|
-
def paint(self, painter, option, index):
|
108
|
-
text = index.model().data(index)
|
109
|
-
|
110
|
-
# 只显示前100个字符
|
111
|
-
text = text[:100] + '...' if len(text) > 100 else text
|
112
|
-
|
113
|
-
painter.drawText(option.rect, Qt.AlignLeft, text)
|
114
|
-
|
115
|
-
|
116
|
-
class JLineViewer(QMainWindow):
|
117
|
-
def __init__(self):
|
118
|
-
super(JLineViewer, self).__init__()
|
119
|
-
self.load_settings()
|
120
|
-
|
121
|
-
# 初始化 allItemsLoaded 变量
|
122
|
-
self.allItemsLoaded = False
|
123
|
-
|
124
|
-
self.initUI()
|
125
|
-
# 开启部件接受拖放的能力(在windows中测试该功能失败)
|
126
|
-
self.setAcceptDrops(True)
|
127
|
-
|
128
|
-
def load_settings(self):
|
129
|
-
self.settings = QSettings('pyxllib', 'JLineViewer')
|
130
|
-
self.lastOpenDir = self.settings.value('lastOpenDir', '')
|
131
|
-
|
132
|
-
def save_settings(self):
|
133
|
-
self.settings.setValue('lastOpenDir', self.lastOpenDir)
|
134
|
-
|
135
|
-
def initUI(self):
|
136
|
-
self.listWidget = QListWidget()
|
137
|
-
self.treeView = MyTreeView()
|
138
|
-
self.plainTextEdit = QPlainTextEdit()
|
139
|
-
self.searchLineEdit = QLineEdit()
|
140
|
-
self.searchButton = QPushButton("普通搜索")
|
141
|
-
|
142
|
-
self.listWidget.itemClicked.connect(self.loadJson)
|
143
|
-
self.searchButton.clicked.connect(self.searchItems)
|
144
|
-
self.searchLineEdit.returnPressed.connect(self.searchItems)
|
145
|
-
self.treeView.clicked.connect(self.editItem)
|
146
|
-
|
147
|
-
splitter = QSplitter(Qt.Horizontal)
|
148
|
-
splitter.addWidget(self.addPane(self.listWidget, 'JSONL Items'))
|
149
|
-
splitter.addWidget(self.addPane(self.treeView, 'JSON Tree View'))
|
150
|
-
splitter.addWidget(self.addPane(self.plainTextEdit, 'Selected Content'))
|
151
|
-
splitter.setSizes([100, 300, 200])
|
152
|
-
|
153
|
-
layout = QVBoxLayout()
|
154
|
-
searchLayout = QHBoxLayout()
|
155
|
-
searchLayout.addWidget(QLabel("搜索条目:"))
|
156
|
-
searchLayout.addWidget(self.searchLineEdit)
|
157
|
-
searchLayout.addWidget(self.searchButton)
|
158
|
-
layout.addLayout(searchLayout)
|
159
|
-
layout.addWidget(splitter)
|
160
|
-
|
161
|
-
self.regexSearchButton = QPushButton("正则搜索")
|
162
|
-
self.regexSearchButton.clicked.connect(self.regexSearchItems) # 连接新的槽函数
|
163
|
-
searchLayout.addWidget(self.regexSearchButton) # 添加按钮到布局
|
164
|
-
|
165
|
-
centralWidget = QWidget()
|
166
|
-
self.setCentralWidget(centralWidget)
|
167
|
-
centralWidget.setLayout(layout)
|
168
|
-
|
169
|
-
openFile = QAction('打开文件', self)
|
170
|
-
openFile.setShortcut('Ctrl+O')
|
171
|
-
openFile.setStatusTip('打开新文件,可以打开jsonl或json格式的文件')
|
172
|
-
openFile.triggered.connect(self.showDialog)
|
173
|
-
|
174
|
-
self.reloadButton = QAction('重新加载', self)
|
175
|
-
self.reloadButton.triggered.connect(self.reload)
|
176
|
-
|
177
|
-
self.loadAllButton = QAction("加载全部", self)
|
178
|
-
self.loadAllButton.setStatusTip('对jsonl最多只会预加载1000行,点击该按钮可以加载剩余全部条目')
|
179
|
-
self.loadAllButton.triggered.connect(self.loadAllItems)
|
180
|
-
|
181
|
-
self.delegate_mode = 'compact'
|
182
|
-
self.toggleDelegateButton = QAction('单行模式', self)
|
183
|
-
self.toggleDelegateButton.triggered.connect(self.toggleDelegate)
|
184
|
-
|
185
|
-
saveFile = QAction('保存文件', self)
|
186
|
-
saveFile.setShortcut('Ctrl+S')
|
187
|
-
saveFile.setStatusTip('保存文件')
|
188
|
-
saveFile.triggered.connect(self.saveFile)
|
189
|
-
|
190
|
-
toolbar = self.addToolBar('文件')
|
191
|
-
toolbar.addAction(openFile)
|
192
|
-
toolbar.addAction(self.reloadButton)
|
193
|
-
toolbar.addAction(self.loadAllButton) # 将按钮添加到布局中
|
194
|
-
toolbar.addAction(self.toggleDelegateButton)
|
195
|
-
# toolbar.addAction(saveFile)
|
196
|
-
|
197
|
-
self.statusBar = QStatusBar()
|
198
|
-
self.setStatusBar(self.statusBar)
|
199
|
-
|
200
|
-
self.setGeometry(300, 300, 350, 300)
|
201
|
-
self.setWindowTitle('JLineEditor')
|
202
|
-
self.treeView.setAlternatingRowColors(True)
|
203
|
-
self.treeView.setIndentation(20)
|
204
|
-
# self.treeView.setSortingEnabled(True)
|
205
|
-
self.treeView.setStyleSheet(LargeStrings.treeViewStyles)
|
206
|
-
# self.plainTextEdit.setWordWrapMode(QTextOption.WordWrap)
|
207
|
-
self.plainTextEdit.setReadOnly(True)
|
208
|
-
self.showMaximized()
|
209
|
-
|
210
|
-
self.treeView.setSortingEnabled(False) # 禁止排序
|
211
|
-
self.treeView.setAnimated(False)
|
212
|
-
# self.plainTextEdit.textChanged.connect(self.updateJson) # 连接 textChanged 信号到新的槽函数
|
213
|
-
|
214
|
-
def toggleDelegate(self):
|
215
|
-
if self.delegate_mode == 'compact':
|
216
|
-
self.treeView.setItemDelegate(ExpandedTextEditDelegate(self.treeView))
|
217
|
-
self.toggleDelegateButton.setText('单行模式')
|
218
|
-
self.delegate_mode = 'expanded'
|
219
|
-
else:
|
220
|
-
self.treeView.setItemDelegate(CompactTextEditDelegate(self.treeView))
|
221
|
-
self.toggleDelegateButton.setText('多行模式')
|
222
|
-
self.delegate_mode = 'compact'
|
223
|
-
|
224
|
-
def addPane(self, widget, title):
|
225
|
-
layout = QVBoxLayout()
|
226
|
-
layout.addWidget(QLabel(title))
|
227
|
-
layout.addWidget(widget)
|
228
|
-
pane = QWidget()
|
229
|
-
pane.setLayout(layout)
|
230
|
-
return pane
|
231
|
-
|
232
|
-
def saveFile(self):
|
233
|
-
fname = QFileDialog.getSaveFileName(self, '保存文件',
|
234
|
-
self.lastOpenDir if hasattr(self, 'lastOpenPath') else '/home')
|
235
|
-
|
236
|
-
if fname[0]:
|
237
|
-
self.lastOpenDir = fname[0]
|
238
|
-
with open(fname[0], 'w', encoding='utf8') as f:
|
239
|
-
for line in self.lines:
|
240
|
-
f.write(line)
|
241
|
-
|
242
|
-
def updateJson(self):
|
243
|
-
newText = self.plainTextEdit.toPlainText()
|
244
|
-
self.currentlyEditingItem.setText(newText) # 更新模型项的内容
|
245
|
-
|
246
|
-
# 更新 JSON 数据
|
247
|
-
# self.lines[self.listWidget.currentRow()] = self.modelToJson(self.model)
|
248
|
-
|
249
|
-
def showDialog(self, *, fname=None):
|
250
|
-
if fname is None:
|
251
|
-
fname = QFileDialog.getOpenFileName(self, '打开文件',
|
252
|
-
self.lastOpenDir,
|
253
|
-
"JSON files (*.json *.jsonl)")
|
254
|
-
fname = fname[0]
|
255
|
-
|
256
|
-
if fname:
|
257
|
-
self.lastOpenDir = os.path.dirname(QFileInfo(fname).absolutePath())
|
258
|
-
self.save_settings()
|
259
|
-
|
260
|
-
# 打开新文件时,重置 allItemsLoaded 变量
|
261
|
-
self.allItemsLoaded = False
|
262
|
-
|
263
|
-
# 清空旧数据
|
264
|
-
self.lines = []
|
265
|
-
self.listWidget.clear()
|
266
|
-
|
267
|
-
# 1 打开文件
|
268
|
-
start_time = time.time() # 开始计时
|
269
|
-
self.lastOpenDir = fname
|
270
|
-
if fname.endswith('.json'):
|
271
|
-
with open(fname, 'r', encoding='utf8') as f:
|
272
|
-
jsonData = json.load(f)
|
273
|
-
self.lines = [json.dumps(jsonData, ensure_ascii=False)]
|
274
|
-
else:
|
275
|
-
with open(fname, 'r', encoding='utf8') as f:
|
276
|
-
self.lines = f.readlines()
|
277
|
-
self.statusBar.showMessage(f"文件打开耗时: {time.time() - start_time:.2f} 秒")
|
278
|
-
QApplication.processEvents()
|
279
|
-
|
280
|
-
# 2 加载条目数据
|
281
|
-
start_time = time.time()
|
282
|
-
self.setWindowTitle(f'JLineViewer - {fname}')
|
283
|
-
self.listWidget.addItems([f'{i + 1}. {line.strip()[:1000]}' # 这里要限制长度,不然遇到长文本软件会巨卡
|
284
|
-
for i, line in enumerate(self.lines[:1000])])
|
285
|
-
QApplication.processEvents()
|
286
|
-
|
287
|
-
# TODO 只有总条目数大于1000时才显示"仅预加载1000条"
|
288
|
-
if len(self.lines) > 1000:
|
289
|
-
self.statusBar.showMessage(f"总条目数: {len(self.lines)}, 仅预加载1000条,"
|
290
|
-
f"加载条目耗时: {time.time() - start_time:.2f} 秒")
|
291
|
-
else:
|
292
|
-
self.statusBar.showMessage(f"总条目数: {len(self.lines)}, 加载条目耗时: {time.time() - start_time:.2f} 秒")
|
293
|
-
|
294
|
-
def reload(self):
|
295
|
-
self.showDialog(fname=self.lastOpenDir or None)
|
296
|
-
|
297
|
-
def loadJson(self, item):
|
298
|
-
index = self.listWidget.row(item)
|
299
|
-
jsonData = json.loads(self.lines[index])
|
300
|
-
self.model = self.dictToModel(jsonData)
|
301
|
-
self.treeView.setModel(self.model)
|
302
|
-
self.treeView.expandAll()
|
303
|
-
|
304
|
-
# 使用自定义的delegate
|
305
|
-
if self.delegate_mode == 'compact':
|
306
|
-
self.treeView.setItemDelegate(CompactTextEditDelegate(self.treeView))
|
307
|
-
else:
|
308
|
-
self.treeView.setItemDelegate(ExpandedTextEditDelegate(self.treeView))
|
309
|
-
|
310
|
-
self.treeView.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
311
|
-
self.treeView.header().setSectionResizeMode(1, QHeaderView.Stretch)
|
312
|
-
|
313
|
-
if hasattr(self, 'lastClickedPath'):
|
314
|
-
path = self.lastClickedPath
|
315
|
-
index = self.model.index(path[0][0], path[0][1]) # 从根开始
|
316
|
-
for row, col in path[1:]:
|
317
|
-
index = self.model.index(row, col, index)
|
318
|
-
if index.isValid():
|
319
|
-
self.treeView.setCurrentIndex(index)
|
320
|
-
self.editItem(index)
|
321
|
-
|
322
|
-
def loadAllItems(self):
|
323
|
-
if not self.allItemsLoaded:
|
324
|
-
start_time = time.time()
|
325
|
-
self.listWidget.clear()
|
326
|
-
self.listWidget.addItems([f'{i + 1}. {line.strip()[:1000]}'
|
327
|
-
for i, line in enumerate(self.lines)])
|
328
|
-
QApplication.processEvents()
|
329
|
-
self.statusBar.showMessage(f"全部加载完毕, 总条目数: {len(self.lines)}, 加载耗时: {time.time() - start_time:.2f} 秒")
|
330
|
-
|
331
|
-
# 加载完所有项目后,设置 allItemsLoaded 变量为 True
|
332
|
-
self.allItemsLoaded = True
|
333
|
-
else:
|
334
|
-
self.statusBar.showMessage(f"所有条目已经加载完毕。")
|
335
|
-
|
336
|
-
def get_item_full_text(self, item):
|
337
|
-
t = item.text()
|
338
|
-
idx = re.search(r'\d+', t).group()
|
339
|
-
return f'{idx}. {self.lines[int(idx) - 1]}'
|
340
|
-
|
341
|
-
def searchItems(self):
|
342
|
-
if hasattr(self, 'lines'):
|
343
|
-
start_time = time.time()
|
344
|
-
searchText = self.searchLineEdit.text()
|
345
|
-
foundCount = 0
|
346
|
-
for i in range(self.listWidget.count()):
|
347
|
-
item = self.listWidget.item(i)
|
348
|
-
if searchText in self.get_item_full_text(item):
|
349
|
-
item.setHidden(False)
|
350
|
-
foundCount += 1
|
351
|
-
else:
|
352
|
-
item.setHidden(True)
|
353
|
-
QApplication.processEvents()
|
354
|
-
self.statusBar.showMessage(
|
355
|
-
f"总条目数: {len(self.lines)}, 找到: {foundCount}, 搜索耗时: {time.time() - start_time:.2f} 秒")
|
356
|
-
|
357
|
-
def regexSearchItems(self):
|
358
|
-
if hasattr(self, 'lines'):
|
359
|
-
start_time = time.time()
|
360
|
-
searchText = self.searchLineEdit.text()
|
361
|
-
regexPattern = re.compile(searchText) # 使用输入的文本创建正则表达式
|
362
|
-
foundCount = 0
|
363
|
-
for i in range(self.listWidget.count()):
|
364
|
-
item = self.listWidget.item(i)
|
365
|
-
if regexPattern.search(self.get_item_full_text(item)): # 使用正则表达式搜索
|
366
|
-
item.setHidden(False)
|
367
|
-
foundCount += 1
|
368
|
-
else:
|
369
|
-
item.setHidden(True)
|
370
|
-
self.statusBar.showMessage(
|
371
|
-
f"总条目数: {len(self.lines)}, 找到: {foundCount}, 搜索耗时: {time.time() - start_time:.2f} 秒")
|
372
|
-
|
373
|
-
def itemToData(self, key_item):
|
374
|
-
value_item = key_item.model().item(key_item.row(), 1)
|
375
|
-
return value_item.text()
|
376
|
-
|
377
|
-
def editItem(self, index=None):
|
378
|
-
self.currentlyEditingItem = self.model.itemFromIndex(index) # 保存当前正在编辑的项
|
379
|
-
target_text = self.currentlyEditingItem.text()
|
380
|
-
self.plainTextEdit.setPlainText(target_text)
|
381
|
-
|
382
|
-
# 保存路径
|
383
|
-
self.lastClickedPath = []
|
384
|
-
while index.isValid():
|
385
|
-
self.lastClickedPath.append((index.row(), index.column()))
|
386
|
-
index = index.parent()
|
387
|
-
self.lastClickedPath.reverse()
|
388
|
-
|
389
|
-
def dictToModel(self, data, parent=None):
|
390
|
-
if parent is None:
|
391
|
-
parent = QStandardItemModel()
|
392
|
-
parent.setHorizontalHeaderLabels(['Key', 'Value'])
|
393
|
-
|
394
|
-
# if isinstance(data, dict):
|
395
|
-
# for key, value in data.items():
|
396
|
-
# self.dataToModel(key, value, parent)
|
397
|
-
|
398
|
-
# 判断数据类型,并相应处理
|
399
|
-
if isinstance(data, dict):
|
400
|
-
for key, value in data.items():
|
401
|
-
self.dataToModel(key, value, parent)
|
402
|
-
elif isinstance(data, list):
|
403
|
-
# 处理列表:创建一个无key的父项,将列表元素作为子项添加
|
404
|
-
self.dataToModel("List", data, parent)
|
405
|
-
else:
|
406
|
-
# 处理基本数据类型:创建一个单独的条目
|
407
|
-
self.dataToModel("Value", data, parent)
|
408
|
-
|
409
|
-
return parent
|
410
|
-
|
411
|
-
def dataToModel(self, key, value, parent):
|
412
|
-
if isinstance(value, dict):
|
413
|
-
item = KeyStandardItem(key)
|
414
|
-
parent.appendRow([item, QStandardItem('')])
|
415
|
-
for k, v in value.items():
|
416
|
-
self.dataToModel(k, v, item)
|
417
|
-
elif isinstance(value, list):
|
418
|
-
# 方案 1
|
419
|
-
# for i, v in enumerate(value):
|
420
|
-
# self.dataToModel(f"{key}[{i}]", v, parent)
|
421
|
-
|
422
|
-
# 方案2 添加一个父节点
|
423
|
-
list_parent = KeyStandardItem(key)
|
424
|
-
parent.appendRow([list_parent, QStandardItem('')])
|
425
|
-
# 将list的元素添加到父节点下
|
426
|
-
for i, v in enumerate(value):
|
427
|
-
self.dataToModel(f"{key}[{i}]", v, list_parent)
|
428
|
-
else:
|
429
|
-
parent.appendRow([KeyStandardItem(key), QStandardItem(str(value))])
|
430
|
-
|
431
|
-
# def itemChanged(self, item):
|
432
|
-
# # 当一个模型项改变时,重新生成 JSON 数据
|
433
|
-
# self.lines[self.listWidget.currentRow()] = self.modelToJson(self.model)
|
434
|
-
#
|
435
|
-
# def modelToJson(self, model, parent=QModelIndex(), key=None):
|
436
|
-
# """ 这段功能有问题,暂不能开启 """
|
437
|
-
# rows = model.rowCount(parent)
|
438
|
-
# if rows == 0:
|
439
|
-
# # leaf node
|
440
|
-
# sibling = model.sibling(parent.row(), 1, parent)
|
441
|
-
# return model.data(sibling)
|
442
|
-
# else:
|
443
|
-
# # branch node
|
444
|
-
# json_data = {}
|
445
|
-
# for i in range(rows):
|
446
|
-
# index = model.index(i, 0, parent)
|
447
|
-
# child_key = model.data(index)
|
448
|
-
# child_value = self.modelToJson(model, index, child_key)
|
449
|
-
# json_data[child_key] = child_value
|
450
|
-
# return json.dumps(json_data, ensure_ascii=False)
|
451
|
-
|
452
|
-
def dragEnterEvent(self, event):
|
453
|
-
"""
|
454
|
-
当用户开始拖动文件到部件上时,这个方法会被调用。
|
455
|
-
我们需要检查拖动的数据是不是文件类型(mime类型是'text/uri-list')。
|
456
|
-
"""
|
457
|
-
print("dragEnterEvent called")
|
458
|
-
# if event.mimeData().hasFormat('text/uri-list'):
|
459
|
-
event.acceptProposedAction()
|
460
|
-
|
461
|
-
def dropEvent(self, event):
|
462
|
-
"""
|
463
|
-
当用户在部件上释放(drop)文件时,这个方法会被调用。
|
464
|
-
我们需要获取文件路径,然后判断文件类型是不是我们支持的类型。
|
465
|
-
如果文件是我们支持的类型,我们就可以处理这个文件。
|
466
|
-
"""
|
467
|
-
print("dropEvent called")
|
468
|
-
# 获取文件路径
|
469
|
-
file_paths = event.mimeData().urls()
|
470
|
-
if file_paths:
|
471
|
-
file_path = file_paths[0].toLocalFile() # 取第一个文件
|
472
|
-
# 检查文件扩展名是不是我们支持的类型
|
473
|
-
if file_path.endswith(('.json', '.jsonl')):
|
474
|
-
# 调用处理文件的方法
|
475
|
-
self.showDialog(fname=file_path)
|
476
|
-
else:
|
477
|
-
QMessageBox.warning(self, "File Type Error",
|
478
|
-
"Only .json or .jsonl files are supported.")
|
479
|
-
|
480
|
-
# 当应用程序关闭时,保存设置
|
481
|
-
def closeEvent(self, event):
|
482
|
-
self.save_settings()
|
483
|
-
event.accept()
|
484
|
-
|
485
|
-
|
486
|
-
def start_jlineviewer(fname=None):
|
487
|
-
app = QApplication(sys.argv)
|
488
|
-
ex = JLineViewer()
|
489
|
-
if isinstance(fname, list): # 可以输入一个list字典数据,会转存到临时目录里查看
|
490
|
-
tempfile = XlPath.tempfile(suffix='.jsonl')
|
491
|
-
tempfile.write_jsonl(fname)
|
492
|
-
fname = tempfile.as_posix()
|
493
|
-
if fname:
|
494
|
-
ex.showDialog(fname=fname)
|
495
|
-
sys.exit(app.exec_())
|
496
|
-
|
497
|
-
|
498
|
-
if __name__ == '__main__':
|
499
|
-
app = QApplication(sys.argv)
|
500
|
-
ex = JLineViewer()
|
501
|
-
|
502
|
-
if len(sys.argv) > 1:
|
503
|
-
ex.showDialog(fname=sys.argv[1])
|
504
|
-
|
505
|
-
sys.exit(app.exec_())
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# @Author : 陈坤泽
|
4
|
+
# @Email : 877362867@qq.com
|
5
|
+
# @Date : 2023/08/02 14:05
|
6
|
+
|
7
|
+
import os
|
8
|
+
import random
|
9
|
+
import re
|
10
|
+
import sys
|
11
|
+
import json
|
12
|
+
import time
|
13
|
+
from types import SimpleNamespace
|
14
|
+
|
15
|
+
import warnings
|
16
|
+
|
17
|
+
warnings.filterwarnings('ignore')
|
18
|
+
|
19
|
+
import logging
|
20
|
+
|
21
|
+
logging.disable(logging.CRITICAL)
|
22
|
+
|
23
|
+
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QFileDialog, QListWidget, QLineEdit, QVBoxLayout, \
|
24
|
+
QSplitter, QTreeView, QPlainTextEdit, QPushButton, QLabel, QHBoxLayout, QSizePolicy, QWidget, QStatusBar, \
|
25
|
+
QAbstractItemView, QHeaderView, QMessageBox
|
26
|
+
from PyQt5.QtWidgets import QItemDelegate, QTextEdit
|
27
|
+
from PyQt5.QtWidgets import QItemDelegate, QDialog, QVBoxLayout, QTextEdit, QPushButton
|
28
|
+
|
29
|
+
from PyQt5.QtGui import QTextOption, QStandardItemModel, QStandardItem
|
30
|
+
from PyQt5.QtCore import Qt, QModelIndex, QSettings, QFileInfo
|
31
|
+
|
32
|
+
from pyxllib.file.specialist import XlPath
|
33
|
+
|
34
|
+
# 一个专门存储大字符串的命名空间
|
35
|
+
LargeStrings = SimpleNamespace()
|
36
|
+
|
37
|
+
# 命名并存储QTreeView的样式(旧的格式配置,在新版中已经不起作用)
|
38
|
+
LargeStrings.treeViewStyles = """
|
39
|
+
QTreeView::item { /* 设置网格线 */
|
40
|
+
border: 1px solid black;
|
41
|
+
}
|
42
|
+
|
43
|
+
QTreeView::item:selected {
|
44
|
+
background: black;
|
45
|
+
color: white;
|
46
|
+
}
|
47
|
+
|
48
|
+
QTreeView::item:selected:active {
|
49
|
+
background: black;
|
50
|
+
color: white;
|
51
|
+
}
|
52
|
+
|
53
|
+
QTreeView::item:selected:!active {
|
54
|
+
background: black;
|
55
|
+
color: white;
|
56
|
+
}
|
57
|
+
""".strip()
|
58
|
+
|
59
|
+
|
60
|
+
class MyTreeView(QTreeView):
|
61
|
+
def __init__(self, parent=None):
|
62
|
+
super().__init__(parent)
|
63
|
+
|
64
|
+
def edit(self, index, trigger, event):
|
65
|
+
if trigger == QAbstractItemView.DoubleClicked:
|
66
|
+
return False
|
67
|
+
# 如果是第 0 列 (keys),则禁止编辑
|
68
|
+
if index.column() == 0:
|
69
|
+
return False
|
70
|
+
return super().edit(index, trigger, event)
|
71
|
+
|
72
|
+
|
73
|
+
class KeyStandardItem(QStandardItem):
|
74
|
+
def data(self, role=None):
|
75
|
+
if role == Qt.TextAlignmentRole:
|
76
|
+
return Qt.AlignLeft | Qt.AlignVCenter
|
77
|
+
return super().data(role)
|
78
|
+
|
79
|
+
|
80
|
+
class ExpandedTextEditDelegate(QItemDelegate):
|
81
|
+
def createEditor(self, parent, option, index):
|
82
|
+
return QTextEdit(parent)
|
83
|
+
|
84
|
+
def sizeHint(self, option, index):
|
85
|
+
size = super().sizeHint(option, index)
|
86
|
+
return size
|
87
|
+
|
88
|
+
def setEditorData(self, editor, index):
|
89
|
+
value = index.model().data(index, Qt.EditRole)
|
90
|
+
editor.setPlainText(value)
|
91
|
+
|
92
|
+
def setModelData(self, editor, model, index):
|
93
|
+
value = editor.toPlainText()
|
94
|
+
model.setData(index, value, Qt.EditRole)
|
95
|
+
|
96
|
+
def updateEditorGeometry(self, editor, option, index):
|
97
|
+
editor.setGeometry(option.rect)
|
98
|
+
|
99
|
+
|
100
|
+
class CompactTextEditDelegate(ExpandedTextEditDelegate):
|
101
|
+
def sizeHint(self, option, index):
|
102
|
+
size = super().sizeHint(option, index)
|
103
|
+
# todo 如果一个4k的屏幕使用2k的时候,这个缩放比例不会自动兼容,阁下又当如何应对
|
104
|
+
size.setHeight(20) # 限制最大高度为20像素,限定每个条目只展示一行
|
105
|
+
return size
|
106
|
+
|
107
|
+
def paint(self, painter, option, index):
|
108
|
+
text = index.model().data(index)
|
109
|
+
|
110
|
+
# 只显示前100个字符
|
111
|
+
text = text[:100] + '...' if len(text) > 100 else text
|
112
|
+
|
113
|
+
painter.drawText(option.rect, Qt.AlignLeft, text)
|
114
|
+
|
115
|
+
|
116
|
+
class JLineViewer(QMainWindow):
|
117
|
+
def __init__(self):
|
118
|
+
super(JLineViewer, self).__init__()
|
119
|
+
self.load_settings()
|
120
|
+
|
121
|
+
# 初始化 allItemsLoaded 变量
|
122
|
+
self.allItemsLoaded = False
|
123
|
+
|
124
|
+
self.initUI()
|
125
|
+
# 开启部件接受拖放的能力(在windows中测试该功能失败)
|
126
|
+
self.setAcceptDrops(True)
|
127
|
+
|
128
|
+
def load_settings(self):
|
129
|
+
self.settings = QSettings('pyxllib', 'JLineViewer')
|
130
|
+
self.lastOpenDir = self.settings.value('lastOpenDir', '')
|
131
|
+
|
132
|
+
def save_settings(self):
|
133
|
+
self.settings.setValue('lastOpenDir', self.lastOpenDir)
|
134
|
+
|
135
|
+
def initUI(self):
|
136
|
+
self.listWidget = QListWidget()
|
137
|
+
self.treeView = MyTreeView()
|
138
|
+
self.plainTextEdit = QPlainTextEdit()
|
139
|
+
self.searchLineEdit = QLineEdit()
|
140
|
+
self.searchButton = QPushButton("普通搜索")
|
141
|
+
|
142
|
+
self.listWidget.itemClicked.connect(self.loadJson)
|
143
|
+
self.searchButton.clicked.connect(self.searchItems)
|
144
|
+
self.searchLineEdit.returnPressed.connect(self.searchItems)
|
145
|
+
self.treeView.clicked.connect(self.editItem)
|
146
|
+
|
147
|
+
splitter = QSplitter(Qt.Horizontal)
|
148
|
+
splitter.addWidget(self.addPane(self.listWidget, 'JSONL Items'))
|
149
|
+
splitter.addWidget(self.addPane(self.treeView, 'JSON Tree View'))
|
150
|
+
splitter.addWidget(self.addPane(self.plainTextEdit, 'Selected Content'))
|
151
|
+
splitter.setSizes([100, 300, 200])
|
152
|
+
|
153
|
+
layout = QVBoxLayout()
|
154
|
+
searchLayout = QHBoxLayout()
|
155
|
+
searchLayout.addWidget(QLabel("搜索条目:"))
|
156
|
+
searchLayout.addWidget(self.searchLineEdit)
|
157
|
+
searchLayout.addWidget(self.searchButton)
|
158
|
+
layout.addLayout(searchLayout)
|
159
|
+
layout.addWidget(splitter)
|
160
|
+
|
161
|
+
self.regexSearchButton = QPushButton("正则搜索")
|
162
|
+
self.regexSearchButton.clicked.connect(self.regexSearchItems) # 连接新的槽函数
|
163
|
+
searchLayout.addWidget(self.regexSearchButton) # 添加按钮到布局
|
164
|
+
|
165
|
+
centralWidget = QWidget()
|
166
|
+
self.setCentralWidget(centralWidget)
|
167
|
+
centralWidget.setLayout(layout)
|
168
|
+
|
169
|
+
openFile = QAction('打开文件', self)
|
170
|
+
openFile.setShortcut('Ctrl+O')
|
171
|
+
openFile.setStatusTip('打开新文件,可以打开jsonl或json格式的文件')
|
172
|
+
openFile.triggered.connect(self.showDialog)
|
173
|
+
|
174
|
+
self.reloadButton = QAction('重新加载', self)
|
175
|
+
self.reloadButton.triggered.connect(self.reload)
|
176
|
+
|
177
|
+
self.loadAllButton = QAction("加载全部", self)
|
178
|
+
self.loadAllButton.setStatusTip('对jsonl最多只会预加载1000行,点击该按钮可以加载剩余全部条目')
|
179
|
+
self.loadAllButton.triggered.connect(self.loadAllItems)
|
180
|
+
|
181
|
+
self.delegate_mode = 'compact'
|
182
|
+
self.toggleDelegateButton = QAction('单行模式', self)
|
183
|
+
self.toggleDelegateButton.triggered.connect(self.toggleDelegate)
|
184
|
+
|
185
|
+
saveFile = QAction('保存文件', self)
|
186
|
+
saveFile.setShortcut('Ctrl+S')
|
187
|
+
saveFile.setStatusTip('保存文件')
|
188
|
+
saveFile.triggered.connect(self.saveFile)
|
189
|
+
|
190
|
+
toolbar = self.addToolBar('文件')
|
191
|
+
toolbar.addAction(openFile)
|
192
|
+
toolbar.addAction(self.reloadButton)
|
193
|
+
toolbar.addAction(self.loadAllButton) # 将按钮添加到布局中
|
194
|
+
toolbar.addAction(self.toggleDelegateButton)
|
195
|
+
# toolbar.addAction(saveFile)
|
196
|
+
|
197
|
+
self.statusBar = QStatusBar()
|
198
|
+
self.setStatusBar(self.statusBar)
|
199
|
+
|
200
|
+
self.setGeometry(300, 300, 350, 300)
|
201
|
+
self.setWindowTitle('JLineEditor')
|
202
|
+
self.treeView.setAlternatingRowColors(True)
|
203
|
+
self.treeView.setIndentation(20)
|
204
|
+
# self.treeView.setSortingEnabled(True)
|
205
|
+
self.treeView.setStyleSheet(LargeStrings.treeViewStyles)
|
206
|
+
# self.plainTextEdit.setWordWrapMode(QTextOption.WordWrap)
|
207
|
+
self.plainTextEdit.setReadOnly(True)
|
208
|
+
self.showMaximized()
|
209
|
+
|
210
|
+
self.treeView.setSortingEnabled(False) # 禁止排序
|
211
|
+
self.treeView.setAnimated(False)
|
212
|
+
# self.plainTextEdit.textChanged.connect(self.updateJson) # 连接 textChanged 信号到新的槽函数
|
213
|
+
|
214
|
+
def toggleDelegate(self):
|
215
|
+
if self.delegate_mode == 'compact':
|
216
|
+
self.treeView.setItemDelegate(ExpandedTextEditDelegate(self.treeView))
|
217
|
+
self.toggleDelegateButton.setText('单行模式')
|
218
|
+
self.delegate_mode = 'expanded'
|
219
|
+
else:
|
220
|
+
self.treeView.setItemDelegate(CompactTextEditDelegate(self.treeView))
|
221
|
+
self.toggleDelegateButton.setText('多行模式')
|
222
|
+
self.delegate_mode = 'compact'
|
223
|
+
|
224
|
+
def addPane(self, widget, title):
|
225
|
+
layout = QVBoxLayout()
|
226
|
+
layout.addWidget(QLabel(title))
|
227
|
+
layout.addWidget(widget)
|
228
|
+
pane = QWidget()
|
229
|
+
pane.setLayout(layout)
|
230
|
+
return pane
|
231
|
+
|
232
|
+
def saveFile(self):
|
233
|
+
fname = QFileDialog.getSaveFileName(self, '保存文件',
|
234
|
+
self.lastOpenDir if hasattr(self, 'lastOpenPath') else '/home')
|
235
|
+
|
236
|
+
if fname[0]:
|
237
|
+
self.lastOpenDir = fname[0]
|
238
|
+
with open(fname[0], 'w', encoding='utf8') as f:
|
239
|
+
for line in self.lines:
|
240
|
+
f.write(line)
|
241
|
+
|
242
|
+
def updateJson(self):
|
243
|
+
newText = self.plainTextEdit.toPlainText()
|
244
|
+
self.currentlyEditingItem.setText(newText) # 更新模型项的内容
|
245
|
+
|
246
|
+
# 更新 JSON 数据
|
247
|
+
# self.lines[self.listWidget.currentRow()] = self.modelToJson(self.model)
|
248
|
+
|
249
|
+
def showDialog(self, *, fname=None):
|
250
|
+
if fname is None:
|
251
|
+
fname = QFileDialog.getOpenFileName(self, '打开文件',
|
252
|
+
self.lastOpenDir,
|
253
|
+
"JSON files (*.json *.jsonl)")
|
254
|
+
fname = fname[0]
|
255
|
+
|
256
|
+
if fname:
|
257
|
+
self.lastOpenDir = os.path.dirname(QFileInfo(fname).absolutePath())
|
258
|
+
self.save_settings()
|
259
|
+
|
260
|
+
# 打开新文件时,重置 allItemsLoaded 变量
|
261
|
+
self.allItemsLoaded = False
|
262
|
+
|
263
|
+
# 清空旧数据
|
264
|
+
self.lines = []
|
265
|
+
self.listWidget.clear()
|
266
|
+
|
267
|
+
# 1 打开文件
|
268
|
+
start_time = time.time() # 开始计时
|
269
|
+
self.lastOpenDir = fname
|
270
|
+
if fname.endswith('.json'):
|
271
|
+
with open(fname, 'r', encoding='utf8') as f:
|
272
|
+
jsonData = json.load(f)
|
273
|
+
self.lines = [json.dumps(jsonData, ensure_ascii=False)]
|
274
|
+
else:
|
275
|
+
with open(fname, 'r', encoding='utf8') as f:
|
276
|
+
self.lines = f.readlines()
|
277
|
+
self.statusBar.showMessage(f"文件打开耗时: {time.time() - start_time:.2f} 秒")
|
278
|
+
QApplication.processEvents()
|
279
|
+
|
280
|
+
# 2 加载条目数据
|
281
|
+
start_time = time.time()
|
282
|
+
self.setWindowTitle(f'JLineViewer - {fname}')
|
283
|
+
self.listWidget.addItems([f'{i + 1}. {line.strip()[:1000]}' # 这里要限制长度,不然遇到长文本软件会巨卡
|
284
|
+
for i, line in enumerate(self.lines[:1000])])
|
285
|
+
QApplication.processEvents()
|
286
|
+
|
287
|
+
# TODO 只有总条目数大于1000时才显示"仅预加载1000条"
|
288
|
+
if len(self.lines) > 1000:
|
289
|
+
self.statusBar.showMessage(f"总条目数: {len(self.lines)}, 仅预加载1000条,"
|
290
|
+
f"加载条目耗时: {time.time() - start_time:.2f} 秒")
|
291
|
+
else:
|
292
|
+
self.statusBar.showMessage(f"总条目数: {len(self.lines)}, 加载条目耗时: {time.time() - start_time:.2f} 秒")
|
293
|
+
|
294
|
+
def reload(self):
|
295
|
+
self.showDialog(fname=self.lastOpenDir or None)
|
296
|
+
|
297
|
+
def loadJson(self, item):
|
298
|
+
index = self.listWidget.row(item)
|
299
|
+
jsonData = json.loads(self.lines[index])
|
300
|
+
self.model = self.dictToModel(jsonData)
|
301
|
+
self.treeView.setModel(self.model)
|
302
|
+
self.treeView.expandAll()
|
303
|
+
|
304
|
+
# 使用自定义的delegate
|
305
|
+
if self.delegate_mode == 'compact':
|
306
|
+
self.treeView.setItemDelegate(CompactTextEditDelegate(self.treeView))
|
307
|
+
else:
|
308
|
+
self.treeView.setItemDelegate(ExpandedTextEditDelegate(self.treeView))
|
309
|
+
|
310
|
+
self.treeView.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
311
|
+
self.treeView.header().setSectionResizeMode(1, QHeaderView.Stretch)
|
312
|
+
|
313
|
+
if hasattr(self, 'lastClickedPath'):
|
314
|
+
path = self.lastClickedPath
|
315
|
+
index = self.model.index(path[0][0], path[0][1]) # 从根开始
|
316
|
+
for row, col in path[1:]:
|
317
|
+
index = self.model.index(row, col, index)
|
318
|
+
if index.isValid():
|
319
|
+
self.treeView.setCurrentIndex(index)
|
320
|
+
self.editItem(index)
|
321
|
+
|
322
|
+
def loadAllItems(self):
|
323
|
+
if not self.allItemsLoaded:
|
324
|
+
start_time = time.time()
|
325
|
+
self.listWidget.clear()
|
326
|
+
self.listWidget.addItems([f'{i + 1}. {line.strip()[:1000]}'
|
327
|
+
for i, line in enumerate(self.lines)])
|
328
|
+
QApplication.processEvents()
|
329
|
+
self.statusBar.showMessage(f"全部加载完毕, 总条目数: {len(self.lines)}, 加载耗时: {time.time() - start_time:.2f} 秒")
|
330
|
+
|
331
|
+
# 加载完所有项目后,设置 allItemsLoaded 变量为 True
|
332
|
+
self.allItemsLoaded = True
|
333
|
+
else:
|
334
|
+
self.statusBar.showMessage(f"所有条目已经加载完毕。")
|
335
|
+
|
336
|
+
def get_item_full_text(self, item):
|
337
|
+
t = item.text()
|
338
|
+
idx = re.search(r'\d+', t).group()
|
339
|
+
return f'{idx}. {self.lines[int(idx) - 1]}'
|
340
|
+
|
341
|
+
def searchItems(self):
|
342
|
+
if hasattr(self, 'lines'):
|
343
|
+
start_time = time.time()
|
344
|
+
searchText = self.searchLineEdit.text()
|
345
|
+
foundCount = 0
|
346
|
+
for i in range(self.listWidget.count()):
|
347
|
+
item = self.listWidget.item(i)
|
348
|
+
if searchText in self.get_item_full_text(item):
|
349
|
+
item.setHidden(False)
|
350
|
+
foundCount += 1
|
351
|
+
else:
|
352
|
+
item.setHidden(True)
|
353
|
+
QApplication.processEvents()
|
354
|
+
self.statusBar.showMessage(
|
355
|
+
f"总条目数: {len(self.lines)}, 找到: {foundCount}, 搜索耗时: {time.time() - start_time:.2f} 秒")
|
356
|
+
|
357
|
+
def regexSearchItems(self):
|
358
|
+
if hasattr(self, 'lines'):
|
359
|
+
start_time = time.time()
|
360
|
+
searchText = self.searchLineEdit.text()
|
361
|
+
regexPattern = re.compile(searchText) # 使用输入的文本创建正则表达式
|
362
|
+
foundCount = 0
|
363
|
+
for i in range(self.listWidget.count()):
|
364
|
+
item = self.listWidget.item(i)
|
365
|
+
if regexPattern.search(self.get_item_full_text(item)): # 使用正则表达式搜索
|
366
|
+
item.setHidden(False)
|
367
|
+
foundCount += 1
|
368
|
+
else:
|
369
|
+
item.setHidden(True)
|
370
|
+
self.statusBar.showMessage(
|
371
|
+
f"总条目数: {len(self.lines)}, 找到: {foundCount}, 搜索耗时: {time.time() - start_time:.2f} 秒")
|
372
|
+
|
373
|
+
def itemToData(self, key_item):
|
374
|
+
value_item = key_item.model().item(key_item.row(), 1)
|
375
|
+
return value_item.text()
|
376
|
+
|
377
|
+
def editItem(self, index=None):
|
378
|
+
self.currentlyEditingItem = self.model.itemFromIndex(index) # 保存当前正在编辑的项
|
379
|
+
target_text = self.currentlyEditingItem.text()
|
380
|
+
self.plainTextEdit.setPlainText(target_text)
|
381
|
+
|
382
|
+
# 保存路径
|
383
|
+
self.lastClickedPath = []
|
384
|
+
while index.isValid():
|
385
|
+
self.lastClickedPath.append((index.row(), index.column()))
|
386
|
+
index = index.parent()
|
387
|
+
self.lastClickedPath.reverse()
|
388
|
+
|
389
|
+
def dictToModel(self, data, parent=None):
|
390
|
+
if parent is None:
|
391
|
+
parent = QStandardItemModel()
|
392
|
+
parent.setHorizontalHeaderLabels(['Key', 'Value'])
|
393
|
+
|
394
|
+
# if isinstance(data, dict):
|
395
|
+
# for key, value in data.items():
|
396
|
+
# self.dataToModel(key, value, parent)
|
397
|
+
|
398
|
+
# 判断数据类型,并相应处理
|
399
|
+
if isinstance(data, dict):
|
400
|
+
for key, value in data.items():
|
401
|
+
self.dataToModel(key, value, parent)
|
402
|
+
elif isinstance(data, list):
|
403
|
+
# 处理列表:创建一个无key的父项,将列表元素作为子项添加
|
404
|
+
self.dataToModel("List", data, parent)
|
405
|
+
else:
|
406
|
+
# 处理基本数据类型:创建一个单独的条目
|
407
|
+
self.dataToModel("Value", data, parent)
|
408
|
+
|
409
|
+
return parent
|
410
|
+
|
411
|
+
def dataToModel(self, key, value, parent):
|
412
|
+
if isinstance(value, dict):
|
413
|
+
item = KeyStandardItem(key)
|
414
|
+
parent.appendRow([item, QStandardItem('')])
|
415
|
+
for k, v in value.items():
|
416
|
+
self.dataToModel(k, v, item)
|
417
|
+
elif isinstance(value, list):
|
418
|
+
# 方案 1
|
419
|
+
# for i, v in enumerate(value):
|
420
|
+
# self.dataToModel(f"{key}[{i}]", v, parent)
|
421
|
+
|
422
|
+
# 方案2 添加一个父节点
|
423
|
+
list_parent = KeyStandardItem(key)
|
424
|
+
parent.appendRow([list_parent, QStandardItem('')])
|
425
|
+
# 将list的元素添加到父节点下
|
426
|
+
for i, v in enumerate(value):
|
427
|
+
self.dataToModel(f"{key}[{i}]", v, list_parent)
|
428
|
+
else:
|
429
|
+
parent.appendRow([KeyStandardItem(key), QStandardItem(str(value))])
|
430
|
+
|
431
|
+
# def itemChanged(self, item):
|
432
|
+
# # 当一个模型项改变时,重新生成 JSON 数据
|
433
|
+
# self.lines[self.listWidget.currentRow()] = self.modelToJson(self.model)
|
434
|
+
#
|
435
|
+
# def modelToJson(self, model, parent=QModelIndex(), key=None):
|
436
|
+
# """ 这段功能有问题,暂不能开启 """
|
437
|
+
# rows = model.rowCount(parent)
|
438
|
+
# if rows == 0:
|
439
|
+
# # leaf node
|
440
|
+
# sibling = model.sibling(parent.row(), 1, parent)
|
441
|
+
# return model.data(sibling)
|
442
|
+
# else:
|
443
|
+
# # branch node
|
444
|
+
# json_data = {}
|
445
|
+
# for i in range(rows):
|
446
|
+
# index = model.index(i, 0, parent)
|
447
|
+
# child_key = model.data(index)
|
448
|
+
# child_value = self.modelToJson(model, index, child_key)
|
449
|
+
# json_data[child_key] = child_value
|
450
|
+
# return json.dumps(json_data, ensure_ascii=False)
|
451
|
+
|
452
|
+
def dragEnterEvent(self, event):
|
453
|
+
"""
|
454
|
+
当用户开始拖动文件到部件上时,这个方法会被调用。
|
455
|
+
我们需要检查拖动的数据是不是文件类型(mime类型是'text/uri-list')。
|
456
|
+
"""
|
457
|
+
print("dragEnterEvent called")
|
458
|
+
# if event.mimeData().hasFormat('text/uri-list'):
|
459
|
+
event.acceptProposedAction()
|
460
|
+
|
461
|
+
def dropEvent(self, event):
|
462
|
+
"""
|
463
|
+
当用户在部件上释放(drop)文件时,这个方法会被调用。
|
464
|
+
我们需要获取文件路径,然后判断文件类型是不是我们支持的类型。
|
465
|
+
如果文件是我们支持的类型,我们就可以处理这个文件。
|
466
|
+
"""
|
467
|
+
print("dropEvent called")
|
468
|
+
# 获取文件路径
|
469
|
+
file_paths = event.mimeData().urls()
|
470
|
+
if file_paths:
|
471
|
+
file_path = file_paths[0].toLocalFile() # 取第一个文件
|
472
|
+
# 检查文件扩展名是不是我们支持的类型
|
473
|
+
if file_path.endswith(('.json', '.jsonl')):
|
474
|
+
# 调用处理文件的方法
|
475
|
+
self.showDialog(fname=file_path)
|
476
|
+
else:
|
477
|
+
QMessageBox.warning(self, "File Type Error",
|
478
|
+
"Only .json or .jsonl files are supported.")
|
479
|
+
|
480
|
+
# 当应用程序关闭时,保存设置
|
481
|
+
def closeEvent(self, event):
|
482
|
+
self.save_settings()
|
483
|
+
event.accept()
|
484
|
+
|
485
|
+
|
486
|
+
def start_jlineviewer(fname=None):
|
487
|
+
app = QApplication(sys.argv)
|
488
|
+
ex = JLineViewer()
|
489
|
+
if isinstance(fname, list): # 可以输入一个list字典数据,会转存到临时目录里查看
|
490
|
+
tempfile = XlPath.tempfile(suffix='.jsonl')
|
491
|
+
tempfile.write_jsonl(fname)
|
492
|
+
fname = tempfile.as_posix()
|
493
|
+
if fname:
|
494
|
+
ex.showDialog(fname=fname)
|
495
|
+
sys.exit(app.exec_())
|
496
|
+
|
497
|
+
|
498
|
+
if __name__ == '__main__':
|
499
|
+
app = QApplication(sys.argv)
|
500
|
+
ex = JLineViewer()
|
501
|
+
|
502
|
+
if len(sys.argv) > 1:
|
503
|
+
ex.showDialog(fname=sys.argv[1])
|
504
|
+
|
505
|
+
sys.exit(app.exec_())
|