开发技巧

把整理好的商品数据(JSON)转成 Shopify 后台可以直接导入的商品 CSV

把整理好的商品数据(JSON)转成 Shopify 后台可以直接导入的商品 CSV:处理多变体、多图片的行合并,检查 handle 格式、价格格式和图片地址,有错误就退出并列出所有问题。列名按 Shopify 帮助中心当前版本的 CSV 文档。

BINBHO · 图文教程

把整理好的商品数据(JSON)转成 Shopify 后台可以直接导入的商品 CSV:处理多变体、多图片的行合并,检查 handle 格式、价格格式和图片地址,有错误就退出并列出所有问题。列名按 Shopify 帮助中心当前版本的 CSV 文档。

先看清成品

本机用 pytest 跑了 3 个测试,全部通过。示例数据第一次转换报了 2 个错误(价格带了 ¥、图片用了 http),改正后输出 4 行 CSV,退出码 0。没有在真实店铺后台导入,导入结果以 Shopify 后台的预览和报错为准。

注意:Shopify 的套餐价格、交易费和应用收费以官方价格页和你所在地区的条款为准,本文不列数字。只上架你有权销售的商品和有授权的图片;不要共享店铺员工账号,每个人用自己的员工账号登录。

做完之后你会得到什么?

成果 说明
to_shopify_csv.py JSON → CSV,UTF-8 编码、LF 换行
行合并 第一行写商品信息,后面几行只写变体或图片
校验 handle、重复、价格、https 图片、alt 长度
导入方式 先用 draft 状态导入,检查后再上架

需要准备哪些环境?

项目 要求 说明
Python 3.9 以上(本机 3.13) 只用标准库,测试要 pytest
图片 公开可访问的 https 地址 例如先上传到后台 Content → Files
店铺后台权限 能导入商品 后台 Products → Import

文件结构

28-shopify-B-src/
├── products.json        # 改正后的示例
├── products.bad.json    # 带 2 个错误的原始示例
├── to_shopify_csv.py
├── test_to_csv.py
└── out.csv              # 本机生成的结果

第 1 步:官方 CSV 的几条规则

规则 来源(帮助中心 CSV 页面)
新建商品时只有 Title 必填;有变体时还要填 URL handle Required columns
handle 只能用字母、数字和连字符,不能有空格 URL handle
价格只写数字,不带货币符号 Price
有变体时,第一行写全商品信息,后面的行只写 handle 和变体信息 Create your product CSV file
每张图片占一行,图片地址必须公开、用 https Adding multiple product images
文件用 UTF-8 编码、LF 换行 Create your product CSV file
Status 可以是 active / draft / archived Status

脚本里的 handle 规则比官方更严格:只允许小写字母,避免大小写不同的 handle 造成混淆。

第 2 步:输入数据

products.json:

[
  {
    "title": "手冲咖啡豆 耶加雪菲 250g",
    "handle": "yirgacheffe-250g",
    "description": "<p>浅烘,柑橘与花香。</p>",
    "vendor": "Demo Roastery",
    "tags": ["coffee", "light-roast"],
    "status": "draft",
    "option_name": "研磨",
    "variants": [
      {"value": "整豆", "price": "88.00", "sku": "YG-250-WB", "qty": 20},
      {"value": "手冲粗细", "price": "88.00", "sku": "YG-250-PO", "qty": 10}
    ],
    "images": [
      {"url": "https://example.com/img/yg-1.jpg", "alt": "耶加雪菲包装正面"},
      {"url": "https://example.com/img/yg-2.jpg", "alt": "咖啡豆特写"},
      {"url": "https://example.com/img/yg-3.jpg", "alt": "冲煮示意"}
    ]
  },
  {
    "title": "V60 滤纸 100 张",
    "handle": "v60-filter-100",
    "description": "<p>白色无漂白。</p>",
    "vendor": "Demo Roastery",
    "tags": ["accessories"],
    "status": "draft",
    "variants": [{"price": "25", "sku": "FLT-100", "qty": 50}],
    "images": [{"url": "https://example.com/img/filter.jpg", "alt": "滤纸"}]
  }
]

