旧系统不开放接口怎么办?我用 OCR + 纠偏字典把告警数据"抠"了出来
旧系统不开放接口怎么办?我用 OCR + 纠偏字典把告警数据"抠"了出来
作者:ITMan彪叔(公众号「ITman彪叔」)| 转载请注明出处
楔子
事情要从一台老系统说起。
它不是什么新潮的云平台,而是一个年代久远的工业/监控类系统,界面还停留在十年前的样子,告警列表在屏幕上慢悠悠地滚。本来它安安静静地跑着,谁也不想动它。但新需求来了:业务方要把这些系统里产生的**告警(Alarm)**集中起来,做统一分析、做看板、做追溯。
理想情况,这应该是一次普通的对接——拿个 API、读一下库表、至少导个 Excel 呗。然而现实啪啪打脸:
- 没有 REST 接口;
- 不给数据库直连权限;
- 界面上连个"导出"按钮都欠奉;
- 唯一能看到告警全貌的地方,就是那块常年亮着的屏幕。
换句话说,这系统把数据"焊死"在界面里了。
上张它的真容,大伙儿感受一下:

老旧系统告警界面实拍:满屏红色告警行滚动,表头恰好是我们要抽的六个字段(Detection Time / Alert / Station / System / Equipment / Reception Time)。注意中间那条*黑色高亮行**——它就是"当前选中行",记住它,后面第二节会讲到怎么靠颜色把它逮出来。界面上没有任何导出按钮,数据只能看、不能拿。*
那怎么办?两条路:要么安排人每天盯着屏幕抄,要么让程序替我们把屏幕"读"下来。前者不现实,后者听着像野路子,但在这类"无接口老系统"的对接里,它其实是最务实、也最常见的方案:把 UI 当成接口,把屏幕当成 API,用 OCR 把可见内容重新结构化。 我们这次干的就是这事。下面把过程掰开讲讲,顺手上了几张图瞅瞅。
一、引擎选型:踩过坑,才知深浅
OCR 引擎一大把,但真落到"老系统那种字体怪、表格密、中英文混排"的场景,差距一下就出来了。我们前后试了三个,交了三笔学费。
先上张对比图,心里有个谱:

1.1 第一站:Tesseract —— 开源老将,却水土不服
Tesseract 几乎是 OCR 的代名词,Java 侧用 tess4j 就能接。我们第一版就用的它,还认真做了图像预处理:灰度、反色、自适应阈值、高斯降噪。
// TesseractOCR.processImage —— 为 Tesseract 做的图像预处理
Mat image = ImageUtils.bufferedImageToMat(bufferedImage);
Mat gray = new Mat();
Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY); // 灰度
Core.bitwise_not(gray, gray); // 反色(黑底白字)
Imgproc.adaptiveThreshold(gray, processed, 255,
Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 11, 2); // 自适应二值
Imgproc.GaussianBlur(processed, denoised, new Size(5, 5), 0); // 降噪
识别时开了中文+英文、LSTM 引擎、自动版面分割:
tesseract.setLanguage("chi_sim+eng");
tesseract.setPageSegMode(3); // 自动页面分割
tesseract.setOcrEngineMode(3); // LSTM 神经网络引擎
结果不理想。 老系统界面有几个典型特征:特殊字体、密集的表格线、中英文混排、还有花哨的背景色。这些对通用训练的 Tesseract 来说全是"干扰项"——错字多、字段错位、表格列经常粘成一团。这第一笔学费告诉我们:通用 OCR 在干净文档上好用,在老系统 UI 截图上往往翻车。
1.2 第二站:百度 C++ OCR —— 效果好,但要花钱
被 Tesseract 劝退后,我们试了某商业 OCR(百度系的 C++ 版本)。识别质量确实上来了,错字少、版面理解强。但它有两个硬伤:
- 收费。长期跑、全量识别,成本不可忽略;
- 集成重。C++ 引擎要塞进我们的 Java(Spring Boot)工程,要么 JNI 要么起独立进程通信,运维复杂度陡增。
商业引擎让我们确认了一件事:方向没错,引擎够强就能解老系统的问题;但"自主可控 + 零成本 + 本地离线(数据安全)"的要求,逼着我们再找。
1.3 第三站:RapidOCR —— 开源、免费、够用
最终落地的是 RapidOCR:它是 PaddleOCR 的 ONNX 移植版,有现成的 rapidocr-onnx-platform 能直接在 Java 里跑,CPU 即可,完全离线。
// OcrUtils.java —— 主识别引擎
static InferenceEngine engine = InferenceEngine.getInstance(Model.ONNX_PPOCR_V3);
public static OcrResult doOcrByRapid(String imagePath){
ParamConfig paramConfig = ParamConfig.getDefaultConfig();
OcrResult ocrResult = engine.runOcr(imagePath, paramConfig);
return ocrResult;
}
实测下来,RapidOCR 的精度已经非常接近商业方案,关键是免费、可离线、不依赖外网(对工业数据尤其重要)。选型就此敲定:RapidOCR 作为主识别引擎。
选型小结一句:别一上来就自研或重金买商业。先用成熟引擎低成本试水,踩坑的成本远低于盲猜。Tesseract 教会我们"老 UI 截图不简单",百度教会我们"强引擎确实能解",RapidOCR 则在成本和效果之间找到了平衡点。
二、真正的难点:不是"认字",是"读表"
很多人以为 OCR 的终点是"把图里的字认出来"。错。那只是起点。
我们要的不是一坨文字,而是告警列表里"当前那一行"的结构化字段:DetectionTime / Alert / Station / System / Equipment / ReceptionTime。把一整张图丢进 OCR,出来的是一堆乱序的文本块,字段和值根本对不上。
所以我们设计了一条结构化抽取流水线(OCRRunner.doOcr()),核心五步:
整图 OCR → ①定位表格窗口 → ②锁定"选中行" → ③检测表格竖线 → ④解析表头列位置 → ⑤逐列切分再识别
上张全景图,先把链路看全:

