☰
牛客榜单追踪器与每日一题打卡:备赛效率自动化工具
2026/10/14 7:22:29 网站建设 项目流程

GCPC总决赛备赛那阵子,我整个人被牛客的榜单折磨得够呛。手机刷一遍网页,电脑刷一遍网页,十分钟不看就心慌,生怕漏了哪个队的过题变化,结果真正有效的训练时间反而被切得稀碎。后来我动手写了一个挂在牛客上的小工具,名字就叫“GCPC总决赛【牛客tracker & 每日一题】”,核心功能就两个:自动追踪比赛实时榜单变化,外加把每日一题做成带打卡记录的刷题任务流。

这个项目解决的实际问题,说起来其实很朴素:一是信息获取,二是练习执行。榜单tracker把“盯排名”这件事自动化,你不需要一直开着网页;每日一题模块把备赛节奏固定下来,每天至少做一道题,做完打卡记录,时间久了进度一目了然。适合的人群也很明确——正在备战系列赛、校赛、区域赛的个人选手或小队,尤其是那种需要一边上课/上班、一边准备比赛的人。这篇文章我会把整个项目的设计思路、核心模块实现、踩过的坑都摊开讲,代码能直接抄作业的就直接给。

1. 项目定位与整体设计思路

1.1 备赛过程中的真实痛点

先说痛点。我在GCPC总决赛前一个月进入高强度备赛状态,当时每天要做的功课有三个部分:刷知识点、补薄弱专题、盯队伍排名。前两件事还好,麻烦的是第三件。牛客这类竞赛平台的榜单页面是动态刷新的,想看最新排名就得不停手动刷新,高峰期经常刷出缓存数据,要等好几秒才能看到真实排名。这中间浪费的时间看似不多,但每天十几次、二十几次地刷,累积起来非常可观。

更麻烦的是,榜单不是一个“安静”的信息源。有些队伍会在某个时间段集中过题,有些队伍长时间不动,你需要区分“暂时卡住”和“彻底放弃”两种情况。单纯靠肉眼盯,很难捕捉到这种微妙变化。我当时的念头很简单:写一个程序定时抓取榜单数据,记录每次抓取时的排名、过题数量、罚时,然后对比两次数据,把变化自动标出来。这样我可以只在自己需要的时候看一眼汇总,不需要一直盯着页面。

1.2 为什么把tracker和每日一题组合在一起

最开始我只想做一个榜单追踪器,后来加每日一题模块是因为一个很现实的发现:备赛群里每天都在转发牛客的每日一题链接,但真正每天做的人寥寥无几。不是大家不想做,而是缺少一个“记录机制”。做过的题没有留下痕迹,今天做没做、连续做了几天、哪个专题做得少,全靠脑子记,记不住。

于是我把每日一题做成了一个带打卡功能的模块。每天从牛客拿到当天的每日一题,做完之后在工具里标记完成,工具自动记录日期和题目信息。这样tracker负责回答“我们队伍现在排第几、谁在追赶我们”,每日一题负责回答“我今天的训练任务完成没有”。一个管外部信息,一个管内部执行,两个模块合在一起,才算是把备赛的闭环补完整了。

1.3 整体架构:一个轻量到不需要服务器的方案

架构上没有搞复杂。整个项目是Python写的,分三层:

  • 数据层:榜单数据用requests抓接口,每日一题数据从牛客页面解析,本地用SQLite存历史记录。
  • 业务层:包含榜单对比逻辑、状态变化计算、每日一题打卡逻辑、进度统计逻辑。
  • 展示层:最开始只有命令行输出,后来加了一个本地网页面板,用Flask起一个小服务,手机上也能打开看。

部署上我建议直接用一台云服务器,或者就在自己电脑上定时跑脚本。项目本身不重,CPU和内存占用可以忽略。之所以这样设计,是因为我不想把这个工具做成一个“重系统”,备赛期间时间有限,工具应该花最少的时间维护,用最少的心智负担去操作,而不是反过来增加负担。

提示:如果你只是自己一个人用,别一上来就写前端页面。先用命令行版本跑通逻辑,后面再加面板,这是最不容易烂尾的做法。

2. 牛客榜单追踪器的核心设计与实现

2.1 榜单数据是怎么拿到的

牛客的竞赛榜单页面,正常浏览器打开之后会看到很多实时数据,但如果你直接去看网页源码,会发现排行榜其实是通过接口动态加载的。找到这个接口是第一步。

