mengtaoguo commited on
Commit
2d0f472
·
verified ·
1 Parent(s): 865f900

Update sync_job.py

Browse files
Files changed (1) hide show
  1. 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 json
 
 
 
 
 
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", "edible") # 可传入 'all' 或具体任务名
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
- # 核心逻辑:如果字段是 medicinal_uses 或明确关闭了 bleach
51
- if rule["col"] == "medicinal_uses" or not rule.get("use_bleach"):
52
- # 穿透所有 <a>, <i>, <br> 标签,获取完整纯文本
 
 
53
  content = el.get_text(separator=" ", strip=True)
54
 
55
- # 剔除 PFAF 特有的长免责声明(避免每个条目都带这段废话)
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
- # 清理因 separator 产生的多余空格
61
  content = re.sub(r'\s+', ' ', content)
62
  else:
63
- # 原有的 HTML 片段保留逻辑
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
- # A. 确定任务范围
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
- # B. 智能化获取待处理名单
91
- print("🔍 正在检索数据库缺失项...")
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"❌ 无法读取数据库: {e}")
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
- # C. 流式连接数据
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 as e:
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 if elapsed > 0 else 0
162
- print(f"🎯 进度: {status['hits']}/{status['total_todo']} | 速度: {status['speed']:.1f}条/秒")
163
 
164
- # 完成自毁逻辑
165
  if status["hits"] >= status["total_todo"]:
166
- print(f"\n🎉 任务圆满完成!已修复/补齐 {status['hits']} 条记录。")
167
- time.sleep(5)
168
  os._exit(0)
169
 
170
- elif status["scanned"] % 1000 == 0:
171
- print(f"📡 已扫描湖泊数据: {status['scanned']:,} 条...")
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)