国庆快乐国庆节第 4 / 7 天
57%10-01 → 10-07还剩 3 天

给库存系统接上立创商城的物料参数

9191 字约 23 分钟更新于 2026-10-040 次阅读#立创#元器件#库存系统#工具

🤖 AI 辅助说明:本文由 AI 辅助整理,代码与步骤均已实际跑通;完整的免责声明与 AI 说明放在文末。

起因:想自动填参数,结果卡在了密钥上

上一篇《本地 AI 电子元器件库存系统》里,我把库存系统做出来了,但录入依然靠手填。于是想加个功能:粘一个型号进去,参数、封装、数据手册自动填好。

第一反应是用立创开放平台的商城 API。申请很快,真要跑起来却卡在三件事上:签名很讲究(每个请求都要把方法、地址、时间戳、随机串、请求体拼成 5 行字符串,再用密钥算一个「防伪码」,少一个换行都会失败);失败提示不友好(凭证不对时,网关统一返回 404,看起来像地址写错,其实是在说「我不认识你」);文档只有签名规则、没有接口清单(每个接口要传什么、返回什么,得登录控制台才看得到)。

不过这条路我最后还是走通了(上一篇里的旧系统就是靠它入库的),只是过程比想象中绕得多。把当时踩到的坑列在这儿,免得你重走一遍:

  • 接口只认「商品 ID」(itemId),不认立创编号(C 开头),也不能用型号或关键字查——编号和关键字我都试过,接口会返回 200,但业务结果是「找不到这个商品」;
  • 当时对方接口 / 文档的地址似乎调整过,我这边状态一直不对,排查起来更费劲;
  • 于是一颗料要走完整的一串:打开立创商城网页 → 搜到同款商品 → 复制商品页链接 → 回到程序里从链接提取商品 ID → 再调 API 拿参数。

能用,但每次入库都要多绕这几步——这也是我后来去找「不靠密钥、直接按型号或编号就能拿参数」的办法的直接原因。

正犯愁时突然想到:这些参数,立创自己的网页上本来就是公开的。网页能显示,数据就一定拿得到,只是走的不是那条「官方大门」。

一、资料从哪来:立创商城的公开页面

简单说,我用到的都是立创商城公开的物料资料,不需要申请开发者密钥:

识别链路:型号 → C 编号 → 商品 ID → 参数与规格书

想要什么 从哪儿来 入口
型号 → 立创编号 立创EDA 的公开搜索 easyeda.com/api/eda/product/search
编号 → 商品信息 立创EDA 的公开详情 easyeda.com/api/products/<编号>/components
参数、封装、规格书 立创商城商品页 页面里自带的 __NEXT_DATA__ 数据

补充一句:立创商城的商品页是提前在服务器上拼好的(为了让搜索引擎能收录),所以名称、品牌、参数表、图片、规格书地址这些内容,在页面打开之前就已经整块写进了 HTML——属于页面自带的公开内容,不需要登录,也不需要任何模拟操作。

二、那官方 API 为什么反而难用?

不是官方 API 不好,而是它面向「下单、查库存、拿批号」这类正式业务,安全要求高,门槛自然就高:

  • 而且它认的是商品 ID 这类内部标识:拿立创编号或关键字去查,接口会返回 200,但业务上告诉你「找不到这个商品」(我当时就卡在这里)。

官方开放平台接口与网页公开数据的对比

对个人做工具来说,这笔账很清楚:先用手边就能拿到的公开数据把功能跑起来;等真需要下单、需要权威库存了,再回头啃签名也不迟。

三、整理过程中踩到的两个小坑

两个坑:商品 ID 不是编号;数据手册地址是半截的

第一个坑:商品 ID 不是立创编号。 一开始想当然地用编号去拼商品页地址,结果被跳到了 404 页面。正确顺序是:先用编号问立创EDA,从返回里拿到真正的商品 ID,再打开商品页。

第二个坑:数据手册的地址是「半截」的。 页面里存的是 /upload/public/pdf/... 这样的半截地址,直接拼主站域名,返回的却是一个网页而不是 PDF。试了几个域名,真正能下到文件的是 atta.szlcsc.com。

四、最后拿到手的数据长什么样

拿一颗常见的无线模块举例子(立创编号 C42411897):