我用的方法很简单:打开浏览器开发者工具,切到Network面板,刷新榜单页面,找返回JSON数据的请求。牛客的榜单接口一般会返回一个包含rankData字段的JSON,里面有每个队伍的排名、学校、过题数、罚时、每道题的提交状态和通过时间。拿到这个接口之后,Python里用requests直接请求,加上和浏览器一致的User-Agent,就能稳定拿到数据。

核心代码大概长这样:

import requests import json def fetch_rank_data(contest_id): url = "https://example-nowcoder-api.com/contest/rank" params = { "contestId": contest_id, "page": 1, "pageSize": 300, "type": 0, } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() return data["data"]["rankData"]

拿到JSON之后,要做的就是字段映射。我自己建了一个小的数据类来统一表示一支队伍的榜单状态:

class TeamRank: def __init__(self, rank, team_name, school, solved, penalty, detail): self.rank = rank self.team_name = team_name self.school = school self.solved = solved self.penalty = penalty self.detail = detail # 各题提交情况

这里有个值得提醒的点:牛客的接口偶尔会变字段名,比如有些版本里过题数叫solved,有些版本里叫acNum。写代码的时候不要硬编码字符串满天飞,最好在一开始定义一个常量映射表,后续万一接口变了,改一处就行。

2.2 增量更新与状态变化计算

榜单数据本身没有太大价值,真正有价值的是“变化”。我每次抓取榜单之后,会和上一次抓取的数据做对比,计算出几个关键字段:

  • rankChange:排名相比上次变化了多少。正数代表上升,负数代表下降。
  • solvedChange:过题数量增量。
  • penaltyChange:罚时增量。
  • 最近过题:本次抓取比上次多出来的题目编号。

这个状态变化计算是tracker最核心的部分。实现思路不复杂,用字典把队伍名映射到上次的榜单对象,然后逐一对比:

def compute_changes(old_rank_map, new_rank_map): changes = [] for team_name, new_rank in new_rank_map.items(): old_rank = old_rank_map.get(team_name) if old_rank is None: changes.append({ "team_name": team_name, "rank_change": 0, "solved_change": new_rank.solved, "status": "new_team" }) else: changes.append({ "team_name": team_name, "rank_change": old_rank.rank - new_rank.rank, "solved_change": new_rank.solved - old_rank.solved, "penalty_change": new_rank.penalty - old_rank.penalty, "new_solved_problem": find_new_problem(old_rank.detail, new_rank.detail), "status": "normal" }) return changes

有了这些变化数据,就可以做可视化输出了。我当时的命令行输出长这样:

[12:30:01] 某队 rank 3 -> 2 (↑1) 过题 5 -> 6 [新过题: C] [12:30:01] 某队 rank 15 -> 18 (↓3) 罚时 +20

你别小看这个输出,它带来的信息密度比肉眼看榜单高得多。榜单每一秒都在变,但你只需要关心那些“相对上一次抓取发生了变化”的队伍,其他队伍不动就不管,过滤掉了大量无意义信息。

2.3 关注列表:只关心自己和自己附近的队伍

比赛队伍几十上百支,如果每轮抓取都把所有队伍的变化打印出来,信息量依然很大。后来我加了一个关注列表机制,用户可以配置一组队伍名,tracker只对这些队伍做详细变化展示,其他队伍的变化只统计数量,不打印细节。

这个关注列表非常实用。我的用法是把自己队伍放进去,再把排名在自己前后五名内的队伍放进去,这样赛场上谁在追我、谁在被我校超越,一目了然。实现上就是一个配置文件,用JSON存关注队伍名单,每次程序启动时读取。

{ "watch_list": ["某队", "某队", "某队"], "watch_range": 5 }

2.4 封榜时间的处理

GCPC总决赛这类比赛有一个很常见的规则:最后一段时间内榜单冻结,不再对外显示实时排名。牛客的榜单接口在封榜后通常不会报错,而是直接返回封榜时刻的数据。如果不做处理,tracker会把这些“不变的数据”当作“没有变化”,看起来一切正常,实际上信息已经失真。

解决方案是在tracker里加一个“封榜检测”逻辑。当队伍的关注列表里出现连续多次抓取数据完全一致、并且赛场已经临近官方公布的最后提交时间时,自动标记为“疑似封榜”,提示用户接下来的数据仅供参考。这个逻辑不复杂,但很救命。我第一次用的时候就是没注意到这个,赛后面试复盘时发现自己盯了一个“假榜单”,还好没影响实际决策。

注意:封榜期间不要再用tracker的数据来调整策略,这时候最该做的是把精力放在自己队伍还没过的那几道题上。

