×

铺货到京东审核不过,多半是商品字段没对齐(附 Python 源码)

万邦科技Lex 万邦科技Lex 发表于2026-10-22 14:36:35 浏览27 评论0

抢沙发发表评论

# 铺货到京东审核不过,多半是商品字段没对齐(附 Python 源码)

> 驳回理由是「商品信息不符合规范」,七个字,什么信息都没给你。真正的答案在类目属性接口里。

## 一、先看一个真实的比例

一个做小家电的商家,从淘宝铺 200 个商品到京东,批量提交后:

- 通过 70 个
- 驳回 130 个

驳回理由五花八门,但翻来覆去就那几句:

```
商品信息不符合规范
类目属性填写不完整
主图不符合要求
标题包含违规内容
```

**这四句话对应 130 个商品,但没告诉你到底是哪个字段、哪个字、哪张图。**

人工一个个改,改了三天,改完又驳回 40 个。因为你在猜,而审核是按规则判的。

那规则在哪?

## 二、审核规则不在文档里,在接口里

这是整件事的关键认知:

**京东的商品审核规则,是按「叶子类目」下发的,不是全局统一的。**

同样是「必填属性」,保温杯类目要求填「容量」「材质」「保温时长」,手机壳类目要求填「适配机型」「材质」「款式」。这些规则不在通用文档里,只能通过类目属性接口实时拉取。

所以正确的做法不是「看文档写死字段」,而是:

1. 提交前,先查这个叶子类目的属性模板
2. 按模板校验你的 payload
3. 不合格的本地先修,别浪费提交次数

这就是「字段对齐」的真正含义——**和类目的 schema 对齐,不是和文档对齐。**

## 三、四类驳回原因,逐类拆解

### 3.1 属性缺失或取值不合法(占比最高,约 45%)

类目属性分四种输入类型,规则完全不同:

| 类型              | 规则                      | 常见错法                    |
| --------------- | ----------------------- | ----------------------- |
| 单选(select)      | 值必须来自 `valueList`,不能自定义 | 传了「不锈钢」,枚举里只有「304不锈钢」   |
| 多选(multiSelect) | 同上,多个值用逗号分隔             | 用了中文逗号,或者超了最大可选数        |
| 自定义(input)      | 允许自由填写,但有长度和字符限制        | 填了表情符号、特殊符号             |
| 数值(number)      | 必须是数字,可能有单位             | 传了「1.5L」,应该传「1.5」+ 单位属性 |

**最典型的错误**:把淘宝的属性值直接搬过来。淘宝的「材质」可以自由填「不锈钢+塑料」,京东的枚举里只有固定几项,对不上就是驳回。

### 3.2 标题违规(约 25%)

标题是驳回重灾区,规则比想象中严:

- **极限词**:最、第一、顶级、极致、国家级、世界级、独家、唯一
- **其他平台名**:淘宝、天猫、拼多多、1688、抖音(这个最容易被忽略,从淘宝搬过来的标题经常带)
- **特殊符号**:`【】★☆◆●■`、emoji、连续多个空格
- **促销词**:包邮、特价、清仓、秒杀、限时(这些要走活动,不能写在标题里)
- **联系方式**:微信、QQ、手机号

淘宝标题里带「【爆款】」这种前缀很常见,搬到京东必驳回。

### 3.3 主图不合规(约 20%)

京东主图有硬性要求:

- 尺寸不小于 800×800,建议 1000×1000
- **纯白底**(RGB 255,255,255),不能有渐变、场景
- 不能有水印、文字、logo、边框
- 不能是拼接图(多张图拼一张)
- 首图必须是正面图

从淘宝搬过来的主图,十张有八张带促销角标或场景背景,全都不合规。

### 3.4 价格和物流属性(约 10%)

- **市场价必须 ≥ 京东价**,否则驳回
- 重量、体积是必填项,很多类目漏填直接驳回
- 部分类目要求商品条码(69 码),格式校验很严

## 四、方案:提交前先跑一遍预检

核心思路很简单:**把审核规则在本地实现一遍,不合格的不提交。**

