liuchengsen hace 1 mes
padre
commit
12083b950b
Se han modificado 2 ficheros con 83 adiciones y 10 borrados
  1. 66 0
      data-pipeline/docx_ingest.py
  2. 17 10
      static/index.html

+ 66 - 0
data-pipeline/docx_ingest.py

@@ -35,6 +35,9 @@ PG_USER = os.environ.get("POSTGRES_USER", "postgres")
 PG_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
 DB_URL = f"postgresql+asyncpg://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{PG_DB}"
 
+IMAGES_DIR = os.environ.get("IMAGES_DIR", "/opt/pharmacopoeia-ai/static/images/drugs")
+IMAGE_URL_PREFIX = "/images/drugs/"
+
 QWEN_API_KEY = os.environ.get("QWEN_API_KEY", "")
 EMBEDDING_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
 EMBEDDING_MODEL = "text-embedding-v3"
@@ -90,6 +93,48 @@ def _extract_table_text(table) -> str:
     return "\n".join(rows)
 
 
+def _extract_images(doc, drug_name):
+    """从 DOCX 提取所有图片,保存到 IMAGES_DIR,返回 {image_rId: url} 映射"""
+    os.makedirs(IMAGES_DIR, exist_ok=True)
+    img_map = {}
+    for rId, rel in doc.part.rels.items():
+        if "image" in rel.reltype:
+            ext = rel.target_ref.split('.')[-1]
+            if ext.lower() not in ('jpg', 'jpeg', 'png', 'gif', 'bmp'):
+                ext = 'png'
+            # 用 rId hash 防止重复
+            fname = f"{drug_name}_{rId}.{ext}"
+            fpath = os.path.join(IMAGES_DIR, fname)
+            if not os.path.exists(fpath):
+                with open(fpath, 'wb') as f:
+                    f.write(rel.target_part.blob)
+            img_map[rId] = IMAGE_URL_PREFIX + fname
+    return img_map
+
+
+def _get_paragraph_images(p_element, img_map):
+    """检查段落中是否包含图片,返回对应的 img url 列表"""
+    urls = []
+    for run in p_element:
+        if run.tag == qn("w:r"):
+            for child in run:
+                tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
+                if tag == 'drawing':
+                    # 尝试从 drawing 的子元素中获取 rId
+                    for dchild in child.iter():
+                        if dchild.tag == qn("a:blip"):
+                            embed = dchild.get(qn("r:embed"))
+                            if embed and embed in img_map:
+                                urls.append(img_map[embed])
+                elif tag == 'pict':
+                    for pchild in child.iter():
+                        if pchild.tag.endswith('}imagedata'):
+                            rid = pchild.get(qn("r:id"))
+                            if rid and rid in img_map:
+                                urls.append(img_map[rid])
+    return urls
+
+
 def parse_docx(filepath: str) -> dict | None:
     """
     解析单个 DOCX 文件为药典条目。
@@ -110,6 +155,9 @@ def parse_docx(filepath: str) -> dict | None:
     except Exception:
         return None
 
+    # 提取图片
+    img_map = _extract_images(doc, display_name)
+
     pinyin = ""
     sections = OrderedDict()
     current_section = "正文"
@@ -134,11 +182,15 @@ def parse_docx(filepath: str) -> dict | None:
 
         # --- 段落 ---
         if child.tag != qn("w:p"):
+            tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
+            if tag in ('drawing', 'pict') and in_drug:
+                current_text.append("[图片]")
             continue
 
         # 从段落 XML 中提取文本和样式
         p_text = ""
         p_style = ""
+        has_image = False
         for p_child in child:
             if p_child.tag == qn("w:pPr"):
                 for style_child in p_child:
@@ -150,8 +202,22 @@ def parse_docx(filepath: str) -> dict | None:
                         t = r_child.text
                         if t:
                             p_text += t
+                    # 检测段落内嵌图片
+                    rt = r_child.tag.split('}')[-1] if '}' in r_child.tag else r_child.tag
+                    if rt in ('drawing', 'pict'):
+                        has_image = True
 
         text = p_text.strip()
+        # 内嵌图片的段落
+        if has_image:
+            img_urls = _get_paragraph_images(child, img_map) if not p_text else []
+            if not text and img_urls:
+                img_tag = ' '.join(f'<img src="{u}" style="max-width:100%"/>' for u in img_urls)
+                current_text.append(img_tag)
+                continue
+            elif not text and not img_urls:
+                current_text.append("[图片]")
+                continue
         if not text or text.isspace():
             continue
 

+ 17 - 10
static/index.html

@@ -31,6 +31,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei
 .content strong{color:#111}
 .content ul,.content ol{margin:8px 0;padding-left:18px;line-height:1.8}
 .content li{margin:4px 0}
+.img-placeholder{display:inline-block;padding:8px 16px;background:#f0f4ff;border:1px dashed #b0c4ff;border-radius:6px;color:#888;font-size:13px;margin:4px 0}
 .content h3{font-size:15px;margin:16px 0 8px;color:var(--bk);font-weight:700}
 .content h3.src-section{font-size:14px;color:#555;font-weight:600;margin:12px 0 6px}
 .content .warn{color:var(--rd);font-weight:600}
@@ -335,14 +336,16 @@ function buildSourceTags(){
   for(var i=0;i<sourcesData.length;i++){
     var s=sourcesData[i];
     if(!s.section||s.section==='正文')continue;
-    var name=s.name||'未知药品';
-    if(!groups[name]){groups[name]={sections:[],seen:new Set(),source:s.source||''};order.push(name);}
+    var name=s.name||'未知';
+    var ver=s.source||s.category||'';
+    var gkey=name+'|'+ver;
+    if(!groups[gkey]){groups[gkey]={sections:[],seen:new Set(),name:name,source:ver};order.push(gkey);}
     var key=s.section;
-    if(!groups[name].seen.has(key)){
-      groups[name].seen.add(key);
+    if(!groups[gkey].seen.has(key)){
+      groups[gkey].seen.add(key);
       var excerpt=(s.excerpt||'').replace(/【.+?】/g,'').replace(/来源:.*$/gm,'').trim();
       var showExcerpt=excerpt.length>400?excerpt.substring(0,400)+'...':excerpt;
-      groups[name].sections.push({section:key,excerpt:showExcerpt,source:s.source||''});
+      groups[gkey].sections.push({section:key,excerpt:showExcerpt});
     }
   }
   if(!order.length)return'';
@@ -364,12 +367,16 @@ function buildSourceTags(){
 
 window.loadDrugDetailBySrc=async function(drugName){
   try{
-    var r=await fetch(API_BASE+'/api/v1/drug/search?keyword='+encodeURIComponent(drugName)+'&page_size=5',{headers:authHeaders()});
+    var r=await fetch(API_BASE+'/api/v1/drug/search?keyword='+encodeURIComponent(drugName)+'&page_size=10',{headers:authHeaders()});
     var d=await r.json();var items=d.items||d.data||[];
-    // 精确匹配优先
-    var exact=items.filter(function(x){return x.name===drugName});
-    if(exact.length>0){loadDrugDetail(exact[0].drug_id)}
-    else if(items.length>0){loadDrugDetail(items[0].drug_id)}
+    // 2025年版优先,再精确匹配
+    items.sort(function(a,b){
+      var va=a.source_version||'', vb=b.source_version||'';
+      if(va==='2025年版' && vb!=='2025年版') return -1;
+      if(vb==='2025年版' && va!=='2025年版') return 1;
+      return (a.name===drugName?-1:0) - (b.name===drugName?-1:0);
+    });
+    if(items.length>0){loadDrugDetail(items[0].drug_id)}
     else{alert('未找到药品: '+drugName)}
   }catch(e){}
 };