☰
用Python+SQLite自建牛客刷题跟踪器:每日提醒与数据可视化
2026/10/13 14:19:42 网站建设 项目流程

早上 8 点整,手机弹出一条 push:今天的恶魔果实已上线,敢不敢吃掉?点进去是牛客当日每日一题的链接。这个每天准时报到的家伙,就是我给自己写的刷题跟踪工具“恶魔果实”,本质是一个牛客tracker。它在本地维护一份刷题数据库,自动同步牛客个人中心的提交记录,每天早上推送当日每日一题,再用热力图、标签统计、连续打卡天数,把“坚持”这件事变成看得见的数据。如果你也和我一样,刷题总是三天打鱼两天晒网,或者想长期保住算法手感,这篇文章会把我从零到一怎么做出来、踩了哪些坑,全部拆给你看。

1. 恶魔果实解决的核心问题:为什么自建 tracker 而不是用现成统计

先聊聊这个工具最开始存在的理由。我用过不止一个刷题平台,也用过各种打卡 App,最后发现一个共同问题:它们都在记录数据,但很少真正帮助我坚持。

牛客个人主页确实有提交曲线,能看到自己今天做了几道题、通过率怎么样,但它不会在早上提醒我“今天该做题了”。牛客也有每日一题栏目,但每天打开之后,如果我不手动记录,一周后就忘得一干二净——那题目到底做没做、做对没做对,全凭记忆。更关键的是,平台自带的统计很少把“连续打卡天数”“标签强弱项”“每周活跃度”这些真正影响刷题策略的信息组合起来看。比如我想知道我最近三十天到底在哪个知识点上翻车最多,现成统计并不能直接回答。

于是我在一个连续断卡两周的晚上,动手写了第一版“恶魔果实”。名字的灵感来自我对刷题行为的理解:每天吃掉一道每日一题,就像吃下一颗果实,长期积累就会拥有面试场上从容解题的“能力”;而副作用也像果实一样明显——你会开始对连续打卡天数斤斤计较,看到热力图出现空白就浑身难受,甚至为了保住 streak 挑软柿子捏。这些心态变化,后面我会专门讲。

这个工具适合谁?我梳理下来主要是三类人。第一类是正在准备国内互联网公司面试的候选人,牛客题库里的每日一题和面试实战题覆盖度很全,中英文题目都有,刷起来对口。第二类是技术管理者,想在团队内部搞算法学习打卡、周报统计。第三类是任何想用数据驱动方式养成长期学习习惯的人,哪怕你完全不在牛客刷题,这套“每日提醒 + 自动记录 + 可视化”的架构也完全可以搬到你的场景里,比如换成力扣、换成背单词网站,甚至换成健身记录。

一句话总结这个 tracker 的价值:它不替你刷题,但它负责让你知道“今天该刷什么”“最近刷得怎么样”“哪里是弱项”,把模糊的“我好像学过”变成精确的“我在某个知识点上已连续失败六次”。

2. 技术路线选择:Python 定时脚本、SQLite 与推送渠道的取舍

在动手之前,我在三种方案之间纠结过一阵。下面是我的对比和最终选择,这部分如果你也想复刻,可以直接作为决策参考。

方案优势劣势我的结论
浏览器插件 / 油猴脚本能直接在提交页面捕获行为,实时性最强数据存在 localStorage,换设备就没了;牛客页面改版脚本就得跟着改适合只在一台电脑上用、不关心历史积累的轻量场景
Python 定时脚本抓取逻辑自由,数据集中存本地,迁移和备份方便需要手动维护登录态和接口解析最终选择,灵活性和可控性最高
用现成的第三方统计工具零开发成本很难覆盖牛客这种垂直平台的数据口径,更不可能定制“每日一题推送”不满足我的核心需求

我最终选了 Python 脚本,不是因为语言多高级,而是这个组合最能解决实际问题:requests 抓接口、标准库处理时间、sqlite3 存数据、cron 负责定时触发、pyecharts 生成报告。整套下来不需要额外部署任何服务,也不用花钱买服务器。

数据存储方面,我一开始用的是 JSON 文件,因为第一版逻辑特别简单,每天拉一次提交记录,追加到数组里完事。但跑了一个多月,数据量到了几千条,问题开始冒出来:一是并发写,如果定时任务在跑的同时我手动执行 sync 命令,两个进程同时写同一个 JSON 文件,偶尔会把文件写坏;二是增量查询很别扭,每次统计都要把全量数据读进内存,然后 filter 一遍。后来我花了俩小时迁移到 SQLite,单文件数据库,没有服务端,备份就是拷贝一个文件,所有麻烦一次性解决。

