直接上代码
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ PostgreSQL 数据字典生成器 (Python 3.11+) 生成完全离线的 HTML 文件,可直接双击在浏览器中打开。 """ import psycopg2 import datetime import os import sys from typing import List, Dict, Any # 数据库配置(请根据实际情况修改) DB_CONFIG = { "host": "127.0.0.1", "port": 5432, "database": "postgres", "user": "postgres", "password": "postgres" } def get_tables_and_comments(conn) -> List[Dict[str, str]]: """获取 public 模式下所有用户表及其注释""" with conn.cursor() as cur: cur.execute(""" SELECT t.table_name, obj_description(c.oid, 'pg_class') AS table_comment FROM information_schema.tables t JOIN pg_class c ON c.relname = t.table_name WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' ORDER BY t.table_name; """) return [ {"TABLE_NAME": row[0], "TABLE_COMMENT": row[1] or ""} for row in cur.fetchall() ] def get_columns_info(conn, table_name: str, table_schema: str = 'public') -> List[Dict[str, Any]]: """获取指定表的所有列详细信息""" with conn.cursor() as cur: # 获取列注释(使用 pg_* 系统表查询) cur.execute(""" SELECT a.attname, COALESCE(col_description(a.attrelid, a.attnum), '') FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = %s AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; """, (table_schema, table_name)) comments = {row[0]: row[1] for row in cur.fetchall()} # 获取列基本信息 cur.execute(""" SELECT c.column_name, c.data_type, c.column_default, c.is_nullable, CASE WHEN c.column_default LIKE 'nextval(%%' THEN TRUE ELSE FALSE END AS is_auto_increment FROM information_schema.columns c WHERE c.table_schema = %s AND c.table_name = %s ORDER BY c.ordinal_position; """, (table_schema, table_name)) columns = [] for row in cur.fetchall(): if row is None: continue col_name = row[0] if len(row) > 0 else "" columns.append({ "COLUMN_NAME": col_name, "COLUMN_TYPE": row[1] if len(row) > 1 else "", "COLUMN_DEFAULT": row[2] if len(row) > 2 else "", "IS_NULLABLE": row[3] if len(row) > 3 else "NO", "IS_AUTO_INCREMENT": row[4] if len(row) > 4 else False, "COLUMN_COMMENT": comments.get(col_name, "") }) return columns def generate_offline_html(tables: List[Dict], db_name: str) -> str: """生成完整的离线 HTML 内容""" # 表清单 - 使用百分比宽度,自动适应页面 table_list_html = '<table border="1" cellspacing="0" cellpadding="5" style="width:100%;table-layout:fixed;">\n' table_list_html += '<colgroup><col style="width:8%"><col style="width:25%"><col style="width:67%"></colgroup>\n' table_list_html += '<thead><tr><th>序号</th><th>表名</th><th>功能说明</th></tr></thead>\n<tbody>\n' for idx, tbl in enumerate(tables, 1): table_list_html += f' <tr><td>{idx}</td><td>{tbl["TABLE_NAME"]}</td><td>{tbl["TABLE_COMMENT"]}</td></tr>\n' table_list_html += '</tbody>\n</table>\n' # 字段详情 detail_html = "" for tbl in tables: detail_html += '<table border="1" cellspacing="0" cellpadding="5" style="width:100%;table-layout:fixed;">\n' detail_html += '<colgroup><col style="width:15%"><col style="width:12%"><col style="width:15%"><col style="width:8%"><col style="width:8%"><col style="width:42%"></colgroup>\n' comment_part = f' - {tbl["TABLE_COMMENT"]}' if tbl["TABLE_COMMENT"] else "" detail_html += f' <caption><b>表名:{tbl["TABLE_NAME"]}{comment_part}</b></caption>\n' detail_html += ' <thead>\n <tr><th>字段名</th><th>数据类型</th><th>默认值</th><th>允许非空</th><th>自动递增</th><th>备注</th></tr>\n </thead>\n <tbody>\n' for col in tbl.get("COLUMNS", []): nullable_display = "是" if col["IS_NULLABLE"] == "YES" else "否" auto_inc_display = "是" if col["IS_AUTO_INCREMENT"] else "-" detail_html += ( f' <tr>' f'<td>{col["COLUMN_NAME"]}</td>' f'<td>{col["COLUMN_TYPE"]}</td>' f'<td>{col["COLUMN_DEFAULT"]}</td>' f'<td>{nullable_display}</td>' f'<td>{auto_inc_display}</td>' f'<td>{col["COLUMN_COMMENT"]}</td>' f'</tr>\n' ) detail_html += ' </tbody>\n</table>\n<br/>\n' # 完整 HTML now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') return f'''<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{db_name} 数据字典</title> <style> body {{ font-family: "宋体", "SimSun", serif; font-size: 12pt; margin: 20px; background: white; color: black; }} h1 {{ text-align: center; color: black; margin-bottom: 15px; font-size: 16pt; }} .summary {{ text-align: center; color: #666666; margin-bottom: 20px; font-size: 11pt; }} table {{ border-collapse: collapse; margin-bottom: 20px; page-break-inside: avoid; word-wrap: break-word; }} caption {{ text-align: left; font-weight: bold; font-size: 12pt; color: black; padding: 6px 0; }} th, td {{ border: 1px solid #000000; padding: 5px 6px; text-align: left; vertical-align: top; font-size: 11pt; word-wrap: break-word; }} thead th {{ background-color: #E0E0E0; font-weight: bold; }} .footer {{ text-align: left; margin-top: 20px; color: #666666; font-size: 11pt; }} </style> </head> <body> <h1>{db_name} 数据字典</h1> <p class="summary">生成时间:{now}</p> {table_list_html} <br/> {detail_html} <p class="footer">总共:{len(tables)} 个数据表</p> </body> </html>''' def main(): try: print("🔍 正在连接 PostgreSQL 数据库...") with psycopg2.connect(**DB_CONFIG) as conn: tables = get_tables_and_comments(conn) print(f"✅ 成功获取 {len(tables)} 张表") total_cols = 0 for tbl in tables: cols = get_columns_info(conn, tbl["TABLE_NAME"]) tbl["COLUMNS"] = cols total_cols += len(cols) html_content = generate_offline_html(tables, DB_CONFIG["database"]) # 生成带时间戳的文件名 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") output_file = f'{DB_CONFIG["database"]}_数据字典_{timestamp}.html' with open(output_file, "w", encoding="utf-8") as f: f.write(html_content) abs_path = os.path.abspath(output_file) print(f"🎉 数据字典已生成!") print(f"📁 文件路径: {abs_path}") print(f"📊 共 {len(tables)} 张表,{total_cols} 个字段") print("\n💡 提示:双击该 HTML 文件即可在浏览器中查看!") except psycopg2.Error as e: print(f"❌ 数据库错误: {e}") sys.exit(1) except Exception as e: print(f"💥 程序异常: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()效果如下图所示: