🤖 本文由 OpenClaw + DeepSeek 自动生成
一次从远程 OCR 降级到本地 Tesseract 的架构痛史,顺便干掉三个隐蔽 Bug。
背景
我平时跑步用 Nike Run Club(NRC),跑完后截个图发到自己的 Telegram Bot,Bot 会自动把图片存入服务器。我有一个后台 Watcher 脚本,专门监控这个图片目录,一旦发现新图片就 OCR 提取运动数据(距离、时长、配速、卡路里),然后自动归档到 Notion 的 Sports 数据库,实现"跑完截个图,一切自动归档"的极简体验。
本来这套流程跑得好好的,直到前天,突然崩了。
问题现象
截图发过去,Bot 这边显示图片已保存,但 Notion 里就是没有新记录。手动跑脚本测试,发现 OCR 一直超时,偶尔跑出来也是空结果。
开始排查。
Bug 1:远程 OCR 的链路灾难
起初 Watcher 的 OCR 链路是这样的:图片 → image 工具调用外部视觉 API → 返回文本。这套链路的核心问题在于:
- 网络链路绕转:图片先被传送到远程 AI 推理服务器,等返回结果再回传
image工具不支持/tmp路径:Watcher 处理的图片在/tmp临时目录,正好是 image 工具不支持读取的路径- 链路过长:每张图都要走一遍
本地 → 远程 API → 解析 → 返回的完整往返
一张截图 OCR 要等 30 秒以上,频繁超时。
解决:彻底抛弃远程视觉 API,改用纯本地 OCR 链路。
第一步:写了一个预处理脚本,对 Nike 跑步截图(深色背景 + 白色文字)做反色预处理,把白字黑底变成黑字白底,PIL 一行搞定:
img_inv = ImageOps.invert(img.convert('RGB'))
第二步:直接用 pytesseract(本地 Tesseract OCR 引擎)做文字识别:
text = pytesseract.image_to_string(img_inv, lang='eng', config='--psm 6')
结果:3 秒出结果,零外部依赖,零网络开销。
Bug 2:被忽略的双 OCR 处理
修复完 Bug 1 之后,重新跑了一遍,发现每张图还是卡了 6-7 秒。
仔细一看代码——原来老的 Watcher 脚本里,在预处理阶段就已经对原图做了一次 OCR,后续逻辑里又调了一次 OCR,同一张图 OCR 了两遍。翻倍耗时,纯纯的 Bug。
解决:删掉预处理的 OCR 调用,单次反色预处理(1.7 秒)→ 单次 Tesseract OCR(1.3 秒),全程 3 秒完成。
Bug 3:函数重名引发的逻辑静默失效
这是最隐蔽的一个 Bug。
Watcher 脚本里有两个地方都定义了叫 is_run_screenshot 的函数——一个在老代码的轮询逻辑里,另一个在 inotify 响应逻辑里。问题是,按照 Python 的解析规则,后定义的函数完全覆盖了先定义的,而第二个定义恰恰是一个空壳实现,永远返回 False。
所以无论发什么截图过去,脚本的逻辑判断都是"非跑步图片,跳过",但没有任何报错——静默失效。
# 第一个定义(轮询模式)—— 有完整逻辑
def is_run_screenshot(text):
if has_duration and (has_pace or has_decimal):
return True
...
# 第二个定义(inotify 模式)—— 空壳,覆盖了第一个
def is_run_screenshot(text):
return False # 静默失效
解决:删掉第二个空壳定义,确保只有一个 is_run_screenshot 函数生效。同时将识别逻辑改为更宽松的版本:
has_duration = bool(re.search(r'\d+:\d+:\d+', text))
has_decimal = bool(re.search(r'\d+\.\d+', text))
has_pace = bool(re.search(r"\d+'\d+", text))
if has_duration and (has_pace or has_decimal):
return True
Bug 4:轮询间隔 3 秒的滞后感
原来的 Watcher 使用 time.sleep(3) 轮询目录,每次循环检查所有文件。从截图落盘到 OCR 完成,平均要等 5-8 秒。
虽然不影响功能完整性,但在 TG 上发完截图后等几秒才有反馈,体验不够流畅。
解决:引入 watchdog 库的 inotify 机制,改为事件驱动:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class InotifyHandler(FileSystemEventHandler):
def on_created(self, event):
time.sleep(0.5) # 等文件写完成
process_image(path)
实测效果:事件触发 < 0.5 秒,截图刚在 TG 上显示"已接收",Watcher 就启动 OCR 了。
最终架构与演进总结
技术栈
| 组件 | 说明 |
|---|---|
| OCR 引擎 | Tesseract + pytesseract(本地) |
| 预处理 | PIL / ImageOps(反色处理) |
| 文件监控 | watchdog.inotify |
| 归档目标 | Notion Sports 数据库 |
| 运行方式 | nohup 后台常驻进程 |
踩坑清单
- 远程 OCR 链路脆弱:网络抖动、路径限制、超时风险 → 本地 OCR 是唯一正解
- 同图双 OCR:预处理阶段的遗留逻辑与主逻辑冲突 → 单次处理原则
- 函数重名覆盖:Python 同名函数后覆盖前,无任何警告 → grep 搜索函数名做一次全文件审计
- 轮询 vs 事件驱动:3 秒轮询在体验上差了一个量级 → 能用事件就别轮询
当前 Watcher 运行状态
# 进程状态
ps aux | grep run_watcher
openclaw 1848607 ... python3 run_watcher.py
# 日志位置
/tmp/run_watcher.log
# cron @reboot 已添加,重启自动拉起
延伸思考
这次 debug 最值得记住的是 Bug 3:函数重名导致的静默失效。没有报错、没有异常日志,逻辑直接被覆盖成空壳,但脚本一直在正常运行——它只是假装在工作而已。
这类 Bug 比显式崩溃更危险:你以为系统在正常工作,实际上它在假装工作。
从那以后我给自己的所有 Python 项目加了一条规则:项目中不允许出现两个同名的顶层函数定义,CI 脚本里加一行 grep -n "^def is_" *.py | sort 检查全部函数签名。
归档时间:2026-06-05 GMT+8
作者:Longfortt
关联文章:《OpenClaw + 私人中台优化实录》