news 2026/9/11 9:45:37

用Python打造宿舍电费监控系统:爬虫、存储与自动告警实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python打造宿舍电费监控系统:爬虫、存储与自动告警实战

简介:面向高校宿舍管理场景的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 查询入口的三种形态,以及各自的应对思路

宿舍电费查询系统在不同学校差异极大,但抽象下来无非三种形态:

  1. 校内网页版查询,通常是 HTML 表单提交或 AJAX 请求返回 JSON。
  2. 微信公众号或小程序内嵌的 H5 页面,底层仍是 HTTP API,但可能在请求头里带 token。
  3. 第三方聚合应用(如完美校园、易校园),接口加密程度较高,需要动态签名。

最稳妥的第一步是用浏览器开发者工具(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-AgentReferer是为了尽量模仿浏览器行为,部分校园系统会对非浏览器请求做拦截。

如果打印出来发现数据嵌套很深,别急着写解析逻辑。先把 JSON 的层级结构看清了,再决定是直接索引取值,还是用jsonpath这类库做模糊匹配。常见做法是先把原始响应保存一份到本地文件,防止后续程序改坏了回不去现场。

2.3 遇到加密参数怎么办

不是所有学校系统都这么直白。有的接口需要sign签名参数,有的 token 是登录后从别的接口拿的。我的建议是分三步排查:

  1. 请求头里是否缺了必要字段,尤其是AuthorizationX-Token,这通常需要登录后动态获取,不适合硬编码。
  2. 整个请求是否有前置步骤,比如先访问一次 H5 页拿到合法 token,再带 token 请求真正接口。可以让 Python 脚本先模拟一次登录或者 curl 一下前置接口。
  3. 签名算法本身是否为前端全动态计算。如果发现在 JavaScript 里动态拼接,可以考虑用pyexecjsnode子进程执行同一段签名逻辑,而不是自己去逆算法。

这里有个容易被忽略的细节: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 就告警”,因为接口返回可能存在瞬时抖动或解析错误,一旦误判就会深夜给宿舍群发消息,很容易吵到室友。

常见的做法是双重判定:

  1. 当前电量低于设定阈值,比如 5 度。
  2. 连续 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 端口,SMTPstarttls对应 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 显示数据时要处理两个常见问题

前端直接展示数据时会发现两个麻烦:

  1. 采集中断导致数据点缺失,折线图会出现断档。
  2. 不同时间点的电量可能都是满值(比如刚充完电),图会显得太平,看不出下降。

针对第一点,前端通过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.log2>&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 度)下表现是否正常。这本质上是一种用真实数据做回归测试的做法,能把很多逻辑问题在部署前就消灭掉。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 9:43:00

Midscene.js 脚本卡顿排查指南:四步定位慢操作并提速

Midscene.js 脚本卡顿排查指南&#xff1a;四步定位慢操作并提速 【免费下载链接】midscene GUI Agent for E2E Testing 项目地址: https://gitcode.com/GitHub_Trending/mid/midscene Midscene.js 性能优化的起点&#xff0c;是先把"慢"拆开看。这篇诊断笔记…

作者头像 李华
网站建设 2026/9/11 9:41:59

认知滞后与技术迭代:如何升级思维应对AI时代

1. 认知革命的滞后性&#xff1a;我们为何仍在用旧模型理解新世界在咖啡馆里听到两位年轻人的对话&#xff1a;"现在AI都能写论文了&#xff0c;我们学这些还有什么用&#xff1f;"这句话让我意识到&#xff0c;尽管技术已迭代数代&#xff0c;但多数人对世界的理解框…

作者头像 李华
网站建设 2026/9/11 9:39:25

低功耗开发实战:从GPIO漏电到AlarmManager滥用的归因分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 9:39:17

Duix.Avatar:30分钟免费部署本地AI数字人

Duix.Avatar&#xff1a;30分钟免费部署本地AI数字人 【免费下载链接】Duix-Avatar &#x1f680; Truly open-source AI avatar(digital human) toolkit for offline video generation and digital human cloning. 项目地址: https://gitcode.com/GitHub_Trending/he/Duix-A…

作者头像 李华
网站建设 2026/9/11 9:38:44

Vircs美国家宽IP倒闭后,Nexip提供真正稳定的住宅IP

加州公寓家宽集体掉队之后&#xff0c;真正能扛风控的住宅 IP 长什么样NexIP住宅ip为你提供解决方案 https://nexip.net/ 去年到今年上半年&#xff0c;中文圈选美西家宽几乎只有一条路径&#xff1a;找一家「自有公寓 AT&T 拉线」的商家&#xff0c;月付三十多刀&#xf…

作者头像 李华
网站建设 2026/9/11 9:37:52

类和对象-下

1.构造函数还有初始化另一种方式&#xff0c;初始化列表初始化列表和构造函数体内 {} 赋值的区别&#xff1a;初始化列表&#xff0c;是对象成员初始化的地方&#xff0c;当函数栈帧创建时&#xff0c;为对象分配内存空间&#xff0c;编译器会先执行初始化列表&#xff0c;直接…

作者头像 李华