简介:eMPrint打印监控软件v7.5 Build 20161018是一套面向企业IT管理员与信息安全人员的打印安全管理工具,集打印记录监控、内容审计、刷卡打印、漫游打印、强制水印与二维码标识、打印审核及成本控制于一体,可有效节约打印成本约30%,并杜绝打印泄密。资源包共7个文件,包含exe安装程序、rar客户端卸载工具、pdf与doc安装指导文档、chm帮助手册、txt说明及htm页面,压缩包约39.7MB,覆盖安装部署到日常使用的完整资料。目前已有522人学习下载,适合需要落地打印管控方案、研究打印审计与保密打印实现思路的读者参考。通过安装指导与帮助文档,可快速理解刷卡认证、漫游打印、页眉水印二维码强制标识及多级审核等核心功能的配置逻辑,为实际部署与排错提供依据。
1. 打印监控这件事,为什么老版本反而更值得研究
手里拿到一个eMPrint打印监控软件 v7.5 Build 20161018.zip,第一反应多半是「这么老的包还能用吗」。但真在企业内网待过就知道,打印监控这类工具的价值不在版本号,而在它能不能把「谁、在哪台机器、什么时间、打了多少页、哪个文档」这条链路稳定记下来。2016 年这个 Build 属于典型的 C/S 架构打印审计方案,服务端收日志、客户端挂驱动、后台出报表,功能边界清晰,依赖少,反而比现在一堆云化方案更容易在隔离网里跑通。它适合两类人:一是要给自己小机房做打印成本核算的运维,二是想拆解打印监控原理、自己写一套轻量替代品的开发者。下面我按「装起来 → 看懂它怎么抓数据 → 自己复现核心逻辑 → 避开老包的坑」这条线讲,能直接抄的地方我都给命令和参数。
2. 把服务端和客户端跑起来:环境、端口与最小验证
2.1 先确认这套 C/S 架构的组成和数据流向
打印监控的核心不是「监控」两个字,而是打印任务在操作系统里流转时,哪一层能被截获。Windows 的打印链路大致是:应用调用 GDI/XPS 生成打印作业 → 假脱机程序(Spooler)接收 → 打印处理器渲染 → 端口监视器送到设备。eMPrint 这类软件通常做两件事:在客户端装一个打印处理器或端口监视器,作业经过时把元数据(用户名、文档名、页数、打印机名)抠出来;再由客户端服务把记录发到服务端数据库。服务端一般就是「Web 后台 + 数据库」,负责存储和出报表。
理解这条链路,后面所有配置才有意义。你要监控的是 Spooler 里的作业,而不是网络流量,所以客户端必须装在发起打印的那台机器上,装在打印服务器上只能看到汇总。老版本常见做法是客户端以 Windows 服务方式常驻,服务端用 IIS 或自带小型 HTTP 服务。端口方面,服务端监听一个固定端口(常见 80/8080 或厂商自定义端口),客户端配置里填服务端 IP 加端口。数据库早期多用 SQL Server Express 或 Access,这也是老包部署轻的原因。
提示:先在一台机器上把服务端和客户端装完,用本地打印机打一页测试页,确认后台能出记录,再去铺客户端。别一上来就全网推。
2.2 服务端部署:数据库、端口、初始账号三件事
服务端安装本质就三步:装数据库、跑安装程序、配连接。以常见的 SQL Server 为例,先确认实例名和认证方式,再在安装向导里填。下面是一段用于验证数据库连通性和建库的 SQL,实际库名按安装向导里的默认值来,不要照抄我的名字。
-- 检查当前实例版本,确认是老版本兼容的 SELECT @@VERSION; -- 建一个独立的打印审计库,避免和业务库混用 CREATE DATABASE PrintAuditDB ON PRIMARY ( NAME = N'PrintAuditDB_Data', FILENAME = N'D:\PrintAudit\PrintAuditDB.mdf', SIZE = 512MB, FILEGROWTH = 256MB ) LOG ON ( NAME = N'PrintAuditDB_Log', FILENAME = N'D:\PrintAudit\PrintAuditDB_log.ldf', SIZE = 256MB, FILEGROWTH = 128MB ); GO -- 建一个专用登录名,只给这个库的读写权限 CREATE LOGIN print_svc WITH PASSWORD = N'ChangeThis_2024!'; GO USE PrintAuditDB; GO CREATE USER print_svc FOR LOGIN print_svc; ALTER ROLE db_datareader ADD MEMBER print_svc; ALTER ROLE db_datawriter ADD MEMBER print_svc; GO逻辑说明:@@VERSION先确认实例可用,老软件对数据库版本有上限,太新的实例可能连不上。建库时把数据和日志分开放,FILEGROWTH给固定增量而不是百分比,避免日志暴涨时按比例增长拖慢写入。专用登录名只授db_datareader和db_datawriter,不给db_owner,这是最小权限原则,后面客户端配置里就用这个账号。
参数上重点看两个:数据库文件路径所在盘要有足够空间,打印日志是持续写入,一年下来几十 GB 很常见;FILEGROWTH别设太小,256MB 起步,否则频繁扩文件会产生碎片。安装向导里如果让你选「Windows 身份验证」还是「SQL 身份验证」,选后者并记下账号密码,因为客户端服务通常以本地系统账户运行,用 Windows 集成认证跨机器容易失败。
2.3 客户端安装与最小验证:一页测试页跑通全链路
客户端安装前先确认服务端地址和端口能通。在客户端机器上用Test-NetConnection验证,比 ping 靠谱,因为 ping 通不代表端口开。
# 验证服务端端口是否可达,把 IP 和端口换成你实际的 Test-NetConnection -ComputerName 192.168.10.20 -Port 8080 -InformationLevel Detailed # 查看本机打印后台服务状态,客户端依赖它 Get-Service -Name Spooler | Select-Object Name, Status, StartType # 安装客户端后,确认它的服务是否起来 Get-Service | Where-Object { $_.DisplayName -like "*print*" } | Select-Object Name, DisplayName, Status逻辑说明:第一条确认网络层和端口层都通,TcpTestSucceeded为 True 才算过。第二条确认 Spooler 是 Running 且启动类型为 Automatic,打印监控客户端要挂进打印链路,Spooler 停了它什么也抓不到。第三条是安装后自检,客户端服务名各版本不同,用模糊匹配找出来,状态必须是 Running。
参数上,客户端配置界面里通常要填「服务器地址」「端口」「客户端标识」。客户端标识建议用机器名或资产编号,别用 IP,因为 IP 会变,报表里对不上人。装完打一页测试页,去服务端后台按时间排序看最新记录,能看到文档名和页数就说明链路通了。如果后台没记录,先看客户端本地日志目录,再看服务端数据库里对应表有没有插入,逐段排查。
3. 打印作业是怎么被抠出来的:Spooler、驱动与日志字段
3.1 从打印处理器到端口监视器,哪一层适合挂钩
要在 Windows 上抓打印作业,常见有三个挂载点。第一是打印处理器(Print Processor),作业进入 Spooler 后由它处理,能拿到文档名和数据类型,但拿页数不方便。第二是端口监视器(Port Monitor),作业要出端口时经过它,能拿到目标打印机和字节数,但用户名可能已经丢了。第三是打印驱动本身,改驱动最灵活但兼容性最差,每换一台打印机就要适配。
eMPrint 这类成熟方案一般组合使用:打印处理器拿用户和文档信息,端口监视器拿打印机和页数,两边用作业 ID 关联。自己写轻量替代品时,我建议先只挂打印处理器,把「谁打了什么文档」记下来,页数用文档大小估算,等稳定了再加端口监视器。这样第一版能快速跑通,不会一上来就陷进驱动兼容的泥潭。
注意:挂钩 Spooler 的组件必须以系统权限运行,且要处理并发。多用户同时打印时,作业 ID 会交错,关联逻辑写错就会出现「A 的文档记到 B 头上」这种血泪事故。
3.2 用 PowerShell 和 WMI 先看清打印作业长什么样
在动手写监控之前,先用系统自带能力把打印作业的字段摸清楚。Windows 提供Win32_PrintJob这个 WMI 类,能直接查到当前队列里的作业。
# 列出当前所有打印作业的关键字段 Get-CimInstance -ClassName Win32_PrintJob | Select-Object JobId, Document, Owner, Name, TotalPages, Size, TimeSubmitted | Format-Table -AutoSize # 持续监听新作业,每 2 秒刷一次,用于观察字段变化 while ($true) { $jobs = Get-CimInstance -ClassName Win32_PrintJob | Select-Object JobId, Document, Owner, Name, TotalPages, Size if ($jobs) { $jobs | Format-Table -AutoSize } Start-Sleep -Seconds 2 }逻辑说明:Win32_PrintJob的Owner是发起打印的用户,Document是文档名,Name里通常包含打印机名和作业 ID,TotalPages是页数,Size是字节数。第一条命令一次性看清字段,第二条模拟监控循环,观察作业从出现到消失的过程。你会发现作业在队列里停留时间很短,所以真实监控不能靠轮询,得用事件或挂钩。
参数上,TotalPages有时为 0,因为部分驱动不上报页数,这时只能用Size估算,或者等作业完成后从打印日志里补。Owner在跨域或本地账户场景下格式不同,可能是DOMAIN\user也可能是MACHINE\user,入库前要统一规范化,否则报表里同一个人会分成两条。
3.3 日志表该存哪些字段:一张够用的表结构
不管用现成软件还是自研,打印审计的数据模型都差不多。下面这张表是我自己项目里用过的精简版,字段不多但够出报表。
| 字段名 | 类型 | 说明 | 是否必填 |
|---|---|---|---|
| id | bigint | 自增主键 | 是 |
| job_id | int | 系统作业 ID,用于关联 | 是 |
| user_name | nvarchar(64) | 规范化后的用户名 | 是 |
| machine_name | nvarchar(64) | 发起打印的机器名 | 是 |
| printer_name | nvarchar(128) | 目标打印机 | 是 |
| document_name | nvarchar(256) | 文档名 | 否 |
| total_pages | int | 页数,未知填 0 | 是 |
| size_bytes | bigint | 作业字节数 | 是 |
| submitted_at | datetime2 | 提交时间 | 是 |
| client_ip | varchar(45) | 客户端 IP,兼容 IPv6 | 否 |
逻辑说明:job_id加machine_name做联合索引,因为作业 ID 只在单机唯一。user_name入库前统一成小写并去掉域前缀,报表按人聚合才不会散。total_pages允许为 0,报表里单独标「页数未知」,不要用 0 直接算成本,否则会低估。submitted_at用datetime2而不是datetime,精度更高,多用户并发时排序更准。
参数上,document_name可能很长,截断到 256 字符够用,再长也没人看。client_ip留 45 位是为了兼容 IPv6,虽然内网多半是 IPv4,但字段设计一次到位省得以后改表。索引别建太多,打印日志是写多读少,索引多了拖慢插入,一般只在submitted_at和user_name上各建一个。
4. 自己复现核心逻辑:一个最小可用的打印采集脚本
4.1 用事件日志订阅替代轮询,抓住作业完成瞬间
轮询会漏作业,正确做法是订阅打印服务的事件日志。Windows 的Microsoft-Windows-PrintService/Operational日志里,事件 ID 307 表示作业已打印,字段里有文档名、用户、页数、打印机。下面这段 PowerShell 订阅这个日志并落库。
# 订阅打印服务操作日志,作业打印完成时触发 $logName = 'Microsoft-Windows-PrintService/Operational' $query = @" <QueryList> <Query Id="0" Path="$logName"> <Select Path="$logName">*[System[(EventID=307)]]</Select> </Query> </QueryList> "@ $action = { $xml = [xml]$Event.ToXml() $data = @{} foreach ($node in $xml.Event.EventData.Data) { $data[$node.Name] = $node.'#text' } $record = [PSCustomObject]@{ JobId = $data['JobId'] UserName = ($data['UserName'] -replace '^.*\\', '').ToLower() MachineName = $data['MachineName'] PrinterName = $data['PrinterName'] DocumentName= $data['DocumentName'] TotalPages = [int]$data['PagesPrinted'] SizeBytes = [long]$data['Size'] SubmittedAt = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') } # 这里换成你的入库逻辑,先输出看字段 $record | ConvertTo-Json -Compress | Out-File -Append -Encoding utf8 'D:\PrintAudit\jobs.jsonl' } Register-WinEvent -LogName $logName -FilterXPath "*[System[(EventID=307)]]" -Action $action -MaxEvents 100逻辑说明:Register-WinEvent注册一个持久订阅,事件来了才触发$action,不占 CPU。事件 XML 里的EventData.Data是键值对,转成哈希表后按名字取值。UserName用正则去掉域前缀再转小写,和前面表结构的设计对齐。PagesPrinted转 int,Size转 long,避免大作业溢出。先写 JSONL 文件是为了验证字段,稳定后再改成写数据库。
参数上,-MaxEvents 100控制单次批量,太大占内存,太小频繁触发。EventID=307是作业完成事件,不同系统版本可能有差异,先用事件查看器手动确认。如果日志没开,要在「事件查看器 → 应用程序和服务日志 → Microsoft → Windows → PrintService → Operational」里启用,否则订阅不到任何东西。
4.2 把采集脚本做成服务:用计划任务保活
脚本不能一直挂在控制台里,得做成开机自启并崩溃重启。用计划任务最省事,不用额外装框架。
# 注册一个开机启动的计划任务,以系统权限运行采集脚本 $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument '-NoProfile -ExecutionPolicy Bypass -File "D:\PrintAudit\collect.ps1"' $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' ` -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 1) ` -ExecutionTimeLimit (New-TimeSpan -Days 0) Register-ScheduledTask -TaskName 'PrintAuditCollector' ` -Action $action -Trigger $trigger -Principal $principal -Settings $settings逻辑说明:-AtStartup保证开机就跑,-UserId 'SYSTEM'用系统账户,权限足够挂钩打印链路。-RestartCount 3和-RestartInterval让脚本崩溃后自动重启,-ExecutionTimeLimit设为 0 表示不限时,因为采集脚本是常驻的。注册完用Get-ScheduledTask -TaskName 'PrintAuditCollector'确认状态。
参数上,-ExecutionPolicy Bypass是为了绕过脚本执行策略,内网机器策略严的时候必须加。-NoProfile避免加载用户配置拖慢启动。如果脚本里要写数据库,连接字符串别硬编码在脚本里,放到只有 SYSTEM 能读的配置文件,或者用 Windows 凭据管理器。
4.3 入库与去重:作业 ID 冲突怎么处理
多台机器上报时,作业 ID 会重复,直接插入主键冲突。正确做法是用「机器名 + 作业 ID + 提交时间」做唯一键,插入前先查重。
-- 建唯一索引,防止重复上报 CREATE UNIQUE INDEX UX_PrintJob_Machine_Job_Time ON dbo.PrintJob (machine_name, job_id, submitted_at); -- 插入时忽略重复,SQL Server 用 MERGE 或先查后插 INSERT INTO dbo.PrintJob (job_id, user_name, machine_name, printer_name, document_name, total_pages, size_bytes, submitted_at, client_ip) SELECT @job_id, @user_name, @machine_name, @printer_name, @document_name, @total_pages, @size_bytes, @submitted_at, @client_ip WHERE NOT EXISTS ( SELECT 1 FROM dbo.PrintJob WHERE machine_name = @machine_name AND job_id = @job_id AND submitted_at = @submitted_at );逻辑说明:唯一索引是最后一道防线,即使应用层去重漏了,数据库也会拦住。WHERE NOT EXISTS是插入前去重,比先SELECT再INSERT少一次往返,并发下也更稳。submitted_at参与唯一键是因为同一台机器作业 ID 会回绕,加上时间才能区分。
参数上,submitted_at的精度要一致,采集端和数据库端都用秒级,别一个带毫秒一个不带,否则去重失效。如果日志量很大,MERGE在高并发下容易死锁,用INSERT ... WHERE NOT EXISTS更安全。定期归档老数据,比如只保留 12 个月,超期的移到历史表,主表保持小体积查询才快。
5. 老包部署的避坑清单:从端口冲突到报表对不上
5.1 客户端装了但后台没记录
现象:客户端服务显示 Running,测试页也打了,服务端后台就是没数据。原因通常是客户端配置里的服务端地址填了主机名但 DNS 解析不到,或者防火墙只放行了 ping 没放行端口。解决:在客户端用Test-NetConnection测端口,不通就查防火墙入站规则;通了还不行就看客户端本地日志,多半是数据库账号密码错或库不存在。老版本对数据库版本有要求,实例太新也会连不上,换兼容模式或降实例版本。
5.2 页数统计忽大忽小
现象:同一份文档,有时记 1 页有时记 3 页。原因是部分驱动在作业提交时上报的页数是估算值,实际渲染后才是真实页数,两个事件都触发就会重复计数。解决:只认作业完成事件(307),忽略提交事件;如果驱动不上报完成页数,用Size除以单页平均字节数估算,并在报表里标注「估算」。别两个来源混用,否则报表永远对不上。
5.3 用户名出现两种格式
现象:报表里同一个人出现user和DOMAIN\user两条。原因是不同客户端登录方式不同,有的用本地账户有的用域账户。解决:入库前统一规范化,去掉域前缀、转小写、去空格。如果域内还有同名不同人的情况,保留完整DOMAIN\user但在报表层做映射表,别在采集层丢信息。
5.4 数据库日志暴涨把盘写满
现象:服务端跑几个月后 C 盘满了,服务起不来。原因是打印日志写入频繁,数据库日志文件按百分比增长,一次大作业就能撑爆。解决:建库时FILEGROWTH设固定增量,日志文件单独放数据盘,开启定期日志备份截断。监控脚本里加一条磁盘剩余空间检查,低于 20% 就告警,别等写满了才救。
5.5 老包在新系统上装不上
现象:安装程序双击没反应,或装到一半报错回滚。原因是 2016 年的包可能没签名,新系统默认拦截,或者依赖的运行时组件版本不对。解决:右键属性里解除锁定,用管理员兼容模式运行;依赖组件单独装,别指望安装包自带。如果还是不行,考虑只提取它的数据库结构和报表逻辑,采集层用前面给的脚本自己实现,反而更可控。
6. 从能跑到好用:报表口径、成本核算与长期维护
把链路跑通只是第一步,真正让打印监控产生价值的是报表口径。我踩过的最大坑是「页数」和「成本」直接挂钩:早期报表用总页数乘以单页成本,结果彩色和黑白一个价,财务一看就说不准。后来改成按打印机分组,每台打印机配一个单页成本,黑白和彩色分开,报表才被认可。这个映射表要单独维护,别写死在代码里。
-- 打印机成本映射表,按打印机和色彩模式配单价 CREATE TABLE dbo.PrinterCost ( printer_name nvarchar(128) NOT NULL, color_mode varchar(16) NOT NULL, -- mono / color cost_per_page decimal(10,4) NOT NULL, effective_from date NOT NULL, PRIMARY KEY (printer_name, color_mode, effective_from) ); -- 按月出成本报表,关联打印记录和成本映射 SELECT p.user_name, p.printer_name, SUM(p.total_pages) AS total_pages, SUM(p.total_pages * c.cost_per_page) AS total_cost FROM dbo.PrintJob p JOIN dbo.PrinterCost c ON c.printer_name = p.printer_name AND c.color_mode = CASE WHEN p.document_name LIKE '%color%' THEN 'color' ELSE 'mono' END AND c.effective_from <= CAST(p.submitted_at AS date) WHERE p.submitted_at >= '2024-01-01' AND p.submitted_at < '2024-02-01' GROUP BY p.user_name, p.printer_name ORDER BY total_cost DESC;逻辑说明:成本映射表带effective_from,单价调整后历史报表不变,这是财务最在意的。色彩模式判断这里用文档名关键字是简化写法,真实场景应该从打印作业的 DEVMODE 里取色彩标志,采集层多存一个字段。报表按人和打印机分组,能直接看出谁在哪个设备上花得多。
参数上,cost_per_page用decimal(10,4)保留四位小数,单页成本经常是几厘钱,用 float 会有精度误差。effective_from用日期而不是时间戳,单价按天生效够用。查询里时间范围用左闭右开,避免月底最后一秒的记录漏掉。
长期维护上,我一般做三件事:每月归档一次老数据到历史表,主表只留最近 12 个月;每季度核对一次打印机成本映射,设备换了单价要更新;采集脚本的日志单独轮转,别和数据库日志混在一起。还有一点,打印监控涉及员工行为数据,部署前要和相关负责人对齐用途和范围,只采必要字段,文档名这种可能含敏感信息的字段,报表里做脱敏或只对特定角色可见。这是技术之外但决定方案能不能长期活下去的事。
我自己这些年做下来最大的教训是:别追求一次到位。第一版只要能稳定记录「谁在什么时候打了多少页」,就已经能回答大部分成本问题。色彩、双面、纸张尺寸这些细节,等第一版跑稳了再逐步加字段。老包也好,自研也好,能持续跑三年不出事的方案,靠的不是功能多,而是口径清楚、字段规范、归档及时。希望帮到你。
本文还有配套的精品资源,点击获取