```
拉类目 schema  →  构建校验器  →  预检 payload  →  出报告  →  只提交通过的
                                      ↓
                                 不合格的:本地修 / 进人工队列
```

这样做的好处不只是少被驳回:

- **省提交配额**(很多接口有日限)
- **能看到具体是哪个字段错了**,而不是「不符合规范」
- **同一批问题能一次性修完**,不用来回试

## 五、Python 源码

依赖:

```bash
pip install httpx pillow
```

### 5.1 拉类目属性模板

```python
# schema.py
import hashlib
from dataclasses import dataclass, field


@dataclass
class AttrSpec:
    attr_id: int
    name: str
    required: bool
    input_type: str                  # select / multiSelect / input / number
    value_list: list[str] = field(default_factory=list)
    max_length: int | None = None
    unit: str | None = None


def _sign(params: dict, secret: str) -> str:
    keys = sorted(params)
    raw = secret + "".join(f"{k}{params[k]}" for k in keys) + secret
    return hashlib.md5(raw.encode()).hexdigest().upper()

# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex 
async def fetch_attrs(client, app_key: str, secret: str, cid: int) -> list[AttrSpec]:
    """
    拉取某个叶子类目的属性模板。
    这一步是整个方案的基础——所有校验规则都从这里来,不要硬编码。
    """
    params = {
        "method": "jingdong.cat.getAttrs",
        "app_key": app_key,
        "timestamp": "2026-01-01 00:00:00",
        "format": "json",
        "v": "2.0",
        "360buy_param_json": f'{{"cid":{cid},"categoryId":{cid}}}',
    }
    params["sign"] = _sign(params, secret)

    r = await client.post("https://api.jd.com/routerjson", data=params)
    resp = r.json().get("jingdong_cat_getAttrs_responce", {})
    rows = (resp.get("returnType") or {}).get("attributes") or []

    specs = []
    for a in rows:
        specs.append(AttrSpec(
            attr_id=int(a.get("attrId", 0)),
            name=a.get("attrName", ""),
            required=str(a.get("required", "")).lower() in ("true", "1", "y"),
            input_type=_norm_type(a.get("inputType")),
            value_list=[v.get("value", "") for v in (a.get("valueList") or [])],
            max_length=a.get("maxLength"),
            unit=a.get("unit"),
        ))
    return specs


def _norm_type(t) -> str:
    """京东的 inputType 是数字,映射成可读类型"""
    mapping = {"1": "select", "2": "multiSelect", "3": "input", "4": "number"}
    return mapping.get(str(t), "input")
```

这里没有硬编码任何属性名,全部从接口拿。**这是这篇代码里最重要的一点**,因为类目规则会变,硬编码的校验器半年就废了。

### 5.2 校验器

```python
# validator.py
import re
from dataclasses import dataclass

from schema import AttrSpec


@dataclass
class Issue:
    field: str
    level: str          # error / warn
    message: str


def validate_attrs(payload: dict, specs: list[AttrSpec]) -> list[Issue]:
    """按类目 schema 校验属性"""
    issues = []
    submitted = {int(k): v for k, v in (payload.get("attrs") or {}).items()}

    for spec in specs:
        val = submitted.get(spec.attr_id)

        if spec.required and (val is None or str(val).strip() == ""):
            issues.append(Issue(spec.name, "error",
                                f"必填属性缺失(attrId={spec.attr_id})"))
            continue
        if val is None or str(val).strip() == "":
            continue

        text = str(val).strip()

        if spec.input_type == "select":
            if spec.value_list and text not in spec.value_list:
                issues.append(Issue(
                    spec.name, "error",
                    f"取值「{text}」不在枚举内,可选:{'、'.join(spec.value_list[:5])}"))

        elif spec.input_type == "multiSelect":
            parts = [p.strip() for p in re.split(r"[,,]", text) if p.strip()]
            for p in parts:
                if spec.value_list and p not in spec.value_list:
                    issues.append(Issue(spec.name, "error",
                                        f"多选值「{p}」不在枚举内"))
            if "," in text:
                issues.append(Issue(spec.name, "error",
                                    "用了中文逗号,京东要求英文逗号分隔"))

        elif spec.input_type == "number":
            if not re.fullmatch(r"\d+(\.\d+)?", text):
                issues.append(Issue(
                    spec.name, "error",
                    f"数值属性含非数字字符「{text}」,单位「{spec.unit or ''}」不要填进值里"))

        elif spec.input_type == "input":
            if spec.max_length and len(text) > spec.max_length:
                issues.append(Issue(spec.name, "error",
                                    f"超出最大长度 {spec.max_length},当前 {len(text)}"))
            if re.search(r"[^\w\s\u4e00-\u9fa5\-.,%/()()·]", text):
                issues.append(Issue(spec.name, "error",
                                    "含特殊字符(表情、符号),建议只保留中文、数字和常见标点"))

    return issues
```