只有一个规格的商品不写 option_name,脚本会让 Option 两列留空。

第 3 步:转换脚本

to_shopify_csv.py:

"""把 products.json 转成 Shopify 商品导入 CSV。列名按 Shopify 帮助中心
“Using CSV files to import and export products” 当前版本。"""
import csv, json, re, sys
from decimal import Decimal, InvalidOperation

COLUMNS = ["URL handle", "Title", "Description", "Vendor", "Tags", "Published on online store",
           "Status", "Option1 name", "Option1 value", "SKU", "Price", "Inventory quantity",
           "Product image URL", "Image position", "Image alt text"]
HANDLE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")

def money(v, where, errors):
    try:
        d = Decimal(str(v))
        if d < 0: raise InvalidOperation
        return f"{d:.2f}"
    except InvalidOperation:
        errors.append(f"{where}: 价格必须是不带货币符号的数字,收到 {v!r}")
        return ""

def build(products):
    rows, errors, seen = [], [], set()
    for p in products:
        h = p.get("handle", "")
        if not HANDLE.match(h): errors.append(f"{h!r}: handle 只能用小写字母、数字和连字符")
        if h in seen: errors.append(f"{h}: handle 重复")
        seen.add(h)
        variants, images = p.get("variants") or [{}], p.get("images", [])
        if len(variants) > 1 and not p.get("option_name"):
            errors.append(f"{h}: 多个变体必须有 option_name")
        for img in images:
            if not img["url"].startswith("https://"): errors.append(f"{h}: 图片要用公开的 https 地址 {img['url']}")
            if len(img.get("alt", "")) > 512: errors.append(f"{h}: alt 超过 512 字符")
        for i in range(max(len(variants), len(images))):
            r = dict.fromkeys(COLUMNS, "")
            r["URL handle"] = h
            if i == 0:   # 第一行写商品级字段
                r.update({"Title": p["title"], "Description": p.get("description", ""), "Vendor": p.get("vendor", ""),
                          "Tags": ", ".join(p.get("tags", [])), "Published on online store": "true",
                          "Status": p.get("status", "draft")})
            if i < len(variants):
                v = variants[i]
                if p.get("option_name"):
                    r["Option1 name"], r["Option1 value"] = p["option_name"], v["value"]
                r["SKU"] = v.get("sku", "")
                r["Price"] = money(v.get("price", "0"), f"{h} 变体 {i+1}", errors)
                r["Inventory quantity"] = str(v.get("qty", 0))
            if i < len(images):
                r["Product image URL"], r["Image position"], r["Image alt text"] = images[i]["url"], str(i + 1), images[i].get("alt", "")
            rows.append(r)
    return rows, errors

if __name__ == "__main__":
    rows, errors = build(json.load(open(sys.argv[1], encoding="utf-8")))
    with open(sys.argv[2], "w", encoding="utf-8", newline="") as f:   # UTF-8 + LF,按官方要求
        w = csv.DictWriter(f, COLUMNS, lineterminator="\n"); w.writeheader(); w.writerows(rows)
    print(f"{len(rows)} 行写入 {sys.argv[2]}")
    for e in errors: print("ERROR", e)
    sys.exit(1 if errors else 0)

第 4 步:测试与运行

test_to_csv.py:

from to_shopify_csv import build

def P(**kw):
    base = {"title": "T", "handle": "t-1", "variants": [{"price": "9.9", "sku": "A"}], "images": []}
    base.update(kw); return base

def test_variant_and_image_rows_merge():
    rows, err = build([P(option_name="Size", variants=[{"value": "S", "price": 1}, {"value": "M", "price": 1}],
                         images=[{"url": "https://x/1.jpg"}, {"url": "https://x/2.jpg"}, {"url": "https://x/3.jpg"}])])
    assert err == [] and len(rows) == 3
    assert rows[0]["Title"] == "T" and rows[1]["Title"] == ""
    assert rows[2]["Option1 value"] == "" and rows[2]["Image position"] == "3"

