简介:面向IT运维人员、网络管理员及企业办公设备负责人的打印监控知识资源,系统整合了打印机状态监测、打印队列观察、墨盒与纸张余量判断、打印时间预测、文档名称追踪、份数统计等核心概念,并结合SNMP、WMI等远程监控技术及PaperCut、HP JetAdvantage等工具,讲解如何通过实时数据优化打印策略、降低纸张消耗并提前发现故障。压缩包共106个文件,以82个json与16个html为主,json与html主要承载结构化数据和离线页面展示,另有少量zip及releases目录文件作为补充,整体大小约2.25MB,目录按wiki/Render结构组织,便于按知识点浏览。当前已有2281人学习下载,既适合入门者快速理解打印机监控体系,也可作为企业制定打印管理方案、控制运营成本时的参考资料。
1. 项目概述
1.1 为什么我盯上了打印机状态监控
如果你管过超过10台打印机的IT运维,或者在公司行政岗上被“打印机又坏了”“没墨了也不知道”“卡纸了没人管”这类问题轰炸过,你大概能体会我接下来说的场景:每天早上刚到工位,电话就响了——三楼那台打印机的硒鼓没粉了,会议室旁边那台又离线了,财务那台一直提示卡纸但没人会拆。等你跑上跑下处理完,一个上午基本就没了。
我这次做的“实时监控打印机状态”,本质就是把这套“人肉巡检”换成“自动巡检+主动告警”。核心思路是通过网络协议直接读取打印机的运行状态、耗材余量、故障代码、纸仓状态,然后汇总到统一面板,在打印机真正“罢工”之前就发现问题。
说白了,这套东西能解决三件事:第一,你不用再挨个跑过去看打印机面板;第二,耗材余量透明化,采购计划能提前做;第三,故障出现当场就能收到通知,不用等用户来报修。适合谁呢?企业IT运维、行政后勤、连锁门店的设备管理员,甚至家里有好几台打印机并且想折腾一下的个人玩家,都能用得上。
1.2 我踩过的坑和这个项目的整体体验
说实话,打印机状态监控不是什么新鲜事,市面上的软件也不少——但要么贵得离谱,要么绑定特定品牌,要么部署起来重得吓人。我这套方案走的是“轻量+通用”路线,基于SNMP协议做数据采集,配一个Python脚本加一个Web展示面板,就能把市面上主流品牌的打印机状态全量接管过来。
整个项目从零到跑通,我大概花了两天时间。第一天在做协议探测和品牌兼容性测试,第二天在写采集程序和告警逻辑。过程中踩了不少坑,比如不同品牌打印机返回的OID数据格式不一致、部分打印机默认关闭了SNMP服务、还有的打印机状态码不能直接当数字解析……这些我都整理在后面的章节里,避免你重复交学费。
2. 技术选型与方案设计思路
2.1 为什么选SNMP而不是其他方案
想知道打印机状态,路不止一条。我评估过几种常见方案,各有利弊:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| SNMP协议采集 | 通过161端口读取设备MIB库 | 通用性强,几乎所有网络打印机都支持 | 需要设备开启SNMP,OID需查表 |
| 厂商私有API | 如HP Web Jetadmin、佳能iWDT等 | 数据完整,含厂商特有状态 | 绑定品牌,多品牌环境要装N套 |
| Windows打印队列轮询 | 通过WMI读取打印服务状态 | 部署简单,无需碰打印机 | 只能监控已添加到Windows的打印机,且拿不到耗材数据 |
| 网络端口扫描 | 定期检测打印机IP是否能ping通 | 零依赖 | 只能知道“在线/离线”,其他全瞎 |
选SNMP几乎是必然的。一是因为它是标准协议,惠普、佳能、爱普生、兄弟、富士施乐这些主流品牌全部内置支持;二是因为MIB库里的数据维度足够全——设备名、型号、固件版本、状态码、碳粉余量、硒鼓寿命、纸仓状态、打印总页数全都能拿到;三是采集过程非常轻,一个UDP报文就能返回一整页数据,几百台设备也不会有压力。
有人可能会问:直接用厂商的管理工具不好吗?好是好,但你公司里如果同时有HP、佳能、兄弟三种牌子,意味着你要装三个控制台,且它们互不通信,最后还是要在三套界面之间来回切换。SNMP方案一套面板管全部,这才是运维真正想要的。
2.2 整体架构:采集、解析、存储、展示
我最终落地的架构分为四层:
- 采集层:Python脚本按固定周期(我设置的60秒)向打印机IP发送SNMP GET/GETNEXT请求,读取预定义的OID列表。
- 解析层:接收返回值后做数据清洗和转换,把OID对应到人类可读的字段(比如把状态码转成“空闲/打印中/卡纸/缺纸”)。
- 存储层:用SQLite存历史数据,方便回看趋势;实时状态则直接存在内存里供Web面板查询。
- 展示层:一个浏览器访问的轻量仪表盘,按部门/楼层分组展示每台打印机的实时状态、耗材余量条、故障告警卡片。
这个架构的好处是每个环节都能独立替换。比如后面想把数据推到钉钉或企业微信,只需要在解析层加一个webhook推送函数,不用动其他模块。想换成MySQL或PostgreSQL,也只改存储层接口就行。
3. 核心功能拆解与数据采集细节
3.1 关键OID:打印机的“体检报告”入口
SNMP采集的第一步,是搞清楚要从哪里拿数据。打印机MIB库里最常用的是Printer MIB(RFC 1759/3805)和厂商私有MIB。我把自己验证可用的核心OID整理成了可以直接照抄的表:
| 监控项 | OID | 返回类型 | 说明 |
|---|---|---|---|
| 设备描述 | 1.3.6.1.2.1.1.1.0 | STRING | 包含品牌、型号、固件信息 |
| 设备名称 | 1.3.6.1.2.1.1.5.0 | STRING | 可自定义的打印机名称 |
| 设备状态 | 1.3.6.1.2.1.25.3.2.1.5.1 | INTEGER | 状态码,需要对照表解析 |
| 错误状态 | 1.3.6.1.2.1.25.3.5.1.2.1 | INTEGER | 具体故障码(卡纸/缺纸/开门等) |
| 碳粉余量 | 1.3.6.1.2.1.43.11.1.1.9.1.1 | INTEGER | 黑色碳粉当前值 |
| 碳粉最大容量 | 1.3.6.1.2.1.43.11.1.1.8.1.1 | INTEGER | 用于计算百分比 |
| 硒鼓当前页数 | 1.3.6.1.2.1.43.11.1.1.9.1.2 | INTEGER | 部分型号此值表示已打印页数 |
| 硒鼓最大寿命 | 1.3.6.1.2.1.43.11.1.1.8.1.2 | INTEGER | 对比当前值可估算剩余寿命 |
| 打印总页数 | 1.3.6.1.2.1.43.10.2.1.4.1.1 | Counter | 总打印/复印页数累计 |
| 当前任务状态 | 1.3.6.1.2.1.25.3.2.1.5.1 | INTEGER | 区分空闲/打印中 |
注意:不同品牌对“碳粉余量”的OID实现略有差异,比如部分机型会走厂商私有MIB而不是标准的Printer MIB。我列的这组OID覆盖了绝大多数惠普和兼容PCL语言的设备,兄弟和佳能部分新机型也能识别。遇到取不到数据的设备,最稳妥的办法是使用MIB Browser扫描整棵Printer MIB树,找到实际赋值的那条分支。
3.2 状态码解析:一个容易翻车的地方
很多教程把打印机状态码直接当普通整数处理,这个做法非常容易被坑。Printer MIB里的状态码是一个位掩码(bitmask),不是简单的枚举值。常见值含义是:
| 状态值 | 二进制 | 含义 |
|---|---|---|
| 1 | 0000 0001 | 其他状态(需细查子状态) |
| 2 | 0000 0010 | 未知状态 |
| 3 | 0000 0011 | 空闲 |
| 4 | 0000 0100 | 正在打印 |
| 5 | 0000 0101 | 预热中 |
问题在于:有些厂商会在这基础上叠加故障位。比如你拿到的值是“131075”,直接查表根本对不上号——它其实是“3(空闲)+131072(某个厂商私有标记位)”的组合值。正确的处理方法是先按位与(bitwise AND)剥离厂商标志位,再解析标准位。
我在代码里是这样处理的:
def parse_printer_status(raw_value): """ 解析打印机状态码(兼容位掩码场景) raw_value: 从SNMP获取的原始INTEGER值 返回: (主状态文本, 是否告警) """ # 部分厂商会把私有信息写在状态码高位,先用掩码提取低字节 standard_mask = 0xFF base = raw_value & standard_mask status_map = { 1: ("其他状态", False), 2: ("未知状态", False), 3: ("空闲", False), 4: ("正在打印", False), 5: ("预热中", False), 6: ("正在停止", False), 7: ("正在测试", False), 8: ("暂停", True), 9: ("离线", True), } main_status, is_alert = status_map.get(base, ("未知状态", True)) # 再检查高字节的故障叠加位(各厂商不一,常见第25位表示卡纸) if raw_value & (1 << 24): main_status = "卡纸/内部故障" is_alert = True return main_status, is_alert3.3 碳粉余量计算:不是所有设备都直接给百分比
碳粉余量是大家最关心的数据,但这里也藏着一个容易想当然的地方:SNMP返回的往往不是百分比,而是当前值和最大容量值。要拿到余量百分比,得自己算:
def calculate_toner_percentage(current_value, max_value): """ 根据当前值和最大容量计算碳粉余量百分比 """ if max_value <= 0: return None # 数据无效 percent = round(current_value / max_value * 100, 1) return min(percent, 100) # 防止个别设备给出的当前值超过标称容量举个实测的例子,我这边一台惠普M429fdw在刚换完硒鼓后,SNMP返回的当前值是96,最大容量是100——余量96%。用了两周后当前值掉到67,余量67%。这个数值和机器面板上显示的耗材条基本吻合。
但我也遇到过让新手抓狂的情况:某品牌打印机返回的current值反而比上一个周期更大。原因是该机型报告的不是“剩余量”而是“消耗量”,此时需要用剩余量 = 最大容量 - 当前值来倒推。判断起来也简单——如果新装硒鼓时当前值接近100,说明是余量;如果接近0,说明是消耗量;如果完全没规律,建议直接查一下该机型的MIB说明文档。
4. 实操过程:从零搭起一套可用的监控系统
4.1 环境准备与前期探测
我的环境是一台Ubuntu 22.04服务器(2核4G内存完全够用),所有打印机和企业内网互通。开局先做两件事:一是确认目标打印机的IP,二是验证SNMP是否可用。
先装工具和依赖:
# 安装SNMP命令行工具和Python依赖 sudo apt update sudo apt install -y snmp snmp-mibs-downloader python3-pip python3-venv pip3 install pysnmp flask sqlite3然后先用命令行测试读数是否正常:
# 测试读取打印机设备描述,-v2c表示SNMP协议版本,-c public是读团体名 snmpget -v2c -c public -t 3 -r 1 192.168.1.100 1.3.6.1.2.1.1.1.0正常的话会返回类似这样的结果:
SNMPv2-MIB::sysDescr.0 = STRING: HP LaserJet M429fdw Model ...如果提示Timeout,先ping确认IP通不通,然后检查打印机网络设置里的“SNMP”开关是否开启。大部分打印机关闭SNMP的方式是在管理后台取消勾选“启用SNMP”,默认是开启的。还有一部分机型需要手动配置团体名,默认一般是public,有人会改成私有字符串,这种情况你得先拿到正确的团体名才能读取。
4.2 核心采集脚本实现
我写了一个采集脚本,核心结构大概是这样的(重点看轮询和缓存逻辑):
import time from pysnmp.hlapi import * import sqlite3 # 打印机IP列表,可以按部门分组管理 PRINTERS = [ {"ip": "192.168.1.100", "name": "三层前台", "location": "3F前台区"}, {"ip": "192.168.1.101", "name": "财务部", "location": "2F财务区"}, {"ip": "192.168.1.102", "name": "设计部", "location": "2F设计区"}, ] # 需要采集的OID清单 OIDS = { "description": "1.3.6.1.2.1.1.1.0", "printer_status": "1.3.6.1.2.1.25.3.2.1.5.1", "toner_current": "1.3.6.1.2.1.43.11.1.1.9.1.1", "toner_max": "1.3.6.1.2.1.43.11.1.1.8.1.1", } COMMUNITY = "public" # 如果你的网络环境改过团体名,这里对应替换 TIMEOUT = 3 # 秒 RETRIES = 1 def snmp_get(ip, oid): """执行SNMP GET请求,返回纯值""" iterator = getCmd( SnmpEngine(), CommunityData(COMMUNITY, mpModel=1), # mpModel=1 对应SNMPv2c UdpTransportTarget((ip, 161), timeout=TIMEOUT, retries=RETRIES), ContextData(), ObjectType(ObjectIdentity(oid)), lookupMib=False, # 不查MIB名称,直接返回OID,提升性能 ) errorIndication, errorStatus, errorIndex, varBinds = next(iterator) if errorIndication: raise ConnectionError(f"SNMP错误: {errorIndication}") return varBinds[0][1].prettyPrint() def collect_all(): """采集所有打印机状态,返回结构化数据""" results = {} for printer in PRINTERS: ip = printer["ip"] entry = {"ip": ip, "name": printer["name"], "location": printer["location"]} try: entry["description"] = snmp_get(ip, OIDS["description"]) entry["printer_status"] = snmp_get(ip, OIDS["printer_status"]) entry["toner_current"] = snmp_get(ip, OIDS["toner_current"]) entry["toner_max"] = snmp_get(ip, OIDS["toner_max"]) entry["online"] = True except ConnectionError: entry["online"] = False entry["printer_status"] = "离线" results[ip] = entry return results这里要说一个性能细节:SNMP是UDP协议,如果某个IP不可达,默认要等超时才能返回。如果你管理几十台设备,一定要把超时时间设短(我用的3秒)并把重试次数设为1,否则一轮轮询下来可能要好几分钟。另外千万不要用同步for循环去挨个请求,正确做法是开线程池并发采集。我实测过,100台设备用线程池并发(20个线程),一轮采集可以压缩到10秒以内。
4.3 持久化存储与Web仪表盘
采集到的数据如果只在内存里飘着,重启就没了。我用SQLite做了一层轻量存储,建表逻辑很简单:
def init_db(): conn = sqlite3.connect('printer_monitor.db') conn.execute(''' CREATE TABLE IF NOT EXISTS printer_status_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, name TEXT, status TEXT, toner_percent REAL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close()Web仪表盘我直接用Flask起了一个极其轻量的服务,核心路由只有两个:一个输出JSON给前端,一个展示HTML页面。前端用原生JavaScript做轮询刷新,每10秒拉一次/api/status,然后用DOM更新卡片颜色和进度条。没有引入Vue或React这些重型框架,因为这里确实用不上。
仪表盘展示的信息我分成三级:
- 绿色(正常):在线且状态为空闲,碳粉余量高于20%
- 黄色(注意):碳粉低于20%,或状态为预热/正在打印
- 红色(告警):离线、卡纸、缺纸、硒鼓寿命耗尽
这样扫一眼颜色就知道哪台需要处理。告警推送部分我用了一个很简单的方案——状态切换成红色时,通过Webhook往企业微信群发一条消息。如果你用钉钉或者Slack,改一下推送URL就行。
import requests def send_alert(printer_name, status, toner): """推送告警到企业微信群机器人""" webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的KEY" data = { "msgtype": "text", "text": { "content": f"打印机告警: {printer_name}, 状态={status}, 碳粉余量={toner}%" } } requests.post(webhook_url, json=data, timeout=5)5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 某个IP超时 | 打印机离线/SNMP未开启/IP错误 | ping验证连通性,进入打印机后台确认SNMP开关 |
| 能读取设备名,但碳粉值为空 | 该机型不使用标准Printer MIB的耗材节点 | 用MIB Browser扫描整棵OID树,找到实际节点 |
| 状态码始终是“未知状态” | 状态码解析未处理厂商私有标志位 | 按bitmask方式剥离高字节,参考3.2节的解析方法 |
| 碳粉百分比波动异常 | 某品牌返回的是消耗量而非余量 | 确认返回语义,必要时用最大容量-当前值换算 |
| 采集一轮耗时过长 | 有IP不可达,且超时设置偏大 | 将超时调低至3秒,重试次数设为1,并发采集 |
| Web面板偶尔卡住 | SQLite并发写入冲突 | 写入加锁或改用WAL模式,读取和写入分离 |
5.2 几个让你少走弯路的细节
SNMP团体名这个坑我专门说一下。很多安全人员会建议把默认的public改掉,这个建议本身没毛病,但改完后采集端要同步更新。我遇到过某部门把一批打印机团体名统一改成了自定义字符串,结果运维脚本没更新,所有设备一夜之间全部显示离线。所以如果是从别人手里接手的监控系统,第一件事就是核对团体名是否匹配。
另外,不同子网的打印机要先确认SNMP端口(UDP 161)在路由器或防火墙上没有被拦。TCP端口能ping通不代表UDP能通,排查时要特别注意。
还有一个非常容易被忽略的点——部分打印机的MIB数据并不是主动推送的,只有被GET的时候才返回当前值。有些品牌的老机型在睡眠模式下响应会变慢或者不响应,看起来像“离线”。解决思路有两种:一是把监控系统的网络唤醒功能打开,二是在打印机电源设置里禁用“深度睡眠”模式。我实测过,某些机型深度睡眠状态下SNMP响应延迟能从几十毫秒跳到5秒以上,非常影响轮询效率。
最后建议你把采集周期设置成60秒。太短了会给打印机造成无谓的协议压力(很老的型号甚至会因此频繁唤醒),太长了又达不到“实时”效果。我调过30秒、60秒、120秒三档,60秒是对监控体验和设备寿命都最友好的平衡点。
5.3 关于打印机品牌兼容性的实测感受
我这边环境里主要跑的是惠普、兄弟、佳能三种品牌。惠普的兼容性最好,标准OID基本一把梭直接通。兄弟的打印机部分老机型不支持标准Printer MIB,需要走私有OID。佳能的新款支持得不错,但开机自检期间MIB树会短暂不可用,采集脚本里要加异常重试。
如果你手头有爱普生的机器,建议先拿着本文的OID列表测一轮。爱普生商用机型对SNMP的支持还不错,但部分家用喷墨机型直接不开放SNMP功能,这种情况只能用网络唤醒加打印机状态API的方案补足。
6. 这个项目的后续进化方向
现在的方案已经能跑得很稳了,如果还想继续加料,我个人觉得有几个方向值得扩展。
一是把采集到的数据接进现有运维平台。比如对接Zabbix或Prometheus,用打印机的SNMP exporter直接上报到监控大盘,这样就不用在多个告警系统之间来回切换了,所有设备统一视角。二是做耗材采购预测。目前SQLite里已经积累了完整的耗材变化曲线,后续可以做一个简单的线性回归,估算每台打印机碳粉还能撑多少天,当预估剩余天数低于7天时自动生成采购建议清单。三是加入打印量统计维度。从MIB库里能拉到打印总页数,定期对比增量就能统计各部门的实际打印量,对行政成本分摊有直接价值。
我自己的下一步计划就是把这些数据做成一个月的趋势报表,看看哪台打印机实际使用效率最高、哪台是问题大户,然后把结论反向输出给设备采购决策。当实时监控跑顺了以后,你会发现自己对设备资产的掌控力不知不觉上了一个台阶。
本文还有配套的精品资源,点击获取