dbViewer - SQLite数据库检索查看器 - 将书籍切片检索的探讨

使用方向:

  • 主要针对多个表(书籍)进行多元、同步、非线性联合检索,举例:使用中医v7库检索腰酸,可同时获得中药学、方剂学、针灸学相关适应症作为治疗参考(亦可独立使用进行有针对性的检索),后期版本将会引入中西医结合及西医书籍同时检索参考
  • 无大语言模型,搜索关键词需要了解书籍相关专业内容,例如,在检索医学书籍时需使用咽痛描写症状,而不是喉咙疼、嗓子疼,用心悸描述心慌、心律不齐,用腹泻描述拉肚子、拉稀是不同的。需要有一定的专业知识或者叫做行业黑话、密语
  • 可供下载的Sqlite数据库:中医v7版(共7本含本草纲目、中药学、方剂学、方剂汤头歌、中医内科学、针灸治疗学、经络腧穴学);药品说明书(完整版及Lite版);外贸商家黄页;商检HS编码;中国500强2025
  • 数据库结构特点:每一个条目具有相同属性,例如方剂学中有方剂名称、来源、药物组成、用量、适应症、机理、医案等等,根据属性特点对书籍进行切片分段
  • 与直接检索书籍的区别:在书籍阅读器中检索为线性检索,即按书籍章节顺序从头到尾列出关键词所在条目,举例:在本草纲目中查找药品甘草,从头到尾约数百条,用户可能需要查找主题条目为甘草而非与甘草有关的方剂或组合。数据库检索的优势是可以单选药物名称列,快速定位到需要的条目
  • 经常应用举例:将大夫开方中的两到三味用量较大或排名靠前的药名称进行检索了解是否来自经方古方,了解学习功效;输入医学专用症状名词来检索适用方剂、中药及针灸治疗方法
  • 欢迎各位老铁参与数据库制作和分享,提出使用意见、建议。书籍数据库的制作可以通过豆包分析生成csv或excel表格,然后转换为sqlite数据库,或者通过转换工具如pandoc将书籍转换为html格式,然后用python读取生成excel,细节格式可在excel中调整。注意:数据库、表名或列名称中不可以包含+/-符号,多sheet表excel转换sqlite数据库时sheet名称对应表名
  • 不联网、纯绿色,无需登陆注册,完全本地使用

动画演示

1、基本操作演示
基本操作演示

2、按症状分别搜索“腰酸”、“心悸 汗”、“心悸 -汗”
心悸无汗
心悸有汗

3、按药物名称检索 "天麻 钩藤"、"桂枝 芍药"
腰酸

4、按药物名称症状或人体部位检索 “天麻 头痛”、"桂枝 肌肉"

功能:

基本操作:

  • 打开未加密数据库(SQLite/DB): 点击"导入"按钮或从文件菜单选择
  • 选择搜索列: 在搜索前选择要在哪些列中搜索(缺省为全选搜索)
  • 执行搜索: 在搜索框中输入内容并点击搜索或按回车
  • 查看详情: 双击表格中的行查看详细信息

搜索语法:

  • 空格或+: 表示"与"关系(同时包含多个关键字)
  • -关键字: 表示排除包含该关键字的记录
  • 示例1: 苹果 -手机 +电脑 搜索包含"苹果"和"电脑"但不包含"手机"的记录
  • 示例2:心悸 汗,表示心悸有汗或无汗;心悸 -汗,表示心悸与汗无关症状

高级功能:

  • 分页浏览: 使用底部分页控件浏览大量数据
  • 自定义显示: 在"查看"菜单中设置详情窗口的字体和样式
  • 历史记录: 自动保存最近打开的数据库文件
  • 列宽调整: 可以手动调整表格列宽,调整后的列宽会保持
  • 详情窗口支持字体、字号及行间距设置,支持多开,复制即为html格式

