Compare commits

...

4 Commits

Author SHA1 Message Date
2c4dd12820 Merge branch 'dev' of https://git.23cm.cn/toom1996/spider into dev
# Conflicts:
#	internal/ingest/client.go
#	internal/ingest/hashid.go
2026-09-20 00:47:24 +08:00
fde57984e7 update 2026-09-20 00:44:47 +08:00
a939848e84 update 2026-09-16 21:31:32 +08:00
d8f667e391 update 2026-09-13 00:52:09 +08:00
25 changed files with 4173 additions and 511 deletions

BIN
bin/spider Normal file

Binary file not shown.

BIN
bin/spider.exe Normal file

Binary file not shown.

View File

@ -4,20 +4,17 @@ import (
"fmt"
"log"
"os"
"time"
"github.com/spf13/cobra"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"my-spiders/internal/config"
"my-spiders/internal/ingest"
)
var (
DB *gorm.DB
cfgFile string
debug bool // 命令行 debug 标志
IngestClient *ingest.Client // 入库管线客户端(enabled 时非 nil)
cfgFile string
debug bool // 命令行 debug 标志
rootCmd = &cobra.Command{
Use: "spider-cli",
@ -39,10 +36,10 @@ func init() {
// 新增全局 -d / --debug 参数
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "调试模式 (仅输出不入库)")
cobra.OnInitialize(initConfigAndDB)
cobra.OnInitialize(initConfig)
}
func initConfigAndDB() {
func initConfig() {
// 1. 初始化配置文件
if err := config.InitConfig(cfgFile); err != nil {
log.Fatalf("[致命错误] %v", err)
@ -53,39 +50,14 @@ func initConfigAndDB() {
config.GlobalConfig.App.Debug = true
}
isDebug := config.GlobalConfig.App.Debug
// 2. 初始化数据库连接
dbCfg := config.GlobalConfig.Database
var err error
DB, err = gorm.Open(mysql.Open(dbCfg.DSN), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
if isDebug {
log.Println("[Warning] 数据库连接失败,当前处于 Debug 模式,将跳过数据库操作...")
DB = nil
return
}
log.Fatalf("[致命错误] 数据库连接失败: %v", err)
// 2. 入库管线:enabled 时构建 HMAC 上报客户端,并用后台盐值初始化 hashid 编码。
// spider 不再直连数据库,所有落库经后端 ingest 接口完成。
ic := config.GlobalConfig.Ingest
if ic.Enabled && ic.Endpoint != "" {
ingest.InitHashID(ic.HashIDSecret)
IngestClient = ingest.NewClient(ic.Endpoint, ic.Secret)
log.Printf("[Info] 入库管线已启用 -> %s", ic.Endpoint)
} else {
IngestClient = nil
}
// 3. 设置连接池参数
sqlDB, err := DB.DB()
if err != nil || sqlDB.Ping() != nil {
if isDebug {
log.Println("[Warning] 数据库 Ping 失败,当前处于 Debug 模式,将跳过数据库操作...")
DB = nil
return
}
log.Fatalf("[致命错误] 数据库不可用: %v", err)
}
sqlDB.SetMaxOpenConns(dbCfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(dbCfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(time.Duration(dbCfg.ConnMaxLifetime) * time.Minute)
sqlDB.SetConnMaxIdleTime(time.Duration(dbCfg.ConnMaxIdleTime) * time.Minute)
log.Printf("[Info] MySQL 连接池初始化完成 | MaxOpen: %d | MaxIdle: %d", dbCfg.MaxOpenConns, dbCfg.MaxIdleConns)
}

View File

@ -6,7 +6,10 @@ import (
"my-spiders/internal/spider"
)
var theImpressionMaxCo int
var (
theImpressionMaxCo int
theImpressionMaxArticles int
)
var theImpressionCmd = &cobra.Command{
Use: "theimpression",
@ -17,7 +20,8 @@ var theImpressionCmd = &cobra.Command{
}
// 复用全局 -d/--debug 标志
isDebug := debug
s := spider.NewTheImpressionSpider(DB, theImpressionMaxCo)
s := spider.NewTheImpressionSpider(theImpressionMaxCo, IngestClient)
s.MaxArticles = theImpressionMaxArticles
s.Debug = isDebug
s.Run()
},
@ -26,4 +30,5 @@ var theImpressionCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(theImpressionCmd)
theImpressionCmd.Flags().IntVarP(&theImpressionMaxCo, "max-co", "m", 5, "最大并发数限制")
theImpressionCmd.Flags().IntVarP(&theImpressionMaxArticles, "max-articles", "n", 0, "最多抓取的街拍任务数(0=不限制,小批量试抓用)")
}

View File

@ -7,9 +7,9 @@ import (
)
var (
brandID int64
onlyPlatform bool
maxCo int
brandID int64
maxCo int
maxShows int
)
var vogueCmd = &cobra.Command{
@ -17,7 +17,7 @@ var vogueCmd = &cobra.Command{
Short: "启动 Vogue 时装发布会采集器",
Run: func(cmd *cobra.Command, args []string) {
// 1. 修复参数数量 & 类型强转 uint(brandID)
vogueSpider := spider.NewVogueSpider(DB, maxCo, uint(brandID), onlyPlatform)
vogueSpider := spider.NewVogueSpider(maxCo, uint(brandID), maxShows, IngestClient)
// 2. 修复方法名:将 Execute() 改为 Run()
vogueSpider.Run()
@ -29,8 +29,10 @@ func init() {
// 绑定命令行参数
vogueCmd.Flags().Int64VarP(&brandID, "brand", "b", 0, "指定抓取的品牌 ID")
vogueCmd.Flags().BoolVarP(&onlyPlatform, "only-platform", "p", false, "是否仅抓取平台关联品牌")
// 修复:将 "c" 改为 "m"(或者 ""),避免与全局的 -c 参数冲突
vogueCmd.Flags().IntVarP(&maxCo, "max-co", "m", 5, "最大并发数限制")
}
// 小批量试爬:仅抓取单个品牌的前 N 场发布会(0=不限制)。用于控制入库量 / 先看效果。
vogueCmd.Flags().IntVarP(&maxShows, "max-shows", "s", 0, "单个品牌最多抓取的发布会场次(0=不限制,小批量试爬用)")
}

View File

@ -1,12 +1,14 @@
# 爬虫框架通用配置
app:
max_co: 3 # 默认最大并发协程数
debug: true # 默认为 true(开发调试),生产环境中设为 false
debug: false # 开发调试仅打印控制台;入库设为 false
# MySQL 数据库及连接池配置
database:
dsn: "root:123456@tcp(127.0.0.1:3306)/database?charset=utf8mb4&parseTime=True&loc=Local"
max_open_conns: 50 # 连接池最大打开连接数
max_idle_conns: 10 # 连接池最大空闲连接数
conn_max_lifetime: 30 # 连接可复用的最长时间(分钟)
conn_max_idle_time: 10 # 连接空闲最大存活时间(分钟)
# 入库管线:爬虫 → 后台 ingest 接口(HMAC 签名),替代直写 brand_runway
# enabled: true 时,vogue 走秀改为签名 POST 上送元数据,由后台 worker 异步下载入库。
# spider 不再直连数据库:品牌任务经 GET /admin/internal/crawl/brands 拉取。
# secret / hashid_secret 必须与后台 INGEST_SECRET / HASHID_SECRET 完全一致。
ingest:
enabled: true
endpoint: "http://localhost:8092/admin/internal/ingest"
secret: "dev-ingest-secret-2026"
hashid_secret: ""

6
go.mod
View File

@ -7,19 +7,13 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/tidwall/gjson v1.19.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.2
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect

16
go.sum
View File

@ -1,5 +1,3 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
@ -11,24 +9,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -108,9 +98,3 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@ -10,8 +10,18 @@ import (
var GlobalConfig Config
type Config struct {
App AppConfig `mapstructure:"app"`
Database DatabaseConfig `mapstructure:"database"`
App AppConfig `mapstructure:"app"`
Ingest IngestConfig `mapstructure:"ingest"`
}
// IngestConfig 入库管线配置:爬虫 → 后台 ingest 接口(HMAC 签名)。
// spider 不直连数据库:品牌任务经 GET /admin/internal/crawl/brands 拉取,
// 抓取结果经 POST /admin/internal/ingest 上报,由后台 worker 异步落库。
type IngestConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用签名上报(替代直写)
Endpoint string `mapstructure:"endpoint"` // 后台 ingest 地址,如 http://localhost:8092/admin/internal/ingest
Secret string `mapstructure:"secret"` // 必须与后台 INGEST_SECRET 一致
HashIDSecret string `mapstructure:"hashid_secret"` // 必须与后台 HASHID_SECRET 一致(用于编码 brand_uid)
}
type AppConfig struct {
@ -19,14 +29,6 @@ type AppConfig struct {
Debug bool `mapstructure:"debug"` // 新增 Debug 字段
}
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime int `mapstructure:"conn_max_idle_time"`
}
// InitConfig 初始化并读取 YAML 配置文件
func InitConfig(cfgFile string) error {
if cfgFile != "" {

155
internal/fetch/fetch.go Normal file
View File

@ -0,0 +1,155 @@
// Package fetch 提供「全局并发限流 + 自动重试」的 HTTP GET,供各爬虫共用。
//
// 为什么需要它:
// - 此前 vogue / theimpression 各自实现了一份 request(),逻辑重复、错误被吞掉(失败只返回 ("", 0));
// - 更严重的是并发失控:vogue 在「品牌级」与「详情级」各开一层信号量,实际并发是 MaxCo²
// (如 5×5=25),与「最多 5 个并发」的本意不符。
//
// 本包用一个「全局信号量」统一约束进程内所有请求的并发上限,各爬虫共享同一个 Client
// 即可保证整体并发受控,不会因嵌套而放大。
package fetch
import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"
)
const (
// DefaultTimeout 单次请求默认超时。
DefaultTimeout = 30 * time.Second
// DefaultMaxConcurrency 默认全局并发上限。
DefaultMaxConcurrency = 5
// DefaultRetries 默认重试次数(不含首次)。
DefaultRetries = 2
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
acceptHeader = "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
)
// Options 构造 Client 的参数,零值字段自动回落默认。
type Options struct {
MaxConcurrency int // 全局并发上限(<=0 回落 5)
MinInterval time.Duration // 两次请求之间的最小间隔,全局限速(0 = 不限速)
Retries int // 失败重试次数,不含首次(<0 回落 2;0 表示不重试)
Backoff time.Duration // 重试基础退避,第 n 次等待 n*Backoff(<=0 回落 1s)
Timeout time.Duration // 单次请求超时(<=0 回落 30s)
}
// Client 带并发限流与重试的 HTTP 客户端。
type Client struct {
hc *http.Client
sem chan struct{} // 全局并发槽(跨所有调用方共享)
tick *time.Ticker // 非 nil 时按 MinInterval 全局限速
retries int
backoff time.Duration
}
// New 构造 Client。返回值实现了一个进程级共享的限流器,多个爬虫应复用同一个实例。
func New(opt Options) *Client {
if opt.MaxConcurrency <= 0 {
opt.MaxConcurrency = DefaultMaxConcurrency
}
if opt.Timeout <= 0 {
opt.Timeout = DefaultTimeout
}
if opt.Backoff <= 0 {
opt.Backoff = time.Second
}
if opt.Retries < 0 {
opt.Retries = DefaultRetries
}
c := &Client{
hc: &http.Client{Timeout: opt.Timeout},
sem: make(chan struct{}, opt.MaxConcurrency),
retries: opt.Retries,
backoff: opt.Backoff,
}
if opt.MinInterval > 0 {
c.tick = time.NewTicker(opt.MinInterval)
}
return c
}
// Close 停止内部限速计时器(未开启限速时为空操作)。
func (c *Client) Close() {
if c.tick != nil {
c.tick.Stop()
}
}
// Get 发起带限流与重试的 GET,返回响应体与最终 HTTP 状态码。
//
// 重试策略:网络错误 / 5xx / 429 会按「线性退避」重试(第 n 次等 n*Backoff);
// 其余 4xx 视为客户端错误(如 403 被反爬、404 页面不存在),不重试,直接返回。
// 调用方拿到非 nil error 时即可判定最终失败,无需再自行区分网络错误与状态码。
func (c *Client) Get(ctx context.Context, url string) ([]byte, int, error) {
// 全局限速:每次请求前消费一个 tick(ticker 通道容量为 1,不会无限堆积)。
if c.tick != nil {
select {
case <-c.tick.C:
case <-ctx.Done():
return nil, 0, ctx.Err()
}
}
// 全局并发上限:超过则在此排队,保证总量受控。
select {
case c.sem <- struct{}{}:
defer func() { <-c.sem }()
case <-ctx.Done():
return nil, 0, ctx.Err()
}
var lastErr error
var lastStatus int
for attempt := 0; attempt <= c.retries; attempt++ {
if attempt > 0 {
delay := c.backoff * time.Duration(attempt)
log.Printf("[fetch] 第 %d/%d 次重试 %s(等待 %s)", attempt, c.retries, url, delay)
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, 0, ctx.Err()
}
}
body, status, err := c.do(ctx, url)
if err == nil && status == http.StatusOK {
return body, status, nil
}
lastErr, lastStatus = err, status
// 4xx(除 429 限流)不重试:重试也只会再次失败。
if status >= 400 && status < 500 && status != http.StatusTooManyRequests {
break
}
}
if lastErr == nil {
lastErr = fmt.Errorf("http %d", lastStatus)
}
return nil, lastStatus, lastErr
}
// do 执行单次请求。
func (c *Client) do(ctx context.Context, url string) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", acceptHeader)
resp, err := c.hc.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
return body, resp.StatusCode, nil
}

View File

@ -0,0 +1,128 @@
package fetch
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestGetSuccess(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
defer srv.Close()
c := New(Options{MaxConcurrency: 1, Retries: 0})
defer c.Close()
body, code, err := c.Get(context.Background(), srv.URL)
if err != nil || code != http.StatusOK || string(body) != "ok" {
t.Fatalf("期望 200/ok,得到 code=%d body=%q err=%v", code, body, err)
}
}
// TestGetRetriesOn5xx 5xx 应重试到上限后失败(首次 + 2 次重试 = 3 次请求)。
func TestGetRetriesOn5xx(t *testing.T) {
var calls int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
c := New(Options{MaxConcurrency: 1, Retries: 2, Backoff: time.Millisecond})
defer c.Close()
_, code, err := c.Get(context.Background(), srv.URL)
if err == nil {
t.Fatal("500 最终应返回 error")
}
if code != http.StatusInternalServerError {
t.Fatalf("最终状态码应为 500,得到 %d", code)
}
if got := atomic.LoadInt32(&calls); got != 3 {
t.Fatalf("500 应请求 3 次(首次+2 重试),实际 %d", got)
}
}
// TestGetNoRetryOn4xx 4xx(非 429)不应重试。
func TestGetNoRetryOn4xx(t *testing.T) {
var calls int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
c := New(Options{MaxConcurrency: 1, Retries: 3, Backoff: time.Millisecond})
defer c.Close()
_, code, err := c.Get(context.Background(), srv.URL)
if err == nil || code != http.StatusNotFound {
t.Fatalf("404 应报错且状态码 404,得到 code=%d err=%v", code, err)
}
if got := atomic.LoadInt32(&calls); got != 1 {
t.Fatalf("404 不应重试(只请求 1 次),实际 %d", got)
}
}
// TestGetRetriesOn429 429(限流)应重试,后续成功则返回成功。
func TestGetRetriesOn429(t *testing.T) {
var calls int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.AddInt32(&calls, 1) == 1 {
w.WriteHeader(http.StatusTooManyRequests)
return
}
_, _ = w.Write([]byte("ok"))
}))
defer srv.Close()
c := New(Options{MaxConcurrency: 1, Retries: 2, Backoff: time.Millisecond})
defer c.Close()
body, code, err := c.Get(context.Background(), srv.URL)
if err != nil || code != http.StatusOK || string(body) != "ok" {
t.Fatalf("429 后重试应成功,得到 code=%d body=%q err=%v", code, body, err)
}
}
// TestConcurrencyLimit 并发上限不得被突破(修复 MaxCo² 回归的守门测试)。
func TestConcurrencyLimit(t *testing.T) {
var inflight, peak int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := atomic.AddInt32(&inflight, 1)
for {
old := atomic.LoadInt32(&peak)
if n <= old || atomic.CompareAndSwapInt32(&peak, old, n) {
break
}
}
time.Sleep(20 * time.Millisecond)
atomic.AddInt32(&inflight, -1)
_, _ = w.Write([]byte("ok"))
}))
defer srv.Close()
const limit = 2
c := New(Options{MaxConcurrency: limit, Retries: 0})
defer c.Close()
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _, _ = c.Get(context.Background(), srv.URL)
}()
}
wg.Wait()
if m := atomic.LoadInt32(&peak); m > limit {
t.Fatalf("并发峰值不应超过 %d,实际 %d", limit, m)
}
}

