|
|
@@ -0,0 +1,185 @@
|
|
|
+"""
|
|
|
+导入完整性验证脚本
|
|
|
+检查 DOCX 文件数 vs 数据库入库数,定位漏读/漏写的药品
|
|
|
+"""
|
|
|
+import os, sys, json, asyncio
|
|
|
+from pathlib import Path
|
|
|
+from collections import Counter
|
|
|
+
|
|
|
+# 添加同目录模块
|
|
|
+sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
+from docx_ingest import find_docx_files, parse_docx
|
|
|
+
|
|
|
+# 数据库
|
|
|
+import asyncpg
|
|
|
+
|
|
|
+SOURCE_DIR = os.environ.get("DOCX_SOURCE_DIR", "/opt/2025")
|
|
|
+DB_HOST = os.environ.get("POSTGRES_HOST", "localhost")
|
|
|
+DB_PORT = os.environ.get("POSTGRES_PORT", "5432")
|
|
|
+DB_NAME = os.environ.get("POSTGRES_DB", "pharmacopoeia")
|
|
|
+DB_USER = os.environ.get("POSTGRES_USER", "postgres")
|
|
|
+DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
|
|
|
+
|
|
|
+
|
|
|
+async def main():
|
|
|
+ # ==========================================
|
|
|
+ # 1. 扫描 DOCX 文件
|
|
|
+ # ==========================================
|
|
|
+ print("=" * 60)
|
|
|
+ print("📂 扫描 DOCX 文件...")
|
|
|
+ files = find_docx_files(SOURCE_DIR)
|
|
|
+ print(f" DOCX 文件总数: {len(files)}")
|
|
|
+
|
|
|
+ # 提取文件名(去 .docx)作为药品名
|
|
|
+ docx_names = set()
|
|
|
+ docx_by_volume = Counter()
|
|
|
+ for fp in files:
|
|
|
+ name = Path(fp).stem
|
|
|
+ docx_names.add(name)
|
|
|
+ vol = "一部"
|
|
|
+ for k in ["output4", "output3", "output2", "output"]:
|
|
|
+ if k in fp:
|
|
|
+ vol = {"output": "一部", "output2": "二部", "output3": "三部", "output4": "四部"}[k]
|
|
|
+ break
|
|
|
+ docx_by_volume[vol] += 1
|
|
|
+ print(f" 去重后唯一药名: {len(docx_names)}")
|
|
|
+ print(f" 各卷分布: {dict(docx_by_volume)}")
|
|
|
+
|
|
|
+ # ==========================================
|
|
|
+ # 2. 查询数据库
|
|
|
+ # ==========================================
|
|
|
+ print("\n📊 查询数据库...")
|
|
|
+ conn = await asyncpg.connect(
|
|
|
+ host=DB_HOST, port=DB_PORT, database=DB_NAME,
|
|
|
+ user=DB_USER, password=DB_PASSWORD
|
|
|
+ )
|
|
|
+
|
|
|
+ # 2.1 药品统计
|
|
|
+ total_drugs = await conn.fetchval("SELECT COUNT(*) FROM drugs WHERE is_active = TRUE")
|
|
|
+ total_chunks = await conn.fetchval("SELECT COUNT(*) FROM drug_chunks WHERE vec IS NOT NULL")
|
|
|
+ total_chunks_null = await conn.fetchval("SELECT COUNT(*) FROM drug_chunks WHERE vec IS NULL")
|
|
|
+
|
|
|
+ print(f" drugs 表 (active): {total_drugs}")
|
|
|
+ print(f" drug_chunks (有向量): {total_chunks}")
|
|
|
+ print(f" drug_chunks (无向量): {total_chunks_null}")
|
|
|
+
|
|
|
+ # 2.2 获取数据库中所有药名
|
|
|
+ db_names = set()
|
|
|
+ rows = await conn.fetch("SELECT name FROM drugs WHERE is_active = TRUE")
|
|
|
+ for r in rows:
|
|
|
+ db_names.add(r["name"])
|
|
|
+ print(f" 数据库中药名: {len(db_names)}")
|
|
|
+
|
|
|
+ # 2.3 按来源版本统计
|
|
|
+ version_counts = await conn.fetch("""
|
|
|
+ SELECT source_version, COUNT(*) FROM drugs
|
|
|
+ WHERE is_active = TRUE GROUP BY source_version ORDER BY COUNT(*) DESC
|
|
|
+ """)
|
|
|
+ print(f" 版本分布:")
|
|
|
+ for r in version_counts:
|
|
|
+ print(f" {r['source_version']}: {r['count']}")
|
|
|
+
|
|
|
+ # 2.4 按分类统计
|
|
|
+ cat_counts = await conn.fetch("""
|
|
|
+ SELECT category, COUNT(*) FROM drugs
|
|
|
+ WHERE is_active = TRUE GROUP BY category ORDER BY COUNT(*) DESC
|
|
|
+ """)
|
|
|
+ print(f" 分类分布:")
|
|
|
+ for r in cat_counts:
|
|
|
+ print(f" {r['category']}: {r['count']}")
|
|
|
+
|
|
|
+ # ==========================================
|
|
|
+ # 3. 对比分析
|
|
|
+ # ==========================================
|
|
|
+ print("\n🔍 对比分析:")
|
|
|
+
|
|
|
+ # 3.1 DOCX 有但 DB 没有的(漏读)
|
|
|
+ missing = docx_names - db_names
|
|
|
+ if missing:
|
|
|
+ print(f"\n ❌ DOCX 有但数据库没有({len(missing)} 个):")
|
|
|
+ for name in sorted(missing)[:30]:
|
|
|
+ print(f" - {name}")
|
|
|
+ if len(missing) > 30:
|
|
|
+ print(f" ... 还有 {len(missing) - 30} 个")
|
|
|
+ else:
|
|
|
+ print(f"\n ✅ 无漏读:所有 DOCX 药名均已在数据库中")
|
|
|
+
|
|
|
+ # 3.2 DB 有但 DOCX 没有的(额外数据,如 Wiki)
|
|
|
+ extra = db_names - docx_names
|
|
|
+ if extra:
|
|
|
+ print(f"\n 📝 数据库额外药名(Wiki/样本数据,{len(extra)} 个):")
|
|
|
+ for name in sorted(extra):
|
|
|
+ print(f" - {name}")
|
|
|
+
|
|
|
+ # ==========================================
|
|
|
+ # 4. 抽查特定药品的 sections
|
|
|
+ # ==========================================
|
|
|
+ print("\n🔬 抽查药品内容完整性:")
|
|
|
+ spot_check = ["布洛芬", "甲硝唑", "阿莫西林", "对乙酰氨基酚", "二甲双胍"]
|
|
|
+ for name in spot_check:
|
|
|
+ drug = await conn.fetchrow(
|
|
|
+ "SELECT drug_id, name, sections, source_version FROM drugs WHERE name = $1 AND is_active = TRUE",
|
|
|
+ name
|
|
|
+ )
|
|
|
+ if drug:
|
|
|
+ sections = json.loads(drug["sections"]) if drug["sections"] else {}
|
|
|
+ chunk_count = await conn.fetchval(
|
|
|
+ "SELECT COUNT(*) FROM drug_chunks WHERE drug_id = $1 AND vec IS NOT NULL",
|
|
|
+ drug["drug_id"]
|
|
|
+ )
|
|
|
+ sec_names = list(sections.keys())
|
|
|
+ print(f" ✅ {name} | {len(sec_names)} sections | {chunk_count} chunks | {drug['source_version']}")
|
|
|
+ print(f" 栏目: {sec_names[:10]}...")
|
|
|
+ else:
|
|
|
+ print(f" ❌ {name}: 数据库中不存在")
|
|
|
+
|
|
|
+ # ==========================================
|
|
|
+ # 5. 检查 chunks 完整性
|
|
|
+ # ==========================================
|
|
|
+ print("\n📐 Chunks 完整性检查:")
|
|
|
+
|
|
|
+ # 5.1 检查 drug_id 引用完整性(孤儿 chunk)
|
|
|
+ orphan_chunks = await conn.fetchval("""
|
|
|
+ SELECT COUNT(*) FROM drug_chunks c
|
|
|
+ LEFT JOIN drugs d ON d.drug_id = c.drug_id
|
|
|
+ WHERE d.id IS NULL
|
|
|
+ """)
|
|
|
+ print(f" 孤儿 chunks(无对应 drug): {orphan_chunks}")
|
|
|
+
|
|
|
+ # 5.2 有 drug 但没有 chunk 的
|
|
|
+ no_chunk_drugs = await conn.fetchval("""
|
|
|
+ SELECT COUNT(*) FROM drugs d
|
|
|
+ WHERE d.is_active = TRUE
|
|
|
+ AND NOT EXISTS (SELECT 1 FROM drug_chunks c WHERE c.drug_id = d.drug_id)
|
|
|
+ """)
|
|
|
+ print(f" 无 chunk 的药品: {no_chunk_drugs}")
|
|
|
+
|
|
|
+ # 5.3 vec 维度是否正确
|
|
|
+ vec_dim = await conn.fetchval("""
|
|
|
+ SELECT vector_dims(vec) FROM drug_chunks WHERE vec IS NOT NULL LIMIT 1
|
|
|
+ """)
|
|
|
+ print(f" 向量维度: {vec_dim}")
|
|
|
+
|
|
|
+ # ==========================================
|
|
|
+ # 6. 汇总结论
|
|
|
+ # ==========================================
|
|
|
+ print("\n" + "=" * 60)
|
|
|
+ print("📋 验证结论:")
|
|
|
+ print(f" DOCX 文件: {len(files)}")
|
|
|
+ print(f" 数据库中药品: {total_drugs}")
|
|
|
+ print(f" 数据库 chunks: {total_chunks}")
|
|
|
+ print(f" 漏读药品: {len(missing)}")
|
|
|
+ print(f" 额外药品 (Wiki等): {len(extra)}")
|
|
|
+ print(f" 孤儿 chunks: {orphan_chunks}")
|
|
|
+ print(f" 无 chunk 药品: {no_chunk_drugs}")
|
|
|
+ if len(missing) == 0 and orphan_chunks == 0 and total_chunks > 0:
|
|
|
+ print("\n ✅ 导入完整,无漏读漏写")
|
|
|
+ else:
|
|
|
+ print(f"\n ⚠️ 发现问题,请检查上述 ❌ 项")
|
|
|
+ print("=" * 60)
|
|
|
+
|
|
|
+ await conn.close()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ asyncio.run(main())
|