update
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
10
.idea/go.imports.xml
generated
Normal file
10
.idea/go.imports.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoImports">
|
||||||
|
<option name="excludedPackages">
|
||||||
|
<array>
|
||||||
|
<option value="golang.org/x/net/context" />
|
||||||
|
</array>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/spider.iml" filepath="$PROJECT_DIR$/.idea/spider.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/spider.iml
generated
Normal file
9
.idea/spider.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
30
cmd/vogue.go
30
cmd/vogue.go
@ -1,44 +1,36 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"my-spiders/internal/config"
|
|
||||||
"my-spiders/internal/spider"
|
"my-spiders/internal/spider"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
brandID int64
|
brandID int64
|
||||||
forceUpdate bool
|
|
||||||
onlyPlatform bool
|
onlyPlatform bool
|
||||||
|
maxCo int
|
||||||
)
|
)
|
||||||
|
|
||||||
var vogueCmd = &cobra.Command{
|
var vogueCmd = &cobra.Command{
|
||||||
Use: "vogue",
|
Use: "vogue",
|
||||||
Short: "自动采集 vogue.com 网站发布会数据",
|
Short: "启动 Vogue 时装发布会采集器",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
log.Println("[Command] 启动 Vogue 采集指令...")
|
// 1. 修复参数数量 & 类型强转 uint(brandID)
|
||||||
|
vogueSpider := spider.NewVogueSpider(DB, maxCo, uint(brandID), onlyPlatform)
|
||||||
|
|
||||||
// 从全局 YAML 配置中获取并发数
|
// 2. 修复方法名:将 Execute() 改为 Run()
|
||||||
maxCo := config.GlobalConfig.App.MaxCo
|
vogueSpider.Run()
|
||||||
|
|
||||||
vogueSpider := spider.NewVogueSpider(DB, maxCo)
|
|
||||||
vogueSpider.BrandID = brandID
|
|
||||||
vogueSpider.ForceUpdate = forceUpdate
|
|
||||||
vogueSpider.OnlyPlatform = onlyPlatform
|
|
||||||
vogueSpider.Debug = config.GlobalConfig.App.Debug // 赋值 Debug 状态
|
|
||||||
|
|
||||||
vogueSpider.Execute()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(vogueCmd)
|
rootCmd.AddCommand(vogueCmd)
|
||||||
|
|
||||||
vogueCmd.Flags().Int64VarP(&brandID, "brandId", "b", 0, "指定的品牌id.")
|
// 绑定命令行参数
|
||||||
vogueCmd.Flags().BoolVarP(&forceUpdate, "forceUpdate", "f", false, "是否对已经保存的数据进行强制更新.")
|
vogueCmd.Flags().Int64VarP(&brandID, "brand", "b", 0, "指定抓取的品牌 ID")
|
||||||
vogueCmd.Flags().BoolVarP(&onlyPlatform, "onlyPlatform", "o", false, "是否只对当前平台品牌更新.")
|
vogueCmd.Flags().BoolVarP(&onlyPlatform, "only-platform", "p", false, "是否仅抓取平台关联品牌")
|
||||||
|
|
||||||
|
// 修复:将 "c" 改为 "m"(或者 ""),避免与全局的 -c 参数冲突
|
||||||
|
vogueCmd.Flags().IntVarP(&maxCo, "max-co", "m", 5, "最大并发数限制")
|
||||||
}
|
}
|
||||||
@ -11,19 +11,43 @@ func (AppBrand) TableName() string {
|
|||||||
return "app_brands" // 根据实际表名调整
|
return "app_brands" // 根据实际表名调整
|
||||||
}
|
}
|
||||||
|
|
||||||
// Article 文章/发布会数据表模型
|
// BrandRunway 时尚发布会主表
|
||||||
type Article struct {
|
type BrandRunway struct {
|
||||||
ID int64 `gorm:"primaryKey;column:id"`
|
ID uint `gorm:"primaryKey;column:id;autoIncrement" json:"id"`
|
||||||
Title string `gorm:"column:title"`
|
Title string `gorm:"column:title" json:"title"`
|
||||||
Images string `gorm:"column:images"`
|
Description string `gorm:"column:description" json:"description"`
|
||||||
Platform string `gorm:"column:platform"`
|
CreatedAt int64 `gorm:"column:created_at" json:"created_at"`
|
||||||
Brand int64 `gorm:"column:brand"`
|
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"`
|
||||||
Module int `gorm:"column:module"`
|
IsDeleted uint8 `gorm:"column:is_deleted" json:"is_deleted"`
|
||||||
Year int `gorm:"column:year"`
|
ImageCount uint16 `gorm:"column:image_count" json:"image_count"`
|
||||||
SourceURL string `gorm:"column:source_url"`
|
BrandID uint `gorm:"column:brand_id" json:"brand_id"`
|
||||||
Cover string `gorm:"column:cover"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Article) TableName() string {
|
// TableName 指定主表名
|
||||||
return "articles" // 根据实际表名调整
|
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"
|
||||||
}
|
}
|
||||||
@ -23,79 +23,77 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type VogueSpider struct {
|
type VogueSpider struct {
|
||||||
DB *gorm.DB
|
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
MaxCo int
|
DB *gorm.DB
|
||||||
BrandID int64
|
MaxCo int // 最大并发数
|
||||||
ForceUpdate bool
|
BrandID uint // 客户端指定的品牌ID
|
||||||
OnlyPlatform bool
|
OnlyPlatform bool // 是否仅抓取该平台关联的品牌
|
||||||
Debug bool // 新增 Debug 标志
|
Debug bool // 供 s.Debug 使用
|
||||||
|
ForceUpdate bool // 供 s.ForceUpdate 使用
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVogueSpider(db *gorm.DB, maxCo int) *VogueSpider {
|
func NewVogueSpider(db *gorm.DB, maxCo int, brandID uint, onlyPlatform bool) *VogueSpider {
|
||||||
|
if maxCo <= 0 {
|
||||||
|
maxCo = 5 // 默认限制 5 个并发,防止打满带宽
|
||||||
|
}
|
||||||
return &VogueSpider{
|
return &VogueSpider{
|
||||||
DB: db,
|
DB: db,
|
||||||
MaxCo: maxCo,
|
MaxCo: maxCo,
|
||||||
|
BrandID: brandID,
|
||||||
|
OnlyPlatform: onlyPlatform,
|
||||||
Client: &http.Client{
|
Client: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute 任务主入口(对应 execute)
|
// Run 爬虫启动总入口
|
||||||
func (s *VogueSpider) Execute() {
|
func (s *VogueSpider) Run() {
|
||||||
tasks, err := s.getTask()
|
tasks, err := s.getTask()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Error] 获取品牌任务失败: %v", err)
|
log.Printf("[错误] 获取任务失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最大查询的品牌数量, 控制并发数 ($maxBrandExecuteCount = $this->maxCo / 2)
|
log.Printf("[Info] 成功获取 %d 个品牌任务,开始执行...", len(tasks))
|
||||||
maxBrandExecuteCount := s.MaxCo / 2
|
|
||||||
if maxBrandExecuteCount < 1 {
|
|
||||||
maxBrandExecuteCount = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 Channel 信号量精准控制并发品牌数,无需 while(true) sleep
|
// 品牌级别的并发控制
|
||||||
sem := make(chan struct{}, maxBrandExecuteCount)
|
brandSem := make(chan struct{}, s.MaxCo)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
sem <- struct{}{} // 占用信号量槽位
|
brandSem <- struct{}{}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
go func(t model.AppBrand) {
|
go func(t model.AppBrand) {
|
||||||
defer func() {
|
defer func() {
|
||||||
<-sem // 释放槽位
|
<-brandSem
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.SpiderStart(t)
|
s.SpiderStart(t)
|
||||||
}(task)
|
}(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
log.Println("[Command] Vogue 所有抓取任务已完成。")
|
log.Println("[Info] Vogue 所有抓取任务已完成!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// _getTask 获取任务列表(对应 _getTask)
|
// getTask 获取需要抓取的品牌任务
|
||||||
func (s *VogueSpider) getTask() ([]model.AppBrand, error) {
|
func (s *VogueSpider) getTask() ([]model.AppBrand, error) {
|
||||||
|
// 1. 本地无数据库时的 Mock 调试场景
|
||||||
// 如果没有数据库连接(如本地调试场景)
|
|
||||||
if s.DB == nil {
|
if s.DB == nil {
|
||||||
log.Println("[Debug] 未连接数据库,启动 Mock 测试数据模式")
|
log.Println("[Debug] 当前未连接数据库,启动 Mock 任务数据模式")
|
||||||
|
|
||||||
// 如果命令行指定了 -b 参数,使用指定的品牌 ID,否则默认给一个示例品牌
|
|
||||||
brandID := s.BrandID
|
brandID := s.BrandID
|
||||||
if brandID == 0 {
|
if brandID == 0 {
|
||||||
brandID = 108
|
brandID = 108 // 默认给一个 Chanel 示例 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
return []model.AppBrand{
|
return []model.AppBrand{
|
||||||
{ID: brandID, Name: "chanel", SpiderOrigin: VoguePlatform},
|
{ID: int64(brandID), Name: "Alexander McQueen", SpiderOrigin: VoguePlatform},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 有数据库时的查询逻辑
|
||||||
var brands []model.AppBrand
|
var brands []model.AppBrand
|
||||||
query := s.DB.Model(&model.AppBrand{})
|
query := s.DB.Model(&model.AppBrand{})
|
||||||
|
|
||||||
@ -113,26 +111,24 @@ func (s *VogueSpider) getTask() ([]model.AppBrand, error) {
|
|||||||
return brands, err
|
return brands, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTaskName 品牌名称格式化(对应 getTaskName)
|
// SpiderStart 针对单个品牌开启抓取
|
||||||
func (s *VogueSpider) getTaskName(name string) string {
|
|
||||||
r := strings.NewReplacer(".", "-", " ", "-", "&", "")
|
|
||||||
return strings.ToLower(r.Replace(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpiderStart 针对单个品牌开始采集
|
|
||||||
func (s *VogueSpider) SpiderStart(task model.AppBrand) {
|
func (s *VogueSpider) SpiderStart(task model.AppBrand) {
|
||||||
brandName := s.getTaskName(task.Name)
|
brandName := s.getTaskName(task.Name)
|
||||||
url := VogueBaseURL + "/fashion-shows/designer/" + brandName
|
url := VogueBaseURL + "/fashion-shows/designer/" + brandName
|
||||||
log.Printf("[Command] brandName: %s; spiderUrl: %s", brandName, url)
|
log.Printf("[Command] brandName: %s; spiderUrl: %s", brandName, url)
|
||||||
|
|
||||||
showsList := s.getShowsList(url)
|
showsList := s.getShowsList(url)
|
||||||
|
if len(showsList) == 0 {
|
||||||
|
log.Printf("[Warning] 未找到品牌 [%s] 的发布会列表", brandName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 核心修复:限制详情页的最大并发数,受 s.MaxCo 控制
|
// 核心修复:限制该品牌下发布会详情页的抓取并发数
|
||||||
detailSem := make(chan struct{}, s.MaxCo)
|
detailSem := make(chan struct{}, s.MaxCo)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, list := range showsList {
|
for _, list := range showsList {
|
||||||
detailSem <- struct{}{} // 达到 MaxCo 上限时自动阻塞等待
|
detailSem <- struct{}{} // 超过 MaxCo 时会自动卡住等待
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
go func(info gjson.Result) {
|
go func(info gjson.Result) {
|
||||||
@ -143,15 +139,15 @@ func (s *VogueSpider) SpiderStart(task model.AppBrand) {
|
|||||||
|
|
||||||
s.getDetail(task.ID, info)
|
s.getDetail(task.ID, info)
|
||||||
|
|
||||||
// 2. 建议:每个请求完成后稍微休眠 200ms,既省带宽又能防封 IP
|
// 频控:每次下载后稍作停顿,保护带宽并防封 IP
|
||||||
// time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}(list)
|
}(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getShowsList 获取发布会列表(对应 getShowsList)
|
// getShowsList 获取品牌的发布会列表 JSON 数据
|
||||||
func (s *VogueSpider) getShowsList(url string) []gjson.Result {
|
func (s *VogueSpider) getShowsList(url string) []gjson.Result {
|
||||||
body, httpCode := s.request(url)
|
body, httpCode := s.request(url)
|
||||||
|
|
||||||
@ -168,37 +164,29 @@ func (s *VogueSpider) getShowsList(url string) []gjson.Result {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDetail 获取发布会详情并保存(对应 getDetail)
|
// getDetail 获取并解析单个发布会详情
|
||||||
func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
|
func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
|
||||||
hed := info.Get("hed").String()
|
hed := info.Get("hed").String()
|
||||||
|
|
||||||
var modelArticle model.Article
|
var exist model.BrandRunway
|
||||||
|
|
||||||
// 如果处于非 Debug 模式,进行数据库查询判重
|
// 如果处于非 Debug 模式,进行数据库查询判重
|
||||||
if !s.Debug && s.DB != nil {
|
if !s.Debug && s.DB != nil {
|
||||||
s.DB.Where("brand = ? AND title = ?", brandID, hed).First(&modelArticle)
|
s.DB.Where("brand_id = ? AND title = ?", brandID, hed).First(&exist)
|
||||||
if modelArticle.ID > 0 && !s.ForceUpdate {
|
if exist.ID > 0 && !s.ForceUpdate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果非 forceUpdate 且记录已存在,跳过更新
|
// 如果非 forceUpdate 且记录已存在,跳过更新
|
||||||
if modelArticle.ID > 0 && !s.ForceUpdate {
|
if exist.ID > 0 && !s.ForceUpdate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
modelArticle.Title = hed
|
// 获取图片(保持你原本的 URL 拼接与请求逻辑)
|
||||||
modelArticle.Images = "[]"
|
|
||||||
modelArticle.Platform = VoguePlatform
|
|
||||||
modelArticle.Brand = brandID
|
|
||||||
modelArticle.Module = 0
|
|
||||||
modelArticle.Year = s.parseYear(hed)
|
|
||||||
|
|
||||||
// 获取图片
|
|
||||||
pageURI := info.Get("url").String()
|
pageURI := info.Get("url").String()
|
||||||
requestURL := VogueBaseURL + pageURI + "/slideshow/collection"
|
requestURL := VogueBaseURL + pageURI + "/slideshow/collection"
|
||||||
log.Printf("正在匹配发布会详情 %s", requestURL)
|
log.Printf("正在匹配发布会详情 %s", requestURL)
|
||||||
modelArticle.SourceURL = requestURL
|
|
||||||
|
|
||||||
body, httpCode := s.request(requestURL)
|
body, httpCode := s.request(requestURL)
|
||||||
if httpCode != 200 || body == "" {
|
if httpCode != 200 || body == "" {
|
||||||
@ -209,7 +197,10 @@ func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
|
|||||||
re := regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);<`)
|
re := regexp.MustCompile(`(?s)window\.__PRELOADED_STATE__\s*=\s*(.*?);<`)
|
||||||
matches := re.FindStringSubmatch(body)
|
matches := re.FindStringSubmatch(body)
|
||||||
|
|
||||||
if len(matches) > 1 {
|
if len(matches) <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
imagesResult := gjson.Get(matches[1], "transformed.runwayGalleries.galleries.0.items")
|
imagesResult := gjson.Get(matches[1], "transformed.runwayGalleries.galleries.0.items")
|
||||||
|
|
||||||
if !imagesResult.Exists() {
|
if !imagesResult.Exists() {
|
||||||
@ -224,6 +215,7 @@ func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
|
|||||||
var saveURL []ImageItem
|
var saveURL []ImageItem
|
||||||
var detailURL []ImageItem
|
var detailURL []ImageItem
|
||||||
|
|
||||||
|
// 保持你原有的图片与细节图提取逻辑
|
||||||
for _, img := range imagesResult.Array() {
|
for _, img := range imagesResult.Array() {
|
||||||
xxlURL := img.Get("image.sources.xxl.url").String()
|
xxlURL := img.Get("image.sources.xxl.url").String()
|
||||||
if xxlURL != "" {
|
if xxlURL != "" {
|
||||||
@ -240,41 +232,96 @@ func (s *VogueSpider) getDetail(brandID int64, info gjson.Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- 下面对接你的双表数据库结构 ----------------
|
||||||
|
|
||||||
|
// 1. 整理图片与封面
|
||||||
|
cover := ""
|
||||||
if len(saveURL) > 0 {
|
if len(saveURL) > 0 {
|
||||||
jsonBytes, _ := json.Marshal(saveURL)
|
cover = saveURL[0].Src
|
||||||
modelArticle.Images = string(jsonBytes)
|
|
||||||
modelArticle.Cover = saveURL[0].Src
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.saveArticle(&modelArticle)
|
// 汇总所有图片(主图 + 细节图)准备存入 brand_runway_images 表
|
||||||
|
var allImages []string
|
||||||
|
for _, item := range saveURL {
|
||||||
|
allImages = append(allImages, item.Src)
|
||||||
|
}
|
||||||
|
for _, item := range detailURL {
|
||||||
|
allImages = append(allImages, item.Src)
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveArticle 数据持久化或 Debug 输出控制
|
now := time.Now().Unix()
|
||||||
func (s *VogueSpider) saveArticle(article *model.Article) {
|
|
||||||
// Debug 模式:只打印结果,不入库
|
// 2. 构建主表 BrandRunway 结构体
|
||||||
if s.Debug {
|
runway := model.BrandRunway{
|
||||||
indentJSON, _ := json.MarshalIndent(article, "", " ")
|
Title: hed,
|
||||||
log.Printf("\n================ [DEBUG OUTPUT] ================\n%s\n================================================\n", string(indentJSON))
|
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))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生产模式:保存到数据库
|
// 4. 执行数据库事务保存(主表 brand_runway + 从表 brand_runway_images)
|
||||||
if s.DB != nil {
|
err := s.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
s.DB.Save(article)
|
// 判断是更新还是新增
|
||||||
log.Printf("[Success] 入库成功: %s", article.SourceURL)
|
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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Create(&runway).Error; err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取年份 (替换 PHP 中的 AppHelper::getYear)
|
// 批量插入图片从表
|
||||||
func (s *VogueSpider) parseYear(title string) int {
|
var runwayImages []model.BrandRunwayImage
|
||||||
re := regexp.MustCompile(`\b(19|20)\d{2}\b`)
|
for idx, imgURL := range allImages {
|
||||||
match := re.FindString(title)
|
runwayImages = append(runwayImages, model.BrandRunwayImage{
|
||||||
if match != "" {
|
RunwayID: runway.ID, // 获取上面生成的自增 ID
|
||||||
year, _ := strconv.Atoi(match)
|
BrandID: uint(brandID),
|
||||||
return year
|
Image: imgURL,
|
||||||
|
SortOrder: uint(idx + 1),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return time.Now().Year()
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTaskName 品牌名称格式化(对应 getTaskName)
|
||||||
|
func (s *VogueSpider) getTaskName(name string) string {
|
||||||
|
r := strings.NewReplacer(".", "-", " ", "-", "&", "")
|
||||||
|
log.Printf("[Debug] 原始品牌名: %s; 格式化后: %s", name, strings.ToLower(r.Replace(name)))
|
||||||
|
return strings.ToLower(r.Replace(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP 请求封装
|
// HTTP 请求封装
|
||||||
@ -299,3 +346,14 @@ func (s *VogueSpider) request(url string) (string, int) {
|
|||||||
|
|
||||||
return string(bodyBytes), resp.StatusCode
|
return string(bodyBytes), resp.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseYear 从标题中提取 4 位年份数字(如 Fall 2024 Ready-to-Wear -> 2024)
|
||||||
|
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)
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
return time.Now().Year()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user