import os, threading, time, subprocess, sys, logging, importlib, re from contextlib import asynccontextmanager from fastapi import FastAPI import uvicorn from datasets import load_dataset from supabase import create_client from bs4 import BeautifulSoup # --- 1. 环境自修复 --- try: import bleach except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "bleach"]) import bleach # 屏蔽无用日志 logging.getLogger("uvicorn.access").setLevel(logging.ERROR) # --- 2. 动态加载配置 --- CONFIG_MODULE_NAME = os.getenv("CONFIG_FILE", "fields_config_plants") TASK_KEY = os.getenv("SYNC_TASK", "all") # 默认跑全量,收割 title try: cfg_module = importlib.import_module(CONFIG_MODULE_NAME) CFG = cfg_module.CONFIG except Exception as e: print(f"❌ 配置加载失败: {e}") sys.exit(1) URL = os.getenv("SUPABASE_URL") KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") # 状态追踪 status = {"total_todo": 0, "scanned": 0, "hits": 0, "speed": 0, "task": TASK_KEY} # --- 3. 增强版清洗函数 --- def extract_and_clean_v2(html, rule): if not html: return "[Empty:NoHtml]" try: soup = BeautifulSoup(html, 'lxml') col_name = rule["col"] # A. 正常寻找配置中的 ID el = soup.find('span', id=rule["html_id"]) # B. 针对 Title 的专项保底逻辑 (处理 id="Head1" 问题) if not el and col_name == "title": title_tag = soup.find('title') if title_tag: raw_t = title_tag.get_text() # 剔除 PFAF 常见后缀 return raw_t.replace("PFAF Plant Database", "").strip() # C. 针对 Common Name 的拼写纠错保底 if not el and col_name == "common_name": # 尝试 PFAF 常见的拼写错误 ID el = soup.find('span', id="ContentPlaceHolder1_lblCommanName") if not el: return "[Empty:NoTag]" # D. 深度内容提取逻辑 # 药用、食用、俗名等字段强制穿透所有内嵌标签 force_text_fields = ['medicinal_uses', 'edible_uses', 'common_name', 'title'] if col_name in force_text_fields or not rule.get("use_bleach"): content = el.get_text(separator=" ", strip=True) # 剔除药用免责声明 disclaimer = "Plants For A Future can not take any responsibility for any adverse effects from the use of plants. Always seek advice from a professional before using a plant medicinally." if disclaimer in content: content = content.replace(disclaimer, "").strip() # 清理多余空格 content = re.sub(r'\s+', ' ', content) else: # 需要保留基础格式的字段 (如栽培详情) content = bleach.clean(str(el), tags=['p', 'br', 'b', 'i'], strip=True) return content if content.strip() else "[Empty:Blank]" except Exception as e: return f"[Error:{str(e)[:20]}]" # --- 4. 同步主逻辑 --- def run_sync_logic(): global status try: client = create_client(URL, KEY) except Exception as e: print(f"❌ Supabase 客户端初始化失败: {e}") return # 确定任务 if TASK_KEY.lower() == "all": active_tasks = CFG["FIELD_RULES"] else: active_tasks = {TASK_KEY: CFG["FIELD_RULES"][TASK_KEY]} print(f"🔍 模式: {TASK_KEY} | 正在检索缺失项...") try: res = client.table(CFG["TARGET_TABLE"]).select("*").execute() db_rows = res.data if res.data else [] except Exception as e: print(f"❌ 读取数据库失败: {e}") return todo_ids = set() db_lookup = {} for r in db_rows: p_id = r[CFG["TARGET_ID_COL"]] needs_update = False for t_key, rule in active_tasks.items(): val = r.get(rule["col"]) # 识别 Null, 空值, 或之前的 [Empty:...] 标记 if val is None or val == "" or str(val).startswith("[Empty:"): needs_update = True break if needs_update: todo_ids.add(p_id) db_lookup[p_id] = r status["total_todo"] = len(todo_ids) print(f"📊 待修复/待补齐: {status['total_todo']:,} 条") if not todo_ids: print("✅ 状态完美,无需更新。") return # 连接湖泊 ds = load_dataset(CFG["REPO_ID"], data_dir=CFG["DATA_DIR"], split="train", streaming=True) start_time = time.time() for row in ds: status["scanned"] += 1 p_id = row.get(CFG["TARGET_ID_COL"]) if p_id in todo_ids: update_payload = {} html = row.get('raw_html') current_db_row = db_lookup.get(p_id, {}) for t_key, rule in active_tasks.items(): col_name = rule["col"] db_val = current_db_row.get(col_name) if db_val is None or db_val == "" or str(db_val).startswith("[Empty:"): update_payload[col_name] = extract_and_clean_v2(html, rule) if update_payload: try: update_payload["updated_at"] = "now()" client.table(CFG["TARGET_TABLE"]).update(update_payload).eq(CFG["TARGET_ID_COL"], p_id).execute() status["hits"] += 1 except Exception: continue if status["hits"] > 0 and status["hits"] % 50 == 0: elapsed = time.time() - start_time status["speed"] = status["hits"] / elapsed print(f"🎯 修复进度: {status['hits']}/{status['total_todo']} | 速度: {status['speed']:.1f}条/秒") if status["hits"] >= status["total_todo"]: print(f"🎉 修复完成!共处理 {status['hits']} 条。") os._exit(0) # --- 5. FastAPI 入口 --- @asynccontextmanager async def lifespan(app: FastAPI): threading.Timer(1.0, run_sync_logic).start() yield app = FastAPI(lifespan=lifespan) @app.get("/") def health(): return {"status": "running", "progress": f"{status['hits']}/{status['total_todo']}", "task": status["task"]} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860, access_log=False)