update
This commit is contained in:
@ -5,8 +5,6 @@
|
||||
// 于后端代码(不再 import 后端 internal 包),双方仅通过「签名 + JSON 载荷」契约通信。
|
||||
package ingest
|
||||
|
||||
import "strings"
|
||||
|
||||
// 入库类型(与后台 dto 保持一致)。
|
||||
const (
|
||||
KindRunway = "runway" // 走秀(默认,需 brand_uid)
|
||||
@ -26,6 +24,7 @@ type RunwayIngest struct {
|
||||
Season string `json:"season"` // spring / fall
|
||||
CollectionType string `json:"collection_type"` // rtw / menswear / couture / resort / pre_fall
|
||||
City string `json:"city"` // 地区/城市(street 用)
|
||||
PublishedAt uint32 `json:"published_at"` // Vogue 详情页 datePublished 的 Unix 时间戳(秒);0=未抽取。标题缺年份时用于矫正 year
|
||||
Images []string `json:"images"` // 兼容旧 worker:铺平的主图 URL 列表
|
||||
// Looks 结构化「主图 + 细节图」分组(Vogue 一个 gallery item = 1 主图 + N 细节图)。
|
||||
// 结构化上报时优先用 Looks;为空时 worker 回退到 Images(全部视为主图)。
|
||||
@ -38,37 +37,4 @@ type RunwayLook struct {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,12 @@ const (
|
||||
var (
|
||||
showsStateRe = regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);</script>`)
|
||||
detailStateRe = regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);<`)
|
||||
// ldJsonRe 匹配详情页里的 <script type="application/ld+json">…</script> 块,
|
||||
// 用于抽取 Article 的 datePublished(Vogue 的发布时间,比标题解析的年份更可靠)。
|
||||
ldJsonRe = regexp.MustCompile(`(?s)<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>`)
|
||||
// publishDateRe 兜底:不少 Vogue 详情页没有 JSON-LD,发布时间只内联在
|
||||
// window.__PRELOADED_STATE__ 的 "publishDate":"<ISO>" 里,用正则兜底抽取。
|
||||
publishDateRe = regexp.MustCompile(`"publishDate"\s*:\s*"([^"]+)"`)
|
||||
)
|
||||
|
||||
type VogueSpider struct {
|
||||
@ -212,6 +218,10 @@ func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) {
|
||||
return
|
||||
}
|
||||
|
||||
// 抽取发布时间(Vogue 详情页 application/ld+json 的 datePublished)。
|
||||
// 缺失或解析失败时返回零值,后端据此跳过(不写空时间)。
|
||||
publishedAt := s.parsePublishedAt(body)
|
||||
|
||||
imagesResult := gjson.GetBytes(matches[1], "transformed.runwayGalleries.galleries.0.items")
|
||||
|
||||
if !imagesResult.Exists() {
|
||||
@ -251,7 +261,7 @@ func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) {
|
||||
|
||||
// Debug 模式:仅控制台输出,不上报
|
||||
if s.Debug {
|
||||
log.Printf("\n================ [DEBUG OUTPUT] ================\n标题: %s\n抓取 URL: %s\n主图(look)数: %d 张 / 含细节共 %d 张\n================================================", hed, requestURL, len(looks), totalImages)
|
||||
log.Printf("\n================ [DEBUG OUTPUT] ================\n标题: %s\n抓取 URL: %s\n主图(look)数: %d 张 / 含细节共 %d 张\n发布时间: %s\n================================================", hed, requestURL, len(looks), totalImages, publishedAt)
|
||||
return
|
||||
}
|
||||
|
||||
@ -266,6 +276,7 @@ func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) {
|
||||
Year: uint16(s.parseYear(hed)),
|
||||
Season: season,
|
||||
CollectionType: collectionType,
|
||||
PublishedAt: publishEpoch(publishedAt),
|
||||
// Looks 优先:结构化承载「主图 + 细节图」分组;Images 仅铺平主图做兼容兜底。
|
||||
Looks: looks,
|
||||
Images: mainImages,
|
||||
@ -273,13 +284,54 @@ func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) {
|
||||
if _, err := s.Ingest.Submit(context.Background(), p); err != nil {
|
||||
log.Printf("[错误] 入库上报失败 [%s]: %v", hed, err)
|
||||
} else {
|
||||
log.Printf("[成功] 已上报待入库: %s (共 %d 个 look / %d 张图)", hed, len(looks), totalImages)
|
||||
log.Printf("[成功] 已上报待入库: %s (共 %d 个 look / %d 张图, 发布时间 %s)", hed, len(looks), totalImages, publishedAt)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parseVogueDate 容错解析 Vogue 的日期字符串:兼容完整 ISO 时间戳(含/不含时区、含小数秒)
|
||||
// 与「仅日期」格式(如 2015-08-08,老页面 publishDate 仅有日期)。解析失败返回零值。
|
||||
func parseVogueDate(s string) (time.Time, bool) {
|
||||
for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05Z07:00", "2006-01-02"} {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t.UTC(), true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// parsePublishedAt 从详情页 HTML 抽取 Vogue 的发布时间(文章发布时间,比标题解析的年份更可靠)。
|
||||
// 优先用 application/ld+json 的 datePublished;部分页面无 JSON-LD,则兜底从内联
|
||||
// "publishDate":"<ISO>"(window.__PRELOADED_STATE__ 等)抽取。解析为 UTC 时间返回(失败返回零值)。
|
||||
func (s *VogueSpider) parsePublishedAt(body []byte) time.Time {
|
||||
// 1) 优先 application/ld+json 的 datePublished(部分页面有)。
|
||||
for _, m := range ldJsonRe.FindAllSubmatch(body, -1) {
|
||||
block := string(m[1])
|
||||
if dp := gjson.Get(block, "datePublished"); dp.Exists() {
|
||||
if t, ok := parseVogueDate(dp.String()); ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2) 兜底:内联 JSON 里的 "publishDate":"<ISO 或 仅日期>"。
|
||||
if m := publishDateRe.FindSubmatch(body); m != nil {
|
||||
if t, ok := parseVogueDate(string(m[1])); ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// publishEpoch 把发布时间转成 Unix 时间戳(秒);零值(未抽取到)→ 0(与 created_at 约定一致)。
|
||||
func publishEpoch(t time.Time) uint32 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return uint32(t.Unix())
|
||||
}
|
||||
|
||||
// getTaskName 品牌名称格式化(对应 getTaskName)
|
||||
func (s *VogueSpider) getTaskName(name string) string {
|
||||
r := strings.NewReplacer(".", "-", " ", "-", "&", "")
|
||||
|
||||
Reference in New Issue
Block a user