xiaohongshu_search.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. #!/usr/bin/env python3
  2. """
  3. 小红书笔记搜索工具
  4. 根据关键词搜索小红书笔记,支持多种筛选条件
  5. """
  6. import requests
  7. import json
  8. import os
  9. import argparse
  10. import time
  11. from datetime import datetime
  12. from typing import Dict, Any
  13. class XiaohongshuSearch:
  14. """小红书笔记搜索API封装类"""
  15. BASE_URL = "http://47.84.182.56:8001"
  16. TOOL_NAME = "xhs_note_search"
  17. PLATFORM = "xiaohongshu"
  18. def __init__(self, results_dir: str = None):
  19. """
  20. 初始化API客户端
  21. Args:
  22. results_dir: 结果输出目录,默认为项目根目录下的 data/search 文件夹
  23. """
  24. self.api_url = f"{self.BASE_URL}/tools/call/{self.TOOL_NAME}"
  25. # 设置结果输出目录
  26. if results_dir:
  27. self.results_base_dir = results_dir
  28. else:
  29. # 默认使用项目根目录的 data/search 文件夹
  30. script_dir = os.path.dirname(os.path.abspath(__file__))
  31. project_root = os.path.dirname(os.path.dirname(script_dir))
  32. self.results_base_dir = os.path.join(project_root, "data", "search")
  33. def search(
  34. self,
  35. keyword: str,
  36. content_type: str = "不限",
  37. sort_type: str = "综合",
  38. publish_time: str = "不限",
  39. cursor: str = "",
  40. timeout: int = 30,
  41. max_retries: int = 3,
  42. retry_delay: int = 2
  43. ) -> Dict[str, Any]:
  44. """
  45. 搜索小红书笔记(带重试机制)
  46. Args:
  47. keyword: 搜索关键词
  48. content_type: 内容类型,可选值:不限、视频、图文,默认为'不限'
  49. sort_type: 排序方式,可选值:综合、最新、最多点赞、最多评论,默认为'综合'
  50. publish_time: 发布时间筛选,可选值:不限、一天内、一周内、半年内,默认为'不限'
  51. cursor: 翻页游标,第一页默认为空,下一页的游标在上一页的返回值中获取
  52. timeout: 请求超时时间(秒),默认30秒
  53. max_retries: 最大重试次数,默认3次
  54. retry_delay: 重试间隔时间(秒),默认2秒
  55. Returns:
  56. API响应的JSON数据
  57. Raises:
  58. requests.exceptions.RequestException: 所有重试都失败时抛出异常
  59. """
  60. payload = {
  61. "keyword": keyword,
  62. "content_type": content_type,
  63. "sort_type": sort_type,
  64. "publish_time": publish_time,
  65. "cursor": cursor
  66. }
  67. last_exception = None
  68. # 重试循环:最多尝试 max_retries 次
  69. for attempt in range(1, max_retries + 1):
  70. try:
  71. if attempt > 1:
  72. print(f" 重试第 {attempt - 1}/{max_retries - 1} 次: {keyword}")
  73. response = requests.post(
  74. self.api_url,
  75. json=payload,
  76. timeout=timeout,
  77. headers={"Content-Type": "application/json"}
  78. )
  79. response.raise_for_status()
  80. api_response = response.json()
  81. # 解析API返回的result字段(是JSON字符串)
  82. if not api_response.get("success"):
  83. raise Exception(f"API返回失败: {api_response}")
  84. result_str = api_response.get("result", "{}")
  85. result = json.loads(result_str)
  86. # 预处理返回数据:提取 image_list 中的 URL 字符串
  87. self._preprocess_response(result)
  88. if attempt > 1:
  89. print(f" ✓ 重试成功")
  90. return result
  91. except requests.exceptions.RequestException as e:
  92. last_exception = e
  93. if attempt < max_retries:
  94. # 还有重试机会,等待后继续
  95. print(f" ✗ 请求失败 (第{attempt}次尝试): {e}")
  96. print(f" 等待 {retry_delay} 秒后重试...")
  97. time.sleep(retry_delay)
  98. else:
  99. # 已达最大重试次数,抛出异常
  100. print(f" ✗ 请求失败 (已达最大重试次数 {max_retries}): {e}")
  101. # 所有重试都失败,抛出最后一次的异常
  102. raise last_exception
  103. def _preprocess_response(self, result: Dict[str, Any]) -> None:
  104. """
  105. 预处理搜索结果,将 image_list 中的字典格式转换为 URL 字符串列表
  106. Args:
  107. result: API返回的原始结果字典(会直接修改)
  108. """
  109. # 获取帖子列表
  110. notes = result.get("data", {}).get("data", [])
  111. for note in notes:
  112. note_card = note.get("note_card", {})
  113. image_list_raw = note_card.get("image_list", [])
  114. # 提取 URL 字符串
  115. image_list = []
  116. for img in image_list_raw:
  117. if isinstance(img, dict) and "image_url" in img:
  118. image_list.append(img["image_url"])
  119. elif isinstance(img, str):
  120. # 如果已经是字符串,直接使用
  121. image_list.append(img)
  122. # 更新为预处理后的列表
  123. note_card["image_list"] = image_list
  124. def save_result(self, keyword: str, result: Dict[str, Any], page: int = 1) -> str:
  125. """
  126. 保存结果到文件
  127. 目录结构: results/xiaohongshu_search/关键词/时间戳_page{页码}.json
  128. Args:
  129. keyword: 搜索关键词
  130. result: API返回的结果
  131. page: 页码
  132. Returns:
  133. 保存的文件路径
  134. """
  135. # 创建目录结构: results/xiaohongshu_search/关键词/
  136. result_dir = os.path.join(self.results_base_dir, "xiaohongshu_search", keyword)
  137. os.makedirs(result_dir, exist_ok=True)
  138. # 文件名使用时间戳和页码
  139. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  140. filename = f"{timestamp}_page{page}.json"
  141. filepath = os.path.join(result_dir, filename)
  142. # 保存结果
  143. with open(filepath, 'w', encoding='utf-8') as f:
  144. json.dump(result, f, ensure_ascii=False, indent=2)
  145. return filepath
  146. def main():
  147. """示例使用"""
  148. # 解析命令行参数
  149. parser = argparse.ArgumentParser(description='小红书笔记搜索工具')
  150. parser.add_argument(
  151. '--results-dir',
  152. type=str,
  153. default='data/search',
  154. help='结果输出目录 (默认: data/search)'
  155. )
  156. parser.add_argument(
  157. '--keyword',
  158. type=str,
  159. required=True,
  160. help='搜索关键词 (必填)'
  161. )
  162. parser.add_argument(
  163. '--content-type',
  164. type=str,
  165. default='不限',
  166. choices=['不限', '视频', '图文'],
  167. help='内容类型 (默认: 不限)'
  168. )
  169. parser.add_argument(
  170. '--sort-type',
  171. type=str,
  172. default='综合',
  173. choices=['综合', '最新', '最多点赞', '最多评论'],
  174. help='排序方式 (默认: 综合)'
  175. )
  176. parser.add_argument(
  177. '--publish-time',
  178. type=str,
  179. default='不限',
  180. choices=['不限', '一天内', '一周内', '半年内'],
  181. help='发布时间筛选 (默认: 不限)'
  182. )
  183. parser.add_argument(
  184. '--cursor',
  185. type=str,
  186. default='',
  187. help='翻页游标 (默认为空,即第一页)'
  188. )
  189. parser.add_argument(
  190. '--page',
  191. type=int,
  192. default=1,
  193. help='页码标识,用于保存文件名 (默认: 1)'
  194. )
  195. args = parser.parse_args()
  196. # 创建API客户端实例
  197. client = XiaohongshuSearch(results_dir=args.results_dir)
  198. # 执行搜索并保存
  199. try:
  200. result = client.search(
  201. args.keyword,
  202. args.content_type,
  203. args.sort_type,
  204. args.publish_time,
  205. args.cursor
  206. )
  207. filepath = client.save_result(args.keyword, result, args.page)
  208. print(f"Output: {filepath}")
  209. except Exception as e:
  210. print(f"Error: {e}", file=__import__('sys').stderr)
  211. if __name__ == "__main__":
  212. main()