推送渠道我也对比了一圈。最终我选了企业微信群机器人作为主力渠道,理由很直接:免费、支持 markdown 格式、延迟基本在秒级,工作电脑上一直登着微信,push 到手机也能收到。

推送方式优点需要注意的地方
企业微信群机器人免费、markdown 格式、不受个人服务器限制需要建一个群,哪怕只有自己
钉钉群机器人类似企业微信,文档成熟自定义关键词设置容易踩坑,消息里必须包含关键词
Server酱能把消息推到微信个人号,体验好免费额度有限,并且依赖第三方跳转
Bark(iOS 专用)极简、推送稳定仅限 iPhone,且需要一直开着 App

我选企业微信还有一个原因:它的 webhook 只需要一个 URL,不需要给脚本配置任何账号密码,跑在服务器上或者家里电脑上都很安全。后面踩坑部分提到的“运行失败报警”,我也复用了同一个 webhook。

整个项目的目录结构非常简单,维护起来几乎零成本:

devil_fruit/ ├── config.yaml # 存放 Cookie、webhook、同步时间等配置 ├── daily_task.py # 每日一题抓取 + 推送 + 记录打卡 ├── sync.py # 手动或定时同步牛客提交记录 ├── report.py # 生成可视化 HTML 报告 ├── streak.py # 连续打卡天数计算 └── data/ ├── fruits.db # SQLite 主数据库 └── reports/ # 生成的热力图、统计图

3. 两个核心功能拆解:每日一题提醒和刷题数据入库

3.1 每日一题提醒怎么设计

这个功能是整个工具里最容易理解的部分,但细节坑最多。核心思路是:每天定时访问牛客的题库页,把当日的每日一题链接、标题、难度抓出来,然后推到企业微信群。

你可能觉得这不就是爬一个页面吗?对,但难点不在“抓”,而在“怎么判断今天和昨天抓的是同一道题”。牛客的每日一题有一个特点:旧题会被重复拿出来。如果今天推荐的是你已经做过的一道题,再给你推一遍“来做题吧”,体验就很蠢,而且容易让人忽略真正的新题。

我的去重逻辑是:把每日一题的题目 ID 存进 SQLite 的 daily_checkin 表。每次抓取成功后,先查这个 ID 有没有出现过,如果出现过,就把它归类为“复习题”,推送文案换成“昨天没吃掉的果实又回来了”;如果是新题,才用完整的“今日新果实”文案。这个逻辑用原生 SQL 实现就两行,但避坑效果非常明显。

抓取页面的频率我控制在每天一次,固定早上 8 点。早上推送的好处是:绝大多数人刚起床或通勤,这时候看到一条轻量的刷题提醒,顺手点开的概率最大。晚上推送我试过,效果很差,脑子里装的是白天的工作和视频,完全不想碰算法。

3.2 提交记录同步接口和数据结构

tracker 的核心功能是记录“我到底刷了什么”。这个数据如果全靠手动填,坚持不过三天,所以必须自动同步牛客个人中心的提交记录。

具体怎么找到同步接口?方法对你来说并不神秘:登录牛客网页版,打开个人中心页面,按 F12 进入开发者工具,切到 Network 面板,再刷新页面,过滤 XHR 请求,一个一个翻返回 JSON 的请求,就能找到返回提交列表的接口。它返回的数据大概长这样:

{ "list": [ { "problemId": 123456, "title": "反转链表", "tags": ["链表", "入门"], "result": "AC", "timeCost": 120, "submitTime": "2024-11-10 08:12:33" } ] }

拿到接口之后,用 requests 带上自己的 Cookie 去请求,逐页解析,写进 SQLite。我的表结构设计得很朴素,字段如下:

CREATE TABLE submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_id TEXT NOT NULL, problem_title TEXT NOT NULL, tags TEXT, difficulty TEXT, submit_at DATETIME NOT NULL, result TEXT NOT NULL, time_cost INTEGER, source TEXT DEFAULT 'pull' ); CREATE TABLE daily_checkin ( day TEXT PRIMARY KEY, problem_id TEXT, solved INTEGER DEFAULT 1, note TEXT );

同步策略我做了两层。第一层是每天凌晨 2 点跑一次全量同步,把昨天一整天的提交记录拉进库;第二层是我每次做完题、觉得心里没底的时候,手动执行python sync.py --incremental,只同步最近 5 分钟的新记录。增量同步靠的是submit_at这个时间戳,每次同步前记录数据库里的最大时间,下次只取比它更新的数据,效率高很多。

这里有一个真实使用中的细节:你在网页上点“提交”之后,提交记录不会立刻出现在个人中心接口里,我实测下来延迟短的几秒,长的能到三五分钟。所以“做完题立刻同步然后发现没有记录”是非常正常的,不用怀疑脚本坏了。我的建议是:至少等 10 分钟再增量同步,或者干脆交给凌晨 2 点的全量任务兜底。

