用
ebay_record_2026h1的图片做模型推理,读出真实card_id/ 评级公司 / 评级分数,回填进 ClickHousecard_transactions_unified_sync。 本文档覆盖:取数 → 缓存 → 推理 → 语种判定 → OCR → 重检索 → 入库 → 人工核验导出,以及全程踩过的坑与解法。
原来的 import_card_transactions_unified.py 走纯文本路线:从 title 用正则提 card_no → 查 PG cards_master 映射 card_id(命中率极低,大量 UNKNOWN),eBay 的 grade_company 直接写死 RAW、grade_score 写死 无,完全没用图片。
本流程改用图片模型推理:
card_id(图像相似度 Top1)grade_company(PSA/BGS/CGC/SGC)grade_score + 判定语种| 指标 | 数值 |
|---|---|
| 推理总量 | 184881 张(category_id=16 Pokemon 且 img_stat=40 且有图) |
| ClickHouse 整表 | 158304 行(本次新增 61947) |
| 评级卡 | 38768 |
| 有评级分数 | 32652 |
| 人工核验案例(sim<0.60) | 26577 → 导出待人工核对 |
┌─────────────────────────────────────────────────────────────┐
│ 本地 Windows (D:\顾工交接\wzj\ebay_sync\) │
│ ─ Anaconda Python,连三库 │
│ ─ 01 导出目标 / 04 入库 / 05 导出人工核验 / finalize_watch │
│ ─ paramiko SSH 编排服务器 │
└───────────────┬─────────────────────────────────────────────┘
│ SSH (martin@192.168.77.249) + scp
▼
┌─────────────────────────────────────────────────────────────┐
│ 服务器 192.168.77.249 (~/wzj/wzj/ebay_sync/) │
│ ─ GPU: V100×2 (GPU1/GPU2) + GTX1060 (GPU0,不用) │
│ ─ conda env: pytorch (推理) / paddleocr (OCR+语种) │
│ ─ ebay_infer_stream / ebay_lang_judge / ebay_ocr_step / │
│ ebay_rematch / run_infer_dual.sh │
└───────────────┬─────────────────────────────────────────────┘
│ 内网拉图
▼
内网 MinIO 192.168.77.80:9000 (~0.1s/张)
三个数据库:
MySQL 100.64.0.21:3306 crawler/readonly/Pass2024 ← 取原始记录 + 图片路径
PG 100.64.0.10:25432 hs_sync_data/readonlyuser/Pass2026 ← cards_master 中文名/图(人工核验用)
ClickHouse 192.168.31.233:8123 card_transactions_ro/... ← 最终入库(可写)
关键隔离点:本地能连三库,服务器能拉图 + 跑 GPU,两者网络隔离,靠 SSH/scp 传 CSV。
[本地 01] MySQL 拉 source_id+全字段
└─ ebay_targets.csv (184881行, 含 img_url=MinIO内网地址)
│ scp 上传服务器
▼
[服务器 infer] 双卡并行 (V100×2, max-items 分片重启防OOM)
下载图 → best.pt 评级壳 → YOLO 卡面分割 → 透视矫正 → DINOv2 特征 → 全库向量检索 Top1
└─ ebay_infer_result.csv (card_id/sim/grade_company/...)
└─ ebay_feats.npz (DINOv2 特征缓存, 供 rematch 复用)
└─ data/ebay_cardcrops/ (卡面图, 供 lang 判语种)
└─ data/ebay_crops/ (评级标签条, 供 ocr 读分数)
▼
[服务器 lang] PaddleOCR 判语种 (kana/CJK/latin + hanzidentifier)
└─ ebay_lang.csv (tx_id, language)
▼
[服务器 ocr] PaddleOCR 读评级标签分数
└─ ebay_ocr_result.csv (tx_id, grade_score)
▼
[服务器 rematch] 语种分层重检索 (复用缓存特征, 不重跑模型)
英文卡只在 tcg us 库找, 避免错配日/中文卡
└─ ebay_infer_result_lang.csv (覆盖 card_id/sim/top5, 增列 language)
│ scp 取回本地
▼
[本地 04] 构建 20 列 CH 行, 增量入库
sim≥0.50 → 入库 ; sim<0.50/失败 → "人工核验" 跳过
└─ card_transactions_unified_sync (+61947 行)
▼
[本地 05] 导出人工核验案例 (info-only)
└─ D:\顾工交接\wzj\人工核验\ebay\ (manual_top5_detail.csv 等)
作用:从 MySQL crawler.ebay_record_2026h1 拉满足条件的记录,产出推理+入库用的 CSV。
category_id=16 (Pokemon) AND img_stat=40 (图下载成功) AND image_uri IS NOT NULL AND image_uri<>''http://192.168.77.80:9000/ + image_uri。不用 img 列的公网 ebay 图(公网慢 ~10×、且部分失效)。--from/--to 时按 last_sold 序(业务取法),否则按 id ASC(冷启动测试)。输出 ebay_targets.csv 字段(9列):
tx_id, source_id, item_id, title, avg_sold_price, asp_type, total_sold, last_sold, img_url
tx_id = ebay_record_2026h1_<source_id>(全流程主键锚点)img_url = 网关 + image_uri(内网图)
python 01_export_targets.py # 默认拉全部满足条件
python 01_export_targets.py --from 2026-06-22 --to 2026-06-28 # 按日期段
作用:逐行下载图 → 评级壳检测 → 卡面分割矫正 → DINOv2 特征 → 全库 Top1 检索,一次跑完 card_id + 评级。
单条处理流程 (process_one):
GradingDetector(best.pt) 在原图上 → (company, subtype, conf, box);检出则裁标签条存 ebay_crops/CardDetector.detect_box_mask → (box, mask, orig)CardRectifier.rectify(mask, box, angle=0) → 正立卡面;存 ebay_cardcrops/CardFeatureExtractor.extract (DINOv2, 768d) → 存 ebay_feats.npz(供 rematch 复用,不重跑模型)VectorIndex.search(feat, top_k=5) → Top1 card_id + sim + top5模型一次性加载:★卡牌区域检测 (card_seg_pn_v2/weights/best.pt,TRT 可选 best.engine;旧 yolov11n_card_seg01.onnx 仅回退) + best.pt + DINOv2 (PokemonCN04) + 图库(78272 卡 ×768d)。
产出:
ebay_infer_result.csv — tx_id/source_id/card_id/similarity/grade_company/grade_subtype/grade_conf/is_graded/crop_file/top5/statusebay_feats.npz — tx_ids + feats(N,768)data/ebay_cardcrops/*.jpg、data/ebay_crops/*.jpg断点续跑:启动时读 ebay_infer_result.csv 已 done 的 tx_id,自动跳过。
见 第五章。把剩余 tx_id 分成 shard0/shard1,两块 V100 并行,每跑 1500 条重启进程释放泄漏内存。
作用:读卡面图 ebay_cardcrops/,用 PaddleOCR (PP-OCRv5) + hanzidentifier 判定语种(kana=日 / CJK=中 / latin=英)。
SHARD=0/2 和 SHARD=1/2 各处理一半ebay_lang_0.csv / _1.csv → 合并成 ebay_lang.csv (tx_id, language)作用:读评级标签条 ebay_crops/,PaddleOCR 识别评级分数(如 GEM MT 10、9.5)。
ebay_ocr_result.csv (tx_id, grade_score)作用:语种分层重检索——复用阶段1缓存的 DINOv2 特征 + 阶段2判定的语种,在对应语种子库内重新检索,覆盖 card_id/similarity/top5。
为什么需要:阶段1全库检索时,英文卡可能错配到日/中文卡(DINOv2 特征相近)。按语种分层后,英文卡只在 tcg us 库找,准确率提升。
性能优化:预建"每种语种 → 子索引"(只建 4 次),替代每次检索重建 mask。检索变成纯 numpy matmul,41876 条秒级完成。
输入:ebay_feats.npz + ebay_lang.csv + ebay_infer_result.csv
输出:ebay_infer_result_lang.csv(覆盖 card_id/sim/top5,增列 language)
作用:合并 ebay_targets.csv(全字段) + 推理结果 + OCR,构建 20 列 CH 行入库。
字段映射策略:
| CH 字段 | 来源 |
|---|---|
| tx_id/source_id/title/item_id/auction_type/total_sold/price_original/sold_date/image_uri | ebay_targets.csv |
| card_id | status==ok 且 sim≥0.50 → 图像 Top1;否则 "人工核验"(不入库,留给 05)|
| grade_company | 检出评级壳且 ∈{PSA,BGS,CGC,SGC} → 该公司;否则 "非评级卡" |
| grade_score | 评级卡 → OCR 分数;否则 "无" |
| source_table/platform/currency | 固定 ebay_record_2026h1/eBay/USD |
入库模式(关键):
--replace:全量替换(先 ALTER DELETE WHERE 1=1 清表再灌)--dry-run:只打印样例不动库--target-valid N:入库有效行封顶(0=全部高置信入库)
python 04_load_clickhouse.py --dry-run # 先校验
python 04_load_clickhouse.py # 增量入库(默认)
注意:sim<0.50 的"人工核验"行不入库(
if cid == MANUAL: continue),留给 05 导出。所以入库数 < 推理数是正常的。
作用:把 sim<0.50 / 匹配不稳的案例导出到本地,供人工核对。
--info-only(只导信息,不下图)cards_master 补候选 card_id 的中文名/img_urlD:\顾工交接\wzj\人工核验\ebay\:
manual_top5_detail.csv — 每个案例的 Top5 候选明细manual_index.csv — 汇总索引infer 是 GPU 密集型(下载只占 ~8% 时间),单卡时 GPU 空闲多。两块 V100 并行 → infer 时间约减半。提 workers(8→32) 只加速下载的 ~8%,收益小;双卡收益大。
prepare_shards.py:读 ebay_infer_result.csv 已 done 的 tx_id,从 ebay_targets.csv 减去,剩余按偶/奇索引分成 shard0/shard1(各 ~18060 条)。
问题:ultralytics model.predict() 会缓存 Results 对象,每图 ~6MB 泄漏,两个 detector(yolo+grading) 双倍。跑到 5120 条时 RSS 飙到 30GB → OOM Killed。
解法:--max-items 1500 让进程处理 1500 条后正常退出(写 checkpoint),外层 run_shard() 循环重启新进程(自动跳过已 done)。
run_shard() {
while true; do
DONE=$(行数)
[ $DONE -ge $TOTAL ] && break
python ebay_infer_stream.py --max-items 1500 ...
NEWDONE=$(新行数)
if [ $NEWDONE -le $DONE ]; then
FAIL=$((FAIL+1))
[ $FAIL -ge 4 ] && break # 连续4次无进展放弃, 防死循环
else FAIL=0; fi
done
}
原实现 np.stack([100k arrays]) 在首次 checkpoint 触发峰值 OOM。改为:
prev_results + results 全量重写np.concatenate([old_keep, new_feats]) 替代 list+stack,并 del 中间量双卡产出 ebay_infer_result_0.csv/_1.csv + ebay_feats_0.npz/_1.npz,合并到主文件(后者覆盖去重),特征用预分配 np.empty((n,768)) 逐条填避免峰值,然后清理分片文件。
Killed process ... anon-rss:30291292kBpredict() 缓存 Results 对象,6MB/图 × 5120 = 30GB;yolo_detector + grading_detector 两个都 predict,双倍泄漏--max-items 1500 分片重启(进程退出才彻底释放)+ del downloaded + 每512条 gc.collect() + checkpoint 改 concatenatesudo pkill milvus 杀掉后又被自动拉起,内存又涨回 7.5GBdocker stop milvus-standalone milvus-minio milvus-etcd + docker update --restart=no(永久停直到手动恢复)。推理完再 finalize_watch 的 [5/5] 恢复run_infer_dual.sh 的 run_shard() 加 4 次无进展放弃机制(5.4)list(10万 arrays) + np.stack 在 stack 那一刻内存峰值爆炸np.concatenate + del 中间量;合并阶段用预分配(5.5)ulimit -v 14000000,结果 DINOv2 模型 mmap 报 Cannot allocate memory (12)setsid nohup ... & 后台启动 run_infer_dual.sh,paramiko o.read() 一直挂住不返回transport.open_session() + chan.exec_command(...) + chan.close(),不读 stdoutdl_fail 从 270 升到 601(~15%),后来稳定 ~9%download_fail,04 入库时归为"人工核验"跳过,不阻断整体WHERE id IN (...) 一次查几万 id 容易超时WHERE 过滤条件 ORDER BY 全量拉(01 采用后者)infer_single.log,双卡改用 infer0/1.log 后显示全是"7分前"和空,误以为卡死;读旧残留 ebay_ocr_result.csv(2141行) 误导<<<TAG>>> 分段一次拿回全部(进程/进度/RSS/GPU/内存/milvus),单次往返;阶段判断优先用进程名(has_lang 等),log-age 兜底05_export_manual.py 写 manual_top5_detail.csv 报 Permission deniedpython 05_export_manual.py(非致命,入库已完成)# 本地后台启动监控(会自动: 轮询PIPELINE_DONE → 下载结果 → 04入库 → 05导出 → 恢复milvus)
python finalize_watch.py
# 1) 本地导出目标
python 01_export_targets.py
# scp 上传 ebay_targets.csv 到服务器 ~/wzj/wzj/ebay_sync/
# 2) 服务器双卡推理(SSH 上去)
cd ~/wzj/wzj
setsid nohup bash ebay_sync/run_infer_dual.sh </dev/null >/dev/null 2>&1 &
# 产物: ebay_infer_result.csv + ebay_feats.npz + ebay_lang.csv + ebay_ocr_result.csv + ebay_infer_result_lang.csv
# 生成 PIPELINE_DONE 标记
# 3) 本地入库(先 dry-run)
python 04_load_clickhouse.py --dry-run
python 04_load_clickhouse.py # 增量入库
# 4) 本地导出人工核验
python 05_export_manual.py
python 查进度.py # 链式版, 单次SSH往返, 显示阶段/进度/RSS/GPU/内存/milvus/进程
# 服务器
sudo docker update --restart=always milvus-etcd milvus-minio milvus-standalone
sudo docker start milvus-etcd milvus-minio # 先依赖
sleep 6
sudo docker start milvus-standalone
(finalize_watch 的 [5/5] 会自动做这步)
| 物理位 | UUID 前缀 | 型号 | 用途 |
|---|---|---|---|
| GPU0 | GPU-bb6d8194 |
GTX1060 3GB | 不用 |
| GPU1 | GPU-9d6999fb |
V100 16GB | shard0 (V1) |
| GPU2 | GPU-db470791 |
V100 16GB | shard1 (V2) |
CUDA_VISIBLE_DEVICES=GPU-9d6999fb-... # 指定单卡
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:64,expandable_segments:True # 减少显存碎片
SIM_THRESHOLD = 0.50(card_id 入库门槛,<0.50 归人工核验)—— config.py + 04--max-items 1500(防 OOM 分片大小)--workers 8 --chunk 256(并发下载/分块)card_transactions.card_transactions_unified_syncORDER BY (card_id, sold_date, platform, tx_id)本地 D:\顾工交接\wzj\ebay_sync\ ↔ 服务器 ~/wzj/wzj/ebay_sync/,同名同结构。
| 文件 | 位置 | 作用 |
|---|---|---|
01_export_targets.py |
本地 | MySQL 拉目标 → ebay_targets.csv |
ebay_infer_stream.py |
服务器 | ★ 核心:下载+评级+分割+特征+检索 |
prepare_shards.py |
服务器 | 剩余 tx_id 分 shard0/shard1 |
run_infer_dual.sh |
服务器 | ★ 双卡编排 + max-items 重启 + 失败检测 |
ebay_merge_results.py |
服务器 | 合并双卡 result + feats,清理分片 |
ebay_lang_judge.py |
服务器 | PaddleOCR 判语种(双卡) |
ebay_ocr_step.py |
服务器 | PaddleOCR 读评级分数(双卡) |
ebay_rematch.py |
服务器 | 语种分层重检索(复用特征) |
04_load_clickhouse.py |
本地 | ★ 构建20列行,增量入库 |
05_export_manual.py |
本地 | 导出人工核验案例 |
finalize_watch.py |
本地 | 轮询 DONE 自动收尾(+恢复milvus) |
查进度.py |
本地 | 链式查进度(单次SSH往返) |
--max-items 分片重启是最稳的通用解法update --restart=no 才能真停set -e 或显式 rc 检查防假完成