跳过正文
  1. 文章/

用 Paperless-ngx 整理十年文档:从 Google Drive 文件夹到全文检索归档库

作者
Yang Hu

将近十年积累的个人文档从 Google Drive 文件夹体系迁移到 Paperless-ngx 的完整记录。 涵盖分类体系设计、从 Google Takeout 批量导入、ML 分类器训练,以及日常收件箱工作流。

为什么要迁移
#

过去多年,我的"文档管理"是一棵手工维护的 Google Drive 文件夹树:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
10 - 文书材料/
  10 - 证件材料/身份证件/
  30 - 移民文档/
  30 - Tax Filing/
  40 - Finance/
  50 - 车辆注册/
  60 - 住房买房/
  80 - Medical/
20 - 家装住房信息/
80 - 旅行计划/

归档时还算顺手,但检索很痛苦。想找"2022 年的保险表格",要翻六个文件夹,还得猜当时的命名。 Paperless-ngx 提供全文检索、OCR、以及会从你自己的标注中学习的 ML 分类器—— 对于横跨移民手续、税务申报、房产合同、医疗记录的文档库来说,这是本质性的提升。

系统架构
#

1
2
3
4
5
6
7
8
9
Google Takeout .zip
  │  migrate.py(分类 + 上传)
Paperless-ngx(Docker,10.0.10.10:5656)
  │  REST API  POST /api/documents/post_document/
  │  OCR + 全文索引
  │  ML 分类器(在已标注语料上重训练)
NAS 存储(/volume1/docker/paperless/)

Paperless-ngx 通过 Synology Container Manager 运行在 Docker 中。 所有存储(文档、数据库、Redis)挂载到 /volume1/docker/paperless/

分类体系设计
#

批量导入之前把分类体系设计好很重要——它是 ML 分类器的训练信号。 400 份文档贴错标签,等于教错了东西。

文档类型
#

目标是互斥且完备的类型——覆盖我实际拥有的文档,不多不少。 全部用中文命名,ML 分类器不管标签是什么语言都能学。

ID名称归入此类的内容
1发票收据已付款的发票、收据、付款确认(仅限完成交易)
3操作手册产品说明书、用户指南、组装说明
4活动通告活动邀请函、公告(非预订类)
5参考资料参考材料、价目表、宣传册
6设备信息设备规格、序列号、保修记录
7日程课表周期性日历、课程表(如音乐课日历)
8金融账单银行/投资/券商账单
9税务文件报税表、W-2、1099、1098、HSA
10身份证件护照、驾照、身份证
11合同协议合同、协议、租约、施工授权书
12医疗记录病历、处方、化验报告
13证明证书证明信、公证、各类证书
14移民文件I-797、I-94、I-20、EAD、绿卡
15签证申请签证申请材料(美、中、加等)
16工资单工资条
17房产文件贷款、建筑许可、房产税、HOA
18车辆文件车辆租约、DMV、年检
28行程预订酒店/机票预订确认、活动门票、通行证、SNO-PARK 等许可证
29报价估价维修估价、施工报价、项目提案(付款前的询价阶段)

设计决策说明:

  • 发票收据 = 已付款交易:酒店确认单归 行程预订;维修估价归 报价估价;只有完成付款的文件归此类
  • 行程预订 vs 发票收据:酒店确认单是预订凭证,不是收据;入住结账后的账单才是收据。门票和通行证也归此类
  • 报价估价 vs 发票收据:未付款的估价/报价是采购前阶段;付款并开票后转为发票收据
  • 设备信息 ≠ 参考资料:设备档案(序列号、保修)与参考材料(价目表、手册)结构不同,分开有意义
  • 活动通告 ≠ 日程课表:前者是一次性通知,后者是反复查阅的参考文档
  • 金融账单合并银行 + 投资:都是周期性账单,必要时用往来方(HSBC vs Vanguard)区分
  • 税务文件涵盖所有税务文档:不需要在类型层面细分 W-2/1099/报税表,标签和往来方承担这个维度
  • 移民文件 vs 签证申请:在途维持身份文件(I-797、EAD)与签证申请材料是不同的业务场景
  • 医疗账单 → 发票收据 + #医疗 标签:医疗账单本质是收据,标签提供"医疗"维度,不需要单独的类型

标签
#

标签处理不属于文档类型的横切维度:

标签用途
#保险保险相关
#医疗医疗主题
#教育教育相关
#财务财务主题
#房产房地产
#旅行旅行
#车辆车辆
#移民移民主题
#签证签证主题
#税务税务主题
#待处理收件箱 / 待审阅
#重要重要、时效性强
#归档已归档,无需操作

年份标签经过考虑后放弃。 最初计划给每份文档加 #2016 到 #2026 的年份标签。 反思后发现:年份标签给 ML 训练信号增加噪音,而"文档年份"自定义字段能更干净地覆盖这个需求 (可按数值过滤和排序,不占用标签云空间)。

自定义字段
#

ID名称类型用途
1Amount浮点数发票/账单金额
2Bill Period日期账单周期截止日
3到期日期日期文件到期日(证件、签证)
4Document Year整数税务年份、历史文档所属年份
5保单/账号字符串保单号、账号

