文档管理这件事,表面上是在解决存储,实际上解决的是检索。我曾经在一台共享目录里堆了上万份 PDF,合同、发票、验收单和技术图纸全混在一起,文件名清一色是日期加流水号,想找某份资料的时候只能让同事挨个翻。直到我把 Mayan EDMS 部署到内网服务器,并且把中文 OCR 彻底配置好之后,才真正告别了这种翻文件夹的日子。
这篇内容把我从选型、部署、配置中文 OCR 到排查问题的整个实操过程都记录下来,包括踩过的坑和测出来的经验。适合正在评估开源文档管理系统、或者已经把 Mayan EDMS 跑起来但中文识别结果不理想的读者。如果你只是想找个地方放文件,那不需要往下看;但如果你需要"扫进来的文件能搜得到字",这文章应该能帮你省不少时间。
1. 为什么是 Mayan EDMS:选型背后的一些考量
1.1 先问清楚:你要的是存档,还是查找?
选型之前我列过一张需求表。表面需求是"把纸质文件电子化归档",但真正让人头疼的需求是:一年后还能根据合同号、客户名或者关键词把文件捞出来。网盘和共享目录做得到存储,做不到全文检索,更做不到给文件定义"合同编号""供应商""金额"这种业务字段。
后来我关注到一个概念:企业级文档管理系统,不只是文件柜,而是"文件柜 + 数据库 + 搜索引擎"的结合体。Mayan EDMS 就是这个定位。它给每个文档挂元数据、文件版本、权限角色、审批流程,扫描进来的图片会丢到后台 OCR 队列里,识别出的文本变成全文检索的索引。也就是说,"搜到内容"这件事,它直接在系统里完成了。
我评估过其他开源方案,有的侧重个人笔记,有的偏电子档案室,但 Mayan 在"文档生命周期管理"这条线上走得比较完整:上传、拆版、分类、元数据、OCR、检索、状态流转都有。对于一个小团队、十几万份文档的场景,功能上是够用的,甚至偏多。
1.2 架构能不能扛住日常使用?
我看系统从来先看架构,因为功能可以迭代,架构决定了它能陪你走多远。Mayan EDMS 是基于 Django 的应用,后台任务用 Celery 异步处理,数据库支持 PostgreSQL、MySQL/MariaDB,消息队列可以用 Redis 或 RabbitMQ。这个组合在开源生态里非常成熟,社区踩坑案例多,出了问题也好查。
最关键的设计是:OCR 不是同步阻塞的。你上传一个几百页的扫描件,系统立刻给你返回"已接收",至于每页图像去哪排队、什么时候识别完,那是 Celery 工人进程的事。这就意味着,哪怕 OCR 一台机器忙不过来,你也可以再加一个 worker 节点横向扩容,Web 界面不会跟着卡死。
存储方面,文件实体、数据库、缓存是分离的。数据库只存元数据和 OCR 结果文本,扫描原件落盘在 media 目录。这个结构对以后的备份、迁移都很友好:备份时把 PostgreSQL dump 一份、media 目录打包一份就完整了。
1.3 中文 OCR 的"最后一公里"
选型确定后,最大的技术风险其实在 OCR。Mayan 默认的 OCR 引擎是 Tesseract,这个引擎本身是优秀的开源 OCR 项目,但问题是:它默认只带英文语言包。你装完 Mayan 上传一份中文合同,识别结果多半是一串乱码或者干脆是空的。
原因不复杂。Tesseract 的识别是分语言的,每种语言对应一个训练好的语言包文件,简体中文对应chi_sim,繁体中文对应chi_tra。英文状态下让 Tesseract 去认汉字,它只能强行按照拉丁字母的形状去匹配,结果自然一塌糊涂。所以"Mayan 支持中文 OCR"这句话的真正含义是:Mayan 的 OCR 框架本身语言无关,你需要另外装好 Tesseract 的中文语言包,并且把运行参数指到chi_sim上。
这一步本身不难,但在实际操作中,它牵涉到容器镜像构建、系统设置、文档语言属性、任务重跑等一系列环节,任何一个没对上,识别结果都不对。我下面的部署和配置部分会按步骤拆开来讲。
2. 部署方案:Docker Compose 组合拳
2.1 组件拆解与资源规划
我推荐用 Docker Compose 部署,原因很简单:Mayan EDMS 的依赖项不少,手动装容易在某一个 Python 包或者系统库上卡住。用容器把运行环境隔离好,出问题大不了删了重建。
跑起来需要这几个角色:
- PostgreSQL:存元数据、用户、权限、OCR 结果,也就是系统的主数据库。
- Redis:做 Celery 的消息代理和结果后端,顺便承担系统缓存。
- Mayan 主容器:里面同时跑 Web 服务和 Celery 工人进程,是核心。
资源上我的建议是至少 4 核 CPU、8 GB 内存起步。如果你要识别大量中文扫描件,CPU 越好越省时间。Tesseract 的 LSTM 识别模型是纯 CPU 计算密集型的,页数多的情况下,4 核机器识别一个几十页的 PDF 可能要几分钟。我最初用 2 核 4 GB 的小机器试,上传一份 50 页的合同,队列排队排到怀疑人生,后来加了资源才算正常。
2.2 一份能跑的 Compose 配置
我实际使用的 compose 文件大概是下面这样,你可以直接把关键部分拿走:
services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: mayan POSTGRES_USER: mayan POSTGRES_PASSWORD: mayan_password volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mayan"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine restart: unless-stopped volumes: - redisdata:/data mayan: image: mayanedms/mayanedms:latest restart: unless-stopped command: ["sh", "-c", "mayan-edms.py performupgrade && mayan-edms.py launch"] depends_on: postgres: condition: service_healthy redis: condition: service_started ports: - "8080:8000" environment: MAYAN_DATABASE_ENGINE: postgresql MAYAN_DATABASE_NAME: mayan MAYAN_DATABASE_USER: mayan MAYAN_DATABASE_PASSWORD: mayan_password MAYAN_DATABASE_HOST: postgres MAYAN_DATABASE_PORT: 5432 MAYAN_CELERY_BROKER_URL: redis://redis:6379/0 MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/1 MAYAN_SECRET_KEY: "换成你自己的一长串随机字符串" volumes: - media:/var/lib/mayan volumes: pgdata: redisdata: media:启动命令就是docker compose up -d。第一次启动会自动执行数据库迁移和初始化,耗时可能比较长,不要急着看页面,先docker compose logs -f mayan观察日志,看到 Web 服务监听 8000 端口的提示再访问。
2.3 初始化阶段最容易踩的坑
先说 SECRET_KEY。这个变量必须设置,不设置服务会拒绝启动。它的作用是给 Django 的会话、密码加密做签名,你可以用任意一长串随机字符,但绝对不要在测试环境和生产环境用同一个值。
另一个容易忽略的是mayan-edms.py performupgrade这一步。每次升级镜像版本,你都需要先把数据库结构升级一遍,再启动服务。我在 compose 的 command 里把它放在了启动之前,就是为了避免忘记。如果你以后手动升级,一定要记得这个顺序,先执行任务指令,再启动 Web。
数据卷权限也值得提一句。Mayan 容器内部以普通用户运行,映射到宿主机目录时可能出现权限不足。遇到上传文件报权限错误,多半是 media 卷的属主不对,把宿主机目录所有权调整成容器内用户的 UID 即可,不用改容器运行方式。
3. 中文 OCR:从语言包到识别流程
3.1 给容器装上中文语言包
官方镜像里默认不会包含中文语言包,这是所有中文用户的第一道坎。解决方法是自己构建一个定制镜像,在官方镜像基础上安装简体、繁体中文语言包。
建一个 Dockerfile,内容如下:
FROM mayanedms/mayanedms:latest ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ tesseract-ocr-chi-sim \ tesseract-ocr-chi-tra && \ rm -rf /var/lib/apt/lists/*然后构建:
docker build -t my-mayan:latest .构建成功后,把 compose 文件里image: mayanedms/mayanedms:latest改成image: my-mayan:latest,重新docker compose up -d即可。
验证语言包是否装好,可以进容器看一眼:
docker compose exec mayan tesseract --list-langs看到chi_sim和chi_tra字样就说明语言包已经就位,如果只有eng,说明没有装进这个容器,检查一下镜像是否构建成功。
这里有个细节要注意:Debian 系的软件包名称 tesseract-ocr-chi-sim,装完后 Tesseract 内部的语言代码是还是chi_sim两个短名中间带下划线。这个写法贯穿后续系统设置,填错了系统会立刻报错。
3.2 在系统里把默认语言切到中文
语言包装好只是第一步,系统还得知道"用中文去识别"。登录管理员账号后,进入 系统设置 → OCR,找到默认语言一栏,把值从默认的eng改成chi_sim。保存后,新提交的 OCR 任务就会使用简体中文模型。
这个全局默认值只管"没指定语言的文档"。Mayan 支持给每个文档单独指定语言,如果你有少量英文合同混在中文文档里,可以在上传时手动把该文档语言设为eng,系统对该文档会走英文识别,而不是全局配置。
如果一份文档里中文英文混排很常见,直接把 OCR 语言填成chi_sim+eng也可以。Tesseract 支持用加号组合多个语言模型,我实测过中英文混排的合同,识别结果比单纯用chi_sim更稳,中文和英文段落都能命中,只是耗时比单语言长一些。
注意:修改 OCR 语言配置只对"之后新投递的任务"生效。对已经识别过的文档,需要进入文档详情,重新提交一次 OCR 任务,旧结果才会被覆盖。
3.3 哪些文件真的会走 OCR 流程
搞清楚这一点,能省很多无效等待。Mayan 对文档的处理分成两条路:
- 如果上传的是扫描版 PDF 或纯图片,也就是没有文本层的文件,系统会用渲染模块把每页变成图像,交给 Tesseract 做 OCR,产出全文。
- 如果上传的是带文本层的 PDF,比如用文字工具导出的电子文件,Mayan 会直接提取文本层内容,根本不会调用 OCR。这种情况下,就算你不装中文语言包,全文检索依然有效。
很多人的误区是:"我传了个中文 PDF,怎么 OCR 没反应?" 先检查这个 PDF 是不是自带文字层。打开看能否直接选中文字即可判断。带文字层的 PDF 不需要 OCR,这是正常逻辑;只有扫描件才需要识别。
常见问题反而是反过来的:有些中文 PDF 文字层存在,但字体编码有问题,提取出来是乱码。这种文件我建议先尝试提取,如果发现检索命中很差,再把文档手动标记为图片类型强制走 OCR,两条路都堵住才算处理干净。
4. 实操过程全记录:从上传到检索
4.1 创建管理员账号与基础准备
首次通过浏览器访问http://服务器IP:8080,系统会进入初始化向导,引导创建管理员账号。这一步可以顺带设置站点名称、默认语言、默认时区。如果习惯命令行操作,也可以进容器的 shell 执行创建命令,两种方式生成的账号没有区别。
管理员账号建好后,我建议先做三件事:建好文档类型、建好元数据模板、生成 API 令牌。文档类型相当于给文件分类,比如"合同""发票""验收报告";元数据是挂在每个文档上的业务字段,比如合同编号、供应商名称。这些配置决定了以后检索和筛选的效率,我甚至认为它们比 OCR 本身还重要。
API 令牌在用户管理界面里生成。令牌的作用是让脚本和外部系统能够通过 API 上传文档,后面批量导入的时候会用到。
4.2 上传第一份中文扫描文档
进入 文档 → 上传,选择你手头的一份中文扫描 PDF。上传表单里通常可以指定该文档的语言,这里选择简体中文。文档类型选择预先建好的"合同"之类的分类,元数据字段按需填写。
上传完成之后,系统会立刻把文档投递到 OCR 后台队列。在文档列表里会看到文档状态是"正在处理",这时候不要刷新得太急,识别需要时间。我拿一份 20 页的黑白扫描合同测试,4 核环境下大概一分钟左右完成,状态变成"已处理"就可以继续操作了。
等待过程中可以进队列管理界面查看进度。如果队列一直停在原地,多半是 worker 没起来,或者消息队列连不上,排查方向后面会讲。
4.3 检索验证:能不能搜到"字"
文档处理完后,验证 OCR 是否生效,最直接的办法就是用全局搜索框搜一个肯定出现在文档里的中文词。比如合同里的"违约责任"或者"付款方式",搜出来应该能直接命中这个文档,并且预览里能看到被高亮的命中片段。
这一步如果搜不到,先别怀疑 OCR 坏,先回到文档详情页查看 OCR 文本内容。详情页会展示系统实际识别出来的纯文本,扫一眼就知道是识别成乱码、识别成英文还是根本没识别。看到原文对应中文,那 OCR 链路没问题,是检索索引的问题;看到乱码,说明语言配置环节还不对,回到第 3 节排查。
这里有一个我自己用的判断标准:OCR 文本区如果能清楚看到"付款"而不是"付软",说明识别模型基本对了;少量单字有奇奇怪怪的错别字是正常现象,不影响检索。重点看大体正确率。
4.4 用 API 批量导入:历史积压文件的处理路径
手动上传适合验证,历史积压的几千份扫描件不能靠手点。Mayan 的 REST API 在/api/v4/路径下,我用 Python 脚本做批量投递,核心逻辑很简单:
import requests API_ADDR = "http://127.0.0.1:8080/api/v4/" TOKEN = "你的API令牌" headers = { "Authorization": "Token {}".format(TOKEN), } def upload_pdf(path: str, language: str = "chi_sim") -> None: with open(path, "rb") as f: resp = requests.post( API_ADDR + "documents/", headers=headers, files={"file": f}, data={"language": language}, ) if resp.ok: print("上传成功,文档ID:", resp.json()["id"]) else: print("上传失败:", resp.status_code, resp.text) upload_pdf("/path/to/扫描件.pdf", language="chi_sim")实际批量时我会加一层目录扫描逻辑,按文件夹名称自动设置文档类型和元数据。还要注意给 API 请求加超时和重试,大批量上传时偶发的网络抖动会造成队列里出现重复文档。
另外提醒一句:批量投喂之前,确认 OCR 队列处理能力跟得上。一次性丢几百份扫描件进队列,worker 会排队处理很久。我的习惯是分批提交,比如每小时投 50 份,观察队列积压情况再调整节奏,避免把消息队列压爆。
5. 常见问题与排查技巧实录
5.1 识别结果全是乱码或者方块
这是中文用户第一个遇到的坑,而且通常不是单个原因。我遇到的情况分为三种:
第一种,语言包装了但系统没切语言。全局 OCR 语言还是eng,文档语言也没指定,Tesseract 用英文模型硬认中文,输出一堆拉丁字母乱串。解决方式很明确:系统设置里改成chi_sim,文档语言也指到简体中文,重新提交 OCR。
第二种,语言包根本没装进容器。你改了系统设置,但容器里根本没有chi_sim.traineddata文件,Tesseract 会直接报找不到语言或者回退到默认。这种要回容器里跑tesseract --list-langs确认。
第三种,改了配置但任务没重跑。OCR 任务一旦跑完,结果就缓存到数据库里了。配置修改只影响新任务,老文档必须手动重新提交识别,不然看到的永远是一堆旧乱码。
我个人的排查顺序永远是:先看语言包,再看系统配置,最后看文档级配置。这三层是从底层到上层的依赖关系,底层不对,上层怎么改都没用。
5.2 中文识别率偏低,能不能再准一点
Tesseract 对印刷体中文的识别率,在扫描质量正常的情况下是比较可用的,但对输入图像质量非常敏感。我踩过的经验可以总结成几条:
- 扫描分辨率至少 300 DPI。低于 200 DPI 时,中文笔画的粘连和断裂会急剧增加,识别率掉得惨不忍睹。扫描时直接设置成黑白模式加 300 DPI,识别效果比彩色图还好。
- 页面尽量摆正。倾斜超过 3 到 5 度的页面,识别会出现整行错位。扫描仪的自动纠偏功能尽量开着,这比 OCR 后处理更省钱。
- 文件类型选对。表格复杂的文件,Tesseract 往往会切错单元格;合同正文这种自然段落的识别率最高,表格类我会配合元数据来兜底,不指望 OCR 把表格内容完全还原。
- 字体越标准越好。宋体、黑体这类常见印刷字体的识别率非常高;艺术字、异形字体、手写批注,识别结果基本不可用,这种情况我会在业务流程上规避,让手写件走人工录入。
还有一个小技巧:如果扫描件上有印章或者背景水印,识别错误率会明显上升。能在扫描端把背景压暗、把印章区域遮掉一部分,效果立竿见影。
5.3 OCR 队列一直 pending,任务不执行
队列不动是个老问题,原因通常不在 OCR 本身,而在任务链路上。按我的排查经验,优先级顺序如下:
第一,看 worker 进程是否还活着。容器部署时执行docker compose logs mayan,如果日志里有大量 Celery 报错,多半是 worker 崩了,重启服务一般能解决。
第二,看 Redis 连接。消息队列连不上,任务只会堆积在 pending 状态。确认 Redis 服务正常、连接地址端口没写错。
第三,看系统资源。中文 OCR 是 CPU 密集型的,4 核机器同时识别几个大文件时,CPU 满了,任务看起来就像"卡住"。此时日志里其实还在慢慢推进,别急着重启,等它跑完就行。
第四,看磁盘空间。OCR 会产生临时渲染文件,磁盘满了会导致渲染失败,任务无限重试。这个最容易忽略,但生产环境里很常见。
5.4 常见问题速查表
下面这个表是我后来整理给同事用的,可以直接当排查手册:
| 现象 | 最可能的原因 | 排查与解决 |
|---|---|---|
| OCR 结果是乱码 | OCR 语言没切换成 chi_sim | 系统设置改默认语言,重新提交文档 OCR |
| OCR 结果为空 | 语言包缺失或 worker 异常 | 检查 tesseract --list-langs,查看 worker 日志 |
| 搜不到中文关键词 | OCR 没跑完或检索索引未更新 | 确认文档状态为已处理,再执行一次检索同步 |
| 队列积压严重 | CPU 资源不足或提交量过大 | 分批提交,增加 CPU 核数,把原文件分辨率控制在合理范围 |
| 带文字层 PDF 无法检索 | 文字层编码异常 | 强制以图像方式重新提交 OCR 处理 |
| 上传文件提示权限错误 | 数据卷权限不正确 | 调整 media 卷属主为容器内用户 UID |
这张表解决不了所有问题,但覆盖了我实际遇到的大部分情况。如果你遇到表里没有的现象,建议去翻 Mayan 的官方文档和社区讨论,先确认你用的版本,再看同类报错,通常能找到答案。
我个人在实际项目中的体会是:中文 OCR 不是"装个包就能 100% 完美"的功能,它需要从扫描源头到系统配置到业务规则全链路配合。我的做法是,把 OCR 定位成"辅助检索"而不是"档案数据",扫描件经过 OCR 之后能搜到关键字、能定位到文件,就已经完成了它的核心任务;关键业务字段一定用系统里的元数据字段去标,不指望纯 OCR 输出作为唯一依据。这样搭配下来,Mayan EDMS 在生产环境跑了一年多,文档检索的可靠性一直是够用的。
最后再分享一个小技巧:批量导入前,先拿自己单位最典型的三种文件各测一遍,一份合同、一份发票、一份带图文的报告,用同样的扫描参数分别识别。这三份的识别效果基本能代表后续大部分文件的水平,提前心里有数,部署上线时才不会被意外打乱节奏。