二进制文件未显示。
+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。
|
||||
+1291
文件差异内容过多而无法显示
加载差异
+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')
|
||||
文件差异内容过多而无法显示
加载差异
@@ -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]))
|
||||
+2765
文件差异内容过多而无法显示
加载差异
+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())
|
||||
+5354
文件差异内容过多而无法显示
加载差异
@@ -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
文件差异内容过多而无法显示
加载差异
+5308
文件差异内容过多而无法显示
加载差异
@@ -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())
|
||||
@@ -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
文件差异内容过多而无法显示
加载差异
@@ -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)
|
||||
@@ -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())
|
||||
在新工单中引用
屏蔽一个用户