| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- #!/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()
|