恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
用Python和SQLite打造“多米诺骨牌”刷题打卡追踪器
首页
资讯中心
/
用Python和SQLite打造“多米诺骨牌”刷题打卡追踪器
用Python和SQLite打造“多米诺骨牌”刷题打卡追踪器
发布时间:2026/10/5 4:10:24
那天晚上的场景我记得很清楚晚上十一点半打开牛客网的每日一题题目还没读完手机弹出来一条消息我顺手回完消息再回到电脑前已经过了零点。当天的格子永远空掉了——我坚持了六十多天的连续记录就此断签。断签之后的那一周我一次都没有再打开过每日一题。明明每天只需要花半小时却像多米诺骨牌一样第一块倒下之后后面连着塌了一大片。“多米诺骨牌”这个小项目就是在那之后动手做的一个围绕牛客每日一题做的个人tracker记录每天完成状态、计算连续天数、按时提醒当天题目用最轻的方式把刷题习惯重新拉回正轨。这篇文章会完整拆解这个tracker的设计思路、连续天数算法、打卡数据入口以及实测中踩到的几个坑给同样在刷每日一题、或者想为自己做一个习惯追踪工具的人当参考。1. 为什么叫“多米诺骨牌”从一次断签开始的连锁反应1.1 刷题最难的不是题目是“连续”每天一道题这个设计本身很轻量难的是“每天都做”。断签之后我更清楚地意识到一种连锁反应第一天断了第二天你会有一种“反正已经断了”的心态不再有维护连续记录的动机第三天你连打开页面的欲望都在下降等到第四五天之前顺手就能做完的题突然变得很重。这种感觉其实和认知负荷有关。维持一个未完成的链条时大脑每天只需要执行一个很小的动作就像按下一个开关链条一旦断了就需要额外决策“要不要重新开始”每一道题都变成一个新的决定消耗就大了。这个体验和“多米诺骨牌”最接近一排立好的骨牌推倒第一块很轻松但它带倒的是后面所有已经进入预备状态的骨牌。你在断签后的第一反应不是“少做了一道”而是“整条链都没了”。1.2 Tracker真正要追踪的不是“做了多少题”如果只是记录“今天做了题”手动写日历就够了。我需要的是把一个“连续链条”拆成可观测的指标连续完成天数、周完成率、月度总完成数、断签事件。追踪和记录的区别在于记录是被动的流水账追踪会主动盯住链条上的薄弱点。连续天数归零是第一块骨牌倒下的信号tracker的作用是把这个信号前置——在断签真正发生之前用提醒和趋势图把它拦住。这也是我把它叫“多米诺骨牌”而不是“刷题日历”的原因这个工具的核心对象不是题目是那根连续不断的链条本身。1.3 为什么选了PythonSQLite这套“土办法”说实话一开始上头想做一个完整的前后端项目React页面、后端服务、用户系统。冷静下来之后觉得不对——个人工具就该用个人工具的规模。最终选型Python 3做脚本、SQLite存数据、cron/计划任务跑提醒、终端输出看板。整套东西维护成本几乎为零换电脑只需要拷贝一个db文件。方案优点缺点我的结论React FastAPI PostgreSQL界面好看、可扩展服务器、部署、维护成本高过度设计Python脚本 SQLite 计划任务上手快、数据可移植界面简陋正合适纯手写笔记本零成本无法统计趋势、容易漏记只能当辅助选“土办法”还有一个原因值不值得用“新技术”取决于这个系统服务的对象。每天只写一条记录、每天只看一次状态这个体量连SQLite的性能都不需要担心更不需要上服务器。工具好不好不看技术栈酷不酷看它能不能稳定帮你解决每天的问题。2. 连续天数是怎么算的不被数据欺骗的算法细节2.1 先定义什么算“完成”写代码之前我先把“完成”定义清楚不然数据后面会打架。我给三种状态done当天独立写出并能讲清思路完全计入连续链。partial看了题解或参考后手写通过但能复述核心思路计入连续链但月度“独立完成率”会打折。skip没做或中断直接断链。为什么要保留partial因为“每天都做”本身比“每天都完全独立做出”更值得坚持但数据上不能自欺。partial只保证链条不断它骗不了汇总统计。换句话讲连续链衡量的是节奏独立AC率衡量的是水平这两个维度必须分开。2.2 连续链怎么算从今天往回回溯确定了状态定义算法其实很简单从今天开始往前数遇到非“done/partial”就停。关键不是做多复杂的计算而是把边界情况处理干净。我给一段核心代码from datetime import date, timedelta ALLOWED {done, partial} def calc_streak(checkins, todayNone): 返回 (当前连续天数, 断开日期) today today or date.today() cur today # 今天还没打卡时链的延伸只看昨天不能把今天算作断点 if checkins.get(cur) is None: cur - timedelta(days1) length 0 while checkins.get(cur) in ALLOWED: length 1 cur - timedelta(days1) broken_on cur if not checkins.get(cur) else None return length, broken_on这里有个反直觉的点今天没打卡不该立刻把连续天数归零因为一天还没结束。正确做法是把今天当作“未知”从昨天开始数。等到今天结束时如果还是没有记录明天的计算自然会从昨天往前数链子就断了。用date对象当key还有一个好处跨月、跨年都不需要特殊处理日期本身是连续的自然维度。2.3 补卡三档政策补卡在tracker里是不可回避的需求谁都有突发状况。我设计了三个档位7天内可补链上标记“补”连续天数正常计算但旁边显示星号。超过7天、仍在当月内的可补但只计入完成率不计连续链。跨月后不再允许补数据永远锁定。这么设计有几个原因一是数据诚实记录的是“真正发生”的习惯二是防止把tracker玩成填表游戏三是给意外留了余地比如出差、生病。关键是在代码里做同一件事允许修数据但不允许把过去某一天从skip改成done来“复活”一条早就倒下的链。实现上也不复杂写数据前判断一下目标日期和今天的时间差就行。2.4 “当天”到底以哪个时区为准这个坑很隐蔽我建议所有人都提前注意。一开始我跑计划任务的服务器用的是UTC时间脚本里datetime.now()默认返回UTC而日期key用的是本地日期。于是发生了一件怪事北京时间凌晨0点到1点打卡会被标成“前一天”连续链直接被误断。修复方式很简单显式指定时区再取日期from datetime import datetime from zoneinfo import ZoneInfo LOCAL ZoneInfo(Asia/Shanghai) today datetime.now(LOCAL).date()不要用绝对时间戳给日期归类人类的“一天”是本地时区的自然日。不管脚本在哪个机器上跑都必须在代码里锁死目标时区。这个坑后面我还踩过一次等到第5章再详细展开。3. 牛客每日一题的数据是怎么进tracker的3.1 牛客每日一题的页面上到底有什么先看看数据源有什么可用信息。牛客网的每日一题页面通常能看到题目标题、题号、通过率、难度标签、推荐理由还有你自己的提交状态。对tracker来说只需要其中几条题号、标题、难度、状态。题号一定要带。我当时觉得有标题就够了结果一个月后想回头找某道题发现同名的题有好几个还得去牛客网翻历史记录非常麻烦。题号是唯一标识有了它回看和复习的成本都很低。3.2 半自动录入不是偷懒是防错我一开始试过全自动抓取页面信息后来放弃了页面结构改一次脚本就要改一次而且抓错状态比手动录错更隐蔽。全自动的录错是静默的你根本不知道哪里脏了手动录错你起码有印象。最终采用“手工为主、命令为辅”每天花十秒把题号和难度复制进命令其他信息自动生成python domino.py done --no2656 --title合并两个有序链表 --difficulty中等 --tag链表 python domino.py status为什么强调半自动因为录入动作本身是一种“仪式感”它让我在提交代码之后刻意回看一眼题目比全自动静默记录更容易留下印象。这也是tracker跟数据采集系统的本质差异它的产出不只是数据库里的记录还包括每天一次主动的确认行为。这个确认行为本身就在强化习惯。3.3 SQLite表结构和防重复存储结构是整个项目的命根子。我用的表设计如下CREATE TABLE dailies ( date TEXT PRIMARY KEY, problem_no TEXT NOT NULL, title TEXT, difficulty TEXT, tag TEXT, status TEXT NOT NULL CHECK(status IN (done, partial, skip)), note TEXT, updated_at TEXT DEFAULT CURRENT_TIMESTAMP );用date做主键是刻意的天然唯一性同一天多次打卡会触发约束错误正好防重复。status字段加CHECK约束防止手滑把状态写成乱七八糟的值。我还在note字段里写当天卡了多久一个月后翻数据能看到自己卡得最多的题目这就是复习时的重点。4. 让骨牌不倒下提醒机制与趋势看板4.1 三级哨兵把“断签”拦在前面提醒机制是“多米诺骨牌”项目里投入产出比最高的一部分。我设了三个时间点早上9点脚本读出今天题目题号拼接URL用系统通知推送。这个动作把“找题”的成本清零了。晚上21点检查今天的记录没有就再提醒一次附一句“今天的骨牌还立着”。23点最后一道哨提醒“距离今天结束还有1小时”。实现上就是三个计划任务调用同一个Python脚本带不同参数。这里有个原则提醒脚本本身不要做太多事它只负责“判断推送”判断逻辑必须复用tracker核心函数。不要把判断逻辑散落在不同脚本里否则提醒逻辑和数据统计对“完成”的定义会出现不一致。4.2 可视化只输出一张“多米诺棋盘”可视化我没做网页一个脚本把最近90天渲染成一行行的格子“■”表示done“◨”表示partial“·”表示skip。每天开终端就能看到棋盘哪块空了非常直观。输出大概长这样最近90天 ■■■·■■◨■■■■■■■■■·■■■■■■■◨■■■·■■■··■ 当前连续14天 本月完成率87.5% 最近断签2025-01-04有人可能觉得这种形式太土但我用了三个月反而认为这是最合适的它不用打开额外页面、不用登录任何平台每天那个时刻自动出现。视觉上“棋盘缺一块”比数字变化更容易触发行动因为你的眼睛会自动盯住断层。4.3 用“提前量”对冲不可控事件提醒归提醒现实中的不可控还是要靠策略对冲。我总结出三个土办法早上看到题目后即使没有完整时间也先花10分钟把思路写进note晚上回来再补代码。如果有出差或长途出行计划主动把当天题目的思考提前放到前一天的note里第二天只需要提交代码就行。实在要断主动选一天断掉而不是被动等23点的哨兵响。提前决定“我今天就是要断”会减少那种“不小心断了”之后的连带心理崩塌。第三个办法尤其重要。“不小心断掉”和“计划内断掉”的心理成本完全不同前者会引发“反正断了”的滑坡后者只是一个普通的休息日。5. 跑了三个月踩过的坑和修复过程5.1 断签误判问题出在UTC时区上完整的排查链路是这样的现象周四、周五连续打卡周六打开看板发现连续天数只有1。第一反应是不是代码bug打印checkins字典发现周四的date key变成了“前一天”。根因周四晚上11点多打卡脚本运行环境是UTC北京时间已经进入周五但UTC还是周四。于是数据库里同时出现“周四UTC”和“周五UTC”两个键而展示端把日期字符串转成“今天”时用的又是北京时间。两个时间源不一致导致同一天的记录被劈成两天链自然就断了。修复只用了一行所有日期key统一用本地时区生成比较链时也直接用date对象的字符串比较不再依赖运行环境的默认时区。这个坑花了我一个晚上最后就是ZoneInfo的差别。5.2 重复录入把tag覆盖了另一个坑更隐蔽。某天我在两个终端窗口里各跑了一次done命令第二次执行时SQLite的date主键冲突写入没有报错因为当时的写入函数写得太“聪明”INSERT INTO dailies(...) ON CONFLICT(date) DO UPDATE SET tag ?结果第二次打卡时tag参数没传旧tag被空值清掉了。等我要按标签筛选链表专题时才发现好多记录的tag是空的。这个教训很深个人脚本也要分清“新增”和“修改”两条路径。后来我改了写入逻辑date已存在时只允许修改note字段tag和difficulty不可被空值覆盖。数据污染这种事一旦发生后面的统计报表全是不可信的。5.3 提醒脚本静默死亡最头疼的问题是某段时间晚上21点的提醒再也没出现但计划任务里却显示“上次运行成功”。排查过程很有意思。我打开日志发现脚本在启动阶段会初始化一个网络会话在公司网络环境下这个初始化会卡住直到超时崩溃。因为异常发生在主逻辑之前所以连日志都没写——程序启动后直接挂在了第一行。修复分两步第一步把网络发送放在真正需要发送消息的try块里任何异常都被捕获写入domino.log第二步加心跳机制——每天0点写一行心跳记录第二天如果没看到心跳就知道脚本挂了。个人工具的可靠性不一定靠监控平台一个小日志文件就够了。6. 三个月的多米诺数据它真的带来了连锁效应6.1 真实数字运行到现在几个关键数字如下指标数值最长连续记录87天总完成率91.7%断签次数2次断签后恢复间隔第一次7天第二次2天两次断签都不是主观偷懒一次是半夜急诊一次是跨城市出差落地时已经过了零点。但有意思的是恢复成本的变化第一次断签后隔了7天才回来第二次只隔了2天。这就是把中断记录下来的意义——你能清楚地看到“恢复成本”在缩减。6.2 真正的变化不是题量是心态数据上的变化其实没有特别夸张每周多做了几道题而已。更明显的变化是心态不再有“今天还没刷题”的悬而未决感。每晚9点左右完成追踪动作之后一天的学习任务就清晰截止了不会再带着“是不是该做题”的念头入睡。坚持做一件事靠的不是意志力而是“每天只有一个小任务”的系统。多米诺骨牌连锁反应的另一种理解第一块骨牌并不比后面任何一块力量更大是整排骨牌已经立好了你只需要推一次。tracker帮我把“每天推一次”这件事固定成了不占决策带宽的例行程序。6.3 允许“学过”之后数据反而更诚实了开头我定过严格策略必须独立AC才算完成。后来实在坚持不住而且发现焦虑感越来越重——很多题看题解能懂但独立AC真的需要大量时间。于是我把规则调整成读完题解、手写代码、能讲清思路就可以记partial。调整后完成率明显上升连续链更稳定而“独立AC数”在月度统计里单独保留。这样就清晰分开了“习惯链”和“熟练度”两个维度连续链衡量的是节奏独立AC率衡量的是水平不该混在一个指标里。这个调整对tracker设计很重要指标要匹配目标。我的目标是保持每天接触算法不是每天都要发明算法。如果你也打算做类似的工具先想清楚你的目标是习惯还是水平再决定数据怎么记。整套东西到现在还在跑每天维护成本不超过十分钟。前几天出差飞机落地已经十一点半我在酒店走廊里把当天的题看完然后在手机Termius里敲下那行打卡命令——那天那种感觉很值。工具从来不会逼人自律它能把“即将倒下”的那块骨牌提前指给你看这就够了。最后分享一个小技巧把连续天数放到终端欢迎信息里像我一样天天开终端的人根本不用特意打开看板就能被提醒。项目不大但连续87天那会儿我是真的被自己的记录惊讶到了。