注意 `number` 那条的提示语:「单位不要填进值里」。这是最高频的低级错误——`1.5L`、`500ml`、`2kg`,全都该拆成数值 + 单位属性。

### 5.3 标题规范化

```python
# title.py
import re

# 极限词(工商明确禁止的绝对化用语)
EXTREME_WORDS = [
    "最", "第一", "顶级", "极致", "国家级", "世界级", "全球首发",
    "独家", "唯一", "首个", "填补空白", "史无前例", "万能",
]

# 其他平台名(从淘宝搬过来最常见的问题)
PLATFORM_WORDS = ["淘宝", "天猫", "拼多多", "1688", "阿里巴巴", "抖音", "快手", "小红书"]

# 促销词(要走活动,不能进标题)
PROMO_WORDS = ["包邮", "特价", "清仓", "秒杀", "限时", "促销", "甩卖", "亏本"]

# 联系方式
CONTACT_PATTERNS = [
    r"微信[:: ]?\w+", r"QQ[:: ]?\d+", r"1[3-9]\d{9}",
    r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}",
]

# 特殊符号
SYMBOL_PATTERN = r"[【】★☆◆●■▲▼※→←↑↓❤♥✅⚠️]"


def normalize_title(title: str, max_len: int = 60) -> tuple[str, list[str]]:
    """返回 (清洗后标题, 改动说明)"""
    notes = []
    t = title

    # 1. 全角转半角(【】这类括号统一去掉)
    t = re.sub(SYMBOL_PATTERN, " ", t)

    # 2. 去平台名
    for w in PLATFORM_WORDS:
        if w in t:
            t = t.replace(w, "")
            notes.append(f"移除平台名「{w}」")

    # 3. 去促销词
    for w in PROMO_WORDS:
        if w in t:
            t = t.replace(w, "")
            notes.append(f"移除促销词「{w}」")

    # 4. 去极限词(只删词,不删整段)
    for w in EXTREME_WORDS:
        if w in t:
            t = t.replace(w, "")
            notes.append(f"移除极限词「{w}」")

    # 5. 去联系方式
    for p in CONTACT_PATTERNS:
        if re.search(p, t):
            t = re.sub(p, "", t)
            notes.append("移除联系方式")

    # 6. 收拢空白、去首尾
    t = re.sub(r"\s+", " ", t).strip(" -_/|")

    # 7. 超长截断(按语义截,尽量断在空格处)
    if len(t) > max_len:
        cut = t[:max_len]
        if " " in cut[max_len // 2:]:
            cut = cut[:cut.rfind(" ")]
        t = cut.strip()
        notes.append(f"截断至 {max_len} 字")

    return t, notes


def check_title(title: str) -> list[str]:
    """提交前最后一道检查,返回问题列表"""
    problems = []
    if len(title) < 8:
        problems.append("标题过短(少于 8 字),信息量不足")
    if len(title) > 60:
        problems.append(f"标题过长({len(title)} 字),上限 60")
    if re.search(r"\s{2,}", title):
        problems.append("含连续空格")
    if title != title.strip():
        problems.append("首尾有空白")
    return problems
```

### 5.4 主图检查

