小红书笔记详情接口测试实战指南

admin8小时前小红书api8

一、小红书笔记详情接口概述

1.1 接口功能

小红书笔记详情接口用于获取指定笔记的完整信息,包括:
表格
数据维度包含字段
基础信息笔记ID、标题、正文内容、发布时间、修改时间
多媒体内容封面图、图片列表、视频地址、视频时长
作者信息用户ID、昵称、头像、粉丝数、关注数、笔记数
互动数据点赞数、收藏数、评论数、分享数、浏览量
标签话题标签列表、@用户、关联话题、IP属地
位置信息地理位置名称、经纬度
商品信息关联商品、购买链接(带货笔记)
相关推荐相关笔记列表

1.2 接口特点

重要提示:小红书并未公开对外提供正式的开放 API,官方接口主要面向合作品牌方和广告主开放。开发者通常通过以下方式获取数据:
  1. 小红书开放平台(需企业认证申请)
  2. 第三方数据服务(聚合 API)
  3. RPA/逆向工程(合规风险较高)

二、接口结构与请求规范

2.1 请求方式与地址

plain
请求方式: HTTP GET / POST
请求地址: https://www.xiaohongshu.com/api/sns/v1/note/{note_id}/detail
          或第三方聚合接口: https://api.example.com/xhs/note/detail

2.2 请求参数

表格
参数名类型必选说明
note_idString笔记唯一标识(24位十六进制字符串)
access_tokenString条件OAuth2.0 授权令牌(官方接口)
api_keyString条件第三方接口密钥
sourceString来源标识(web/app)
timestampLong时间戳(防重放攻击)

2.3 响应数据结构

JSON
{
  "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 安装依赖

bash
pip install requests
pip install pytest
pip install pytest-html        # 测试报告生成pip install allure-pytest      # Allure 测试报告pip install python-dotenv      # 环境变量管理pip install loguru             # 日志记录

3.2 项目结构

plain
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 客户端封装

Python
# utils/api_client.pyimport requestsimport timeimport hashlibimport jsonfrom typing import Dict, Optionalfrom loguru import loggerclass 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 测试用例设计

Python
# testcases/test_note_detail.pyimport pytestimport allurefrom 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.pyimport randomimport stringclass 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 配置

ini
# pytest.ini[pytest]testpaths = testcasespython_files = test_*.pypython_classes = Test*python_functions = test_*addopts = -v --tb=short --html=reports/report.html --self-contained-htmlmarkers =
    smoke: 冒烟测试
    regression: 回归测试
    stress: 压力测试
    slow: 慢速测试

五、测试执行与报告

5.1 执行测试

bash
# 执行全部测试pytest# 执行冒烟测试pytest -m smoke# 生成 Allure 报告pytest --alluredir=reports/allure
allure serve reports/allure# 执行压力测试pytest -m stress -n 4

5.2 测试报告示例

plain
============================= 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: 403IP 被封禁或缺少权限更换 IP,检查 API Key 权限
code: 429请求频率过高触发限流降低请求频率,使用指数退避
data: null笔记已删除或私密跳过该笔记,记录日志
字段类型不匹配API 版本变更更新测试断言,关注接口文档
视频 URL 失效CDN 链接过期验证 URL 可访问性,使用备用地址

七、合规注意事项

  1. 数据使用范围:仅用于自有业务分析,不得转售或商用
  2. 隐私保护:用户个人信息需脱敏处理
  3. 频率控制:遵守平台限流策略,建议 ≤ 10 次/秒
  4. 官方授权:生产环境建议使用小红书开放平台正式接口

八、总结

表格
测试维度覆盖内容用例数量
功能测试正常请求、参数校验、数据完整性7
异常测试非法参数、空值、越界3
性能测试响应时间、并发、限流2
数据校验字段类型、逻辑关系、范围贯穿全部
通过系统化的接口测试,可以确保小红书笔记详情接口的稳定性和数据准确性,为后续的内容分析、数据挖掘等业务场景奠定坚实基础。


如遇任何疑问或有进一步的需求,请随时与我私信或者评论联系

返回列表

上一篇:接口调用的代码实现:从入门到实战

没有最新的文章了...

相关文章

小红书笔记视频详情接口深度解析:smallredbook.item_get_video_pro

一、接口概述smallredbook.item_get_video_pro 是小红书开放平台提供的一款专业级笔记视频详情获取接口,是标准版 item_get_video 的增强版本。该接口专为电商场景...

某书关键词搜索商品接口(smallredbook.item_search)返回值全景解读

一、接口定位smallredbook.item_search 是小红书开放平台提供的「关键词搜索商品」标准接口。一句话:给定任意关键词,一次性拿到 20 个结构化商品卡片,可用于选品、比价、内容挂车、...

示例代码解析:使用 item_get_video_pro 获取小红书笔记详情

一、功能介绍item_get_video_pro 是一个用于获取小红书(Xiaohongshu)笔记详情的接口,支持获取包括标题、描述、点赞数、评论数、封面图、视频链接等在内的完整笔记信息。该接口适用...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。