首页
/ Paperless-ngx 高级用法详解:自动匹配算法、AI 特性、消费钩子与文件命名体系

Paperless-ngx 高级用法详解:自动匹配算法、AI 特性、消费钩子与文件命名体系

2026-09-05 19:08:53作者:谭伦延

本文基于 paperless-ngx 仓库的 docs/advanced_usage.md 展开,系统讲解这套文档管理系统中"让文档自己分类、自己归档、自己挂钩脚本"的全部高级机制:七种匹配算法与神经网络自动标注、可选的 LLM/AI 特性、消费前后钩子脚本、基于 Jinja2 模板的文件命名体系、条形码处理、双面拼页、SSO 集成与 GPG 解密等,并结合仓库源码逐一点明每个特性的底层实现位置与验证方式,帮助读者从"会用"深入到"懂原理、可排障"。

自动匹配:让标签、通讯方、文档类型和存储路径自己生效

paperless-ngx 会在每次消费文档时,用数据库中每个标签(Tag)、通讯方(Correspondent)、文档类型(DocumentType)和存储路径(StoragePath)上定义的匹配规则,去比对文档正文内容。例如定义一个名为 Home Utility 的标签,其 matchbc hydromatching_algorithmExact,那么只要后续消费的文档正文中出现 bc hydro,该文档就会被自动打上 Home Utility 标签。

官方文档明确提示:匹配逻辑非常强大但也需要实验调优,四种对象(标签/通讯方/文档类型/存储路径)都要通过 Web 界面为其设置 match 文本和匹配算法,保存后再消费一份文档即可看到自动标注生效。

可用的七种算法如下:

  • None:不执行任何匹配。
  • Any:只要 match 中的任一词在文档中出现即命中。例如 Bank1 Bank2 会匹配包含其中任意一词的文档。
  • All:要求 match 中的每个词都出现,但不要求顺序一致。
  • Exactmatch 必须按原样(保留顺序)出现在文档中。
  • 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.pymatches() 函数中,枚举定义见 src/documents/models.pyMATCH_ANYMATCH_ALLMATCH_LITERAL(即 Exact)、MATCH_REGEXMATCH_FUZZYMATCH_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 还会对正则算法做编译校验。
  • 模糊算法的阈值是 90MATCH_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.pymatch_correspondents/match_tags/match_storage_paths 等函数会调用 src/documents/classifier.pyDocumentClassifier 进行 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_BACKENDollama(本地运行)或 openai-like(OpenAI 本身或任何 OpenAI 兼容 API)。
  • PAPERLESS_AI_LLM_MODELopenai-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_BACKENDhuggingface(完全本地嵌入)或 ollama / openai-like
  • 索引只在 AI 已启用且设置了嵌入后端时才会构建

索引由 PAPERLESS_LLM_INDEX_TASK_CRONdocs/configuration.md)控制定时更新,默认每天一次,也可手动重建/压缩,操作见 管理 LLM 索引。注意:huggingface 本地嵌入会在首次使用时把嵌入模型下载到 paperless 数据目录,首次运行需要网络和一定磁盘空间。

文档聊天

LLM 索引启用后,应用顶栏的聊天控件可以回答关于你文档的问题:它视当前视图作用于单篇或多篇文档,回答中会附带所引用来源文档的链接。

AI 安全要点

  • 文档内容被当作不可信数据传给 LLM。
  • 默认允许解析到私有/回环地址的 AI 端点(供本地后端使用);将 PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS 设为 false 可阻断此类端点。

挂钩消费流程:Pre/Post 脚本

有时你希望在文档被消费前后执行任意操作。paperless 用两个简单钩子实现:把脚本放到 paperless 可读取并执行的位置,再把路径写入 paperless.confdocker-compose.env

关键约束:脚本在阻塞进程中执行。脚本跑得太久会显著拖慢消费流程;如需异步,请在脚本内 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)!
...
  1. 外部 scripts 目录挂载到容器内位置;
  2. 用容器内路径设置要执行的脚本;
  3. 同样可写在 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_FORMATdocs/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_NONEdocs/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 对象,包含 idpktitlecontentpage_countcreatedaddedmodifiedmime_typechecksumarchive_checksumarchive_serial_numberfilenamearchive_filenameoriginal_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_FLOWERdocs/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)!
  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)!
  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 大小写敏感的方法。大小写不敏感库中无法同时创建标签 NameNAME(被视为相同);但开启大小写敏感后搜索也会大小写敏感——标题为 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_PAGESdocs/configuration.md),含分隔符的页面会被保留,且成为新文档的第一页

ASN 分配:启用后,条形码的整数值被用作文档归档序列号(ASN),便于回溯纸质原件。若同时启用拆分,遇到 ASN 条形码也会拆分,但与分隔符拆分不同,含 ASN 条形码的页面会保留——每个 ASN 条形码从该页起开启新文档,因此可以把 ASN 条形码印在正文页上。

标签分配:启用后解析条形码并尝试解释、分配标签,相关设置为 PAPERLESS_CONSUMER_ENABLE_TAG_BARCODEPAPERLESS_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/barfoo/bar/double-sided 都会让拼合后的文档表现得像上传到 foo/bar,获得 foobar 两个标签,但不含 double-sided

与文档拆分的交互:可以叠加使用文档拆分,但用普通单面拆分标记页时,拆分后的文档开头会有一张空白页(或标记页背面的内容)。解决办法:做一张正反面都印有拆分条形码的标记页,多余页面就会被自动移除。

SSO 与第三方认证

paperless-ngx 自带 Django 认证系统,也可通过以下方式集成外部认证:

Remote User 认证:依赖某些 SSO 应用提供的 remote user 机制,相关配置项:PAPERLESS_ENABLE_HTTP_REMOTE_USERPAPERLESS_HTTP_REMOTE_USER_HEADER_NAMEPAPERLESS_LOGOUT_REDIRECT_URL(见 docs/configuration.md)。

OpenID Connect 与社会认证:自 2.5.0 版本起通过 django-allauth 包支持集成外部认证系统,用户可用集成的第三方系统登录(可选地注册),配置见 PAPERLESS_SOCIALACCOUNT_PROVIDERSdocs/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_LOGINdocs/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_filegpg --decrypt name_of_file.asc 自测通过)。

配置步骤

  1. 启用 PAPERLESS_ENABLE_GPG_DECRYPTORdocs/configuration.md);
  2. 在宿主机执行 gpgconf --list-dir agent-socket 找到 agent socket(可能为 ~/.gnupg/S.gpg-agent),并找到公钥环位置;
  3. 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_HOMEdocs/configuration.md)。

排障:确认宿主机上 gpg-agent 正在运行;确认在容器内用上述 gpg 命令加解密可用;检查 /usr/src/paperless/.gnupg 下所有文件权限正确(socket 与 keyring 均为 paperless 用户 rw 私有权限)。

小结

docs/advanced_usage.md 覆盖了 paperless-ngx 从"规则匹配 + 神经网络"自动标注、可选 LLM 能力、消费前后钩子、Jinja2 文件命名,到条形码拆页、双面拼页、SSO 与 GPG 解密的完整进阶能力面。理解这些特性时,建议按本文给出的源码路径(匹配算法钩子执行模板过滤器自动匹配训练任务)对照阅读,把文档承诺的行为与实现一一对应,既能快速配置,也能在出问题时定位到代码层面。

登录后查看全文
热门项目推荐
相关项目推荐