Spaces:
Runtime error
Runtime error
Update sync_job.py
Browse files- sync_job.py +52 -58
sync_job.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
| 1 |
import os, threading, time, subprocess, sys, logging, importlib, re
|
| 2 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
# --- 1. 环境自修复 ---
|
| 5 |
try:
|
|
@@ -8,18 +13,12 @@ except ImportError:
|
|
| 8 |
subprocess.check_call([sys.executable, "-m", "pip", "install", "bleach"])
|
| 9 |
import bleach
|
| 10 |
|
| 11 |
-
from fastapi import FastAPI
|
| 12 |
-
import uvicorn
|
| 13 |
-
from datasets import load_dataset
|
| 14 |
-
from supabase import create_client
|
| 15 |
-
from bs4 import BeautifulSoup
|
| 16 |
-
|
| 17 |
# 屏蔽无用日志
|
| 18 |
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
|
| 19 |
|
| 20 |
# --- 2. 动态加载配置 ---
|
| 21 |
CONFIG_MODULE_NAME = os.getenv("CONFIG_FILE", "fields_config_plants")
|
| 22 |
-
TASK_KEY = os.getenv("SYNC_TASK", "
|
| 23 |
|
| 24 |
try:
|
| 25 |
cfg_module = importlib.import_module(CONFIG_MODULE_NAME)
|
|
@@ -33,34 +32,50 @@ KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
|
| 33 |
|
| 34 |
# 状态追踪
|
| 35 |
status = {"total_todo": 0, "scanned": 0, "hits": 0, "speed": 0, "task": TASK_KEY}
|
| 36 |
-
app = FastAPI()
|
| 37 |
|
| 38 |
-
# --- 3. 增强版清洗函数
|
| 39 |
def extract_and_clean_v2(html, rule):
|
| 40 |
if not html:
|
| 41 |
return "[Empty:NoHtml]"
|
| 42 |
try:
|
| 43 |
-
# 使用 lxml 提升解析速度和容错
|
| 44 |
soup = BeautifulSoup(html, 'lxml')
|
|
|
|
|
|
|
|
|
|
| 45 |
el = soup.find('span', id=rule["html_id"])
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
if not el:
|
| 48 |
return "[Empty:NoTag]"
|
| 49 |
|
| 50 |
-
#
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
content = el.get_text(separator=" ", strip=True)
|
| 54 |
|
| 55 |
-
# 剔除
|
| 56 |
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."
|
| 57 |
if disclaimer in content:
|
| 58 |
content = content.replace(disclaimer, "").strip()
|
| 59 |
|
| 60 |
-
# 清理
|
| 61 |
content = re.sub(r'\s+', ' ', content)
|
| 62 |
else:
|
| 63 |
-
#
|
| 64 |
content = bleach.clean(str(el), tags=['p', 'br', 'b', 'i'], strip=True)
|
| 65 |
|
| 66 |
return content if content.strip() else "[Empty:Blank]"
|
|
@@ -76,25 +91,19 @@ def run_sync_logic():
|
|
| 76 |
print(f"❌ Supabase 客户端初始化失败: {e}")
|
| 77 |
return
|
| 78 |
|
| 79 |
-
#
|
| 80 |
if TASK_KEY.lower() == "all":
|
| 81 |
active_tasks = CFG["FIELD_RULES"]
|
| 82 |
-
print(f"🌟 模式: 全字段模式 (All-in-one)")
|
| 83 |
else:
|
| 84 |
-
if TASK_KEY not in CFG["FIELD_RULES"]:
|
| 85 |
-
print(f"❌ 任务 Key '{TASK_KEY}' 不在配置中")
|
| 86 |
-
os._exit(1)
|
| 87 |
active_tasks = {TASK_KEY: CFG["FIELD_RULES"][TASK_KEY]}
|
| 88 |
-
print(f"📡 模式: 单任务模式 ({TASK_KEY})")
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
try:
|
| 93 |
-
# 注意:如果数据量极大,建议这里加 limit 或分批 select
|
| 94 |
res = client.table(CFG["TARGET_TABLE"]).select("*").execute()
|
| 95 |
db_rows = res.data if res.data else []
|
| 96 |
except Exception as e:
|
| 97 |
-
print(f"❌
|
| 98 |
return
|
| 99 |
|
| 100 |
todo_ids = set()
|
|
@@ -104,8 +113,8 @@ def run_sync_logic():
|
|
| 104 |
p_id = r[CFG["TARGET_ID_COL"]]
|
| 105 |
needs_update = False
|
| 106 |
for t_key, rule in active_tasks.items():
|
| 107 |
-
# 检查字段是否为空 (None 或 空字符串)
|
| 108 |
val = r.get(rule["col"])
|
|
|
|
| 109 |
if val is None or val == "" or str(val).startswith("[Empty:"):
|
| 110 |
needs_update = True
|
| 111 |
break
|
|
@@ -114,17 +123,13 @@ def run_sync_logic():
|
|
| 114 |
db_lookup[p_id] = r
|
| 115 |
|
| 116 |
status["total_todo"] = len(todo_ids)
|
| 117 |
-
|
| 118 |
-
print(f"\n" + "="*50)
|
| 119 |
-
print(f"📊 待处理总数: {status['total_todo']:,} 条")
|
| 120 |
-
print("="*50 + "\n")
|
| 121 |
|
| 122 |
if not todo_ids:
|
| 123 |
-
print("✅
|
| 124 |
return
|
| 125 |
|
| 126 |
-
#
|
| 127 |
-
print(f"📡 正在连接数据湖 {CFG['REPO_ID']}...")
|
| 128 |
ds = load_dataset(CFG["REPO_ID"], data_dir=CFG["DATA_DIR"], split="train", streaming=True)
|
| 129 |
start_time = time.time()
|
| 130 |
|
|
@@ -139,49 +144,38 @@ def run_sync_logic():
|
|
| 139 |
|
| 140 |
for t_key, rule in active_tasks.items():
|
| 141 |
col_name = rule["col"]
|
| 142 |
-
# 只有在 DB 中该字段确实为空时才重新清洗
|
| 143 |
db_val = current_db_row.get(col_name)
|
| 144 |
if db_val is None or db_val == "" or str(db_val).startswith("[Empty:"):
|
| 145 |
update_payload[col_name] = extract_and_clean_v2(html, rule)
|
| 146 |
|
| 147 |
-
# --- 原子更新 ---
|
| 148 |
if update_payload:
|
| 149 |
try:
|
| 150 |
update_payload["updated_at"] = "now()"
|
| 151 |
client.table(CFG["TARGET_TABLE"]).update(update_payload).eq(CFG["TARGET_ID_COL"], p_id).execute()
|
| 152 |
status["hits"] += 1
|
| 153 |
-
except Exception
|
| 154 |
-
print(f"\n⚠️ 提交失败 (ID: {p_id}) | {str(e)[:100]}")
|
| 155 |
-
time.sleep(2)
|
| 156 |
continue
|
| 157 |
|
| 158 |
-
|
| 159 |
-
if status["hits"] > 0 and (status["hits"] % 20 == 0 or status["hits"] == status["total_todo"]):
|
| 160 |
elapsed = time.time() - start_time
|
| 161 |
-
status["speed"] = status["hits"] / elapsed
|
| 162 |
-
print(f"🎯 进度: {status['hits']}/{status['total_todo']} | 速度: {status['speed']:.1f}条/秒")
|
| 163 |
|
| 164 |
-
# 完成自毁逻辑
|
| 165 |
if status["hits"] >= status["total_todo"]:
|
| 166 |
-
print(f"
|
| 167 |
-
time.sleep(5)
|
| 168 |
os._exit(0)
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
@app.on_event("startup")
|
| 174 |
-
def startup():
|
| 175 |
threading.Timer(1.0, run_sync_logic).start()
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
@app.get("/")
|
| 178 |
def health():
|
| 179 |
-
return {
|
| 180 |
-
"status": "running",
|
| 181 |
-
"progress": f"{status['hits']}/{status['total_todo']}",
|
| 182 |
-
"scanned": status["scanned"],
|
| 183 |
-
"speed": f"{status['speed']:.2f} rows/sec"
|
| 184 |
-
}
|
| 185 |
|
| 186 |
if __name__ == "__main__":
|
| 187 |
uvicorn.run(app, host="0.0.0.0", port=7860, access_log=False)
|
|
|
|
| 1 |
import os, threading, time, subprocess, sys, logging, importlib, re
|
| 2 |
+
from contextlib import asynccontextmanager
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
import uvicorn
|
| 5 |
+
from datasets import load_dataset
|
| 6 |
+
from supabase import create_client
|
| 7 |
+
from bs4 import BeautifulSoup
|
| 8 |
|
| 9 |
# --- 1. 环境自修复 ---
|
| 10 |
try:
|
|
|
|
| 13 |
subprocess.check_call([sys.executable, "-m", "pip", "install", "bleach"])
|
| 14 |
import bleach
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# 屏蔽无用日志
|
| 17 |
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
|
| 18 |
|
| 19 |
# --- 2. 动态加载配置 ---
|
| 20 |
CONFIG_MODULE_NAME = os.getenv("CONFIG_FILE", "fields_config_plants")
|
| 21 |
+
TASK_KEY = os.getenv("SYNC_TASK", "all") # 默认跑全量,收割 title
|
| 22 |
|
| 23 |
try:
|
| 24 |
cfg_module = importlib.import_module(CONFIG_MODULE_NAME)
|
|
|
|
| 32 |
|
| 33 |
# 状态追踪
|
| 34 |
status = {"total_todo": 0, "scanned": 0, "hits": 0, "speed": 0, "task": TASK_KEY}
|
|
|
|
| 35 |
|
| 36 |
+
# --- 3. 增强版清洗函数 ---
|
| 37 |
def extract_and_clean_v2(html, rule):
|
| 38 |
if not html:
|
| 39 |
return "[Empty:NoHtml]"
|
| 40 |
try:
|
|
|
|
| 41 |
soup = BeautifulSoup(html, 'lxml')
|
| 42 |
+
col_name = rule["col"]
|
| 43 |
+
|
| 44 |
+
# A. 正常寻找配置中的 ID
|
| 45 |
el = soup.find('span', id=rule["html_id"])
|
| 46 |
|
| 47 |
+
# B. 针对 Title 的专项保底逻辑 (处理 id="Head1" 问题)
|
| 48 |
+
if not el and col_name == "title":
|
| 49 |
+
title_tag = soup.find('title')
|
| 50 |
+
if title_tag:
|
| 51 |
+
raw_t = title_tag.get_text()
|
| 52 |
+
# 剔除 PFAF 常见后缀
|
| 53 |
+
return raw_t.replace("PFAF Plant Database", "").strip()
|
| 54 |
+
|
| 55 |
+
# C. 针对 Common Name 的拼写纠错保底
|
| 56 |
+
if not el and col_name == "common_name":
|
| 57 |
+
# 尝试 PFAF 常见的拼写错误 ID
|
| 58 |
+
el = soup.find('span', id="ContentPlaceHolder1_lblCommanName")
|
| 59 |
+
|
| 60 |
if not el:
|
| 61 |
return "[Empty:NoTag]"
|
| 62 |
|
| 63 |
+
# D. 深度内容提取逻辑
|
| 64 |
+
# 药用、食用、俗名等字段强制穿透所有内嵌标签
|
| 65 |
+
force_text_fields = ['medicinal_uses', 'edible_uses', 'common_name', 'title']
|
| 66 |
+
|
| 67 |
+
if col_name in force_text_fields or not rule.get("use_bleach"):
|
| 68 |
content = el.get_text(separator=" ", strip=True)
|
| 69 |
|
| 70 |
+
# 剔除药用免责声明
|
| 71 |
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."
|
| 72 |
if disclaimer in content:
|
| 73 |
content = content.replace(disclaimer, "").strip()
|
| 74 |
|
| 75 |
+
# 清理多余空格
|
| 76 |
content = re.sub(r'\s+', ' ', content)
|
| 77 |
else:
|
| 78 |
+
# 需要保留基础格式的字段 (如栽培详情)
|
| 79 |
content = bleach.clean(str(el), tags=['p', 'br', 'b', 'i'], strip=True)
|
| 80 |
|
| 81 |
return content if content.strip() else "[Empty:Blank]"
|
|
|
|
| 91 |
print(f"❌ Supabase 客户端初始化失败: {e}")
|
| 92 |
return
|
| 93 |
|
| 94 |
+
# 确定任务
|
| 95 |
if TASK_KEY.lower() == "all":
|
| 96 |
active_tasks = CFG["FIELD_RULES"]
|
|
|
|
| 97 |
else:
|
|
|
|
|
|
|
|
|
|
| 98 |
active_tasks = {TASK_KEY: CFG["FIELD_RULES"][TASK_KEY]}
|
|
|
|
| 99 |
|
| 100 |
+
print(f"🔍 模式: {TASK_KEY} | 正在检索缺失项...")
|
| 101 |
+
|
| 102 |
try:
|
|
|
|
| 103 |
res = client.table(CFG["TARGET_TABLE"]).select("*").execute()
|
| 104 |
db_rows = res.data if res.data else []
|
| 105 |
except Exception as e:
|
| 106 |
+
print(f"❌ 读取数据库失败: {e}")
|
| 107 |
return
|
| 108 |
|
| 109 |
todo_ids = set()
|
|
|
|
| 113 |
p_id = r[CFG["TARGET_ID_COL"]]
|
| 114 |
needs_update = False
|
| 115 |
for t_key, rule in active_tasks.items():
|
|
|
|
| 116 |
val = r.get(rule["col"])
|
| 117 |
+
# 识别 Null, 空值, 或之前的 [Empty:...] 标记
|
| 118 |
if val is None or val == "" or str(val).startswith("[Empty:"):
|
| 119 |
needs_update = True
|
| 120 |
break
|
|
|
|
| 123 |
db_lookup[p_id] = r
|
| 124 |
|
| 125 |
status["total_todo"] = len(todo_ids)
|
| 126 |
+
print(f"📊 待修复/待补齐: {status['total_todo']:,} 条")
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
if not todo_ids:
|
| 129 |
+
print("✅ 状态完美,无需更新。")
|
| 130 |
return
|
| 131 |
|
| 132 |
+
# 连接湖泊
|
|
|
|
| 133 |
ds = load_dataset(CFG["REPO_ID"], data_dir=CFG["DATA_DIR"], split="train", streaming=True)
|
| 134 |
start_time = time.time()
|
| 135 |
|
|
|
|
| 144 |
|
| 145 |
for t_key, rule in active_tasks.items():
|
| 146 |
col_name = rule["col"]
|
|
|
|
| 147 |
db_val = current_db_row.get(col_name)
|
| 148 |
if db_val is None or db_val == "" or str(db_val).startswith("[Empty:"):
|
| 149 |
update_payload[col_name] = extract_and_clean_v2(html, rule)
|
| 150 |
|
|
|
|
| 151 |
if update_payload:
|
| 152 |
try:
|
| 153 |
update_payload["updated_at"] = "now()"
|
| 154 |
client.table(CFG["TARGET_TABLE"]).update(update_payload).eq(CFG["TARGET_ID_COL"], p_id).execute()
|
| 155 |
status["hits"] += 1
|
| 156 |
+
except Exception:
|
|
|
|
|
|
|
| 157 |
continue
|
| 158 |
|
| 159 |
+
if status["hits"] > 0 and status["hits"] % 50 == 0:
|
|
|
|
| 160 |
elapsed = time.time() - start_time
|
| 161 |
+
status["speed"] = status["hits"] / elapsed
|
| 162 |
+
print(f"🎯 修复进度: {status['hits']}/{status['total_todo']} | 速度: {status['speed']:.1f}条/秒")
|
| 163 |
|
|
|
|
| 164 |
if status["hits"] >= status["total_todo"]:
|
| 165 |
+
print(f"🎉 修复完成!共处理 {status['hits']} 条。")
|
|
|
|
| 166 |
os._exit(0)
|
| 167 |
|
| 168 |
+
# --- 5. FastAPI 入口 ---
|
| 169 |
+
@asynccontextmanager
|
| 170 |
+
async def lifespan(app: FastAPI):
|
|
|
|
|
|
|
| 171 |
threading.Timer(1.0, run_sync_logic).start()
|
| 172 |
+
yield
|
| 173 |
+
|
| 174 |
+
app = FastAPI(lifespan=lifespan)
|
| 175 |
|
| 176 |
@app.get("/")
|
| 177 |
def health():
|
| 178 |
+
return {"status": "running", "progress": f"{status['hits']}/{status['total_todo']}", "task": status["task"]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
if __name__ == "__main__":
|
| 181 |
uvicorn.run(app, host="0.0.0.0", port=7860, access_log=False)
|