# 小红书页面结构分析

> 通过浏览器自动化(Chrome DevTools Protocol)分析得出，用于指导爬虫选择器编写

## 1. 登录状态检测

### 未登录标志（优先检测）

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 二维码图片 | `xpath://img[contains(@src, "qrcode")]` | 登录弹窗中的二维码 |
| 登录提示文字 | `xpath://span[contains(text(), "登录后查看")]` | "登录后查看搜索结果" |
| 扫码登录文字 | `xpath://span[contains(text(), "扫码登录")]` | 弹窗标题 |
| 手机号登录文字 | `xpath://span[contains(text(), "手机号登录")]` | 弹窗标题 |
| 登录按钮 | `css:.login-btn, button.login-btn` | 红色登录按钮 |
| 关闭图标 | `css:.close-icon` | 登录弹窗的关闭按钮 |

### 已登录标志

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 用户主页链接 | `css:.user.side-bar-component a[href*="/user/profile/"]` | 侧边栏"我"的链接 |
| 用户头像 | `css:.side-bar .reds-avatar` | 侧边栏头像容器 |
| 头像图片 | `css:img[src*="avatar"]` | 头像图片 |

### 检测逻辑

```
1. 先检查登录弹窗（二维码、登录文字）→ 有则未登录
2. 再检查用户链接和头像 → 有则已登录
3. 检查侧边栏文本："我"存在且无"登录" → 已登录
4. 默认返回未登录（更安全）
```

**重要发现**：登录弹窗存在时，底层页面的用户链接仍然存在，必须优先检测弹窗。

---

## 2. 搜索页笔记卡片

### 卡片容器

```css
section.note-item
```

### 卡片属性

| 属性 | 说明 |
|------|------|
| `data-index` | 笔记索引（0开始） |
| `data-width` | 图片原始宽度 |
| `data-height` | 图片原始高度 |

### 卡片内部结构

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 封面链接 | `a.cover` | href包含笔记ID和xsec_token |
| 封面图片 | `a.cover img` | src为图片URL |
| 隐藏链接 | `a[href^="/explore/"]` | display:none，包含纯净笔记ID |
| 标题 | `.footer .title span` | 笔记标题文字 |
| 作者区域 | `.card-bottom-wrapper .author` | 作者信息容器 |
| 作者头像 | `.author-avatar` | 作者头像图片 |
| 作者名 | `.name` | 作者昵称 |
| 发布时间 | `.time` | 格式：2025-12-04 |
| 点赞区域 | `.like-wrapper` | 点赞图标和数字 |

### 笔记链接格式

```
封面链接: /search_result/{笔记ID}?xsec_token={token}&xsec_source=
隐藏链接: /explore/{笔记ID}
```

---

## 3. 笔记详情弹窗

点击笔记后弹出的详情页，URL变为 `/explore/{笔记ID}?xsec_token=...`

### 整体结构

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 遮罩层 | `.note-detail-mask` | 弹窗背景 |
| 笔记容器 | `.note-container` | 弹窗主体 |
| 内容滚动区 | `.note-scroller` | 可滚动的内容区域 |
| 内容区 | `.note-content` | 标题和正文容器 |

### 内容元素

| 元素 | 选择器 | 示例数据 |
|------|--------|----------|
| 标题 | `.note-content .title` | "简单日常的小鞋子" |
| 正文 | `.note-text` | 包含描述和#标签 |
| 发布日期 | `.date` | "2025-12-04" |

### 作者信息

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 作者区域 | `.author-wrapper` | 作者信息容器 |
| 作者名 | `.author-wrapper .name` | "吸溜溜溜吧唧" |
| 关注按钮 | `.note-detail-follow-btn` | 红色关注按钮 |

### 图片轮播

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 轮播容器 | `.xhs-slider-container` | 图片轮播器 |
| 轮播区域 | `.note-slider` | 轮播内容 |
| 图片容器 | `.note-slider-img` | 单张图片容器 |
| 分页指示器 | `.slider-pagination-container` | 底部小圆点 |

---

## 4. 互动数据

底部互动栏，包含点赞、收藏、评论、分享

| 元素 | 选择器 | 示例 |
|------|--------|------|
| 点赞数 | `.like-wrapper .count` | "1426" |
| 收藏数 | `.collect-wrapper .count` | "387" |
| 评论数 | `.chat-wrapper .count` | "9" |
| 评论输入框 | `.content-input` | "说点什么..." |

### 数字解析

