用Python+SQLite自建牛客刷题跟踪器:每日提醒与数据可视化 早上 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酱能把消息推到微信个人号体验好免费额度有限并且依赖第三方跳转BarkiOS 专用极简、推送稳定仅限 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表里每一天都有一条solved1的记录连续多少天就是多少中间断一天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。它会在数据库里插入一条sourcemanual的记录。关键在于冲突处理如果同一天同题目已经有一条自动同步的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% 的问题了剩下的全是锦上添花。工具的意义在于建立反馈回路而不在于代码多复杂。把第一版做出来你就已经胜过绝大多数只收藏不行动的人了。