site logo

Marico's space

900 个食谱的 Paprika 库导入 Mealie 时能保留多少内容,以及如何修复其余部分

编程技术 2026-09-22 14:49:40 7

最近折腾了把 Paprika 900 个食谱迁移到 Mealie,整个过程踩了几个坑,这篇把迁移结果说清楚。Paprika 的食谱内容能完整过来,但周围的元数据基本全丢——配料数量、评分、收藏夹、缩放系数这些要么变成纯文本,要么直接消失。迁移本身很快,真正的活儿在后面的脚本修复阶段。

按人群划分的结论:

  • 用了十年 Paprika 3、攒了 900 个食谱的老用户:先一股脑导入,然后逐个字段核查再做决定。丢的东西集中在元数据,那些信息从食谱正文根本没法还原。
  • 从 Nextcloud Cookbook 自建迁移来的:三个来源里最干净的,因为 Nextcloud Cookbook 已经是 schema.org JSON 文档,跟 Mealie 模型几乎一对一映射。
  • RecipeKeeper 用户(Windows + iOS):修复耗时最长,导出格式是 HTML 文档加图片文件夹,所有内容都得从 markup 里扒出来。
  • 会写 Python 3 的独立开发者:写脚本修复,流程就是分页 GET /api/recipes 遍历,然后对每个食谱发 PATCH。900 个食谱大概 20 行代码能搞定。
  • 普通家庭用户、不会碰终端的:先别删 Paprika App,留着只读模式再用一个季度,购物车和离线功能比字段映射重要多了。
  • 还在观望的:先用临时 Mealie 实例跑一遍导入,数据库单独建,因为迁移脚本不是幂等的,失败一次删库重来比修数据省事。

本质上就是拿结构换控制权:Mealie 给你一个可查询的库,但代价是你得自己重建 Paprika 里本来就没结构化的那部分数据。

900 个食谱的 Paprika 导出包实际包含什么

从 Paprika 3 导出得到一个 .paprikarecipes 后缀的文件。改成 .zip 打开,里面每个食谱一个 gzip 压缩的 .paprikarecipe 文件。解压一个出来就是一段平铺的 JSON。没有 schema 文件、没有索引、各食谱之间也没关联。

导入之前先跑一遍 unzip -l library.paprikarecipes | wc -l。如果确认是 900 个食谱,就该看到 900 条记录加上 zip 头部的几行。这是你的基准数量,之后要和 Mealie 里显示的总数对比。

每个 JSON 对象携带的字段是固定的:

  • 身份字段:uidhash,Paprika 同步用的 UUID 和内容哈希,对 Mealie 模型没有对应出口。
  • 自由文本块:ingredientsdirectionsnotesdescription,全是单字符串、换行分隔,不是数组。
  • 时间和份量:prep_timecook_timetotal_timeservings,存的都是"1 hr 20 min"这种人类可读字符串,不是整数也不是 ISO 8601 时长。
  • 图片数据:photo_data 存 base64 JPEG,还有 photophoto_hashimage_urlphotos 数组(存额外图片)。
  • 分类和个人标记:categories 是字符串数组,加上 ratingdifficultyon_favoritesscalesourcesource_urlcreated 和自由文本 nutritional_info

这些数据本身没问题,它只是 Paprika 的同步格式,不是交换格式——这个区别决定了下面所有数据丢失的原因。

每个 Paprika 字段在 Mealie 里落在哪里

导入本身在实例内部执行,从分组数据管理下的迁移页面操作。上传 .paprikarecipes 文件通过浏览器,整个库在一次请求里经过你的反向代理。900 个食谱加 base64 照片的包体积足够触发 nginx 默认的 client_max_body_size 限制(1m),直接返回 413 错误,Mealie 根本没见到文件。先把这个限制调高,不管 Mealie 跑在 VPS、家用服务器、NAS 还是雅Delimiter 盒子上。

