package xiaohongshu import ( "context" "encoding/json" "fmt" "time" "github.com/go-rod/rod" ) // FeedDetailAction 表示 Feed 详情页动作 type FeedDetailAction struct { page *rod.Page } // NewFeedDetailAction 创建 Feed 详情页动作 func NewFeedDetailAction(page *rod.Page) *FeedDetailAction { return &FeedDetailAction{page: page} } // GetFeedDetail 获取 Feed 详情页数据 func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken string) (*FeedDetailResponse, error) { page := f.page.Context(ctx).Timeout(60 * time.Second) // 构建详情页 URL url := makeFeedDetailURL(feedID, xsecToken) // 导航到详情页 page.MustNavigate(url) page.MustWaitDOMStable() time.Sleep(1 * time.Second) // 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串 result := page.MustEval(`() => { if (window.__INITIAL_STATE__) { return JSON.stringify(window.__INITIAL_STATE__); } return ""; }`).String() if result == "" { return nil, fmt.Errorf("__INITIAL_STATE__ not found") } // 定义响应结构并直接反序列化 var initialState struct { Note struct { NoteDetailMap map[string]struct { Note FeedDetail `json:"note"` Comments CommentList `json:"comments"` } `json:"noteDetailMap"` } `json:"note"` } if err := json.Unmarshal([]byte(result), &initialState); err != nil { return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err) } // 从 noteDetailMap 中获取对应 feedID 的数据 noteDetail, exists := initialState.Note.NoteDetailMap[feedID] if !exists { return nil, fmt.Errorf("feed %s not found in noteDetailMap", feedID) } return &FeedDetailResponse{ Note: noteDetail.Note, Comments: noteDetail.Comments, }, nil } func makeFeedDetailURL(feedID, xsecToken string) string { return fmt.Sprintf("https://www.xiaohongshu.com/explore/%s?xsec_token=%s&xsec_source=pc_feed", feedID, xsecToken) }