Spaces:
Runtime error
Runtime error
Update bs4_quality_scanner.py
Browse files- bs4_quality_scanner.py +29 -28
bs4_quality_scanner.py
CHANGED
|
@@ -8,8 +8,7 @@ url = os.environ.get("SUPABASE_URL")
|
|
| 8 |
key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
| 9 |
supabase = create_client(url, key)
|
| 10 |
|
| 11 |
-
# ---
|
| 12 |
-
# min_len: 低于此长度疑似抓取不全; max_len: 高于此长度疑似抓错位置或吞了后面内容
|
| 13 |
FIELD_RULES = {
|
| 14 |
'latin_name': {'min': 5, 'max': 80},
|
| 15 |
'common_name': {'min': 2, 'max': 100},
|
|
@@ -24,11 +23,17 @@ FIELD_RULES = {
|
|
| 24 |
'medicinal_uses': {'min': 10, 'max': 5000},
|
| 25 |
'other_uses': {'min': 10, 'max': 5000},
|
| 26 |
'cultivation_details': {'min': 20, 'max': 8000},
|
| 27 |
-
'propagation': {'min': 20, 'max': 4000}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
def analyze_value_pro(field_name, val):
|
| 31 |
-
"""
|
| 32 |
raw_s = str(val or "")
|
| 33 |
s = raw_s.strip()
|
| 34 |
|
|
@@ -39,31 +44,30 @@ def analyze_value_pro(field_name, val):
|
|
| 39 |
# 1. 乱码检测
|
| 40 |
has_garbage = bool(re.search(r'[ÂÃÅÐÑÒÓÔÕÖר]', s))
|
| 41 |
|
| 42 |
-
# 2. 离群值
|
| 43 |
is_outlier = False
|
| 44 |
length = len(s)
|
| 45 |
|
| 46 |
if field_name in FIELD_RULES:
|
| 47 |
rule = FIELD_RULES[field_name]
|
| 48 |
-
# 长度异常判定
|
| 49 |
if length < rule['min'] or length > rule['max']:
|
| 50 |
is_outlier = True
|
| 51 |
-
# 结构异常判定:
|
| 52 |
-
if " " in raw_s or "<div>" in raw_s.lower()
|
| 53 |
is_outlier = True
|
| 54 |
|
| 55 |
return False, (has_garbage or is_outlier), length
|
| 56 |
|
| 57 |
def run_bs4_audit():
|
| 58 |
target_columns = list(FIELD_RULES.keys())
|
| 59 |
-
# 增加 outlier 统计项
|
| 60 |
stats = {col: {'filled': 0, 'empty': 0, 'outlier': 0, 'len_sum': 0, 'score_sum': 0} for col in target_columns}
|
| 61 |
|
| 62 |
-
log(f"\n[{time.strftime('%H:%M:%S')}] 🛡️ 启动
|
| 63 |
|
| 64 |
offset = 0
|
| 65 |
limit = 200
|
| 66 |
while True:
|
|
|
|
| 67 |
res = supabase.table("bs4_plants").select(",".join(target_columns)).range(offset, offset + limit - 1).execute()
|
| 68 |
if not res.data: break
|
| 69 |
|
|
@@ -79,21 +83,18 @@ def run_bs4_audit():
|
|
| 79 |
if is_bad:
|
| 80 |
stats[col]['outlier'] += 1
|
| 81 |
|
| 82 |
-
# 评分
|
| 83 |
-
if is_emp
|
| 84 |
-
elif is_bad: score = 50
|
| 85 |
-
else: score = 100
|
| 86 |
stats[col]['score_sum'] += score
|
| 87 |
|
| 88 |
offset += len(res.data)
|
| 89 |
|
| 90 |
-
# ---
|
| 91 |
-
log("\n" + "="*
|
| 92 |
log(f"{'字段名称':<25} | {'填充':<6} | {'异常/离群':<10} | {'空缺':<6} | {'均长':<6} | {'健康分'}")
|
| 93 |
-
log("-" *
|
| 94 |
|
| 95 |
report_batch = []
|
| 96 |
-
# 按健康分从低到高排序
|
| 97 |
sorted_cols = sorted(stats.items(), key=lambda x: (x[1]['score_sum']/offset if offset>0 else 0))
|
| 98 |
|
| 99 |
for col, data in sorted_cols:
|
|
@@ -102,26 +103,26 @@ def run_bs4_audit():
|
|
| 102 |
|
| 103 |
log(f"{col:<25} | {data['filled']:<6} | {data['outlier']:<10} | {data['empty']:<6} | {avg_len:<6} | {total_score}")
|
| 104 |
|
| 105 |
-
# 为了兼容你之前的数据库表,我们将 outlier 存入原本的 dirty_count 字段,或者你可以扩充字段
|
| 106 |
report_batch.append({
|
| 107 |
"field_name": col,
|
| 108 |
"total_records": offset,
|
| 109 |
"filled_count": data['filled'],
|
| 110 |
"empty_count": data['empty'],
|
| 111 |
-
"dirty_count": data['outlier'],
|
| 112 |
"avg_char_count": avg_len,
|
| 113 |
"quality_score": total_score,
|
| 114 |
"updated_at": "now()"
|
| 115 |
})
|
| 116 |
|
|
|
|
| 117 |
supabase.table("bs4_field_quality_report").upsert(report_batch).execute()
|
| 118 |
-
log("="*
|
| 119 |
-
log(f"✅
|
| 120 |
|
| 121 |
if __name__ == "__main__":
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 8 |
key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
| 9 |
supabase = create_client(url, key)
|
| 10 |
|
| 11 |
+
# --- 扩展后的字段规则字典 (同步 JSON 补齐字段) ---
|
|
|
|
| 12 |
FIELD_RULES = {
|
| 13 |
'latin_name': {'min': 5, 'max': 80},
|
| 14 |
'common_name': {'min': 2, 'max': 100},
|
|
|
|
| 23 |
'medicinal_uses': {'min': 10, 'max': 5000},
|
| 24 |
'other_uses': {'min': 10, 'max': 5000},
|
| 25 |
'cultivation_details': {'min': 20, 'max': 8000},
|
| 26 |
+
'propagation': {'min': 20, 'max': 4000},
|
| 27 |
+
# --- 新增补齐字段 ---
|
| 28 |
+
'weed_potential': {'min': 2, 'max': 50},
|
| 29 |
+
'found_in': {'min': 5, 'max': 5000},
|
| 30 |
+
'conservation_status': {'min': 5, 'max': 500},
|
| 31 |
+
'special_uses': {'min': 5, 'max': 1000},
|
| 32 |
+
'author': {'min': 1, 'max': 50}
|
| 33 |
}
|
| 34 |
|
| 35 |
def analyze_value_pro(field_name, val):
|
| 36 |
+
"""深度检测逻辑"""
|
| 37 |
raw_s = str(val or "")
|
| 38 |
s = raw_s.strip()
|
| 39 |
|
|
|
|
| 44 |
# 1. 乱码检测
|
| 45 |
has_garbage = bool(re.search(r'[ÂÃÅÐÑÒÓÔÕÖר]', s))
|
| 46 |
|
| 47 |
+
# 2. 离群值检测
|
| 48 |
is_outlier = False
|
| 49 |
length = len(s)
|
| 50 |
|
| 51 |
if field_name in FIELD_RULES:
|
| 52 |
rule = FIELD_RULES[field_name]
|
|
|
|
| 53 |
if length < rule['min'] or length > rule['max']:
|
| 54 |
is_outlier = True
|
| 55 |
+
# 结构异常判定:HTML残留或异常空白
|
| 56 |
+
if " " in raw_s or "<div>" in raw_s.lower():
|
| 57 |
is_outlier = True
|
| 58 |
|
| 59 |
return False, (has_garbage or is_outlier), length
|
| 60 |
|
| 61 |
def run_bs4_audit():
|
| 62 |
target_columns = list(FIELD_RULES.keys())
|
|
|
|
| 63 |
stats = {col: {'filled': 0, 'empty': 0, 'outlier': 0, 'len_sum': 0, 'score_sum': 0} for col in target_columns}
|
| 64 |
|
| 65 |
+
log(f"\n[{time.strftime('%H:%M:%S')}] 🛡️ 启动全字段深度扫描 (含新增字段)...")
|
| 66 |
|
| 67 |
offset = 0
|
| 68 |
limit = 200
|
| 69 |
while True:
|
| 70 |
+
# 动态获取字段
|
| 71 |
res = supabase.table("bs4_plants").select(",".join(target_columns)).range(offset, offset + limit - 1).execute()
|
| 72 |
if not res.data: break
|
| 73 |
|
|
|
|
| 83 |
if is_bad:
|
| 84 |
stats[col]['outlier'] += 1
|
| 85 |
|
| 86 |
+
# 质量评分
|
| 87 |
+
score = 0 if is_emp else (50 if is_bad else 100)
|
|
|
|
|
|
|
| 88 |
stats[col]['score_sum'] += score
|
| 89 |
|
| 90 |
offset += len(res.data)
|
| 91 |
|
| 92 |
+
# --- 输出可视化表格 ---
|
| 93 |
+
log("\n" + "="*100)
|
| 94 |
log(f"{'字段名称':<25} | {'填充':<6} | {'异常/离群':<10} | {'空缺':<6} | {'均长':<6} | {'健康分'}")
|
| 95 |
+
log("-" * 100)
|
| 96 |
|
| 97 |
report_batch = []
|
|
|
|
| 98 |
sorted_cols = sorted(stats.items(), key=lambda x: (x[1]['score_sum']/offset if offset>0 else 0))
|
| 99 |
|
| 100 |
for col, data in sorted_cols:
|
|
|
|
| 103 |
|
| 104 |
log(f"{col:<25} | {data['filled']:<6} | {data['outlier']:<10} | {data['empty']:<6} | {avg_len:<6} | {total_score}")
|
| 105 |
|
|
|
|
| 106 |
report_batch.append({
|
| 107 |
"field_name": col,
|
| 108 |
"total_records": offset,
|
| 109 |
"filled_count": data['filled'],
|
| 110 |
"empty_count": data['empty'],
|
| 111 |
+
"dirty_count": data['outlier'],
|
| 112 |
"avg_char_count": avg_len,
|
| 113 |
"quality_score": total_score,
|
| 114 |
"updated_at": "now()"
|
| 115 |
})
|
| 116 |
|
| 117 |
+
# 回写到统计表
|
| 118 |
supabase.table("bs4_field_quality_report").upsert(report_batch).execute()
|
| 119 |
+
log("="*100)
|
| 120 |
+
log(f"✅ 巡检完成。共处理 {offset} 条记录。")
|
| 121 |
|
| 122 |
if __name__ == "__main__":
|
| 123 |
+
# 保持之前商定的逻辑:启动时运行一遍。
|
| 124 |
+
# 如果想手动触发,重启 Space 即可。
|
| 125 |
+
try:
|
| 126 |
+
run_bs4_audit()
|
| 127 |
+
except Exception as e:
|
| 128 |
+
log(f"❌ 运行报错: {e}")
|