Paprika 字段 Mealie 目的地 途中发生了什么变化
name name 加自动生成的 slug 重复名称会在 slug 尾部追加数字后缀
ingredients recipeIngredient 数组 按换行拆成每行一条,存为原始文本
directions recipeInstructions 数组 拆成步骤,步骤标题为空,不分组
categories 分类和标签 按需创建,所以 Paprika 的 40 个分类会变成 40 条新记录
photo_data data/recipes/<id>/images/ base64 JPEG 解码后写成原始图、最小图、极小图三个 WebP
source_url orgURL 原样保留,死链也照搬
prep_timecook_time prepTimeperformTime 保留原始人类可读字符串,不解析成分钟数

上面每行都是有字段能过来的。下一节说那些根本没行可写的。

哪些 Paprika 字段悄无声息地消失了

界面上不会提示你丢了什么。导入只报告创建了多少条食谱,然后就没了。以下是 Mealie 的食谱模型里根本没有对应位置的字段:

  • difficultyPaprika 给每个食谱存了简单/中等/困难标记,Mealie 整个 schema 里没有这个字段,所以值被读取后直接丢弃。
  • scale保存的份量倍数没了。Mealie 在查看时根据 recipeYield 做缩放,所以一个你一直按 2 倍做的食谱会恢复到基准用量。
  • on_favoritesMealie 里收藏夹在用户记录上,不在食谱记录上,所以 120 个打星标的食谱会留给你一个空空的收藏列表。
  • nutritional_infoPaprika 存一段自由文本。Mealie 需要热量、脂肪、蛋白质、碳水、纤维、钠、糖七个独立数值字段,一段文字没法拆成七个类型化列,除非做解析。
  • uidhash同步标识符被替换成 Mealie 新的 UUID,没有任何稳定键值能让你把导入后的食谱和源文件对应回去。

动手之前先数一数。解压导出包,然后对每个字段跑类似这样的命令:for f in *.paprikarecipe; do gunzip -c "$f"; done | jq -r 'select(.difficulty != "") | .name' | wc -l。大多数 900 个食谱的库难度标记只出现在几十个上,缩放系数更少。修复工作量要花在字段实际有值的地方,不是字段格式上写着"可以有"的地方。

为什么配料变成纯文本而不是结构化数量

因为两个应用对配料的建模不同,导入程序拒绝猜测。

  • Mealie 每个配料存五个字段:数值型 quantityunit 引用、food 引用、自由 note,以及未处理的 originalText。单位和食材是独立的数据库表,不是食谱里的字符串。
  • Paprika 每个配料存一行文字:"2 cups all-purpose flour, sifted" 就是一个字符串,没分隔符告诉你哪部分是数量,导出格式里也没有字段说明。Mealie 的导入程序把整行放进 originalText,然后把 quantity、unit、food 全留空,而不是自己瞎拆。
  • 缩放功能失效:Mealie 界面里的份量倍数乘的是数值型 quantity。没有存数量的话,点 2x 只会改变份量标签显示,每个配料行还是"2 cups"原封不动。
  • 购物车合并失效:Mealie 合并指向同一条食材记录的列表条目。导入三个需要洋葱的食谱会得到三条无法合并的独立条目,因为没有洋葱记录可以匹配。
  • 食材和单位筛选表是空的:按食材浏览或按特定配料筛选食谱,查询的是那些表。900 个食谱原始导入后,两个表可能仍然是零条记录。

数量很关键。假设每个食谱平均 10 条配料,900 个食谱就是大约 9000 个字符串需要结构化。这是迁移中最大的单项修复工作,不过 Mealie 自带一个解析器来解决这个问题,下文会细说。

照片能保留多少张,第二张和第三张呢

每个食谱只保留一张照片,其他的没了。

  • 主图会解码并重新编码:photo_data 里的 base64 JPEG 被写到磁盘上作为三个尺寸的 WebP:original.webpmin-original.webptiny-original.webp,放在 data/recipes/<uuid>/images/ 下。图片是完整的,但字节不一样了,所以跟源文件做校验和比对永远对不上。
  • photos 数组没有下落:Paprika 允许给一个食谱附多张图。Mealie 的食谱模型只带一张主图加一个独立的 assets 文件夹,迁移不填充 assets,所以第二张及后续照片被读取后直接丢弃。
  • 只有 image_url 的食谱进来是空白:如果一个食谱只有远程 URL 没有嵌入数据,导入过程中不会去抓取,卡片会显示默认占位图。
  • 图片存储离开数据库:所有东西都落在文件系统上,不在 SQLite 或 PostgreSQL 里。单独的 pg_dump 不是完整的库备份,导入后跑一下 du -sh data/recipes 才是你做磁盘规划真正要看的数字。

