BOM:R2 物料清单与淘宝 / 立创比价(表格 + 抓价脚本与数据)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:42 +08:00
共同撰写人 Claude Opus 5.5
父节点 bfe094a5d7
当前提交 22a255acd7
共修改 19 个文件,包含 39549 行新增和 0 行删除
二进制文件未显示。
+75
查看文件
@@ -0,0 +1,75 @@
# PhoneRail-R2 BOM 与淘宝比价
2026-09-13 生成。数量口径:**一套 = 1 块底板 + 5 片刀片**。
| 文件 | 内容 |
|---|---|
| `PhoneRail-R2_BOM_淘宝比价.xlsx` | 「说明」页 + 「BOM+淘宝比价」页(77 行:型号 / 立创料号 / 位号 / 数量 / 立创阶梯价与链接 / 淘宝搜索链接 / 4 家店的价格、付款人数、标题、链接 / 备注) |
| `_src/` | 全部抓取与生成脚本 + 原始数据,可重跑(见文末) |
## 数据来源
- **器件清单**:EasyEDA 工程 PhoneRail-R2,13 页原理图逐页 `sch_PrimitiveComponent.getAll(undefined, false)` 读出
(底板 A1~A6/A1B/A5B 共 164 件,刀片 B1~B4/B3B 共 82 件),按立创料号合并。
⚠ `getAll(..., true)`(跨页)返回的件数不对,必须逐页 `openDocument` 后读。
- 不采购、未列入:`J1` 金手指(板边铜)、`J5` 电池排线焊盘(非器件)。
- **立创价**:`https://wmsc.lcsc.com/ftps/wm/product/detail?productCode=Cxxxx`,带 `Cookie: currencyCode=CNY`
返回人民币阶梯价(`currencyPrice`);按一套总数取阶梯。这是 LCSC 国际站人民币报价,与 szlcsc 国内站可能略有出入。
szlcsc 国内站对 curl 直接 WAF 拦截。4 种料接口无报价:C22383480(470µF)、C38117(SWPA4030)、C9900163599(ESP32 模组)、C50951(排针)。
- **淘宝**:匿名接口一律跳登录,必须在登录的浏览器里做。用独立 profile 起 Chrome(`--remote-debugging-port=9333`),
用户扫码登录后通过 CDP 驱动。**用户要求:每两次搜索间隔 30 秒**(商品页访问同样适用)。
## 结论
- 立创参考总价 **≈ ¥760 / 套**(含 ESP32 N16R8 ¥36;换料前没有 ESP32 报价时是 ¥724)。
- **淘宝列表价 = 该链接最便宜 SKU 的价格**,阻容多为一包(50/100 只)价,不能直接当单价比。
- **10 颗 IC 的淘宝最低价不到立创 30%**(TPS54622 立创 ¥14.3 / 淘宝 ¥1.1 起;USB2517 ¥32 / ¥2.8 起 等),
多为散新、翻新或引流价。TPS54622、TPS25940、INA226、USB2517、DAC63204、ESP32 模组建议走立创或品牌店。
- **6 项淘宝无原厂同款,表里是替代品,下单前要核对**:
- `PCIE-64P11L`(SOFNG,板上高 11.1 mm、带定位柱)→ 淘宝只有 64P 立式贴片 x4 通用款,**与插槽实测尺寸直接相关**;
- `YTC1P-2510-01` 侧键顶针 → 通用 pogo pin,按 Ø2 mm / 高 3.5 mm / 贴片 选;
- `6.3TPV2200M10X10.5`(Rubycon 2200µF)→ 替代品核对 ESR ≤ 60 mΩ、纹波电流;
- `NX3225SA 24MHz` → 须 9 pF 负载、±10 ppm;
- `T491A226K010AT` 钽电容、`F1206SB2000V063TM` 慢断保险丝 → 同规格他牌。
- ESP32-S3-WROOM-1:**2026-09-13 定为 N16R8**(立创 C2913202,1 片起 ¥36.1)。已核对:八线 PSRAM 占用的 IO35/36/37 在板上都没接,IO45 悬空(3.3 V Flash),不冲突。详见 R2 方案文档 §20.6。原理图与 PCB 上 U20 的料号已换成 C2913202(改属性,版图未动)。
### 淘宝实际成本(2026-09-13 实测,Excel「淘宝采购成本」页)
逐个打开商品页,点选对应规格,读实际价格、每份数量和运费,按 `ceil(需求/每份数量)×规格价 + 每家店运费` 汇总:
| 方案 | 货款 | 运费 | **合计 / 套** |
|---|---:|---:|---:|
| A:全部淘宝(21 家店,主要是欧贝顿 32 种、优信电子 22 种) | ¥517 | ¥41.5 | **≈ ¥558** |
| B:8 颗关键 IC 走立创,其余淘宝 | ¥730 | ¥26.5 + 立创运费 | **≈ ¥756** |
- 8 颗关键 IC(TPS54622 / TPS25940 / INA226 / USB2517 / DAC63204 / PCA9555 / LM61495 / ESP32 模组):淘宝 ¥202,立创 ¥414。
**两个方案差的 ¥200 基本就是这 8 颗的真假风险。**
- 最贵的一项是 **PCIe 插槽 ¥99**(立新电子商城,富士康 2EH5327,¥19.8×5)。创典电子的 64P 立式 SMT X4 列表价只要 ¥2.1,
换它能省约 ¥88,但规格价没进页面核实,而且必须先核对插槽尺寸。
- 选店时已经排除了低于立创价 30% 的 IC 报价。选中的 TPS54622 ¥4.4、PCA9555 ¥3.75 仍只有立创价的约 30%,照样要警惕。
- 这几项是估的:SPM6530 电感按 1 只/份(¥4.14×6,其他店有 ¥0.6~0.8 的);10nF 电容按 100 只/份;
AMS1117 按 1 只/份;佳信源电子页面没写运费,按 ¥4 算。100kΩ 改用欧贝顿 100 只/份 ¥2 那条链接(页面实测)。
- 阻容整包买,富余很多。立创参考价 ¥760 只是「单价×需求数」,没算整包和运费,**不能和上面的合计直接比**。
- 读商品页时淘宝弹过 2 次验证,1 次跳登录页、1 次滑块,用户拖过滑块后继续。
## 淘宝抓取的坑(实测)
- **搜索页**:`s.taobao.com/search?q=` 的结果是前端渲染,等 DOM 里出现指向 `item.taobao.com/item` / `detail.tmall.com/item`
且含 `¥` 的 `<a>` 即可解析;卡片 innerText 依次是 标题 / 标签 / `¥` / 整数 / `.小数` / 付款人数 / 地区 / 店名。
- **商品页**:数据在 SSR 注入的 `window.__ICE_APP_CONTEXT__.loaderData.home.data.res`(`skuBase.props` 有全部 SKU 名,
SKU 名常含包装数如「0402 100K 1% 100个」)。**各 SKU 价格不在数据里**(`hideOtherPrice`),
要点击 SKU 的 `<span>` 后读页面 `¥` 后面的数字;`快递: …` 行给运费。
- **风控**:短时间连续开商品页会被重定向到 `login.taobao.com …_____tmd_____/page/login_jump`(cookie 仍在)。
脚本遇到会等待并换备选店。之前触发是因为试探时没按间隔连开了几页。
- 标题匹配要注意:去掉 `.` 做归一化会把 `2.2UF` 撞成 `22UF`、`2.5mR` 撞成 `5mR`;同型号混卖不同封装(TL431 SOP-8/SOT-23);
E96 阻值(43.2K、13.7K 等)不要接受「全系列」链接,那种通常只有 E24。
## 重跑
在 `BOM/_src/` 下(Python 3.14,需 `openpyxl`、`websockets`):
1. 需要刷新器件清单时:EasyEDA 桥接在线,重跑逐页读取(原脚本 `dump2.py` 的逻辑已写进本文「数据来源」),产出 `sch_pages.json` → 合并成 `bom_agg.json`。
2. 起 Chrome:`chrome.exe --remote-debugging-port=9333 --user-data-dir=<独立目录> https://login.taobao.com/`,扫码登录。
3. `python scrape.py`(按 `queries.json` 搜,断点续跑,写 `tb.json`)。
4. `python build.py` → 生成 xlsx;匹配规则在 `CORE` / `SPECIAL` / `PKG` 里调。
5. 成本:`python cost_pick.py`(选店 → `cost_plan.json`)→ `python visit.py`(点选 SKU 读价 → `cost_detail.json`)→ 人工核对规格,错的在 `fix.py` 里写明 SKU 重点 → `python total.py`(包装数表 `PACK` 在里面)→ `python sheet.py` 写入 Excel。
文件差异内容过多而无法显示 加载差异
+251
查看文件
@@ -0,0 +1,251 @@
import json, re, math
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
import os
T = os.path.dirname(os.path.abspath(__file__)) + '/'
L = json.load(open(T + 'bom_agg.json', encoding='utf-8'))
TB = json.load(open(T + 'tb.json', encoding='utf-8'))
LC = json.load(open(T + 'lcsc_cny.json', encoding='utf-8'))
Q = {x['sid']: x for x in json.load(open(T + 'queries.json', encoding='utf-8'))}
SETS = 1 # 1 底板 + 5 刀片 = 1 套
# 非阻容料:标题里必须出现的型号核心(去掉空格/横杠后大写匹配)
CORE = {
'C382328': 'AO4407', 'C163651': 'SMBJ13A', 'C2887585': 'TL431', 'C49851': 'INA226',
'C5210749': 'LMR51430', 'C38117': 'SWPA4030S100', 'C76857': 'SPM6530T4R7', 'C2913202': 'ESP32S3WROOM1N16R8',
'C50951': '', 'C130026': 'TCA9548', 'C5215473': 'DAC63204', 'C478081': 'USB2517', 'C2762192': '24M',
'C165948': 'TYPEC31M12', 'C6186': 'AMS1117', 'C116575': 'T491A226', 'C19188869': 'PCIE',
'C20917': 'AO3400', 'C2891776': 'BZT52C8V2', 'C15127': 'AO3401', 'C8678': 'SS34', 'C2847671': '2510',
'C13564': 'NCP18XH103', 'C310990': 'F1206HC12', 'C151920': 'SMBJ22A', 'C42459160': 'CH224',
'C3188410': 'LM61495', 'C780205': 'SRP1265A4R7', 'C84248': 'TPS54622', 'C2653873': 'TPS25940',
'C8545': '2N7002', 'C310998': 'F1206SB2', 'C2688893': 'PCA9555', 'C225104': 'A1251', 'C353034': 'TS5A3157',
'C3828985': '2200', 'C5221288': 'YTC1P', 'C424561': 'BM2450DS', 'C2687116': 'USBLC62', 'C131941': 'TPS22918',
'C841779': '5MR', 'C22383480': '470UF',
}
CORE['C5210749'] = 'LMR51430YDDC'
# 封装必须对(标题里常把同型号不同封装混卖)
PKG = {'C2887585': 'SOT23', 'C20917': 'SOT23', 'C15127': 'SOT23', 'C8545': 'SOT23',
'C382328': 'SOP8|SOIC8', 'C6186': 'SOT223', 'C8678': 'SMA|DO214AC', 'C2891776': 'SOD123'}
import re as _re
SPECIAL = {
'C19188869': lambda U, t: '64P' in U and _re.search(r'X4|4X', U) and _re.search(r'贴片|SMT', U) and '164' not in U,
'C5221288': lambda U, t: _re.search(r'贴片|SMT', U) and _re.search(r'POGO|顶针|弹簧针', U) and not _re.search(r'夹|间距|烧录', U),
'C116575': lambda U, t: _re.search(r'22UF|226', U) and '10V' in U and _re.search(r'A型|3216', U),
'C310998': lambda U, t: _re.search(r'(?<![\d.])2A', U) and '慢' in U and '1206' in U,
'C2762192': lambda U, t: '24M' in U and '3225' in U and _re.search(r'(?<!\d)9PF|负载9/', U) and '有源' not in U,
'C3828985': lambda U, t: '2200UF' in t and '63V' in t and _re.search(r'10X105|6\.3TPV', t + U),
'C225104': lambda U, t: 'A1251WV' in t,
}
E24 = [1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0, 3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1]
GENERIC = {'C5221288': '通用顶针,须按 Ø2mm / 高 3.5mm / 贴片 在规格里核对',
'C19188869': '非原厂同款(原设计 SOFNG PCIE-64P11L,板上高 11.1mm 带定位柱),下单前核对尺寸/定位柱',
'C2913202': '2026-09-13 定为 N16R8;多规格链接要选 N16R8 这一项',
'C3828985': '原设计为 Rubycon 6.3TPV 固态/高分子,替代品需核对 ESR≤60mΩ、纹波电流',
'C2762192': '原设计 NDK ±10ppm 9pF;替代品须 9pF 负载电容'}
def norm(s):
return re.sub(r'[\s\-_/·.,,()()]', '', s.upper())
def cap_tokens(desc):
v = desc.split()[0] # e.g. 100nF
m = re.match(r'([\d.]+)(pF|nF|uF)', v)
num, u = float(m.group(1)), m.group(2)
pf = num * {'pF': 1, 'nF': 1e3, 'uF': 1e6}[u]
toks = {v.upper()}
# 三位码
e = int(math.floor(math.log10(pf))) - 1
code = f"{int(round(pf / 10**e)):02d}{e}" if e >= 0 else None
if code: toks.add(code)
if pf >= 1e3: toks.add(f"{pf/1e3:g}NF")
if pf >= 1e4: toks.add(f"{pf/1e6:g}UF")
return toks
def relevant(sid, r, it):
t = norm(it['title'] + ' ' + it['tags'])
q = Q[sid]['q']
U = (it['title'] + ' ' + it['tags']).upper()
if sid in SPECIAL:
return SPECIAL[sid](U, t)
if sid in CORE:
c = CORE[sid]
if sid == 'C50951':
return '2.54' in it['title'] and '排针' in it['title']
if sid == 'C841779':
return bool(re.search(r'(?<![\d.])(5MR|R005|0\.005R|5毫欧)', it['title'].upper()))
if sid in PKG and not re.search(PKG[sid], t):
return False
return c in t
fp = r['fp'] or ''
pk = re.search(r'(0402|0603|0805|1206|2512)', fp).group(1)
if pk not in t:
return False
if '贴片电阻' in q:
raw = (it['title'] + ' ' + it['tags']).upper().replace('Ω', '').replace('欧', '')
if '热敏' in raw or '排阻' in raw or '采样' in raw and pk != '2512':
return False
v = q.split()[1].upper() # 100K / 43.2K / 2R
m = re.match(r'([\d.]+)([KMR])', v); num, u = m.group(1), m.group(2)
forms = {v, num + u}
if '.' in num:
a, b = num.split('.'); forms.add(f'{a}{u}{b}')
mult = {'R': 1, 'K': 1e3, 'M': 1e6}[u]
ohm = float(num) * mult
e = int(math.floor(math.log10(ohm))) - 2
if e >= 0: forms.add(f"{int(round(ohm / 10**e)):03d}{e}")
if u == 'R': forms |= {num + 'R0', num + 'R00'}
if any(re.search(r'(?<![\d.])' + re.escape(f) + r'(?![\d])', raw) for f in forms):
return True
mant = ohm / 10 ** math.floor(math.log10(ohm))
if any(abs(mant - e) < 1e-6 for e in E24) and re.search(r'全系列|系列|全阻值|多阻值|阻值齐全', it['title']):
it['series'] = True
return True
return False
if '贴片电容' in q:
raw = (it['title'] + ' ' + it['tags']).upper().replace(' ', '')
volt = re.search(r'(\d+(\.\d)?V)', r['desc']).group(1).upper()
if not re.search(r'(?<![\d.])' + re.escape(volt), raw):
return False
return any(re.search(r'(?<![\d.])' + re.escape(k) + r'(?![\d])', raw) for k in cap_tokens(r['desc']))
return True
def sales_n(s):
m = re.search(r'([\d.]+)(万)?\+?人', s)
if not m: return 0
return float(m.group(1)) * (10000 if m.group(2) else 1)
def pack_of(title):
m = re.search(r'[((]?(\d+)\s*(只|个|颗|片|pcs|PCS)[))]?', title)
return int(m.group(1)) if m and int(m.group(1)) > 1 else None
def lcsc_price(sid, qty):
v = LC.get(sid) or {}
ps = v.get('prices') or []
best = None
for p in ps:
if qty >= p['ladder'] or best is None:
best = p
return (best['currencyPrice'], best['ladder']) if best else (None, None)
rows = []
for r in L:
sid = r['sid'] or ''
if not sid.startswith('C'):
continue
items = [i for i in TB.get(sid, {}).get('items', []) if i['price']]
rel = [i for i in items if relevant(sid, r, i)]
for i in rel:
i['p'] = float(i['price']); i['s'] = sales_n(i['sales']); i['pack'] = pack_of(i['title'])
# 选店:先按销量挑,同店只取一条,再补最低价
picks, shops = [], set()
for i in sorted(rel, key=lambda i: (i.get('series', False), -i['s'])):
if i['shop'] in shops: continue
picks.append(i); shops.add(i['shop'])
if len(picks) == 3: break
cheap = [i for i in sorted(rel, key=lambda i: i['p']) if i['shop'] not in shops and i['s'] > 0]
if cheap: picks.append(cheap[0])
picks.sort(key=lambda i: i['p'])
lp, ll = lcsc_price(sid, r['total'])
rows.append(dict(r=r, picks=picks, nrel=len(rel), nall=len(items), lp=lp, ll=ll))
# ---------- Excel ----------
wb = Workbook()
ws = wb.active; ws.title = 'BOM+淘宝比价'
hdr = ['#', '分类', '型号', '厂商', '立创料号', '规格', '封装', '底板位号', '刀片位号(每片)', '底板数', '刀片每片数', '总数(1底板+5刀片)',
'立创单价¥', '立创价阶梯', '立创小计¥', '立创链接', '淘宝搜索词', '淘宝搜索链接', '备注']
for k in range(1, 5):
hdr += [f'店{k} 店铺', f'店{k} 列表价¥', f'店{k} 付款人数', f'店{k} 标题', f'店{k} 链接']
ws.append(hdr)
def cat(r):
m, fp = r['mid'] or '', r['fp'] or ''
if re.match(r'0402WGF|RHC|FRC|HoGT|NCP18', m): return '电阻'
if re.match(r'CL\d|GRM|0402CG|T491|470uF|6.3TPV', m): return '电容'
if re.match(r'SPM|SWPA|SRP', m): return '电感'
if re.match(r'AO|2N7|SS34|SMBJ|BZT|USBLC', m): return '分立/保护'
if re.match(r'F1206', m): return '保险丝'
if re.search(r'TYPE-C|PCIE|A1251|2510|BM24|YTC|排针', m): return '连接器'
if 'MHZ' in m: return '晶振'
return 'IC/模组'
ORDER = ['IC/模组', '电感', '分立/保护', '保险丝', '晶振', '连接器', '电容', '电阻']
rows.sort(key=lambda x: (ORDER.index(cat(x['r'])), x['r']['mid']))
total_lcsc = 0
def note(x):
r = x['r']; sid = r['sid']; ns = []
if sid in GENERIC: ns.append(GENERIC[sid])
if not x['picks']: ns.append('淘宝没搜到可对应的链接')
c = cat(r)
if x['lp'] and x['picks'] and c in ('IC/模组',) and min(i['p'] for i in x['picks']) < 0.3 * x['lp']:
ns.append('淘宝最低价不到立创的 30%,警惕散新/翻新/引流价')
if not x['lp']: ns.append('立创接口无报价(料号下架或非立创自营)')
return ';'.join(ns)
for n, x in enumerate(rows, 1):
r = x['r']; sid = r['sid']; pid = (LC.get(sid) or {}).get('pid')
sub = round(x['lp'] * r['total'], 2) if x['lp'] else None
total_lcsc += sub or 0
q = Q[sid]['q']
row = [n, cat(r), r['mid'], r['m'], sid, r['desc'], r['fp'], ','.join(r['base']), ','.join(r['blade']),
len(r['base']), len(r['blade']), r['total'], x['lp'], f"≥{x['ll']}" if x['ll'] else '', sub,
f"https://item.szlcsc.com/{pid}.html" if pid else '', q,
'https://s.taobao.com/search?q=' + __import__('urllib.parse').parse.quote(q), note(x)]
for i in x['picks']:
url = ('https://detail.tmall.com/item.htm?id=' if i['tmall'] else 'https://item.taobao.com/item.htm?id=') + i['id']
row += [i['shop'], i['p'], i['sales'], ('【系列链接,需在规格里选阻值】' if i.get('series') else '') + i['title'], url]
ws.append(row)
thin = Side(style='thin', color='CCCCCC')
for c in ws[1]:
c.font = Font(bold=True, color='FFFFFF'); c.fill = PatternFill('solid', fgColor='305496')
c.alignment = Alignment(wrap_text=True, vertical='center')
widths = {19: 30, 1: 4, 2: 9, 3: 22, 4: 14, 5: 11, 6: 26, 7: 18, 8: 18, 9: 18, 10: 6, 11: 6, 12: 8, 13: 9, 14: 8, 15: 9, 16: 12, 17: 22, 18: 12}
for col in range(1, ws.max_column + 1):
w = widths.get(col)
if w is None:
w = {1: 14, 2: 9, 3: 9, 4: 36, 5: 10}[(col - 20) % 5 + 1]
ws.column_dimensions[get_column_letter(col)].width = w
for row in ws.iter_rows(min_row=2):
for c in row:
c.alignment = Alignment(vertical='top', wrap_text=False)
if isinstance(c.value, str) and c.value.startswith('https://'):
c.hyperlink = c.value; c.value = '打开'; c.font = Font(color='0563C1', underline='single')
ws.freeze_panes = 'D2'
ws.auto_filter.ref = ws.dimensions
ws.append([])
ws.append(['', '', '立创参考总价(按总数取对应阶梯价)', '', '', '', '', '', '', '', '', '', '', '', round(total_lcsc, 2)])
# 说明页
ns = wb.create_sheet('说明')
notes = [
'数据来源',
f'· 器件清单:从 EasyEDA 工程 PhoneRail-R2 的 13 页原理图逐页读出(底板 8 页 + 刀片 5 页),按立创料号合并。',
'· 数量按「1 块底板 + 5 片刀片」一套计算;不含 J1 金手指(板边铜箔,不采购)和 J5 电池排线焊盘(非器件)。',
'· 立创价:LCSC 官方接口的人民币报价,按本套总数取阶梯价;立创商城国内站与此可能略有出入。',
'· 淘宝:登录后按搜索词逐个搜索(每次间隔 30 秒),取前 24 条结果;按标题匹配型号/阻值容值/封装过滤掉不相关的,',
' 再挑销量最高的 3 家不同店铺 + 1 家最低价店铺,按价格排序。',
'',
'⚠ 看价格前必读',
'· 淘宝「列表价」是该链接所有 SKU 里最低的那一个,不一定是你要的型号/阻值/数量档;多规格链接(如阻值全系列)尤其如此。',
'· 阻容类淘宝通常按包(50/100 只)卖,列表价是一包的价格,不是单颗价;立创价是单颗价。两者不能直接相除比较。',
'· IC 类如果淘宝价明显低于立创价(比如低到 1/3 以下),多为拆机、翻新、散新或引流价,关键器件(TPS54622、TPS25940、',
' INA226、USB2517、DAC63204、ESP32 模组)建议走立创或品牌/授权店。',
'· 淘宝结果随时间、账号和地区变化;链接在抓取时有效。',
]
for s in notes:
ns.append([s])
ns.column_dimensions['A'].width = 120
ns['A1'].font = Font(bold=True); ns['A8'].font = Font(bold=True, color='C00000')
wb.move_sheet('说明', offset=-1)
out = 'D:/DCDC/BOM/PhoneRail-R2_BOM_淘宝比价.xlsx'
import os; os.makedirs('D:/DCDC/BOM', exist_ok=True)
wb.save(out)
json.dump([{'mid': x['r']['mid'], 'sid': x['r']['sid'], 'cat': cat(x['r']), 'total': x['r']['total'], 'lp': x['lp'], 'nrel': x['nrel'], 'nall': x['nall'],
'picks': [{k: i[k] for k in ('shop', 'p', 'sales', 'title', 'id', 'tmall')} for i in x['picks']]} for x in rows],
open(T + 'rows.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(out, len(rows), 'lcsc total', round(total_lcsc, 2))
for x in rows:
if len(x['picks']) < 2:
print('FEW', x['r']['sid'], x['r']['mid'], x['nrel'], '/', x['nall'])
+14
查看文件
@@ -0,0 +1,14 @@
import asyncio,json,urllib.request,websockets,itertools
_id=itertools.count(1)
async def open_ws():
tabs=json.load(urllib.request.urlopen('http://127.0.0.1:9333/json'))
page=[t for t in tabs if t['type']=='page'][0]
return await websockets.connect(page['webSocketDebuggerUrl'],max_size=50_000_000)
async def send(ws,method,params=None):
i=next(_id); await ws.send(json.dumps({'id':i,'method':method,'params':params or {}}))
while True:
m=json.loads(await ws.recv())
if m.get('id')==i: return m
async def ev(ws,expr):
r=await send(ws,'Runtime.evaluate',{'expression':expr,'awaitPromise':True,'returnByValue':True})
return r.get('result',{}).get('result',{}).get('value')
文件差异内容过多而无法显示 加载差异
+39
查看文件
@@ -0,0 +1,39 @@
import json, re, math, collections
exec(open('build.py', encoding='utf-8').read().split('# ---------- Excel ----------')[0])
# 店铺覆盖度:一家店能供的料越多越优先(省运费)
cover = collections.Counter()
REL = {}
for r in L:
sid = r['sid'] or ''
if not sid.startswith('C'): continue
items = [i for i in TB.get(sid, {}).get('items', []) if i['price']]
rel = [i for i in items if relevant(sid, r, i)]
for i in rel:
i['p'] = float(i['price']); i['s'] = sales_n(i['sales'])
REL[sid] = (r, rel)
for s in {i['shop'] for i in rel}:
cover[s] += 1
def cat2(r):
m = r['mid'] or ''
if re.match(r'0402WGF|RHC|FRC|HoGT|NCP18|CL\d|GRM|0402CG|T491|470uF|6.3TPV', m): return 'passive'
return 'other'
plan = []
for sid, (r, rel) in REL.items():
lp = lcsc_price(sid, r['total'])[0]
cands = []
for i in rel:
bad = cat2(r) == 'other' and lp and i['p'] < 0.3 * lp and lp > 1.0
score = 3 * math.log1p(cover[i['shop']]) + math.log1p(i['s']) - (4 if i.get('series') else 0) - (8 if bad else 0) - (3 if i['s'] == 0 else 0)
cands.append((score, i))
cands.sort(key=lambda x: -x[0])
plan.append({'sid': sid, 'mid': r['mid'], 'desc': r['desc'], 'need': r['total'], 'lcsc': lp,
'cands': [dict(id=i['id'], tmall=i['tmall'], shop=i['shop'], p=i['p'], sales=i['sales'], title=i['title'], score=round(s, 2)) for s, i in cands[:3]]})
json.dump(plan, open('cost_plan.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
shops = collections.Counter(p['cands'][0]['shop'] for p in plan if p['cands'])
print(len(plan), 'shops', len(shops)); print(shops.most_common(15))
for p in plan:
c = p['cands'][0] if p['cands'] else None
print(p['mid'][:22].ljust(22), p['need'], p['lcsc'], '|', c and (c['p'], c['shop'][:10], c['title'][:40]))
文件差异内容过多而无法显示 加载差异
+23
查看文件
@@ -0,0 +1,23 @@
import asyncio,json,re
import cdp
exec(open('visit.py',encoding='utf-8').read().split('async def main')[0])
FIX={'C43473':['0402 22.1K 1% 100个'],'C6186':['AMS1117-3.3'],'C2847671':['KF2510 直针 4P','【50只】'],
'C50951':['2.54mm_1*5P_直针_铜(10只)'],'C310998':['63V/2A(10只)'],'C5221288':['3.5MM 贴片式【20个】']}
D=json.load(open('cost_detail.json',encoding='utf-8'))
async def main():
ws=await cdp.open_ws()
for n,(k,skus) in enumerate(FIX.items()):
if n: await asyncio.sleep(30)
await cdp.send(ws,'Page.navigate',{'url':D[k]['url']})
for i in range(20):
await asyncio.sleep(1)
if await cdp.ev(ws,SKUJS): break
ok=[]
for s in skus:
ok.append(await cdp.ev(ws,'('+CLICK+')('+json.dumps(s)+')')); await asyncio.sleep(1.5)
txt=await cdp.ev(ws,'document.body.innerText') or ''
pr=parse_price(txt,D[k]['title'] or '')
D[k].update(chosen=skus,price=pr,fixed=True,click_ok=ok)
print(k,skus,ok,pr,flush=True)
json.dump(D,open('cost_detail.json','w',encoding='utf-8'),ensure_ascii=False,indent=1)
asyncio.run(main())
文件差异内容过多而无法显示 加载差异
+393
查看文件
@@ -0,0 +1,393 @@
[
{
"sid": "C382328",
"q": "AO4407",
"mid": "AO4407"
},
{
"sid": "C1525",
"q": "0402 100nF 16V X7R 贴片电容",
"mid": "CL05B104KO5NNNC"
},
{
"sid": "C163651",
"q": "SMBJ13A",
"mid": "SMBJ13A"
},
{
"sid": "C22383480",
"q": "470uF 25V 8x12 低阻 电解电容",
"mid": "470uF 25V"
},
{
"sid": "C440198",
"q": "0805 10uF 50V X5R 贴片电容",
"mid": "GRM21BR61H106KE43L"
},
{
"sid": "C25894",
"q": "0402 43.2K 1% 贴片电阻",
"mid": "0402WGF4322TCE"
},
{
"sid": "C25744",
"q": "0402 10K 1% 贴片电阻",
"mid": "0402WGF1002TCE"
},
{
"sid": "C25900",
"q": "0402 4.7K 1% 贴片电阻",
"mid": "0402WGF4701TCE"
},
{
"sid": "C2887585",
"q": "TL431",
"mid": "TL431"
},
{
"sid": "C49851",
"q": "INA226AIDGSR",
"mid": "INA226AIDGSR"
},
{
"sid": "C841779",
"q": "2512 5mR 1% 3W 合金采样电阻",
"mid": "HoGT2512-3W-5mR-1%"
},
{
"sid": "C2570006",
"q": "2512 2R 1% 贴片电阻 2W",
"mid": "RHC2512FT2R00"
},
{
"sid": "C25741",
"q": "0402 100K 1% 贴片电阻",
"mid": "0402WGF1003TCE"
},
{
"sid": "C5210749",
"q": "LMR51430YDDCR",
"mid": "LMR51430YDDCR"
},
{
"sid": "C377773",
"q": "0805 2.2uF 50V X5R 贴片电容",
"mid": "CL21A225KBQNNNE"
},
{
"sid": "C38117",
"q": "SWPA4030S100MT",
"mid": "SWPA4030S100MT"
},
{
"sid": "C12891",
"q": "1206 22uF 25V X5R 贴片电容",
"mid": "CL31A226KAHNNNE"
},
{
"sid": "C43473",
"q": "0402 22.1K 1% 贴片电阻",
"mid": "0402WGF2212TCE"
},
{
"sid": "C76857",
"q": "SPM6530T-4R7M",
"mid": "SPM6530T-4R7M"
},
{
"sid": "C25854",
"q": "0402 13.7K 1% 贴片电阻",
"mid": "0402WGF1372TCE"
},
{
"sid": "C2913202",
"q": "ESP32-S3-WROOM-1-N16R8",
"mid": "ESP32-S3-WROOM-1-N16R8"
},
{
"sid": "C50951",
"q": "2.54mm 单排针 5P 直针",
"mid": "2.54mm 1*5P 直排针"
},
{
"sid": "C130026",
"q": "TCA9548APWR",
"mid": "TCA9548APWR"
},
{
"sid": "C5215473",
"q": "DAC63204RTER",
"mid": "DAC63204RTER"
},
{
"sid": "C478081",
"q": "USB2517I-JZX",
"mid": "USB2517I-JZX"
},
{
"sid": "C2762192",
"q": "3225 24M 无源晶振 10ppm 9pF",
"mid": "NX3225SA-24MHZ-EXS00A-CS08583",
"q_old": "NX3225SA-24MHZ-EXS00A-CS08583"
},
{
"sid": "C1547",
"q": "0402 12pF 50V C0G 贴片电容",
"mid": "0402CG120J500NT"
},
{
"sid": "C25752",
"q": "0402 12K 1% 贴片电阻",
"mid": "0402WGF1202TCE"
},
{
"sid": "C52923",
"q": "0402 1uF 25V X5R 贴片电容",
"mid": "CL05A105KA5NQNC"
},
{
"sid": "C165948",
"q": "TYPE-C-31-M-12",
"mid": "TYPE-C-31-M-12"
},
{
"sid": "C25905",
"q": "0402 5.1K 1% 贴片电阻",
"mid": "0402WGF5101TCE"
},
{
"sid": "C25765",
"q": "0402 20K 1% 贴片电阻",
"mid": "0402WGF2002TCE"
},
{
"sid": "C6186",
"q": "AMS1117-3.3",
"mid": "AMS1117-3.3"
},
{
"sid": "C116575",
"q": "钽电容 A型 3216 22uF 10V",
"mid": "T491A226K010AT",
"q_old": "T491A226K010AT"
},
{
"sid": "C26083",
"q": "0402 1M 1% 贴片电阻",
"mid": "0402WGF1004TCE"
},
{
"sid": "C19188869",
"q": "PCIE X4 插槽 贴片 64P 立式",
"mid": "PCIE-64P11L",
"q_old": "PCIE-64P11L"
},
{
"sid": "C11702",
"q": "0402 1K 1% 贴片电阻",
"mid": "0402WGF1001TCE"
},
{
"sid": "C20917",
"q": "AO3400A",
"mid": "AO3400A"
},
{
"sid": "C2891776",
"q": "BZT52C8V2",
"mid": "BZT52C8V2"
},
{
"sid": "C15127",
"q": "AO3401A",
"mid": "AO3401A"
},
{
"sid": "C8678",
"q": "SS34",
"mid": "SS34"
},
{
"sid": "C25756",
"q": "0402 15K 1% 贴片电阻",
"mid": "0402WGF1502TCE"
},
{
"sid": "C2847671",
"q": "2510-4A 2.54mm 4P 直针座",
"mid": "2510-4A"
},
{
"sid": "C13564",
"q": "NCP18XH103F03RB",
"mid": "NCP18XH103F03RB"
},
{
"sid": "C310990",
"q": "F1206HC12A0TM",
"mid": "F1206HC12A0TM"
},
{
"sid": "C151920",
"q": "SMBJ22A",
"mid": "SMBJ22A"
},
{
"sid": "C307331",
"q": "0402 100nF 50V X7R 贴片电容",
"mid": "CL05B104KB54PNC"
},
{
"sid": "C28323",
"q": "0805 1uF 50V X7R 贴片电容",
"mid": "CL21B105KBFNNNE"
},
{
"sid": "C42459160",
"q": "CH224A",
"mid": "CH224A"
},
{
"sid": "C25750",
"q": "0402 120K 1% 贴片电阻",
"mid": "0402WGF1203TCE"
},
{
"sid": "C3188410",
"q": "LM61495QRPHRQ1",
"mid": "LM61495QRPHRQ1"
},
{
"sid": "C780205",
"q": "SRP1265A-4R7M",
"mid": "SRP1265A-4R7M"
},
{
"sid": "C25745",
"q": "0402 110K 1% 贴片电阻",
"mid": "0402WGF1103TCE"
},
{
"sid": "C84248",
"q": "TPS54622RHLR",
"mid": "TPS54622RHLR"
},
{
"sid": "C2653873",
"q": "TPS25940ARVCR",
"mid": "TPS25940ARVCR"
},
{
"sid": "C11537",
"q": "0402 68.1K 1% 贴片电阻",
"mid": "0402WGF6812TCE"
},
{
"sid": "C15195",
"q": "0402 10nF 50V X7R 贴片电容",
"mid": "CL05B103KB5NNNC"
},
{
"sid": "C13594",
"q": "0402 33nF 16V X7R 贴片电容",
"mid": "CL05B333KO5NNNC"
},
{
"sid": "C1546",
"q": "0402 100pF 50V C0G 贴片电容",
"mid": "0402CG101J500NT"
},
{
"sid": "C29402",
"q": "0402 30.1K 1% 贴片电阻",
"mid": "0402WGF3012TCE"
},
{
"sid": "C25792",
"q": "0402 47K 1% 贴片电阻",
"mid": "0402WGF4702TCE"
},
{
"sid": "C8545",
"q": "2N7002",
"mid": "2N7002"
},
{
"sid": "C2934046",
"q": "2512 10R 1% 贴片电阻 1W",
"mid": "FRC2512F10R0TS"
},
{
"sid": "C407675",
"q": "0402 5.49K 1% 贴片电阻",
"mid": "0402WGF5491TCE"
},
{
"sid": "C25755",
"q": "0402 150K 1% 贴片电阻",
"mid": "0402WGF1503TCE"
},
{
"sid": "C310998",
"q": "1206 贴片保险丝 2A 慢断",
"mid": "F1206SB2000V063TM",
"q_old": "F1206SB2000V063TM"
},
{
"sid": "C2688893",
"q": "PCA9555PW,112",
"mid": "PCA9555PW,112"
},
{
"sid": "C36871",
"q": "0402 68K 1% 贴片电阻",
"mid": "0402WGF6802TCE"
},
{
"sid": "C225104",
"q": "A1251WV-S-2P",
"mid": "A1251WV-S-2P"
},
{
"sid": "C353034",
"q": "TS5A3157DBVR",
"mid": "TS5A3157DBVR"
},
{
"sid": "C3828985",
"q": "2200uF 6.3V 10x10.5 贴片电解电容",
"mid": "6.3TPV2200M10X10.5",
"q_old": "6.3TPV2200M10X10.5"
},
{
"sid": "C15008",
"q": "1206 100uF 6.3V X5R 贴片电容",
"mid": "CL31A107MQHNNNE"
},
{
"sid": "C5221288",
"q": "贴片弹簧顶针 直径2mm 高3.5mm pogo pin",
"mid": "YTC1P-2510-01",
"q_old": "YTC1P-2510-01"
},
{
"sid": "C424561",
"q": "BM24-50DS/2-0.35V(51)",
"mid": "BM24-50DS/2-0.35V(51)"
},
{
"sid": "C2687116",
"q": "USBLC6-2SC6",
"mid": "USBLC6-2SC6"
},
{
"sid": "C131941",
"q": "TPS22918DBVR",
"mid": "TPS22918DBVR"
},
{
"sid": "C25796",
"q": "0402 56K 1% 贴片电阻",
"mid": "0402WGF5602TCE"
}
]
+3185
查看文件
文件差异内容过多而无法显示 加载差异
文件差异内容过多而无法显示 加载差异
+29
查看文件
@@ -0,0 +1,29 @@
import asyncio,json,urllib.parse,random,os,time
import cdp
EX=r'''JSON.stringify([...document.querySelectorAll("a")].filter(a=>/item\.taobao\.com\/item|detail\.tmall\.com\/item/.test(a.href)&&/¥/.test(a.innerText)).slice(0,24).map(a=>{const id=(a.href.match(/[?&]id=(\d+)/)||[])[1];const t=a.innerText.split("\n").map(s=>s.trim()).filter(Boolean);const i=t.indexOf("¥");let p="";if(i>=0){p=t[i+1]||"";if(t[i+2]&&/^\.\d+$/.test(t[i+2]))p+=t[i+2];}const sales=t.find(s=>/人付款|人收货|已售/.test(s))||"";return {id,tmall:/tmall/.test(a.href),title:t[0],price:p,sales,shop:t[t.length-1],tags:t.slice(1,i).join("/")};}))'''
async def main():
Q=json.load(open('queries.json',encoding='utf-8'))
out=json.load(open('tb.json',encoding='utf-8')) if os.path.exists('tb.json') else {}
ws=await cdp.open_ws()
for n,x in enumerate(Q):
if x['sid'] in out and out[x['sid']]['items']: continue
await cdp.send(ws,'Page.navigate',{'url':'https://s.taobao.com/search?q='+urllib.parse.quote(x['q'])})
items=[]
for k in range(40):
await asyncio.sleep(1)
v=await cdp.ev(ws,EX)
items=json.loads(v) if v else []
if len(items)>=8 or (items and k>10): break
if k==20 and not items:
txt=await cdp.ev(ws,'document.body.innerText.slice(0,300)+location.href')
print('NO ITEMS/CAPTCHA?',x['q'],(txt or '')[:200].replace('\n',' '),flush=True)
for w in range(100):
await asyncio.sleep(3); v=await cdp.ev(ws,EX); items=json.loads(v) if v else []
if items: break
if w==30: await cdp.send(ws,'Page.navigate',{'url':'https://s.taobao.com/search?q='+urllib.parse.quote(x['q'])})
break
out[x['sid']]={'q':x['q'],'items':items,'ts':time.time()}
json.dump(out,open('tb.json','w',encoding='utf-8'),ensure_ascii=False,indent=1)
print(n,x['sid'],x['q'],len(items),items[0]['price'] if items else '',flush=True)
await asyncio.sleep(30)
asyncio.run(main())
+25
查看文件
@@ -0,0 +1,25 @@
import json,openpyxl
from openpyxl.styles import Font,PatternFill,Alignment
T=json.load(open('total.json',encoding='utf-8'))
f='D:/DCDC/BOM/PhoneRail-R2_BOM_淘宝比价.xlsx'; wb=openpyxl.load_workbook(f)
if '淘宝采购成本' in wb.sheetnames: del wb['淘宝采购成本']
ws=wb.create_sheet('淘宝采购成本',1)
ws.append(['一套(1 底板 + 5 刀片)淘宝采购成本 —— 2026-09-13 逐页点选规格实测'])
ws.append(['方案 A:全部淘宝','货款 ¥%.2f'%T['goods'],'运费 ¥%.2f(%d 家店)'%(T['shipsum'],len(T['ship'])),'合计 ¥%.2f'%(T['goods']+T['shipsum'])])
ws.append(['方案 B:8 颗关键 IC 走立创、其余淘宝','货款 ¥%.2f'%T['alt'],'淘宝运费 ¥%.2f(立创运费另计)'%T['sh_alt'],'合计 ¥%.2f'%(T['alt']+T['sh_alt'])])
ws.append(['关键 IC = TPS54622 / TPS25940 / INA226 / USB2517 / DAC63204 / PCA9555 / LM61495 / ESP32 模组;这 8 项淘宝 ¥%.2f,立创 ¥%.2f'%(sum(r['cost'] for r in T['rows'] if r['key']),sum(r['lcsc_cost'] for r in T['rows'] if r['key']))])
ws.append([])
hdr=['型号','立创料号','需求数','店铺','选中规格','规格价¥','每份数量','买几份','小计¥','立创单价×需求¥','关键IC','商品链接','备注']
ws.append(hdr)
for c in ws[ws.max_row]: c.font=Font(bold=True,color='FFFFFF'); c.fill=PatternFill('solid',fgColor='305496')
hr=ws.max_row
for r in sorted(T['rows'],key=lambda r:-r['cost']):
ws.append([r['mid'],r['sid'] if r['sid']!='C9900163599' else 'C2913202',r['need'],r['shop'],r['sku'],r['price'],r['pack'],r['packs'],r['cost'],r['lcsc_cost'],'是' if r['key'] else '',r['url'],r['note']])
c=ws.cell(ws.max_row,12); c.hyperlink=c.value; c.value='打开'; c.font=Font(color='0563C1',underline='single')
ws.append([]); ws.append(['运费(每家店取最高一项)'])
for k,v in sorted(T['ship'].items(),key=lambda x:-x[1]): ws.append(['',k,v,T['shipnote'].get(k,'')])
for i,w in enumerate([24,11,7,22,28,8,8,7,9,12,6,8,30],1): ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width=w
ws['A1'].font=Font(bold=True,size=12)
for r in (2,3): ws.cell(r,4).font=Font(bold=True,color='C00000')
ws.freeze_panes=ws.cell(hr+1,2)
wb.save(f); print('saved')
+17096
查看文件
文件差异内容过多而无法显示 加载差异
+1266
查看文件
文件差异内容过多而无法显示 加载差异
+31
查看文件
@@ -0,0 +1,31 @@
import json,math,re,collections
P=json.load(open('cost_plan.json',encoding='utf-8')); D=json.load(open('cost_detail.json',encoding='utf-8'))
PACK={'C382328':1,'C1525':100,'C163651':20,'C22383480':10,'C440198':20,'C25894':100,'C25744':100,'C25900':100,'C2887585':20,'C49851':1,'C841779':10,'C2570006':10,'C25741':100,'C5210749':1,'C377773':50,'C38117':5,'C12891':10,'C43473':100,'C76857':1,'C25854':100,'C9900163599':1,'C50951':10,'C130026':1,'C5215473':1,'C478081':1,'C2762192':5,'C1547':100,'C25752':100,'C52923':100,'C165948':5,'C25905':100,'C25765':100,'C6186':1,'C116575':5,'C26083':50,'C19188869':1,'C11702':100,'C20917':5,'C2891776':50,'C15127':5,'C8678':20,'C25756':100,'C2847671':50,'C13564':25,'C310990':1,'C151920':20,'C307331':50,'C28323':50,'C42459160':1,'C25750':100,'C3188410':1,'C780205':1,'C25745':100,'C84248':1,'C2653873':1,'C11537':100,'C15195':100,'C13594':50,'C1546':100,'C29402':100,'C25792':100,'C8545':20,'C2934046':20,'C407675':100,'C25755':100,'C310998':10,'C2688893':1,'C36871':100,'C225104':1,'C353034':1,'C3828985':1,'C15008':10,'C5221288':20,'C424561':1,'C2687116':1,'C131941':1,'C25796':100}
ASSUMED={'C76857':'包装数未写明,按 1 只计','C15195':'包装数未写明,按 100 只计','C6186':'包装数未写明,按 1 只计','C25741':'改用欧贝顿 0402 100K 1% 100个(¥2,页面实测)'}
KEY={'C84248','C2653873','C49851','C478081','C5215473','C2688893','C3188410','C9900163599'}
LC_ESP=36.1474
rows=[]; ship=collections.defaultdict(float); shipnote={}
for p in P:
sid=p['sid']; d=D.get(sid) or {}
price=float(d['price']); shop=d['shop']; url=d['url']; sku=d.get('chosen')
if sid=='C25741':
price=2.0; shop='欧贝顿旗舰店'; url='https://detail.tmall.com/item.htm?id=608928061564'; sku=['0402 100K 1% 100个']; d={'ship':'免运费'}
packs=math.ceil(p['need']/PACK[sid]); cost=round(packs*price,2)
s=d.get('ship') or ''
m=re.match(r'([\d.]+)',s); sv=float(m.group(1)) if m else (0.0 if '免运费' in s else 4.0)
if not m and '免运费' not in s: shipnote[shop]='页面未给运费,按 ¥4 估'
ship[shop]=max(ship[shop],sv)
lc=p['lcsc'] if sid!='C9900163599' else LC_ESP
rows.append(dict(sid=sid,mid=p['mid'] if sid!='C9900163599' else 'ESP32-S3-WROOM-1-N16R8',need=p['need'],shop=shop,sku=' / '.join(x for x in (sku or []) if x),price=price,pack=PACK[sid],packs=packs,cost=cost,lcsc=lc,lcsc_cost=round(lc*p['need'],2) if lc else None,url=url,note=ASSUMED.get(sid,''),key=sid in KEY))
goods=sum(r['cost'] for r in rows); sh=sum(ship.values())
print('goods',round(goods,2),'ship',round(sh,2),'shops',len(ship),'total',round(goods+sh,2))
alt=goods-sum(r['cost'] for r in rows if r['key'])+sum(r['lcsc_cost'] for r in rows if r['key'])
kshops={r['shop'] for r in rows if r['key']}; nonkey_shops={r['shop'] for r in rows if not r['key']}
sh_alt=sum(v for k,v in ship.items() if k in nonkey_shops)
print('alt keyIC via LCSC: goods',round(alt,2),'ship(taobao)',sh_alt,'total',round(alt+sh_alt,2))
print('key taobao',round(sum(r['cost'] for r in rows if r['key']),2),'key lcsc',round(sum(r['lcsc_cost'] for r in rows if r['key']),2))
byshop=collections.defaultdict(lambda:[0,0.0])
for r in rows: byshop[r['shop']][0]+=1; byshop[r['shop']][1]+=r['cost']
for k,v in sorted(byshop.items(),key=lambda x:-x[1][1]): print(f' {k[:14]:14} {v[0]:2}种 货¥{v[1]:.2f} 运费¥{ship[k]} {shipnote.get(k,"")}')
for r in sorted(rows,key=lambda r:-r['cost'])[:15]: print(f" {r['mid'][:22]:22} x{r['need']:<3} ¥{r['price']}/{r['pack']} ×{r['packs']} = ¥{r['cost']} (立创 ¥{r['lcsc_cost']})")
json.dump(dict(rows=rows,ship=ship,shipnote=shipnote,goods=goods,shipsum=sh,alt=alt,sh_alt=sh_alt),open('total.json','w',encoding='utf-8'),ensure_ascii=False,indent=1)
+91
查看文件
@@ -0,0 +1,91 @@
import asyncio, json, os, re, math, time
import cdp
exec(open('build.py', encoding='utf-8').read().split('def relevant')[0]) # CORE, SPECIAL, norm, cap_tokens ...
PLAN = json.load(open('cost_plan.json', encoding='utf-8'))
Qd = {x['sid']: x for x in json.load(open('queries.json', encoding='utf-8'))}
OUT = 'cost_detail.json'
out = json.load(open(OUT, encoding='utf-8')) if os.path.exists(OUT) else {}
SKUJS = r'''(()=>{const r=(window.__ICE_APP_CONTEXT__||{}).loaderData?.home?.data?.res; if(!r) return null;
const props=(r.skuBase?.props||[]).map(p=>({name:p.name,values:(p.values||[]).map(v=>v.name)}));
return JSON.stringify({title:r.item?.title,props})})()'''
CLICK = r'''((txt)=>{const el=[...document.querySelectorAll("span,div")].filter(e=>e.children.length<=2&&e.innerText&&e.innerText.trim()===txt);if(!el.length)return false;el[el.length-1].click();return true})'''
def tokens(p):
sid, q = p['sid'], Qd[p['sid']]['q']
if '贴片电阻' in q:
v = q.split()[1].upper(); m = re.match(r'([\d.]+)([KMR])', v); num, u = m.group(1), m.group(2)
f = {v, num + u, num + u + 'Ω'}
if '.' in num: a, b = num.split('.'); f.add(f'{a}{u}{b}')
return [f], [q.split()[0]]
if '贴片电容' in q:
return [cap_tokens(p['desc'])], [re.search(r'(\d+(\.\d)?V)', p['desc']).group(1).upper()]
c = CORE.get(sid, '')
return [{c}] if c else [], []
def score_sku(name, p):
must, nice = tokens(p)
U = name.upper()
s = 0
for grp in must:
if any(re.search(r'(?<![\d.])' + re.escape(t.upper()) + r'(?![\d])', U) or (len(t) >= 5 and norm(t) in norm(name)) for t in grp if t):
s += 5
for t in nice:
if re.search(r'(?<![\d.])' + re.escape(t.upper()), U): s += 2
return s
def parse_price(txt, title):
i = txt.find('¥', max(0, txt.find(title[:12])) if title else 0)
if i < 0: i = txt.find('¥')
if i < 0: return None
seg = txt[i + 1:i + 60].split('\n'); val = ''
for s in seg:
s = s.strip()
if not s: continue
if re.fullmatch(r'[\d.]+|\.\d+|-|~', s): val += s
elif val: break
else: break
return val or None
async def main():
ws = await cdp.open_ws()
for n, p in enumerate(PLAN):
if p['sid'] in out and out[p['sid']].get('price'): continue
rec = None
for c in p['cands'][:2]:
url = ('https://detail.tmall.com/item.htm?id=' if c['tmall'] else 'https://item.taobao.com/item.htm?id=') + c['id']
await cdp.send(ws, 'Page.navigate', {'url': url})
info = None
for k in range(20):
await asyncio.sleep(1)
v = await cdp.ev(ws, SKUJS)
if v: info = json.loads(v); break
if not info:
t = await cdp.ev(ws, 'location.href+" "+document.title')
print(' no page', c['id'], t, flush=True)
if t and ('login' in t or '验证' in t or 'punish' in t):
print('CAPTCHA/LOGIN — waiting', flush=True)
for w in range(120):
await asyncio.sleep(5)
if await cdp.ev(ws, SKUJS): break
await asyncio.sleep(30); continue
chosen = []
for pr in info['props']:
vals = pr['values']
if len(vals) <= 1:
chosen.append(vals[0] if vals else None); continue
best = max(vals, key=lambda x: (score_sku(x, p), -vals.index(x)))
chosen.append(best)
await cdp.ev(ws, '(' + CLICK + ')(' + json.dumps(best) + ')')
await asyncio.sleep(1.2)
txt = await cdp.ev(ws, 'document.body.innerText') or ''
price = parse_price(txt, info['title'] or '')
ship = re.search(r'快递[::]\s*([^\n]+)', txt)
rec = dict(url=url, shop=c['shop'], title=info['title'], props=info['props'], chosen=chosen,
sku_score=[score_sku(x, p) for x in chosen if x], price=price, ship=ship.group(1) if ship else None)
break
out[p['sid']] = rec or {'price': None}
json.dump(out, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(n, p['mid'], rec and (rec['price'], rec['chosen'], rec['ship']), flush=True)
await asyncio.sleep(30)
asyncio.run(main())