项目 结果
型号 E103-RTL8189FTV
品名 2.4GHz 无线模块
品牌 EBYTE(亿佰特)
封装 SMD,12×12mm
分类 WiFi 模块
最小包装 1000 圆盘
数据手册 atta.szlcsc.com 上的 PDF

参数表也能整段拿到:核心芯片 RTL8189FTV-VC-CG、支持协议 IEEE802.11b/g/n、接口类型 SDIO、工作电压、发射功率、接收灵敏度、工作温度……一共十几项,恰好是入库时最不想手打的东西。

五、自己动手:最小可运行实现

下面两份脚本都是能直接跑的完整代码:一个文件、一条命令,不装任何第三方库。Node 18 以上用 demo.mjs,Python 3.8 以上用 demo.py,输出一致——输入编号或型号都行。

脚本运行效果:命令进、参数出

Node 版(demo.mjs)

// demo.mjs —— 用法:node demo.mjs <立创编号 或 型号>
const UA = "my-bom-tool/1.0"; // 写上自己工具的名字即可,不需要伪装浏览器
const input = process.argv[2];
if (!input) {
  console.error("用法:node demo.mjs <立创编号 或 型号>");
  process.exit(1);
}
 
const getJson = async (url) => {
  const res = await fetch(url, { headers: { "user-agent": UA } });
  if (!res.ok) throw new Error(`${url} → HTTP ${res.status}`);
  return res.json();
};
 
// ① 型号 → 立创编号(如果输入的就是 C 开头的编号,这一步可以跳过)
async function findCode(keyword) {
  const data = await getJson(
    `https://easyeda.com/api/eda/product/search?keyword=${encodeURIComponent(keyword)}&page=1&pageSize=10`,
  );
  const hit = data?.result?.productList?.[0];
  if (!hit) throw new Error("没有找到这个型号");
  return hit.number; // 形如 C42411897
}
 
// ② 编号 → 商品 ID(商品页地址用的是 ID,不是编号)
async function findProductId(code) {
  const url = `https://easyeda.com/api/products/${code}/components?version=6.4.19.5`;
  const info = (await getJson(url))?.result;
  if (!info?.lcsc?.id) throw new Error("没有拿到商品 ID");
  return info.lcsc.id; // 形如 44398166
}
 
// ③ 商品 ID → 参数(参数就在商品页的 HTML 里)
async function readItem(id) {
  const res = await fetch(`https://item.szlcsc.com/${id}.html`, {
    headers: { "user-agent": UA },
  });
  const html = await res.text();
  const raw = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/)?.[1];
  if (!raw) throw new Error("页面结构可能变了");
  const web = JSON.parse(raw).props.pageProps.webData;
  const rec = web.productRecord ?? {};
  const files = (rec.fileTypeVOList ?? []).flatMap((group) => group.detailVOList ?? []);
  const pdf =
    rec.pdfFileDetailVO?.fileUrl ??
    files.find((file) => String(file.fileUrl ?? "").toLowerCase().endsWith(".pdf"))?.fileUrl;
  return {
    品名: rec.productName,
    型号: rec.productModel,
    品牌: web.brandVO?.brandName,
    封装: rec.encapsulationModel,
    分类: web.currentCatalog?.catalogName,
    最小包装: `${rec.productMinEncapsulationNumber ?? ""}${rec.productMinEncapsulationUnit ?? ""}`,
    数据手册: pdf ? `https://atta.szlcsc.com${pdf}` : "", // 注意这个域名
    参数: (web.paramList ?? []).map((p) => `${p.parameterName}=${p.parameterDetailValue}`),
  };
}
 
// 串起来
const code = /^c\d+/i.test(input) ? input.toUpperCase() : await findCode(input);
const id = await findProductId(code);
console.log({ 编号: code, ...(await readItem(id)) });
node demo.mjs C42411897
node demo.mjs RTL8189FTV

Python 版(demo.py)

#!/usr/bin/env python3
"""立创物料参数查询:输入型号或立创编号,打印品名/品牌/封装/参数/数据手册。
 
用法:
    python demo.py C42411897
    python demo.py RTL8189FTV
 
只用 Python 标准库,不需要安装任何依赖(Python 3.8+)。
"""
 
import json
import re
import sys
import urllib.parse
import urllib.request
 