147
internal/ingest/client.go Normal file
View File

@ -0,0 +1,147 @@
package ingest
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
// Client 向后台 ingest 接口发送 HMAC 签名上报的客户端。
type Client struct {
Endpoint string
Secret string
HTTPClient *http.Client
}
// NewClient 创建上报客户端。endpoint 形如 http://host:8092/admin/internal/ingest。
func NewClient(endpoint, secret string) *Client {
return &Client{
Endpoint: endpoint,
Secret: secret,
HTTPClient: &http.Client{Timeout: 30 * time.Second},
}
}
// Submit 把一条走秀上报签名后 POST 到后台,返回 job_id(202 入队即返回)。
// 失败返回 error(网络/校验/服务端拒绝),调用方应记日志并继续,不中断整轮抓取。
func (c *Client) Submit(ctx context.Context, p RunwayIngest) (uint32, error) {
if c == nil || c.Endpoint == "" {
return 0, fmt.Errorf("ingest client 未配置")
}
body, err := json.Marshal(p)
if err != nil {
return 0, err
}
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce, err := newNonce()
if err != nil {
return 0, err
}
sig := Sign(c.Secret, ts, nonce, string(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Endpoint, bytes.NewReader(body))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Signature", sig)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusAccepted {
return 0, fmt.Errorf("ingest http %d: %s", resp.StatusCode, string(raw))
}
var out struct {
JobID uint32 `json:"job_id"`
Status string `json:"status"`
}
if err := json.Unmarshal(raw, &out); err != nil {
// 202 但响应结构非预期:任务已入队,记日志后当作成功
log.Printf("[Ingest] 响应解析失败但已被接受: %s", string(raw))
return 0, nil
}
return out.JobID, nil
}
// newNonce 生成 16 字节随机十六进制串,作为一次性 X-Nonce 防重放。
func newNonce() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// CrawlBrand 后端抓取任务接口返回的单个品牌(brand_uid 已编码,name 为英文名)。
type CrawlBrand struct {
BrandUID string `json:"brand_uid"`
Name string `json:"name"`
}
// crawlBrandsURL 由 ingest endpoint 推导抓取任务接口地址(把末尾 /ingest 换成 /crawl/brands)。
func (c *Client) crawlBrandsURL() string {
base := c.Endpoint
if i := strings.LastIndex(base, "/ingest"); i != -1 {
base = base[:i]
}
return strings.TrimRight(base, "/") + "/crawl/brands"
}
// GetCrawlBrands 拉取爬虫要抓取的品牌任务列表(HMAC 签名 GET,复用与上报相同的验签算法)。
// brandID>0 时只取该品牌(对应 --brand 单品牌调试)。返回 brand_uid(直接上送 ingest)+ 英文名。
func (c *Client) GetCrawlBrands(ctx context.Context, brandID uint) ([]CrawlBrand, error) {
if c == nil || c.Endpoint == "" {
return nil, fmt.Errorf("ingest client 未配置")
}
url := c.crawlBrandsURL()
if brandID > 0 {
url += fmt.Sprintf("?brand=%d", brandID)
}
// GET 无请求体,签名串的 body 部分为空(与后台 IngestAuth 验签一致)。
ts := NowTS()
nonce, err := newNonce()
if err != nil {
return nil, err
}
sig := Sign(c.Secret, ts, nonce, "")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Signature", sig)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crawl brands http %d: %s", resp.StatusCode, string(raw))
}
var out struct {
Brands []CrawlBrand `json:"brands"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("crawl brands 解析失败: %w", err)
}
return out.Brands, nil
}

81
internal/ingest/hashid.go Normal file
View File

@ -0,0 +1,81 @@
package ingest
import (
"crypto/sha256"
"encoding/binary"
"strings"
)
// 以下 Feistel + base62 编码与后台 internal/pkg/hashid 完全一致,
// 仅实现编码侧(爬虫只需把品牌数字主键编码为 brand_uid 上送,无需解码)。
// 盐值必须 == 后台 HASHID_SECRET,否则生成的 brand_uid 后台解码不出正确主键。
const (
hidAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
hidBase = 62
hidMinLen = 8
)
var hidKeys [4]uint32
// InitHashID 用与后台 HASHID_SECRET 相同的盐值初始化编码密钥。
// secret 为空时回落后台内置默认盐(仅防顺序枚举,不安全)。
func InitHashID(secret string) {
if secret == "" {
secret = "fashion-archive-default-salt-change-me"
}
h := sha256.Sum256([]byte(secret))
for i := 0; i < 4; i++ {
hidKeys[i] = binary.BigEndian.Uint32(h[i*4 : i*4+4])
}
}
// hidFeistel 32-bit 平衡 Feistel 网络,encrypt=true 加密。
func hidFeistel(v uint32, encrypt bool) uint32 {
const rounds = 8
l, r := uint16(v>>16), uint16(v&0xffff)
for i := 0; i < rounds; i++ {
idx := i
if !encrypt {
idx = rounds - 1 - i
}
round := func(h uint16) uint16 {
f := uint32(h)*0x9E3779B1 + hidKeys[idx%4]
return uint16((f ^ (f >> 16)) & 0xffff)
}
if encrypt {
nl, nr := r, uint16(l)^round(r)
l, r = nl, nr
} else {
nl, nr := uint16(l)^round(l), l
l, r = nl, nr
}
}
return uint32(l)<<16 | uint32(r)
}
func encodeNum(n uint32) string {
x := hidFeistel(n, true)
var sb strings.Builder
for x > 0 {
sb.WriteByte(hidAlphabet[x%hidBase])
x /= hidBase
}
if sb.Len() == 0 {
sb.WriteByte(hidAlphabet[0])
}
runes := []rune(sb.String())
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
out := string(runes)
if len(out) < hidMinLen {
out = strings.Repeat("0", hidMinLen-len(out)) + out
}
return out
}
// EncodeBrand 把品牌数字主键编码为 hashid 串(与后台 brand_uid 同算法)。
func EncodeBrand(id uint32) string {
return encodeNum(id)
}

27
internal/ingest/hmac.go Normal file
View File

@ -0,0 +1,27 @@
package ingest
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
// Sign 与后台 internal/pkg/hmac.Sign 完全一致:
// HMAC_SHA256(secret, timestamp + "." + nonce + "." + body) 的十六进制串。
// bodyRaw 必须是请求体原始字节,确保与后台收到的字节严格一致。
func Sign(secret, timestamp, nonce, body string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write([]byte(nonce))
mac.Write([]byte("."))
mac.Write([]byte(body))
return hex.EncodeToString(mac.Sum(nil))
}
// NowTS 返回当前 Unix 秒字符串(X-Timestamp 用)。
func NowTS() string {
return strconv.FormatInt(time.Now().Unix(), 10)
}

View File

@ -0,0 +1,77 @@
package ingest
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"testing"
"time"
)
// verify 是后台 internal/pkg/hmac.Verify 的逐字镜像,仅用于本测试验证「爬虫签名能被
// 后台同算法校验通过」这条契约——算法必须与后台保持一致。
func verify(secret, body, sig, ts, nonce string, ttl int) bool {
if sig == "" || ts == "" || nonce == "" {
return false
}
if ttl <= 0 {
ttl = 300
}
tsN, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
if diff := now - tsN; diff > int64(ttl) || diff < -int64(ttl) {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write([]byte(nonce))
mac.Write([]byte("."))
mac.Write([]byte(body))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
func TestSignVerifyRoundtrip(t *testing.T) {
secret := "test-secret"
body := `{"brand_uid":"001DESke","title_en":"Fall 2024 Ready-to-Wear"}`
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "abc123nonce"
sig := Sign(secret, ts, nonce, body)
if !verify(secret, body, sig, ts, nonce, 300) {
t.Fatal("后台应当校验通过爬虫签名,但未通过")
}
// 篡改 body
if verify(secret, body+"x", sig, ts, nonce, 300) {
t.Fatal("body 被篡改后不应通过校验")
}
// 错误密钥
if verify("wrong", body, sig, ts, nonce, 300) {
t.Fatal("错误密钥不应通过校验")
}
// 过期时间戳
old := strconv.FormatInt(time.Now().Unix()-600, 10)
if verify(secret, body, Sign(secret, old, nonce, body), old, nonce, 300) {
t.Fatal("过期时间戳不应通过校验")
}
}
func TestEncodeBrandNonSequential(t *testing.T) {
// 相邻主键编码结果应互不相关(防顺序枚举),且同值稳定
a := EncodeBrand(108)
b := EncodeBrand(109)
if a == b {
t.Fatal("相邻 id 编码必须不同")
}
if EncodeBrand(108) != a {
t.Fatal("编码必须稳定(同输入同输出)")
}
if len(a) < 8 {
t.Fatalf("编码长度不应短于 minLen=8,实际 %d", len(a))
}
}

45
internal/ingest/parse.go Normal file
View File

@ -0,0 +1,45 @@
package ingest
import "strings"
// ParseCollection 从走秀标题解析出 collection_type 与 season。
//
// 仅输出后端白名单枚举(与 backend_v2/internal/pkg/season.Derive 一致):
// collection_type ∈ {rtw, menswear, couture, resort, pre_fall}
// season ∈ {spring, fall}(resort/pre_fall 无 spring/fall 词,season 留空,由 Derive 按 collection 前缀补码)
//
// 常见 Vogue 标题形态:
// "Fall 2024 Ready-to-Wear" → (rtw, fall)
// "Spring 2024 Menswear" → (menswear, spring)
// "Fall 2024 Couture" → (couture, fall)
// "Resort 2024" → (resort, "")
// "Pre-Fall 2024" → (pre_fall, "")
// "Cruise" 视作 resort 的同义表述(Vogue 两词混用);未识别到系列词时兜底为 rtw(成衣)。
func ParseCollection(title string) (collectionType, season string) {
t := strings.ToLower(title)
// season:仅 spring / fall 两种
switch {
case strings.Contains(t, "spring"):
season = "spring"
case strings.Contains(t, "fall"):
season = "fall"
}
// collection_type:按关键字优先级匹配,最后兜底 rtw
switch {
case strings.Contains(t, "menswear"), strings.Contains(t, "men's"):
collectionType = "menswear"
case strings.Contains(t, "couture"):
collectionType = "couture"
case strings.Contains(t, "resort"), strings.Contains(t, "cruise"):
collectionType = "resort"
case strings.Contains(t, "pre-fall"), strings.Contains(t, "pre fall"), strings.Contains(t, "prefall"):
collectionType = "pre_fall"
case strings.Contains(t, "ready-to-wear"), strings.Contains(t, "ready to wear"), strings.Contains(t, "rtw"):
collectionType = "rtw"
default:
collectionType = "rtw"
}
return collectionType, season
}

View File

@ -0,0 +1,74 @@
// Package ingest 爬虫侧入库客户端:把走秀元数据以 HMAC 签名 POST 到后台
// ingest 接口(:8092/admin/internal/ingest),替代原先直写 brand_runway 表。
//
// 与后台 internal/pkg/hmac、internal/pkg/hashid 保持同算法,使爬虫模块完全解耦
// 于后端代码(不再 import 后端 internal 包),双方仅通过「签名 + JSON 载荷」契约通信。
package ingest
import "strings"
// 入库类型(与后台 dto 保持一致)。
const (
KindRunway = "runway" // 走秀(默认,需 brand_uid)
KindStreet = "street" // 街拍(无品牌,需 city/title)
)
// RunwayIngest 与后台 dto.RunwayIngest 的 JSON 字段一一对应。
// 爬虫只传元数据 + 图片 URL 列表,图下载/OSS 由后台 worker 完成。
type RunwayIngest struct {
Kind string `json:"kind"` // runway | street,缺省 runway
BrandUID string `json:"brand_uid"` // 品牌 hashid(EncodeBrand 生成),runway 必填
TitleEn string `json:"title_en"` // 英文标题(优先;street 作为单标题)
TitleCn string `json:"title_cn"` // 中文标题(可空)
DescriptionEn string `json:"description_en"` // 英文描述(可空)
DescriptionCn string `json:"description_cn"` // 中文描述(可空)
Year uint16 `json:"year"` // 年份
Season string `json:"season"` // spring / fall
CollectionType string `json:"collection_type"` // rtw / menswear / couture / resort / pre_fall
City string `json:"city"` // 地区/城市(street 用)
Images []string `json:"images"` // 兼容旧 worker:铺平的主图 URL 列表
// Looks 结构化「主图 + 细节图」分组(Vogue 一个 gallery item = 1 主图 + N 细节图)。
// 结构化上报时优先用 Looks;为空时 worker 回退到 Images(全部视为主图)。
Looks []RunwayLook `json:"looks,omitempty"`
}
// RunwayLook 一场秀中的一个 look:1 张主图 + 0..N 张细节图。
type RunwayLook struct {
Main string `json:"main"` // 主图(look)原始 URL
Details []string `json:"details"` // 细节图原始 URL 列表
}
// ParseCollection 从 Vogue 标题推导 collection_type 与 season,供后台补 season_code。
//
// "Fall 2024 Ready-to-Wear" -> ("rtw", "fall")
// "Spring 2025 Menswear" -> ("menswear", "spring")
// "Resort 2024" -> ("resort", "")
// "Pre-Fall 2024" -> ("pre_fall", "")
// "Couture Fall 2024" -> ("couture", "fall")
func ParseCollection(title string) (collectionType, season string) {
t := strings.ToLower(title)
switch {
case strings.Contains(t, "resort"):
collectionType = "resort"
case strings.Contains(t, "pre-fall"), strings.Contains(t, "pre fall"):
collectionType = "pre_fall"
case strings.Contains(t, "couture"):
collectionType = "couture"
case strings.Contains(t, "menswear"), strings.Contains(t, "men's"):
collectionType = "menswear"
case strings.Contains(t, "ready"):
collectionType = "rtw"
default:
// 仅出现 spring/fall 而无明确品类词时,回落为 rtw(绝大多数季场秀)
if strings.Contains(t, "spring") || strings.Contains(t, "fall") {
collectionType = "rtw"
}
}
switch {
case strings.Contains(t, "spring"):
season = "spring"
case strings.Contains(t, "fall"):
season = "fall"
}
return collectionType, season
}

View File

@ -1,79 +0,0 @@
package model
// AppBrand 品牌数据表模型
type AppBrand struct {
ID int64 `gorm:"primaryKey;column:id"`
Name string `gorm:"column:name"`
SpiderOrigin string `gorm:"column:spider_origin"`
}
func (AppBrand) TableName() string {
return "app_brands" // 根据实际表名调整
}
// BrandRunway 时尚发布会主表
type BrandRunway struct {
ID uint `gorm:"primaryKey;column:id;autoIncrement" json:"id"`
Title string `gorm:"column:title" json:"title"`
Description string `gorm:"column:description" json:"description"`
CreatedAt int64 `gorm:"column:created_at" json:"created_at"`
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"`
IsDeleted uint8 `gorm:"column:is_deleted" json:"is_deleted"`
ImageCount uint16 `gorm:"column:image_count" json:"image_count"`
BrandID uint `gorm:"column:brand_id" json:"brand_id"`
Year uint16 `gorm:"column:year" json:"year"`
Cover string `gorm:"column:cover" json:"cover"`
SourceURL string `gorm:"column:source_url" json:"source_url"`
CollectionType string `gorm:"column:collection_type" json:"collection_type"`
Season string `gorm:"column:season" json:"season"`
SeasonCode string `gorm:"column:season_code" json:"season_code"`
}
// TableName 指定主表名
func (BrandRunway) TableName() string {
return "brand_runway"
}
// BrandRunwayImage 发布会图片表
type BrandRunwayImage struct {
ID uint `gorm:"primaryKey;column:id;autoIncrement" json:"id"`
CreatedAt int64 `gorm:"column:created_at" json:"created_at"`
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"`
IsDeleted uint8 `gorm:"column:is_deleted" json:"is_deleted"`
Image string `gorm:"column:image" json:"image"`
RunwayID uint `gorm:"column:runway_id" json:"runway_id"`
BrandID uint `gorm:"column:brand_id" json:"brand_id"`
Name string `gorm:"column:name" json:"name"`
SortOrder uint `gorm:"column:sort_order" json:"sort_order"`
}
// TableName 指定图片明细表名
func (BrandRunwayImage) TableName() string {
return "brand_runway_images"
}
// Article The Impression 街拍文章表(对应 PHP 的 articles 模型)
// 字段与 PHP 的 ArticleModuleEnum::STREET 文章结构保持一致
type Article struct {
ID uint `gorm:"primaryKey;column:id;autoIncrement" json:"id"`
Title string `gorm:"column:title" json:"title"`
Platform string `gorm:"column:platform" json:"platform"`
Module string `gorm:"column:module" json:"module"`
Year uint16 `gorm:"column:year" json:"year"`
Cover string `gorm:"column:cover" json:"cover"`
Images string `gorm:"column:images" json:"images"`
SourceURL string `gorm:"column:source_url" json:"source_url"`
Brand uint `gorm:"column:brand;default:0" json:"brand"`
CreatedAt int64 `gorm:"column:created_at" json:"created_at"`
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"`
}
// TableName 指定文章主表名
func (Article) TableName() string {
return "articles"
}
// ImageItem 图片条目,序列化进 Article.Images(JSON 数组)
type ImageItem struct {
Src string `json:"src"`
}

