梧桐精舍
东湖沙湾论彩 · 少量开放,精准服务

首页 / 盘口棱镜 / 文章

盘口规律雷达 联赛排名

2026-11-05 16:27:36 · 盘口棱镜

联赛排名显示 · UI 说明文档

版本:v1.0 · 更新日期:2026-10-23
对应当前实现:standings.py + gui.py + scanner.py + radar_publish.py

一、功能概述

盘口规律雷达在比赛卡片、详情页、JSON 输出中,为主客队名追加该队在所属联赛的当前排名。显示格式对齐竞彩网的 [澳超9] 风格,帮助用户一眼识别两队实力层级差距。

修改前:布里斯班 vs 珀斯
修改后:布里斯班[澳超11] vs 珀斯[澳超8]

二、UI 显示位置

2.1 卡片列表(主界面)

文件:gui.py · 第 1168–1169 行

每场比赛卡片的队名标签由原来的纯队名改为带排名格式。使用 standings.rank_label() 统一生成。

┌─────────────────────────────────────┐
│ 周五001 澳超  2026-10-24 08:00       │
│ 布里斯班[澳超11]  vs  珀斯[澳超8]     │   ← 排名显示在队名后
│   主胜 平 客胜    ✓ 倾向客胜         │
└─────────────────────────────────────┘

2.2 详情页标题

文件:gui.py · 第 1283–1284 行

点击卡片进入详情页后,标题行的主客队名同样带排名格式。

2.3 JSON 输出(radar_latest.json)

文件:radar_publish.py · _slim_match() 函数

JSON 里的队名字段从原来的 home / away(纯队名字符串)变为:

字段类型说明
homestr原始队名(纯文本,用于搜索/匹配)
awaystr原始队名
home_rankint 或 null主队联赛排名
away_rankint 或 null客队联赛排名
home_labelstr带排名的完整显示文本(如 "布里斯班[澳超11]")
away_labelstr带排名的完整显示文本

使用建议:前端渲染时用 home_label / away_label,搜索/过滤时用 home / away。


三、显示格式规则

3.1 基本格式

队名[联赛简称+排名]

示例:

联赛主客队显示
澳超布里斯班 vs 珀斯布里斯班[澳超11] vs 珀斯[澳超8]
英超曼城 vs 阿森纳曼城[英超1] vs 阿森纳[英超2]
德甲斯图加特 vs 门兴斯图加特[德甲5] vs 门兴[德甲10]
西甲莱万特 vs 毕尔巴鄂莱万特[西甲10] vs 毕尔巴鄂[西甲18]

3.2 联赛名裁剪

rank_label() 会自动去除联赛名末尾的冗余后缀让显示更紧凑:

原始联赛名裁剪后
西甲锦标赛西甲
德国杯赛德国杯
欧冠欧冠(不变)

3.3 降级规则

排名查询任一环节失败时,UI 自动降级为纯队名,不破坏旧行为:

失败场景显示
联赛不在白名单(如欧冠、国际赛)布里斯班 vs 珀斯(跟以前一样)
队名在数据库里查不到(normalizer 未知)沃夫斯堡 vs 波鸿
近 365 天该联赛已完赛场次不足A vs B
jcw_history.db 不存在或损坏全部显示纯队名
normalizer 模块加载失败(主软件未安装)全部显示纯队名

四、覆盖范围

4.1 有排名的联赛

覆盖竞彩所有单一国家联赛。完整白名单见 standings.py 第 45–119 行 _LEAGUE_WHITELIST:

  • 英超系:英超、英冠、英甲、英乙、英非联
  • 西甲系:西甲、西乙、西协甲、西协乙
  • 意甲系:意甲、意乙、意丙
  • 德甲系:德甲、德乙、德丙
  • 法甲系:法甲、法乙、法丙
  • 荷甲系:荷甲、荷乙、荷丙
  • 葡超系:葡超、葡甲
  • 东欧:俄超、乌超、波甲、匈甲、捷甲、希超、塞超、克甲、丹超、斯亚甲
  • 北欧:瑞超、挪超、芬超、苏超
  • 西欧:比甲、奥超、土超
  • 美洲:美职足、美超、墨超、墨甲、阿甲、巴甲、巴乙、智甲、哥伦甲、厄瓜多尔甲、秘鲁甲、委内瑞拉甲
  • 亚洲:日职、日乙、日丙、韩职、韩乙、澳超、马来超、泰超、新加坡超、印尼甲、越南甲、香港超、印度超
  • 非洲:埃及超、南非超

