安全加固: - /api/v1/predict 限流改用 get_client_ip 防 X-Forwarded-For 伪造 - 新增 TRUST_PROXY_HEADERS/REQUIRE_ADMIN_AUTH 配置,默认 fail-closed - 生产环境管理接口未配置鉴权时拒绝(503),不再放行 数据管线修复: - injuries IntegrityError 改用 begin_nested(SAVEPOINT)隔离批次 - injuries 空名单 vs 未配置语义区分(has_data 精确标记) - bzzoiro 统计字段映射(shots/possession/cards) + 入库条件放宽 - available_at 语义收紧(回测防泄漏) - team_names NFKD 去变音双重查找 + 补齐变体键 预测系统改进: - multi-agent 全专家失败时 status=degraded 跳过终裁 - agent_weights 独立持久化到 predictions 表 新增迁移: - 0014_predictions_agent_weights.py 新增测试(8个文件): - test_ip_spoofing.py: 限流防伪造 - test_require_admin_fail_closed.py: 生产 fail-closed - test_injuries_integrity_rollback.py: SAVEPOINT 隔离 - test_injuries_slice_semantic.py: 空名单 vs 未配置 - test_available_at.py: 回测防泄漏 - test_bzzoirot_stats.py: 统计字段映射 - test_team_names_normalize.py: NFKD 变体 - test_multi_agent_degraded.py: 全失败 degraded - test_agent_weights_persist.py: 权重持久化
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""队名归一化:各源队名 → 统一规范名。
|
|
|
|
迁移自旧项目 app/data/team_names.py。
|
|
|
|
NFKD 归一化会剥离变音符号(ü→u),导致「Bayern München」与「Bayern Munich」
|
|
映射到不同规范名。修复:双重查找(原始名 + NFKD 名) + 补齐常见变体键。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import unicodedata
|
|
|
|
NORMALIZE_MAP = {
|
|
# ---- 英超 ----
|
|
"Man City": "Manchester City",
|
|
"Man United": "Manchester United",
|
|
"Newcastle": "Newcastle United",
|
|
"Nott'm Forest": "Nottingham Forest",
|
|
"Wolves": "Wolverhampton Wanderers",
|
|
"West Ham": "West Ham United",
|
|
"Tottenham": "Tottenham Hotspur",
|
|
"Spurs": "Tottenham Hotspur",
|
|
"Brighton": "Brighton and Hove Albion",
|
|
"West Brom": "West Bromwich Albion",
|
|
"Stoke": "Stoke City",
|
|
"Huddersfield": "Huddersfield Town",
|
|
"Swansea": "Swansea City",
|
|
"Hull": "Hull City",
|
|
"Cardiff": "Cardiff City",
|
|
"Luton": "Luton Town",
|
|
"Norwich": "Norwich City",
|
|
"Bournemouth": "AFC Bournemouth",
|
|
"Ipswich": "Ipswich Town",
|
|
"Leicester": "Leicester City",
|
|
"Leeds": "Leeds United",
|
|
"Sheffield United": "Sheffield United",
|
|
"Southampton": "Southampton",
|
|
"Arsenal": "Arsenal",
|
|
"Aston Villa": "Aston Villa",
|
|
"Brentford": "Brentford",
|
|
"Chelsea": "Chelsea",
|
|
"Crystal Palace": "Crystal Palace",
|
|
"Everton": "Everton",
|
|
"Fulham": "Fulham",
|
|
"Liverpool": "Liverpool",
|
|
# ---- 西甲 ----
|
|
"Atletico Madrid": "Atlético Madrid",
|
|
"Athletic Club": "Athletic Club",
|
|
"Real Betis": "Real Betis",
|
|
"Celta Vigo": "Celta Vigo",
|
|
"Deportivo Alaves": "Deportivo Alavés",
|
|
"Girona": "Girona",
|
|
"Las Palmas": "Las Palmas",
|
|
"Leganes": "Leganés",
|
|
"Mallorca": "Mallorca",
|
|
"Osasuna": "Osasuna",
|
|
"Rayo Vallecano": "Rayo Vallecano",
|
|
"Real Sociedad": "Real Sociedad",
|
|
"Sevilla": "Sevilla",
|
|
"Valencia": "Valencia",
|
|
"Villarreal": "Villarreal",
|
|
"Espanyol": "Espanyol",
|
|
"Getafe": "Getafe",
|
|
"Real Madrid": "Real Madrid",
|
|
"Barcelona": "Barcelona",
|
|
# ---- 德甲 ----
|
|
"Bayern Munich": "Bayern München",
|
|
"Bayern Munchen": "Bayern München", # NFKD stripped variant
|
|
"Bayern München": "Bayern München", # canonical with umlaut (direct hit)
|
|
"FC Koln": "FC Köln",
|
|
"FC Köln": "FC Köln",
|
|
"RB Leipzig": "RB Leipzig",
|
|
"Borussia Dortmund": "Borussia Dortmund",
|
|
"Borussia M'gladbach": "Borussia Mönchengladbach",
|
|
"Borussia Monchengladbach": "Borussia Mönchengladbach", # NFKD stripped
|
|
"Borussia Mönchengladbach": "Borussia Mönchengladbach", # canonical
|
|
"Bayer Leverkusen": "Bayer Leverkusen",
|
|
"Eintracht Frankfurt": "Eintracht Frankfurt",
|
|
"VfB Stuttgart": "VfB Stuttgart",
|
|
"VfL Wolfsburg": "VfL Wolfsburg",
|
|
"Werder Bremen": "Werder Bremen",
|
|
"TSG Hoffenheim": "TSG Hoffenheim",
|
|
"SC Freiburg": "SC Freiburg",
|
|
"Union Berlin": "Union Berlin",
|
|
"Mainz": "Mainz 05",
|
|
"Augsburg": "FC Augsburg",
|
|
"Bochum": "VfL Bochum",
|
|
"Heidenheim": "1. FC Heidenheim",
|
|
"St. Pauli": "FC St. Pauli",
|
|
"Holstein Kiel": "Holstein Kiel",
|
|
# ---- 意甲 ----
|
|
"AC Milan": "AC Milan",
|
|
"Inter": "Inter Milan",
|
|
"Inter Milan": "Inter Milan",
|
|
"Juventus": "Juventus",
|
|
"Napoli": "SSC Napoli",
|
|
"Roma": "AS Roma",
|
|
"Lazio": "Lazio",
|
|
"Atalanta": "Atalanta",
|
|
"Fiorentina": "ACF Fiorentina",
|
|
"Bologna": "Bologna",
|
|
"Torino": "Torino",
|
|
"Monza": "AC Monza",
|
|
"Udinese": "Udinese",
|
|
"Sassuolo": "Sassuolo",
|
|
"Empoli": "Empoli",
|
|
"Cagliari": "Cagliari",
|
|
"Genoa": "Genoa",
|
|
"Lecce": "Lecce",
|
|
"Hellas Verona": "Hellas Verona",
|
|
"Parma": "Parma",
|
|
"Como": "Como",
|
|
"Venezia": "Venezia",
|
|
# ---- 法甲 ----
|
|
"PSG": "Paris Saint-Germain",
|
|
"Paris Saint-Germain": "Paris Saint-Germain",
|
|
"Marseille": "Olympique Marseille",
|
|
"Lyon": "Olympique Lyonnais",
|
|
"Monaco": "AS Monaco",
|
|
"Lille": "Lille OSC",
|
|
"Nice": "OGC Nice",
|
|
"Rennes": "Stade Rennais",
|
|
"Lens": "RC Lens",
|
|
"Strasbourg": "RC Strasbourg",
|
|
"Brest": "Stade Brestois",
|
|
"Nantes": "FC Nantes",
|
|
"Reims": "Stade de Reims",
|
|
"Toulouse": "Toulouse FC",
|
|
"Montpellier": "Montpellier HSC",
|
|
"Le Havre": "Le Havre AC",
|
|
"Lorient": "FC Lorient",
|
|
"Saint-Etienne": "AS Saint-Étienne",
|
|
"Saint-Etienne": "AS Saint-Étienne", # NFKD stripped (ê → e)
|
|
"AS Saint-Étienne": "AS Saint-Étienne", # canonical prefix
|
|
"AS Saint-Etienne": "AS Saint-Étienne", # NFKD stripped with prefix
|
|
"Angers": "Angers SCO",
|
|
"Auxerre": "AJ Auxerre",
|
|
"Leganes": "Leganés",
|
|
}
|
|
|
|
|
|
def normalize(name: str) -> str:
|
|
"""队名归一化:变音符号变体 → 统一规范名。
|
|
|
|
双重查找策略:
|
|
1. 原始名(带变音)直接查表
|
|
2. NFKD 去变音后再查表
|
|
|
|
确保「Bayern München」「Bayern Munchen」「Bayern Munich」
|
|
都映射到同一规范名「Bayern München」。
|
|
"""
|
|
if not name:
|
|
return ""
|
|
|
|
# 1. 原始名直接查表(保留变音变体)
|
|
stripped = name.strip()
|
|
if stripped in NORMALIZE_MAP:
|
|
return NORMALIZE_MAP[stripped]
|
|
|
|
# 2. NFKD 去变音后查表
|
|
n = unicodedata.normalize("NFKD", stripped)
|
|
n = "".join(c for c in n if not unicodedata.combining(c))
|
|
n = n.strip()
|
|
return NORMALIZE_MAP.get(n, n)
|