3. 每日一题模块与打卡机制

3.1 每日一题从哪里获取

牛客的每日一题一般有固定的入口,每天会更新一道题,题面会附上难度标签和知识点标签。获取方式有两种:一种是直接爬接口,另一种是解析页面。优先用接口,因为页面解析受HTML结构影响大,牛客改版一次你的代码就要跟着改一次。

我用的方案是:每天定时请求牛客每日一题的接口,拿到当天题目的标题、题号、难度、知识点,插入本地数据库。如果当天接口暂时没返回数据,就等待一段时间再试,因为每日一题的更新时间有时候会在凌晨附近,不一定是零点整就更新。

3.2 本地SQLite设计与打卡流程

本地数据库表结构设计得很简单:

CREATE TABLE daily_problem ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_date TEXT NOT NULL, problem_id TEXT NOT NULL, title TEXT NOT NULL, difficulty TEXT, tags TEXT, solved INTEGER DEFAULT 0, solve_time TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP );

每天拿到题目后插入一条记录,solved默认0。做完题之后运行打卡命令:

python tracker.py daily-check --problem-id 12345

这个命令会把对应题目的solved更新为1,记录完成时间。兑完打卡之后,每日一题记录的连续天数就会更新。连续天数我单独建了一张表来记录,因为连续打卡统计用单独的字段存更清晰。

3.3 进度统计与可视化

统计功能一开始只是打印一个简单表格,后来发现光有表格不够直观。我就加了一个小面板,本地网页上显示:

  • 本月打卡日历:类似GitHub贡献图,每一天一个格子,打了卡就变深色。
  • 专题分布:每个知识点的做题数量条形图。
  • 难度分布:简单、中等、困难各做了多少题。
  • 当前连续打卡天数。

这个面板是锦上添花的东西,但它的价值在于“看得见的正反馈”。坚持刷题最难的不是做题本身,而是“坚持”这个状态看不见摸不着。一旦有了日历格子,每天打卡就多了一点仪式感,人是有这种心理机制的,你看到连续天数在涨,就不想断。

3.4 每日一题和tracker如何联动

两个模块不是孤立的。我设计了一个简单的联动规则:每日一题打卡时,如果当天有GCPC的比赛或者模拟赛,tracker会把当日赛题数据里涉及的知识点自动标注到每日一题的tags里。这样一来,赛后看打卡记录时,能直观看到“我们今天练了图论,结果赛场上图论题就卡住了”,方便做赛题复盘。

更实际的一个联动是状态提醒。每日一题打卡完成之后,tracker会更新本地状态文件;如果你部署了推送机器人,还可以在手机收到一条打卡成功的通知。这个功能实现成本不高,但非常提气,尤其是你白天有事差点忘了刷题的时候,机器人一句话能把你拉回来。

4. 实操实录:从0搭建一个可用版本

4.1 环境准备与项目结构

我在写这个项目的时候用的是Python 3.10,需要安装的库只有requests、Flask和APScheduler。SQLite是Python自带的,不需要额外装。整个项目的文件结构大致如下:

gcpc-tracker/ ├── config.json # 配置文件 ├── tracker.py # 主入口 ├── rank_fetcher.py # 榜单抓取 ├── daily_problem.py # 每日一题模块 ├── storage.py # SQLite操作 ├── panel.py # 本地网页面板 └── templates/ └── index.html # 面板模板

4.2 榜单追踪器的关键实现

榜单追踪器核心是一个死循环定时任务,用APScheduler控制抓取频率。抓取频率我建议60秒一次,别太频繁,牛客的接口虽然没有严格的频率限制,但人家是公开平台,抓太频繁既不礼貌也可能被临时限流。

主流程代码如下:

from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() @scheduler.scheduled_job("interval", minutes=1) def scheduled_fetch(): fetch_and_update() if __name__ == "__main__": scheduler.start()

fetch_and_update函数负责完成一次完整的抓取-计算-存储-输出流程。存储时要注意,历史数据不要丢,至少要保存最近50次抓取的完整榜单,这样赛后可以复盘整场比赛的名次变化曲线。

4.3 每日一题定时检查与打卡命令

每日一题模块我设置了三个定时任务:

  • 每天7点:检查当天每日一题是否已经入库,没有就自动抓取。
  • 每天21点:检查当天题目是否已打卡,如果没打卡,推送一条提醒。
  • 每周日21点:生成本周刷题小结。

打卡命令我上面已经写了,这里不重复。提醒推送我用的是通用webhook机器人,一条JSON POST就能发消息,不需要额外引入重量级依赖。

