|
|
@@ -0,0 +1,184 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""
|
|
|
+图片 URL 迁移脚本:DB sections/chunks 中的旧图片路径 → 新 hash 文件名路径
|
|
|
+
|
|
|
+同时处理两种可能的 DB 现状(增量安全):
|
|
|
+ - 旧 SQL 已执行: /images/甲硝唑_rId9.jpg → /images/fa123757_rId9.jpg
|
|
|
+ - 旧 SQL 未执行: /images/drugs/甲硝唑_rId9.jpg → /images/fa123757_rId9.jpg
|
|
|
+"""
|
|
|
+
|
|
|
+import re
|
|
|
+import sys
|
|
|
+import os
|
|
|
+import hashlib
|
|
|
+import asyncio
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+import asyncpg
|
|
|
+
|
|
|
+
|
|
|
+# ======================================================================
|
|
|
+# 配置
|
|
|
+# ======================================================================
|
|
|
+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")
|
|
|
+
|
|
|
+# 新旧 URL 前缀
|
|
|
+OLD_PREFIX = "/images/drugs/"
|
|
|
+NEW_PREFIX = "/images/"
|
|
|
+
|
|
|
+# 旧文件名安全的规则(与原 docx_ingest.py 的 _safe_drug_name 一致)
|
|
|
+_RE_SAFE = re.compile(r'[\s()() ]+')
|
|
|
+
|
|
|
+# 匹配所有旧格式图片 URL(兼容 /images/ 和 /images/drugs/ 两种前缀)
|
|
|
+# group(1) = 旧前缀(中文 safe_name), group(2) = rId, group(3) = 扩展名
|
|
|
+_OLD_IMG_RE = re.compile(
|
|
|
+ r'/images/(?:drugs/)?' # /images/ 或 /images/drugs/
|
|
|
+ r'([^"_\s]+)' # 旧文件名前缀(中文 safe_name)
|
|
|
+ r'_(rId\d+)' # _rIdX 引用
|
|
|
+ r'\.(jpg|jpeg|png|gif|bmp)' # 扩展名
|
|
|
+)
|
|
|
+
|
|
|
+# 只用于计数查询的 LIKE 模式
|
|
|
+_SEARCH_PATTERN = '/images/%_rId%.'
|
|
|
+
|
|
|
+
|
|
|
+def old_safe_name(drug_name: str) -> str:
|
|
|
+ """与原始 docx_ingest.py 的 _safe_drug_name 一致"""
|
|
|
+ return _RE_SAFE.sub("_", drug_name).strip("_")
|
|
|
+
|
|
|
+
|
|
|
+def new_hash_prefix(drug_name: str) -> str:
|
|
|
+ """新的 hash 前缀(与更新后的 docx_ingest.py 一致)"""
|
|
|
+ return hashlib.md5(drug_name.encode()).hexdigest()[:8]
|
|
|
+
|
|
|
+
|
|
|
+def replace_urls(text: str, mappings: dict[str, str]) -> str:
|
|
|
+ """替换文本中所有旧格式图片 URL → 新 hash 格式。"""
|
|
|
+
|
|
|
+ def _replacer(m: re.Match) -> str:
|
|
|
+ old = m.group(1) # 旧 safe_name(中文)
|
|
|
+ rId = m.group(2) # rIdX
|
|
|
+ ext = m.group(3) # jpg/png/gif
|
|
|
+ new = mappings.get(old)
|
|
|
+ if new is None:
|
|
|
+ # 没匹配到的保留原文件名,但确保前缀是 /images/
|
|
|
+ return f"{NEW_PREFIX}{old}_{rId}.{ext}"
|
|
|
+ return f"{NEW_PREFIX}{new}_{rId}.{ext}"
|
|
|
+
|
|
|
+ return _OLD_IMG_RE.sub(_replacer, text)
|
|
|
+
|
|
|
+
|
|
|
+async def main():
|
|
|
+ # 1. 连接数据库
|
|
|
+ conn = await asyncpg.connect(
|
|
|
+ host=DB_HOST, port=DB_PORT, database=DB_NAME,
|
|
|
+ user=DB_USER, password=DB_PASSWORD,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 2. 查有图片引用的所有药品(同时兼容 /images/drugs/ 和 /images/ 两种状态)
|
|
|
+ drugs = await conn.fetch(f"""
|
|
|
+ SELECT DISTINCT name FROM drugs
|
|
|
+ WHERE sections::text LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ drug_names = [row["name"] for row in drugs]
|
|
|
+ print(f"从 drugs 表找到有图片引用的药品: {len(drug_names)} 个")
|
|
|
+
|
|
|
+ # 3. 构建映射: 旧 safe_name → 新 hash
|
|
|
+ mappings: dict[str, str] = {}
|
|
|
+ for name in drug_names:
|
|
|
+ safe = old_safe_name(name)
|
|
|
+ h = new_hash_prefix(name)
|
|
|
+ mappings[safe] = h
|
|
|
+
|
|
|
+ print(f"映射条目: {len(mappings)}")
|
|
|
+ for i, (old, new) in enumerate(mappings.items()):
|
|
|
+ if i >= 10:
|
|
|
+ print(f" ... 还有 {len(mappings) - 10} 条")
|
|
|
+ break
|
|
|
+ print(f" {old} → {new}")
|
|
|
+
|
|
|
+ # 4. 统计待更新量
|
|
|
+ sections_count = await conn.fetchval(f"""
|
|
|
+ SELECT COUNT(*) FROM drugs
|
|
|
+ WHERE sections::text LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ chunks_count = await conn.fetchval(f"""
|
|
|
+ SELECT COUNT(*) FROM drug_chunks
|
|
|
+ WHERE content LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ print(f"\n待更新: drugs={sections_count} chunks={chunks_count}")
|
|
|
+
|
|
|
+ if sections_count == 0 and chunks_count == 0:
|
|
|
+ print("✅ 无需更新")
|
|
|
+ await conn.close()
|
|
|
+ return
|
|
|
+
|
|
|
+ # 5. 更新 drug_chunks.content(纯文本,直接替换)
|
|
|
+ if chunks_count > 0:
|
|
|
+ print("\n📦 更新 drug_chunks ...")
|
|
|
+ rows = await conn.fetch(f"""
|
|
|
+ SELECT id, content FROM drug_chunks
|
|
|
+ WHERE content LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ updates = []
|
|
|
+ for row in rows:
|
|
|
+ new_content = replace_urls(row["content"], mappings)
|
|
|
+ if new_content != row["content"]:
|
|
|
+ updates.append((new_content, row["id"]))
|
|
|
+ if updates:
|
|
|
+ async with conn.transaction():
|
|
|
+ await conn.executemany(
|
|
|
+ "UPDATE drug_chunks SET content = $1 WHERE id = $2",
|
|
|
+ updates,
|
|
|
+ )
|
|
|
+ print(f" ✅ 更新了 {len(updates)} 条")
|
|
|
+ else:
|
|
|
+ print(" ✅ 无需更新")
|
|
|
+
|
|
|
+ # 6. 更新 drugs.sections(jsonb,需 text ↔ jsonb 转换)
|
|
|
+ if sections_count > 0:
|
|
|
+ print("\n📦 更新 drugs.sections ...")
|
|
|
+ rows = await conn.fetch(f"""
|
|
|
+ SELECT drug_id, sections::text AS txt FROM drugs
|
|
|
+ WHERE sections::text LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ updates = []
|
|
|
+ for row in rows:
|
|
|
+ new_text = replace_urls(row["txt"], mappings)
|
|
|
+ if new_text != row["txt"]:
|
|
|
+ updates.append((new_text, row["drug_id"]))
|
|
|
+ if updates:
|
|
|
+ async with conn.transaction():
|
|
|
+ await conn.executemany(
|
|
|
+ "UPDATE drugs SET sections = $1::jsonb WHERE drug_id = $2",
|
|
|
+ updates,
|
|
|
+ )
|
|
|
+ print(f" ✅ 更新了 {len(updates)} 条")
|
|
|
+ else:
|
|
|
+ print(" ✅ 无需更新")
|
|
|
+
|
|
|
+ # 7. 验证残留:确认无旧格式 URL
|
|
|
+ remaining_d = await conn.fetchval(f"""
|
|
|
+ SELECT COUNT(*) FROM drugs
|
|
|
+ WHERE sections::text LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+ remaining_c = await conn.fetchval(f"""
|
|
|
+ SELECT COUNT(*) FROM drug_chunks
|
|
|
+ WHERE content LIKE '%{_SEARCH_PATTERN}%'
|
|
|
+ """)
|
|
|
+
|
|
|
+ print(f"\n残留旧格式: drugs={remaining_d} chunks={remaining_c}")
|
|
|
+ if remaining_d == 0 and remaining_c == 0:
|
|
|
+ print("✅ 迁移完成")
|
|
|
+ else:
|
|
|
+ print("⚠️ 仍有残留,请手动检查")
|
|
|
+
|
|
|
+ await conn.close()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ asyncio.run(main())
|