UA = "my-bom-tool/1.0"
TIMEOUT = 15
SEARCH = "https://easyeda.com/api/eda/product/search"
DETAIL = "https://easyeda.com/api/products/{code}/components?version=6.4.19.5"
ITEM = "https://item.szlcsc.com/{id}.html"
PDF_BASE = "https://atta.szlcsc.com"
 
 
def get(url, accept="application/json, text/plain, */*"):
    request = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": accept})
    with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
        return response.read().decode("utf-8", "replace")
 
 
def get_json(url):
    return json.loads(get(url))
 
 
def find_code(keyword):
    """型号 -> 立创编号(C 开头)"""
    payload = get_json(SEARCH + "?keyword=" + urllib.parse.quote(keyword) + "&page=1&pageSize=10")
    hits = (payload.get("result") or {}).get("productList") or []
    if not hits:
        raise SystemExit("没有找到匹配「%s」的型号" % keyword)
    first = hits[0]
    return first.get("number") or first.get("mpn")
 
 
def load_item(code):
    """立创编号 -> 商品 ID + 型号(立创EDA 的公开详情)"""
    payload = get_json(DETAIL.format(code=code))
    result = payload.get("result") or {}
    lcsc = result.get("lcsc") or {}
    title = result.get("title") or ""
    model = title.rsplit("_", 1)[0] if "_" in title else title
    return str(lcsc.get("id") or ""), model
 
 
def load_detail(product_id):
    """商品 ID -> 商城商品页里自带的 __NEXT_DATA__"""
    html = get(ITEM.format(id=product_id), accept="text/html")
    matched = re.search(r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.S)
    if not matched:
        raise SystemExit("商品页结构变了,没找到 __NEXT_DATA__")
    web = json.loads(matched.group(1))["props"]["pageProps"]["webData"]
    return web.get("productRecord") or {}, web
 
 
def main():
    if len(sys.argv) < 2:
        raise SystemExit("用法:python demo.py <型号或立创编号>")
    keyword = sys.argv[1].strip()
    code = keyword.upper() if re.fullmatch(r"C\d{3,}", keyword, re.I) else find_code(keyword)
    product_id, model = load_item(code)
    if not product_id:
        raise SystemExit("立创EDA 没收录 %s,拿不到商品 ID" % code)
    record, web = load_detail(product_id)
    images = [x for x in str(record.get("luceneBreviaryImageUrls") or "").split("<$>") if x]
    pdf = str((record.get("pdfFileDetailVO") or {}).get("fileUrl") or "")
    if not pdf:
        for group in record.get("fileTypeVOList") or []:
            for file in group.get("detailVOList") or []:
                if str(file.get("fileUrl") or "").lower().endswith(".pdf"):
                    pdf = str(file["fileUrl"])
                    break
    rows = [
        ("立创编号", record.get("productCode") or code),
        ("品名", record.get("productName") or ""),
        ("型号", record.get("productModel") or model),
        ("品牌", (web.get("brandVO") or {}).get("brandName") or ""),
        ("封装", record.get("encapsulationModel") or ""),
        ("分类", (web.get("currentCatalog") or {}).get("catalogName") or ""),
        ("最小包装", "%s%s" % (record.get("productMinEncapsulationNumber") or "", record.get("productMinEncapsulationUnit") or "")),
        ("数据手册", PDF_BASE + pdf if pdf else "(无)"),
    ]
    for name, value in rows:
        print("%s: %s" % (name, value))
    params = (web.get("paramList") or [])[:12]
    if params:
        print("")
        print("参数:")
        for param in params:
            value = param.get("parameterDetailValue") or param.get("parameterValue") or ""
            print("  - %s: %s" % (param.get("parameterName"), value))
    print("")
    print("主图: %s" % (record.get("breviaryImageUrl") or (images[0] if images else "(无)")))
 
 
if __name__ == "__main__":
    main()
python demo.py C42411897
python demo.py RTL8189FTV

以 C42411897 为例,两份脚本打印出来的都是这些内容:

立创编号: C42411897
品名: 2.4GHz无线模块
型号: E103-RTL8189FTV
品牌: EBYTE(亿佰特)
封装: SMD,12x12mm
分类: WiFi模块
最小包装: 1000圆盘
数据手册: https://atta.szlcsc.com/upload/public/pdf/source/20250228/1570F1779EEBB0E83D0A90BA7B153B26.pdf
 