def test_price_format():
    rows, err = build([P(variants=[{"price": "9.9"}])])
    assert rows[0]["Price"] == "9.90"
    _, err = build([P(variants=[{"price": "¥25"}])])
    assert "货币符号" in err[0]

def test_handle_rules():
    _, err = build([P(handle="Bad Handle"), P(handle="t-1"), P(handle="t-1")])
    assert any("小写" in e for e in err) and any("重复" in e for e in err)
python -m pytest -q          # 3 passed
python to_shopify_csv.py products.json out.csv

out.csv:

URL handle,Title,Description,Vendor,Tags,Published on online store,Status,Option1 name,Option1 value,SKU,Price,Inventory quantity,Product image URL,Image position,Image alt text
yirgacheffe-250g,手冲咖啡豆 耶加雪菲 250g,<p>浅烘,柑橘与花香。</p>,Demo Roastery,"coffee, light-roast",true,draft,研磨,整豆,YG-250-WB,88.00,20,https://example.com/img/yg-1.jpg,1,耶加雪菲包装正面
yirgacheffe-250g,,,,,,,研磨,手冲粗细,YG-250-PO,88.00,10,https://example.com/img/yg-2.jpg,2,咖啡豆特写
yirgacheffe-250g,,,,,,,,,,,,https://example.com/img/yg-3.jpg,3,冲煮示意
v60-filter-100,V60 滤纸 100 张,<p>白色无漂白。</p>,Demo Roastery,accessories,true,draft,,,FLT-100,25.00,50,https://example.com/img/filter.jpg,1,滤纸

检查结果:耶加雪菲有 2 个变体、3 张图,合并成 3 行,只有第 1 行有标题;第 3 行只有图片。

第 5 步:导入(本机未执行)

  1. 后台 Products → Import,上传 out.csv。
  2. 第一次导入不要勾选 “Overwrite products with matching handles”。
  3. 看预览,确认没问题后导入。商品是 draft 状态,检查完再改成 active。

调试示例

第一次运行退出码是 1

$ python to_shopify_csv.py products.bad.json out.csv
4 行写入 out.csv
ERROR v60-filter-100: 图片要用公开的 https 地址 http://example.com/img/filter.jpg
ERROR v60-filter-100 变体 1: 价格必须是不带货币符号的数字,收到 '¥25'

价格是从中文表格里复制过来的,带了 ¥;图片地址是旧站的 http 链接。把价格改成 25、图片换成 https 后,退出码变成 0。脚本会一次列出所有错误,不会遇到第一个就停。

常见问题

现象 原因 处理
后台导入后中文是乱码 用 Excel 另存为了别的编码 用 UTF-8 保存,或者直接上传脚本的输出
变体变成了独立商品 变体行的 handle 和第一行不一致 同一商品的每一行 handle 都要相同
现有变体被删掉了 覆盖导入时缺了 Option 列 更新已有商品时要带上 Option1 name/value
图片没导入 地址需要登录才能访问,或者不是 https 换成公开的 https 地址

模板规则

  • 源数据用 JSON 保存,CSV 只作为导入用的输出。
  • 先用 draft 状态导入,确认后再上架。
  • 覆盖导入前先从后台导出备份。
  • 列名以帮助中心的当前版本为准,新旧模板不要混用。

项目资源

用途 地址
商品 CSV 说明 https://help.shopify.com/en/manual/products/import-export/using-csv
导入商品 https://help.shopify.com/en/manual/products/import-export/import-products
Python csv 模块 https://docs.python.org/3/library/csv.html

商品 CSV 最容易出错的地方是“一个商品占多行”。用脚本生成,再加上校验,就不用在表格里手动对齐行了。