test_xiaohongshu_search_api.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. #!/usr/bin/env python3
  2. """
  3. 小红书搜索API测试脚本
  4. 用于测试和调试 xhs_note_search API 接口
  5. """
  6. import requests
  7. import json
  8. import time
  9. import sys
  10. from datetime import datetime
  11. from typing import Dict, Any, List, Tuple
  12. from xiaohongshu_search import XiaohongshuSearch
  13. # 颜色输出
  14. class Colors:
  15. """终端颜色输出"""
  16. GREEN = '\033[92m'
  17. RED = '\033[91m'
  18. YELLOW = '\033[93m'
  19. BLUE = '\033[94m'
  20. CYAN = '\033[96m'
  21. RESET = '\033[0m'
  22. BOLD = '\033[1m'
  23. class TestResult:
  24. """测试结果记录"""
  25. def __init__(self):
  26. self.total = 0
  27. self.passed = 0
  28. self.failed = 0
  29. self.errors = []
  30. self.details = []
  31. class XiaohongshuAPITester:
  32. """小红书API测试器"""
  33. BASE_URL = "http://47.84.182.56:8001"
  34. API_ENDPOINT = "/tools/call/xhs_note_search"
  35. def __init__(self, verbose: bool = True):
  36. """
  37. 初始化测试器
  38. Args:
  39. verbose: 是否输出详细日志
  40. """
  41. self.verbose = verbose
  42. self.result = TestResult()
  43. self.client = XiaohongshuSearch()
  44. def log(self, message: str, color: str = Colors.RESET):
  45. """输出日志"""
  46. if self.verbose:
  47. print(f"{color}{message}{Colors.RESET}")
  48. def log_section(self, title: str):
  49. """输出章节标题"""
  50. print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}")
  51. print(f"{Colors.BOLD}{Colors.CYAN}{title}{Colors.RESET}")
  52. print(f"{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}\n")
  53. def test_raw_api_request(
  54. self,
  55. keyword: str,
  56. content_type: str = "不限",
  57. sort_type: str = "综合",
  58. publish_time: str = "不限",
  59. cursor: str = "",
  60. timeout: int = 30
  61. ) -> Tuple[bool, Dict[str, Any], float]:
  62. """
  63. 直接测试原始API请求(不使用封装类)
  64. Returns:
  65. (是否成功, 响应数据, 响应时间)
  66. """
  67. url = f"{self.BASE_URL}{self.API_ENDPOINT}"
  68. payload = {
  69. "keyword": keyword,
  70. "content_type": content_type,
  71. "sort_type": sort_type,
  72. "publish_time": publish_time,
  73. "cursor": cursor
  74. }
  75. self.log(f"\n{Colors.YELLOW}[请求详情]{Colors.RESET}")
  76. self.log(f"URL: {url}")
  77. self.log(f"Method: POST")
  78. self.log(f"Headers: {{'Content-Type': 'application/json'}}")
  79. self.log(f"Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
  80. start_time = time.time()
  81. try:
  82. response = requests.post(
  83. url,
  84. json=payload,
  85. timeout=timeout,
  86. headers={"Content-Type": "application/json"}
  87. )
  88. elapsed = time.time() - start_time
  89. self.log(f"\n{Colors.YELLOW}[响应详情]{Colors.RESET}")
  90. self.log(f"状态码: {response.status_code}")
  91. self.log(f"响应时间: {elapsed:.2f}秒")
  92. self.log(f"响应头: {dict(response.headers)}")
  93. # 尝试解析JSON
  94. try:
  95. result = response.json()
  96. self.log(f"响应体预览: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...")
  97. # 检查HTTP状态码
  98. if response.status_code == 200:
  99. return True, result, elapsed
  100. else:
  101. self.log(f"{Colors.RED}HTTP错误: {response.status_code}{Colors.RESET}")
  102. return False, {"error": f"HTTP {response.status_code}", "detail": result}, elapsed
  103. except json.JSONDecodeError as e:
  104. self.log(f"{Colors.RED}JSON解析失败: {e}{Colors.RESET}")
  105. self.log(f"原始响应: {response.text[:500]}")
  106. return False, {"error": "JSON解析失败", "detail": str(e), "raw": response.text}, elapsed
  107. except requests.exceptions.Timeout:
  108. elapsed = time.time() - start_time
  109. self.log(f"{Colors.RED}请求超时({timeout}秒){Colors.RESET}")
  110. return False, {"error": "Timeout", "timeout": timeout}, elapsed
  111. except requests.exceptions.ConnectionError as e:
  112. elapsed = time.time() - start_time
  113. self.log(f"{Colors.RED}连接错误: {e}{Colors.RESET}")
  114. return False, {"error": "ConnectionError", "detail": str(e)}, elapsed
  115. except requests.exceptions.RequestException as e:
  116. elapsed = time.time() - start_time
  117. self.log(f"{Colors.RED}请求异常: {e}{Colors.RESET}")
  118. return False, {"error": "RequestException", "detail": str(e)}, elapsed
  119. def run_test(self, test_name: str, test_func, *args, **kwargs) -> bool:
  120. """
  121. 运行单个测试
  122. Returns:
  123. 是否通过
  124. """
  125. self.result.total += 1
  126. self.log(f"\n{Colors.BLUE}[测试 {self.result.total}] {test_name}{Colors.RESET}")
  127. try:
  128. success, data, elapsed = test_func(*args, **kwargs)
  129. if success:
  130. self.result.passed += 1
  131. self.log(f"{Colors.GREEN}✓ 通过{Colors.RESET} (耗时: {elapsed:.2f}秒)")
  132. self.result.details.append({
  133. "name": test_name,
  134. "status": "PASS",
  135. "elapsed": elapsed
  136. })
  137. return True
  138. else:
  139. self.result.failed += 1
  140. error_info = {
  141. "name": test_name,
  142. "error": data.get("error", "Unknown"),
  143. "detail": data.get("detail", "")
  144. }
  145. self.result.errors.append(error_info)
  146. self.result.details.append({
  147. "name": test_name,
  148. "status": "FAIL",
  149. "elapsed": elapsed,
  150. "error": error_info
  151. })
  152. self.log(f"{Colors.RED}✗ 失败{Colors.RESET}")
  153. self.log(f"{Colors.RED}错误: {error_info}{Colors.RESET}")
  154. return False
  155. except Exception as e:
  156. self.result.failed += 1
  157. error_info = {
  158. "name": test_name,
  159. "error": "Exception",
  160. "detail": str(e)
  161. }
  162. self.result.errors.append(error_info)
  163. self.result.details.append({
  164. "name": test_name,
  165. "status": "ERROR",
  166. "error": error_info
  167. })
  168. self.log(f"{Colors.RED}✗ 异常: {e}{Colors.RESET}")
  169. return False
  170. def test_basic_keywords(self):
  171. """测试基础关键词搜索"""
  172. self.log_section("1. 基础关键词测试")
  173. # 测试简单关键词
  174. self.run_test(
  175. "简单关键词: 美食",
  176. self.test_raw_api_request,
  177. keyword="美食"
  178. )
  179. # 测试复杂中文关键词
  180. self.run_test(
  181. "复杂关键词: 表情包怎么制作",
  182. self.test_raw_api_request,
  183. keyword="表情包怎么制作"
  184. )
  185. # 测试英文关键词
  186. self.run_test(
  187. "英文关键词: food",
  188. self.test_raw_api_request,
  189. keyword="food"
  190. )
  191. # 测试带特殊符号的关键词
  192. self.run_test(
  193. "特殊符号: Python编程#入门",
  194. self.test_raw_api_request,
  195. keyword="Python编程#入门"
  196. )
  197. def test_content_types(self):
  198. """测试不同内容类型"""
  199. self.log_section("2. 内容类型测试")
  200. keyword = "美食"
  201. for content_type in ["不限", "视频", "图文"]:
  202. self.run_test(
  203. f"内容类型: {content_type}",
  204. self.test_raw_api_request,
  205. keyword=keyword,
  206. content_type=content_type
  207. )
  208. def test_sort_types(self):
  209. """测试不同排序方式"""
  210. self.log_section("3. 排序方式测试")
  211. keyword = "旅游"
  212. for sort_type in ["综合", "最新", "最多点赞", "最多评论"]:
  213. self.run_test(
  214. f"排序方式: {sort_type}",
  215. self.test_raw_api_request,
  216. keyword=keyword,
  217. sort_type=sort_type
  218. )
  219. def test_publish_times(self):
  220. """测试不同发布时间筛选"""
  221. self.log_section("4. 发布时间筛选测试")
  222. keyword = "健身"
  223. for publish_time in ["不限", "一天内", "一周内", "半年内"]:
  224. self.run_test(
  225. f"发布时间: {publish_time}",
  226. self.test_raw_api_request,
  227. keyword=keyword,
  228. publish_time=publish_time
  229. )
  230. def test_edge_cases(self):
  231. """测试边缘情况"""
  232. self.log_section("5. 边缘情况测试")
  233. # 空关键词
  234. self.run_test(
  235. "空关键词",
  236. self.test_raw_api_request,
  237. keyword=""
  238. )
  239. # 超长关键词
  240. self.run_test(
  241. "超长关键词(100字符)",
  242. self.test_raw_api_request,
  243. keyword="如何" * 50
  244. )
  245. # 纯空格
  246. self.run_test(
  247. "纯空格关键词",
  248. self.test_raw_api_request,
  249. keyword=" "
  250. )
  251. # 超时测试(设置1秒超时)
  252. self.run_test(
  253. "超时测试(1秒)",
  254. self.test_raw_api_request,
  255. keyword="美食",
  256. timeout=1
  257. )
  258. def test_invalid_parameters(self):
  259. """测试无效参数"""
  260. self.log_section("6. 无效参数测试")
  261. # 无效的content_type
  262. self.run_test(
  263. "无效content_type: 音频",
  264. self.test_raw_api_request,
  265. keyword="音乐",
  266. content_type="音频"
  267. )
  268. # 无效的sort_type
  269. self.run_test(
  270. "无效sort_type: 随机",
  271. self.test_raw_api_request,
  272. keyword="音乐",
  273. sort_type="随机"
  274. )
  275. def test_using_wrapper_class(self):
  276. """测试使用封装类"""
  277. self.log_section("7. 封装类测试")
  278. def test_wrapper(keyword: str, **kwargs) -> Tuple[bool, Dict, float]:
  279. """使用XiaohongshuSearch类进行测试"""
  280. start_time = time.time()
  281. try:
  282. result = self.client.search(keyword, **kwargs)
  283. elapsed = time.time() - start_time
  284. return True, result, elapsed
  285. except Exception as e:
  286. elapsed = time.time() - start_time
  287. return False, {"error": str(e)}, elapsed
  288. self.run_test(
  289. "封装类: 基础搜索",
  290. test_wrapper,
  291. keyword="美食"
  292. )
  293. self.run_test(
  294. "封装类: 完整参数",
  295. test_wrapper,
  296. keyword="旅游",
  297. content_type="视频",
  298. sort_type="最新",
  299. publish_time="一周内"
  300. )
  301. def generate_report(self) -> Dict[str, Any]:
  302. """生成测试报告"""
  303. self.log_section("测试报告")
  304. report = {
  305. "timestamp": datetime.now().isoformat(),
  306. "summary": {
  307. "total": self.result.total,
  308. "passed": self.result.passed,
  309. "failed": self.result.failed,
  310. "pass_rate": f"{(self.result.passed/self.result.total*100):.1f}%" if self.result.total > 0 else "N/A"
  311. },
  312. "details": self.result.details,
  313. "errors": self.result.errors
  314. }
  315. # 打印摘要
  316. print(f"\n{Colors.BOLD}测试摘要:{Colors.RESET}")
  317. print(f" 总计: {self.result.total}")
  318. print(f" {Colors.GREEN}通过: {self.result.passed}{Colors.RESET}")
  319. print(f" {Colors.RED}失败: {self.result.failed}{Colors.RESET}")
  320. print(f" 通过率: {report['summary']['pass_rate']}")
  321. if self.result.errors:
  322. print(f"\n{Colors.RED}{Colors.BOLD}失败详情:{Colors.RESET}")
  323. for i, error in enumerate(self.result.errors, 1):
  324. print(f"{Colors.RED} {i}. {error['name']}{Colors.RESET}")
  325. print(f" 错误: {error['error']}")
  326. if error.get('detail'):
  327. print(f" 详情: {error['detail'][:200]}")
  328. return report
  329. def save_report(self, filepath: str = None):
  330. """保存测试报告"""
  331. if filepath is None:
  332. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  333. filepath = f"test_report_{timestamp}.json"
  334. report = self.generate_report()
  335. with open(filepath, 'w', encoding='utf-8') as f:
  336. json.dump(report, f, ensure_ascii=False, indent=2)
  337. print(f"\n{Colors.GREEN}报告已保存: {filepath}{Colors.RESET}")
  338. return filepath
  339. def run_all_tests(self):
  340. """运行所有测试"""
  341. print(f"{Colors.BOLD}{Colors.CYAN}")
  342. print("=" * 70)
  343. print("小红书搜索API测试")
  344. print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  345. print("=" * 70)
  346. print(Colors.RESET)
  347. # 运行所有测试
  348. self.test_basic_keywords()
  349. self.test_content_types()
  350. self.test_sort_types()
  351. self.test_publish_times()
  352. self.test_edge_cases()
  353. self.test_invalid_parameters()
  354. self.test_using_wrapper_class()
  355. # 生成并保存报告
  356. return self.save_report()
  357. def run_quick_test(keyword: str = "表情包怎么制作"):
  358. """
  359. 快速测试单个关键词
  360. Args:
  361. keyword: 要测试的关键词
  362. """
  363. tester = XiaohongshuAPITester(verbose=True)
  364. print(f"{Colors.BOLD}{Colors.CYAN}")
  365. print("=" * 70)
  366. print(f"快速测试: {keyword}")
  367. print("=" * 70)
  368. print(Colors.RESET)
  369. tester.run_test(
  370. f"测试关键词: {keyword}",
  371. tester.test_raw_api_request,
  372. keyword=keyword
  373. )
  374. tester.generate_report()
  375. def main():
  376. """主函数"""
  377. import argparse
  378. parser = argparse.ArgumentParser(description='小红书搜索API测试工具')
  379. parser.add_argument(
  380. '--mode',
  381. type=str,
  382. choices=['full', 'quick'],
  383. default='quick',
  384. help='测试模式: full(完整测试) 或 quick(快速测试)'
  385. )
  386. parser.add_argument(
  387. '--keyword',
  388. type=str,
  389. default='表情包怎么制作',
  390. help='快速测试的关键词'
  391. )
  392. parser.add_argument(
  393. '--verbose',
  394. action='store_true',
  395. default=True,
  396. help='输出详细日志'
  397. )
  398. args = parser.parse_args()
  399. if args.mode == 'full':
  400. tester = XiaohongshuAPITester(verbose=args.verbose)
  401. tester.run_all_tests()
  402. else:
  403. run_quick_test(args.keyword)
  404. if __name__ == "__main__":
  405. main()