Files
tophux_scrape/product/sqlite_viewer.py

674 lines
25 KiB
Python
Raw Normal View History

2025-11-27 07:54:42 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQLite数据库查看器 - 基于PySide6
功能打开product目录下的product.db的sqlite文件显示表和数据的界面
"""
import sys
import os
import sqlite3
from loguru import logger
from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
QWidget, QPushButton, QTableWidget, QTableWidgetItem,
QListWidget, QListWidgetItem, QSplitter, QFileDialog,
QLabel, QStatusBar, QMessageBox, QHeaderView, QComboBox,
2025-11-28 22:29:47 +08:00
QLineEdit, QGroupBox, QTextEdit, QStyledItemDelegate, QMenu)
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QAction, QFontMetrics
class MultiLineDelegate(QStyledItemDelegate):
"""多行文本委托,支持自动调整行高"""
def __init__(self, parent=None):
super().__init__(parent)
self.min_height = 30 # 最小行高
self.max_height = 200 # 最大行高
def paint(self, painter, option, index):
"""自定义绘制,支持多行文本"""
# 保存原始选项
opt = option
# 获取文本内容
text = index.data(Qt.DisplayRole)
if text is None:
text = ""
# 设置文本换行
text = str(text)
# 计算文本高度
metrics = QFontMetrics(option.font)
rect = option.rect
# 计算需要的行数
lines = text.count('\n') + 1
line_height = metrics.lineSpacing()
text_height = lines * line_height + 10 # 添加一些边距
# 限制高度在最小和最大值之间
if text_height < self.min_height:
text_height = self.min_height
elif text_height > self.max_height:
text_height = self.max_height
# 调整绘制区域高度
opt.rect.setHeight(text_height)
# 调用父类绘制方法
super().paint(painter, opt, index)
def sizeHint(self, option, index):
"""返回建议的单元格大小"""
# 获取文本内容
text = index.data(Qt.DisplayRole)
if text is None:
text = ""
text = str(text)
# 计算文本尺寸
metrics = QFontMetrics(option.font)
# 计算行数
lines = text.count('\n') + 1
line_height = metrics.lineSpacing()
text_height = lines * line_height + 10 # 添加边距
# 计算文本宽度(考虑换行)
if '\n' in text:
# 多行文本,计算最长行的宽度
max_width = 0
for line in text.split('\n'):
line_width = metrics.horizontalAdvance(line) + 20
max_width = max(max_width, line_width)
else:
# 单行文本
max_width = metrics.horizontalAdvance(text) + 20
# 限制高度
if text_height < self.min_height:
text_height = self.min_height
elif text_height > self.max_height:
text_height = self.max_height
# 最小宽度设置为100像素
max_width = max(max_width, 100)
return QSize(max_width, text_height)
2025-11-27 07:54:42 +08:00
class SQLiteViewer(QMainWindow):
"""SQLite数据库查看器主窗口"""
def __init__(self):
super().__init__()
logger.info("初始化SQLite数据库查看器")
self.db_connection = None
self.current_table = None
self.init_ui()
def init_ui(self):
"""初始化用户界面"""
logger.info("设置主窗口界面")
self.setWindowTitle("SQLite数据库查看器")
self.setGeometry(100, 100, 1200, 800)
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout(central_widget)
# 创建顶部按钮布局
self.create_top_buttons(main_layout)
# 创建筛选控件区域
self.create_filter_section(main_layout)
# 创建分割器(左侧表列表,右侧数据表格)
self.create_splitter(main_layout)
# 创建状态栏
self.create_status_bar()
# 创建菜单栏
self.create_menubar()
logger.info("界面初始化完成")
def create_top_buttons(self, layout):
"""创建顶部按钮布局"""
logger.info("创建顶部按钮")
button_layout = QHBoxLayout()
# 打开数据库按钮
self.open_button = QPushButton("打开SQLite数据库")
self.open_button.clicked.connect(self.open_database)
button_layout.addWidget(self.open_button)
# 刷新按钮
self.refresh_button = QPushButton("刷新")
self.refresh_button.clicked.connect(self.refresh_data)
self.refresh_button.setEnabled(False)
button_layout.addWidget(self.refresh_button)
# 数据库路径显示
self.db_path_label = QLabel("未打开数据库")
button_layout.addWidget(self.db_path_label)
button_layout.addStretch()
layout.addLayout(button_layout)
def create_filter_section(self, layout):
"""创建筛选控件区域"""
logger.info("创建筛选控件区域")
# 创建筛选分组框
filter_group = QGroupBox("数据筛选")
filter_layout = QHBoxLayout(filter_group)
# 字段选择标签
filter_layout.addWidget(QLabel("筛选字段:"))
# 字段选择下拉框
self.field_combo = QComboBox()
self.field_combo.setMinimumWidth(150)
filter_layout.addWidget(self.field_combo)
# 筛选条件标签
filter_layout.addWidget(QLabel("筛选条件:"))
# 筛选条件输入框
self.filter_input = QLineEdit()
2025-11-28 20:42:32 +08:00
self.filter_input.setPlaceholderText("输入筛选条件,如:<75 或 name='test' 或 created_at>'2024-01-01'")
2025-11-27 07:54:42 +08:00
self.filter_input.setMinimumWidth(300)
filter_layout.addWidget(self.filter_input)
# 筛选按钮
self.filter_button = QPushButton("筛选")
self.filter_button.clicked.connect(self.apply_filter)
filter_layout.addWidget(self.filter_button)
# 清除筛选按钮
self.clear_filter_button = QPushButton("清除筛选")
self.clear_filter_button.clicked.connect(self.clear_filter)
self.clear_filter_button.setEnabled(False)
filter_layout.addWidget(self.clear_filter_button)
filter_layout.addStretch()
# 初始状态下禁用筛选控件
self.field_combo.setEnabled(False)
self.filter_input.setEnabled(False)
self.filter_button.setEnabled(False)
layout.addWidget(filter_group)
def create_splitter(self, layout):
"""创建分割器界面"""
logger.info("创建分割器界面")
splitter = QSplitter(Qt.Horizontal)
# 左侧:表列表
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.addWidget(QLabel("数据库表列表:"))
self.table_list = QListWidget()
self.table_list.itemClicked.connect(self.on_table_selected)
left_layout.addWidget(self.table_list)
# 右侧:数据表格
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.addWidget(QLabel("表数据:"))
self.data_table = QTableWidget()
self.data_table.setAlternatingRowColors(True)
2025-11-28 22:29:47 +08:00
# 设置表格支持多行内容和可调整列宽
self.data_table.setItemDelegate(MultiLineDelegate(self.data_table))
self.data_table.setWordWrap(True) # 启用自动换行
self.data_table.setTextElideMode(Qt.ElideNone) # 不省略文本
# 设置列头支持拖拽调整大小
header = self.data_table.horizontalHeader()
header.setSectionsMovable(True) # 允许移动列
header.setStretchLastSection(False) # 不自动拉伸最后一列
# 设置行头自动调整高度
self.data_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
# 添加右键菜单支持
self.data_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.data_table.customContextMenuRequested.connect(self.show_table_context_menu)
2025-11-27 07:54:42 +08:00
right_layout.addWidget(self.data_table)
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setSizes([300, 900])
layout.addWidget(splitter)
def create_status_bar(self):
"""创建状态栏"""
logger.info("创建状态栏")
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
def create_menubar(self):
"""创建菜单栏"""
logger.info("创建菜单栏")
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu("文件")
open_action = QAction("打开数据库", self)
open_action.triggered.connect(self.open_database)
file_menu.addAction(open_action)
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
def open_database(self):
"""打开SQLite数据库文件"""
logger.info("打开数据库文件对话框")
# 默认打开product目录下的product.db
default_path = os.path.join('product', 'product.db')
if os.path.exists(default_path):
file_path, _ = QFileDialog.getOpenFileName(
self, "打开SQLite数据库", default_path, "SQLite数据库文件 (*.db *.sqlite *.sqlite3)"
)
else:
file_path, _ = QFileDialog.getOpenFileName(
self, "打开SQLite数据库", "", "SQLite数据库文件 (*.db *.sqlite *.sqlite3)"
)
if file_path:
logger.info(f"打开数据库文件: {file_path}")
self.connect_to_database(file_path)
def connect_to_database(self, file_path):
"""连接到指定的SQLite数据库"""
try:
if self.db_connection:
self.db_connection.close()
self.db_connection = sqlite3.connect(file_path)
logger.info("数据库连接成功")
self.db_path_label.setText(f"数据库: {os.path.basename(file_path)}")
self.status_bar.showMessage(f"已连接到数据库: {os.path.basename(file_path)}")
self.refresh_button.setEnabled(True)
# 加载表列表
self.load_table_list()
except sqlite3.Error as e:
logger.error(f"数据库连接失败: {e}")
QMessageBox.critical(self, "错误", f"无法打开数据库: {e}")
def load_table_list(self):
"""加载数据库表列表"""
if not self.db_connection:
return
try:
cursor = self.db_connection.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = cursor.fetchall()
self.table_list.clear()
for table in tables:
item = QListWidgetItem(table[0])
self.table_list.addItem(item)
logger.info(f"加载了 {len(tables)} 个表")
self.status_bar.showMessage(f"已加载 {len(tables)} 个表")
except sqlite3.Error as e:
logger.error(f"加载表列表失败: {e}")
QMessageBox.critical(self, "错误", f"加载表列表失败: {e}")
def on_table_selected(self, item):
"""当表被选中时加载表数据和字段列表"""
table_name = item.text()
logger.info(f"选中表: {table_name}")
self.current_table = table_name
self.load_table_data(table_name)
self.update_field_combo(table_name)
def load_table_data(self, table_name):
"""加载指定表的数据"""
if not self.db_connection:
return
try:
cursor = self.db_connection.cursor()
# 获取表结构
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
# 获取数据
cursor.execute(f"SELECT * FROM {table_name}")
data = cursor.fetchall()
# 设置表格
self.data_table.setRowCount(len(data))
self.data_table.setColumnCount(len(column_names))
self.data_table.setHorizontalHeaderLabels(column_names)
# 填充数据
for row_idx, row_data in enumerate(data):
for col_idx, cell_data in enumerate(row_data):
2025-11-28 22:29:47 +08:00
# 处理None值和格式化数据
if cell_data is None:
display_text = ""
elif isinstance(cell_data, (int, float)):
# 数字类型保持原样,但转换为字符串
display_text = str(cell_data)
else:
# 文本类型,保留原始格式,包括换行符
display_text = str(cell_data)
item = QTableWidgetItem(display_text)
item.setToolTip(display_text) # 添加悬停提示
2025-11-27 07:54:42 +08:00
self.data_table.setItem(row_idx, col_idx, item)
2025-11-28 22:29:47 +08:00
# 调整列宽 - 使用Interactive模式让用户可以手动调整
header = self.data_table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Interactive)
# 设置初始列宽为内容宽度,但有最大宽度限制
for col in range(len(column_names)):
# 计算该列内容的最大宽度
max_width = 0
for row in range(min(100, len(data))): # 只检查前100行避免性能问题
item = self.data_table.item(row, col)
if item and item.text():
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
max_width = max(max_width, text_width)
# 设置列宽最小100像素最大400像素
column_width = min(max(max_width, 100), 400)
self.data_table.setColumnWidth(col, column_width)
2025-11-27 07:54:42 +08:00
logger.info(f"加载表 {table_name} 数据完成,共 {len(data)}")
self.status_bar.showMessage(f"{table_name}: {len(data)} 行数据")
except sqlite3.Error as e:
logger.error(f"加载表数据失败: {e}")
QMessageBox.critical(self, "错误", f"加载表数据失败: {e}")
def update_field_combo(self, table_name):
"""更新字段选择下拉框"""
if not self.db_connection:
return
try:
cursor = self.db_connection.cursor()
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
# 清空当前字段列表
self.field_combo.clear()
# 添加所有字段到下拉框
for column in columns:
field_name = column[1] # 字段名在第二个位置
self.field_combo.addItem(field_name)
# 启用筛选控件
self.field_combo.setEnabled(True)
self.filter_input.setEnabled(True)
self.filter_button.setEnabled(True)
logger.info(f"更新字段下拉框: {table_name}, 共 {len(columns)} 个字段")
except sqlite3.Error as e:
logger.error(f"获取表字段信息失败: {e}")
QMessageBox.warning(self, "错误", f"获取表字段信息失败: {e}")
def apply_filter(self):
"""应用筛选条件"""
if not self.db_connection or not self.current_table:
return
selected_field = self.field_combo.currentText()
filter_condition = self.filter_input.text().strip()
if not selected_field or not filter_condition:
QMessageBox.warning(self, "警告", "请选择筛选字段并输入筛选条件")
return
try:
cursor = self.db_connection.cursor()
2025-11-28 20:42:32 +08:00
# 检查是否为数值比较(支持 <, >, <=, >=, =, != 操作符)
import re
numeric_pattern = r'^\s*([><]=?|!=|=)\s*([\d.]+)\s*$'
match = re.match(numeric_pattern, filter_condition)
2025-11-27 07:54:42 +08:00
2025-11-28 20:42:32 +08:00
if match:
# 数值比较
operator = match.group(1)
value = match.group(2)
query = f"SELECT * FROM {self.current_table} WHERE {selected_field} {operator} ?"
filter_value = float(value)
else:
# 文本模糊匹配
query = f"SELECT * FROM {self.current_table} WHERE {selected_field} LIKE ?"
filter_value = f"%{filter_condition}%"
2025-11-27 07:54:42 +08:00
# 执行查询
cursor.execute(query, (filter_value,))
data = cursor.fetchall()
# 获取表结构
cursor.execute(f"PRAGMA table_info({self.current_table})")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
# 更新表格显示
self.data_table.setRowCount(len(data))
self.data_table.setColumnCount(len(column_names))
self.data_table.setHorizontalHeaderLabels(column_names)
# 填充筛选后的数据
for row_idx, row_data in enumerate(data):
for col_idx, cell_data in enumerate(row_data):
2025-11-28 22:29:47 +08:00
# 处理None值和格式化数据
if cell_data is None:
display_text = ""
elif isinstance(cell_data, (int, float)):
# 数字类型保持原样,但转换为字符串
display_text = str(cell_data)
else:
# 文本类型,保留原始格式,包括换行符
display_text = str(cell_data)
item = QTableWidgetItem(display_text)
item.setToolTip(display_text) # 添加悬停提示
2025-11-27 07:54:42 +08:00
self.data_table.setItem(row_idx, col_idx, item)
2025-11-28 22:29:47 +08:00
# 调整列宽 - 使用Interactive模式让用户可以手动调整
header = self.data_table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Interactive)
# 设置初始列宽为内容宽度,但有最大宽度限制
for col in range(len(column_names)):
# 计算该列内容的最大宽度
max_width = 0
for row in range(min(100, len(data))): # 只检查前100行避免性能问题
item = self.data_table.item(row, col)
if item and item.text():
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
max_width = max(max_width, text_width)
# 设置列宽最小100像素最大400像素
column_width = min(max(max_width, 100), 400)
self.data_table.setColumnWidth(col, column_width)
2025-11-27 07:54:42 +08:00
# 启用清除筛选按钮
self.clear_filter_button.setEnabled(True)
2025-11-28 20:42:32 +08:00
if match:
logger.info(f"应用数值筛选条件: {selected_field} {operator} {value}, 匹配到 {len(data)} 行数据")
self.status_bar.showMessage(f"筛选结果: {len(data)} 行数据 (条件: {selected_field} {operator} {value})")
else:
logger.info(f"应用文本筛选条件: {selected_field} LIKE '%{filter_condition}%', 匹配到 {len(data)} 行数据")
self.status_bar.showMessage(f"筛选结果: {len(data)} 行数据 (条件: {selected_field} 包含 '{filter_condition}')")
2025-11-27 07:54:42 +08:00
except sqlite3.Error as e:
logger.error(f"筛选数据失败: {e}")
QMessageBox.critical(self, "错误", f"筛选数据失败: {e}")
def clear_filter(self):
"""清除筛选条件,显示所有数据"""
if not self.current_table:
return
try:
# 重新加载完整数据
self.load_table_data(self.current_table)
# 清空筛选条件
self.filter_input.clear()
# 禁用清除筛选按钮
self.clear_filter_button.setEnabled(False)
logger.info("清除筛选条件,显示所有数据")
self.status_bar.showMessage("已清除筛选条件,显示所有数据")
except Exception as e:
logger.error(f"清除筛选失败: {e}")
QMessageBox.critical(self, "错误", f"清除筛选失败: {e}")
def refresh_data(self):
"""刷新当前数据"""
logger.info("刷新数据")
if self.current_table:
self.load_table_data(self.current_table)
else:
self.load_table_list()
2025-11-28 22:29:47 +08:00
def show_table_context_menu(self, position):
"""显示表格右键菜单"""
menu = QMenu()
# 添加菜单项
auto_resize_action = menu.addAction("自动调整列宽")
auto_resize_rows_action = menu.addAction("自动调整行高")
copy_action = menu.addAction("复制选中内容")
# 显示菜单
action = menu.exec(self.data_table.mapToGlobal(position))
if action == auto_resize_action:
self.auto_resize_columns()
elif action == auto_resize_rows_action:
self.auto_resize_rows()
elif action == copy_action:
self.copy_selected_content()
def auto_resize_columns(self):
"""自动调整所有列宽"""
logger.info("自动调整列宽")
# 遍历所有列
for col in range(self.data_table.columnCount()):
# 计算该列内容的最大宽度
max_width = 0
for row in range(min(100, self.data_table.rowCount())): # 只检查前100行
item = self.data_table.item(row, col)
if item and item.text():
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
max_width = max(max_width, text_width)
# 设置列宽最小100像素最大500像素
column_width = min(max(max_width, 100), 500)
self.data_table.setColumnWidth(col, column_width)
self.status_bar.showMessage("已自动调整列宽")
def auto_resize_rows(self):
"""自动调整所有行高"""
logger.info("自动调整行高")
# 触发重新计算行高
self.data_table.resizeRowsToContents()
self.status_bar.showMessage("已自动调整行高")
def copy_selected_content(self):
"""复制选中的内容"""
selected_items = self.data_table.selectedItems()
if not selected_items:
return
# 按行列组织数据
rows = {}
for item in selected_items:
row = item.row()
col = item.column()
if row not in rows:
rows[row] = {}
rows[row][col] = item.text()
# 构建复制的文本
text_lines = []
for row in sorted(rows.keys()):
row_data = []
for col in sorted(rows[row].keys()):
row_data.append(rows[row][col])
text_lines.append('\t'.join(row_data))
# 复制到剪贴板
clipboard = QApplication.clipboard()
clipboard.setText('\n'.join(text_lines))
self.status_bar.showMessage(f"已复制 {len(selected_items)} 个单元格的内容")
2025-11-27 07:54:42 +08:00
def closeEvent(self, event):
"""关闭事件处理"""
logger.info("关闭应用程序")
if self.db_connection:
self.db_connection.close()
event.accept()
def main():
"""主函数"""
logger.info("启动SQLite数据库查看器")
# 配置日志
logger.add("sqlite_viewer.log", rotation="10 MB", level="INFO")
app = QApplication(sys.argv)
# 设置应用程序信息
app.setApplicationName("SQLite数据库查看器")
app.setApplicationVersion("1.0.0")
viewer = SQLiteViewer()
viewer.show()
logger.info("应用程序启动完成")
sys.exit(app.exec())
if __name__ == "__main__":
main()