- 普通数字：直接显示，如 "1426"
- 千单位：显示 "1.2k" 或 "1.2万"
- 需要解析处理

---

## 5. 评论区

### 评论容器

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 评论区容器 | `.comments-container` | 评论列表容器 |
| 评论元素 | `.comments-el` | 评论区域 |

### 单条评论

| 元素 | 选择器 | 示例 |
|------|--------|------|
| 评论项 | `.comment-item` | 单条评论容器 |
| 父评论 | `.parent-comment` | 一级评论 |
| 子评论 | `.comment-item-sub` | 回复评论 |
| 评论者名 | `.comment-item .name` | "Mimin" |
| 评论内容 | `.comment-item .content` | "求鞋子链接宝子" |
| 评论时间 | `.comment-item .date` | "2025-12-19福建" |
| 评论地区 | `.comment-item .location` | "福建" |
| 点赞按钮 | `.comment-item .like` | 评论点赞 |
| 回复按钮 | `.comment-menu` | 回复操作 |

> **注意**: 评论时间用`.date`不是`.time`！`.time`在评论中不存在。

### 评论数据结构

```json
{
  "author": "Mimin",
  "content": "求鞋子链接宝子",
  "time": "2025-12-19福建"
}
```

**注意**：时间字段包含地区信息，格式为 `日期+地区`

---

## 6. 视频笔记

视频笔记与图文笔记的区别

| 元素 | 选择器 | 说明 |
|------|--------|------|
| 视频元素 | `video` | HTML5视频标签 |
| 视频源 | `video[src]` | 视频URL |
| 播放按钮 | `.play-icon` | 封面上的播放图标 |

### 识别视频笔记

```python
# 在详情页检测
video = page.ele('xpath://video', timeout=0.2)
if video:
    note_type = "视频"
    video_url = video.attr('src')
```

---

## 7. 筛选和排序

搜索页顶部的筛选标签

### 内容类型

| 标签 | 说明 |
|------|------|
| 全部 | 所有类型 |
| 图文 | 仅图文笔记 |
| 视频 | 仅视频笔记 |
| 用户 | 搜索用户 |

### 排序标签

| 标签 | 说明 |
|------|------|
| 综合 | 默认排序（红色激活） |
| 最新 | 按时间排序 |
| 万能百搭 | 热门标签 |
| 韩系 | 热门标签 |
| ... | 更多标签 |

---

## 8. 反爬策略观察

### URL Token

- `xsec_token`: 安全令牌，每次请求生成
- `xsec_source`: 来源标识（pc_search等）

### 请求限制

- 频繁请求会触发验证码
- 建议添加随机延迟

### Cookie依赖

- 必须登录才能获取完整数据
- Cookie需要定期刷新

---

## 9. 最佳实践

### 选择器优先级

1. **CSS选择器** - 简洁快速
2. **XPath** - 复杂条件时使用
3. **组合选择器** - 提高准确性

### 推荐选择器

```python
# 笔记卡片
"css:section.note-item"

# 详情标题
"css:.note-content .title"

# 详情正文
"css:.note-text"

# 互动数据
"css:.like-wrapper .count"
"css:.collect-wrapper .count"
"css:.chat-wrapper .count"

# 评论
"css:.comment-item .content"
```

### 超时设置

| 场景 | 推荐超时 |
|------|----------|
| 页面加载 | 2-3秒 |
| 元素查找 | 0.2-0.5秒 |
| 弹窗加载 | 0.3-0.5秒 |

---

---

## 10. 瀑布流布局分析

### 布局结构

小红书搜索结果使用**三列瀑布流**布局：

```
列1 (left≈286px)  列2 (left≈548px)  列3 (left≈809px)
     ↓                  ↓                  ↓
   笔记0              笔记1              笔记2
   笔记4              笔记3              笔记5
   笔记6              笔记7              笔记8
    ...               ...               ...
```

### DOM顺序 vs 视觉顺序

| DOM索引 | 视觉位置 | left | top |
|---------|----------|------|-----|
| 0 | 第1行左 | 286 | 217 |
| 1 | 第1行中 | 548 | 217 |
| 2 | 第1行右 | 809 | 217 |
| 3 | 第2行中 | 548 | 602 |
| 4 | 第2行左 | 286 | 630 |
| 5 | 第2行右 | 809 | 630 |

**发现**：DOM顺序≠视觉顺序，瀑布流导致每列高度不同。

### 按视觉顺序爬取