3.3 连续打卡判定与时间边界

连续打卡天数(streak)是这个 tracker 最让人上瘾的指标,也是所有逻辑里最容易翻车的部分。我的判定逻辑很简单:从今天往前数,daily_checkin表里每一天都有一条solved=1的记录,连续多少天就是多少;中间断一天,streak 归零重来。

听起来很清晰,但边界问题极其烦人。什么叫“一天”?牛客个人中心的统计口径可能是自然日,也可能是某个服务器时区。我一开始直接用牛客返回的时间戳做 group by,结果经常发现“今天明明 AC 了三道题,怎么 streak 还是断的”。后来我把submit_at统一转换成北京时间凌晨 0 点到 23:59:59 作为一天,再入库。同时把全量同步任务放在凌晨 2 点:这样 0 点到 2 点之间产生的提交记录,也能被正确地归属到前一天。

如果你的打卡边界不想按自然日,想按“24 小时滚动窗口”或者“早上 6 点为新的一天开始”,SQL 里的日期截断逻辑做相应调整就行。但我的建议是不要弄太复杂,凌晨 0 点就是内心中最符合直觉的“一天结束”。

4. 实战踩坑复盘:登录态失效、接口字段漂移和同步时序

整个项目最有价值的部分不是代码,而是下面这一堆坑。我一个个说,每个都付出了真实时间成本。

4.1 Cookie 失效后脚本第一步就挂

牛客的接口需要登录态,我是靠浏览器里复制 Cookie 到config.yaml来实现的。问题在于,Cookie 是有有效期的,短则几天,长则几周。过期之后脚本第一次请求,返回的是 HTML 登录页,而不是 JSON。我的第一版代码直接做data["list"],当时就炸出来一个KeyError,然后定时任务在服务器上静默失败,连续三天没提醒,我还没发现。

后来我加了一个“异常返回感知”机制:任何一次请求,如果返回体不是合法的 JSON,或者 JSON 里没有预期字段,立刻触发企业微信报警,标题是“恶魔果实脚本异常”,内容把返回体的前 200 个字符附带上。这样 Cookie 一失效,我当天就能看到报警,而不是一周后才发现 tracker 变成哑巴。

这个经验放之四海皆准:定时任务最怕的不是失败,而是静默失败。凡是上了 cron 的脚本,都建议配置一个失败报警,报警渠道要和你每天必看的东西绑定,比如微信或邮件。

4.2 接口字段漂移,让我明白了 normalize 的重要性

牛客的接口不是一成不变的。某一天我发现同步进来的result字段全是 1,而不是以前熟悉的AC。排查之后才发现,接口改版把提交结果的英文状态码改成了数字枚举。还有一次是tags字段从字符串数组变成了逗号分隔的字符串。

这种“字段漂移”防不胜防,唯一稳妥的办法是在所有数据进入数据库之前,过一层normalize()函数。我会把result统一映射成AC/WA/TLE/MLE/CE/RE六种标准值;把tags无论传来的是数组还是字符串,都统一成 JSON 字符串再入库。这样下游所有统计逻辑都只依赖规范化之后的格式,接口再怎么变,我只需要改一个函数。

4.3 请求频率太激进,被牛客提示“操作频繁”

有一回我调试分页逻辑,用 for 循环一口气把个人提交记录拉了五十多页,刚跑到十几页,接口开始返回“操作频繁,请稍后再试”。当时我意识到这个问题比我想象中严重:如果脚本以后再全量同步,触发了同样的限制,整个 tracker 就废了。

解决办法很简单:所有请求之间随机 sleep 1.5 到 3 秒,同时把全量同步的频率限制在每天凌晨一次。拉取五十页数据,最长也就多花一分钟,这个成本完全可以接受。记住一个原则:任何自动化脚本,都要把自己当成一个“温和的人类用户”——不要短时间高频访问,不要同时开多个线程抓同一个站,抓到限制提示就立刻停手并延后重试。

4.4 每日一题推了才五分钟,题目页面已经改了

这个坑是在去重逻辑上线前踩的。有一天我收到推送,点进去发现题目和我前两天做过的一模一样,于是我去查题库页的 HTML,发现“每日一题”区域有两种渲染状态:一天里不同的时间段,显示的题可能不一样。也就是说,早期版本的抓取逻辑可能早上抓到的是 A 题,晚上刷新变成 B 题。

我没有去深究牛客为什么要这样设计,我只需要自己的数据是稳定的。我的方案是:只在每天 8 点整抓一次,抓到什么就是什么,以这个结果作为当天“每日一题”的最终口径。这样即使页面后面换了题,我的打卡记录也不会混乱,因为我没有一天多次抓取的逻辑。

