# ETL: Open Food Facts 导入 (M2) 把 Open Food Facts (OFF, ODbL 许可) 的食品数据采集、转换并入库。只有 Python 采集侧写库,每条记录都以 `openfoodfacts` 为来源记录**字段级溯源**。 ## 流程 ``` OFF API / dump(jsonl[.gz]) → adapters/openfoodfacts.py # 读取(限速 + User-Agent) / 解析 dump → etl/transform.py # 字段映射 + 单位归一 + 营养 per_100g + 分类映射(关键词) → etl/load.py # psycopg upsert(product/food_detail/product_image) + product_source 溯源 ``` ## 运行 先确保本地依赖与迁移就绪:`docker compose up -d postgres` + `migrate ... up`。 ```bash # 用 OFF API 拉指定条码(客户端限速, 默认 4s/次) python -m opengoods.jobs.seed_off --barcodes 3017624010701 5449000000996 # 用下载好的 OFF dump 批量导入(可 .gz), 限制条数 python -m opengoods.jobs.seed_off --dump products.jsonl.gz --limit 1000 ``` DSN 默认读 `OPENGOODS_DATABASE_URL`。 ## 字段映射要点 | OFF | OpenGoods | 处理 | |-----|-----------|------| | `code` | `product.gtin` | GTIN-8/12/13/14 校验位验证, 不合法则不作为 gtin | | `product_name_zh/_/_en` | `product.name` | 优先中文 | | `brands` | `brand` | 取第一个, normalized_name 去重 | | `quantity` | `net_content_*` | 解析 "500 g"/"1,5 L" → 经 `units.py` 归一(原始+归一双存) | | `nutriments.*_100g` | `food_detail.nutriments` | per_100g; 能量 kJ/kcal 双存, 缺一自动换算 | | `allergens_tags`/`additives_tags` | `allergens`/`additives` | 去 `en:` 前缀 | | `nutriscore_grade` | `nutri_score` | 大写单字母 | | `categories*`/name | `category_id` | 关键词映射到自建品类树(起步版, 后续换 OFF 分类→GPC 映射表) | | `image_front_url` | `product_image` | 标 CC-BY-SA 许可 | > 全量 dump 约数 GB;CI 与单测用 fixture 离线验证 transform,DB 集成测试在无库时自动跳过。