Files
goods/docs/etl-openfoodfacts.md
T
lixu 766a573989 feat(M2): Open Food Facts 采集导入 ETL
- adapters/openfoodfacts.py: OFF API 适配器(限速+User-Agent) + JSONL/.gz dump 读取
- etl/transform.py: 纯函数转换(GTIN 校验/净含量解析+归一/营养 per_100g 双能量/过敏原添加剂清洗/关键词分类映射)
- etl/load.py: psycopg upsert(product/food_detail/product_image) + product_source 字段级溯源
- jobs/seed_off.py: CLI(--barcodes API / --dump 文件 / --limit)
- 测试: 13 个离线 transform 单测(fixture) + 可跳过的 DB 集成测试
- 依赖: 增加 psycopg[binary]
- 实跑: 从 OFF API 拉 5 个真实条码入本地 postgres 验证通过
- docs/etl-openfoodfacts.md: 流程/运行/字段映射

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-08 07:01:04 +00:00

1.9 KiB
Raw Blame History

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

# 用 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 约数 GBCI 与单测用 fixture 离线验证 transform,DB 集成测试在无库时自动跳过。