```javascript
// 获取位置并排序
const notes = document.querySelectorAll('section.note-item');
const positions = [];
notes.forEach((n, i) => {
    const rect = n.getBoundingClientRect();
    positions.push({
        domIndex: i,
        left: Math.round(rect.left),
        top: Math.round(rect.top)
    });
});

// 1. 识别列（根据left值聚类，允许50px误差）
const columns = [];
positions.forEach(p => {
    if (!columns.find(c => Math.abs(c - p.left) < 50)) 
        columns.push(p.left);
});
columns.sort((a, b) => a - b);

// 2. 将笔记分配到列
const columnItems = columns.map(() => []);
positions.forEach(p => {
    const colIndex = columns.findIndex(c => Math.abs(c - p.left) < 50);
    if (colIndex >= 0) columnItems[colIndex].push(p);
});

// 3. 每列按top排序
columnItems.forEach(col => col.sort((a, b) => a.top - b.top));

// 4. 交织合并：从左到右，从上到下
const result = [];
const maxRows = Math.max(...columnItems.map(c => c.length));
for (let row = 0; row < maxRows; row++) {
    for (let col = 0; col < columnItems.length; col++) {
        if (row < columnItems[col].length) {
            result.push(columnItems[col][row].domIndex);
        }
    }
}
return result;
```

**示例**：DOM顺序 `[0,1,2,3,4,5]` → 排序后 `[0,1,2,4,3,5]`（因为第二行DOM索引3在中列，4在左列）

---

---

## 11. __INITIAL_STATE__ 数据结构（2026-02-02更新）

小红书使用React，页面数据存储在 `window.__INITIAL_STATE__` 中。

### 关键路径

```javascript
window.__INITIAL_STATE__.note = {
    currentNoteId: "当前笔记ID（可能有缓存问题）",
    noteDetailMap: {
        "笔记ID": {
            note: {
                title: "标题",
                desc: "描述",
                user: {
                    nickname: "作者昵称",
                    userId: "用户ID"
                },
                imageList: [
                    { url: "图片URL", urlDefault: "默认图片URL" }
                ],
                video: {
                    consumer: {
                        originVideoKey: "视频key"  // 拼接URL
                    },
                    url: "视频URL",
                    media: { stream: { h264: [{ masterUrl: "..." }] } }
                },
                interactInfo: {
                    likedCount: 点赞数,
                    collectedCount: 收藏数,
                    commentCount: 评论数
                },
                time: "发布时间戳",
                ipLocation: "IP地区"
            }
        }
    }
}
```

### 重要提示

**`currentNoteId` 可能有缓存问题！** 应该从URL提取noteId：

```javascript
const urlMatch = window.location.href.match(/explore\/([a-zA-Z0-9]+)/);
const noteId = urlMatch ? urlMatch[1] : state.note.currentNoteId;
```

### 视频URL拼接

```javascript
// 方法1：使用originVideoKey
const videoUrl = 'https://sns-video-bd.xhscdn.com/' + video.consumer.originVideoKey;

// 方法2：直接URL
const videoUrl = video.url;  // 可能是blob:格式

// 方法3：H264流
const videoUrl = video.media.stream.h264[0].masterUrl;
```

---

## 12. Live图特征（2026-02-02更新）

小红书支持Live Photo（动态图片），会产生两个版本：

| 版本 | URL特征 | 说明 |
|------|---------|------|
| 静态版 | 普通图片URL | 静态图片 |
| 动态版 | 包含 `/live/` 或 `_live` | 动态图片 |

### 示例

```
静态：https://ci.xiaohongshu.com/xxx/abc.jpg
动态：https://ci.xiaohongshu.com/xxx/live/abc.webp
或
静态：https://sns-webpic-qc.xhscdn.com/xxx_abc.jpg
动态：https://sns-webpic-qc.xhscdn.com/xxx_abc_live.webp
```

---

## 更新记录

| 日期 | 版本 | 更新内容 |
|------|------|----------|
| 2026-01-31 | v1.0 | 初始版本，基于CDP分析 |
| 2026-01-31 | v1.1 | 修复登录检测优先级问题 |
| 2026-01-31 | v1.2 | 添加瀑布流布局分析，支持从左到右爬取 |
| 2026-01-31 | v1.3 | 修复评论时间选择器(.time->.date)，完整流程验证通过 |
| 2026-01-31 | v1.4 | 重写瀑布流排序算法：列聚类+交织合并，确保从左到右顺序 |
| 2026-02-02 | v1.5 | 添加__INITIAL_STATE__数据结构分析 |
| 2026-02-02 | v1.6 | 添加Live图特征说明 |