往来方
#

设置了出现频率足以作为过滤条件的实体:

ID名称
5IRS
6California FTB
7Google LLC
8USCIS
9US Dept of State
10Vanguard
11HSBC
12County of Santa Clara

导入前:关闭 ML 自动匹配
#

批量导入 400 份文档之前,先关闭所有文档类型、标签、往来方的 Auto/ML 匹配。 如果自动匹配在导入过程中处于开启状态,Paperless 可能用半训练的模型尝试重新分类, 覆盖你精心指定的元数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 关闭所有文档类型的匹配
curl -s "http://10.0.10.10:5656/api/document_types/?page_size=50" \
  -H "Authorization: Token YOUR_TOKEN" | jq '.results[] | .id' | \
while read id; do
  curl -s -X PATCH "http://10.0.10.10:5656/api/document_types/$id/" \
    -H "Authorization: Token YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"matching_algorithm": 0}'
done

# 标签和往来方同理
# matching_algorithm: 0=无, 1=任意, 2=全部, 3=字面, 4=正则, 6=Auto/ML

导入完成后,对需要 ML 建议的类型、标签、往来方重新设置 "matching_algorithm": 6

例外:#待处理 应永久保持 0(无)。 它是由工作流指定的状态标签,不是内容类别—— ML 没有理由去猜测它。

迁移脚本
#

migrate.py 一次性完成分类和上传,直接读取 Google Takeout .zip 而无需全部解压。

分类逻辑
#

每个文件通过按优先级排列的 if 链根据文件夹路径进行分类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def classify(rel_path: str) -> dict:
    p = rel_path
    filename = Path(p).name
    year = extract_year(Path(p).parts)  # 从路径任意部分提取年份
    corr = corr_from_filename(filename) # 从文件名关键词匹配往来方

    if "10 - 身份证件" in p:
        return result(DOCTYPE["身份证件"], [TAG["移民"]])

    if "30 - 移民文档" in p:
        return result(DOCTYPE["移民文件"], [TAG["移民"]])

    if "30 - Tax Filing" in p:
        tax_tags = [TAG["税务"]]
        if re.search(r'w[\-\s]?2\b', fl):
            return result(DOCTYPE["税务文件"], tax_tags, corr or CORR["Google LLC"])
        if re.search(r'(f?1099|f?1098|f?5498|1095)', fl):
            return result(DOCTYPE["税务文件"], tax_tags, corr)
        return result(DOCTYPE["税务文件"], tax_tags)

    # ... 更多规则 ...

    return result(None, [TAG["待处理"]])  # 兜底:进收件箱

年份从路径中任意匹配 \b(20[12]\d)\b 的部分提取—— 30 - Tax Filing/2022/W2.pdf 会自动得到 year=2022。

往来方先从文件名关键词推断(googlehsbcvanguardirsftb), 再由文件夹特定规则覆盖。

上传
#

1
2
3
4
5
6
7
resp = requests.post(
    f"{API_BASE}/documents/post_document/",
    headers={"Authorization": f"Token {API_TOKEN}"},
    files={"document": (filename, data)},
    data=form_data,   # (key, value) 元组列表,支持重复字段
    timeout=120,
)

标签必须以重复表单字段方式发送(不是 JSON 数组):

1
2
for tag_id in meta["tags"]:
    form_data.append(("tags", tag_id))

自定义字段使用 JSON 编码的字典,键为字符串:

1
form_data.append(("custom_fields", json.dumps({str(YEAR_FIELD_ID): year})))

文件类型过滤
#

跳过混入 Takeout 的非文档文件:

1
2
3
4
5
6
SKIP_EXTENSIONS = {
    ".html", ".csv", ".qfx", ".gsheet", ".gdoc", ".gslides",
    ".gdraw", ".gmap", ".java", ".bin", ".db", ".exe", ".7z",
    ".rar", ".gshortcut", ".pptx", ".xls", ".xlsx", ".tar",
    ".doc", ".docx",  # Paperless OCR 支持不稳定
}

.doc/.docx 技术上受 Paperless 支持,但 OCR 效果不稳定; 如果需要全文检索,建议先导出为 PDF。

预演模式
#

1
python3 migrate.py --dry-run

输出示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
=================================================================
  IMPORT PREVIEW — 358 files to import
=================================================================

By document type:
  税务文件        89 files
  移民文件        72 files
  金融账单        61 files
  ...

Skipped (non-document files): 47

Sample mappings (first 30):
  10 - 文书材料/30 - Tax Filing/2022/W2-Google.pdf
    → 税务文件 | tags:['税务'] | corr:Google LLC | year:2022
    title: W2-Google

执行导入
#

1
python3 migrate.py --execute --yes

--yes 跳过确认提示(通过 SSH 运行时 input() 会挂起,必须加此参数)。 Paperless 按内容哈希去重,重复运行是安全的。

导入结果
#

358 份文档分布在 17 种文档类型中(后续分类体系扩展至 19 种,见下文)。含 OCR 处理时间,上传约耗时 25 分钟。 排除 .doc/.docx 后零错误。