4.4 部署与长期运行的经验

部署方面,如果你有自己的云服务器,直接把项目放到服务器上跑就行,用nohub或者systemd管理进程。如果是本地电脑,建议设置开机自启,或者干脆像我一样在服务器上跑。

这里有一个值得说的细节:跑定时任务的时候,要特别注意脚本报错后是否会静默退出。我的做法是写一个简单的错误捕获,每次任务执行完记录一条日志,一旦连续三次任务失败,就发一条告警消息。这种“监控的监控”在备赛期很实用,因为这个阶段你没有精力天天检查工具是否还在正常跑。

5. 常见问题排查与避坑实录

5.1 榜单接口请求失败或超时

牛客的接口整体来说很稳定,但高峰期偶尔会出现响应变慢的情况。我自己遇到过几次请求超时,一开始以为是代码写错了,后来发现是比赛开始时很多人同时刷接口,服务器响应速度变慢。

解决方案有两个层面:一是代码层面给requests设置合理的timeout,并捕获超时异常后等待下一轮再抓;二是抓取频率适当降低,从60秒改成90秒或者120秒,比赛过程中没必要做到秒级追踪,一两分钟内的变化完全够用。

5.2 榜单数据字段缺失问题

有一次接口返回的某支队伍数据里少了罚时字段,直接导致程序抛KeyError。排查后发现,这支队伍可能因为某些原因没有有效提交记录,接口返回的数据结构就不完整。

解决办法是写一个兜底函数,所有字段都用get(x, 默认值)的方式读取,缺失字段给一个安全的默认值。这个坑虽然不大,但一旦崩了就是整个程序崩,所以必须防。

5.3 封榜检测逻辑误报

封榜检测我最初是写死“连续3次数据不变就判定封榜”,结果训练赛时出现过误报——有两次抓取间隔内,场上所有队伍都没过题,数据自然没变化,脚本就误判为封榜了。

改进后的逻辑加入了时间判断:只有在比赛进行到最后30分钟时,才启用封榜检测;并且至少连续5次抓取数据完全一致才触发提示。这样基本不会误报了。

5.4 每日一题“今日题目”获取不到

每日一题接口偶尔会返回空数据,因为牛客后台还没更新当天的题目。我的处理方案很朴素:每天7点先试抓一次,如果抓到空数据就等30分钟后再试,最多试到中午12点。实测下来,绝大多数时候9点前都能拿到当天题目,只有极少数情况会拖到上午。

5.5 本地网页面板手机打不开

Flask面板跑起来之后,手机浏览器访问不了,通常是防火墙或者监听地址的问题。Flask默认监听127.0.0.1,只能本机访问,改成0.0.0.0之后局域网内就能访问了。另外,如果你用的是云服务器,记得在安全组放行对应的端口。

我在本地跑的时候还遇到过一次端口被占用,后来在启动命令里指定了一个不常用的端口,问题就解决了。

6. 实战复盘:这个工具到底带来了什么

整个GCPC总决赛备赛周期,我用了这个工具大概四周时间。从数据上看,连续打卡天数最高到过19天,中途断了一次是因为比赛当天太晚忘记打卡,第二天反应过来已经断了。排名追踪方面,tracker记录了我们队伍从热身赛的第20名左右,到总决赛正式比赛前半段的第9名,整个变化曲线清晰可查。

但我觉得更重要的是另一个变化:我不再“频繁地刷榜单了”。以前每10分钟看一次,现在每天打开面板两次,上午看一次榜单变化小结,晚上看一次打卡状态,中间的时间全部用来做题。工具的作用是帮你把注意力从“盯排名”转移到“写代码”上,这比排名数字本身的上涨更值钱。

用的时候我也总结出几条经验,不一定适合所有人,但你可以参考:

  1. 关注列表不要设太多队伍,5到8个足够。超过这个数量,输出的信息又变成噪音了。
  2. 每日一题打卡时间尽量固定,固定时间容易形成习惯,不容易断。
  3. tracker和每日一题分开用也可以,但合在一起更容易坚持。因为两个模块互相提醒,你对这个工具的依赖度会更高,使用意愿也会更强。

这类工具后续还有很多可以扩展的方向,比如接入更多平台、把队友的提交记录也纳入分析、赛前自动生成模拟题单。但作为一次备赛工具,我现在的版本已经足够满足需求了。如果你也在备战类似的比赛,不妨从榜单追踪器开始动手写一个,不一定非要做得功能齐全,先把最困扰你的那个“信息焦虑”解决掉,剩下的自然会有动力去完善。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询