google_trends.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from datetime import datetime
  4. from pytrends.request import TrendReq
  5. class GoogleTrendsError(RuntimeError):
  6. pass
  7. @dataclass(frozen=True)
  8. class TrendSeries:
  9. keyword: str
  10. timeframe: str
  11. series: list[int]
  12. fetched_at: str
  13. def _normalize_timeframe(timeframe: str) -> str:
  14. tf = str(timeframe).strip().lower()
  15. if tf in {"7d", "7day", "7days"}:
  16. return "today 7-d"
  17. if tf in {"30d", "30day", "30days"}:
  18. return "today 1-m"
  19. if tf in {"90d", "90day", "90days"}:
  20. return "today 3-m"
  21. if tf in {"12m", "1y", "365d"}:
  22. return "today 12-m"
  23. return timeframe
  24. class GoogleTrendsProvider:
  25. def __init__(self):
  26. self._client = TrendReq(hl="en-US", tz=120, retries=2, backoff_factor=0.2)
  27. def suggestions(self, keyword: str) -> list[dict]:
  28. try:
  29. return self._client.suggestions(keyword)
  30. except Exception as exc:
  31. raise GoogleTrendsError(f"suggestions failed for {keyword!r}: {exc}") from exc
  32. def related_queries(self, keyword: str) -> dict:
  33. try:
  34. self._client.build_payload([keyword], timeframe="today 12-m")
  35. return self._client.related_queries() or {}
  36. except Exception as first_exc:
  37. try:
  38. suggestions = self.suggestions(keyword)
  39. topic = next((s["mid"] for s in suggestions if s.get("mid")), None)
  40. if not topic:
  41. raise first_exc
  42. self._client.build_payload([topic], timeframe="today 12-m")
  43. return self._client.related_queries() or {}
  44. except Exception as exc:
  45. raise GoogleTrendsError(f"related_queries failed for {keyword!r}: {exc}") from exc
  46. def related_topics(self, keyword: str) -> dict:
  47. try:
  48. self._client.build_payload([keyword], timeframe="today 12-m")
  49. return self._client.related_topics() or {}
  50. except Exception as first_exc:
  51. try:
  52. suggestions = self.suggestions(keyword)
  53. topic = next((s["mid"] for s in suggestions if s.get("mid")), None)
  54. if not topic:
  55. raise first_exc
  56. self._client.build_payload([topic], timeframe="today 12-m")
  57. return self._client.related_topics() or {}
  58. except Exception as exc:
  59. raise GoogleTrendsError(f"related_topics failed for {keyword!r}: {exc}") from exc
  60. def interest_over_time(self, keyword: str, timeframe: str = "7d") -> TrendSeries:
  61. timeframe = _normalize_timeframe(timeframe)
  62. try:
  63. self._client.build_payload([keyword], timeframe=timeframe)
  64. frame = self._client.interest_over_time()
  65. except Exception as first_exc:
  66. try:
  67. suggestions = self.suggestions(keyword)
  68. topic = next((s["mid"] for s in suggestions if s.get("mid")), None)
  69. if not topic:
  70. raise first_exc
  71. self._client.build_payload([topic], timeframe=timeframe)
  72. frame = self._client.interest_over_time()
  73. keyword = topic
  74. except Exception as exc:
  75. raise GoogleTrendsError(f"interest_over_time failed for {keyword!r} timeframe={timeframe!r}: {exc}") from exc
  76. if frame is None or frame.empty:
  77. series = [0, 0, 0, 0, 0, 0]
  78. else:
  79. col = keyword if keyword in frame.columns else frame.columns[0]
  80. series = [int(v) for v in frame[col].tail(6).tolist()]
  81. if len(series) < 6:
  82. series = ([series[0]] * (6 - len(series)) + series) if series else [0] * 6
  83. return TrendSeries(
  84. keyword=keyword,
  85. timeframe=timeframe,
  86. series=series,
  87. fetched_at=datetime.utcnow().isoformat() + "Z",
  88. )