参数:
  - 核心芯片: RTL8189FTV-VC-CG
  - 支持协议: IEEE802.11b/g/n
  - 接口类型: SDIO
  - 频率: 2.484GHz
  - 天线形式: 邮票孔
  - 发射功率: 16dBm
  - 工作电压: 3V~3.5V
  - 接收电流: 79mA
  - 发送电流: 319mA
  - 接收灵敏度: -90dBm;-92dBm;-86dBm
  - 工作温度: -20℃~+70℃

三点提醒:请求头里写个自己的工具名(示例里的 my-bom-tool/1.0)就够了,不需要伪装成浏览器;要长期稳定地跑,把下一节那三件事补上;这是给个人工具用的思路,要做成对外服务或高频使用,请走官方开放平台的正式接口。

六、怎么让它长期稳定地跑?

「能拿到」和「能一直拿到」是两回事。我做了三件事:

缓存、节流与退避

  1. 尽量少请求:识别结果在本地缓存 90 天。参数、封装、手册几乎不会变(价格和库存会变,所以不拿它们当入库依据);同一颗料第二次识别直接命中缓存,一个请求都不发。
  2. 对上游客气一点:请求排队,间隔不小于 350 毫秒(相当于主动把节奏放慢);一次识别最多请求三次。
  3. 万一被限速,就等一等:遇到 403 / 429 / 503 自动冷却,5 秒 → 30 秒 → 5 分钟阶梯往后退,冷却期间旧的缓存照常可用。

顺便回答一个很多人关心的问题:会不会要求登录或者人机验证? 整条链路没有账号,对方的安全策略最多只能按 IP 限速——最坏的结果是「等一会儿再来」,不会封号。前提是别把它当批量刷数据的工具,正常个人使用完全没问题(「型号 → 编号」我用的是立创EDA 的搜索,和商城是同一套物料资料)。

七、最后的效果

现在我的库存系统里,入库弹窗顶部多了一个「识别」框:输入型号、立创编号或商品链接,都能自动获取并一键填入表单——料号、名称、规格(品牌 / 封装 / 用途)、备注(编号、商品 ID、最小包装、参数表、数据手册)全都自动带出来。要整理一批料的时候还有「批量识别」:把一列型号粘进去,它会排队一条条抓,带进度和结果列表,最后可以一键全部入库。

如果你也在做自己的元器件管理工具,希望这篇能帮你省下卡在密钥上的那几个小时。核心思路就一句话:优先用平台公开的物料资料,往往比走开发者接口更快拿到——但要记得对它友好一点,别高频请求。

⚠️ 免责声明:本文为个人学习笔记,按「现状」提供,不保证准确性、完整性与时效性,也不构成任何专业建议;文中代码与接口调用仅供个人学习与技术研究,禁止用于高频抓取、批量传播、商业用途或任何违法用途。您须自行遵守《中华人民共和国网络安全法》《数据安全法》《个人信息保护法》《著作权法》等法律法规,以及立创商城 / 立创EDA(嘉立创)的用户协议、robots 规则与使用条款;本文提及的平台、名称与商标归各自权利人所有,与本博客无任何隶属或合作关系。因使用本文内容产生的任何直接或间接后果(含设备损坏、数据丢失、账号风险与法律责任),均由使用者自行承担;此外,本站为个人非商业博客,全部内容免费分享、截至目前作者未因此取得任何收益或对价,作者与读者之间不构成任何服务或合同关系,使用风险与后果请自行承担。详见《免责声明》。

🤖 AI 生成说明:本文由 AI 辅助撰写与整理,虽已尽力人工核对、两份脚本也在本地实际运行验证过(Node 18+ / Python 3.14),但仍可能存在错误、遗漏或过时之处;示例数据取自立创商城的公开页面。采纳关键结论前请自行复核,如有疏漏欢迎指正。


本文配套的功能在自己的博客系统里实现(Next.js),入口是私有的;上面两份脚本都能独立运行,不需要我这边的东西。涉及的三方接口均为公开只读接口,请遵守对方的使用条款,不要滥用。

相关文章


评论

评论区由 giscus 提供(基于 GitHub Discussions),内容会保存在 GitHub 上;详见《隐私政策》。