4.2 没有排名的联赛

白名单故意不包含以下赛制——因为它们不是单一国家联赛,"欧冠混合 79 支队一起排"这种排名没有意义:

不包含的联赛类型示例
跨俱乐部杯赛欧冠、欧罗巴、解放者杯、亚冠精英
国际赛世预赛、国家友谊赛、美洲杯、亚洲杯
杯赛国王杯、德国杯、巴西杯
竞彩特殊赛事冠军杯、挑战杯

这些比赛在 UI 上的队名不带 [联赛名+排名] 后缀。


五、数据来源与刷新

5.1 数据源

项目值
数据库文件D:\3D\SOFT\体彩足球竞彩\data\jcw_history.db
数据表matches
筛选条件league = 当前联赛 且 is_finished = 1 且 match_date >= 今日 - 365天

只用已完赛场次计算排名,进行中的比赛不影响积分榜。

5.2 计算规则

与 FIFA/欧洲主流联赛积分榜一致:

积分 = 胜 × 3 + 平 × 1 + 负 × 0
排序 = 积分 DESC → 净胜球 DESC → 进球数 DESC → 队名字母 ASC

5.3 365 天窗口设计

没有硬切赛季,而是滚动最近 12 个月的已完赛场次。原因:

  • 跨赛季时战斗力排名更平滑(英超冬窗后不必等到 5 月才能反映)
  • 竞彩比赛数据里的赛季边界不是很可靠
  • 已完赛场次数量充足(每个顶级联赛每年 300–400 场)

5.4 缓存与刷新

首次调用 league_rank("曼城", "阿森纳", "英超")
  → _ensure("英超")
    → 查 _CACHE 有没有英超的 bucket
    → 没有 → 查 db_key=(db_path, mtime, size)
    → db 变化了(竞彩新更新了完赛结果) → 重建
    → db 没变 → 用内存缓存
    → 用 sqlite3 执行一次 SELECT WHERE league='英超' AND is_finished=1 AND match_date>=cutoff
    → 构建积分榜 → 存入缓存 → 返回

后续调用同一联赛 → 直接从缓存取,0 开销
触发重建条件说明
jcw_history.db 的 mtime 或 size 变了竞彩周末更新完赛结果后,下次扫描自动拿到新排名
显式调用 standings.clear_cache()测试/调试用

六、降级与容错设计

6.1 三级队名匹配

standings.py 的 _lookup() 函数会依次尝试,最大化命中率:

层级方式示例
1原样命中索引 key"曼城" → "曼城"
2normalizer 扩展变体后逐个命中"Man City" → "曼城"
3双向 substring 模糊(唯一匹配时)"布里斯班" ↔ "布里斯班狮吼"

6.2 Normalizer 依赖

队名归一化依赖主软件的 graph_db.team_name_normalizer(懒加载,模块不存在时返回 None 降级为直接字符串比较)。normalizer 2026-10-23 批量补充了竞彩常用异译(沃夫斯堡→沃尔夫斯堡、墨胜利→墨尔本胜利、埃夫斯堡→埃尔夫斯堡、哈尔姆斯→哈尔姆斯达斯 等 20+ 条),以及架构级修复让 team_names_zh.json 里的 canonical 名自动成为 prefix-matching 的 key(布里斯班→布里斯班狮吼、珀斯→珀斯光荣、伊普斯→伊普斯维奇 等全部自动覆盖)。

⚠️ 共享依赖风险:team_name_normalizer.py 位于 D:\3D\SOFT\体彩足球竞彩\graph_db\ 下,是主软件「竞彩足球AI会话工具」的共享文件。本次修改(setdefault(zh, zh) 架构修复 + 20+ 条竞彩异译)会同时影响主软件的图数据库 UID 合并逻辑(compute_uid_merge_map),请确认主软件的合并诊断没有引入新的误合并。如果主软件使用 PyInstaller 打包,dist/ 目录下的副本需要重新打包才能同步生效——磁盘上的 dist/竞彩足球AI会话工具/_internal/graph_db/team_name_normalizer.py 不会自动跟随源码变化。

