把纸变成数据:为什么个人站主要自建文档管理系统
我做了这么多年站,最头疼的从来不是技术问题,而是「找东西」。合同、发票、身份证复印件、域名证书、备案材料、供应商报价单——它们散在邮箱附件、微信收藏、几个文件夹,还有一堆扫描件躺在某块移动硬盘里。等到要报税、要对账、要交材料的时候,翻半天翻不出来。更麻烦的是很多是纸质发票和合同,扫描成 PDF 之后连自己都记不住存到哪了。
Paperless-ngx 就是为解决这个问题而生的:它是一个开源文档管理系统,能自动对扫描件做 OCR(光学字符识别)、按内容自动分类打标签、提供全文搜索,然后你就能像用搜索引擎一样检索自己的所有文档。本文记录我用 Docker 部署它、接入扫描仪、配置自动消费规则(consume folder)的完整过程,全程数据留在自己服务器上,不上云。
Paperless-ngx 是什么,和网盘文件夹有什么本质区别
很多人第一反应是「我用网盘 + 文件夹分类不就行了吗」。区别在于三点:
- OCR 全文检索:网盘只能搜文件名,Paperless 能搜到文档内容里面的字。你扫描了一张纸质发票,搜发票号或者供应商名字,能直接命中,哪怕文件名是
scan_20261001.pdf。 - 自动分类:你写好规则(比如「包含『增值税』的文档 → 打上『发票』标签,归到『财务』通讯簿」),新文档进来就自动归档,不需要你手动整理。
- 自托管且不依赖第三方:所有 PDF、缩略图、OCR 文本、元数据都在你自己的服务器和数据库里。对于合同、证件这类敏感文档,这一点比功能本身还重要。
它的前身是 Paperless(原本基于 Python/Django)和 Paperless-ng(社区 fork),Paperless-ngx 是当前活跃维护的正式继承人,文档完善、更新勤快,是目前社区公认的首选。
架构与部署前提:几个必须先想清楚的点
Paperless-ngx 不是一个单容器应用,它由几部分组成:
- Web 应用(Django):提供界面和 API。
- 消费目录监听器:监视一个文件夹,丢进去的 PDF/图片会被自动摄入(consume)。
- OCR 引擎:默认用 Tesseract,中文识别需要额外语言包。
- 数据库:可以用 SQLite(单人够用)或 PostgreSQL(更好,推荐)。
- Redis:做任务队列,处理 OCR、分类等异步任务。
硬件上,OCR 是 CPU 密集型任务,一台 2 核 2G 的机器跑起来没问题,但摄入大量扫描件时 CPU 会飙高。磁盘建议留 20GB 以上,因为每份文档会存原文件 + 归档 PDF(带 OCR 文本层)+ 缩略图。
⚠️ 一个很关键的部署前决定:中文 OCR 必须装 tesseract-ocr-chi-sim 语言包,官方镜像默认只有英文。不装的话,扫描的中文文档 OCR 出来是空白或乱码,全文检索形同虚设。这是新手第一个大坑。
用 Docker Compose 部署:镜像、数据与配置
官方推荐用 Docker Compose 部署,我按官方示例改了一版。先建目录结构:
/opt/paperless/ docker-compose.yml docker-compose.env # 环境变量 data/ # 数据库、索引、缩略图 media/ # 原始文档与归档 PDF consume/ # 丢文件进来就会被摄入 export/ # 导出备份用
docker-compose.env 是核心配置文件,几个必改项:
PAPERLESS_URL=https://docs.example.com
PAPERLESS_TIME_ZONE=Asia/Shanghai
PAPERLESS_OCR_LANGUAGE=chi_sim+eng # 中文 + 英文,关键!
PAPERLESS_ADMIN_USER=admin
PAPERLESS_ADMIN_PASSWORD=改成一个强密码
PAPERLESS_SECRET_KEY=用openssl生成一串随机字符串
PAPERLESS_OCR_OUTPUT_TYPE=pdfa-2 # 归档格式,长期保存友好
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}
PAPERLESS_OCR_LANGUAGE=chi_sim+eng 就是要中文正确识别的关键。注意 Paperless 用的语言名是 Tesseract 的命名(chi_sim 是简体),不是 zh-CN。想识别繁中再加 chi_tra。
然后 docker-compose.yml 大致如下(简化版,实际官方给的是完整五服务编排):
services:
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "127.0.0.1:8000:8000" # 只绑回环,外部走反代
volumes:
- ./data:/usr/src/paperless/data
- ./media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
db:
image: postgres:16
restart: unless-stopped
volumes:
- ./pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: 换一个强密码
broker:
image: redis:7
restart: unless-stopped
volumes:
- ./redisdata:/data
启动:docker compose up -d。首次启动 webserver 会花几分钟做数据库迁移和初始化,日志里会刷一大片 migration 输出,这是正常的,别以为卡死了。看到 Starting development server 或 gunicorn 监听日志就说明好了。
服务起来后本地 curl http://127.0.0.1:8000 能拿到登录页面,用 PAPERLESS_ADMIN_USER 和密码登录。
接入扫描仪:三种把纸质文档送进系统的路径
Paperless 本身不驱动扫描仪,它只负责「摄入」。把纸质文档变成数字的有三条路:
- 消费目录(Consume Folder)——最通用。你把扫描好的 PDF 或图片丢进
/opt/paperless/consume/,Paperless 会在几秒内自动摄入。可以用scp、可以是你扫描仪 App 的 SMB/WebDAV 目标(前文写过 Nginx 搭 WebDAV),也可以直接放在服务器共享目录里。 - 邮件摄入。配置一个邮箱,Paperless 定时收信并摄入附件。适合「别人发来的合同/发票直接进系统」的场景,需要配 IMAP 账号。
- 扫描仪直接推。支持「扫描到 SMB/FTP/Email」的打印机,把目标指向服务器的 SMB 共享,扫描完直接落进消费目录。
我个人的组合是:家用扫描仪通过 SMB 推到服务器共享目录,那个目录被我软链接到 consume 目录;手机拍照的发票则走邮件摄入。两种来源统一汇聚,进系统后统一 OCR。
这里有个细节:消费目录里的文件被摄入后会移动到 media 目录,原文件不会留在 consume。所以如果你想保留一份「原始未经处理」的副本,不要让扫描仪直接写 consume,而是先落到一个暂存目录,再由脚本复制到 consume。这样有「原始件」和「已处理件」两份,多一层保险。
配置 OCR、自动分类规则与标签体系
Paperless 真正的威力在自动化规则上。进入「设置」,重点配这几块:
1. 消费目录规则(Consume Templates / Storage Paths):定义摄入的文档存到哪个物理子目录、按什么格式命名文件。文件名格式用占位符,比如 {{ created_year }}/{{ correspondent }}/{{ title }},让系统自动按年份和往来方分层。这比手动建文件夹强太多。
2. 自动匹配规则(Matching Rules):给每个「往来方(Correspondent)」「文档类型」「标签」设置匹配条件。Paperless 支持三种匹配方式:
- 内容匹配:文档 OCR 后的文本包含某个关键词(比如包含「供电局」就归到「电费」类型)。
- 文件名匹配:适合扫描仪输出的命名有规律的情况。
- 自动分配标签:可以设置「匹配此往来方的文档自动打上 X 标签」。
设置得当的话,新文档进来几乎不需要人工干预:OCR 出「中国移动」,自动归到「电信」往来方、打上「月账单」标签、存进对应目录。你只需要偶尔校对算法猜错的那几份。
3. 标签体系设计建议:不要一开始就建太多标签,容易变成手动分类的负担。我建议三层结构:文档类型(发票/合同/证件/说明)、往来方(谁发的)、状态(待处理/已归档/重要)。80% 的检索靠「往来方 + 日期」,标签只作为补充维度。
中文 OCR 的准确率需要耐心调:Tesseract 对印刷体尚可,但对手写、传真件、倾斜扫描的识别率明显下降。扫描时尽量保持平整、用 300dpi、黑白模式,识别率会高很多。
反向代理、HTTPS 与移动端使用
和前面所有自托管服务一样,公网访问走 Nginx 反代 + Let's Encrypt,绝不直接暴露 8000 端口:
server {
listen 443 ssl http2;
server_name docs.example.com;
ssl_certificate /etc/letsencrypt/live/docs.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docs.example.com/privkey.pem;
client_max_body_size 200M; # 大 PDF 上传
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # OCR 大文件耗时长,别设太短
proxy_buffering off;
}
}
client_max_body_size 要放够,扫码生成的多页 PDF 可能几十 MB,默认 1M 会直接 413。proxy_read_timeout 也别太短,虽然 OCR 是异步任务,但上传后的一些同步操作可能耗时。
Paperless 没有官方移动 App(历史上有个社区 App 但维护状态一般),移动端主要靠响应式 Web 界面——它做的 PWA 在手机上体验还可以,建议在浏览器里「加到主屏幕」,配合手机的系统分享(把文件分享到「添加到主屏幕」的 Paperless)就能把手机里收到的 PDF 直接上传。如果你需要更强的移动端,可以调它的 REST API 自己接(Paperless 的 API 文档很完整,支持 token 认证)。
备份、升级与长期维护
Paperless 的数据分三部分,备份策略要分别对待:
- media 目录:原始文档 + OCR 归档 PDF + 缩略图。这是最核心的资产,必须异地备份。
- data 目录:搜索索引、配置。可以重建(重新摄入即恢复),但重建耗时,最好也备份。
- PostgreSQL 数据库:存元数据、标签、往来方关系。必须导出备份,用
pg_dump,别直接拷数据目录文件。
Paperless 官方提供导出功能(Document Exporter):docker compose exec webserver document_exporter ../export,它会把所有文档和元数据导成一个可移植的目录结构,是跨机器迁移或长期归档的首选方式。我把它加进每周 cron,导出后让 restic 同步到异地。
升级很直接:docker compose pull && docker compose up -d。升级前务必 pg_dump 备份数据库和 media 目录——Paperless 做过好几次数据库结构大迁移(尤其从 ng 到 ngx 那次),虽然有自动迁移,但没有备份一旦出岔子就是灾难。
维护上一个常被忽略的点是索引大小。随着文档增多,全文索引和缩略图会占用可观磁盘。定期检查 data/index 目录大小,如果太大可以重建索引(document_index reindex)。另外,OCR 队列如果积压(大量文档同时摄入),可以调高 PAPERLESS_TASK_WORKERS,但注意别把 CPU 占满影响服务器上其它服务。
六个坑总结
- 没装中文 OCR 语言包:
PAPERLESS_OCR_LANGUAGE=chi_sim+eng不设,中文文档识别全废。 - 端口暴露公网:8000 直接对外,文档系统等于公开。永远只绑回环 + 反代。
client_max_body_size太小:多页扫描 PDF 上传 413,配够 100M+。- 不做数据库备份:只备份 media 不导出数据库,元数据全丢,标签往来方要重来。
- 扫描质量差导致 OCR 错漏:300dpi 平整扫描,别用手机歪着拍。
- 升级前不备份:数据库结构迁移不可逆,升级前先
pg_dump和导出 media。
自建文档管理系统的回报是长期的:你花一个周末把历史文档全扫进去,之后十年里每一次「要交 XX 材料」「上次那份合同在哪」的问题,都能在几秒内用搜索解决。数据在自己手里,检索速度比翻网盘快得多,这才是我坚持自托管的理由。