基于SNMP的打印机状态监控:从OID采集到Web仪表盘
2026/9/2 3:11:30 网站建设 项目流程

简介:面向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.0STRING包含品牌、型号、固件信息
设备名称1.3.6.1.2.1.1.5.0STRING可自定义的打印机名称
设备状态1.3.6.1.2.1.25.3.2.1.5.1INTEGER状态码,需要对照表解析
错误状态1.3.6.1.2.1.25.3.5.1.2.1INTEGER具体故障码(卡纸/缺纸/开门等)
碳粉余量1.3.6.1.2.1.43.11.1.1.9.1.1INTEGER黑色碳粉当前值
碳粉最大容量1.3.6.1.2.1.43.11.1.1.8.1.1INTEGER用于计算百分比
硒鼓当前页数1.3.6.1.2.1.43.11.1.1.9.1.2INTEGER部分型号此值表示已打印页数
硒鼓最大寿命1.3.6.1.2.1.43.11.1.1.8.1.2INTEGER对比当前值可估算剩余寿命
打印总页数1.3.6.1.2.1.43.10.2.1.4.1.1Counter总打印/复印页数累计
当前任务状态1.3.6.1.2.1.25.3.2.1.5.1INTEGER区分空闲/打印中

注意:不同品牌对“碳粉余量”的OID实现略有差异,比如部分机型会走厂商私有MIB而不是标准的Printer MIB。我列的这组OID覆盖了绝大多数惠普和兼容PCL语言的设备,兄弟和佳能部分新机型也能识别。遇到取不到数据的设备,最稳妥的办法是使用MIB Browser扫描整棵Printer MIB树,找到实际赋值的那条分支。

3.2 状态码解析:一个容易翻车的地方

很多教程把打印机状态码直接当普通整数处理,这个做法非常容易被坑。Printer MIB里的状态码是一个位掩码(bitmask),不是简单的枚举值。常见值含义是:

状态值二进制含义
10000 0001其他状态(需细查子状态)
20000 0010未知状态
30000 0011空闲
40000 0100正在打印
50000 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_alert

3.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库里能拉到打印总页数,定期对比增量就能统计各部门的实际打印量,对行政成本分摊有直接价值。

我自己的下一步计划就是把这些数据做成一个月的趋势报表,看看哪台打印机实际使用效率最高、哪台是问题大户,然后把结论反向输出给设备采购决策。当实时监控跑顺了以后,你会发现自己对设备资产的掌控力不知不觉上了一个台阶。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询