6.3 白名单守卫

任何不在 _LEAGUE_WHITELIST 的联赛直接返回空排名,不构建积分榜,彻底避免杯赛/国际赛的无意义混合排名。

6.4 try/except 全链路保护

# scanner.py 注入时的完整保护
try:
    from standings import league_rank
    feat["rank"] = league_rank(home, away, league)
except Exception:
    feat["rank"] = {}

_ensure() 内部 _build() 也包了 try/except——即使 sqlite3 连不上、normalizer 崩溃、数据格式异常,都不会让整个扫描器挂掉。


七、技术实现

7.1 模块边界

standings.py          ← 核心模块:积分榜计算 + 队名匹配 + 格式化
    ↑
    │ 调用 (懒加载)
graph_db.team_name_normalizer  ← 队名归一化(主软件共享依赖)
                                    ↑
                                    │ 不在本目录下
                                    │ 通过 config.get_main_app_path()
                                    │ 从 D:\3D\SOFT\体彩足球竞彩\
                                    │ 动态 sys.path 加载
    
scanner.py            ← 注入点:扫描后 feat["rank"]
    ↓
radar_publish.py      ← JSON 序列化:home_rank/away_rank/home_label/away_label
    ↓
gui.py                ← UI 消费:卡片列表 + 详情页标题

7.2 核心接口(standings.py)

# 查询两队排名(主接口)
def league_rank(home: str, away: str, league: str) -> Dict
# 返回:{home_rank, away_rank, home_played, away_played, home_total, away_total, home_pts, away_pts, source}

# 格式化显示(供 GUI / JSON 共用)
def rank_label(team: str, rank: Optional[int], league: str = "") -> str
# 例子:rank_label("布里斯班", 11, "澳超") → "布里斯班[澳超11]"
# 例子:rank_label("布里斯班", None, "澳超") → "布里斯班"  (降级)

# 测试用,强制重建缓存
def clear_cache() -> None

7.3 线程安全

_ensure() 内用 threading.Lock 保护 _CACHE 字典的读写。Python GIL + Lock 双重保障下,多线程并发调用不会产生竞态条件。

7.4 性能

2026-10-23 全扫描 35 场实测:

指标数值
冷启动首次构建 12 个联赛~3s(多联赛并行耗时)
后续扫描~4s(含全流程)
每联赛首次 sql 查询< 0.1s
standings 缓存后命中率100%(同一进程内)

八、后续规划

P1 · archive.py 归档

让归档记录也保存 home_rank / away_rank,这样后续可以做"排名差距 vs 爆冷概率"的量化分析。当前 archive.py 里还没有排名字段。

P2 · Web 前端

radar_latest.json 已输出 home_label / away_label。如果 p3d.csh.space 的前端有自定义模板,需要把显示文本从 home / away 换成 home_label / away_label。

P3 · 白名单维护

竞彩新增联赛时(如 2026-27 赛季如果开新的亚冠精英联赛),需要手动往 _LEAGUE_WHITELIST 里加。可以在 scanner 的 feat["rank"] 注释区标一条提示方便后续定位。

P4 · Team name fallback 降级说明

如果主软件 team_name_normalizer.py 被卸载,standings 会自动降级为基于 substring 的模糊匹配(_lookup() 的第三步),但命中率会下降。建议在 normalizer 里持续补充竞彩常用简称。


附录:验证结果

2026-10-23 修复后(normalizer 批量补异译 + 白名单扩展):

指标数值
联赛场次双方排名覆盖率12/12 = 100%
scanner 扫描有 home_rank13/35
全扫描耗时4.20s
normalizer 特别检查11/11 ✓

之前失败的案例(现在全部命中):

输入修复前修复后
布里斯班布里斯班布里斯班狮吼
珀斯珀斯珀斯光荣
墨胜利墨胜利墨尔本胜利
沃夫斯堡沃夫斯堡沃尔夫斯堡
埃夫斯堡埃夫斯堡埃尔夫斯堡
哈尔姆斯哈尔姆斯哈尔姆斯达斯
米堡米堡米德尔斯堡
伊普斯伊普斯伊普斯维奇
毕尔巴鄂毕尔巴鄂毕尔巴鄂竞技
门兴命中命中(已工作)
← 返回盘口棱镜文章列表

同栏目其它文章