4.5 手动做题和自动同步打架,怎么稳心态

不是所有题目都能被自动同步到。有一些题目我是线下在 IDE 里写、在纸上画,没有在牛客提交过;另一些题我在牛客做了但没点“提交”,只是本地跑通。sync 抓不到这些,tracker 的每日打卡就缺了这些“暗题”。

解决方案是加了一个manual子命令,用法很简单:python sync.py --manual --problem 反转链表 --result AC。它会在数据库里插入一条source='manual'的记录。关键在于冲突处理:如果同一天同题目已经有一条自动同步的AC记录,而我又手动标记了这条题的笔记,以手动为准,因为手动的信息更丰富,包含了我自己对这道题的备注。这个冲突规则不一定适合所有人,但对我的使用习惯来说是最顺手的。

5. 把坚持变成可视化:热力图、标签统计和每周报告

数据进库之后,如果没有可视化,tracker 就只是一个沉默的数据库。我花了一个周末把“恶魔果实”的报告模块做出来,从此“坚持”这东西真的变成了肉眼可见的东西。

热力图是最直观的模块,类似 GitHub 的 contribution graph。实现思路不复杂:从daily_checkin表里取出最近一年每天solved的数量,按日期维度聚合成一个{date: count}的字典,然后用 pyecharts 的热力图组件渲染成 HTML。最终页面上看到的就是一个格子矩阵:今天绿了,说明打卡成功;空白一大片,说明那段时间在偷懒。报告生成后我直接双击打开,不需要任何 Web 服务。

标签统计则是为了回答“我到底弱在哪”。开发了一个多月的盲区之后我才发现,光靠回忆根本拼凑不出真实的薄弱项。于是我在report.py里写了一个聚合查询:按tags分组,统计每个标签的总提交次数、AC 次数、失败次数和最近一周的参与度。

标签提交次数AC 次数AC 率最后一次提交
数组231878%2024-11-08
动态规划12433%2024-10-20
链表151280%2024-11-09
二分查找9778%2024-10-25

这个表格一出来,我的刷题策略立刻变了。过去我习惯性刷自己会的题,数据告诉我动态规划 AC 率只有三成,而且最后的提交记录在半个月前,说明我在潜意识里避开它。从那以后,我要求自己每天的每日一题如果是 DP 标签,必须至少认真读题 20 分钟再决定是否跳过。

每周报告也是从 SQLite 里聚合出来的:本周新增题目数、活跃天数、平均难度、最长连续打卡、本周 AC 率变化。每周日晚上推送到企业微信群,格式用 markdown。这不是为了给别人看,而是为了让我在周日晚上客观地回答三个问题:这周我到底写了多少题?哪一天断档了?下周应该刻意练习哪个标签?

6. 跑完一个刷题季的数据复盘与后续改造计划

“恶魔果实”正式上线到现在跑了将近一个季度,数据放在面前,说实话有些地方连我自己都没有预料到。

第一周习惯还没养成,断卡三次,热力图一片亚健康;第二周我开始认真看待 streak,断卡频率明显下降;一个月之后,固定的 8 点 push 已经成了生物钟的一部分,我甚至不需要点开推送就知道今天要刷题。一个季度下来,最长连续打卡 32 天,动态规划的 AC 率从 33% 提到 51%,虽然还是薄弱项,但至少不再看到 DP 题就绕道走。

有些使用体验是数据反映不出来的。比如你会为了保住连续打卡去挑简单的题目来做,这种心态非常隐蔽,等你发现的时候,热力图已经好看了一大片,但真正的算法水平并没有同步提升。我现在的做法是:streak 只代表“坚持”,不代表“成长”;要结合标签统计一起看,如果连续打卡天数在涨、但某个弱项标签的提交次数还在持续下降,说明你只是在用重复劳动安慰自己。

另一个完全在意料之外的价值是,这个 tracker 成了团队内部一个小工具的原型。我把它改了一版,接入了另一个技术学习平台,用同样的数据库结构和背诵打卡逻辑,在组里跑了一个月的“每日英语释义打卡”。这让我确信:这套“定时抓取数据 + 本地 SQLite + 推送提醒 + 可视化报告”的最小闭环,在任何一个需要长期坚持的场景里都能复用。

最后给想复刻的朋友一个很实际的建设路径:不要一上来就写完整版。先做最小闭环——一个脚本,每天抓每日一题并 push 到微信,数据只存一个 JSON 文件。跑通之后再花一天加提交记录同步,再花一个周末做可视化。我这个版本做到三分之二的时候,其实已经能解决 80% 的问题了,剩下的全是锦上添花。工具的意义在于建立反馈回路,而不在于代码多复杂。把第一版做出来,你就已经胜过绝大多数只收藏不行动的人了。

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

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

立即咨询