关于闪退
以下情况可能引起程序闪退

  • 点击检索时有录屏等影响内存的操作
  • 内存小于4GB检索较大如10万条以上的库时
  • 存取权限不够或存储空间不足
  • 连续点击检索或回车

操作步骤1
操作步骤2
穴位查找

下载地址

v2.15 x64便携版 - https://wwbur.lanzoul.com/iWhyM3gqp69g
v1.16 x86便携版 - https://wwbur.lanzoul.com/i0BO93gbvypg

常用数据库

中医V7.db
https://wwbur.lanzoul.com/iSIRt3g7yiih 密码:6eiv
https://www.doubao.com/drive/s/66a13ccbf93830a2
包含:本草纲目(全四册) (李时珍) 1645条、中药学(本科教材 第十版)650条、汤头歌诀白话解 350条、方剂学(本科教材 第十版)227条、中医内科学(第十版)54条、针灸治疗学(第十版)121条、经络腧穴学(第十版)347条

药品说明书Lite - (精简版22583条 55MB,去掉重复内容,例如相同名称及用途,仅包装或品牌不同等等)
https://wwbur.lanzoul.com/iiRB53fsc76h
https://www.doubao.com/drive/s/b5efb26316fdaa4d

药品说明书 -(完整版125688条 448MB)
https://wwbur.lanzoul.com/iEZCe3fsc3vi
https://www.doubao.com/drive/s/40ff7e611abb9e11

外贸商家黄页 - (7W+条 买家数据库)
https://wwbur.lanzoul.com/iA4dw3gbnrvc

商检HS代码 - (N年以前的数据,仅供参考)
https://wwbur.lanzoul.com/iWHUc3fsc20b 密码:d7gt

更新网盘:
https://www.doubao.com/drive/s/84182895c7236012
https://www.123912.com/s/xntA-xcfbv

运行环境

  • x64版在x64Win10/11下测试正常
  • x86版在在x64及x86的Win7/8/10/11运行正常,检索速度略慢
  • 360会对Python打包程序普遍性误报

源代码1.00

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import sqlite3
import os
from typing import List, Dict, Any

