一、小红书笔记详情接口概述
1.1 接口功能
小红书笔记详情接口用于获取指定笔记的完整信息,包括:
| 数据维度 | 包含字段 |
|---|---|
| 基础信息 | 笔记ID、标题、正文内容、发布时间、修改时间 |
| 多媒体内容 | 封面图、图片列表、视频地址、视频时长 |
| 作者信息 | 用户ID、昵称、头像、粉丝数、关注数、笔记数 |
| 互动数据 | 点赞数、收藏数、评论数、分享数、浏览量 |
| 标签话题 | 标签列表、@用户、关联话题、IP属地 |
| 位置信息 | 地理位置名称、经纬度 |
| 商品信息 | 关联商品、购买链接(带货笔记) |
| 相关推荐 | 相关笔记列表 |
1.2 接口特点
重要提示:小红书并未公开对外提供正式的开放 API,官方接口主要面向合作品牌方和广告主开放。 开发者通常通过以下方式获取数据:
- 小红书开放平台(需企业认证申请)
- 第三方数据服务(聚合 API)
- RPA/逆向工程(合规风险较高)
二、接口结构与请求规范
2.1 请求方式与地址测试
请求方式: HTTP GET / POST
请求地址: https://www.xiaohongshu.com/api/sns/v1/note/{note_id}/detail
或第三方聚合接口: https://api.example.com/xhs/note/detail
2.2 请求参数
| 参数名 | 类型 | 必选 | 说明 |
|---|---|---|---|
note_id | String | 是 | 笔记唯一标识(24位十六进制字符串) |
access_token | String | 条件 | OAuth2.0 授权令牌(官方接口) |
api_key | String | 条件 | 第三方接口密钥 |
source | String | 否 | 来源标识(web/app) |
timestamp | Long | 否 | 时间戳(防重放攻击) |
2.3 响应数据结构
{
"code": 0,
"msg": "success",
"data": {
"note_id": "649c46ab000000002702ad36",
"title": "夏日必备的5款防晒霜测评",
"content": "夏天快到了,给大家分享几款我常用的防晒霜...",
"content_rich": [
{"type": "text", "text": "夏天快到了..."},
{"type": "image", "url": "https://sns-img-hw.xhscdn.com/xxx.jpg", "width": 1080, "height": 1440},
{"type": "video", "url": "https://sns-video-hw.xhscdn.com/xxx.mp4", "duration": 156}
],
"tags": ["防晒霜", "夏日护肤", "美妆测评"],
"at_users": [{"user_id": "5123456789", "name": "美妆达人"}],
"location": {"name": "上海市", "longitude": 121.47, "latitude": 31.23},
"statistics": {
"like_count": 12563,
"collect_count": 8952,
"comment_count": 1256,
"share_count": 325,
"view_count": 156890
},
"author": {
"user_id": "612345678",
"name": "护肤小能手",
"avatar": "https://sns-avatar.xhscdn.com/avatar.jpg",
"follower_count": 56800,
"following_count": 320,
"note_count": 128
},
"create_time": 1625097600,
"update_time": 1625100800,
"related_notes": []
}
}
三、测试环境搭建
3.1 安装依赖
pip install requests
pip install pytest
pip install pytest-html # 测试报告生成
pip install allure-pytest # Allure 测试报告
pip install python-dotenv # 环境变量管理
pip install loguru # 日志记录
3.2 项目结构
xhs_api_test/
├── config/
│ └── config.py # 配置文件
├── testcases/
│ ├── test_note_detail.py # 笔记详情接口测试
│ └── conftest.py # pytest fixtures
├── utils/
│ ├── api_client.py # API 请求封装
│ ├── data_generator.py # 测试数据生成
│ └── assertions.py # 断言工具
├── reports/ # 测试报告目录
├── data/ # 测试数据文件
└── pytest.ini # pytest 配置
四、核心代码实现
4.1 API 客户端封装
# utils/api_client.py
import requests
import time
import hashlib
import json
from typing import Dict, Optional
from loguru import logger
class XHSApiClient:
"""
小红书笔记详情 API 客户端
支持官方接口和第三方聚合接口
"""
def __init__(self, base_url: str, api_key: Optional[str] = None,
api_secret: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.api_secret = api_secret
self.session = requests.Session()
self.session.headers.update({
"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",
"Accept": "application/json",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": "https://www.xiaohongshu.com/"
})
def _generate_sign(self, params: Dict) -> str:
"""生成请求签名(第三方接口使用)"""
if not self.api_secret:
return ""
sorted_params = sorted(params.items())
param_str = "".join([f"{k}{v}" for k, v in sorted_params])
sign_str = f"{self.api_secret}{param_str}{self.api_secret}"
return hashlib.md5(sign_str.encode()).hexdigest().upper()
def get_note_detail(self, note_id: str, extra_params: Optional[Dict] = None) -> Dict:
"""
获取笔记详情
Args:
note_id: 笔记 ID
extra_params: 额外参数
Returns:
API 响应字典
"""
url = f"{self.base_url}/api/sns/v1/note/{note_id}/detail"
params = {
"source": "web",
"timestamp": int(time.time())
}
if self.api_key:
params["api_key"] = self.api_key
params["sign"] = self._generate_sign(params)
if extra_params:
params.update(extra_params)
try:
logger.info(f"请求笔记详情 | note_id: {note_id}")
response = self.session.get(url, params=params, timeout=30)
response.raise_for_status()
result = response.json()
logger.info(f"响应状态: code={result.get('code')}, msg={result.get('msg')}")
return result
except requests.exceptions.Timeout:
logger.error(f"请求超时 | note_id: {note_id}")
return {"code": -1, "msg": "请求超时", "data": None}
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP 错误 | status: {e.response.status_code}")
return {"code": e.response.status_code, "msg": str(e), "data": None}
except requests.exceptions.RequestException as e:
logger.error(f"请求异常: {e}")
return {"code": -2, "msg": str(e), "data": None}
def get_note_detail_batch(self, note_ids: list) -> Dict[str, Dict]:
"""批量获取笔记详情"""
results = {}
for note_id in note_ids:
results[note_id] = self.get_note_detail(note_id)
time.sleep(1) # 限流控制
return results
4.2 测试用例设计
# testcases/test_note_detail.py
import pytest
import allure
from utils.api_client import XHSApiClient
@allure.feature("小红书笔记详情接口")
@allure.story("基础功能测试")
class TestNoteDetail:
@pytest.fixture(scope="class")
def client(self):
"""初始化 API 客户端"""
return XHSApiClient(
base_url="https://www.xiaohongshu.com",
api_key="your_api_key",
api_secret="your_api_secret"
)
@allure.title("TC01: 获取正常笔记详情")
@allure.description("传入有效的笔记ID,验证返回数据完整性和字段类型")
@pytest.mark.smoke
def test_get_note_detail_success(self, client):
"""测试正常获取笔记详情"""
note_id = "649c46ab000000002702ad36" # 有效笔记ID
with allure.step("发送请求"):
response = client.get_note_detail(note_id)
with allure.step("验证响应状态"):
assert response["code"] == 0, f"期望 code=0, 实际 code={response['code']}"
assert response["msg"] == "success"
with allure.step("验证数据结构"):
data = response["data"]
assert data is not None, "data 字段不应为空"
# 验证必需字段存在
required_fields = [
"note_id", "title", "content", "author",
"statistics", "create_time"
]
for field in required_fields:
assert field in data, f"缺少必需字段: {field}"
with allure.step("验证字段类型"):
assert isinstance(data["note_id"], str)
assert isinstance(data["title"], str)
assert isinstance(data["statistics"]["like_count"], int)
assert isinstance(data["statistics"]["comment_count"], int)
assert isinstance(data["author"]["follower_count"], int)
with allure.step("验证数据一致性"):
assert data["note_id"] == note_id, "返回的 note_id 应与请求一致"
assert data["statistics"]["like_count"] >= 0, "点赞数不应为负数"
assert data["statistics"]["view_count"] >= data["statistics"]["like_count"], \
"浏览量应大于等于点赞数"
@allure.title("TC02: 笔记ID不存在")
@allure.description("传入不存在的笔记ID,验证返回正确的错误信息")
def test_note_not_found(self, client):
"""测试笔记不存在场景"""
note_id = "000000000000000000000000" # 不存在的ID
response = client.get_note_detail(note_id)
assert response["code"] != 0, "应返回非零错误码"
assert "msg" in response, "应包含错误信息"
assert "data" not in response or response["data"] is None
@allure.title("TC03: 笔记ID格式错误")
@allure.description("传入格式非法的笔记ID,验证参数校验")
@pytest.mark.parametrize("invalid_id", [
"", # 空字符串
"123", # 过短
"abcdefghijklmnopqrstuvwx", # 非十六进制
"649c46ab000000002702ad3g", # 包含非法字符
"649c46ab000000002702ad360", # 过长(25位)
])
def test_invalid_note_id(self, client, invalid_id):
"""测试非法笔记ID格式"""
response = client.get_note_detail(invalid_id)
assert response["code"] != 0 or response.get("data") is None, \
f"非法ID '{invalid_id}' 应返回错误或空数据"
@allure.title("TC04: 笔记ID为空")
@allure.description("不传笔记ID,验证参数缺失处理")
def test_missing_note_id(self, client):
"""测试缺少必要参数"""
response = client.get_note_detail("")
assert response["code"] != 0, "缺少参数应返回错误"
@allure.title("TC05: 验证多媒体内容类型")
@allure.description("分别测试图文笔记和视频笔记的内容结构")
@pytest.mark.parametrize("note_id,expected_type", [
("649c46ab000000002702ad36", "image"), # 图文笔记
("649c46ab000000002702ad37", "video"), # 视频笔记
])
def test_note_content_type(self, client, note_id, expected_type):
"""测试不同内容类型的笔记"""
response = client.get_note_detail(note_id)
if response["code"] != 0:
pytest.skip("笔记可能已删除或不可访问")
data = response["data"]
content_rich = data.get("content_rich", [])
if expected_type == "image":
image_items = [item for item in content_rich if item.get("type") == "image"]
assert len(image_items) > 0, "图文笔记应包含图片"
elif expected_type == "video":
video_items = [item for item in content_rich if item.get("type") == "video"]
assert len(video_items) > 0, "视频笔记应包含视频"
assert "duration" in video_items[0], "视频应包含时长信息"
@allure.title("TC06: 验证作者信息完整性")
@allure.description("检查作者字段的完整性和数据类型")
def test_author_info(self, client):
"""测试作者信息"""
note_id = "649c46ab000000002702ad36"
response = client.get_note_detail(note_id)
if response["code"] != 0:
pytest.skip("笔记不可访问")
author = response["data"]["author"]
assert "user_id" in author
assert "name" in author
assert "avatar" in author
assert isinstance(author["follower_count"], int)
assert author["follower_count"] >= 0
@allure.title("TC07: 验证互动数据范围")
@allure.description("验证互动数据的合理范围")
def test_interaction_data(self, client):
"""测试互动数据有效性"""
note_id = "649c46ab000000002702ad36"
response = client.get_note_detail(note_id)
if response["code"] != 0:
pytest.skip("笔记不可访问")
stats = response["data"]["statistics"]
# 数据范围校验
assert stats["like_count"] >= 0, "点赞数不能为负"
assert stats["collect_count"] >= 0, "收藏数不能为负"
assert stats["comment_count"] >= 0, "评论数不能为负"
assert stats["share_count"] >= 0, "分享数不能为负"
assert stats["view_count"] >= 0, "浏览量不能为负"
# 逻辑关系校验
assert stats["view_count"] >= stats["like_count"], "浏览量应>=点赞数"
assert stats["like_count"] >= stats["collect_count"], "点赞数通常>=收藏数"
@allure.title("TC08: 验证时间戳格式")
@allure.description("检查创建时间和修改时间的时间戳格式")
def test_timestamp_format(self, client):
"""测试时间戳格式"""
note_id = "649c46ab000000002702ad36"
response = client.get_note_detail(note_id)
if response["code"] != 0:
pytest.skip("笔记不可访问")
data = response["data"]
assert isinstance(data["create_time"], (int, float)), "create_time 应为时间戳"
assert data["create_time"] > 0, "时间戳应为正数"
if "update_time" in data:
assert data["update_time"] >= data["create_time"], \
"修改时间应晚于创建时间"
@allure.feature("小红书笔记详情接口")
@allure.story("性能与异常测试")
class TestNoteDetailPerformance:
@pytest.fixture(scope="class")
def client(self):
return XHSApiClient(
base_url="https://www.xiaohongshu.com",
api_key="your_api_key"
)
@allure.title("TC09: 接口响应时间")
@allure.description("验证接口响应时间在可接受范围内")
def test_response_time(self, client):
"""测试响应时间"""
import time
note_id = "649c46ab000000002702ad36"
start = time.time()
response = client.get_note_detail(note_id)
elapsed = time.time() - start
allure.attach(f"实际响应时间: {elapsed:.2f}s", "性能数据")
assert elapsed < 3, f"响应时间 {elapsed:.2f}s 超过阈值 3s"
@allure.title("TC10: 并发请求测试")
@allure.description("测试接口在高并发下的稳定性")
@pytest.mark.stress
def test_concurrent_requests(self, client):
"""测试并发请求"""
import threading
note_ids = ["649c46ab000000002702ad36"] * 5
results = []
def fetch(note_id):
results.append(client.get_note_detail(note_id))
threads = [threading.Thread(target=fetch, args=(nid,)) for nid in note_ids]
for t in threads:
t.start()
for t in threads:
t.join()
success_count = sum(1 for r in results if r.get("code") == 0)
assert success_count >= 3, f"并发请求成功率过低: {success_count}/5"
@allure.title("TC11: 限流测试")
@allure.description("测试高频请求下的限流处理")
@pytest.mark.stress
def test_rate_limiting(self, client):
"""测试限流"""
note_id = "649c46ab000000002702ad36"
responses = []
for _ in range(20):
responses.append(client.get_note_detail(note_id))
# 检查是否有 429 状态码
rate_limited = any(r.get("code") == 429 for r in responses)
if rate_limited:
allure.attach("触发限流", "限流验证")
4.3 测试数据生成器
Python
# utils/data_generator.py
import random
import string
class NoteDataGenerator:
"""测试数据生成器"""
@staticmethod
def valid_note_id():
"""生成有效的笔记ID(24位十六进制)"""
return ''.join(random.choices(string.hexdigits.lower(), k=24))
@staticmethod
def invalid_note_ids():
"""生成各类非法笔记ID"""
return [
"", # 空
"123", # 过短
"xyz", # 非十六进制
"g" * 24, # 非法字符
''.join(random.choices(string.hexdigits.lower(), k=23)), # 23位
''.join(random.choices(string.hexdigits.lower(), k=25)), # 25位
]
@staticmethod
def boundary_note_ids():
"""边界值测试数据"""
return [
"0" * 24, # 全0
"f" * 24, # 全f
"0" + "f" * 23, # 边界混合
]
4.4 Pytest 配置
# pytest.ini
[pytest]
testpaths = testcases
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --html=reports/report.html --self-contained-html
markers =
smoke: 冒烟测试
regression: 回归测试
stress: 压力测试
slow: 慢速测试
五、测试执行与报告
5.1 执行测试
# 执行全部测试
pytest
# 执行冒烟测试
pytest -m smoke
# 生成 Allure 报告
pytest --alluredir=reports/allure
allure serve reports/allure
# 执行压力测试
pytest -m stress -n 4
5.2 测试报告示例
============================= test session starts ==============================
platform darwin -- Python 3.11.0, pytest-7.4.0, pluggy-1.0.0
rootdir: /Users/dev/xhs_api_test
collected 11 items
testcases/test_note_detail.py::TestNoteDetail::test_get_note_detail_success PASSED [ 9%]
testcases/test_note_detail.py::TestNoteDetail::test_note_not_found PASSED [ 18%]
testcases/test_note_detail.py::TestNoteDetail::test_invalid_note_id[ ] PASSED [ 27%]
testcases/test_note_detail.py::TestNoteDetail::test_invalid_note_id[123] PASSED [ 36%]
testcases/test_note_detail.py::TestNoteDetail::test_invalid_note_id[...] PASSED [ 45%]
testcases/test_note_detail.py::TestNoteDetail::test_note_content_type PASSED [ 54%]
testcases/test_note_detail.py::TestNoteDetail::test_author_info PASSED [ 63%]
testcases/test_note_detail.py::TestNoteDetail::test_interaction_data PASSED [ 72%]
testcases/test_note_detail.py::TestNoteDetail::test_timestamp_format PASSED [ 81%]
testcases/test_note_detail.py::TestNoteDetailPerformance::test_response_time PASSED [ 90%]
testcases/test_note_detail.py::TestNoteDetailPerformance::test_concurrent_requests PASSED [100%]
============================== 11 passed in 15.32s ==============================
六、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
code: -1, msg: 请求超时 | 网络延迟或接口响应慢 | 增加超时时间,使用代理池 |
code: 403 | IP 被封禁或缺少权限 | 更换 IP,检查 API Key 权限 |
code: 429 | 请求频率过高触发限流 | 降低请求频率,使用指数退避 |
data: null | 笔记已删除或私密 | 跳过该笔记,记录日志 |
| 字段类型不匹配 | API 版本变更 | 更新测试断言,关注接口文档 |
| 视频 URL 失效 | CDN 链接过期 | 验证 URL 可访问性,使用备用地址 |
七、合规注意事项
- 数据使用范围:仅用于自有业务分析,不得转售或商用
- 隐私保护:用户个人信息需脱敏处理
- 频率控制:遵守平台限流策略,建议 ≤ 10 次/秒
- 官方授权:生产环境建议使用小红书开放平台正式接口
八、总结
| 测试维度 | 覆盖内容 | 用例数量 |
|---|---|---|
| 功能测试 | 正常请求、参数校验、数据完整性 | 7 |
| 异常测试 | 非法参数、空值、越界 | 3 |
| 性能测试 | 响应时间、并发、限流 | 2 |
| 数据校验 | 字段类型、逻辑关系、范围 | 贯穿全部 通过系统化的接口测试,可以确保小红书笔记详情接口的稳定性和数据准确性,为后续的内容分析、数据挖掘等业务场景奠定坚实基础。 |

