Spaces:
Runtime error
Runtime error
Update bs4_quality_scanner.py
Browse files- bs4_quality_scanner.py +76 -33
bs4_quality_scanner.py
CHANGED
|
@@ -8,24 +8,58 @@ url = os.environ.get("SUPABASE_URL")
|
|
| 8 |
key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
| 9 |
supabase = create_client(url, key)
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
is_empty = not s or s.startswith("[Empty") or s.lower() == "none"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
has_garbage = bool(re.search(r'[ÂÃÅÐÑÒÓÔÕÖר]', s))
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
def run_bs4_audit():
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
'usda_hardiness', 'known_hazards', 'habitats', 'range',
|
| 22 |
-
'physical_characteristics', 'edible_uses', 'medicinal_uses',
|
| 23 |
-
'other_uses', 'cultivation_details', 'propagation'
|
| 24 |
-
]
|
| 25 |
-
|
| 26 |
-
stats = {col: {'filled': 0, 'empty': 0, 'dirty': 0, 'len_sum': 0, 'score_sum': 0} for col in target_columns}
|
| 27 |
|
| 28 |
-
log(f"\n[{time.strftime('%H:%M:%S')}]
|
| 29 |
|
| 30 |
offset = 0
|
| 31 |
limit = 200
|
|
@@ -35,50 +69,59 @@ def run_bs4_audit():
|
|
| 35 |
|
| 36 |
for row in res.data:
|
| 37 |
for col in target_columns:
|
| 38 |
-
is_emp,
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
offset += len(res.data)
|
| 46 |
|
| 47 |
-
# ---
|
| 48 |
-
log("\n" + "="*
|
| 49 |
-
log(f"{'字段名称':<25} | {'填充
|
| 50 |
-
log("-" *
|
| 51 |
|
| 52 |
report_batch = []
|
| 53 |
-
#
|
| 54 |
sorted_cols = sorted(stats.items(), key=lambda x: (x[1]['score_sum']/offset if offset>0 else 0))
|
| 55 |
|
| 56 |
for col, data in sorted_cols:
|
| 57 |
avg_len = int(data['len_sum'] / data['filled']) if data['filled'] > 0 else 0
|
| 58 |
-
|
| 59 |
|
| 60 |
-
log(f"{col:<25} | {data['filled']:<
|
| 61 |
|
|
|
|
| 62 |
report_batch.append({
|
| 63 |
"field_name": col,
|
| 64 |
"total_records": offset,
|
| 65 |
"filled_count": data['filled'],
|
| 66 |
"empty_count": data['empty'],
|
| 67 |
-
"dirty_count": data['
|
| 68 |
"avg_char_count": avg_len,
|
| 69 |
-
"quality_score":
|
| 70 |
"updated_at": "now()"
|
| 71 |
})
|
| 72 |
|
| 73 |
-
# --- 回写数据库 ---
|
| 74 |
supabase.table("bs4_field_quality_report").upsert(report_batch).execute()
|
| 75 |
-
log("="*
|
| 76 |
-
log(f"✅
|
| 77 |
|
| 78 |
if __name__ == "__main__":
|
| 79 |
while True:
|
| 80 |
try:
|
| 81 |
run_bs4_audit()
|
| 82 |
except Exception as e:
|
| 83 |
-
log(f"❌ 运行
|
| 84 |
-
time.sleep(600)
|
|
|
|
| 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},
|
| 16 |
+
'family': {'min': 3, 'max': 50},
|
| 17 |
+
'title': {'min': 10, 'max': 200},
|
| 18 |
+
'usda_hardiness': {'min': 1, 'max': 15},
|
| 19 |
+
'known_hazards': {'min': 4, 'max': 2000},
|
| 20 |
+
'habitats': {'min': 5, 'max': 1000},
|
| 21 |
+
'range': {'min': 5, 'max': 1000},
|
| 22 |
+
'physical_characteristics': {'min': 20, 'max': 5000},
|
| 23 |
+
'edible_uses': {'min': 10, 'max': 5000},
|
| 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 |
+
|
| 35 |
is_empty = not s or s.startswith("[Empty") or s.lower() == "none"
|
| 36 |
+
if is_empty:
|
| 37 |
+
return True, False, 0
|
| 38 |
+
|
| 39 |
+
# 1. 乱码检测
|
| 40 |
has_garbage = bool(re.search(r'[ÂÃÅÐÑÒÓÔÕÖר]', s))
|
| 41 |
+
|
| 42 |
+
# 2. 离群值/异常检测 (Outlier)
|
| 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 |
+
# 结构异常判定:如果包含超过 5 个连续空格,或者包含 HTML 标签残余
|
| 52 |
+
if " " in raw_s or "<div>" in raw_s.lower() or "<span>" 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
|
|
|
|
| 69 |
|
| 70 |
for row in res.data:
|
| 71 |
for col in target_columns:
|
| 72 |
+
is_emp, is_bad, length = analyze_value_pro(col, row.get(col))
|
| 73 |
+
|
| 74 |
+
if is_emp:
|
| 75 |
+
stats[col]['empty'] += 1
|
| 76 |
+
else:
|
| 77 |
+
stats[col]['filled'] += 1
|
| 78 |
+
stats[col]['len_sum'] += length
|
| 79 |
+
if is_bad:
|
| 80 |
+
stats[col]['outlier'] += 1
|
| 81 |
+
|
| 82 |
+
# 评分逻辑优化:离群异常和乱码按 50 分计(警告级),空值 0 分
|
| 83 |
+
if is_emp: score = 0
|
| 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" + "="*95)
|
| 92 |
+
log(f"{'字段名称':<25} | {'填充':<6} | {'异常/离群':<10} | {'空缺':<6} | {'均长':<6} | {'健康分'}")
|
| 93 |
+
log("-" * 95)
|
| 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:
|
| 100 |
avg_len = int(data['len_sum'] / data['filled']) if data['filled'] > 0 else 0
|
| 101 |
+
total_score = round(data['score_sum'] / offset, 2)
|
| 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("="*95)
|
| 119 |
+
log(f"✅ 深度巡检完成。异常记录已标记,下次巡检 10 分钟后...")
|
| 120 |
|
| 121 |
if __name__ == "__main__":
|
| 122 |
while True:
|
| 123 |
try:
|
| 124 |
run_bs4_audit()
|
| 125 |
except Exception as e:
|
| 126 |
+
log(f"❌ 运行报错: {e}")
|
| 127 |
+
time.sleep(600)
|