class DatabaseViewer:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("dbViewer - SQLite数据库检索查看器")
        self.root.geometry("1200x800")
        
        # 设置Win11风格的主题
        self.set_win11_style()
        
        self.db_connection = None
        self.current_db_path = None
        self.selected_columns = []  # 存储用户选择的列
        self.current_page = 1  # 当前页码
        self.page_size = 100   # 每页显示的数据量
        self.total_pages = 1   # 总页数
        self.current_table = ""  # 当前显示的表名
        self.current_columns = []  # 当前表的列名
        
        self.create_widgets()
        
    def set_win11_style(self):
        """设置Win11风格的界面样式"""
        style = ttk.Style()
        style.theme_use('vista')  # 使用vista主题,最接近Win11风格
        
        # 配置样式
        style.configure("Treeview", 
                       background="#f5f5f5",
                       foreground="black",
                       rowheight=25,
                       fieldbackground="#f5f5f5",
                       font=("Segoe UI", 10))
        
        style.configure("Treeview.Heading",
                       font=("Segoe UI", 10, "bold"),
                       background="#0078d4",
                       foreground="white")
        
        style.configure("TButton",
                       font=("Segoe UI", 10),
                       padding=6)
        
        style.configure("TLabel",
                       font=("Segoe UI", 10))
        
        style.configure("TEntry",
                       font=("Segoe UI", 10))
        
        # 设置选中颜色
        style.map("Treeview",
                 background=[('selected', '#0078d4')],
                 foreground=[('selected', 'white')])
    
    def create_widgets(self):
        """创建界面组件"""
        # 主框架
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 顶部控制面板
        control_frame = ttk.Frame(main_frame)
        control_frame.pack(fill=tk.X, pady=(0, 10))
        
        # 数据库文件选择
        ttk.Button(control_frame, text="导入数据库文件", 
                  command=self.load_database).pack(side=tk.LEFT, padx=(0, 10))
        
        self.db_label = ttk.Label(control_frame, text="未选择数据库文件")
        self.db_label.pack(side=tk.LEFT, padx=(0, 10))
        
        # 列选择按钮
        ttk.Button(control_frame, text="选择搜索列", 
                  command=self.select_columns).pack(side=tk.LEFT, padx=(0, 10))
        
        # 搜索框
        search_frame = ttk.Frame(control_frame)
        search_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True)
        
        ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=30)
        self.search_entry.pack(side=tk.LEFT, padx=(5, 5), fill=tk.X, expand=True)
        self.search_entry.bind('<Return>', lambda e: self.perform_search())
        
        ttk.Button(search_frame, text="搜索", 
                  command=self.perform_search).pack(side=tk.LEFT)
        
        # 结果显示区域
        result_frame = ttk.Frame(main_frame)
        result_frame.pack(fill=tk.BOTH, expand=True)
        
        # 创建Treeview用于显示结果
        self.result_tree = ttk.Treeview(result_frame, show='headings')
        
        # 添加滚动条
        scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
        self.result_tree.configure(yscrollcommand=scrollbar.set)
        
        self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 绑定双击事件
        self.result_tree.bind('<Double-1>', self.on_item_double_click)
        
        # 分页控制
        pagination_frame = ttk.Frame(main_frame)
        pagination_frame.pack(fill=tk.X, pady=(10, 0))
        
        ttk.Button(pagination_frame, text="首页", 
                  command=self.first_page).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(pagination_frame, text="上一页", 
                  command=self.previous_page).pack(side=tk.LEFT, padx=(0, 5))
        
        self.page_label = ttk.Label(pagination_frame, text="第 1 页 / 共 1 页")
        self.page_label.pack(side=tk.LEFT, padx=10)
        
        ttk.Button(pagination_frame, text="下一页", 
                  command=self.next_page).pack(side=tk.LEFT, padx=(5, 5))
        ttk.Button(pagination_frame, text="末页", 
                  command=self.last_page).pack(side=tk.LEFT, padx=(0, 5))
        
        # 每页显示数量选择
        ttk.Label(pagination_frame, text="每页显示:").pack(side=tk.LEFT, padx=(20, 5))
        self.page_size_var = tk.StringVar(value="100")
        page_size_combo = ttk.Combobox(pagination_frame, textvariable=self.page_size_var, 
                                      values=["50", "100", "200", "500"], width=8)
        page_size_combo.pack(side=tk.LEFT, padx=(0, 10))
        page_size_combo.bind('<<ComboboxSelected>>', self.change_page_size)
        
        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, pady=(10, 0))
    
    def load_database(self):
        """加载数据库文件"""
        file_path = filedialog.askopenfilename(
            title="选择数据库文件",
            filetypes=[("SQLite数据库文件", "*.db"), ("所有文件", "*.*")]
        )
        
        if file_path:
            try:
                # 关闭之前的连接
                if self.db_connection:
                    self.db_connection.close()
                
                # 连接新数据库
                self.db_connection = sqlite3.connect(file_path)
                self.current_db_path = file_path
                self.db_label.config(text=os.path.basename(file_path))
                self.status_var.set(f"已加载数据库: {os.path.basename(file_path)}")
                
                # 清空之前的搜索结果
                self.clear_results()
                
                # 重置分页
                self.current_page = 1
                self.total_pages = 1
                self.update_page_label()
                
            except sqlite3.Error as e:
                messagebox.showerror("错误", f"无法打开数据库文件: {str(e)}")
    
    def get_all_tables(self) -> List[str]:
        """获取所有表名"""
        if not self.db_connection:
            return []
        
        cursor = self.db_connection.cursor()
        cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
        return [row[0] for row in cursor.fetchall()]
    
    def get_table_schema(self, table_name: str) -> List[Dict[str, Any]]:
        """获取表结构"""
        if not self.db_connection:
            return []
        
        cursor = self.db_connection.cursor()
        cursor.execute(f"PRAGMA table_info({table_name})")
        
        columns = []
        for row in cursor.fetchall():
            columns.append({
                'cid': row[0],
                'name': row[1],
                'type': row[2],
                'notnull': row[3],
                'default': row[4],
                'pk': row[5]
            })
        return columns
    
    def select_columns(self):
        """打开列选择对话框"""
        if not self.db_connection:
            messagebox.showwarning("警告", "请先导入数据库文件")
            return
        
        # 创建列选择窗口
        column_window = tk.Toplevel(self.root)
        column_window.title("选择搜索列")
        column_window.geometry("500x400")
        column_window.transient(self.root)
        column_window.grab_set()
        
        # 创建框架
        main_frame = ttk.Frame(column_window)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 表选择
        table_frame = ttk.Frame(main_frame)
        table_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Label(table_frame, text="选择表:").pack(side=tk.LEFT)
        
        self.table_var = tk.StringVar()
        tables = self.get_all_tables()
        table_combo = ttk.Combobox(table_frame, textvariable=self.table_var, values=tables)
        table_combo.pack(side=tk.LEFT, padx=(5, 0), fill=tk.X, expand=True)
        
        if tables:
            table_combo.set(tables[0])
            self.table_var.set(tables[0])
        
        # 列选择列表
        columns_frame = ttk.Frame(main_frame)
        columns_frame.pack(fill=tk.BOTH, expand=True)
        
        # 列列表
        self.columns_listbox = tk.Listbox(columns_frame, selectmode=tk.MULTIPLE)
        scrollbar = tk.Scrollbar(columns_frame, orient=tk.VERTICAL, command=self.columns_listbox.yview)
        self.columns_listbox.configure(yscrollcommand=scrollbar.set)
        
        self.columns_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 按钮框架
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=(10, 0))
        
        ttk.Button(button_frame, text="全选", 
                  command=self.select_all_columns).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(button_frame, text="全不选", 
                  command=self.deselect_all_columns).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(button_frame, text="确定", 
                  command=lambda: self.apply_column_selection(column_window)).pack(side=tk.RIGHT)
        
        # 绑定表选择事件
        table_combo.bind('<<ComboboxSelected>>', self.on_table_selected)
        
        # 初始加载列
        if tables:
            self.load_columns_for_table(tables[0])
    
    def on_table_selected(self, event):
        """当表被选择时加载对应的列"""
        table_name = self.table_var.get()
        self.load_columns_for_table(table_name)
    
    def load_columns_for_table(self, table_name):
        """加载指定表的列"""
        if not table_name:
            return
        
        columns = self.get_table_schema(table_name)
        column_names = [col['name'] for col in columns]
        
        # 更新列列表
        self.columns_listbox.delete(0, tk.END)
        for col in column_names:
            self.columns_listbox.insert(tk.END, col)
    
    def select_all_columns(self):
        """选择所有列"""
        self.columns_listbox.select_set(0, tk.END)
    
    def deselect_all_columns(self):
        """取消选择所有列"""
        self.columns_listbox.select_clear(0, tk.END)
    
    def apply_column_selection(self, window):
        """应用列选择"""
        selected_indices = self.columns_listbox.curselection()
        selected_columns = [self.columns_listbox.get(i) for i in selected_indices]
        table_name = self.table_var.get()
        
        # 保存选择的列和表
        self.selected_columns = [(table_name, col) for col in selected_columns]
        self.current_table = table_name
        
        # 更新状态
        if selected_columns:
            self.status_var.set(f"已选择 {len(selected_columns)} 个列进行搜索")
        else:
            self.status_var.set("未选择任何列,将搜索所有列")
        
        window.destroy()
    
    def perform_search(self):
        """执行搜索"""
        search_text = self.search_var.get().strip()
        if not search_text:
            messagebox.showwarning("警告", "请输入搜索内容")
            return
        
        if not self.db_connection:
            messagebox.showwarning("警告", "请先导入数据库文件")
            return
        
        self.clear_results()
        self.status_var.set("正在搜索...")
        self.root.update()
        
        try:
            # 如果没有选择表,使用第一个表
            if not self.current_table:
                tables = self.get_all_tables()
                if not tables:
                    messagebox.showwarning("警告", "数据库中没有表")
                    return
                self.current_table = tables[0]
            
            # 如果没有选择列,使用所有列
            if not self.selected_columns:
                columns = self.get_table_schema(self.current_table)
                self.selected_columns = [(self.current_table, col['name']) for col in columns]
            
            # 获取表的所有列信息
            all_columns = self.get_table_schema(self.current_table)
            self.current_columns = [col['name'] for col in all_columns]
            
            # 设置Treeview列
            self.setup_treeview_columns()
            
            # 计算总记录数
            total_count = self.get_total_count(search_text)
            if total_count == 0:
                self.status_var.set("未找到匹配的记录")
                return
            
            # 计算总页数
            self.total_pages = (total_count + self.page_size - 1) // self.page_size
            if self.current_page > self.total_pages:
                self.current_page = self.total_pages
            
            # 加载当前页数据
            self.load_page_data(search_text)
            
            # 更新分页信息
            self.update_page_label()
            
        except sqlite3.Error as e:
            messagebox.showerror("错误", f"搜索过程中发生错误: {str(e)}")
            self.status_var.set("搜索出错")
    
    def get_total_count(self, search_text):
        """获取匹配记录总数"""
        cursor = self.db_connection.cursor()
        
        # 构建查询条件
        conditions = []
        params = []
        
        for table, column in self.selected_columns:
            conditions.append(f"{column} LIKE ?")
            params.append(f'%{search_text}%')
        
        where_clause = " OR ".join(conditions)
        query = f"SELECT COUNT(*) FROM {self.current_table} WHERE {where_clause}"
        
        cursor.execute(query, params)
        return cursor.fetchone()[0]
    
    def load_page_data(self, search_text):
        """加载当前页数据"""
        cursor = self.db_connection.cursor()
        
        # 构建查询条件
        conditions = []
        params = []
        
        for table, column in self.selected_columns:
            conditions.append(f"{column} LIKE ?")
            params.append(f'%{search_text}%')
        
        where_clause = " OR ".join(conditions)
        
        # 计算偏移量
        offset = (self.current_page - 1) * self.page_size
        
        # 构建查询
        columns_str = ", ".join(self.current_columns)
        query = f"SELECT {columns_str} FROM {self.current_table} WHERE {where_clause} LIMIT ? OFFSET ?"
        params.extend([self.page_size, offset])
        
        cursor.execute(query, params)
        rows = cursor.fetchall()
        
        # 插入数据到Treeview
        for row in rows:
            self.result_tree.insert('', tk.END, values=row)
        
        self.status_var.set(f"已加载第 {self.current_page} 页,共 {len(rows)} 条记录")
    
    def setup_treeview_columns(self):
        """设置Treeview列"""
        # 清除现有列
        for col in self.result_tree['columns']:
            self.result_tree.heading(col, text="")
            self.result_tree.column(col, width=0)
        
        # 设置新列
        self.result_tree['columns'] = self.current_columns
        
        # 设置列标题和宽度
        for col in self.current_columns:
            self.result_tree.heading(col, text=col)
            # 根据列名长度设置初始宽度
            width = min(200, max(80, len(col) * 10))
            self.result_tree.column(col, width=width)
    
    def clear_results(self):
        """清空搜索结果"""
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)
    
    def on_item_double_click(self, event):
        """双击列表项显示详细信息"""
        selection = self.result_tree.selection()
        if not selection:
            return
        
        item = self.result_tree.item(selection[0])
        values = item['values']
        
        # 创建详情窗口
        detail_window = tk.Toplevel(self.root)
        detail_window.title(f"详细信息 - {self.current_table}")
        detail_window.geometry("600x400")
        detail_window.transient(self.root)
        detail_window.grab_set()
        
        # 创建文本框显示详细信息
        text_frame = ttk.Frame(detail_window)
        text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        text_widget = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD, font=("Consolas", 10))
        text_widget.pack(fill=tk.BOTH, expand=True)
        
        # 格式化输出详细信息
        detail_text = f"表名: {self.current_table}\n"
        detail_text += "=" * 50 + "\n\n"
        
        for i, (col_name, value) in enumerate(zip(self.current_columns, values)):
            detail_text += f"{col_name}: {value}\n"
        
        text_widget.insert(tk.END, detail_text)
        text_widget.config(state=tk.DISABLED)
        
        # 添加关闭按钮
        button_frame = ttk.Frame(detail_window)
        button_frame.pack(fill=tk.X, padx=10, pady=10)
        
        ttk.Button(button_frame, text="关闭", 
                  command=detail_window.destroy).pack(side=tk.RIGHT)
    
    # 分页控制方法
    def first_page(self):
        """跳转到首页"""
        if self.current_page != 1:
            self.current_page = 1
            self.refresh_current_page()
    
    def previous_page(self):
        """跳转到上一页"""
        if self.current_page > 1:
            self.current_page -= 1
            self.refresh_current_page()
    
    def next_page(self):
        """跳转到下一页"""
        if self.current_page < self.total_pages:
            self.current_page += 1
            self.refresh_current_page()
    
    def last_page(self):
        """跳转到末页"""
        if self.current_page != self.total_pages:
            self.current_page = self.total_pages
            self.refresh_current_page()
    
    def change_page_size(self, event=None):
        """改变每页显示数量"""
        try:
            new_size = int(self.page_size_var.get())
            if new_size > 0:
                self.page_size = new_size
                self.current_page = 1
                # 如果当前有搜索条件,重新执行搜索
                search_text = self.search_var.get().strip()
                if search_text and self.db_connection:
                    self.perform_search()
        except ValueError:
            pass
    
    def refresh_current_page(self):
        """刷新当前页数据"""
        search_text = self.search_var.get().strip()
        if search_text and self.db_connection:
            self.clear_results()
            self.load_page_data(search_text)
            self.update_page_label()
    
    def update_page_label(self):
        """更新分页标签"""
        self.page_label.config(text=f"第 {self.current_page} 页 / 共 {self.total_pages} 页")
    
    def run(self):
        """运行应用程序"""
        self.root.mainloop()
        
        # 程序退出时关闭数据库连接
        if self.db_connection:
            self.db_connection.close()

if __name__ == "__main__":
    app = DatabaseViewer()
    app.run()

主要新增功能说明:

  1. 列选择功能

    • 添加了"选择搜索列"按钮,点击后弹出列选择对话框
    • 可以选择表和表中的特定列进行搜索
    • 支持全选和全不选功能
  2. 表格形式展示

    • 搜索结果现在以表格形式展示,显示所有字段内容
    • 列宽根据列名长度自动调整
  3. 分页懒加载

    • 添加了分页控制组件(首页、上一页、下一页、末页)
    • 可以设置每页显示的数据量(50/100/200/500)
    • 显示当前页码和总页数
  4. 性能优化

    • 使用LIMIT和OFFSET实现分页查询,避免一次性加载大量数据
    • 只加载当前页的数据,提高响应速度
  5. 用户体验改进

    • 状态栏显示当前加载的记录数
    • 双击表格行可以查看该行的详细信息
    • 支持通过回车键触发搜索

这些改进使得数据库查看器更加实用,特别是对于大型数据库,可以有效提高搜索和浏览性能。

posted on 2026-01-22 10:26  igaoyuan  阅读(5)  评论(0)    收藏  举报

导航