零依赖文件去重、OCR与 docx 表格自动填充
最近遇到一个需求:从一个文件夹里找出重复的文件,再把每个文件的文字内容,按结构填写进一个 docx 表格。
细想之下,这件事其实是三个相互独立的小问题:
- 找重复:如何高效判断两个文件的内容完全一致;
- 读文字:文件是图片和 PDF,需要先把其中文字提取出来;
- 填表格:把提取出来的文字按规则填进 docx 表格模板。
这套方案用 Go 实现,OCR 部分借助 macOS 系统能力,零第三方依赖,拷贝即用。下文会给出完整思路与核心代码(字段示例均为占位,可按你的实际文档替换),并重点交代实现要点与踩过的坑。
一、先想清楚要做什么
需求看起来是一件事,但动手前要把它切成三段来想——每一段的解决路径完全不同:
1 | |
「找文件重复」则是另一条独立主线,但它与 OCR 之间还有一层配合关系:同一份材料可能同时存在 PDF 与 JPG 两种格式,字节完全不同,哈希判重发现不了它们。这类「格式不同但内容相同」的重复,只能放到 OCR 之后按内容里的核心标识去重——这个判断划定了两个工具的职责边界。
技术选型方面,去重与填表用 Go:可编译成单文件、无第三方依赖,zip 与哈希都是标准库原生能力;OCR 用 Swift,因为 Vision 框架仅苹果生态可用,且自带的高质量中文识别是当下性价比最高的方案(详见第三节)。
二、找重复文件:先按大小粗筛,再算哈希
核心思路:避免对每个文件都做完整哈希
最朴素的去重是对每个文件算一遍完整哈希,但哈希一个 1GB 的文件很慢,而且绝大多数文件根本走不到这一步。于是方案分两步:
- 按文件大小分组:大小不同,内容必然不同,直接排除;
- 只对同大小的文件计算 SHA-256,哈希一致才判定为重复。
「粗筛在前、精算在后」,实际进入哈希环节的文件少了一个数量级,核心逻辑不过几行:
1 | |
几个值得注意的设计点
空文件是特例:大小为 0 的文件无需哈希,统一按「大小 0」归入一组即可。副作用是——所有空文件会被看成同一组重复,多余的会被移走。这是用简化换来的便利,实践中完全可接受,但需要意识到它的存在。
保留规则要确定:每一组重复文件里「保留谁」必须有一个稳定规则。这里用的是扫描顺序:保留第一个遇到的,其余都算多余副本。
移动前先重命名:多余副本移到目标目录时,直接同名搬移会互相覆盖,所以在原名前加上「大小_序号」前缀;序号是组内第几个副本,天然唯一。
预览优先,默认不执行:移动是破坏性操作,工具默认处于「预览」模式,只打印每一对「源 → 目标」的清单,确认无误后再显式开启执行。
移动而非复制:同一磁盘分区内 rename 是原子的系统调用,不产生额外 IO;跨分区会失败,失败仅记日志并继续,不影响其他组。
工具边界
这个工具只能找字节完全相同的副本。任何「看起来一样但字节不同」的情况(不同格式、重新扫描)都查不出来。它不能解决全部去重问题——剩下的交给第五节的「按标识去重」。
三、OCR:直接信任系统 Vision 框架