```python
# image_check.py
from io import BytesIO

import httpx
from PIL import Image


async def check_main_image(url: str, client: httpx.AsyncClient) -> list[str]:
    """
    检查主图是否符合京东要求:
    - 尺寸 >= 800x800
    - 白底(边缘四角接近纯白)
    - 近似正方形
    """
    problems = []
    try:
        r = await client.get(url, timeout=15)
        img = Image.open(BytesIO(r.content)).convert("RGB")
    except Exception as e:
        return [f"图片无法读取:{e}"]

    w, h = img.size
    if w < 800 or h < 800:
        problems.append(f"尺寸不足:{w}x{h},要求 ≥800x800")
    if abs(w - h) / max(w, h) > 0.05:
        problems.append(f"非正方形:{w}x{h},建议 1:1")

    # 检查四角是否为白底(各取 20x20 区域)
    corners = [
        img.crop((0, 0, 20, 20)),
        img.crop((w - 20, 0, w, 20)),
        img.crop((0, h - 20, 20, h)),
        img.crop((w - 20, h - 20, w, h)),
    ]
    for i, c in enumerate(corners):
        avg = tuple(sum(ch) // (20 * 20) for ch in zip(*list(c.getdata())))
        if min(avg) < 240:
            problems.append(f"非白底:第 {i + 1} 角平均色 RGB{avg}")
            break

    return problems
```

四角取白判断是个近似方案,够用。要更准的话做边缘像素统计,但 800x800 的商品图,四角白基本就是白底图。

### 5.5 预检 + 提交

```python
# preflight.py
import asyncio
from dataclasses import dataclass, field

import httpx

from schema import fetch_attrs
from validator import validate_attrs, Issue
from title import normalize_title, check_title
from image_check import check_main_image

# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex 
@dataclass
class PreflightResult:
    sku: str
    ok: bool
    payload: dict
    errors: list[Issue] = field(default_factory=list)
    warns: list[Issue] = field(default_factory=list)
    notes: list[str] = field(default_factory=list)


async def preflight(item: dict, client, cfg: dict, attr_cache: dict) -> PreflightResult:
    cid = int(item["cid"])

    # 1. 类目 schema(带缓存,同一类目只拉一次)
    if cid not in attr_cache:
        attr_cache[cid] = await fetch_attrs(
            client, cfg["key"], cfg["secret"], cid)
    specs = attr_cache[cid]

    errors, warns = [], []

    # 2. 属性校验
    for issue in validate_attrs(item, specs):
        (errors if issue.level == "error" else warns).append(issue)

    # 3. 标题规范化
    title, notes = normalize_title(item["title"])
    item["title"] = title
    for p in check_title(title):
        errors.append(Issue("标题", "error", p))

    # 4. 主图
    for url in item["images"][:1]:
        for p in await check_main_image(url, client):
            errors.append(Issue("主图", "error", p))
    if not item["images"]:
        errors.append(Issue("主图", "error", "缺少主图"))

    # 5. 价格关系
    jd_price = float(item.get("jd_price") or 0)
    market_price = float(item.get("market_price") or 0)
    if jd_price <= 0:
        errors.append(Issue("京东价", "error", "京东价必须大于 0"))
    if market_price and market_price < jd_price:
        errors.append(Issue(
            "市场价", "error",
            f"市场价 {market_price} 低于京东价 {jd_price},会被驳回"))

    # 6. 物流属性(重量体积必填)
    if not item.get("weight"):
        errors.append(Issue("重量", "error", "物流重量必填"))
    if not item.get("volume"):
        warns.append(Issue("体积", "warn", "体积未填,部分类目会驳回"))

    return PreflightResult(
        sku=str(item["sku"]),
        ok=not errors,
        payload=item,
        errors=errors, warns=warns, notes=notes,
    )


async def run_batch(items: list[dict], cfg: dict, concurrency: int = 5):
    attr_cache: dict = {}
    results: list[PreflightResult] = []
    sem = asyncio.Semaphore(concurrency)

    async with httpx.AsyncClient(timeout=30) as client:
        async def one(it):
            async with sem:
                try:
                    results.append(await preflight(it, client, cfg, attr_cache))
                except Exception as e:
                    results.append(PreflightResult(
                        sku=str(it.get("sku")),
                        ok=False, payload=it,
                        errors=[Issue("系统", "error", str(e))],
                    ))

        await asyncio.gather(*[one(i) for i in items])

    passed = [r for r in results if r.ok]
    failed = [r for r in results if not r.ok]

    print(f"预检完成:通过 {len(passed)} / 驳回 {len(failed)} / 共 {len(results)}")
    print(f"类目 schema 缓存 {len(attr_cache)} 个")
    return passed, failed


def print_report(failed: list[PreflightResult]):
    """把错误按原因归类,方便一次性修完"""
    from collections import Counter
    counter = Counter()
    for r in failed:
        for e in r.errors:
            counter[f"{e.field}: {e.message[:40]}"] += 1

    print("\n=== 驳回原因分布 ===")
    for reason, cnt in counter.most_common(20):
        print(f"{cnt:>4} 个 | {reason}")
```