先数一数损失量再决定要不要管。解压导出包,对文件跑 jq 'select((.photos | length) > 1) | .name'。大多数库多图食谱只有几十个,通常是你自己做过拍过照的那些。如果数量在 50 以下,手动通过食谱编辑器重新附图比写上传脚本更快。

评分、收藏夹、缩放系数和永远到不了的数据

有些可以从导出文件恢复。有些是永久丢失,因为它本来就没在导出文件里。

你曾经有的数据 在 Mealie 里应该放在哪里 你能做什么
星级评分 食谱的 rating,新版本还有单独的用户评分 导入后先验证一个已知的 5 星食谱是否显示,然后从 JSON 批量 PATCH 其余的
收藏标记 用户维度的收藏关系,不是食谱列 按源文件里 on_favorites 筛选,每个食谱一次 API 调用重建
保存的缩放系数 字段根本不存在 只对你常按固定倍数做的食谱,手动把系数合并进 recipeYield 文本
添加日期 createdAtdateAdded 900 个食谱全显示导入日期,按名称排序直到你从 created 字段回填
上次做的时间及历史 lastMade 加时间线事件 什么都过不来,也不可能过来,因为 Paprika 根本没导出烹饪日志
工具和设备 专门的工具表 从零开始,Paprika 没有可映射的对应概念

按数据是否存在来排修复优先级。评分、收藏夹和创建日期都躺在解压出来的 .paprikarecipe 文件里,所以本质上就是脚本回填。烹饪时间线不一样——Mealie 的时间线靠你事后记录的事件构建,意味着历史从导入那天开始,你之前做过的那些年在新系统里根本不存在。这个认了就行,别浪费时间找 workaround。

Nextcloud Cookbook 导入器和 Paprika 导入器对比

Nextcloud Cookbook 的迁移更干净,因为它的源格式本来就是为交换设计的,不是为同步设计的。

  • 迁移单位是文件夹,不是压缩包:Nextcloud Cookbook 每个食谱存为一个目录,里面有 recipe.jsonfull.jpgthumb.jpg。上传前把父目录打成 zip,用 find . -name recipe.json | wc -l 先确认数量。
  • recipe.json 是 schema.org Recipe 文档:和 Mealie 内部用的词汇表一样,和 Mealie 抓取 URL 用的词汇表也一样,所以映射是字段对字段,而不是 key 对猜。
  • 配料已经预拆好了:recipeIngredient 已经是 JSON 数组,每个配料一条,去掉了所有换行拆分错误。但字符串本身仍然是非结构化的,所以前面说的解析工作仍然存在且不可避免。
  • 时长是 ISO 8601 格式:"PT1H20M" 直接进来就是真实时长,而不是 Paprika 导出那种人类可读字符串。
  • 营养数据逐键映射:schema.org nutrition 对象把热量、脂肪、蛋白质、碳水暴露为独立属性,直接进 Mealie 的七个营养字段,不需要解析。
  • 关键词直接变成标签:keywords 是扁平列表,不存在值该进分类还是标签的歧义。

这类数据丢失性质不同。Nextcloud Cookbook 本身就没存收藏标记、烹饪日志和个人星级评分,所以没有什么个人信号要丢失,也没什么要回填的。迁移的是结构,而你之前本来就没有历史记录。

RecipeKeeper 导出在进入 Mealie 时丢了什么

