简介:面向高校宿舍管理场景的Python电费监控程序,旨在解决宿舍用电数据采集、统计与可视化展示问题,适合Python学习者、校园信息化开发者及有课设需求的学生参考。代码包共7个文件,以5个Python脚本为核心,分别实现电费账户ID获取、房间遍历查询、用电监控及代理请求等关键功能,另有1个HTML前端页面用于结果展示,1个Markdown文档介绍项目背景与使用方法,压缩包仅10KB,轻量精炼、便于阅读。目前已有90人学习下载。资源基于一个完整项目整理,各脚本分工明确、逻辑清晰,可直接运行或二次开发。通过学习该程序,读者能掌握requests请求、数据解析、定时任务与Web页面渲染的联动实现,理解小型监控系统的整体架构,对课程设计、宿舍管理工具开发乃至Python综合应用都有实际参考价值。
1. 为什么宿舍电费监控值得自己写一套
大学宿舍的电费查询入口通常分散在校内网页、微信小程序或后勤App里,没有一个统一渠道会主动提醒你余额告急。断电往往发生在深夜,等发现时空调已经停了,只能摸黑给手机充电等第二天去充值。与其每天手动打开查询页面刷余额,不如写一个 Python 定时任务把电量抓下来、存进本地数据库,低于阈值自动发消息提醒。这套逻辑本质上就是一个轻量级的监控告警系统,涉及 HTTP 请求、数据解析、持久化存储、阈值判定和通知推送,覆盖了绝大多数 Python 自动化项目的核心链路。
这个方案适合有一定 Python 基础、想练手爬虫和定时任务的人,也适合懒得天天查电费、想在宿舍低成本搭一套可用工具的在校生。整个过程不需要高端硬件,一台能跑 Python 的电脑或 Linux 小主机就够了。成本低、见效快,且换到其它校园场景(门禁余额、健身房预约、图书馆座位)时,采集层和数据层的代码基本可以平移复用。
2. 摸清电费查询接口,先解决数据怎么来的问题
2.1 查询入口的三种形态,以及各自的应对思路
宿舍电费查询系统在不同学校差异极大,但抽象下来无非三种形态:
- 校内网页版查询,通常是 HTML 表单提交或 AJAX 请求返回 JSON。
- 微信公众号或小程序内嵌的 H5 页面,底层仍是 HTTP API,但可能在请求头里带 token。
- 第三方聚合应用(如完美校园、易校园),接口加密程度较高,需要动态签名。
最稳妥的第一步是用浏览器开发者工具(F12)抓包,找到真正返回密钥数据的那个 XHR 请求,而不是去解析 HTML 页面。因为很多页面上的数字是异步加载的,直接 requests 拉页面源码经常拿不到电量值。找到请求后,把 URL、请求方法、请求头、参数复制出来,在 Python 里用 requests 库原样复现一遍,能通就说明接口可用。
2.2 一个最小可用的采集器,先跑通再管异常
在写完整的监控程序之前,先做一个只执行一次的手动抓取脚本,确认接口通、字段对。下面这段代码以常见的 JSON API 为例,演示最基础的采集流程:
import requests import json # 从浏览器开发者工具里复制的完整 URL url = "https://your-campus.edu/api/dorm/electricity" # 浏览器请求头里挑这几个复制的就够了,Cookie 是登录态关键 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Cookie": "sessionid=your-session-id", "Referer": "https://your-campus.edu/dorm/" } # 有些接口用 GET 带查询参数,有些用 POST 提交 JSON params = { "room_id": "5-423", "type": "room" } resp = requests.get(url, headers=headers, params=params, timeout=10) resp.raise_for_status() # 4xx/5xx 直接抛异常 data = resp.json() print(json.dumps(data, ensure_ascii=False, indent=2))这段代码里有几个参数值得说明:timeout=10是必需的,宿舍网络不稳时,没有超时上限的请求可能卡住整个任务;raise_for_status()会在接口返回 404 或 500 时直接抛出异常,避免拿错误页面当正常数据存库;headers里的User-Agent和Referer是为了尽量模仿浏览器行为,部分校园系统会对非浏览器请求做拦截。
如果打印出来发现数据嵌套很深,别急着写解析逻辑。先把 JSON 的层级结构看清了,再决定是直接索引取值,还是用jsonpath这类库做模糊匹配。常见做法是先把原始响应保存一份到本地文件,防止后续程序改坏了回不去现场。
2.3 遇到加密参数怎么办
不是所有学校系统都这么直白。有的接口需要sign签名参数,有的 token 是登录后从别的接口拿的。我的建议是分三步排查:
- 请求头里是否缺了必要字段,尤其是
Authorization或X-Token,这通常需要登录后动态获取,不适合硬编码。 - 整个请求是否有前置步骤,比如先访问一次 H5 页拿到合法 token,再带 token 请求真正接口。可以让 Python 脚本先模拟一次登录或者 curl 一下前置接口。
- 签名算法本身是否为前端全动态计算。如果发现在 JavaScript 里动态拼接,可以考虑用
pyexecjs或node子进程执行同一段签名逻辑,而不是自己去逆算法。
这里有个容易被忽略的细节:Cookie 往往有过期时间,对监控程序来说,必须保证程序在任何一次运行失败时能感知到 Cookie 时效问题。我一般会让程序在返回数据里识别“请重新登录”或跳转登录页的标记,遇到这种情况就把告警发出来而不是静默重试。
3. 数据落地和阈值判定:SQLite 存储与告警触发
3.1 为什么要用 SQLite 而不是直接写文件
采集到的电量是一个随时间变化的数值,天然适合表结构存储。方案上有三个选择:
- 直接追加写入 JSON 或 CSV:简单但查询趋势时要全量读再过滤,数据量大后效率低。
- 部署 MySQL 或 PostgreSQL:宿舍环境没必要,额外增加安装和维护成本。
- SQLite:单文件、零配置、Python 内置
sqlite3模块支持,最适合这种个人级监控任务。
SQLite 在大约 100MB 数据量以下表现足够好,电费半小时一条的记录,攒一年也就一万多条,完全在舒适区内。而且 SQLite 文件可以随时拷贝备份,扔到网盘或者传到手机上都行。
数据库只需一张表,核心字段是:记录时间、宿舍房间号、剩余电量(千瓦时)、余额(如果有)、原始响应备注。一般不需要复杂索引,给读取时间加一个普通索引足够。
3.2 建表、写入和查询的完整代码
下面的代码实现了建表和插入监控数据的完整逻辑。要确保脚本在初次运行时自动建表,不需要提前手工创建。
import sqlite3 from datetime import datetime DB_PATH = "dorm_electricity.db" def init_db(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS electricity_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, room TEXT NOT NULL, record_time TEXT NOT NULL, remaining_power REAL NOT NULL, balance REAL, remark TEXT ) """) cursor.execute(""" CREATE INDEX IF NOT EXISTS idx_record_time ON electricity_records (record_time) """) conn.commit() conn.close() def save_record(room, power, balance=None, remark=""): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( "INSERT INTO electricity_records (room, record_time, remaining_power, balance, remark) VALUES (?, ?, ?, ?, ?)", (room, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), power, balance, remark) ) conn.commit() conn.close() def query_recent(room, limit=7): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( "SELECT record_time, remaining_power FROM electricity_records WHERE room = ? ORDER BY record_time DESC LIMIT ?", (room, limit) ) rows = cursor.fetchall() conn.close() return rows这里的要点是参数化查询。另外REAL类型用于存储浮点电量值,精度对电费监控够用;AUTOINCREMENT可以防止历史记录被删后 ID 复用,虽然这不是必须,但能避免一些数据迁移上的小麻烦。每次写入前不必手动检查表是否存在,因为init_db()在脚本入口调用一次即可。
3.3 阈值判定的两个关键参数:阈值本身和防抖窗口
只存数据不告警毫无意义。但阈值判定不能简单写成“电量小于 X 就告警”,因为接口返回可能存在瞬时抖动或解析错误,一旦误判就会深夜给宿舍群发消息,很容易吵到室友。
常见的做法是双重判定:
- 当前电量低于设定阈值,比如 5 度。
- 连续 N 次采样(或者持续超过 M 小时)都低于该值,才触发告警。
第二种条件能过滤掉偶发的瞬时异常。整套判定逻辑用状态来描述比用散落的 if 语句更清晰。
class PowerMonitor: def __init__(self, room, low_threshold=5.0, consecutive_count=3): self.room = room self.low_threshold = low_threshold self.consecutive_count = consecutive_count self.low_count = 0 def check(self, power): if power < 0: # 负数通常代表欠费,属于紧急状态,直接告警 return "critical" if power <= self.low_threshold: self.low_count += 1 else: self.low_count = 0 if self.low_count >= self.consecutive_count: return "warning" return "ok"这个监控器的设计逻辑比较好地处理了几个边界场景:负数直接进入紧急状态,因为欠费停电和余额不足是两回事;连续计数保持在一个对象内部,不会在外部重复记录状态;阈值和连续次数都可以在构造时指定,便于为不同宿舍设置不同参数。
触发告警后,还需要额外一个机制:告警后不重复通知。否则脚本每 30 分钟跑一次、每次都在阈值以下,消息会轰炸手机。可以用一个简单的is_notified标志位,或者读取上次成功通知的时间戳,间隔超过一定时长(如 12 小时)才再发一条。
3.4 告警推送:服务商邮箱是低门槛方案
告警渠道没有绝对选型,但实际用于宿舍场景的基本三选一:
- 邮件 SMTP:最通用,无需额外安装依赖,使用 Python 标准库 smtplib,适合推送历史趋势图。
- 微信通知:第三方渠道很多,但不是标准协议,需要注册或 token 失效风险,适合有额外需求的用户。
- 钉钉/企业微信机器人:如果有校园组织账号可以直接用,没有的话需要个人注册,入门门槛比邮件高一些。
考虑到这套程序的使用人群是学生,保持低门槛相对重要。下面是基于 QQ 邮箱或 163 邮箱发送告警邮件的模型,需要在邮箱设置里开启 SMTP 授权码。
import smtplib from email.mime.text import MIMEText from email.header import Header def send_alert(subject, content): smtp_server = "smtp.qq.com" smtp_port = 465 sender_email = "your-account@qq.com" auth_code = "your-smtp-auth-code" receiver_email = "target@example.com" msg = MIMEText(content, "plain", "utf-8") msg["Subject"] = Header(subject, "utf-8") msg["From"] = sender_email msg["To"] = receiver_email with smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=30) as server: server.login(sender_email, auth_code) server.send_message(msg)注意SMTP_SSL一般对应 465 端口,SMTP加starttls对应 587 端口,不同邮件服务商的默认方式有差异,写配置时要注意区分。平时测试可以先把告警逻辑跑一次,再决定是否正式启用,避免真正需要告警时才发现发信配置有问题。
4. 可视化面板:用 Flask 把历史电量拉成趋势图
4.1 展示层的目的是发现规律,不只是看当前数字
命令行直接print电量数据,适合调试,但不适合长期使用。人眼从一大片数字里找趋势很吃力,但折线图能一眼看出:某个时段电量下降特别快、是不是最近空调开得太猛、周末用电和平时差别有多大。
选择 Flask 做 Web 展示,原因很简单:它可以只提供一个静态页面加两个 API 端口,依赖轻、模板不用太复杂,跟数据库共用同一个 Python 进程时数据流转最顺。ECharts 的可视化图表库通过 CDN 引入即可,不需要额外安装前端构建工具链。
4.2 提供一个查询接口,返回最近 N 天的趋势数据
后端只需要一个接口:给定宿舍房间号,返回最近 7 天的电量快照。前端拿到数据后用 ECharts 画折线图即可。
from flask import Flask, jsonify, request import sqlite3 app = Flask(__name__) def query_history(room, days=7): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT record_time, remaining_power FROM electricity_records WHERE room = ? AND record_time >= datetime('now', ?) ORDER BY record_time ASC """, (room, f"-{days} days")) rows = [dict(row) for row in cursor.fetchall()] conn.close() return rows @app.route("/api/electricity") def api_electricity(): room = request.args.get("room", "5-423") days = int(request.args.get("days", 7)) return jsonify(query_history(room, days)) @app.route("/") def index(): return open("templates/index.html", encoding="utf-8").read() if __name__ == "__main__": app.run(host="0.0.0.0", port=8080, debug=False)这里值得说的参数是host="0.0.0.0",它让宿主机上其他设备(比如手机)也能通过内网 IP 访问这个页面,而不仅是 localhost。debug=False在生产运行时不开启调试模式,避免调试器暴露真实代码路径。datetime('now', '-7 days')是 SQLite 内置的时间范围过滤语法,比传入 Python 字符串更直接。
前端页面用 ECharts 并不复杂,最重要的是理解它的数据流。fetch拿到后端 JSON 后,把时间塞进 xAxis 的 data,再把电量数值塞进 series 的 data,其余样式按需调整即可。前后端分离的程度越小,越容易维护。
4.3 显示数据时要处理两个常见问题
前端直接展示数据时会发现两个麻烦:
- 采集中断导致数据点缺失,折线图会出现断档。
- 不同时间点的电量可能都是满值(比如刚充完电),图会显得太平,看不出下降。
针对第一点,前端通过connectNulls: false让缺失部分断开更直观;针对第二点,可以在展示时追加一条余额或日增耗电量的辅助折线,让用户看到一天用掉多少而不只是存量。在后端补齐这条计算逻辑会比前端拼接方便得多。
5. 部署、定时调度与排查:让脚本自己跑起来
5.1 定时策略怎么定才对
定时轮询频率不宜过高。学校查询接口谈不上多大的吞吐压力,但过于频繁的请求既会给服务器增加负担,又可能触发反爬策略。半小时一次是比较平衡的频率,可以覆盖断电前的紧急告警需求,也不会造成数据冗余或接口拦截。
Windows 平台用任务计划程序,Linux 或树莓派用 crontab,macOS 可以挂 launchd。对于绝大多数宿舍场景,一个 cron 表达式就可以完成任务。下面是一个达到同样目的的 Linux crontab 示例:
*/30 * * * * /usr/bin/python3 /home/yourname/dorm_monitor.py >> /home/yourname/monitor.log 2>&1这个表达式的含义是每 30 分钟执行一次监控脚本,并把所有标准输出和错误输出追加到monitor.log。2>&1粗糙但有效,排查问题全靠这个日志。如果你使用 Windows 做定时任务,别忘了在任务计划里的程序路径填上python.exe的完整路径,工作目录设置到脚本所在文件夹,否则相对路径会全部失效。
5.2 脚本自身的异常兜底
监控程序跑久了之后,最常见的崩溃原因是网络、Cookie 过期和学校系统改版。理想情况下,定时任务应在任何异常时通过告警渠道通知你,而不是默默死在日志里。
import traceback def main(): try: data = fetch_data(room_id) save_record(room_id, data["power"], balance=data.get("balance")) monitor.check(data["power"]) if monitor.status in ("warning", "critical"): send_alert(room_id, f"当前余电 {data['power']} 度") except Exception: send_alert("电费监控程序异常", traceback.format_exc()) if __name__ == "__main__": init_db() main()把traceback.format_exc()塞进邮件内容,失败那一刻的堆栈信息就保留下来了。这一点价值极高:很多问题发生在你没盯屏幕的时候,只有日志和告警才能还原现场。
5.3 排查时看日志的具体指标
跑起来之后,日志里重点盯三类指标。
第一类是请求耗时。校园网高峰期可能出现超过 30 秒的响应,这会拖慢一轮轮询,需要针对性地调整timeout到合理值。
第二类是数据结构变化。如果学校后台加了一个字段或改了 JSON 结构,你解析代码会直接抛 KeyError 或 IndexError,日志会在第一时间暴露问题。
第三类是告警次数是否合理。如果一个月内连续触发超过 10 次电量低告警,说明阈值设得太高或者实际用电太快,需要用可视化趋势图确认是哪种情况,再决定调整阈值还是优化提醒逻辑。
5.4 一个验证脚本自洽性的技巧:回放历史数据
在正式跑监控之前,用一个已经积累好数据的数据库文件,从所有历史记录中模拟逐条注入监控类,这样可以在不等待真实采样的前提下,验证阈值判定和告警逻辑在各类边界值(0 度、负数、突然跳变的 999 度)下表现是否正常。这本质上是一种用真实数据做回归测试的做法,能把很多逻辑问题在部署前就消灭掉。
本文还有配套的精品资源,点击获取