````

跑起来:

```python
# main.py
import asyncio
import json

from preflight import run_batch, print_report
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex 

async def main():
    cfg = {"key": "your_appkey", "secret": "your_secret"}
    items = json.load(open("goods.json", encoding="utf-8"))

    passed, failed = await run_batch(items, cfg)
    print_report(failed)

    # 只提交通过的,失败的导出成待修清单
    with open("ready_to_submit.json", "w", encoding="utf-8") as f:
        json.dump([r.payload for r in passed], f, ensure_ascii=False, indent=2)

    with open("need_fix.json", "w", encoding="utf-8") as f:
        json.dump([{
            "sku": r.sku,
            "errors": [f"{e.field}: {e.message}" for e in r.errors],
            "warns": [f"{e.field}: {e.message}" for e in r.warns],
        } for r in failed], f, ensure_ascii=False, indent=2)

    print(f"\n可提交 {len(passed)} 个,待修 {len(failed)} 个")


if __name__ == "__main__":
    asyncio.run(main())
````

**这个 `need_fix.json` 是整套流程里最实用的产出。** 运营拿着这份清单,能一次改完所有问题,而不是被驳回一次改一点、再驳回再改。

## 六、实测效果

同一批 200 个商品:

| 指标      | 直接提交        | 预检后提交        |
| ------- | ----------- | ------------ |
| 首次通过率   | 35%(70/200) | 96%(192/200) |
| 驳回后返工   | 3 天         | 半天           |
| 提交次数    | 3 轮以上       | 1 轮          |
| 驳回原因可定位 | 不能          | 精确到字段        |

剩下 8 个没过的,是类目本身选错了(比如把「电热水壶」放到了「咖啡机」类目),这种情况预检也救不了——类目选错,属性怎么填都不对。

## 七、五个容易忽略的点

1. **类目 schema 要缓存,但别缓存太久。** 同一个叶子类目的属性模板,一天拉一次就够。但类目规则会变,缓存一周以上就可能校验失效。
2. **必填属性可能藏在子属性里。** 有些类目要求「品牌」先选,选完才出现「型号」这个必填项。所以校验要递归,不能只看第一层。
3. **多选属性的分隔符。** 京东要英文逗号,中文逗号会被当成一个整体值。这个细节导致的驳回占比很高。
4. **市场价不能低于京东价。** 这条规则文档里写得很小,但驳回率极高。反过来设置(市场价 = 京东价 × 1.2 左右)最安全。
5. **标题别做过度清洗。** 极限词是删词,不是删句。把「最好的保温杯」删成「保温杯」是对的,删成空字符串就废了。清洗后一定要复查长度。

## 八、最后

这套方案的核心不是代码,是一个认知转变:

**审核不是玄学,是规则。规则既然能通过接口拿到,就能在本地跑一遍。**

与其提交后被驳回、看那七个字的模糊提示、然后猜,不如提交前花 3 秒跑个预检——省的是三轮返工的时间。

代码在上面,属性模板、标题清洗、图片检查、预检报告都齐了。如果你不想自己维护这些平台规则(各平台驳回标准还不一样,改起来挺烦),也可以用现成的三平台 API 接口服务,商品字段按标准格式输出,类目属性对齐这块能省不少事。

有在做的可以聊聊,你们的驳回率现在是多少,主要卡在哪一类。

---

**标签**:`Python` `电商` `京东` `商品发布` `数据校验`


群贤毕至

访客