① 定位表格窗口
OCR 出的文本块里,先找到标识表格区域的关键字。我们用告警窗口标题里的 Alerts ( 来锚定:
// OcrUtils.locationWindow
if (line.contains("Alerts (") || line.contains("Alerts(")) {
locationRect = parseTextBlockRect(textBlock); // 取该文本块的包围盒
break;
}
② 锁定"选中行"——一个颜色上的巧思
老系统里,列表的"当前选中行"通常是高亮的(红/蓝底)。我们没法靠文字判断哪行是"当前行",但可以靠颜色。思路是:从表格区域往下,逐行切片,统计每一行区域的平均像素颜色,找出那个"和大家不一样"的高亮行:
// OcrUtils.locationSelectRow(节选)
for (int j = 0; j < 20; j++) {
rectangle.y = y + (int)(23.8 * j); // 按行高逐行下移
Rect roi = new Rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
Mat roiMat = new Mat(mat, roi);
Scalar mean = Core.mean(roiMat); // 该行的平均颜色
if (mean.val[2] < 100) { // 某颜色通道低于阈值 → 判定为高亮选中行
rowLocationRect = new Rectangle(roi.x, roi.y, roi.width, roi.height);
break;
}
}
这步是整条流水线里最"取巧"也最实用的设计:不选文字,选颜色。只要老系统用高亮色标记当前行,我们就能源位定位,不依赖任何文案约定。
③ 检测表格竖线
有了行、有了区域,还要知道"列"在哪。用 OpenCV 的 Canny + 霍夫变换把表格的竖直分隔线找出来,作为列边界:
// OcrUtils.findVerticalLines
Mat gray = gray(src);
Mat canny = canny(gray);
Mat lines = HoughLinesP(canny); // 霍夫直线检测
for (int i = 0; i < lines.rows(); i++) {
double[] line = lines.get(i, 0);
if (isAlmostSame((int)line[0], (int)line[2])) { // x1≈x2 → 近似竖线
verticalLines.add(line);
}
}
④ 解析表头,建立"列 → 位置"的映射
用一组表头关键字反查每列的位置。这里先埋个伏笔——表里直接容忍了 OCR 的错字:
// OcrUtils.parseTableHeaderRects(节选)
String[][] headers = {
{"Detection Time"},
{"Alert"},
{"Station"},
{"Sustem","System","Svstem"}, // System 常被识别成 Sustem / Svstem
{"Eouioment","Eauioment"}, // Equipment 的错字变体
{"Receotion Time","Recention Time"} // Reception Time 的错字变体
};
看到没?System 的错字 Sustem/Svstem、Equipment 的错字 Eouioment/Eauioment 被直接写进了匹配规则。这其实就是"纠偏"思想的第一种落地形式:在语义层面对已知误识别做容错。
⑤ 逐列切分,再识别
拿到列边界后,把每一列单独 crop 出来,重新 OCR,得到干净的字段值:
// OCRRunner.doOcr(节选)
for (int i = 0; i < columnLinesArray.length - 1; i++) {
Rectangle rect1 = new Rectangle((int)line[0], rowLocationRect.y, ...);
Mat roiMat = mat.submat(new Rect(rect1.x, rect1.y, rect1.width, rect1.height));
Imgcodecs.imwrite(root + "\\ocr\\column-" + i + ".jpg", roiMat); // 存列图
OcrResult ocrResult2 = OcrUtils.doOcrByRapid(title); // 单列再识别
columnOcrResults.put(columnNames[i], ocrResult2.getStrRes().trim());
}
至此,一张告警截图 → 一行结构化告警记录,才真正成型。
放一张程序实跑的截图,大伙儿感受一下——日志输出和上面五步是一一对应的:

程序实跑效果:填上 RTSP 地址(或者勾选"使用测试图片"用历史截图),点"文字识别",日志区依次输出:OCR 识别开始 → 定位窗口位置 → 定位选中行位置 → 检测垂直线位置 → 六列逐列识别结果(DetectionTime/Alert/Station/System/Equipment/ReceptionTime)→ 存在告警记录则更新、不存在则新增。一条龙跑完,告警就落库了。
这一节的结论是:结构化抽取的难度,往往比"整图 OCR"高一个量级。 真正的功夫在 OCR 之后——几何定位、列切分、语义对齐,这些后处理才是老系统对接的胜负手。
三、关键一招:纠偏字典
技术上能跑了,但很快撞上第二个现实:OCR 永远不是 100% 准的,尤其老系统那点特殊字体。一个字母看走眼,字段就错位,入库的数据就脏。
我们为此设计了一套"纠偏字典"机制,代码里分四种形态:
形态一:标点 / 空格归一化
老系统的时间串可能是 25/09/2025 00:00:02,也可能是 25/09/2025 00:00:02(多空格),标点还可能是中文的。先归一化再比对:
// OCRRunner.dictReplace
public static String dictReplace(String str){
str = str.replace(" ","");
str = str.replace(",",",");
str = str.replace(":",":");
return str;
}
形态二:表头错字容忍(已落地)
就是上面第二节④里那张 headers 错字表——把高频误识别直接当"同义词"匹配。
形态三:时间格式鲁棒解析
同一含义的时间,OCR 可能吐出有无空格、有无毫秒的不同串,解析时一并兼容:
// Utils.parseTime —— 同时兼容多种时间格式
Pattern pattern1 = Pattern.compile("(\\d{2}/\\d{2}/\\d{4})\\s*(\\d{2}:\\d{2}:\\d{2})");
Pattern pattern2 = Pattern.compile("(\\d{2}/\\d{2}/\\d{4})\\s*(\\d{2}:\\d{2}:\\d{2}:\\d{3})");
// 还有明确空格分隔的 pattern3 / pattern4 ……
形态四:可运营的通用字典(设计已就绪,待填充)
我们还写了一个通用的、可运营的纠错字典,运营/实施同学不用改代码就能补充规则:
// CorrectDeviationDict.java
// deviation_dict.txt 内容形如: RADARISTATION=RADAR STATION
private static Map<String,String> readDict(){
// 逐行读取 key=value,装入 deviationDict
}
public static String correctDeviation(final String str){
deviationDict.keySet().forEach(key ->
result[0] = result[0].replace(key, deviationDict.get(key)));
return result[0];
}
说句实话:目前这个通用字典文件还是空的,也还没完全接入最终列结果。但这恰恰点出了"纠偏"的本质——它不是一次性的技术,而是一个需要持续运营的活字典。每一次真实误识别,都是往字典里加一条规则的机会。等字典沉淀够了,OCR 的准确率会被"人工经验"再拔高一截。
一句话总结纠偏字典:它就像给 OCR 配了个校对员,把机器看走眼的地方,按我们积累的"经验"改回来。
四、数据落地:识别完存哪
抽出来的结构化告警,最终落到自己的 MySQL 里。实体很直白:
@TableName("alarm")
public class Alarm {
private String id;
private Date detectionTime; // 检测时间
private String alert; // 告警内容
private String station; // 站点
private String system; // 系统
private String equipment; // 设备
private Date receptionTime; // 接收时间
private String description;
private String identification;// 附加标识(json)
}
这里有个极易踩的坑——幂等。告警是"同一事件"被反复识别的:同一行可能每分钟都被截到一次。如果无脑 insert,表会被刷爆。我们的做法是拿 检测时间 + 设备 当唯一键:
// OCRRunner.doOcr(节选)
Date detectionTime = Utils.parseTime(columnOcrResults.get("DetectionTime"));
String equipment = columnOcrResults.get("Equipment");
String id = detectionTime.toString() + equipment; // 业务唯一键
Alarm existAlarm = this.alarmService.getAlarmById(id);
if (existAlarm != null) {
existAlarm.setAlert(alert);
this.alarmService.updateById(existAlarm); // 已存在 → 更新
} else {
alarm.setId(id); /* 填充字段 */ this.saveAlarm(alarm); // 不存在 → 新增
}
这样重复识别只会更新、不会新增,数据干净,看板也稳。
总结
回过头看,这次"无接口老系统"的对接,本质上是一次用计算机视觉重建数据管道的过程。几点沉淀,跟大伙儿交个底:
- UI 就是接口,屏幕就是 API。 系统不给任何数据出口时,把可见内容重新结构化,是最短路径。
- 选型别恋战。 Tesseract 的学费、百度的成本,都是"先试再定"的代价;最终 RapidOCR 在效果/成本/离线之间取得了平衡。
- 胜负手在 OCR 之后。 定位窗口、锁定选中行(颜色巧思)、检测竖线、解析表头、逐列切分、纠偏——这些几何+语义后处理,才是老系统对接的真正难点。
- 纠偏要产品化。 把纠错规则做成可运营的字典(而非写死在代码里),准确率才能随业务持续生长。
- 入库要幂等。 重复识别是常态,唯一键 + 更新兜底,避免脏数据洪流。
最后补一句题外话:为了能离线验证整套识别逻辑(不必每次都拉着真实视频流),项目里我专门加了一个"使用测试图片"开关——勾上后用 ocr/ 目录里的历史截图跑完整流水线。这个小开关在调试阶段帮了大忙,也顺手修掉了它原本"接通了却不被任何按钮触发"的 bug(改起来就一句话:让"文字识别"按钮改走统一的 OCRRunner.doOcr() 入口)。
所谓万事开头难,难在不开头。天下事有难易乎,干就是了。
本文代码均取自实际项目(Spring Boot + JavaCV + OpenCV + RapidOCR + MyBatis-Plus),为可读性做了节选。
如果这篇对你有点用,欢迎关注公众号「ITman彪叔」,后台回复"OCR"可交流更多老系统对接的坑。也可以加作者微信,一起聊聊计算机视觉怎么在工控场景落地。
浙公网安备 33010602011771号