Paperless-ngx 高级用法详解:自动匹配算法、AI 特性、消费钩子与文件命名体系
本文基于 paperless-ngx 仓库的 docs/advanced_usage.md 展开,系统讲解这套文档管理系统中"让文档自己分类、自己归档、自己挂钩脚本"的全部高级机制:七种匹配算法与神经网络自动标注、可选的 LLM/AI 特性、消费前后钩子脚本、基于 Jinja2 模板的文件命名体系、条形码处理、双面拼页、SSO 集成与 GPG 解密等,并结合仓库源码逐一点明每个特性的底层实现位置与验证方式,帮助读者从"会用"深入到"懂原理、可排障"。
自动匹配:让标签、通讯方、文档类型和存储路径自己生效
paperless-ngx 会在每次消费文档时,用数据库中每个标签(Tag)、通讯方(Correspondent)、文档类型(DocumentType)和存储路径(StoragePath)上定义的匹配规则,去比对文档正文内容。例如定义一个名为 Home Utility 的标签,其 match 为 bc hydro、matching_algorithm 为 Exact,那么只要后续消费的文档正文中出现 bc hydro,该文档就会被自动打上 Home Utility 标签。
官方文档明确提示:匹配逻辑非常强大但也需要实验调优,四种对象(标签/通讯方/文档类型/存储路径)都要通过 Web 界面为其设置 match 文本和匹配算法,保存后再消费一份文档即可看到自动标注生效。
可用的七种算法如下:
- None:不执行任何匹配。
- Any:只要
match中的任一词在文档中出现即命中。例如Bank1 Bank2会匹配包含其中任意一词的文档。 - All:要求
match中的每个词都出现,但不要求顺序一致。 - Exact:
match必须按原样(保留顺序)出现在文档中。 - Regular expression:将
match作为正则表达式解析,在文档中查找匹配。 - Fuzzy match:基于 rapidfuzz 的 partial ratio 做部分模糊匹配。
- Auto:基于神经网络自动推断,无需设置
match,详见下文。
使用 Any 或 All 算法时,可以用双引号包裹多词短语。例如用 Any 算法定义 "Bank of America" BofA,会命中包含 "Bank of America" 或 "BofA" 的文档,但不会命中 "Bank of South America"。
源码印证:matches() 与各算法的真实实现
所有算法的判定逻辑集中在 src/documents/matching.py 的 matches() 函数中,枚举定义见 src/documents/models.py(MATCH_ANY、MATCH_ALL、MATCH_LITERAL(即 Exact)、MATCH_REGEX、MATCH_FUZZY、MATCH_AUTO)。从源码结构看,有几个值得注意的实现细节:
- Any/All 走分词+词边界正则:_split_match() 把
match文本拆成关键词,双引号内的词被归并成一个短语(内部空格被规范化为\s+),然后用\b{word}\b做词边界搜索——这就是为什么Bank of South America不会命中 "Bank of America" 短语。 - Exact 使用
re.escape:匹配串先做转义再做整体词边界搜索,因此匹配串中的特殊字符不会被当作正则解释。 - 正则算法有安全超时:正则匹配经由 src/documents/regex.py 中的
safe_regex_search执行,受MATCH_REGEX_TIMEOUT_SECONDS(默认 0.1 秒)保护,防止恶意或失当的正则卡死消费流程;API 层在 src/documents/serialisers.py 还会对正则算法做编译校验。 - 模糊算法的阈值是 90:
MATCH_FUZZY分支先剔除标点和(可选)转小写,再调用fuzz.partial_ratio(match, text, score_cutoff=90),即相似度必须达到 90 分才命中。 - 调试可观测:每次命中都会通过
log_reason记录一条 debug 日志,说明"为什么这个对象匹配上了这份文档",便于排障时解释匹配行为。
Auto 自动匹配:内置神经网络
Auto 算法不依赖 match 文本,而是学习你在既有文档上的人工标注习惯:神经网络根据历史文档"哪些文档被打了某标签"来推断新文档该不该被打。例如所有"美国银行账户 123"的银行对账单都打了 bofa123 标签且该标签算法设为 Auto,网络就会学会自动给它续标。
使用 Auto 必须了解文档列出的五条注意事项:
- 文档变化不会立即反映到模型上,神经网络需要重新训练;paperless 默认每小时自动检查一次变化并触发训练。
- Auto 只统计不在收件箱中的文档(即未带任何收件箱标签的文档),确保网络只从你已经确认标注正确的文档学习。
- 对象与文档内容之间必须存在相关性:银行对账单含银行名和账号,效果较好;"TODO" 这类与内容无关的标签无法自动推断。
- 样本要足够多:一千份文档里只有一封来自"五年前买过东西的小网店",再次购物时很可能不会自动命中。
- 需要足够的负例:如果你所有文档只来自 "Webshop" 和 "Bank" 两家且两者都设为 Auto,网络会给任何新文档二选一地标注。
从源码结构看,Auto 的实际执行链路是:src/documents/matching.py 中 match_correspondents/match_tags/match_storage_paths 等函数会调用 src/documents/classifier.py 的 DocumentClassifier 进行 predict_*,只有当对象 matching_algorithm == MATCH_AUTO 且其 pk 等于预测结果时才采纳(o.pk == pred_id and o.matching_algorithm == MATCH_AUTO)。matches() 中对 MATCH_AUTO 直接返回 False 并注释 "this is done elsewhere",印证了规则匹配与模型预测是两条并行通路。周期性重训练由 src/documents/tasks.py 中的后台任务驱动,它会检查是否还存在 Auto 算法对象来决定是否需要训练。
AI 特性(可选,默认关闭)
paperless-ngx 内置一组由 LLM 驱动的可选项:AI 辅助建议、相似文档检索(RAG)和文档聊天。三者默认关闭,且永远不会取代内置的非 LLM 匹配与建议。
隐私警告:启用后,文档内容(及元数据)会发送到你配置的 LLM 后端;如果后端是远程托管服务,文档将离开你的服务器并可能产生费用。若隐私敏感,优先选择本地后端(Ollama 或自托管的 OpenAI 兼容网关)。
所有 AI 设置均可用 PAPERLESS_AI_* 环境变量提供(见 docs/configuration.md),也可在管理后台 Settings → Application Configuration 中设置;数据库中的值优先于环境变量。
启用 AI 特性的最小配置
PAPERLESS_AI_ENABLED:总开关。PAPERLESS_AI_LLM_BACKEND:ollama(本地运行)或openai-like(OpenAI 本身或任何 OpenAI 兼容 API)。PAPERLESS_AI_LLM_MODEL,openai-like下通常还需要PAPERLESS_AI_LLM_API_KEY和/或PAPERLESS_AI_LLM_ENDPOINT;Ollama 则要求PAPERLESS_AI_LLM_ENDPOINT指向你的 Ollama 服务器。
生成与嵌入模型的选型建议可参考社区维护的 wiki 页面"AI Model Recommendations"(仓库文档中给出指引)。
AI 辅助建议
启用 AI 后,paperless-ngx 可以把文档发给 LLM,让其建议标题、标签、通讯方、文档类型、存储路径和日期。该能力是按请求 opt-in 的,通过文档详情页的 "Suggest" 控件触发,与经典分类器建议并存、互不排斥。建议输出的语言可用 PAPERLESS_AI_LLM_OUTPUT_LANGUAGE 控制,否则跟随用户界面语言。
LLM 索引(RAG)与相似文档
设置嵌入(embedding)后端即启用 LLM 索引——文档的向量索引,用于 RAG。启用后,建议会以相似既有文档为依据(grounding),文档聊天也能检索到相关上下文。启用方式:
PAPERLESS_AI_LLM_EMBEDDING_BACKEND:huggingface(完全本地嵌入)或ollama/openai-like。- 索引只在 AI 已启用且设置了嵌入后端时才会构建。
索引由 PAPERLESS_LLM_INDEX_TASK_CRON(docs/configuration.md)控制定时更新,默认每天一次,也可手动重建/压缩,操作见 管理 LLM 索引。注意:huggingface 本地嵌入会在首次使用时把嵌入模型下载到 paperless 数据目录,首次运行需要网络和一定磁盘空间。
文档聊天
LLM 索引启用后,应用顶栏的聊天控件可以回答关于你文档的问题:它视当前视图作用于单篇或多篇文档,回答中会附带所引用来源文档的链接。
AI 安全要点
- 文档内容被当作不可信数据传给 LLM。
- 默认允许解析到私有/回环地址的 AI 端点(供本地后端使用);将
PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS设为false可阻断此类端点。
挂钩消费流程:Pre/Post 脚本
有时你希望在文档被消费前后执行任意操作。paperless 用两个简单钩子实现:把脚本放到 paperless 可读取并执行的位置,再把路径写入 paperless.conf 或 docker-compose.env:
PAPERLESS_PRE_CONSUME_SCRIPT(docs/configuration.md)PAPERLESS_POST_CONSUME_SCRIPT(docs/configuration.md)
关键约束:脚本在阻塞进程中执行。脚本跑得太久会显著拖慢消费流程;如需异步,请在脚本内 fork 进程后立即退出。
脚本的 stdout 和 stderr 会逐行记入 webserver 日志,同时记录退出码。脚本不存在或执行失败会分别产生 pre_consume_script_not_found / pre_consume_script_error 等状态信息——这部分逻辑在 src/documents/consumer.py 中实现:先检查 settings.PRE_CONSUME_SCRIPT 是否为文件(Path(...).is_file()),再注入环境变量并执行。
Pre-consumption 脚本
在消费者发现 consume 目录中的新文档之后、对文档做任何处理之前执行,可用环境变量:
| 环境变量 | 含义 |
|---|---|
DOCUMENT_SOURCE_PATH |
被消费文档的原始路径 |
DOCUMENT_WORKING_PATH |
消费过程将操作的原始文件副本路径 |
TASK_ID |
处理该新文档所用任务的 UUID(如有) |
注意与警告(原文档逐条强调):
- 修改文档的 pre 脚本只能改
DOCUMENT_WORKING_PATH指向的文件,否则可能触发第二个消费任务、两个任务争抢同一文件导致失败; - 如果以非确定性方式修改
DOCUMENT_WORKING_PATH,可能导致重复文档入库。
一个经典例子——用 OCR 重写 PDF 后再进入消费流程:
/usr/local/bin/ocr-pdf:
#!/usr/bin/env bash
pdf2pdfocr.py -i ${DOCUMENT_WORKING_PATH}
/etc/paperless.conf:
...
PAPERLESS_PRE_CONSUME_SCRIPT="/usr/local/bin/ocr-pdf"
...
该脚本会把即将消费的文档路径传给 ocr-pdf,后者用 pdf2pdfocr 覆盖写出 OCR 版本并退出,消费流程随后基于修改后的文件开始。
Post-consumption 脚本
在消费者成功处理文档并归档后执行,可用环境变量:
| 环境变量 | 含义 |
|---|---|
DOCUMENT_ID |
文档的数据库主键 |
DOCUMENT_FILE_NAME |
格式化后的文件名(不含路径) |
DOCUMENT_TYPE |
文档类型(如有) |
DOCUMENT_CREATED |
文档创建日期时间 |
DOCUMENT_MODIFIED |
文档最后修改日期时间 |
DOCUMENT_ADDED |
文档加入 paperless 的日期时间 |
DOCUMENT_SOURCE_PATH |
原始文档文件路径 |
DOCUMENT_ARCHIVE_PATH |
生成的归档文件路径(如有) |
DOCUMENT_THUMBNAIL_PATH |
生成的缩略图路径 |
DOCUMENT_DOWNLOAD_URL |
文档下载 URL |
DOCUMENT_THUMBNAIL_URL |
缩略图 URL |
DOCUMENT_OWNER |
文档属主用户名(如有) |
DOCUMENT_CORRESPONDENT |
已分配的通讯方(如有) |
DOCUMENT_TAGS |
应用的标签(逗号分隔,如有) |
DOCUMENT_ORIGINAL_FILENAME |
原始文件名 |
TASK_ID |
导入文档所用任务 UUID(如有) |
脚本可以是任意语言。仓库自带一个 shell 示例 scripts/post-consumption-example.sh,它会打印 DOCUMENT_ID、文件名、文档类型、归档/源路径、各时间戳、缩略图与下载 URL、属主、通讯方和标签,可直接照抄改造。
注意:post 脚本无法取消消费流程;警告:post 脚本不应直接修改文档文件。
Docker 下的钩子接入
Docker 部署时需要通过宿主机挂载把脚本送进容器:
...
webserver:
...
volumes:
...
- /home/paperless-ngx/scripts:/path/in/container/scripts/ # (1)!
environment: # (3)!
...
PAPERLESS_POST_CONSUME_SCRIPT: /path/in/container/scripts/post-consumption-example.sh # (2)!
...
- 外部 scripts 目录挂载到容器内位置;
- 用容器内路径设置要执行的脚本;
- 同样可写在
docker-compose.env中。
排障三招:
- 跟踪 Compose 日志:
cd ~/paperless-ngx; docker compose logs -f - 检查脚本权限(权限错误时):
sudo chmod 755 post-consumption-example.sh - 把脚本输出重定向到日志文件,如
echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log
文件命名处理(File Name Handling)
默认情况下 paperless 把文档存入 media 目录并用自增标识符重命名,你会得到 0000123.pdf 这样的文件。通常无需手动访问这些文件,但如果想自定义命名,有两个入口:PAPERLESS_FILENAME_FORMAT(docs/configuration.md)或存储路径。paperless 会自动补全 .pdf、.jpg 等扩展名。对于带版本(version)的文档,每个版本沿用相同的命名规则与存储路径解析,并追加 _v1、_v2 之类的版本后缀。
例如配置:
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}
会得到如下目录结构:
2019/
My bank/
Statement January.pdf
Statement February.pdf
2020/
My bank/
Statement January.pdf
Letter.pdf
Letter_01.pdf
Shoe store/
My new shoes.pdf
两条重要告诫:
- 不要手动移动 media 目录里的文件:paperless 记录着每份文档上一次存储的文件名,手动改名会被报为 missing 且无法找回。
- 文档每次保存时都会重新检查文件名:修改/删除存储路径会自动反映到文件系统;但修改
PAPERLESS_FILENAME_FORMAT后,需要手动运行document renamer迁移既有文档。
占位符完整清单
| 占位符 | 说明 |
|---|---|
{{ asn }} |
归档序列号(ASN),无则为 "none" |
{{ correspondent }} |
通讯方名称,无则为 "none" |
{{ document_type }} |
文档类型名称,无则为 "none" |
{{ tag_list }} |
全部标签的逗号分隔列表 |
{{ title }} |
文档标题 |
{{ created }} |
创建日期(ISO 8601,如 2024-03-14) |
{{ created_year }} |
创建年份(含世纪,如 2024) |
{{ created_year_short }} |
创建年份(两位,补零,如 24) |
{{ created_month }} |
创建月份(01–12) |
{{ created_month_name }} |
创建月份全名(按 locale) |
{{ created_month_name_short }} |
创建月份缩写(按 locale) |
{{ created_day }} |
创建日(01–31) |
{{ added }} |
加入 paperless 的完整日期(ISO) |
{{ added_year }} |
加入年份 |
{{ added_year_short }} |
加入年份(两位补零) |
{{ added_month }} |
加入月份(01–12) |
{{ added_month_name }} |
加入月份全名(按 locale) |
{{ added_month_name_short }} |
加入月份缩写(按 locale) |
{{ added_day }} |
加入日(01–31) |
{{ owner_username }} |
属主用户名(如有),否则 "none" |
{{ original_name }} |
原始文件名(去扩展名,如有),否则 "none" |
{{ doc_pk }} |
paperless 主键(PK) |
两条告诫:
- 占位符(尤其
{{ tag_list }})可能触碰操作系统的最大路径长度限制;超限时文件保留原路径并把问题写入日志。 - 这些变量都是简单字符串,但
PAPERLESS_FILENAME_FORMAT本身可以是完整的 Jinja2 模板,见下文。
关于文件名合法性,paperless 会尽量保留数据库信息,但标题/通讯方名称中的 : \ / 等若干非法字符会被替换为连字符;若两份文档算出同名文件名,paperless 自动追加 _01、_02(对版本文件,计数器追加在版本后缀之后,如 statement_v2_01.pdf);若占位符表达式出错,则回退到默认命名方案。另外注意:理论上可以把文件存到 media 目录之外(如 PAPERLESS_FILENAME_FORMAT=../../my/custom/location/{title}),但在 Docker 中,落在预定义卷之外的文件重启后会丢失。
空占位符处理
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE(docs/configuration.md)开启后,空占位符解析为 "" 而非 "none",空占位符前的空格被去掉,空目录被省略。
存储路径(Storage Paths)
单一存储布局不够用时,存储路径可以为每份文档精确指定落盘位置:
- 每个存储路径就是一个
PAPERLESS_FILENAME_FORMAT,遵循上述全部规则; - 每个文档按上文匹配算法分配存储路径,但随时可手动覆盖。
文档给出的示例:
By Year = {{ created_year }}/{{ correspondent }}/{{ title }}
Insurances = Insurances/{{ correspondent }}/{{ created_year }}-{{ created_month }}-{{ created_day }} {{ title }}
映射后的结果:
2019/ # By Year
My bank/
Statement January.pdf
Statement February.pdf
Insurances/ # Insurances
Healthcare 123/
2022-01-01 Statement January.pdf
2022-02-02 Letter.pdf
2022-02-03 Letter.pdf
Dental 456/
2021-12-01 New Conditions.pdf
提示:存储路径是可选的。文档没有分配存储路径时,应用全局 PAPERLESS_FILENAME_FORMAT。
Filename Templates:Jinja2 模板与自定义过滤器
文件名格式基于 Jinja2 模板构建,支持逻辑控制结构与过滤器,模板可以多行、渲染为单行。此外模板可访问一个受限的 document 对象,包含 id、pk、title、content、page_count、created、added、modified、mime_type、checksum、archive_checksum、archive_serial_number、filename、archive_filename、original_filename 等常见元数据字段;关联对象以嵌套方式暴露有限字段,例如 document.correspondent.name。
三个自定义过滤器的实现都在 src/documents/templating/filters.py,可直接对照源码理解行为:
get_cf_value——访问自定义字段(get_cf_value():字段存在且值非空则返回值,否则返回默认值或 None):
{{ custom_fields | get_cf_value('field_name') }}
{{ custom_fields | get_cf_value('field_name', 'default_value') }}
参数:custom_fields(必须是模板提供的自定义字段数据)、name(字段名)、default(可选默认值)。自定义字段名含空格时必须用此过滤器,例如 {{ custom_fields | get_cf_value('Invoice Number') }}。
datetime——strftime 格式化(format_datetime():字符串入参会先按日期解析,再套 Python strftime):
{{ created | datetime('%B %d, %Y at %I:%M %p') }}
<!-- 输出: "January 15, 2024 at 02:30 PM" -->
{{ custom_fields | get_cf_value('Date Field') | datetime('%A, %B %d, %Y') }}
localize_date——Babel 本地化日期(localize_date():字符串按日期时间解析,locale 非法时抛 ValueError;datetime 走 format_datetime,date 走 format_date)。注意它要求真正的 date/datetime 对象,所以要直接访问 document.created,而不是字符串化的 {{ created }}:
{{ document.created | localize_date('short', 'en_US') }}
<!-- 输出: "1/15/24" -->
{{ document.created | localize_date('medium', 'en_US') }}
<!-- 输出: "Jan 15, 2024" -->
{{ document.created | localize_date('medium', 'fr_FR') }}
<!-- 输出: "15 janv. 2024" -->
{{ document.created | localize_date('dd/MM/yyyy', 'en_GB') }}
<!-- 输出: "15/01/2024" -->
格式预设(Format Presets):
- short:缩写格式(如 "1/15/24")
- medium:中等格式(如 "Jan 15, 2024")
- long:含完整月份名(如 "January 15, 2024")
- full:含星期(如 "Monday, January 15, 2024")
额外模板变量:
{{ tag_name_list }}:标签名列表(按标签名排序),注意它是列表不是字符串;{{ custom_fields }}:字段名到"类型+值"的映射,可按字段名取值或判断字段是否存在。
模板示例(文档原例全录)
按 ASN 区间分层:
somepath/
{% if document.archive_serial_number >= 0 and document.archive_serial_number <= 200 %}
asn-000-200/{{title}}
{% elif document.archive_serial_number >= 201 and document.archive_serial_number <= 400 %}
asn-201-400
{% if document.archive_serial_number >= 201 and document.archive_serial_number < 300 %}
/asn-2xx
{% elif document.archive_serial_number >= 300 and document.archive_serial_number < 400 %}
/asn-3xx
{% endif %}
{% endif %}
/{{ title }}
ASN 为 205 时结果为 somepath/asn-201-400/asn-2xx/Title.pdf;355 时为 somepath/asn-201-400/asn-3xx/Title.pdf。
按 MIME 类型分流:
{% if document.mime_type == "application/pdf" %}
pdfs
{% elif document.mime_type == "image/png" %}
pngs
{% else %}
others
{% endif %}
/{{ title }}
PDF 落到 pdfs/Title.pdf,PNG 落到 pngs/Title.png。
按自定义字段分流:
{% if "Invoice" in custom_fields %}
invoices/{{ custom_fields.Invoice.value }}
{% else %}
not-invoices/{{ title }}
{% endif %}
字段 "Invoice" 值为 123 的文档进 invoices/123.pdf,没有该字段的进 not-invoices/Title.pdf。
组合日期与自定义字段:
invoices/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%Y') }}/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%m') }}/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%d') }}/
Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf_value("Date Field","2024-01-01")|replace("-", "") }}.pdf
"Date Field" 为 2022-01-01、"Select Field" 为 OptionTwo 时生成 invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf。
还可以用 slugify 过滤器净化标题:{{ title | slugify }}。
其他进阶能力
无效 PDF 的自动修复
当 MIME 类型探测不准确(PDF 格式不规范或包含错误)时,paperless 会在处理前尝试用 qpdf "清洗"这些无效 PDF。
Celery 监控(Flower)
异步任务由 celery worker 执行,可用 Flower 工具查看运行中、排队、已完成任务的详情(数量、耗时等),Flower 也可向 Prometheus 导出指标。启用方式为设置 PAPERLESS_ENABLE_FLOWER(docs/configuration.md)。进一步配置需要创建 flowerconfig.py 放入 src/paperless 目录;Docker 安装可用卷挂载实现:
services:
# ...
webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports:
- 5555:5555 # (2)!
# ...
volumes:
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
:ro表示只读挂载;2. Flower 默认监听 5555 端口,可配置。
自定义容器初始化(Custom Container Initialization)
Docker 镜像支持在启动期间运行用户脚本(例如安装额外工具或 Python 包)。做法:把包含脚本的目录挂载到 /custom-cont-init.d。安全要求:目录必须 root 所有、权限 a=rx,脚本只允许 root 可写。脚本在 webserver 完成启动前直接运行,执行身份为 root;如需切换用户,镜像内提供 gosu(优先于 sudo)。这是高级功能,脚本写坏可能导致功能异常或数据丢失,遇到问题请先禁用自定义脚本再排查:
services:
# ...
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
:ro只读挂载,进一步防止目录被改动。
安装第三方解析器插件
第三方解析器插件是声明了 paperless_ngx.parsers 入口点的 Python 包,用于支持更多文件格式。创建方式见开发文档,插件列表见社区 wiki。
警告:第三方插件不在官方支持范围内。由插件引发或需要修改插件才能解决的问题会被直接关闭;提交 bug 前务必先移除全部插件复现问题。
Docker:用自定义容器初始化脚本在 webserver 启动前安装包。创建 shell 脚本并挂载到 /custom-cont-init.d:
#!/bin/bash
# /path/to/my/scripts/install-parsers.sh
pip install my-paperless-parser-package
services:
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro
脚本以 root 身份在 webserver 启动前运行,因此插件在 paperless-ngx 启动发现阶段即可用。
Bare metal:装进运行 paperless-ngx 的同一个 Python 环境。标准裸机安装即 paperless 用户环境:
sudo -Hu paperless pip3 install my-paperless-parser-package
若使用 uv 或虚拟环境,先激活再执行:
uv pip install my-paperless-parser-package
# 或
pip install my-paperless-parser-package
安装后重启全部 paperless-ngx 服务以发现新插件。
验证:下次启动后在应用日志中找确认行:
Loaded third-party parser 'My Parser' v1.0.0 by Acme Corp (entrypoint: 'my_parser').
若未出现,检查包装在正确环境、且其 pyproject.toml 声明了 paperless_ngx.parsers 入口点。
MySQL 注意事项
大小写敏感性:数据库接口不提供配置 MySQL 大小写敏感的方法。大小写不敏感库中无法同时创建标签 Name 与 NAME(被视为相同);但开启大小写敏感后搜索也会大小写敏感——标题为 Invoice 的文档搜 invoice 会找不到。按 Django 文档,让表大小写敏感需要手工干预,对每张表执行:
ALTER TABLE <table_name> CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
也可设置新表默认值(不影响既有表):
ALTER DATABASE <db_name> CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
警告:建议 MariaDB 10.4+。老系统上改用
utf8mb3字符集或许能解决部署问题,但utf8mb3会在消费环节引发utf8mb4不会有的问题。
时区缺失:MySQL/MariaDB 默认没有时区信息(部分官方镜像如 MariaDB 镜像已代劳),缺失会导致基于日期的查询行为异常。修复命令:
- MySQL:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql -p - MariaDB:
mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql -p
条形码(Barcodes)
paperless 支持利用条形码自动执行任务。条形码目前仅支持 PDF 文档和 TIFF(后者需开启 TIFF 支持)。检测库支持的类型包括:EAN-13/UPC-A、UPC-E、EAN-8、Code 128、Code 93、Code 39、Codabar、Interleaved 2 of 5、QR Code、Data Matrix、Aztec、PDF417。在 paperless 中类型不重要,只有内容重要。启用方式见条形码配置,其中两个设置可独立开启,但存在下述交互。
文档拆分:开启拆分后,paperless 默认在分隔符条形码之后拆分:
- 含分隔符条形码的页面开启新文档,且新文档从下一页开始;
- 含分隔符条形码的页面本身被丢弃。
这专为 PATCH-T 之类的专用分隔页设计。若启用 PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES(docs/configuration.md),含分隔符的页面会被保留,且成为新文档的第一页。
ASN 分配:启用后,条形码的整数值被用作文档归档序列号(ASN),便于回溯纸质原件。若同时启用拆分,遇到 ASN 条形码也会拆分,但与分隔符拆分不同,含 ASN 条形码的页面会保留——每个 ASN 条形码从该页起开启新文档,因此可以把 ASN 条形码印在正文页上。
标签分配:启用后解析条形码并尝试解释、分配标签,相关设置为 PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE 与 PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING(见 docs/configuration.md)。
在标签条形码处拆分:默认标签条形码只打标签不拆分。把 PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT 设为 true 后,含标签条形码的页面触发拆分,特性包括:
- 含标签条形码的页面保留在结果文档中;
- 每个拆分文档只提取自己页内的标签;
- 一个文档中多个标签条形码可触发多次拆分;
- 与 ASN 条形码无缝配合——每个拆分文档拥有各自的 ASN 与标签。
示例:6 页扫描中第 3 页是 TAG:invoice、第 5 页是 TAG:receipt,将产生三份文档:第 1–2 页(无标签)、第 3–4 页(invoice)、第 5–6 页(receipt)。这适合批量扫描时用标签条形码页同时"分隔+分类"。
双面文档自动拼页(Collation)
如果你的扫描仪原生支持双面扫描,不需要此功能。此功能默认关闭,开启方式见配置。
原理:只有单面 ADF 的扫描仪扫双面文档很麻烦——本功能自动把两次扫描拼成一份文档并重排页面。
操作示例:6 页(3 张纸)双面文档。第一次正常放进 ADF,确保第 1 页先扫,ADF 扫出第 1、3、5 页;把扫描结果上传到 consume 目录的正确子目录(默认 double-sided;注意 paperless 不会自动创建该目录)。paperless 处理后把它移入内部暂存区。然后把纸堆**上下颠倒(不重排顺序)**再扫一次,ADF 扫出第 6、4、2 页;该文件复制进子目录后,paperless 把两次扫描拼合,反转第二次"偶数页"扫描的页序,得到 1–6 正确排序的文档,按正常流程处理。
技巧:扫偶数页时可省略末尾空白页(如第 6 页空白就只扫第 2、4 页),但不要省略中间空白页。
可能出错的场景:
- 第一次"奇数页"扫描页数少于第二次(ADF 漏页),paperless 会删除暂存副本和该扫描,报错要求从头重扫(先奇后偶)。
- 扫描文件必须按正确顺序、一次一份地被消费:上传时要保证 paperless 正在运行;若用轮询,
CONSUMER_POLLING_INTERVAL要小于第二份扫描出现的时间间隔(如 5–10 秒甚至更低)。 - 若开始双面扫描后忘了上传第二份,为避免隔天拼错文档,paperless 只保留"奇数页"文件最多 30 分钟,超时后把下一份扫描视为全新的奇数页扫描,旧暂存文件被丢弃。
与 "subdirs as tags" 的交互:拼页可与子目录即标签特性叠加(非必需)。只要创建命名正确的 double-sided 子目录并上传即可:double-sided/foo/bar 和 foo/bar/double-sided 都会让拼合后的文档表现得像上传到 foo/bar,获得 foo 和 bar 两个标签,但不含 double-sided。
与文档拆分的交互:可以叠加使用文档拆分,但用普通单面拆分标记页时,拆分后的文档开头会有一张空白页(或标记页背面的内容)。解决办法:做一张正反面都印有拆分条形码的标记页,多余页面就会被自动移除。
SSO 与第三方认证
paperless-ngx 自带 Django 认证系统,也可通过以下方式集成外部认证:
Remote User 认证:依赖某些 SSO 应用提供的 remote user 机制,相关配置项:PAPERLESS_ENABLE_HTTP_REMOTE_USER、PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME、PAPERLESS_LOGOUT_REDIRECT_URL(见 docs/configuration.md)。
OpenID Connect 与社会认证:自 2.5.0 版本起通过 django-allauth 包支持集成外部认证系统,用户可用集成的第三方系统登录(可选地注册),配置见 PAPERLESS_SOCIALACCOUNT_PROVIDERS(docs/configuration.md)及 django-allauth 官方文档。将已有账户关联到社会账户:先用常规凭据登录,从用户下拉菜单进入 "My Profile",即可看到连接社会账户的选项;启用后登录页会出现注册入口。
GitHub 登录示例:
PAPERLESS_APPS="allauth.socialaccount.providers.github"
PAPERLESS_SOCIALACCOUNT_PROVIDERS='{"github": {"APPS": [{"provider_id": "github","name": "Github","client_id": "<CLIENT_ID>","secret": "<CLIENT_SECRET>"}]}}'
OIDC(以 Keycloak 为例):
PAPERLESS_APPS="allauth.socialaccount.providers.openid_connect"
PAPERLESS_SOCIALACCOUNT_PROVIDERS='
{"openid_connect": {"APPS": [{"provider_id": "keycloak","name": "Keycloak","client_id": "paperless","secret": "<CLIENT_SECRET>","settings": { "server_url": "https://<KEYCLOAK_SERVER>/realms/<REALM>/.well-known/openid-configuration"}}]}}'
禁用常规登录:外部认证就绪后,可用 PAPERLESS_DISABLE_REGULAR_LOGIN(docs/configuration.md)关闭常规登录,和/或用 PAPERLESS_REDIRECT_LOGIN_TO_SSO 自动把用户重定向到 SSO。
消费前解密 GPG 加密邮件
paperless-ngx 可以在消费前解密 GPG 加密的邮件。
前置条件:宿主机需安装 gpg-agent >= 2.1.1 并配置好邮件加解密(可用 gpg --encrypt --armor -r person@email.com name_of_file 与 gpg --decrypt name_of_file.asc 自测通过)。
配置步骤:
- 启用
PAPERLESS_ENABLE_GPG_DECRYPTOR(docs/configuration.md); - 在宿主机执行
gpgconf --list-dir agent-socket找到 agent socket(可能为~/.gnupg/S.gpg-agent),并找到公钥环位置; - Docker 部署需追加卷挂载:
webserver:
volumes:
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
- <path to gpg-agent socket>:/usr/src/paperless/.gnupg/S.gpg-agent
裸机安装则无需额外配置;如需单独的 GNUPG_HOME,可设置 PAPERLESS_EMAIL_GNUPG_HOME(docs/configuration.md)。
排障:确认宿主机上 gpg-agent 正在运行;确认在容器内用上述 gpg 命令加解密可用;检查 /usr/src/paperless/.gnupg 下所有文件权限正确(socket 与 keyring 均为 paperless 用户 rw 私有权限)。
小结
docs/advanced_usage.md 覆盖了 paperless-ngx 从"规则匹配 + 神经网络"自动标注、可选 LLM 能力、消费前后钩子、Jinja2 文件命名,到条形码拆页、双面拼页、SSO 与 GPG 解密的完整进阶能力面。理解这些特性时,建议按本文给出的源码路径(匹配算法、钩子执行、模板过滤器、自动匹配训练任务)对照阅读,把文档承诺的行为与实现一一对应,既能快速配置,也能在出问题时定位到代码层面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00