diff --git a/lib/db_columns_def.py b/lib/db_columns_def.py index 5bbd8b6..f6db594 100644 --- a/lib/db_columns_def.py +++ b/lib/db_columns_def.py @@ -9,7 +9,9 @@ columns_d = { "opt": Column("opt", DataType.TEXT), "url": Column("url", DataType.BLOB), "notes": Column("notes", DataType.BLOB), - "path": Column("path", DataType.BLOB, nullable=False), + "uuid": Column("uuid", DataType.TEXT, nullable=False), + "filepath": Column("filepath", DataType.BLOB, nullable=False), + "path": Column("path", DataType.BLOB), } all_columns = [ @@ -20,6 +22,8 @@ all_columns = [ columns_d["opt"], columns_d["url"], columns_d["notes"], + columns_d["uuid"], + columns_d["filepath"], columns_d["path"], ] @@ -29,3 +33,9 @@ query_columns = [ columns_d["username"], columns_d["url"], ] + +# 从数据库中读取 UUID 和 文件路径分析相似度 +sim_columns = [ + columns_d["uuid"], + columns_d["filepath"], +] diff --git a/lib/kps_operations.py b/lib/kps_operations.py index 65a5ca3..3ccdbce 100644 --- a/lib/kps_operations.py +++ b/lib/kps_operations.py @@ -41,7 +41,9 @@ def read_kps_to_db(kps_file: str | PathLike[str], password: str, extract_otp(entry.otp), blob_fy(trim_str(entry.url)), blob_fy(entry.notes), - blob_fy("::".join([kps_file] + entry.path[:-1])), + str(entry.uuid), + blob_fy(kps_file), + blob_fy("::".join(entry.path[:-1])), ]) sqh.insert_into(table_name, all_columns[1:], values) diff --git a/src/gbx_kps_login.py b/src/gbx_kps_login.py index b1c8c8e..52d5fc2 100644 --- a/src/gbx_kps_login.py +++ b/src/gbx_kps_login.py @@ -8,7 +8,13 @@ from lib.kps_operations import read_kps_to_db class GbxKpsLogin(QtWidgets.QGroupBox): - def __init__(self, path: str, sqh: Sqlite3Worker, config: dict, parent=None): + def __init__( + self, + path: str, + sqh: Sqlite3Worker, + config: dict, + parent: QtWidgets.QWidget = None + ): super().__init__(parent) self.sqh = sqh self.config = config @@ -75,10 +81,12 @@ class GbxKpsLogin(QtWidgets.QGroupBox): def on_pbn_load_clicked(self): try: - read_kps_to_db(kps_file=self.lne_path.text(), - password=self.lne_password.text(), - table_name=self.config["table_name"], - sqh=self.sqh) + read_kps_to_db( + kps_file=self.lne_path.text(), + password=self.lne_password.text(), + table_name=self.config["table_name"], + sqh=self.sqh + ) except CredentialsError: QtWidgets.QMessageBox.critical(self, "密码错误", f"{self.lne_path.text()}\n密码错误") diff --git a/src/mw_kps_unifier.py b/src/mw_kps_unifier.py index d754c2a..9e84258 100644 --- a/src/mw_kps_unifier.py +++ b/src/mw_kps_unifier.py @@ -3,6 +3,8 @@ from PySide6 import QtWidgets, QtCore, QtGui from .page_load import PageLoad from .page_query import PageQuery +from .page_similar import PageSimilar + from .cmbx_styles import StyleComboBox from lib.Sqlite3Helper import Sqlite3Worker from lib.db_columns_def import all_columns @@ -10,7 +12,13 @@ from lib.config_utils import write_config class UiKpsUnifier(object): - def __init__(self, default_db_path: str, config: dict, sqh: Sqlite3Worker, window: QtWidgets.QMainWindow): + def __init__( + self, + default_db_path: str, + config: dict, + sqh: Sqlite3Worker, + window: QtWidgets.QMainWindow + ): window.setWindowTitle('KeePassXC 多合一') self.cw = QtWidgets.QWidget(window) self.vly_m = QtWidgets.QVBoxLayout() @@ -26,7 +34,8 @@ class UiKpsUnifier(object): self.menu_view = self.menu_bar.addMenu("视图") self.act_load = QtGui.QAction("加载", self.cw) self.act_query = QtGui.QAction("查询", self.cw) - self.menu_view.addActions([self.act_load, self.act_query]) + self.act_similar = QtGui.QAction("相似度", self.cw) + self.menu_view.addActions([self.act_load, self.act_query, self.act_similar]) self.menu_help = self.menu_bar.addMenu("帮助") self.act_about = QtGui.QAction("关于", self.cw) @@ -51,6 +60,8 @@ class UiKpsUnifier(object): self.sw_m.addWidget(self.page_load) self.page_query = PageQuery(sqh, config, self.cw) self.sw_m.addWidget(self.page_query) + self.page_similar = PageSimilar(sqh, config, self.cw) + self.sw_m.addWidget(self.page_similar) def update_sqh(self, sqh: Sqlite3Worker): self.page_load.update_sqh(sqh) @@ -71,6 +82,8 @@ class KpsUnifier(QtWidgets.QMainWindow): self.ui.act_open.triggered.connect(self.on_act_open_triggered) self.ui.act_load.triggered.connect(self.on_act_load_triggered) self.ui.act_query.triggered.connect(self.on_act_query_triggered) + self.ui.act_similar.triggered.connect(self.on_act_similar_triggered) + self.ui.act_about.triggered.connect(self.on_act_about_triggered) self.ui.act_about_qt.triggered.connect(self.on_act_about_qt_triggered) @@ -114,6 +127,9 @@ class KpsUnifier(QtWidgets.QMainWindow): def on_act_query_triggered(self): self.ui.sw_m.setCurrentIndex(1) + def on_act_similar_triggered(self): + self.ui.sw_m.setCurrentIndex(2) + def on_act_about_triggered(self): QtWidgets.QMessageBox.about( self, diff --git a/src/page_load.py b/src/page_load.py index 2dc6e91..207ad3b 100644 --- a/src/page_load.py +++ b/src/page_load.py @@ -1,7 +1,6 @@ # coding: utf8 import sqlite3 from pathlib import Path - from PySide6 import QtWidgets from .gbx_kps_login import GbxKpsLogin @@ -10,7 +9,12 @@ from lib.Sqlite3Helper import Sqlite3Worker class WgLoadKps(QtWidgets.QWidget): - def __init__(self, sqh: Sqlite3Worker, config: dict, parent=None): + def __init__( + self, + sqh: Sqlite3Worker, + config: dict, + parent: QtWidgets.QWidget = None + ): super().__init__(parent) self.sqh = sqh self.config = config @@ -55,7 +59,12 @@ class WgLoadKps(QtWidgets.QWidget): class PageLoad(QtWidgets.QWidget): - def __init__(self, sqh: Sqlite3Worker, config: dict, parent=None): + def __init__( + self, + sqh: Sqlite3Worker, + config: dict, + parent: QtWidgets.QWidget = None + ): super().__init__(parent) self.sqh = sqh self.config = config diff --git a/src/page_similar.py b/src/page_similar.py new file mode 100644 index 0000000..d088f0e --- /dev/null +++ b/src/page_similar.py @@ -0,0 +1,92 @@ +# coding: utf8 +from itertools import combinations +from uuid import UUID +from PySide6 import QtWidgets, QtCore +from PySide6.QtCore import QAbstractTableModel + +from lib.Sqlite3Helper import Sqlite3Worker +from lib.db_columns_def import sim_columns + + +class SimilarDataModel(QAbstractTableModel): + def __init__(self, similar_data: list[tuple], parent=None): + super().__init__(parent) + self.similar_data = similar_data + self.headers = ["文件1", "文件2", "相似度"] + + def rowCount(self, parent: QtCore.QModelIndex = ...): + return len(self.similar_data) + + def columnCount(self, parent: QtCore.QModelIndex = ...): + return len(self.headers) + + def data(self, index: QtCore.QModelIndex, role: int = ...): + if role == QtCore.Qt.ItemDataRole.DisplayRole: + return self.similar_data[index.row()][index.column()] + if role == QtCore.Qt.ItemDataRole.TextAlignmentRole: + if index.column() == 2: + return QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter + + def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...): + if orientation == QtCore.Qt.Orientation.Horizontal: + if role == QtCore.Qt.ItemDataRole.DisplayRole: + return self.headers[section] + + +class PageSimilar(QtWidgets.QWidget): + def __init__( + self, + sqh: Sqlite3Worker, + config: dict, + parent: QtWidgets.QWidget = None + ): + super().__init__(parent) + self.sqh = sqh + self.config = config + + self.hly_m = QtWidgets.QHBoxLayout() + self.setLayout(self.hly_m) + + self.vly_left = QtWidgets.QVBoxLayout() + self.hly_m.addLayout(self.vly_left) + + self.pbn_read_db = QtWidgets.QPushButton("读取数据库", self) + self.pbn_read_db.setMinimumWidth(config["button_min_width"]) + self.vly_left.addWidget(self.pbn_read_db) + + self.vly_left.addStretch(1) + + self.tbv_m = QtWidgets.QTableView(self) + self.hly_m.addWidget(self.tbv_m) + + self.pbn_read_db.clicked.connect(self.on_pbn_read_db_clicked) + + def on_pbn_read_db_clicked(self): + _, results = self.sqh.select(self.config["table_name"], sim_columns) + file_uuids: dict[str, list[UUID]] = {} + for u, filepath in results: + filepath = filepath.decode("utf8") + if filepath not in file_uuids: + file_uuids[filepath] = [] + file_uuids[filepath].append(u) + + files = file_uuids.keys() + if len(files) < 2: + QtWidgets.QMessageBox.warning(self, "警告", "数据库中存在的文件数少于 2,无法检查相似度。") + return + + similar_data: list[tuple] = [] + + comb = combinations(files, 2) + for i, j in list(comb): + uuids_i = file_uuids[i] + uuids_j = file_uuids[j] + len_i = len(uuids_i) + len_j = len(uuids_j) + + set_inter = set(uuids_i).intersection(uuids_j) + sim = (len(set_inter) * 2) / (len_i + len_j) + similar_data.append((i, j, f"{sim * 100:.2f}")) + + model = SimilarDataModel(similar_data, self) + self.tbv_m.setModel(model)