RecipeKeeper 交出的格式是三个来源里最弱的,因为它导出的是网页,不是数据文件。

  • 整个库是一个文档:zip 里只有 recipes.html 加一个 images 目录。没有按食谱分的文件,所以基准数量要从 markup 里数,不是从文件列表里数:grep -c 'itemprop="name"' recipes.html
  • 解析依赖 microdata 属性完整保留:导入器从 HTML 里读取 itemprop 值。用编辑器打开再保存,如果 markup 被重写了,导入会找到更少的食谱,报告成功,但不给你任何错误信息去排查。
  • 照片是相对路径引用,不是嵌入的:images 文件夹必须留在 zip 里、放在 HTML 期望的路径位置。重组压缩包的话,食谱导入干干净净但图片全没了,而且没有警告。
  • Unicode 分数直接透传:RecipeKeeper 把 ½ 和 ¼ 写成单字符。进 originalText 完全没问题,但配料解析器处理"1/2 cup"比处理那个 glyph 可靠得多,这就把工作量推到了修复阶段。
  • 个人字段是展示性 markup:菜品类型、评分、来源都是样式化页面元素,不是类型化列,所以实际过来多少完全取决于你导出版本的 markup 形状。
  • 没有任何稳定标识符:跑两次导入就得到两份完整的食谱副本,没有键值可以用来匹配去重。

在信任 900 个食谱的导入结果之前,先挑 10 个覆盖每个你在意字段的样本做验证。

取消订阅之前怎么核查导入结果

从用户资料里创建一个 API token,然后从外部检查库,而不是在界面里滚来滚去。

  • 先比总数:curl -H "Authorization: Bearer $TOKEN" "https://your-mealie-host/api/recipes?page=1&perPage=1" | jq .total,跟前面从导出包里数的文件数对比。如果 900 个进去 894 个出来,有 6 个文件没找到。
  • diff 名称列表,不只是比数量:perPage=1000 拉所有名称,排序,然后跟源文件提取的名称 diff。带数字后缀的 slug 说明有重复标题,空缺的名称说明有文件被跳过了——光比数量看不出来,因为一个重复会抵消一个失败。
  • 随机抽 30 个样本,不是前 30 个:列表顶部通常是最新最简单的那批。随机抽样才能暴露那些步骤里有表格、嵌套列表或多图的食谱。
  • 用 HEAD 请求探测图片,不要用鼠标点:遍历食谱 ID,对每个请求图片路径。统计 404 响应数只需要一个循环,精确告诉你有多少卡片在显示占位图。
  • 数你预期为空的字段:查有多少配料条目没有食材引用、有多少食谱评分是 null。这两个数字决定了接下来修复工作的大小。
  • 永久存档导出文件:.paprikarecipes zip 现在是每个被 Mealie 丢弃字段的唯一副本,放在备份旁边一起存。

数量匹配只能证明行挪过去了,证明不了库可用。

重复、失败和悄无声息消失的食谱

半成功的迁移看起来和完全成功的迁移一模一样。界面只报告它创建了什么,从不报告它跳过了什么。

  • 单个文件失败的原因是些无聊的东西:字符编码问题、截断的 base64 图片数据、格式错误的 JSON 体,都会导致那一个食谱停止。导入继续跑,总数低于你的基准,但浏览器里没有任何提示告诉你哪个文件死了。
  • 容器日志是唯一的见证:导入过程中盯着日志而不是事后看,用 docker logs -f mealie 开第二个终端。每个文件的错误在那里有命名,其他地方找不到。
  • 重复标题通常是真的食谱:Paprika 允许两个叫"烤鸡"的条目。Mealie 两个都保留,给第二个的 slug 追加数字后缀。把所有带后缀的 slug 都删掉是快速丢失真实数据的方式,删除前先比对正文内容。
  • 真正的重复风险是重复运行导入:迁移创建记录,不做匹配更新。传两次同样的文件,900 个食谱变成 1800 个,没有任何键值能把两份副本配对回去。
  • 给迁移批次打标签留边界:如果迁移表单提供给创建内容打标签的功能,打开它。否则导入后立即通过批量操作打一个带日期的标签比如 import-2026-04,这样一条过滤后的 /api/recipes/bulk-actions/delete 调用就能撤销整个批次。
  • 每次尝试前做 Mealie 备份:从设置区恢复备份是单次操作,而手动删 900 个食谱是你不会想花一个晚上干的事。