7
internal/spider/regex.go Normal file
View File

@ -0,0 +1,7 @@
package spider
import "regexp"
// yearRe 预编译的 4 位年份正则,供 vogue / theimpression 共用
// (预编译避免在每次解析时重新编译)。
var yearRe = regexp.MustCompile(`\b(19|20)\d{2}\b`)

View File

@ -1,21 +1,19 @@
package spider
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"gorm.io/gorm"
"my-spiders/internal/model"
"my-spiders/internal/fetch"
"my-spiders/internal/ingest"
)
const (
@ -28,23 +26,27 @@ const (
// TheImpressionSpider The Impression 街拍采集器(对应 PHP 的 TheImpressionStreetCommand)
type TheImpressionSpider struct {
Client *http.Client
DB *gorm.DB
MaxCo int // 最大并发数
Debug bool // Debug 模式:仅输出不入库
ForceUpdate bool // 强制更新已存在的记录
Fetcher *fetch.Client // 带全局并发限流 + 重试的 HTTP 客户端
Ingest *ingest.Client // 入库管线客户端(nil = 无法入库,仅 Debug 输出)
MaxCo int // 最大并发数
MaxArticles int // 小批量模式:最多抓取的街拍任务数(0=不限制)
Debug bool // Debug 模式:仅输出不入库
}
func NewTheImpressionSpider(db *gorm.DB, maxCo int) *TheImpressionSpider {
func NewTheImpressionSpider(maxCo int, ingestClient *ingest.Client) *TheImpressionSpider {
if maxCo <= 0 {
maxCo = 5
}
return &TheImpressionSpider{
DB: db,
MaxCo: maxCo,
Client: &http.Client{
Timeout: 30 * time.Second,
},
Ingest: ingestClient,
MaxCo: maxCo,
// 全局并发上限 = maxCo;叠加 200ms 最小间隔做全局限速(替代原先的 time.Sleep)。
Fetcher: fetch.New(fetch.Options{
MaxConcurrency: maxCo,
MinInterval: 200 * time.Millisecond,
Retries: fetch.DefaultRetries,
Backoff: time.Second,
}),
}
}
@ -60,13 +62,13 @@ func (s *TheImpressionSpider) Run() {
// 1. 抓取 street-style 首页 banner(.parallax .mask-overlay)
bannerURL := TheImpressionBaseURL + "/street-style"
body, httpCode := s.request(bannerURL)
if httpCode != 200 || body == "" {
log.Printf("[错误] %s 请求失败, code: %d", bannerURL, httpCode)
bannerBody, bannerCode, bannerErr := s.Fetcher.Get(context.Background(), bannerURL)
if bannerErr != nil || bannerCode != 200 || len(bannerBody) == 0 {
log.Printf("[错误] %s 请求失败, code: %d, err: %v", bannerURL, bannerCode, bannerErr)
return
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(bannerBody)))
if err != nil {
log.Printf("[错误] 解析 %s 失败: %v", bannerURL, err)
return
@ -90,15 +92,15 @@ func (s *TheImpressionSpider) Run() {
"%s/wp-json/codetipi-zeen/v1/block?paged=%d&type=1&data%%5Bargs%%5D%%5Bcat%%5D=1",
TheImpressionBaseURL, i,
)
body, httpCode := s.request(listURL)
if httpCode != 200 || body == "" {
log.Printf("[错误] %s 请求失败, code: %d", listURL, httpCode)
listBody, listCode, listErr := s.Fetcher.Get(context.Background(), listURL)
if listErr != nil || listCode != 200 || len(listBody) == 0 {
log.Printf("[错误] %s 请求失败, code: %d, err: %v", listURL, listCode, listErr)
return
}
// 接口返回的是 JSON 数组,第 2 个元素(index 1)是包含文章的 HTML 片段
var arr []interface{}
if err := json.Unmarshal([]byte(body), &arr); err != nil {
if err := json.Unmarshal(listBody, &arr); err != nil {
log.Printf("[错误] %s JSON 解析失败: %v", listURL, err)
continue
}
@ -129,28 +131,29 @@ func (s *TheImpressionSpider) Run() {
})
}
// 小批量模式:仅抓取前 N 个街拍(用于试抓 / 控制入库量)。
if s.MaxArticles > 0 && len(tasks) > s.MaxArticles {
log.Printf("[Info] 小批量模式:仅抓取前 %d / %d 个街拍", s.MaxArticles, len(tasks))
tasks = tasks[:s.MaxArticles]
}
log.Printf("[Info] 共收集到 %d 个待抓取任务,开始执行...", len(tasks))
// 3. 并发抓取详情页
sem := make(chan struct{}, s.MaxCo)
// 3. 并发抓取详情页:不再自建信号量,HTTP 并发与频控统一由 s.Fetcher 承担。
var wg sync.WaitGroup
for _, t := range tasks {
sem <- struct{}{}
wg.Add(1)
go func(tk streetTask) {
defer func() {
<-sem
wg.Done()
}()
defer wg.Done()
s.getDetail(tk)
time.Sleep(200 * time.Millisecond) // 频控,防封 IP
}(t)
}
wg.Wait()
log.Println("[Info] The Impression 所有抓取任务已完成!")
}
// getDetail 抓取并解析单个文章详情(对应 PHP 的 getDetail)
// getDetail 抓取并解析单个街拍详情(对应 PHP 的 getDetail),通过入库管线上报至后台审核。
// 判重与入库(street_snap / street_snap_draft)全部交由后台 worker 处理,本爬虫不直连数据库。
func (s *TheImpressionSpider) getDetail(tk streetTask) {
url := tk.URL
title := tk.Title
@ -158,128 +161,106 @@ func (s *TheImpressionSpider) getDetail(tk streetTask) {
return
}
// 判重:按 title + platform 查询是否已存在
var exist model.Article
if !s.Debug && s.DB != nil {
s.DB.Where("title = ? AND platform = ?", title, TheImpressionPlatform).First(&exist)
if exist.ID > 0 && !s.ForceUpdate {
return
}
}
body, httpCode := s.request(url)
if httpCode != 200 || body == "" {
log.Printf("[错误] %s 请求失败, code: %d", url, httpCode)
body, code, err := s.Fetcher.Get(context.Background(), url)
if err != nil || code != 200 || len(body) == 0 {
log.Printf("[错误] %s 请求失败, code: %d, err: %v", url, code, err)
return
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
if err != nil {
log.Printf("[错误] 解析 %s 失败: %v", url, err)
return
}
// 采集 figure a img 的图片(去重)
imagesMap := make(map[string]model.ImageItem)
doc.Find("figure a img").Each(func(_ int, node *goquery.Selection) {
// 采集文章正文 figure 内图片(去重,按 DOM 出现顺序保序)。
// 注意:theimpression.com 当前结构为 <figure class="wp-block-image"><img>,
// 图片并不包裹在 <a> 内,故选择器须用 "figure img" 而非旧版的 "figure a img";
// 同时兜底读取 data-src(部分懒加载主题把真图地址放在 data-src)。
seen := make(map[string]struct{})
var imageURLs []string
doc.Find("figure img").Each(func(_ int, node *goquery.Selection) {
src, _ := node.Attr("src")
if src != "" {
if _, ok := imagesMap[src]; !ok {
log.Printf("采集图片: %s", src)
imagesMap[src] = model.ImageItem{Src: src}
}
if src == "" {
src, _ = node.Attr("data-src")
}
if src == "" {
return
}
// 过滤掉占位/1px 透明图与空 srcset 噪点
if strings.Contains(src, "data:image") || strings.HasSuffix(src, "1x1.gif") {
return
}
if _, ok := seen[src]; ok {
return
}
seen[src] = struct{}{}
log.Printf("采集图片: %s", src)
imageURLs = append(imageURLs, src)
})
if len(imagesMap) == 0 {
if len(imageURLs) == 0 {
log.Printf("[Warning] %s 未采集到图片,跳过保存.", url)
return
}
cover := ""
var imageList []model.ImageItem
for _, v := range imagesMap {
if cover == "" {
cover = v.Src
}
imageList = append(imageList, v)
}
year := s.parseYear(title)
city := s.parseCity(title)
imagesJSON, _ := json.Marshal(imageList)
now := time.Now().Unix()
article := model.Article{
Title: title,
Platform: TheImpressionPlatform,
Module: TheImpressionModule,
Year: uint16(s.parseYear(title)),
Cover: cover,
Images: string(imagesJSON),
SourceURL: url,
Brand: 0,
CreatedAt: now,
UpdatedAt: now,
}
// Debug 模式或无数据库连接时,仅控制台输出
if s.Debug || s.DB == nil {
out, _ := json.MarshalIndent(article, "", " ")
log.Printf("\n================ [DEBUG OUTPUT] ================\n%s\n提取图片总数: %d 张\n================================================",
string(out), len(imageList))
// Debug 模式:仅控制台输出,不上报
if s.Debug {
log.Printf("[DEBUG] 街拍: %s | year=%d | city=%s | url=%s | images=%d",
title, year, city, url, len(imageURLs))
return
}
// 入库(存在则更新,不存在则新增)
err = s.DB.Transaction(func(tx *gorm.DB) error {
if exist.ID > 0 {
article.ID = exist.ID
if err := tx.Save(&article).Error; err != nil {
return err
}
} else {
if err := tx.Create(&article).Error; err != nil {
return err
}
}
return nil
})
// 入库分支:启用入库管线(s.Ingest != nil)时,把街拍元数据 HMAC 签名 POST 到后台
// ingest,由后台 worker 异步下载图、传七牛、写 street_snap_draft 待审;通过后才晋升
// street_snap 正式表(图片统一存七牛云)。判重全交后台 worker(按实体键 city+year 幂等)。
if s.Ingest == nil {
log.Printf("[Warning] 入库管线未启用 (Ingest==nil),无法上报街拍: %s", title)
return
}
if err != nil {
log.Printf("[错误] 数据入库失败 [%s]: %v", title, err)
p := ingest.RunwayIngest{
Kind: ingest.KindStreet,
TitleEn: title,
Year: uint16(year),
City: city,
Images: imageURLs,
}
if _, err := s.Ingest.Submit(context.Background(), p); err != nil {
log.Printf("[错误] 街拍入库上报失败 [%s]: %v", title, err)
} else {
log.Printf("[成功] 保存文章: %s (共 %d 张图片)", title, len(imageList))
log.Printf("[成功] 已上报街拍待入库: %s (共 %d 张图片)", title, len(imageURLs))
}
}
// request HTTP 请求封装(带浏览器 UA)
func (s *TheImpressionSpider) request(url string) (string, int) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", 0
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
resp, err := s.Client.Do(req)
if err != nil {
return "", 0
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", resp.StatusCode
}
return string(bodyBytes), resp.StatusCode
}
// parseYear 从标题中提取 4 位年份数字(对应 PHP 的 AppHelper::getYear)
// parseYear 从标题中提取 4 位年份(对应 PHP 的 AppHelper::getYear)。
// 提取不到时返回 0(未知年份),不再回退到「当前年份」——那会把旧街拍伪造成今年。
func (s *TheImpressionSpider) parseYear(title string) int {
re := regexp.MustCompile(`\b(19|20)\d{2}\b`)
match := re.FindString(title)
if match != "" {
y, _ := strconv.Atoi(match)
if m := yearRe.FindString(title); m != "" {
y, _ := strconv.Atoi(m)
return y
}
return time.Now().Year()
log.Printf("[Warning] 标题未含年份,按未知(0)处理: %s", title)
return 0
}
// streetCities 常见时装周城市(用于从街拍标题中粗提 city 字段)。
var streetCities = []string{
"New York", "London", "Milan", "Paris", "Florence", "Berlin",
"Tokyo", "Seoul", "Shanghai", "Beijing", "Copenhagen", "Stockholm",
"Sydney", "Madrid", "Barcelona", "Amsterdam", "São Paulo", "Sao Paulo",
}
// parseCity 从街拍标题中粗略识别城市名(命中时装周城市则返回,否则空串)。
func (s *TheImpressionSpider) parseCity(title string) string {
t := strings.ToLower(title)
for _, city := range streetCities {
if strings.Contains(t, strings.ToLower(city)) {
return city
}
}
return ""
}

View File

@ -1,10 +1,8 @@
package spider
import (
"encoding/json"
"io"
"context"
"log"
"net/http"
"regexp"
"strconv"
"strings"
@ -12,9 +10,9 @@ import (
"time"
"github.com/tidwall/gjson"
"gorm.io/gorm"
"my-spiders/internal/model"
"my-spiders/internal/fetch"
"my-spiders/internal/ingest"
)
const (
@ -22,28 +20,41 @@ const (
VoguePlatform = "vogue"
)
// 预编译正则(避免每次调用都重新编译)。
// 列表页与详情页的内联 state 结束标记不同,故保留两条,勿轻易合并。
var (
showsStateRe = regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);</script>`)
detailStateRe = regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);<`)
)
type VogueSpider struct {
Client *http.Client
DB *gorm.DB
MaxCo int // 最大并发数
BrandID uint // 客户端指定的品牌ID
OnlyPlatform bool // 是否仅抓取该平台关联的品牌
Debug bool // 供 s.Debug 使用
ForceUpdate bool // 供 s.ForceUpdate 使用
Fetcher *fetch.Client // 带全局并发限流 + 重试的 HTTP 客户端
Ingest *ingest.Client // 入库管线客户端(nil = 离线/Mock 模式)
MaxCo int // 最大并发数
BrandID uint // 客户端指定的品牌 ID(--brand,单品牌调试用)
MaxShows int // 小批量模式:单品牌最多抓取的发布会场次(0=不限制)
Debug bool // Debug 模式:仅输出不入库
}
func NewVogueSpider(db *gorm.DB, maxCo int, brandID uint, onlyPlatform bool) *VogueSpider {
// NewVogueSpider 创建 Vogue 爬虫。spider 不再直连数据库:品牌任务经 ingest 接口从后端拉取,
// 抓取结果经 ingest 上报后端,由后端异步下载图、写正式表。spider 是纯 HTTP 客户端。
func NewVogueSpider(maxCo int, brandID uint, maxShows int, ingestClient *ingest.Client) *VogueSpider {
if maxCo <= 0 {
maxCo = 5 // 默认限制 5 个并发,防止打满带宽
}
return &VogueSpider{
DB: db,
MaxCo: maxCo,
BrandID: brandID,
OnlyPlatform: onlyPlatform,
Client: &http.Client{
Timeout: 30 * time.Second,
},
Ingest: ingestClient,
MaxCo: maxCo,
BrandID: brandID,
MaxShows: maxShows,
// 全局并发上限 = maxCo(单一信号量,避免「品牌级 × 详情级」叠加成 maxCo²);
// 再叠加 200ms 最小间隔做全局限速,替代原先散落在 goroutine 里的 time.Sleep。
Fetcher: fetch.New(fetch.Options{
MaxConcurrency: maxCo,
MinInterval: 200 * time.Millisecond,
Retries: fetch.DefaultRetries,
Backoff: time.Second,
}),
}
}
@ -57,7 +68,8 @@ func (s *VogueSpider) Run() {
log.Printf("[Info] 成功获取 %d 个品牌任务,开始执行...", len(tasks))
// 品牌级别的并发控制
// 品牌级并发:仅限制「同时在跑的品牌 goroutine 数」,避免一次性对巨量品牌开 goroutine;
// 真正的 HTTP 并发由 Fetcher 的全局信号量统一控制(修正此前两层信号量叠加成 MaxCo² 的问题)。
brandSem := make(chan struct{}, s.MaxCo)
var wg sync.WaitGroup
@ -65,7 +77,7 @@ func (s *VogueSpider) Run() {
brandSem <- struct{}{}
wg.Add(1)
go func(t model.AppBrand) {
go func(t ingest.CrawlBrand) {
defer func() {
<-brandSem
wg.Done()
@ -78,41 +90,44 @@ func (s *VogueSpider) Run() {
log.Println("[Info] Vogue 所有抓取任务已完成!")
}
// getTask 获取需要抓取的品牌任务
func (s *VogueSpider) getTask() ([]model.AppBrand, error) {
// 1. 本地无数据库时的 Mock 调试场景
if s.DB == nil {
log.Println("[Debug] 当前未连接数据库,启动 Mock 任务数据模式")
brandID := s.BrandID
if brandID == 0 {
brandID = 108 // 默认给一个 Chanel 示例 ID
// getTask 从后端 ingest 接口拉取要抓取的品牌任务(不再直连数据库读 brand 表)。
// 无 ingest 客户端时退化为 Mock(给一个示例品牌),便于完全离线调试。
func (s *VogueSpider) getTask() ([]ingest.CrawlBrand, error) {
if s.Ingest == nil {
log.Println("[Debug] 未配置入库客户端,启动 Mock 任务数据模式")
id := s.BrandID
if id == 0 {
id = 108 // 默认给一个 Chanel 示例 ID
}
return []model.AppBrand{
{ID: int64(brandID), Name: "Alexander McQueen", SpiderOrigin: VoguePlatform},
return []ingest.CrawlBrand{
{BrandUID: ingest.EncodeBrand(uint32(id)), Name: "Alexander McQueen"},
}, nil
}
// 2. 有数据库时的查询逻辑
var brands []model.AppBrand
query := s.DB.Model(&model.AppBrand{})
if s.BrandID > 0 {
query = query.Where("id = ?", s.BrandID)
} else {
query = query.Where("id > ?", 1)
if s.OnlyPlatform {
query = query.Where("spider_origin = ?", VoguePlatform)
}
query = query.Order("id asc")
brands, err := s.Ingest.GetCrawlBrands(context.Background(), s.BrandID)
if err != nil {
return nil, err
}
return brands, nil
}
err := query.Find(&brands).Error
return brands, err
// sourceURL 由列表项拼出图集详情页地址(供抓取详情页用;不再用于去重/上报)。
func (s *VogueSpider) sourceURL(info gjson.Result) string {
return VogueBaseURL + info.Get("url").String() + "/slideshow/collection"
}
// filterUncrawled 返回待抓取的图集列表(原样返回)。
//
// 历史上这里会调用后端 ExistsSourceURLs 预检、在抓取前跳过已爬图集以省流量;
// 现 source_url 已从 ingestion 管线移除,后端按「实体键(品牌+季节码+系列)」去重,
// 预检既不可行也不必要——重复抓取由 worker 在晋升阶段幂等收敛,不会重复建行。
// 因此这里直接返回完整列表,去重交给后端。
func (s *VogueSpider) filterUncrawled(showsList []gjson.Result) []gjson.Result {
return showsList
}
// SpiderStart 针对单个品牌开启抓取
func (s *VogueSpider) SpiderStart(task model.AppBrand) {
func (s *VogueSpider) SpiderStart(task ingest.CrawlBrand) {
brandName := s.getTaskName(task.Name)
url := VogueBaseURL + "/fashion-shows/designer/" + brandName
log.Printf("[Command] brandName: %s; spiderUrl: %s", brandName, url)
@ -123,24 +138,30 @@ func (s *VogueSpider) SpiderStart(task model.AppBrand) {
return
}
// 核心修复:限制该品牌下发布会详情页的抓取并发数
detailSem := make(chan struct{}, s.MaxCo)
// 抓取前不再做预检(source_url 预检已下线);直接抓取,去重由后端 worker 按实体键幂等收敛。
showsList = s.filterUncrawled(showsList)
if len(showsList) == 0 {
log.Printf("[Info] 品牌 [%s] 的图集均已爬取过,全部跳过", brandName)
return
}
// 小批量模式:仅抓取前 N 场发布会(用于试爬 / 控制入库量)。
// 可多次递增 --max-shows 追加抓取;已晋升的秀由后端按实体键(品牌+季节码+系列)幂等收敛,不会重复建行。
if s.MaxShows > 0 && len(showsList) > s.MaxShows {
log.Printf("[Info] 小批量模式:仅抓取前 %d / %d 场发布会", s.MaxShows, len(showsList))
showsList = showsList[:s.MaxShows]
}
// 详情页并发抓取:这里不再自建信号量,HTTP 并发与频控统一由 s.Fetcher 承担
// (避免与品牌级信号量叠加导致并发放大)。
var wg sync.WaitGroup
for _, list := range showsList {
detailSem <- struct{}{} // 超过 MaxCo 时会自动卡住等待
wg.Add(1)
go func(info gjson.Result) {
defer func() {
<-detailSem // 释放槽位
wg.Done()
}()
s.getDetail(task.ID, info)
// 频控:每次下载后稍作停顿,保护带宽并防封 IP
time.Sleep(200 * time.Millisecond)
defer wg.Done()
s.getDetail(task.BrandUID, info)
}(list)
}
@ -149,172 +170,114 @@ func (s *VogueSpider) SpiderStart(task model.AppBrand) {
// getShowsList 获取品牌的发布会列表 JSON 数据
func (s *VogueSpider) getShowsList(url string) []gjson.Result {
body, httpCode := s.request(url)
if httpCode == 200 && body != "" {
re := regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);</script>`)
matches := re.FindStringSubmatch(body)
if len(matches) > 1 {
collections := gjson.Get(matches[1], "transformed.runwayDesignerContent.designerCollections")
return collections.Array()
}
body, code, err := s.Fetcher.Get(context.Background(), url)
if err != nil {
log.Printf("[错误] 获取发布会列表失败 %s: %v", url, err)
return nil
}
if code != 200 || len(body) == 0 {
log.Printf("[Info] %s 返回异常, code: %d", url, code)
return nil
}
if m := showsStateRe.FindSubmatch(body); len(m) > 1 {
collections := gjson.GetBytes(m[1], "transformed.runwayDesignerContent.designerCollections")
return collections.Array()
}
log.Printf("[Info] %s 未找到数据.", url)
return nil
}
// getDetail 获取并解析单个发布会详情
func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) {
hed := info.Get("hed").String()
var exist model.BrandRunway
// 如果处于非 Debug 模式,进行数据库查询判重
if !s.Debug && s.DB != nil {
s.DB.Where("brand_id = ? AND title = ?", brandID, hed).First(&exist)
if exist.ID > 0 && !s.ForceUpdate {
return
}
}
// 如果非 forceUpdate 且记录已存在,跳过更新
if exist.ID > 0 && !s.ForceUpdate {
return
}
// 判重交由后端 worker 处理(按实体键幂等去重),spider 不再直连 brand_runway 判重。
// 获取图片(保持你原本的 URL 拼接与请求逻辑)
pageURI := info.Get("url").String()
requestURL := VogueBaseURL + pageURI + "/slideshow/collection"
requestURL := s.sourceURL(info)
log.Printf("正在匹配发布会详情 %s", requestURL)
body, httpCode := s.request(requestURL)
if httpCode != 200 || body == "" {
log.Printf("[Warning] %s 请求失败.", requestURL)
body, code, err := s.Fetcher.Get(context.Background(), requestURL)
if err != nil {
log.Printf("[Warning] %s 请求失败: %v", requestURL, err)
return
}
if code != 200 || len(body) == 0 {
log.Printf("[Warning] %s 返回异常, code: %d", requestURL, code)
return
}
re := regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);<`)
matches := re.FindStringSubmatch(body)
matches := detailStateRe.FindSubmatch(body)
if len(matches) <= 1 {
return
}
imagesResult := gjson.Get(matches[1], "transformed.runwayGalleries.galleries.0.items")
imagesResult := gjson.GetBytes(matches[1], "transformed.runwayGalleries.galleries.0.items")
if !imagesResult.Exists() {
log.Printf("[Warning] %s 获取图片失败.", requestURL)
return
}
type ImageItem struct {
Src string `json:"src"`
}
// 按 Vogue 原生结构逐 look 提取:每个 gallery item = 1 张主图(image.sources.xxl.url)
// + 0..N 张细节图(details[].image.sources.xxl.url)。主图与细节图在此建立父子关系,
// 不再像旧逻辑那样把所有图扁平混排(那样会丢失「细节图属于哪张主图」)。
var looks []ingest.RunwayLook
// 兼容旧 worker / 兜底:把主图铺平进 Images(无细节分组)。
var mainImages []string
var saveURL []ImageItem
var detailURL []ImageItem
// 保持你原有的图片与细节图提取逻辑
for _, img := range imagesResult.Array() {
xxlURL := img.Get("image.sources.xxl.url").String()
if xxlURL != "" {
saveURL = append(saveURL, ImageItem{Src: xxlURL})
log.Printf("%s", xxlURL)
mainURL := img.Get("image.sources.xxl.url").String()
if mainURL == "" {
continue
}
mainImages = append(mainImages, mainURL)
// 详情图片提取
look := ingest.RunwayLook{Main: mainURL}
for _, detail := range img.Get("details").Array() {
detailXxlURL := detail.Get("image.sources.xxl.url").String()
if detailXxlURL != "" {
detailURL = append(detailURL, ImageItem{Src: detailXxlURL})
look.Details = append(look.Details, detailXxlURL)
}
}
log.Printf("[look] 主图 %s 附 %d 张细节图", mainURL, len(look.Details))
looks = append(looks, look)
}
// ---------------- 下面对接你的双表数据库结构 ----------------
// 1. 整理图片与封面
cover := ""
if len(saveURL) > 0 {
cover = saveURL[0].Src
totalImages := len(mainImages)
for _, l := range looks {
totalImages += len(l.Details)
}
// 汇总所有图片(主图 + 细节图)准备存入 brand_runway_images 表
var allImages []string
for _, item := range saveURL {
allImages = append(allImages, item.Src)
}
for _, item := range detailURL {
allImages = append(allImages, item.Src)
}
now := time.Now().Unix()
// 2. 构建主表 BrandRunway 结构体
runway := model.BrandRunway{
Title: hed,
BrandID: uint(brandID),
Year: uint16(s.parseYear(hed)),
Cover: cover,
SourceURL: requestURL,
ImageCount: uint16(len(allImages)),
CreatedAt: now,
UpdatedAt: now,
}
// 3. Debug 模式或无数据库连接时控制台输出
if s.Debug || s.DB == nil {
out, _ := json.MarshalIndent(runway, "", " ")
log.Printf("\n================ [DEBUG OUTPUT] ================\n%s\n提取图片总数: %d 张\n================================================", string(out), len(allImages))
// Debug 模式:仅控制台输出,不上报
if s.Debug {
log.Printf("\n================ [DEBUG OUTPUT] ================\n标题: %s\n抓取 URL: %s\n主图(look)数: %d 张 / 含细节共 %d 张\n================================================", hed, requestURL, len(looks), totalImages)
return
}
// 4. 执行数据库事务保存(主表 brand_runway + 从表 brand_runway_images)
err := s.DB.Transaction(func(tx *gorm.DB) error {
// 判断是更新还是新增
if exist.ID > 0 {
runway.ID = exist.ID
if err := tx.Save(&runway).Error; err != nil {
return err
}
// 删除旧关联图片
if err := tx.Where("runway_id = ?", exist.ID).Delete(&model.BrandRunwayImage{}).Error; err != nil {
return err
}
// 入库分支:把走秀元数据 HMAC 签名 POST 到后台 ingest,由后台 worker 异步下载图、
// 补 season_code、写正式表(替代直写 brand_runway)。brand_uid 直接来自任务接口。
if s.Ingest != nil {
collectionType, season := ingest.ParseCollection(hed)
p := ingest.RunwayIngest{
Kind: ingest.KindRunway,
BrandUID: brandUID,
TitleEn: hed,
Year: uint16(s.parseYear(hed)),
Season: season,
CollectionType: collectionType,
// Looks 优先:结构化承载「主图 + 细节图」分组;Images 仅铺平主图做兼容兜底。
Looks: looks,
Images: mainImages,
}
if _, err := s.Ingest.Submit(context.Background(), p); err != nil {
log.Printf("[错误] 入库上报失败 [%s]: %v", hed, err)
} else {
if err := tx.Create(&runway).Error; err != nil {
return err
}
log.Printf("[成功] 已上报待入库: %s (共 %d 个 look / %d 张图)", hed, len(looks), totalImages)
}
// 批量插入图片从表
var runwayImages []model.BrandRunwayImage
for idx, imgURL := range allImages {
runwayImages = append(runwayImages, model.BrandRunwayImage{
RunwayID: runway.ID, // 获取上面生成的自增 ID
BrandID: uint(brandID),
Image: imgURL,
SortOrder: uint(idx + 1),
CreatedAt: now,
UpdatedAt: now,
})
}
if len(runwayImages) > 0 {
if err := tx.Create(&runwayImages).Error; err != nil {
return err
}
}
return nil
})
if err != nil {
log.Printf("[错误] 数据入库失败 [%s]: %v", hed, err)
} else {
log.Printf("[成功] 保存发布会: %s (共 %d 张图片)", hed, len(allImages))
return
}
}
// getTaskName 品牌名称格式化(对应 getTaskName)
@ -324,36 +287,14 @@ func (s *VogueSpider) getTaskName(name string) string {
return strings.ToLower(r.Replace(name))
}
// HTTP 请求封装
func (s *VogueSpider) request(url string) (string, int) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", 0
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
resp, err := s.Client.Do(req)
if err != nil {
return "", 0
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", resp.StatusCode
}
return string(bodyBytes), resp.StatusCode
}
// parseYear 从标题中提取 4 位年份数字(如 Fall 2024 Ready-to-Wear -> 2024)
// parseYear 从标题中提取 4 位年份(如 Fall 2024 Ready-to-Wear -> 2024)。
// 提取不到时返回 0(未知年份),交由后端 season.Derive 处理(year=0 → season_code 为空);
// 绝不回退到「当前年份」——那会把旧秀伪造成今年,污染排序与去重。
func (s *VogueSpider) parseYear(title string) int {
re := regexp.MustCompile(`\b(19|20)\d{2}\b`)
match := re.FindString(title)
if match != "" {
y, _ := strconv.Atoi(match)
if m := yearRe.FindString(title); m != "" {
y, _ := strconv.Atoi(m)
return y
}
return time.Now().Year()
log.Printf("[Warning] 标题未含年份,按未知(0)处理: %s", title)
return 0
}

BIN
spider

Binary file not shown.

7
spider_batch.log Normal file
View File

@ -0,0 +1,7 @@
2026/09/11 20:20:20 [Info] 入库管线已启用 -> http://localhost:8092/admin/internal/ingest
2026/09/11 20:20:20 [Info] 成功获取 1 个品牌任务,开始执行...
2026/09/11 20:20:20 [Debug] 原始品牌名: Acne Studios; 格式化后: acne-studios
2026/09/11 20:20:20 [Command] brandName: acne-studios; spiderUrl: https://www.vogue.com/fashion-shows/designer/acne-studios
2026/09/11 20:20:23 [Info] 预检命中 82/82 个已爬取图集,跳过;待抓取 0 个
2026/09/11 20:20:23 [Info] 品牌 [acne-studios] 的图集均已爬取过,全部跳过
2026/09/11 20:20:23 [Info] Vogue 所有抓取任务已完成!

300
spider_gucci.log Normal file
View File

@ -0,0 +1,300 @@
2026/09/11 22:31:49 [Info] 入库管线已启用 -> http://localhost:8092/admin/internal/ingest
2026/09/11 22:31:49 [Info] 成功获取 1 个品牌任务,开始执行...
2026/09/11 22:31:49 [Debug] 原始品牌名: Gucci; 格式化后: gucci
2026/09/11 22:31:49 [Command] brandName: gucci; spiderUrl: https://www.vogue.com/fashion-shows/designer/gucci
2026/09/11 22:31:52 [Info] 小批量模式:仅抓取前 5 / 132 场发布会
2026/09/11 22:31:52 正在匹配发布会详情 https://www.vogue.com/fashion-shows/resort-2026/gucci/slideshow/collection
2026/09/11 22:31:52 正在匹配发布会详情 https://www.vogue.com/fashion-shows/fall-2026-ready-to-wear/gucci/slideshow/collection
2026/09/11 22:31:52 正在匹配发布会详情 https://www.vogue.com/fashion-shows/pre-fall-2026/gucci/slideshow/collection
2026/09/11 22:31:52 正在匹配发布会详情 https://www.vogue.com/fashion-shows/spring-2026-ready-to-wear/gucci/slideshow/collection
2026/09/11 22:31:52 正在匹配发布会详情 https://www.vogue.com/fashion-shows/resort-2027/gucci/slideshow/collection
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a0691752b1abbae6f2ec0/master/w_2560,c_limit/00001-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06916ed9876b786d0129/master/w_2560,c_limit/00002-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a0691dc15e1429d4cbe32/master/w_2560,c_limit/00003-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069417f832ce597658b5/master/w_2560,c_limit/00004-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a0695b9af6b72617d951d/master/w_2560,c_limit/00005-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06966ed9876b786d012f/master/w_2560,c_limit/00006-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06979bc95159c66b944e/master/w_2560,c_limit/00007-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a0698c73846e15adc725d/master/w_2560,c_limit/00008-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a0699711a23d1067ee9c7/master/w_2560,c_limit/00009-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069a2e1ccd0c8ce443f5/master/w_2560,c_limit/00010-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069b90e50c9dbb0acdb7/master/w_2560,c_limit/00011-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069c2e1ccd0c8ce443fb/master/w_2560,c_limit/00012-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069d4b2600279313e563/master/w_2560,c_limit/00013-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069e68044e9311f15e4f/master/w_2560,c_limit/00014-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a069fc73846e15adc7268/master/w_2560,c_limit/00015-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a0b9af6b72617d952b/master/w_2560,c_limit/00016-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a14951d452e0fbb3d4/master/w_2560,c_limit/00017-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a27062b9037631f4f2/master/w_2560,c_limit/00018-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a3b9af6b72617d9531/master/w_2560,c_limit/00019-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a49b03d4f210f1055c/master/w_2560,c_limit/00020-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a6f788445d7d512c44/master/w_2560,c_limit/00021-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a9c73846e15adc7271/master/w_2560,c_limit/00022-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06a82e1ccd0c8ce44403/master/w_2560,c_limit/00023-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06aa794b317cd75ee532/master/w_2560,c_limit/00024-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06ab68044e9311f15e55/master/w_2560,c_limit/00025-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06ad90e50c9dbb0acdbd/master/w_2560,c_limit/00026-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06ad7062b9037631f4f8/master/w_2560,c_limit/00027-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06ae9c325bba91fadf83/master/w_2560,c_limit/00028-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b0b9af6b72617d9537/master/w_2560,c_limit/00029-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b02e1ccd0c8ce4440a/master/w_2560,c_limit/00030-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b2794b317cd75ee53d/master/w_2560,c_limit/00031-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b4a232ba599921c1e0/master/w_2560,c_limit/00032-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b4752b1abbae6f2ed0/master/w_2560,c_limit/00033-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b52e1ccd0c8ce44410/master/w_2560,c_limit/00034-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b9f788445d7d512c52/master/w_2560,c_limit/00035-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06b941f668f57ef49467/master/w_2560,c_limit/00036-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06bbeda24b02d657c202/master/w_2560,c_limit/00037-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06bd2e1ccd0c8ce4441b/master/w_2560,c_limit/00038-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06bdc73846e15adc7280/master/w_2560,c_limit/00039-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06bf4b2600279313e56c/master/w_2560,c_limit/00040-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c2794b317cd75ee543/master/w_2560,c_limit/00041-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c14b2600279313e572/master/w_2560,c_limit/00042-gucci-resort-2027-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c359fff5e945637285/master/w_2560,c_limit/00043-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c468044e9311f15e60/master/w_2560,c_limit/00044-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c89b03d4f210f1057a/master/w_2560,c_limit/00045-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c69b03d4f210f10574/master/w_2560,c_limit/00046-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06c84b2600279313e578/master/w_2560,c_limit/00047-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06cab5b90ecd3cca014a/master/w_2560,c_limit/00048-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06cb4133fd6ba6df6104/master/w_2560,c_limit/00049-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06cc29abd45c193a1f13/master/w_2560,c_limit/00050-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06cd711a23d1067ee9dc/master/w_2560,c_limit/00051-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06ce794b317cd75ee551/master/w_2560,c_limit/00052-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d0752b1abbae6f2ed6/master/w_2560,c_limit/00053-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d26ed9876b786d0138/master/w_2560,c_limit/00054-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d22e1ccd0c8ce44426/master/w_2560,c_limit/00055-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d56ed9876b786d013e/master/w_2560,c_limit/00056-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d69c325bba91fadf91/master/w_2560,c_limit/00057-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d8a232ba599921c1e6/master/w_2560,c_limit/00058-gucci-resort-2027-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d8b5b90ecd3cca0150/master/w_2560,c_limit/00059-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06d929abd45c193a1f1c/master/w_2560,c_limit/00060-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06db68044e9311f15e6e/master/w_2560,c_limit/00061-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06deb5b90ecd3cca0156/master/w_2560,c_limit/00062-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [look] 主图 https://assets.vogue.com/photos/6a0a06dd59fff5e945637298/master/w_2560,c_limit/00063-gucci-resort-2027-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:53 [成功] 已上报待入库: Resort 2027 (共 63 个 look / 134 张图)
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29868b125d7500f38fa/master/w_2560,c_limit/00001-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29840e2eb8fb8bccd5c/master/w_2560,c_limit/00002-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2990536c0e58428ec57/master/w_2560,c_limit/00003-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29bec9e56390d3c0343/master/w_2560,c_limit/00004-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29d12fc8087a4b68a5c/master/w_2560,c_limit/00005-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29d187bff3f8a3480ee/master/w_2560,c_limit/00006-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f29fb978bd780db879bc/master/w_2560,c_limit/00007-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a13cdcc32ad45be22b/master/w_2560,c_limit/00008-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a18acfb60be7c39369/master/w_2560,c_limit/00009-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a2bc21d805bf329ada/master/w_2560,c_limit/00010-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a460f25b7e62d4c774/master/w_2560,c_limit/00011-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a616f926d4a2c164b5/master/w_2560,c_limit/00012-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a5b2d17b44b610cbde/master/w_2560,c_limit/00013-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a768b125d7500f38fc/master/w_2560,c_limit/00014-gucci-resort-2026-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a968b125d7500f38ff/master/w_2560,c_limit/00015-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2a9187bff3f8a3480f0/master/w_2560,c_limit/00016-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2acee3c01f48b80a774/master/w_2560,c_limit/00017-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2acd0d2baf7b176c7a3/master/w_2560,c_limit/00018-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2adb978bd780db879be/master/w_2560,c_limit/00019-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b02b79d44243efd1d4/master/w_2560,c_limit/00020-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b08acfb60be7c3936b/master/w_2560,c_limit/00021-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b112fc8087a4b68a5e/master/w_2560,c_limit/00022-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b48acfb60be7c3936d/master/w_2560,c_limit/00023-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b420306ddb341ea06e/master/w_2560,c_limit/00024-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b4211e2d9cc1fdbea3/master/w_2560,c_limit/00025-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b840e5bfe1996d16a7/master/w_2560,c_limit/00026-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b868b125d7500f3902/master/w_2560,c_limit/00027-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2b8bc21d805bf329adc/master/w_2560,c_limit/00028-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2bc24deb2471c247651/master/w_2560,c_limit/00029-gucci-resort-2026-credit-brand.jpg 附 4 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2bcab36f20de7af8494/master/w_2560,c_limit/00030-gucci-resort-2026-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2bd8acfb60be7c39370/master/w_2560,c_limit/00031-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2bf211e2d9cc1fdbea6/master/w_2560,c_limit/00032-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c0b2d17b44b610cbe1/master/w_2560,c_limit/00033-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c03650fb81757bca54/master/w_2560,c_limit/00034-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c268b125d7500f3904/master/w_2560,c_limit/00035-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c3b307fa7bf6d4e579/master/w_2560,c_limit/00036-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c46e92acc911d0cfdb/master/w_2560,c_limit/00037-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c512fc8087a4b68a60/master/w_2560,c_limit/00038-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c73650fb81757bca56/master/w_2560,c_limit/00039-gucci-resort-2026-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c8d4b2ea7c11de8806/master/w_2560,c_limit/00040-gucci-resort-2026-credit-brand.jpg 附 3 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2c9d4b2ea7c11de8808/master/w_2560,c_limit/00041-gucci-resort-2026-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/6826f2ca272f917535482261/master/w_2560,c_limit/00042-gucci-resort-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [成功] 已上报待入库: Resort 2026 (共 42 个 look / 127 张图)
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d36ae530252d0b9b77/master/w_2560,c_limit/00001-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d275300db6e628e0dd/master/w_2560,c_limit/00002-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d2d34aaf50ee468686/master/w_2560,c_limit/00003-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d7fffa071ef1933589/master/w_2560,c_limit/00004-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d842e63b8b62766929/master/w_2560,c_limit/00005-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8d8c6655877971ab0f7/master/w_2560,c_limit/00006-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8dbe86285b2183efd47/master/w_2560,c_limit/00007-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8dd113b585837febe90/master/w_2560,c_limit/00008-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8ddc6655877971ab0fd/master/w_2560,c_limit/00009-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8df162e29636a2cb982/master/w_2560,c_limit/00010-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e18bd473891a512fd0/master/w_2560,c_limit/00011-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e2d08063c920b9ed2c/master/w_2560,c_limit/00012-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e38bd473891a512fd7/master/w_2560,c_limit/00013-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e53fde453b99cbb31f/master/w_2560,c_limit/00014-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e65fee983aa5123289/master/w_2560,c_limit/00015-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8e79184b92f4dc52e07/master/w_2560,c_limit/00016-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8eb57966e4abfc21805/master/w_2560,c_limit/00017-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8eb3a8f05b89fdd7c6a/master/w_2560,c_limit/00018-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8eb113b585837febe96/master/w_2560,c_limit/00019-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8ee6ddeb3513db02afa/master/w_2560,c_limit/00020-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f0fffa071ef193358f/master/w_2560,c_limit/00021-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f0ea5539abc092d495/master/w_2560,c_limit/00022-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f2e86285b2183efd55/master/w_2560,c_limit/00023-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f442e63b8b62766934/master/w_2560,c_limit/00024-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f5fdeb03b5ba7ec72e/master/w_2560,c_limit/00025-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f6162e29636a2cb988/master/w_2560,c_limit/00026-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8f805db6c5acc229603/master/w_2560,c_limit/00027-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8fac6655877971ab10d/master/w_2560,c_limit/00028-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8fa3fde453b99cbb332/master/w_2560,c_limit/00029-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8fc113b585837febe9c/master/w_2560,c_limit/00030-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8ff3a8f05b89fdd7c70/master/w_2560,c_limit/00031-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e8fe75300db6e628e0e6/master/w_2560,c_limit/00032-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e900fdeb03b5ba7ec737/master/w_2560,c_limit/00033-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90242e63b8b6276693a/master/w_2560,c_limit/00034-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9045fee983aa5123292/master/w_2560,c_limit/00035-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e904d08063c920b9ed35/master/w_2560,c_limit/00036-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90519b858d5d66632c9/master/w_2560,c_limit/00037-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e907e0c3eaf5b25c089f/master/w_2560,c_limit/00038-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9098bd473891a512fe3/master/w_2560,c_limit/00039-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90a848a39800b805135/master/w_2560,c_limit/00040-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90c6ddeb3513db02b05/master/w_2560,c_limit/00041-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90e113b585837febea2/master/w_2560,c_limit/00042-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e90ee0c3eaf5b25c08a5/master/w_2560,c_limit/00043-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91017a5c0e47915fe34/master/w_2560,c_limit/00044-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e912848a39800b805140/master/w_2560,c_limit/00045-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91357966e4abfc21813/master/w_2560,c_limit/00046-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9141c150ac4fa847796/master/w_2560,c_limit/00047-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9166ddeb3513db02b0b/master/w_2560,c_limit/00048-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9179184b92f4dc52e16/master/w_2560,c_limit/00049-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91875300db6e628e0ec/master/w_2560,c_limit/00050-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91a8bd473891a512fee/master/w_2560,c_limit/00051-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91b1c150ac4fa84779c/master/w_2560,c_limit/00052-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91c42e63b8b62766940/master/w_2560,c_limit/00053-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91d113b585837febea8/master/w_2560,c_limit/00054-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e91f1c150ac4fa8477a2/master/w_2560,c_limit/00055-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e920fdeb03b5ba7ec74e/master/w_2560,c_limit/00056-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9219184b92f4dc52e1c/master/w_2560,c_limit/00057-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e923d08063c920b9ed45/master/w_2560,c_limit/00058-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e923d08063c920b9ed4b/master/w_2560,c_limit/00059-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e925d34aaf50ee468691/master/w_2560,c_limit/00060-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9265f1f1b829bc6bcce/master/w_2560,c_limit/00061-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9273a8f05b89fdd7c81/master/w_2560,c_limit/00062-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9293a8f05b89fdd7c87/master/w_2560,c_limit/00063-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e92a113b585837febeae/master/w_2560,c_limit/00064-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e92b05db6c5acc229612/master/w_2560,c_limit/00065-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e92dea5539abc092d4a5/master/w_2560,c_limit/00066-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e995c6655877971ab13e/master/w_2560,c_limit/00067-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e92ffdeb03b5ba7ec759/master/w_2560,c_limit/00068-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e933c6655877971ab125/master/w_2560,c_limit/00069-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e933fffa071ef193359d/master/w_2560,c_limit/00070-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9343fde453b99cbb343/master/w_2560,c_limit/00071-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 2 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e93819b858d5d66632dc/master/w_2560,c_limit/00072-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e93957966e4abfc2181f/master/w_2560,c_limit/00073-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9385fee983aa51232b0/master/w_2560,c_limit/00074-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e93c848a39800b805156/master/w_2560,c_limit/00075-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e93e75300db6e628e0fa/master/w_2560,c_limit/00076-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e93ed34aaf50ee4686a1/master/w_2560,c_limit/00077-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9b37fe60fdf8f629f89/master/w_2560,c_limit/00078-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9438bd473891a512ffa/master/w_2560,c_limit/00079-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e943848a39800b80515c/master/w_2560,c_limit/00080-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e948848a39800b805167/master/w_2560,c_limit/00081-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e948c6655877971ab130/master/w_2560,c_limit/00082-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:54 [look] 主图 https://assets.vogue.com/photos/69a6e9d1fffa071ef19335a6/master/w_2560,c_limit/00083-gucci-fall-2026-ready-to-wear-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:54 [成功] 已上报待入库: Fall 2026 Ready-to-Wear (共 83 个 look / 141 张图)
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb474a4ee5f94bd2271a/master/w_2560,c_limit/00001-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4788b582cdf7c3aff3/master/w_2560,c_limit/00002-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb46f7a2897adc1b49fe/master/w_2560,c_limit/00003-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb49f79c6e9267cef5ac/master/w_2560,c_limit/00004-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4a554ccac7d1e1f4b0/master/w_2560,c_limit/00005-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4a32c2e2d3bf98b10d/master/w_2560,c_limit/00006-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4d2dd0f37bf34746a9/master/w_2560,c_limit/00007-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4df4dd14a320411132/master/w_2560,c_limit/00008-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4d0bfa06b1dea5d12b/master/w_2560,c_limit/00009-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb4f43b90a2074d6181e/master/w_2560,c_limit/00010-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb51b06f3238a60b0225/master/w_2560,c_limit/00011-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb51f79c6e9267cef5ae/master/w_2560,c_limit/00012-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb52db59dc04636d13dd/master/w_2560,c_limit/00013-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb54752b433b219c544e/master/w_2560,c_limit/00014-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb54752b433b219c544f/master/w_2560,c_limit/00015-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb55b06f3238a60b0227/master/w_2560,c_limit/00016-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb576607436cdc76efb3/master/w_2560,c_limit/00017-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb57008045d04b846a0b/master/w_2560,c_limit/00018-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb58f4dd14a320411134/master/w_2560,c_limit/00019-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5a9378bbf43e259849/master/w_2560,c_limit/00020-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5c32c2e2d3bf98b110/master/w_2560,c_limit/00021-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5bbd4a8b9260dcf286/master/w_2560,c_limit/00022-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5ca226facb3a9f1175/master/w_2560,c_limit/00023-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5e88b582cdf7c3aff5/master/w_2560,c_limit/00024-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5f9378bbf43e25984c/master/w_2560,c_limit/00025-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb5f752b433b219c5451/master/w_2560,c_limit/00026-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb61a226facb3a9f1178/master/w_2560,c_limit/00027-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb63329a0224809638dd/master/w_2560,c_limit/00028-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb63f7a2897adc1b4a00/master/w_2560,c_limit/00029-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6343b90a2074d61820/master/w_2560,c_limit/00030-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb669378bbf43e25984e/master/w_2560,c_limit/00031-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb66f7a2897adc1b4a02/master/w_2560,c_limit/00032-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6743b90a2074d61822/master/w_2560,c_limit/00033-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb69dd32dc180f6b3561/master/w_2560,c_limit/00034-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6af79c6e9267cef5b0/master/w_2560,c_limit/00035-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6b176c2cf4dce76b8c/master/w_2560,c_limit/00036-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6ca0026000fda92320/master/w_2560,c_limit/00037-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/68d0eb6ca226facb3a9f117a/master/w_2560,c_limit/00038-gucci-spring-2026-ready-to-wear-credit-brand.jpg 附 0 张细节图
2026/09/11 22:31:55 [成功] 已上报待入库: Spring 2026 Ready-to-Wear (共 38 个 look / 38 张图)
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4adc46c427d039bde2/master/w_2560,c_limit/00001-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4af740689c42c1c802/master/w_2560,c_limit/00002-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4b5e97b8363f04de82/master/w_2560,c_limit/00003-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4d313e8fffafe175c8/master/w_2560,c_limit/00004-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4d78b18aebb7357852/master/w_2560,c_limit/00005-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b4e438959cabcb3b738/master/w_2560,c_limit/00006-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b50990c447c7554d0b0/master/w_2560,c_limit/00007-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b51438959cabcb3b73a/master/w_2560,c_limit/00008-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b51dc46c427d039bde4/master/w_2560,c_limit/00009-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b531c2b6ff4b63b38a7/master/w_2560,c_limit/00010-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b551810535ae4b59741/master/w_2560,c_limit/00011-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5524a46ebebe8e4fb9/master/w_2560,c_limit/00012-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b56180867a056740f58/master/w_2560,c_limit/00013-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b58f25c7fb049e2f391/master/w_2560,c_limit/00014-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b59234aad71fd57d1ff/master/w_2560,c_limit/00015-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b598d8b5b664f5f26e5/master/w_2560,c_limit/00016-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5b180867a056740f5a/master/w_2560,c_limit/00017-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5c1c2b6ff4b63b38a9/master/w_2560,c_limit/00018-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5da22c6f62ceacf159/master/w_2560,c_limit/00019-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5e504fbe9c49ee94a2/master/w_2560,c_limit/00020-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b5ff740689c42c1c806/master/w_2560,c_limit/00021-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b60234aad71fd57d202/master/w_2560,c_limit/00022-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b61e591e2b2f98159f5/master/w_2560,c_limit/00023-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b62313e8fffafe175ca/master/w_2560,c_limit/00024-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b632a6d46e3815fa1af/master/w_2560,c_limit/00025-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b655e97b8363f04de86/master/w_2560,c_limit/00026-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b65ebdcca4392e74a9d/master/w_2560,c_limit/00027-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b66bbc3962e1a82e882/master/w_2560,c_limit/00028-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b68180867a056740f5d/master/w_2560,c_limit/00029-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6824a46ebebe8e4fbb/master/w_2560,c_limit/00030-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b691810535ae4b59743/master/w_2560,c_limit/00031-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6bf740689c42c1c809/master/w_2560,c_limit/00032-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6b18715b0da0d8f6ba/master/w_2560,c_limit/00033-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6c75aa28c41b11352e/master/w_2560,c_limit/00034-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6e78b18aebb7357854/master/w_2560,c_limit/00035-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6ebbc3962e1a82e884/master/w_2560,c_limit/00036-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b6f78b18aebb7357856/master/w_2560,c_limit/00037-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b728d8b5b664f5f26e7/master/w_2560,c_limit/00038-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7175aa28c41b113531/master/w_2560,c_limit/00039-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b71bbc3962e1a82e886/master/w_2560,c_limit/00040-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b75f740689c42c1c80b/master/w_2560,c_limit/00041-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b75c1c3c6b90dcb55ad/master/w_2560,c_limit/00042-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b75313e8fffafe175cc/master/w_2560,c_limit/00043-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b77438959cabcb3b73e/master/w_2560,c_limit/00044-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b782a6d46e3815fa1b1/master/w_2560,c_limit/00045-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b79a22c6f62ceacf15c/master/w_2560,c_limit/00046-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7af25c7fb049e2f393/master/w_2560,c_limit/00047-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305bb754a4b7d4e38430d4/master/w_2560,c_limit/00048-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7cebdcca4392e74a9f/master/w_2560,c_limit/00049-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7d180867a056740f5f/master/w_2560,c_limit/00050-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7ec1c3c6b90dcb55af/master/w_2560,c_limit/00051-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b7f438959cabcb3b740/master/w_2560,c_limit/00052-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b8175aa28c41b113533/master/w_2560,c_limit/00053-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b815e97b8363f04de89/master/w_2560,c_limit/00054-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b82da9a25400a479980/master/w_2560,c_limit/00055-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b84f2904f35b853d113/master/w_2560,c_limit/00056-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b842a6d46e3815fa1b3/master/w_2560,c_limit/00057-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [look] 主图 https://assets.vogue.com/photos/69305b85f25c7fb049e2f395/master/w_2560,c_limit/00058-gucci-pre-fall-2026-credit-brand.jpg 附 1 张细节图
2026/09/11 22:31:55 [成功] 已上报待入库: Pre-Fall 2026 (共 58 个 look / 116 张图)
2026/09/11 22:31:55 [Info] Vogue 所有抓取任务已完成!

2810
spider_run.log Normal file

File diff suppressed because it is too large Load Diff