
最近折腾了把 Paprika 900 个食谱迁移到 Mealie,整个过程踩了几个坑,这篇把迁移结果说清楚。Paprika 的食谱内容能完整过来,但周围的元数据基本全丢——配料数量、评分、收藏夹、缩放系数这些要么变成纯文本,要么直接消失。迁移本身很快,真正的活儿在后面的脚本修复阶段。
按人群划分的结论:
本质上就是拿结构换控制权:Mealie 给你一个可查询的库,但代价是你得自己重建 Paprika 里本来就没结构化的那部分数据。
从 Paprika 3 导出得到一个 .paprikarecipes 后缀的文件。改成 .zip 打开,里面每个食谱一个 gzip 压缩的 .paprikarecipe 文件。解压一个出来就是一段平铺的 JSON。没有 schema 文件、没有索引、各食谱之间也没关联。
导入之前先跑一遍 unzip -l library.paprikarecipes | wc -l。如果确认是 900 个食谱,就该看到 900 条记录加上 zip 头部的几行。这是你的基准数量,之后要和 Mealie 里显示的总数对比。
每个 JSON 对象携带的字段是固定的:
uid 和 hash,Paprika 同步用的 UUID 和内容哈希,对 Mealie 模型没有对应出口。ingredients、directions、notes 和 description,全是单字符串、换行分隔,不是数组。prep_time、cook_time、total_time 和 servings,存的都是"1 hr 20 min"这种人类可读字符串,不是整数也不是 ISO 8601 时长。photo_data 存 base64 JPEG,还有 photo、photo_hash、image_url 和 photos 数组(存额外图片)。categories 是字符串数组,加上 rating、difficulty、on_favorites、scale、source、source_url、created 和自由文本 nutritional_info。这些数据本身没问题,它只是 Paprika 的同步格式,不是交换格式——这个区别决定了下面所有数据丢失的原因。
导入本身在实例内部执行,从分组数据管理下的迁移页面操作。上传 .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_time、cook_time
|
prepTime、performTime
|
保留原始人类可读字符串,不解析成分钟数 |
上面每行都是有字段能过来的。下一节说那些根本没行可写的。
界面上不会提示你丢了什么。导入只报告创建了多少条食谱,然后就没了。以下是 Mealie 的食谱模型里根本没有对应位置的字段:
difficulty:Paprika 给每个食谱存了简单/中等/困难标记,Mealie 整个 schema 里没有这个字段,所以值被读取后直接丢弃。scale:保存的份量倍数没了。Mealie 在查看时根据 recipeYield 做缩放,所以一个你一直按 2 倍做的食谱会恢复到基准用量。on_favorites:Mealie 里收藏夹在用户记录上,不在食谱记录上,所以 120 个打星标的食谱会留给你一个空空的收藏列表。nutritional_info:Paprika 存一段自由文本。Mealie 需要热量、脂肪、蛋白质、碳水、纤维、钠、糖七个独立数值字段,一段文字没法拆成七个类型化列,除非做解析。uid 和 hash:同步标识符被替换成 Mealie 新的 UUID,没有任何稳定键值能让你把导入后的食谱和源文件对应回去。动手之前先数一数。解压导出包,然后对每个字段跑类似这样的命令:for f in *.paprikarecipe; do gunzip -c "$f"; done | jq -r 'select(.difficulty != "") | .name' | wc -l。大多数 900 个食谱的库难度标记只出现在几十个上,缩放系数更少。修复工作量要花在字段实际有值的地方,不是字段格式上写着"可以有"的地方。
因为两个应用对配料的建模不同,导入程序拒绝猜测。
quantity、unit 引用、food 引用、自由 note,以及未处理的 originalText。单位和食材是独立的数据库表,不是食谱里的字符串。originalText,然后把 quantity、unit、food 全留空,而不是自己瞎拆。quantity。没有存数量的话,点 2x 只会改变份量标签显示,每个配料行还是"2 cups"原封不动。数量很关键。假设每个食谱平均 10 条配料,900 个食谱就是大约 9000 个字符串需要结构化。这是迁移中最大的单项修复工作,不过 Mealie 自带一个解析器来解决这个问题,下文会细说。
每个食谱只保留一张照片,其他的没了。
photo_data 里的 base64 JPEG 被写到磁盘上作为三个尺寸的 WebP:original.webp、min-original.webp 和 tiny-original.webp,放在 data/recipes/<uuid>/images/ 下。图片是完整的,但字节不一样了,所以跟源文件做校验和比对永远对不上。photos 数组没有下落:Paprika 允许给一个食谱附多张图。Mealie 的食谱模型只带一张主图加一个独立的 assets 文件夹,迁移不填充 assets,所以第二张及后续照片被读取后直接丢弃。image_url 的食谱进来是空白:如果一个食谱只有远程 URL 没有嵌入数据,导入过程中不会去抓取,卡片会显示默认占位图。pg_dump 不是完整的库备份,导入后跑一下 du -sh data/recipes 才是你做磁盘规划真正要看的数字。先数一数损失量再决定要不要管。解压导出包,对文件跑 jq 'select((.photos | length) > 1) | .name'。大多数库多图食谱只有几十个,通常是你自己做过拍过照的那些。如果数量在 50 以下,手动通过食谱编辑器重新附图比写上传脚本更快。
有些可以从导出文件恢复。有些是永久丢失,因为它本来就没在导出文件里。
| 你曾经有的数据 | 在 Mealie 里应该放在哪里 | 你能做什么 |
|---|---|---|
| 星级评分 | 食谱的 rating,新版本还有单独的用户评分 |
导入后先验证一个已知的 5 星食谱是否显示,然后从 JSON 批量 PATCH 其余的 |
| 收藏标记 | 用户维度的收藏关系,不是食谱列 | 按源文件里 on_favorites 筛选,每个食谱一次 API 调用重建 |
| 保存的缩放系数 | 字段根本不存在 | 只对你常按固定倍数做的食谱,手动把系数合并进 recipeYield 文本 |
| 添加日期 |
createdAt 和 dateAdded
|
900 个食谱全显示导入日期,按名称排序直到你从 created 字段回填 |
| 上次做的时间及历史 |
lastMade 加时间线事件 |
什么都过不来,也不可能过来,因为 Paprika 根本没导出烹饪日志 |
| 工具和设备 | 专门的工具表 | 从零开始,Paprika 没有可映射的对应概念 |
按数据是否存在来排修复优先级。评分、收藏夹和创建日期都躺在解压出来的 .paprikarecipe 文件里,所以本质上就是脚本回填。烹饪时间线不一样——Mealie 的时间线靠你事后记录的事件构建,意味着历史从导入那天开始,你之前做过的那些年在新系统里根本不存在。这个认了就行,别浪费时间找 workaround。
Nextcloud Cookbook 的迁移更干净,因为它的源格式本来就是为交换设计的,不是为同步设计的。
recipe.json 加 full.jpg 和 thumb.jpg。上传前把父目录打成 zip,用 find . -name recipe.json | wc -l 先确认数量。recipe.json 是 schema.org Recipe 文档:和 Mealie 内部用的词汇表一样,和 Mealie 抓取 URL 用的词汇表也一样,所以映射是字段对字段,而不是 key 对猜。recipeIngredient 已经是 JSON 数组,每个配料一条,去掉了所有换行拆分错误。但字符串本身仍然是非结构化的,所以前面说的解析工作仍然存在且不可避免。keywords 是扁平列表,不存在值该进分类还是标签的歧义。这类数据丢失性质不同。Nextcloud Cookbook 本身就没存收藏标记、烹饪日志和个人星级评分,所以没有什么个人信号要丢失,也没什么要回填的。迁移的是结构,而你之前本来就没有历史记录。
RecipeKeeper 交出的格式是三个来源里最弱的,因为它导出的是网页,不是数据文件。
recipes.html 加一个 images 目录。没有按食谱分的文件,所以基准数量要从 markup 里数,不是从文件列表里数:grep -c 'itemprop="name"' recipes.html。itemprop 值。用编辑器打开再保存,如果 markup 被重写了,导入会找到更少的食谱,报告成功,但不给你任何错误信息去排查。originalText 完全没问题,但配料解析器处理"1/2 cup"比处理那个 glyph 可靠得多,这就把工作量推到了修复阶段。在信任 900 个食谱的导入结果之前,先挑 10 个覆盖每个你在意字段的样本做验证。
从用户资料里创建一个 API token,然后从外部检查库,而不是在界面里滚来滚去。
curl -H "Authorization: Bearer $TOKEN" "https://your-mealie-host/api/recipes?page=1&perPage=1" | jq .total,跟前面从导出包里数的文件数对比。如果 900 个进去 894 个出来,有 6 个文件没找到。perPage=1000 拉所有名称,排序,然后跟源文件提取的名称 diff。带数字后缀的 slug 说明有重复标题,空缺的名称说明有文件被跳过了——光比数量看不出来,因为一个重复会抵消一个失败。.paprikarecipes zip 现在是每个被 Mealie 丢弃字段的唯一副本,放在备份旁边一起存。数量匹配只能证明行挪过去了,证明不了库可用。
半成功的迁移看起来和完全成功的迁移一模一样。界面只报告它创建了什么,从不报告它跳过了什么。
docker logs -f mealie 开第二个终端。每个文件的错误在那里有命名,其他地方找不到。import-2026-04,这样一条过滤后的 /api/recipes/bulk-actions/delete 调用就能撤销整个批次。