xiaohongshu.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Xiaohongshu formal acquisition adapter."""
  2. from __future__ import annotations
  3. from typing import Any, Iterable
  4. from acquisition.content_mode import infer_content_mode
  5. from acquisition.crawler import fetch_post_detail
  6. from acquisition.platforms.base import PlatformCandidate, PlatformItem, PlatformSearchPage
  7. from acquisition.search import search_xiaohongshu_page
  8. from core.config import Settings
  9. class XiaohongshuAdapter:
  10. platform = "xiaohongshu"
  11. def search(
  12. self,
  13. query: str,
  14. *,
  15. settings: Settings,
  16. limit: int,
  17. rate_limiter: Any,
  18. ) -> list[PlatformCandidate]:
  19. pages = self.search_pages(
  20. query,
  21. settings=settings,
  22. limit=limit,
  23. rate_limiter=rate_limiter,
  24. max_pages=1,
  25. )
  26. return [candidate for page in pages for candidate in page.candidates]
  27. def search_pages(
  28. self,
  29. query: str,
  30. *,
  31. settings: Settings,
  32. limit: int,
  33. rate_limiter: Any,
  34. max_pages: int = 2,
  35. ) -> Iterable[PlatformSearchPage]:
  36. cursor = ""
  37. rank = 1
  38. seen: set[str] = set()
  39. for page_index in range(1, max_pages + 1):
  40. page = search_xiaohongshu_page(
  41. query,
  42. cursor=cursor,
  43. content_type=None,
  44. limit=limit,
  45. settings=settings,
  46. rate_limiter=rate_limiter,
  47. )
  48. candidates: list[PlatformCandidate] = []
  49. for page_rank, row in enumerate(page.rows, start=1):
  50. source_id = row.get("id") or ""
  51. if not source_id or source_id in seen:
  52. continue
  53. seen.add(source_id)
  54. raw = {
  55. **row,
  56. "page_index": page_index,
  57. "page_rank": page_rank,
  58. "source_cursor": page.cursor,
  59. "request_payload": page.request_payload,
  60. }
  61. candidates.append(
  62. PlatformCandidate(
  63. rank=rank,
  64. platform=self.platform,
  65. source_id=source_id,
  66. url=row.get("url") or "",
  67. title=row.get("title") or "",
  68. author=row.get("nick_name") or "",
  69. cover_url=row.get("cover_url") or "",
  70. raw=raw,
  71. )
  72. )
  73. rank += 1
  74. yield PlatformSearchPage(
  75. page_index=page_index,
  76. candidates=candidates,
  77. raw_count=page.raw_count,
  78. cursor=page.cursor,
  79. next_cursor=page.next_cursor,
  80. has_more=page.has_more,
  81. provider=page.provider,
  82. request_payload=page.request_payload,
  83. )
  84. if not page.has_more or not page.next_cursor or page.next_cursor == cursor:
  85. break
  86. cursor = page.next_cursor
  87. def fetch_detail(
  88. self,
  89. candidate: PlatformCandidate,
  90. *,
  91. settings: Settings,
  92. rate_limiter: Any,
  93. ) -> PlatformItem:
  94. post = fetch_post_detail(
  95. candidate.source_id or candidate.url,
  96. settings=settings,
  97. rate_limiter=rate_limiter,
  98. )
  99. image_urls = post.image_urls or ([candidate.cover_url] if candidate.cover_url else [])
  100. content_mode = infer_content_mode(
  101. platform=self.platform,
  102. content_type=post.content_type,
  103. body_text=post.body_text,
  104. image_urls=image_urls,
  105. video_urls=post.video_urls,
  106. raw={**candidate.raw, "detail_content_type": post.content_type},
  107. )
  108. return PlatformItem(
  109. platform=self.platform,
  110. source_id=post.content_id or candidate.source_id,
  111. url=post.url or candidate.url,
  112. content_type=post.content_type or "",
  113. content_mode=content_mode,
  114. title=post.title or candidate.title,
  115. author=post.author_name or candidate.author,
  116. body_text=post.body_text or "",
  117. image_urls=image_urls,
  118. video_urls=post.video_urls,
  119. raw=post.raw if isinstance(post.raw, dict) else {},
  120. )