#!/usr/bin/env python3 """ 小红书搜索API测试脚本 用于测试和调试 xhs_note_search API 接口 """ import requests import json import time import sys from datetime import datetime from typing import Dict, Any, List, Tuple from xiaohongshu_search import XiaohongshuSearch # 颜色输出 class Colors: """终端颜色输出""" GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' BLUE = '\033[94m' CYAN = '\033[96m' RESET = '\033[0m' BOLD = '\033[1m' class TestResult: """测试结果记录""" def __init__(self): self.total = 0 self.passed = 0 self.failed = 0 self.errors = [] self.details = [] class XiaohongshuAPITester: """小红书API测试器""" BASE_URL = "http://47.84.182.56:8001" API_ENDPOINT = "/tools/call/xhs_note_search" def __init__(self, verbose: bool = True): """ 初始化测试器 Args: verbose: 是否输出详细日志 """ self.verbose = verbose self.result = TestResult() self.client = XiaohongshuSearch() def log(self, message: str, color: str = Colors.RESET): """输出日志""" if self.verbose: print(f"{color}{message}{Colors.RESET}") def log_section(self, title: str): """输出章节标题""" print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}") print(f"{Colors.BOLD}{Colors.CYAN}{title}{Colors.RESET}") print(f"{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}\n") def test_raw_api_request( self, keyword: str, content_type: str = "不限", sort_type: str = "综合", publish_time: str = "不限", cursor: str = "", timeout: int = 30 ) -> Tuple[bool, Dict[str, Any], float]: """ 直接测试原始API请求(不使用封装类) Returns: (是否成功, 响应数据, 响应时间) """ url = f"{self.BASE_URL}{self.API_ENDPOINT}" payload = { "keyword": keyword, "content_type": content_type, "sort_type": sort_type, "publish_time": publish_time, "cursor": cursor } self.log(f"\n{Colors.YELLOW}[请求详情]{Colors.RESET}") self.log(f"URL: {url}") self.log(f"Method: POST") self.log(f"Headers: {{'Content-Type': 'application/json'}}") self.log(f"Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}") start_time = time.time() try: response = requests.post( url, json=payload, timeout=timeout, headers={"Content-Type": "application/json"} ) elapsed = time.time() - start_time self.log(f"\n{Colors.YELLOW}[响应详情]{Colors.RESET}") self.log(f"状态码: {response.status_code}") self.log(f"响应时间: {elapsed:.2f}秒") self.log(f"响应头: {dict(response.headers)}") # 尝试解析JSON try: result = response.json() self.log(f"响应体预览: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...") # 检查HTTP状态码 if response.status_code == 200: return True, result, elapsed else: self.log(f"{Colors.RED}HTTP错误: {response.status_code}{Colors.RESET}") return False, {"error": f"HTTP {response.status_code}", "detail": result}, elapsed except json.JSONDecodeError as e: self.log(f"{Colors.RED}JSON解析失败: {e}{Colors.RESET}") self.log(f"原始响应: {response.text[:500]}") return False, {"error": "JSON解析失败", "detail": str(e), "raw": response.text}, elapsed except requests.exceptions.Timeout: elapsed = time.time() - start_time self.log(f"{Colors.RED}请求超时({timeout}秒){Colors.RESET}") return False, {"error": "Timeout", "timeout": timeout}, elapsed except requests.exceptions.ConnectionError as e: elapsed = time.time() - start_time self.log(f"{Colors.RED}连接错误: {e}{Colors.RESET}") return False, {"error": "ConnectionError", "detail": str(e)}, elapsed except requests.exceptions.RequestException as e: elapsed = time.time() - start_time self.log(f"{Colors.RED}请求异常: {e}{Colors.RESET}") return False, {"error": "RequestException", "detail": str(e)}, elapsed def run_test(self, test_name: str, test_func, *args, **kwargs) -> bool: """ 运行单个测试 Returns: 是否通过 """ self.result.total += 1 self.log(f"\n{Colors.BLUE}[测试 {self.result.total}] {test_name}{Colors.RESET}") try: success, data, elapsed = test_func(*args, **kwargs) if success: self.result.passed += 1 self.log(f"{Colors.GREEN}✓ 通过{Colors.RESET} (耗时: {elapsed:.2f}秒)") self.result.details.append({ "name": test_name, "status": "PASS", "elapsed": elapsed }) return True else: self.result.failed += 1 error_info = { "name": test_name, "error": data.get("error", "Unknown"), "detail": data.get("detail", "") } self.result.errors.append(error_info) self.result.details.append({ "name": test_name, "status": "FAIL", "elapsed": elapsed, "error": error_info }) self.log(f"{Colors.RED}✗ 失败{Colors.RESET}") self.log(f"{Colors.RED}错误: {error_info}{Colors.RESET}") return False except Exception as e: self.result.failed += 1 error_info = { "name": test_name, "error": "Exception", "detail": str(e) } self.result.errors.append(error_info) self.result.details.append({ "name": test_name, "status": "ERROR", "error": error_info }) self.log(f"{Colors.RED}✗ 异常: {e}{Colors.RESET}") return False def test_basic_keywords(self): """测试基础关键词搜索""" self.log_section("1. 基础关键词测试") # 测试简单关键词 self.run_test( "简单关键词: 美食", self.test_raw_api_request, keyword="美食" ) # 测试复杂中文关键词 self.run_test( "复杂关键词: 表情包怎么制作", self.test_raw_api_request, keyword="表情包怎么制作" ) # 测试英文关键词 self.run_test( "英文关键词: food", self.test_raw_api_request, keyword="food" ) # 测试带特殊符号的关键词 self.run_test( "特殊符号: Python编程#入门", self.test_raw_api_request, keyword="Python编程#入门" ) def test_content_types(self): """测试不同内容类型""" self.log_section("2. 内容类型测试") keyword = "美食" for content_type in ["不限", "视频", "图文"]: self.run_test( f"内容类型: {content_type}", self.test_raw_api_request, keyword=keyword, content_type=content_type ) def test_sort_types(self): """测试不同排序方式""" self.log_section("3. 排序方式测试") keyword = "旅游" for sort_type in ["综合", "最新", "最多点赞", "最多评论"]: self.run_test( f"排序方式: {sort_type}", self.test_raw_api_request, keyword=keyword, sort_type=sort_type ) def test_publish_times(self): """测试不同发布时间筛选""" self.log_section("4. 发布时间筛选测试") keyword = "健身" for publish_time in ["不限", "一天内", "一周内", "半年内"]: self.run_test( f"发布时间: {publish_time}", self.test_raw_api_request, keyword=keyword, publish_time=publish_time ) def test_edge_cases(self): """测试边缘情况""" self.log_section("5. 边缘情况测试") # 空关键词 self.run_test( "空关键词", self.test_raw_api_request, keyword="" ) # 超长关键词 self.run_test( "超长关键词(100字符)", self.test_raw_api_request, keyword="如何" * 50 ) # 纯空格 self.run_test( "纯空格关键词", self.test_raw_api_request, keyword=" " ) # 超时测试(设置1秒超时) self.run_test( "超时测试(1秒)", self.test_raw_api_request, keyword="美食", timeout=1 ) def test_invalid_parameters(self): """测试无效参数""" self.log_section("6. 无效参数测试") # 无效的content_type self.run_test( "无效content_type: 音频", self.test_raw_api_request, keyword="音乐", content_type="音频" ) # 无效的sort_type self.run_test( "无效sort_type: 随机", self.test_raw_api_request, keyword="音乐", sort_type="随机" ) def test_using_wrapper_class(self): """测试使用封装类""" self.log_section("7. 封装类测试") def test_wrapper(keyword: str, **kwargs) -> Tuple[bool, Dict, float]: """使用XiaohongshuSearch类进行测试""" start_time = time.time() try: result = self.client.search(keyword, **kwargs) elapsed = time.time() - start_time return True, result, elapsed except Exception as e: elapsed = time.time() - start_time return False, {"error": str(e)}, elapsed self.run_test( "封装类: 基础搜索", test_wrapper, keyword="美食" ) self.run_test( "封装类: 完整参数", test_wrapper, keyword="旅游", content_type="视频", sort_type="最新", publish_time="一周内" ) def generate_report(self) -> Dict[str, Any]: """生成测试报告""" self.log_section("测试报告") report = { "timestamp": datetime.now().isoformat(), "summary": { "total": self.result.total, "passed": self.result.passed, "failed": self.result.failed, "pass_rate": f"{(self.result.passed/self.result.total*100):.1f}%" if self.result.total > 0 else "N/A" }, "details": self.result.details, "errors": self.result.errors } # 打印摘要 print(f"\n{Colors.BOLD}测试摘要:{Colors.RESET}") print(f" 总计: {self.result.total}") print(f" {Colors.GREEN}通过: {self.result.passed}{Colors.RESET}") print(f" {Colors.RED}失败: {self.result.failed}{Colors.RESET}") print(f" 通过率: {report['summary']['pass_rate']}") if self.result.errors: print(f"\n{Colors.RED}{Colors.BOLD}失败详情:{Colors.RESET}") for i, error in enumerate(self.result.errors, 1): print(f"{Colors.RED} {i}. {error['name']}{Colors.RESET}") print(f" 错误: {error['error']}") if error.get('detail'): print(f" 详情: {error['detail'][:200]}") return report def save_report(self, filepath: str = None): """保存测试报告""" if filepath is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filepath = f"test_report_{timestamp}.json" report = self.generate_report() with open(filepath, 'w', encoding='utf-8') as f: json.dump(report, f, ensure_ascii=False, indent=2) print(f"\n{Colors.GREEN}报告已保存: {filepath}{Colors.RESET}") return filepath def run_all_tests(self): """运行所有测试""" print(f"{Colors.BOLD}{Colors.CYAN}") print("=" * 70) print("小红书搜索API测试") print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 70) print(Colors.RESET) # 运行所有测试 self.test_basic_keywords() self.test_content_types() self.test_sort_types() self.test_publish_times() self.test_edge_cases() self.test_invalid_parameters() self.test_using_wrapper_class() # 生成并保存报告 return self.save_report() def run_quick_test(keyword: str = "表情包怎么制作"): """ 快速测试单个关键词 Args: keyword: 要测试的关键词 """ tester = XiaohongshuAPITester(verbose=True) print(f"{Colors.BOLD}{Colors.CYAN}") print("=" * 70) print(f"快速测试: {keyword}") print("=" * 70) print(Colors.RESET) tester.run_test( f"测试关键词: {keyword}", tester.test_raw_api_request, keyword=keyword ) tester.generate_report() def main(): """主函数""" import argparse parser = argparse.ArgumentParser(description='小红书搜索API测试工具') parser.add_argument( '--mode', type=str, choices=['full', 'quick'], default='quick', help='测试模式: full(完整测试) 或 quick(快速测试)' ) parser.add_argument( '--keyword', type=str, default='表情包怎么制作', help='快速测试的关键词' ) parser.add_argument( '--verbose', action='store_true', default=True, help='输出详细日志' ) args = parser.parse_args() if args.mode == 'full': tester = XiaohongshuAPITester(verbose=args.verbose) tester.run_all_tests() else: run_quick_test(args.keyword) if __name__ == "__main__": main()