Two CLI gates needed before scaling Pro batch beyond top-5:
--skip-ext mp4,qt,mov (attachment filter)
Skips video extensions in attachment download. Phase 1 measurements
showed mp4+qt occupy ~54% of attachment storage. Entry still recorded
in metadata.json with skipped:ext:<token> so we can re-fetch later if
the policy changes. Honors both server-declared `ext` and filename
suffix, case-insensitively.
--max-source-mb N (Pro source size cap)
Trips inside the chain replay loop on encrypted-blob total. On trip:
raise ProjectOversizeError, wipe partial source/, append a row to
data/state/oshwhub_pro_oversize.jsonl. Lets us shortlist 50+ Pro
projects without one X86-board-class outlier (~500 MB) blowing the
LFS budget. Std and Pro 2.x legacy are not capped (both <2 MB in
sample).
Verified:
- cap=0 trips on first blob (1.2 MB), source/ wiped, state recorded
- cap=100 runs full ESP-VoCat (7.5 MB plain, 278 docs)
- skip-ext microtest: 8/8 cases (case-insensitive, declared/suffix
fallback, empty-token edge cases)
Plan + frozen candidate list for the next 50 projects:
- docs/plans/oshwhub_batch50.md
- data/state/oshwhub_batch50_candidates.jsonl (gitignore exception added)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.6 KiB
oshwhub 扩抓批 50:执行计划
创建:2026-04-29
目标:在已落库的 15 项(5 Pro + 10 Std)基础上,再抓 50 个高质量项目
候选清单:data/state/oshwhub_batch50_candidates.jsonl(50 行)
承接:docs/sources/oshwhub_listing_full.md(全量索引盘点)
1. 目标
- 把 corpus 从 15 项扩到 65 项,覆盖 Pro + Std + Pro 3.x + Pro 2.x 各形态
- 在 LFS 1 TB 红线内,先做 ~3-4 GB 量级的中等规模验证
- 结束后用 license 白名单评估这 50 项里有多少能进 Forge 投影
- 暴露 ≥ top-5 才会出现的 corner case(例如:超大 Pro 项目、私有附件、跨账号 fork)
显式不做:详情页全量扫描(19h 工作量,留待后续按 A 档全池一次性做);下游 KiCad 转换(Phase 3 还在做)。
2. 候选池筛选标准
数据源:data/state/oshwhub_listing_full.jsonl(33,695 项全量 listing 索引)
硬过滤:
grade >= 3 AND like >= 10(A 档)uuid不在已抓 15 项内
排序:复合质量打分
score = like*3 + star + fork*2 + views/100 + comments_count*2 + grade*50
多样性约束:
- 单作者上限 2 个项目(避免
course-examples/45coll等高产账号占满) - Pro / Std 25:25 平衡(Pro 是新平台增长焦点,Std 是 7 年质量积累)
3. 选出的 50 项概况
| 维度 | 值 |
|---|---|
| 总数 | 50(25 Pro + 25 Std) |
| 唯一作者 | 43 |
| Max 单作者 | 2 |
| like 区间 | min=246 / median=554 / max=2137 |
| view 区间 | min=54k / median=117k / max=362k |
| grade 分布 | 4: 33 项, 3: 17 项 |
Pro 头部(前 5):HelloWord-Keyboard 2137 likes / STM32 桌面宠物 / OV-Watch 智能手表 / mini 加热台 / ESP-SparkBot
Std 头部(前 5):触摸调光雪花灯 1036 likes / 桌面可调电源 / 立创 EDA PCB 直尺 / 自平衡莱洛三角 / PN532 NFC
完整名单见 data/state/oshwhub_batch50_candidates.jsonl。
4. 执行步骤
Step 0 — 候选池冻结 ✅
data/state/oshwhub_batch50_candidates.jsonl 已生成,作为本次批的"事实档案"。后续就算重排序也以这份为准。
Step 1 — 详情页抓 license(~5 min,QPS=0.5)
只对这 50 项扫详情页,提取 license / 完整 attachments 列表 / 原始描述。 不在这一步下载附件、不下源工程,只是获取每项的 metadata 全集。
PYTHONUNBUFFERED=1 uv run python -u -m crawlers.oshwhub \
--uuids "$(jq -r .uuid data/state/oshwhub_batch50_candidates.jsonl | paste -sd,)" \
--no-files \
--out data/raw/oshwhub
目前
crawl_one已经会落 metadata.json + description.md + cover;--no-files跳过附件。
预期产出:50 个 data/raw/oshwhub/<uuid>/metadata.json,含 license 字段。
Step 2 — license 分布盘点
写个一次性脚本读 50 份 metadata.json,统计 license 分布。这一步只做盘点不做过滤——按 CLAUDE.md "研究用、不分发" 原则,所有 license 都入库。下游 Forge 投影时再用白名单过滤。
uv run python - <<'EOF'
import json, glob, collections
licenses = collections.Counter()
for p in glob.glob("data/raw/oshwhub/*/metadata.json"):
m = json.load(open(p))
if m["uuid"] in batch_50_uuids:
licenses[m.get("license") or "<none>"] += 1
print(licenses)
EOF
预期会有大量 NC-SA / GPL(参考已抓 5 Pro:5/5 都是 GPL/NC-SA)。
Step 3 — Pro 子集预检 + 抓源(~30-60 min)
25 个 Pro 项目里要先判 3.x vs 2.x(branch_uuid 是否 null),然后对应走 modern / legacy 路径:
PYTHONUNBUFFERED=1 uv run python -u -m crawlers.oshwhub \
--backfill-pro-source \
--uuids "$(jq -r 'select(.origin=="pro") | .uuid' \
data/state/oshwhub_batch50_candidates.jsonl | paste -sd,)" \
--pro-cookie ~/.secrets/pro-lceda-cookie-header.txt
时间预算(已应用 chain replay sleep 优化,commit 1e06ba6):
- 小型项目(chain ~10):~30s 各
- 中型(chain ~30-50):~1-2 min 各
- 大型(chain ~100+):~3-5 min 各
- 超大(X86 量级 chain ~700):~3-5 min 各(CDN 部分降到 0.2s/req)
按已抓 5 项观察,预期 25 项里:
- ~22 个 Pro 3.x(chain replay)
- ~3 个 Pro 2.x(plaintext dataStr)
Step 4 — Std 子集抓源(~30 min)
PYTHONUNBUFFERED=1 uv run python -u -m crawlers.oshwhub \
--backfill-source \
--uuids "$(jq -r 'select(.origin=="std") | .uuid' \
data/state/oshwhub_batch50_candidates.jsonl | paste -sd,)"
每项 ~30s(QPS=0.2 / 5s sleep × 几个 API 调用),25 项 ≈ 12-15 min。
Step 5 — 附件下载(按需,可选)
附件(Gerber / STEP / PDF / 视频)默认存 LFS,但视频占大头(之前 Phase 1 实测 mp4+qt 占 54%)。
建议加 --skip-ext mp4,qt,mov 节省 30-50% 体积。
PYTHONUNBUFFERED=1 uv run python -u -m crawlers.oshwhub \
--uuids "$(jq -r .uuid data/state/oshwhub_batch50_candidates.jsonl | paste -sd,)" \
--out data/raw/oshwhub
# 注:当前 crawler 没有 --skip-ext 选项;要加得改 crawl_one;如果偷懒就先全抓再人工删
这一步没那么紧迫,可以挪到 Step 3/4 完成后单独跑。
Step 6 — 验收 + 记录
跑 scripts/build_index.py 重建 projects.md,更新 log.md,把统计信息写到 docs/plans/oshwhub_batch50.md 末尾"实施结果"段。
5. 资源预算
| 项 | 估算 | 备注 |
|---|---|---|
| API 调用 | ~600 次 | 50 详情 + 25×3 Pro meta + 25 Std API + chain blobs |
| 时间 | ~1.5-2 小时 | 远低于之前估算的 19h(因为只 50 项 + sleep 优化) |
| 存储(源工程) | ~1.5 GB | Pro 平均 30 MB、Std 平均 15 MB |
| 存储(附件,估) | 2-3 GB | 含视频;不含视频估 1 GB |
| 总 LFS 增量 | ~3-4 GB | 远低于现有 LFS 余量 |
| 网络下行 | ~4 GB | 大部分走 LFS |
距离 200 GB"对象存储迁移决策点"还很远。
6. 风险与预案
| 风险 | 概率 | 影响 | 预案 |
|---|---|---|---|
| Pro cookie 过期半路 | 中 | 卡住 Pro 抓取 | 重抓 cookie;爬虫已有 oshwhub_pro_failed.jsonl 重试机制(未实现,现在失败只 stderr) |
| 单个 Pro 项目超大(X86 量级 ~500 MB) | 中 | LFS 单项膨胀 | 设置 size cap,超过 200 MB 跳过 + 记 state |
| license 全是 NC-SA → 下游 Forge 投影门槛低 | 高 | Forge 阶段才发现 | 提前在 Step 2 盘点;本批不做过滤,只入库 |
| 某个项目附件含 mp4 视频 → 附件总量爆炸 | 中 | 多消耗 1-2 GB LFS | 加 --skip-ext mp4,qt,mov 选项(需小改 crawler) |
| 抓 50 项过程中触发反爬 / 限流 | 低 | 中途中断 | 已有 5s/req QPS;如果触发,加 jitter;从中断点续 |
7. 验收标准
data/raw/oshwhub/下新增 ≥ 45 个项目目录(允许 ≤ 5 项失败)- 每个新增项目的
metadata.json含license、source_format、source_path - Pro 25 项里 3.x / 2.x 分类正确(看
source_format字段) - LFS 增量 ≤ 5 GB
projects.md自动重建后包含新 50 项- log.md 顶部一条总结性日志
8. 后续
成功结束后的下一步候选:
- license 详情扫描全 A 档 —— 把 license 信息覆盖到 2,806 项 A 档候选池,为下一次扩抓提供过滤条件
- 扩抓批 200(按 license 白名单 + 同样多样性策略)
- Pro 工程二进制图(IMAGE/)补抓 —— 当前的真实数据 gap,要不要补取决于下游需求
- 风控压测 —— 在某一批做并发提速试验,找 oshwhub / pro.lceda.cn 的限流真上限