Files
tophux_scrape/product/web_sqlite_viewer.py

741 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
现代化Web SQLite数据库查看器
基于Flask框架提供自然的内容显示和筛选功能
"""
from flask import Flask, render_template, jsonify, request, send_from_directory
import sqlite3
import os
import json
from datetime import datetime
from loguru import logger
app = Flask(__name__)
# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), 'product.db')
class SQLiteWebViewer:
def __init__(self, db_path):
self.db_path = db_path
logger.info(f"初始化Web SQLite查看器数据库路径: {db_path}")
def get_tables(self):
"""获取所有表名"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
tables = [row[0] for row in cursor.fetchall()]
conn.close()
logger.info(f"获取到 {len(tables)} 个表")
return tables
except Exception as e:
logger.error(f"获取表列表失败: {e}")
return []
def get_table_structure(self, table_name):
"""获取表结构"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
conn.close()
structure = []
for col in columns:
structure.append({
'cid': col[0],
'name': col[1],
'type': col[2],
'notnull': col[3],
'default': col[4],
'pk': col[5]
})
logger.info(f"获取表 {table_name} 结构,共 {len(structure)} 个字段")
return structure
except Exception as e:
logger.error(f"获取表结构失败: {e}")
return []
def get_table_data(self, table_name, page=1, per_page=50, search_field=None, search_value=None):
"""获取表数据,支持分页和搜索"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 获取总记录数
if search_field and search_value:
count_query = f"SELECT COUNT(*) FROM {table_name} WHERE {search_field} LIKE ?"
cursor.execute(count_query, (f'%{search_value}%',))
else:
count_query = f"SELECT COUNT(*) FROM {table_name}"
cursor.execute(count_query)
total_count = cursor.fetchone()[0]
# 获取分页数据
offset = (page - 1) * per_page
if search_field and search_value:
data_query = f"SELECT * FROM {table_name} WHERE {search_field} LIKE ? LIMIT ? OFFSET ?"
cursor.execute(data_query, (f'%{search_value}%', per_page, offset))
else:
data_query = f"SELECT * FROM {table_name} LIMIT ? OFFSET ?"
cursor.execute(data_query, (per_page, offset))
rows = cursor.fetchall()
# 获取列名
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [col[1] for col in cursor.fetchall()]
conn.close()
# 处理数据,检测多行文本
processed_rows = []
for row in rows:
processed_row = []
for cell in row:
if cell is None:
processed_row.append({'value': '', 'type': 'empty'})
elif isinstance(cell, str) and ('\n' in cell or len(cell) > 100):
# 多行文本或长文本
lines = cell.count('\n') + 1
processed_row.append({
'value': cell,
'type': 'multiline',
'lines': lines,
'length': len(cell)
})
else:
processed_row.append({'value': str(cell), 'type': 'normal'})
processed_rows.append(processed_row)
logger.info(f"获取表 {table_name} 数据,第 {page} 页,共 {len(processed_rows)} 条记录")
return {
'columns': columns,
'rows': processed_rows,
'total_count': total_count,
'page': page,
'per_page': per_page,
'total_pages': (total_count + per_page - 1) // per_page
}
except Exception as e:
logger.error(f"获取表数据失败: {e}")
return {'columns': [], 'rows': [], 'total_count': 0, 'page': 1, 'per_page': 50, 'total_pages': 0}
# 初始化查看器
viewer = SQLiteWebViewer(DB_PATH)
@app.route('/')
def index():
"""主页"""
logger.info("访问主页")
return render_template('index.html')
@app.route('/api/tables')
def get_tables():
"""获取所有表"""
tables = viewer.get_tables()
return jsonify({'tables': tables})
@app.route('/api/table/<table_name>/structure')
def get_table_structure(table_name):
"""获取表结构"""
structure = viewer.get_table_structure(table_name)
return jsonify({'structure': structure})
@app.route('/api/table/<table_name>/data')
def get_table_data(table_name):
"""获取表数据"""
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 50))
search_field = request.args.get('search_field')
search_value = request.args.get('search_value')
data = viewer.get_table_data(table_name, page, per_page, search_field, search_value)
return jsonify(data)
@app.route('/static/<path:filename>')
def static_files(filename):
"""静态文件"""
return send_from_directory('static', filename)
def create_html_template():
"""创建HTML模板"""
template_dir = 'templates'
if not os.path.exists(template_dir):
os.makedirs(template_dir)
html_content = '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQLite数据库查看器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
text-align: center;
}
.controls {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
margin-top: 20px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-weight: 600;
color: #34495e;
font-size: 0.9em;
}
select, input {
padding: 12px 15px;
border: 2px solid #e0e6ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
background: white;
}
select:focus, input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.data-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.table-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.table-info h2 {
color: #2c3e50;
font-size: 1.5em;
}
.stats {
display: flex;
gap: 20px;
font-size: 0.9em;
color: #7f8c8d;
}
.table-wrapper {
overflow-x: auto;
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
font-size: 14px;
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 12px;
text-align: left;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
td {
padding: 12px;
border-bottom: 1px solid #ecf0f1;
vertical-align: top;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
tr:hover {
background-color: #e3f2fd;
transition: background-color 0.3s ease;
}
.multiline-cell {
white-space: pre-wrap;
line-height: 1.6;
max-height: 200px;
overflow-y: auto;
padding: 8px;
background: #fff3cd;
border-radius: 6px;
border-left: 4px solid #ffc107;
}
.normal-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.empty-cell {
color: #95a5a6;
font-style: italic;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 25px;
flex-wrap: wrap;
}
.page-info {
color: #7f8c8d;
font-weight: 600;
}
.page-btn {
padding: 8px 12px;
border: 2px solid #e0e6ed;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 40px;
text-align: center;
}
.page-btn:hover {
border-color: #667eea;
background: #667eea;
color: white;
}
.page-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 40px;
color: #7f8c8d;
font-size: 1.1em;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
border: 1px solid #f5c6cb;
margin: 20px 0;
}
.no-data {
text-align: center;
padding: 40px;
color: #7f8c8d;
font-size: 1.1em;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.table-info {
flex-direction: column;
gap: 15px;
text-align: center;
}
.stats {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗄️ SQLite数据库查看器</h1>
<div class="controls">
<div class="control-group">
<label for="tableSelect">选择数据表:</label>
<select id="tableSelect">
<option value="">加载中...</option>
</select>
</div>
<div class="control-group">
<label for="searchField">筛选字段:</label>
<select id="searchField" disabled>
<option value="">选择字段...</option>
</select>
</div>
<div class="control-group">
<label for="searchValue">筛选内容:</label>
<input type="text" id="searchValue" placeholder="输入筛选内容..." disabled>
</div>
<button class="btn" onclick="loadData()">刷新数据</button>
</div>
</div>
<div class="data-container">
<div class="table-info">
<h2 id="tableName">请选择数据表</h2>
<div class="stats">
<span id="recordCount">记录数: 0</span>
<span id="pageInfo">第 0 页,共 0 页</span>
</div>
</div>
<div id="dataContainer">
<div class="no-data">请选择数据表以查看内容</div>
</div>
<div id="pagination" class="pagination" style="display: none;">
<button class="page-btn" onclick="changePage('prev')" id="prevBtn">上一页</button>
<span class="page-info" id="pageInfoDetail"></span>
<button class="page-btn" onclick="changePage('next')" id="nextBtn">下一页</button>
</div>
</div>
</div>
<script>
let currentTable = '';
let currentPage = 1;
let perPage = 50;
let totalPages = 1;
let currentData = null;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadTables();
// 绑定事件
document.getElementById('tableSelect').addEventListener('change', function() {
currentTable = this.value;
currentPage = 1;
if (currentTable) {
loadTableStructure();
loadData();
}
});
document.getElementById('searchField').addEventListener('change', loadData);
document.getElementById('searchValue').addEventListener('input', debounce(loadData, 500));
});
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 加载表列表
async function loadTables() {
try {
const response = await fetch('/api/tables');
const data = await response.json();
const select = document.getElementById('tableSelect');
select.innerHTML = '<option value="">选择数据表...</option>';
data.tables.forEach(table => {
const option = document.createElement('option');
option.value = table;
option.textContent = table;
select.appendChild(option);
});
} catch (error) {
console.error('加载表列表失败:', error);
showError('加载表列表失败: ' + error.message);
}
}
// 加载表结构
async function loadTableStructure() {
if (!currentTable) return;
try {
const response = await fetch(`/api/table/${currentTable}/structure`);
const data = await response.json();
const searchField = document.getElementById('searchField');
searchField.innerHTML = '<option value="">所有字段</option>';
data.structure.forEach(field => {
const option = document.createElement('option');
option.value = field.name;
option.textContent = field.name;
searchField.appendChild(option);
});
searchField.disabled = false;
document.getElementById('searchValue').disabled = false;
} catch (error) {
console.error('加载表结构失败:', error);
}
}
// 加载数据
async function loadData() {
if (!currentTable) return;
const container = document.getElementById('dataContainer');
container.innerHTML = '<div class="loading">📊 数据加载中...</div>';
const searchField = document.getElementById('searchField').value;
const searchValue = document.getElementById('searchValue').value;
try {
let url = `/api/table/${currentTable}/data?page=${currentPage}&per_page=${perPage}`;
if (searchField && searchValue) {
url += `&search_field=${searchField}&search_value=${encodeURIComponent(searchValue)}`;
}
const response = await fetch(url);
currentData = await response.json();
displayData(currentData);
updatePagination();
} catch (error) {
console.error('加载数据失败:', error);
showError('加载数据失败: ' + error.message);
}
}
// 显示数据
function displayData(data) {
const container = document.getElementById('dataContainer');
if (!data.rows || data.rows.length === 0) {
container.innerHTML = '<div class="no-data">📭 没有找到数据</div>';
return;
}
let html = '<div class="table-wrapper"><table><thead><tr>';
// 表头
data.columns.forEach(col => {
html += `<th>${col}</th>`;
});
html += '</tr></thead><tbody>';
// 数据行
data.rows.forEach(row => {
html += '<tr>';
row.forEach(cell => {
if (cell.type === 'multiline') {
html += `<td><div class="multiline-cell">${escapeHtml(cell.value)}</div></td>`;
} else if (cell.type === 'empty') {
html += '<td><div class="empty-cell">空</div></td>';
} else {
html += `<td><div class="normal-cell">${escapeHtml(cell.value)}</div></td>`;
}
});
html += '</tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
// 更新统计信息
document.getElementById('tableName').textContent = `📋 ${currentTable}`;
document.getElementById('recordCount').textContent = `记录数: ${data.total_count}`;
document.getElementById('pageInfo').textContent = `第 ${currentPage} 页,共 ${data.total_pages} 页`;
}
// 更新分页
function updatePagination() {
if (!currentData) return;
totalPages = currentData.total_pages;
const pagination = document.getElementById('pagination');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageInfo = document.getElementById('pageInfoDetail');
if (totalPages <= 1) {
pagination.style.display = 'none';
return;
}
pagination.style.display = 'flex';
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
pageInfo.textContent = `${currentPage} / ${totalPages}`;
}
// 翻页
function changePage(direction) {
if (direction === 'prev' && currentPage > 1) {
currentPage--;
loadData();
} else if (direction === 'next' && currentPage < totalPages) {
currentPage++;
loadData();
}
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示错误
function showError(message) {
const container = document.getElementById('dataContainer');
container.innerHTML = `<div class="error">❌ ${message}</div>`;
}
</script>
</body>
</html>'''
with open(os.path.join(template_dir, 'index.html'), 'w', encoding='utf-8') as f:
f.write(html_content)
logger.info("HTML模板创建完成")
if __name__ == '__main__':
logger.info("启动Web SQLite查看器")
# 创建HTML模板
create_html_template()
# 检查数据库文件
if not os.path.exists(DB_PATH):
logger.warning(f"数据库文件不存在: {DB_PATH}")
logger.info("将创建一个空的数据库用于演示")
# 创建示例数据库
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 创建示例表
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
features TEXT,
price REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 插入示例数据
sample_data = [
('产品A', '这是一个非常优秀的产品\n具有多种实用功能\n用户反馈很好', '高性能\n易用性\n稳定性', 99.99),
('产品B', '创新设计\n简洁界面\n强大功能', '创新\n美观\n实用', 149.99),
('产品C', '专业级解决方案\n适用于企业环境\n支持大规模部署', '企业级\n可扩展\n安全', 299.99)
]
cursor.executemany('INSERT INTO products (name, description, features, price) VALUES (?, ?, ?, ?)', sample_data)
conn.commit()
conn.close()
logger.info("示例数据库创建完成")
logger.info(f"Web服务器启动访问地址: http://localhost:5000")
logger.info("按 Ctrl+C 停止服务器")
app.run(debug=True, host='0.0.0.0', port=5000)