RAG/scan_databases.py

403 lines
16 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
"""
扫描 MySQL 数据库,分析表结构并生成配置
自动识别包含内容列的表include 模式):
- 只考虑表中包含 text、longtext、mediumtext、tinytext 类型的列
- 或者表中包含 blob、longblob、mediumblob、tinyblob 类型的列
支持通过 scan_exclude_config.json 配置文件额外排除不需要的数据库和表
"""
import pymysql
import json
import os
import re
from typing import List, Dict, Any, Set
from config import settings
def load_exclude_config(config_path: str = 'scan_exclude_config.json') -> Dict[str, Any]:
"""
加载排除配置文件
Args:
config_path: 配置文件路径
Returns:
排除配置字典,包含 exclude_databases, exclude_databases_patterns,
和 exclude_tables_patterns仅支持正则表达式模式
"""
default_config = {
"exclude_databases": ["information_schema", "performance_schema", "mysql", "sys"],
"exclude_databases_patterns": [],
"exclude_tables_patterns": {}
}
if not os.path.exists(config_path):
print(f"排除配置文件不存在: {config_path},使用默认配置")
return default_config
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 确保配置格式正确
if 'exclude_databases' not in config:
config['exclude_databases'] = default_config['exclude_databases']
if 'exclude_databases_patterns' not in config:
config['exclude_databases_patterns'] = []
if 'exclude_tables_patterns' not in config:
config['exclude_tables_patterns'] = {}
return config
except Exception as e:
print(f"加载排除配置文件失败: {e},使用默认配置")
return default_config
def matches_pattern(name: str, patterns: List[str]) -> bool:
"""
检查名称是否匹配任何正则表达式模式
Args:
name: 要检查的名称
patterns: 正则表达式模式列表
Returns:
如果匹配任何模式返回 True否则返回 False
"""
for pattern in patterns:
try:
if re.search(pattern, name):
return True
except re.error as e:
print(f"警告: 正则表达式模式 '{pattern}' 无效: {e}")
continue
return False
def get_mysql_connection():
"""获取 MySQL 连接"""
return pymysql.connect(
host=settings.MYSQL_HOST,
port=settings.MYSQL_PORT,
user=settings.MYSQL_USER,
password=settings.MYSQL_PASSWORD,
charset=settings.MYSQL_CHARSET,
cursorclass=pymysql.cursors.DictCursor
)
def get_databases(conn, exclude_databases: Set[str] = None, exclude_patterns: List[str] = None) -> List[str]:
"""
获取所有数据库列表
Args:
conn: MySQL 连接
exclude_databases: 要排除的数据库集合(精确匹配)
exclude_patterns: 要排除的数据库正则表达式模式列表
Returns:
数据库列表
"""
if exclude_databases is None:
exclude_databases = set()
if exclude_patterns is None:
exclude_patterns = []
with conn.cursor() as cursor:
cursor.execute("SHOW DATABASES")
databases = [row['Database'] for row in cursor.fetchall()]
# 过滤掉排除的数据库
filtered_databases = []
for db in databases:
# 检查精确匹配
if db in exclude_databases:
continue
# 检查正则表达式匹配
if matches_pattern(db, exclude_patterns):
continue
filtered_databases.append(db)
return filtered_databases
def get_tables(conn, database: str) -> List[str]:
"""获取指定数据库的所有表"""
with conn.cursor() as cursor:
cursor.execute(f"USE `{database}`")
cursor.execute("SHOW TABLES")
# 表名在结果中的键名是 'Tables_in_{database}'
key = f"Tables_in_{database}"
return [row[key] for row in cursor.fetchall()]
def get_table_columns(conn, database: str, table: str) -> List[Dict[str, Any]]:
"""获取表的列信息"""
with conn.cursor() as cursor:
cursor.execute(f"USE `{database}`")
cursor.execute(f"DESCRIBE `{table}`")
return cursor.fetchall()
def should_include_table(table_name: str, columns: List[Dict[str, Any]]) -> bool:
"""
判断表是否应该被包含(仅根据列类型判断,并排除敏感信息表)
只考虑 text、longtext、mediumtext、tinytext 以及 blob 等类型的字段
排除包含敏感信息的表authentication、private、key、secret、password 等)
Args:
table_name: 表名
columns: 表的列信息列表
Returns:
如果表应该被包含返回 True否则返回 False
"""
# 排除敏感信息表(检查表名)
table_lower = table_name.lower()
sensitive_keywords = ['authentication', 'auth', 'private', 'key', 'secret', 'password',
'pwd', 'token', 'credential', 'credential', 'session', 'login',
'user_password', 'user_secret', 'api_key', 'access_key', 'secret_key']
# 检查表名是否包含敏感关键词
if any(keyword in table_lower for keyword in sensitive_keywords):
return False
# 检查列名是否包含敏感关键词
column_names = [col['Field'].lower() for col in columns]
if any(keyword in col_name for col_name in column_names for keyword in sensitive_keywords):
return False
column_types = [col['Type'].lower() for col in columns]
# 只检查是否有文本类型列text、longtext、mediumtext、tinytext、blob、longblob、mediumblob、tinyblob
text_types = ['text', 'longtext', 'mediumtext', 'tinytext', 'blob', 'longblob', 'mediumblob', 'tinyblob']
has_text_type = any(any(text_type in col_type for text_type in text_types) for col_type in column_types)
return has_text_type
def analyze_table_structure(columns: List[Dict[str, Any]]) -> Dict[str, Any]:
"""分析表结构,推断配置项"""
column_names = [col['Field'] for col in columns]
column_types = {col['Field']: col['Type'].lower() for col in columns}
# 查找 ID 列(通常是 id, *_id
id_column = None
for col in column_names:
if col.lower() == 'id':
id_column = col
break
elif col.lower().endswith('_id'):
id_column = col
break
# 查找内容列(只考虑 text、longtext、mediumtext、tinytext 以及 blob 等类型)
text_types = ['text', 'longtext', 'mediumtext', 'tinytext', 'blob', 'longblob', 'mediumblob', 'tinyblob']
content_columns = []
for col in column_names:
col_type = column_types.get(col, '').lower()
# 只包含 text 和 blob 类型的列
if any(text_type in col_type for text_type in text_types):
if col != id_column and not col.lower().endswith('_id'):
content_columns.append(col)
# 查找标题列(可能是 title, subject, name
title_column = None
title_keywords = ['title', 'subject', 'name']
for col in column_names:
if col.lower() in title_keywords:
title_column = col
break
# 查找更新时间列(可能是 updated_at, updated_on, modified_at, update_time
updated_at_column = None
update_keywords = ['updated_at', 'updated_on', 'modified_at', 'update_time', 'updated_time']
for col in column_names:
if col.lower() in update_keywords:
updated_at_column = col
break
# 查找元数据列author, category 等,不包括时间相关列)
metadata_columns = []
# 时间相关的关键词,这些不应该出现在 metadata_columns 中
time_keywords = ['created_at', 'created_on', 'created_time', 'create_time', 'create_by',
'updated_at', 'updated_on', 'updated_time', 'update_time', 'modified_at',
'created_unix', 'updated_unix']
# 元数据关键词(不包括时间相关)
metadata_keywords = ['author', 'category', 'tag', 'tags', 'status', 'type', 'priority', 'source']
for col in column_names:
col_lower = col.lower()
# 排除时间相关的列
if any(time_keyword in col_lower for time_keyword in time_keywords):
continue
# 排除以 created_ 或 create_ 开头的列(时间相关)
if col_lower.startswith('created_') or col_lower.startswith('create_'):
continue
# 排除以 updated_ 或 update_ 开头的列(时间相关)
if col_lower.startswith('updated_') or col_lower.startswith('update_'):
continue
# 排除以 modified_ 开头的列(时间相关)
if col_lower.startswith('modified_'):
continue
# 添加符合条件的元数据列
if col_lower in metadata_keywords:
metadata_columns.append(col)
return {
'id_column': id_column or 'id',
'content_columns': content_columns[:3] if content_columns else ['content'], # 最多取3个
'title_column': title_column,
'updated_at_column': updated_at_column,
'metadata_columns': metadata_columns[:5] if metadata_columns else None # 最多取5个
}
def generate_config(conn, exclude_config: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""
生成配置列表
Args:
conn: MySQL 连接
exclude_config: 排除配置字典
Returns:
配置列表
"""
if exclude_config is None:
exclude_config = {
"exclude_databases": [],
"exclude_databases_patterns": [],
"exclude_tables_patterns": {}
}
configs = []
exclude_databases = set(exclude_config.get('exclude_databases', []))
exclude_db_patterns = exclude_config.get('exclude_databases_patterns', [])
exclude_table_patterns = exclude_config.get('exclude_tables_patterns', {})
databases = get_databases(conn, exclude_databases, exclude_db_patterns)
print(f"找到 {len(databases)} 个数据库: {', '.join(databases)}")
if exclude_databases:
print(f"排除的数据库(精确匹配): {', '.join(sorted(exclude_databases))}")
if exclude_db_patterns:
print(f"排除的数据库(正则表达式): {', '.join(exclude_db_patterns)}")
for database in databases:
print(f"\n扫描数据库: {database}")
try:
tables = get_tables(conn, database)
# 获取该数据库要排除的表(正则表达式模式)
db_exclude_patterns = exclude_table_patterns.get(database, [])
if db_exclude_patterns:
print(f" 排除的表(正则表达式): {', '.join(db_exclude_patterns)}")
# 过滤掉排除的表(仅使用正则表达式模式)
filtered_tables = []
for table in tables:
# 检查正则表达式匹配
if matches_pattern(table, db_exclude_patterns):
continue
filtered_tables.append(table)
print(f" 找到 {len(tables)} 个表,排除后 {len(filtered_tables)} 个表")
# 进一步过滤:只包含有内容列的表
included_tables = []
excluded_by_content = []
for table in filtered_tables:
try:
columns = get_table_columns(conn, database, table)
if should_include_table(table, columns):
included_tables.append(table)
else:
excluded_by_content.append(table)
except Exception as e:
print(f" 检查表 {table} 时出错: {e}")
continue
print(f" 自动识别包含内容列的表: {len(included_tables)}")
if excluded_by_content:
print(f" 排除无内容列的表: {len(excluded_by_content)}")
if len(excluded_by_content) <= 10:
print(f" {', '.join(excluded_by_content)}")
else:
print(f" {', '.join(excluded_by_content[:10])} ... (共 {len(excluded_by_content)} 个)")
for table in included_tables:
print(f" 分析表: {table}")
try:
columns = get_table_columns(conn, database, table)
analysis = analyze_table_structure(columns)
# 生成配置项
config = {
'name': f"{database}_{table}",
'database': database,
'table_name': table,
'id_column': analysis['id_column'],
'content_column': ','.join(analysis['content_columns']),
'title_column': analysis['title_column'],
'metadata_columns': ','.join(analysis['metadata_columns']) if analysis['metadata_columns'] else None,
'content_separator': '\n',
'updated_at_column': analysis['updated_at_column']
}
# 清理 None 值
config = {k: v for k, v in config.items() if v is not None}
configs.append(config)
print(f" ID列: {analysis['id_column']}")
print(f" 内容列: {', '.join(analysis['content_columns'])}")
if analysis['title_column']:
print(f" 标题列: {analysis['title_column']}")
if analysis['updated_at_column']:
print(f" 更新时间列: {analysis['updated_at_column']}")
if analysis['metadata_columns']:
print(f" 元数据列: {', '.join(analysis['metadata_columns'])}")
except Exception as e:
print(f" 错误: {e}")
continue
except Exception as e:
print(f" 错误: {e}")
continue
return configs
def main():
"""主函数"""
try:
# 加载排除配置
exclude_config = load_exclude_config()
conn = get_mysql_connection()
print("成功连接到 MySQL 服务器")
print("=" * 60)
print("扫描模式: 自动识别包含内容列的表include 模式)")
print("识别条件:")
print(" - 表中包含 text、longtext、mediumtext、tinytext 类型的列")
print(" - 或者表中包含 blob、longblob、mediumblob、tinyblob 类型的列")
print(" - 自动排除包含敏感信息的表authentication、private、key、secret、password 等)")
print("=" * 60)
if os.path.exists('scan_exclude_config.json'):
print(f"额外排除配置: scan_exclude_config.json")
print()
configs = generate_config(conn, exclude_config)
conn.close()
# 输出 JSON 配置
print(f"\n\n生成 {len(configs)} 个配置项")
print("\n配置 JSON:")
print(json.dumps(configs, indent=2, ensure_ascii=False))
# 保存到文件
output_file = 'databases_config_new.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(configs, f, indent=2, ensure_ascii=False)
print(f"\n配置已保存到: {output_file}")
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()