ML 分类器训练
#

所有文档完成 OCR 且批量清除 #待处理 标签后:

1
2
3
# 在 NAS 上运行(需要 docker 权限)
/usr/local/bin/docker exec paperless-webserver-1 \
  python3 manage.py document_create_classifier

~400 份已标注文档为分类器提供了扎实的训练集。 最常见的文档类型(税务文件、移民文件、金融账单各有 50-90 个样本)效果较好。 较少见的类型随着文档增加会持续改善。

训练前等 OCR 完成。 分类器在 OCR 文本上训练,不是原始文件。 过早运行意味着在空白或残缺的文本上训练。

收件箱工作流
#

新文档(手动上传、移动端扫描、邮件导入)自动获得 #待处理 标签:

设置 → 工作流 → 添加工作流:

  • 名称:新文档收件箱
  • 触发:文档添加(类型 2)
  • 动作:指定标签 #待处理(动作类型 1)

无论来源如何,每份新文档都会触发工作流,给你一个可靠的收件箱视图。

#待处理 的 matching_algorithm 永久设为 0(无)——它只由工作流指定,ML 不会猜测它。 这保证了它作为状态信号的干净性。

审阅完文档后移除 #待处理,文档从收件箱移出。

日常使用方式
#

文档来源
#

来源方式
纸质文档手机扫描 App(如 Scanner Pro),上传到 Paperless
邮件附件Paperless 邮件收件箱(IMAP 轮询,单独配置)
下载的 PDF拖放到 Paperless UI 或消费文件夹
消费文件夹NAS 上的 SMB 共享,从 Windows/Mac 可访问

审阅流程
#

  1. 打开保存的搜索:标签:#待处理
  2. 逐份文档:确认类型,补充缺失标签,修正标题
  3. 移除 #待处理——文档离开收件箱
  4. 对时效性强或即将到期的文档加 #重要

检索文档
#

  • 全文搜索处理大多数场景(如"EAD renewal 2023")
  • 按文档类型 + 往来方过滤账单(金融账单 + Vanguard)
  • 按往来方 + Document Year 自定义字段过滤税务文档
  • 完整处理且无需后续操作的文档加 #归档

凭证备份
#

将 Paperless 管理员密码和 API Token 存入密码管理器(Bitwarden)。 API Token 在 设置 → Tokens 中查看;如需轮换,更新所有脚本中的引用。

排障记录
#

批量清除 #待处理 标签
#

如果在禁用 Auto/ML 之前运行了分类器,它可能已经学会给所有文档打此标签。 通过 API 批量清除:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import requests

API_BASE  = "http://10.0.10.10:5656/api"
API_TOKEN = "YOUR_TOKEN"
TAG_ID    = 7  # #待处理

headers = {"Authorization": f"Token {API_TOKEN}",
           "Content-Type": "application/json"}

r = requests.get(f"{API_BASE}/documents/?tags__id__all={TAG_ID}&page_size=500",
                 headers=headers)
doc_ids = [d["id"] for d in r.json()["results"]]
print(f"待清理文档数:{len(doc_ids)}")

r = requests.post(f"{API_BASE}/documents/bulk_edit/",
    headers=headers,
    json={"documents": doc_ids,
          "method": "remove_tag",
          "parameters": {"tag": TAG_ID}})
print(r.status_code, r.text)

文档类型被错误自动指定
#

ML 在语料不均衡或有噪音时可能分错。手动纠正后重新训练:

1
2
/usr/local/bin/docker exec paperless-webserver-1 \
  python3 manage.py document_create_classifier

每次手动纠正都会反馈到训练集。

custom_fields 格式错误
#

Paperless 期望自定义字段为以字符串为键的 JSON 编码字典:

1
2
3
4
5
# ✓ 正确
json.dumps({"4": 2024})

# ✗ 错误——返回 400 错误
json.dumps([{"field": 4, "value": 2024}])

端口被占用 / 防火墙
#

如果 Paperless 不可达,检查 NAS 上 5656 端口是否有冲突服务。 DSM 防火墙也可能拦截特定子网的访问——检查 控制面板 → 安全性 → 防火墙。

我的配置
#

项目
NASSynology DS1621+
NAS IP10.0.10.10
Paperless 端口5656
Docker 镜像ghcr.io/paperless-ngx/paperless-ngx:latest
导入文档数358
导入来源Google Takeout(单个 .zip,约 2 GB)
OCR 语言中文 + 英文
训练语料~400 份已标注文档,覆盖 19 种类型

备注
#

  • post_document 接口是异步的——Paperless 将文档加入 OCR 队列后立即返回 {"result": "OK"},文档要等 OCR 完成后才可搜索(通常每份几秒到一分钟,取决于 NAS 负载)
  • Paperless 按文件内容的 SHA256 去重——重复运行导入脚本是安全的,重复文档会被静默跳过
  • Google Takeout 默认导出 Google Docs/Sheets 为原生格式;如需 PDF, 创建 Takeout 时选择"导出格式: PDF"
  • document_create_classifier 命令在训练集未变化时输出 No updates since last training——这是正常现象,不是错误