fix: 抓取前预检已入库的秀,避免重复下载(重爬 62 分钟 0 新增)

问题:图片 key 是内容 sha1,不下载就无法判断是否已存在;
后端又没存 source_url,无法做 URL 级判重。
于是每次重爬都把几百张已下过的图再下一遍 —— 线上实测
Celine 跑了 62 分钟、约 600MB,入库 0 新增。

修复:
- 后端新增 GET /admin/internal/shows/covered?brand_uid=
  按 (年份, 季节, 类型) 聚合已入库图片张数
- spider 抓取前预检,整场已入库(>=10 张)就跳过,一张图都不下载
- 加 --force 可强制重抓(默认跳过)
- 媒体直传并发改为全进程共享信号量:此前秀级 2 × 每场 6 = 12 路
  同时打 Vogue,被限流后单张从 9.8s 掉到 ~90s
- 批次下载失败时打印第一条错误(此前只统计个数,查不出原因)
This commit is contained in:
toom1996
2026-10-25 20:07:54 +08:00
parent 6a8b7f0704
commit 934a8961b0
5 changed files with 167 additions and 7 deletions

View File

@ -31,8 +31,27 @@ import (
type Uploader struct {
Client *ingest.Client // 复用它来签发预签名 URL(HMAC)
Prefix string // 存储前缀:runway / street
Concurrency int // 下载与上传的并发数
Concurrency int // **全进程共享**的下载/上传并发数
HTTP *http.Client
// sem 全进程共享的信号量,避免「每场秀各开 N 路」叠乘。
//
// 早先每个 UploadAll 调用各自 make 一个信号量:秀级并发 2 × 每场 6 = 12 路
// 同时打 Vogue,被对方限流,单张从 9.8s 掉到 ~90s,还伴随 50% 失败。
once sync.Once
sem chan struct{}
}
// semaphore 惰性初始化共享信号量(并发数非法时兜底为 1,否则无缓冲 channel 会死锁)。
func (u *Uploader) semaphore() chan struct{} {
u.once.Do(func() {
c := u.Concurrency
if c <= 0 {
c = 1
}
u.sem = make(chan struct{}, c)
})
return u.sem
}
// Result 单张图的直传结果;Err 非空表示这张失败(调用方应回退为上报 URL)。
@ -94,7 +113,19 @@ func (u *Uploader) uploadBatch(ctx context.Context, urls []string, results []Res
}
data[i] = body
})
log.Printf("[media] 下载完成 %d 张(失败 %d)", countOK(data), n-countOK(data))
// 失败时把**第一条**错误打出来:失败通常是同一个原因(限流 / 连接重置),
// 全打会把日志刷爆,而一条就足以定位。此前只统计个数,导致 50% 失败却查不出原因。
if ok := countOK(data); ok < n {
for i := 0; i < n; i++ {
if errs[i] != nil {
log.Printf("[media][warn] 下载失败 %s: %v", clipURL(urls[start+i]), errs[i])
break
}
}
log.Printf("[media] 下载完成 %d 张(失败 %d)", ok, n-ok)
} else {
log.Printf("[media] 下载完成 %d 张(失败 0)", ok)
}
// ② 生成本批 key(内容 sha1 + 原扩展名)
for i := 0; i < n; i++ {
@ -164,11 +195,10 @@ func (u *Uploader) parallel(n int, fn func(i int)) {
if n == 0 {
return
}
if u.Concurrency <= 0 {
// 兜底:并发为 0 会造成无缓冲 channel 死锁(曾把整条抓取卡住)。
u.Concurrency = 1
}
sem := make(chan struct{}, u.Concurrency)
// 共享信号量:并发上限是**整个进程**的,不是每次调用各自一份。
// 早先每次 UploadAll 各建一个:秀级并发 2 × 每场 6 = 12 路同时打 Vogue,
// 被对方限流后单张从 9.8s 掉到 ~90s,并伴随大量失败。
sem := u.semaphore()
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
@ -224,6 +254,14 @@ func (u *Uploader) put(ctx context.Context, putURL, originalURL string, data []b
return nil
}
// clipURL 截短 URL 只用于日志(完整 URL 太长,会淹没真正有用的错误信息)。
func clipURL(u string) string {
if len(u) <= 90 {
return u
}
return u[:90] + "..."
}
// countOK 数一下成功下载的张数(用于日志定位卡在哪一步)。
func countOK(data [][]byte) int {
n := 0