选型:为什么不用 tesseract
在 macOS 上做中文 OCR,最容易被忽略的最优解是系统自带的 Vision 框架:
- 内置高质量中文识别,印刷体效果稳定,无需训练模型;
- 零安装:不用装 tesseract 或各种语言包;
- 识别语言直接声明为「中文 + 英文」,中英混排的文档也能处理。
代价是必须用 Swift(或 ObjC)调用,因此方案里需要一个小的 Swift 脚本充当助手,由 Go 主程序负责调用。
识别请求的关键配置
Vision 识别请求的配置很短,几个关键项直接决定了识别质量:
1 | |
两个绕不开的预处理
统一输入形式:Vision 的识别请求只接受 CGImage(位图),需要先做格式转换:
- 图片:直接解码成位图;
- PDF:先用 PDF 渲染管线把每一页画成位图,这里有个关键参数——按 2 倍分辨率渲染:
1 | |
扫描件的字号本来就偏小,1 倍渲染经常错字,升到 2 倍后识别率提升非常明显,代价只是速度略降。
恢复阅读顺序:识别结果是「一组带坐标的文本块」,且坐标系原点在左下角,与常规的左上角习惯相反。因此要按 Y 坐标从大到小排序(见上面代码里的 sorted),才能还原成「从上到下」的阅读顺序。这是后续字段解析能按序进行的前提,也是最容易做错的地方。
分页标记:防止字段跨页粘连
PDF 是多页的,识别完每一页要在结果里追加一个页码标记:
1 | |
Go 端读到这个标记就把正在收集的字段截断、重置状态,否则页尾的半句话会与下一页页首的内容粘连,字段就废了。
进程间的协作约定
- 识别结果逐行打到标准输出,Go 端逐行读取;
- 一切错误(打不开文件、识别失败)打到标准错误,绝不混进结果流;
- 编译自动化:Go 程序首次运行时若发现助手可执行文件不存在,会自动完成编译(并开启优化开关,识别大文件时速度相差好几倍),对用户完全透明。
需要正视的局限
- 只支持 macOS(系统框架强绑定);
- 手写体识别不稳定,印刷体效果最好;
- 排序只用了 Y 坐标,不含左右关系。横排的「标签:值」没问题,但多栏排版会出现左右栏文字串行——这是使用本方案前必须评估的硬边界。
四、docx:把它当成一个 ZIP 包来改
docx 的本质
.docx 不是一个二进制格式,而是一个 ZIP 压缩包。正文在包内的 word/document.xml 里,样式、字体、页眉页脚是另外几个独立的 XML。所以「填表格」本质上就是 两个动作:
- 解包,改
word/document.xml里的表格; - 重新打包,其余文件原样复制回去。
了解这个本质之后,就不再需要任何第三方 docx 库——一个 zip 库 + 字符串处理就够了。
解析:把「配置」和「业务」分离
整套方案里最值得借鉴的是「配置与业务分离」:把一切与具体文档字段相关的内容抽成文件顶部的配置项,解析逻辑本身保持完全通用。配置分两类:
1 | |
- 字段标签:表格每一列对应正文里以什么词开头。命中标签前缀的行,就开始收集该字段;
- 分隔词:正文里一些与数据无关的杂文本。命中的行代表当前字段到此结束。
字段解析是一个状态机:命中标签 → 进入收集态;命中分隔词 → 结束收集。这样正文里一行放不下的多行内容(如人名列表换行)也能被正确合并进同一字段:
1 | |
三个 OCR 特有的坑
标签被断行:排版原因,OCR 经常把标签拆成两行,比如「名」和「称:xxx」。拼回去的规则很朴素——当前行恰好是某标签的前缀,且下一行以标签剩余部分开头,就合并:
1 | |
这个规则简单但极其有效。
形近字混淆:OCR 常把数字 0 认成字母 O(反过来也一样)。编码类字段提取出来后,统一做一次字符纠正,效果立竿见影。
字段清洗分层降级:以「带年份前缀的编码类字段」为例,提取按优先级逐级退让——
1 | |
提取顺序是:优先匹配标签后面的值 → 退而从整行里找字母数字混合串 → 再退而求其次找纯数字串。每层用不同正则,识别质量差时也能兜住一部分结果。
两个兜底策略,进一步压低失败率
- 文件名兜底:文件名往往自带元信息(如「某某-2026-ABC123.jpg」)。当 OCR 失败或没读全时,从文件名里提取年份与编码作为次要来源。方案不依赖它,却能把空字段的比例降下一截;
- 空白过滤:目录里混入的无关图片(截图、照片)OCR 出来也「有字」,却拼不出完整字段。规则是:主字段为空、或除主字段外一无所获的内容,一律判为无效文件,不进表格,只在日志里报一行——这一步直接决定最终表格的干净程度。
写入:模板的三种定位点
填充不是「整体替换」,而是把模板 XML 切成三块——列网格结束处、第一个数据行(表头)、表格结束标记之后——再在表头后逐行插入生成的数据行:
1 | |
这个「切三块、插中间」的写法,比正则替换健壮得多。
写入前要理解的两个 Office 单位
写单元格时有两个单位容易踩坑:
1 | |
- 字号按半磅记:
24不是 24 磅,而是 24 半磅 = 12 磅。写错整体字号会大一号; - 列宽按 twips 记:1 twip = 1/20 磅,模板里抄下来的列宽值不能当像素直接用。
此外,OCR 拿到的文本可能含 & < > 等 XML 特殊字符,写入前必须转义(标准库 encoding/xml 足矣),否则 Word 会直接提示「文件已损坏」。
重新打包:只动一个文件
重新打包的规则很简单:ZIP 里所有条目,除了正文 XML 用新内容,其余一律按原始字节流复制:
1 | |
这样模板的样式、字体、水印、页眉页脚全部原样保留,生成文档的外观与模板完全一致。注意每个生成单元格都要补齐属性块,即使值是空的也要保持完整结构,否则表格的边框与行高会残缺。
五、最后一道工序:排序与去重
填表前的最后一步,对识别结果做两件整理:
1 | |
- 排序:核心标识通常带年份前缀,降序即「新的在前」;没有标识的记录(空串)自然沉到表格末尾,不会被误插进中间;
- 去重:同一份材料的 PDF 与 JPG 各识别了一次,标识必然相同,同标识只保留一条——这正是第一节所说的「格式不同但内容相同」的重复,只有在这一步才除得掉。
两个操作都遵循「先排序、再去重、保留靠前一条」的顺序,保证结果稳定可复现。
六、踩坑清单(快速回顾)
把整个过程中值得记住的点浓缩成一份对照清单:
- 先粗筛再精算:按大小分组后只哈希同大小的文件,避免全量哈希的开销;
- 空文件是一组:size=0 不哈希,所有空文件视为同组;
- 破坏性操作默认预览:移动类操作先列清单,确认后再执行;
- PDF 要 2 倍渲染:1 倍渲染的小字号识别率明显下降;
- Vision 坐标系左下角:按
maxY降序才能还原从上到下的阅读顺序; - 分页要截断:跨页字段会粘连,必须用分页标记重置状态;
- 把业务抽成配置:字段标签、分隔词都在文件顶部配置,逻辑与业务解耦;
- 清洗分层降级:标签匹配 → 整行匹配 → 降级匹配,逐级退让;
- 文件名是第二数据源:OCR 读不到时从文件名提取,兜底但不过度依赖;
- 无效内容要过滤:拼不出完整字段的文档直接排除,保证表干净;
- 模板切三块插中间:属性 + 表头 + 尾部,比正则替换稳健;
- 记清两个 Office 单位:字号半磅、列宽 twips,错了整体变形;
- XML 必须转义:特殊字符不转义,Word 直接报损坏;
- 重打包只动正文:其余条目字节级原样复制,保留外观。
结语
这套方案最大的收获,不是某一处技巧,而是把一个大需求拆成三个无依赖的小工具:文件去重、系统 OCR、docx 模板填充。每一件单独来看都很朴素,组合起来却解决了一个真实的批量归档问题。
如果你也要处理类似的「批量识别 + 填表」需求,直接套用这三段式骨架,把字段配置换成自己的文档结构,剩下的交给状态机与分层清洗去兜底。最后切记:任何自动化方案,都要留一条「日志可查、结果可人工复核」的退路。