profile.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """步骤:打开用户/作者主页并读取抖音号。"""
  2. from __future__ import annotations
  3. import re
  4. from playwright.sync_api import Page
  5. from collectors.douyin.common import human_pause
  6. def _to_abs_url(href: str) -> str:
  7. if href.startswith("http"):
  8. return href
  9. if href.startswith("//"):
  10. return f"https:{href}"
  11. if href.startswith("/"):
  12. return f"https://www.douyin.com{href}"
  13. return f"https://www.douyin.com/{href}"
  14. def _normalize_user_key(href: str) -> str:
  15. """用 /user/xxx 路径作为同一用户缓存键,避免 query 差异导致重复开页。"""
  16. if not href:
  17. return ""
  18. m = re.search(r"/user/[^/?#]+", href)
  19. return m.group(0) if m else href.strip()
  20. def _extract_douyin_id(profile: Page) -> str:
  21. """从用户主页提取抖音号;先等到页面出现「抖音号」文案,并做二次确认。"""
  22. try:
  23. profile.wait_for_function(
  24. """() => /抖音号/.test(document.body.innerText || '')""",
  25. timeout=20_000,
  26. )
  27. except Exception:
  28. human_pause(2.5, 4.0)
  29. def _read_once() -> str:
  30. return (
  31. profile.evaluate(
  32. """() => {
  33. const bodyText = document.body.innerText || '';
  34. const m = bodyText.match(/抖音号[::\\s]*([A-Za-z0-9_.\\-]+)/);
  35. if (m) return m[1];
  36. const spans = Array.from(document.querySelectorAll('span, p'));
  37. for (const el of spans) {
  38. const t = (el.innerText || '').trim();
  39. const mm = t.match(/^抖音号[::\\s]*([A-Za-z0-9_.\\-]+)$/);
  40. if (mm) return mm[1];
  41. }
  42. return '';
  43. }"""
  44. )
  45. or ""
  46. ).strip()
  47. douyin_id = _read_once()
  48. if not douyin_id:
  49. human_pause(1.5, 2.5)
  50. douyin_id = _read_once()
  51. if not douyin_id:
  52. raise RuntimeError("主页上未解析到抖音号")
  53. # 二次读取,避免偶发读到未渲染完的占位
  54. human_pause(0.4, 0.8)
  55. again = _read_once()
  56. if again and again != douyin_id:
  57. print(f"[抖音号校验] 两次读取不一致: {douyin_id} vs {again},采用第二次")
  58. douyin_id = again
  59. return douyin_id
  60. def _open_profile_by_url(page: Page, url: str) -> Page:
  61. profile = page.context.new_page()
  62. profile.goto(url, wait_until="domcontentloaded")
  63. profile.wait_for_load_state("domcontentloaded")
  64. return profile
  65. def _open_author_profile(page: Page, author_link: str) -> Page:
  66. """
  67. 在新标签打开作者主页,避免同页跳进作者作品流,
  68. 保证后续仍在「搜索结果」视频弹层里上下切换。
  69. """
  70. return _open_profile_by_url(page, author_link)
  71. def _open_user_profile(
  72. page: Page,
  73. href: str,
  74. avatar_x: int | None = None,
  75. avatar_y: int | None = None,
  76. ) -> Page:
  77. """
  78. 打开评论者主页。
  79. 一律用主页链接在新标签打开,避免点错头像/坐标过期导致抖音号错配。
  80. """
  81. _ = avatar_x, avatar_y
  82. url = _to_abs_url(href)
  83. if "/user/" not in url:
  84. raise RuntimeError(f"无效用户主页链接: {url}")
  85. profile = page.context.new_page()
  86. profile.goto(url, wait_until="domcontentloaded")
  87. profile.wait_for_load_state("domcontentloaded")
  88. return profile