weixin.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. # -*- coding: utf-8 -*-
  2. """
  3. 微信视频号发布器
  4. 参考: matrix/tencent_uploader/main.py
  5. """
  6. import asyncio
  7. import os
  8. from datetime import datetime
  9. from typing import List
  10. from .base import (
  11. BasePublisher, PublishParams, PublishResult,
  12. WorkItem, WorksResult, CommentItem, CommentsResult
  13. )
  14. def format_short_title(origin_title: str) -> str:
  15. """
  16. 格式化短标题
  17. - 移除特殊字符
  18. - 长度限制在 6-16 字符
  19. """
  20. allowed_special_chars = "《》"":+?%°"
  21. filtered_chars = [
  22. char if char.isalnum() or char in allowed_special_chars
  23. else ' ' if char == ',' else ''
  24. for char in origin_title
  25. ]
  26. formatted_string = ''.join(filtered_chars)
  27. if len(formatted_string) > 16:
  28. formatted_string = formatted_string[:16]
  29. elif len(formatted_string) < 6:
  30. formatted_string += ' ' * (6 - len(formatted_string))
  31. return formatted_string
  32. class WeixinPublisher(BasePublisher):
  33. """
  34. 微信视频号发布器
  35. 使用 Playwright 自动化操作视频号创作者中心
  36. 注意: 需要使用 Chrome 浏览器,否则可能出现 H264 编码错误
  37. """
  38. platform_name = "weixin"
  39. login_url = "https://channels.weixin.qq.com/platform"
  40. publish_url = "https://channels.weixin.qq.com/platform/post/create"
  41. cookie_domain = ".weixin.qq.com"
  42. async def init_browser(self, storage_state: str = None):
  43. """初始化浏览器 - 参考 matrix 使用 channel=chrome 避免 H264 编码错误"""
  44. from playwright.async_api import async_playwright
  45. playwright = await async_playwright().start()
  46. # 参考 matrix: 使用系统内的 Chrome 浏览器,避免 H264 编码错误
  47. # 如果没有安装 Chrome,则使用默认 Chromium
  48. try:
  49. self.browser = await playwright.chromium.launch(
  50. headless=self.headless,
  51. channel="chrome" # 使用系统 Chrome
  52. )
  53. print(f"[{self.platform_name}] 使用系统 Chrome 浏览器")
  54. except Exception as e:
  55. print(f"[{self.platform_name}] Chrome 不可用,使用 Chromium: {e}")
  56. self.browser = await playwright.chromium.launch(headless=self.headless)
  57. if storage_state and os.path.exists(storage_state):
  58. self.context = await self.browser.new_context(storage_state=storage_state)
  59. else:
  60. self.context = await self.browser.new_context()
  61. self.page = await self.context.new_page()
  62. return self.page
  63. async def set_schedule_time(self, publish_date: datetime):
  64. """设置定时发布"""
  65. if not self.page:
  66. return
  67. print(f"[{self.platform_name}] 设置定时发布...")
  68. # 点击定时选项
  69. label_element = self.page.locator("label").filter(has_text="定时").nth(1)
  70. await label_element.click()
  71. # 选择日期
  72. await self.page.click('input[placeholder="请选择发表时间"]')
  73. publish_month = f"{publish_date.month:02d}"
  74. current_month = f"{publish_month}月"
  75. # 检查月份
  76. page_month = await self.page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")')
  77. if page_month != current_month:
  78. await self.page.click('button.weui-desktop-btn__icon__right')
  79. # 选择日期
  80. elements = await self.page.query_selector_all('table.weui-desktop-picker__table a')
  81. for element in elements:
  82. class_name = await element.evaluate('el => el.className')
  83. if 'weui-desktop-picker__disabled' in class_name:
  84. continue
  85. text = await element.inner_text()
  86. if text.strip() == str(publish_date.day):
  87. await element.click()
  88. break
  89. # 输入时间
  90. await self.page.click('input[placeholder="请选择时间"]')
  91. await self.page.keyboard.press("Control+KeyA")
  92. await self.page.keyboard.type(str(publish_date.hour))
  93. # 点击其他地方确认
  94. await self.page.locator("div.input-editor").click()
  95. async def handle_upload_error(self, video_path: str):
  96. """处理上传错误"""
  97. if not self.page:
  98. return
  99. print(f"[{self.platform_name}] 视频出错了,重新上传中...")
  100. await self.page.locator('div.media-status-content div.tag-inner:has-text("删除")').click()
  101. await self.page.get_by_role('button', name="删除", exact=True).click()
  102. file_input = self.page.locator('input[type="file"]')
  103. await file_input.set_input_files(video_path)
  104. async def add_title_tags(self, params: PublishParams):
  105. """添加标题和话题"""
  106. if not self.page:
  107. return
  108. await self.page.locator("div.input-editor").click()
  109. await self.page.keyboard.type(params.title)
  110. if params.tags:
  111. await self.page.keyboard.press("Enter")
  112. for tag in params.tags:
  113. await self.page.keyboard.type("#" + tag)
  114. await self.page.keyboard.press("Space")
  115. print(f"[{self.platform_name}] 成功添加标题和 {len(params.tags)} 个话题")
  116. async def add_short_title(self):
  117. """添加短标题"""
  118. if not self.page:
  119. return
  120. try:
  121. short_title_element = self.page.get_by_text("短标题", exact=True).locator("..").locator(
  122. "xpath=following-sibling::div").locator('span input[type="text"]')
  123. if await short_title_element.count():
  124. # 获取已有内容作为短标题
  125. pass
  126. except:
  127. pass
  128. async def upload_cover(self, cover_path: str):
  129. """上传封面图"""
  130. if not self.page or not cover_path or not os.path.exists(cover_path):
  131. return
  132. try:
  133. await asyncio.sleep(2)
  134. preview_btn_info = await self.page.locator(
  135. 'div.finder-tag-wrap.btn:has-text("更换封面")').get_attribute('class')
  136. if "disabled" not in preview_btn_info:
  137. await self.page.locator('div.finder-tag-wrap.btn:has-text("更换封面")').click()
  138. await self.page.locator('div.single-cover-uploader-wrap > div.wrap').hover()
  139. # 删除现有封面
  140. if await self.page.locator(".del-wrap > .svg-icon").count():
  141. await self.page.locator(".del-wrap > .svg-icon").click()
  142. # 上传新封面
  143. preview_div = self.page.locator("div.single-cover-uploader-wrap > div.wrap")
  144. async with self.page.expect_file_chooser() as fc_info:
  145. await preview_div.click()
  146. preview_chooser = await fc_info.value
  147. await preview_chooser.set_files(cover_path)
  148. await asyncio.sleep(2)
  149. await self.page.get_by_role("button", name="确定").click()
  150. await asyncio.sleep(1)
  151. await self.page.get_by_role("button", name="确认").click()
  152. print(f"[{self.platform_name}] 封面上传成功")
  153. except Exception as e:
  154. print(f"[{self.platform_name}] 封面上传失败: {e}")
  155. async def check_captcha(self) -> dict:
  156. """检查页面是否需要验证码"""
  157. if not self.page:
  158. return {'need_captcha': False, 'captcha_type': ''}
  159. try:
  160. # 检查各种验证码
  161. captcha_selectors = [
  162. 'text="请输入验证码"',
  163. 'text="滑动验证"',
  164. '[class*="captcha"]',
  165. '[class*="verify"]',
  166. ]
  167. for selector in captcha_selectors:
  168. try:
  169. if await self.page.locator(selector).count() > 0:
  170. print(f"[{self.platform_name}] 检测到验证码: {selector}")
  171. return {'need_captcha': True, 'captcha_type': 'image'}
  172. except:
  173. pass
  174. # 检查登录弹窗
  175. login_selectors = [
  176. 'text="请登录"',
  177. 'text="扫码登录"',
  178. '[class*="login-dialog"]',
  179. ]
  180. for selector in login_selectors:
  181. try:
  182. if await self.page.locator(selector).count() > 0:
  183. print(f"[{self.platform_name}] 检测到需要登录: {selector}")
  184. return {'need_captcha': True, 'captcha_type': 'login'}
  185. except:
  186. pass
  187. except Exception as e:
  188. print(f"[{self.platform_name}] 验证码检测异常: {e}")
  189. return {'need_captcha': False, 'captcha_type': ''}
  190. async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
  191. """发布视频到视频号"""
  192. print(f"\n{'='*60}")
  193. print(f"[{self.platform_name}] 开始发布视频")
  194. print(f"[{self.platform_name}] 视频路径: {params.video_path}")
  195. print(f"[{self.platform_name}] 标题: {params.title}")
  196. print(f"[{self.platform_name}] Headless: {self.headless}")
  197. print(f"{'='*60}")
  198. self.report_progress(5, "正在初始化浏览器...")
  199. # 初始化浏览器(使用 Chrome)
  200. await self.init_browser()
  201. print(f"[{self.platform_name}] 浏览器初始化完成")
  202. # 解析并设置 cookies
  203. cookie_list = self.parse_cookies(cookies)
  204. print(f"[{self.platform_name}] 解析到 {len(cookie_list)} 个 cookies")
  205. await self.set_cookies(cookie_list)
  206. if not self.page:
  207. raise Exception("Page not initialized")
  208. # 检查视频文件
  209. if not os.path.exists(params.video_path):
  210. raise Exception(f"视频文件不存在: {params.video_path}")
  211. print(f"[{self.platform_name}] 视频文件存在,大小: {os.path.getsize(params.video_path)} bytes")
  212. self.report_progress(10, "正在打开上传页面...")
  213. # 访问上传页面
  214. await self.page.goto(self.publish_url, wait_until="domcontentloaded", timeout=60000)
  215. await asyncio.sleep(3)
  216. # 检查是否跳转到登录页
  217. current_url = self.page.url
  218. print(f"[{self.platform_name}] 当前页面: {current_url}")
  219. if "login" in current_url:
  220. screenshot_base64 = await self.capture_screenshot()
  221. return PublishResult(
  222. success=False,
  223. platform=self.platform_name,
  224. error="Cookie 已过期,需要重新登录",
  225. need_captcha=True,
  226. captcha_type='login',
  227. screenshot_base64=screenshot_base64,
  228. page_url=current_url,
  229. status='need_captcha'
  230. )
  231. # 使用 AI 检查验证码
  232. ai_captcha = await self.ai_check_captcha()
  233. if ai_captcha['has_captcha']:
  234. print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha['captcha_type']}", flush=True)
  235. screenshot_base64 = await self.capture_screenshot()
  236. return PublishResult(
  237. success=False,
  238. platform=self.platform_name,
  239. error=f"检测到{ai_captcha['captcha_type']}验证码,需要使用有头浏览器完成验证",
  240. need_captcha=True,
  241. captcha_type=ai_captcha['captcha_type'],
  242. screenshot_base64=screenshot_base64,
  243. page_url=current_url,
  244. status='need_captcha'
  245. )
  246. # 传统方式检查验证码
  247. captcha_result = await self.check_captcha()
  248. if captcha_result['need_captcha']:
  249. screenshot_base64 = await self.capture_screenshot()
  250. return PublishResult(
  251. success=False,
  252. platform=self.platform_name,
  253. error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
  254. need_captcha=True,
  255. captcha_type=captcha_result['captcha_type'],
  256. screenshot_base64=screenshot_base64,
  257. page_url=current_url,
  258. status='need_captcha'
  259. )
  260. self.report_progress(15, "正在选择视频文件...")
  261. # 上传视频 - 参考 matrix/tencent_uploader/main.py
  262. # matrix 使用: div.upload-content 点击后触发文件选择器
  263. upload_success = False
  264. # 方法1: 参考 matrix - 点击 div.upload-content
  265. try:
  266. upload_div = self.page.locator("div.upload-content")
  267. if await upload_div.count() > 0:
  268. print(f"[{self.platform_name}] 找到 upload-content 上传区域")
  269. async with self.page.expect_file_chooser(timeout=10000) as fc_info:
  270. await upload_div.click()
  271. file_chooser = await fc_info.value
  272. await file_chooser.set_files(params.video_path)
  273. upload_success = True
  274. print(f"[{self.platform_name}] 通过 upload-content 上传成功")
  275. except Exception as e:
  276. print(f"[{self.platform_name}] upload-content 上传失败: {e}")
  277. # 方法2: 尝试其他选择器
  278. if not upload_success:
  279. upload_selectors = [
  280. 'div[class*="upload-area"]',
  281. 'div[class*="drag-upload"]',
  282. 'div.add-wrap',
  283. '[class*="uploader"]',
  284. ]
  285. for selector in upload_selectors:
  286. if upload_success:
  287. break
  288. try:
  289. upload_area = self.page.locator(selector).first
  290. if await upload_area.count() > 0:
  291. print(f"[{self.platform_name}] 尝试点击上传区域: {selector}")
  292. async with self.page.expect_file_chooser(timeout=10000) as fc_info:
  293. await upload_area.click()
  294. file_chooser = await fc_info.value
  295. await file_chooser.set_files(params.video_path)
  296. upload_success = True
  297. print(f"[{self.platform_name}] 通过点击上传区域成功")
  298. break
  299. except Exception as e:
  300. print(f"[{self.platform_name}] 选择器 {selector} 失败: {e}")
  301. # 方法3: 直接设置 file input
  302. if not upload_success:
  303. try:
  304. file_input = self.page.locator('input[type="file"]')
  305. if await file_input.count() > 0:
  306. await file_input.first.set_input_files(params.video_path)
  307. upload_success = True
  308. print(f"[{self.platform_name}] 通过 file input 上传成功")
  309. except Exception as e:
  310. print(f"[{self.platform_name}] file input 上传失败: {e}")
  311. if not upload_success:
  312. screenshot_base64 = await self.capture_screenshot()
  313. return PublishResult(
  314. success=False,
  315. platform=self.platform_name,
  316. error="未找到上传入口",
  317. screenshot_base64=screenshot_base64,
  318. page_url=await self.get_page_url(),
  319. status='failed'
  320. )
  321. self.report_progress(20, "正在填充标题和话题...")
  322. # 添加标题和话题
  323. await self.add_title_tags(params)
  324. self.report_progress(30, "等待视频上传完成...")
  325. # 等待上传完成
  326. for _ in range(120):
  327. try:
  328. button_info = await self.page.get_by_role("button", name="发表").get_attribute('class')
  329. if "weui-desktop-btn_disabled" not in button_info:
  330. print(f"[{self.platform_name}] 视频上传完毕")
  331. # 上传封面
  332. self.report_progress(50, "正在上传封面...")
  333. await self.upload_cover(params.cover_path)
  334. break
  335. else:
  336. # 检查上传错误
  337. if await self.page.locator('div.status-msg.error').count():
  338. if await self.page.locator('div.media-status-content div.tag-inner:has-text("删除")').count():
  339. await self.handle_upload_error(params.video_path)
  340. await asyncio.sleep(3)
  341. except:
  342. await asyncio.sleep(3)
  343. self.report_progress(60, "处理视频设置...")
  344. # 添加短标题
  345. try:
  346. short_title_el = self.page.get_by_text("短标题", exact=True).locator("..").locator(
  347. "xpath=following-sibling::div").locator('span input[type="text"]')
  348. if await short_title_el.count():
  349. short_title = format_short_title(params.title)
  350. await short_title_el.fill(short_title)
  351. except:
  352. pass
  353. # 定时发布
  354. if params.publish_date:
  355. self.report_progress(70, "设置定时发布...")
  356. await self.set_schedule_time(params.publish_date)
  357. self.report_progress(80, "正在发布...")
  358. # 点击发布 - 参考 matrix
  359. for i in range(30):
  360. try:
  361. # 参考 matrix: div.form-btns button:has-text("发表")
  362. publish_btn = self.page.locator('div.form-btns button:has-text("发表")')
  363. if await publish_btn.count():
  364. print(f"[{self.platform_name}] 点击发布按钮...")
  365. await publish_btn.click()
  366. # 等待跳转到作品列表页面 - 参考 matrix
  367. await self.page.wait_for_url(
  368. "https://channels.weixin.qq.com/platform/post/list",
  369. timeout=10000
  370. )
  371. self.report_progress(100, "发布成功")
  372. print(f"[{self.platform_name}] 视频发布成功!")
  373. screenshot_base64 = await self.capture_screenshot()
  374. return PublishResult(
  375. success=True,
  376. platform=self.platform_name,
  377. message="发布成功",
  378. screenshot_base64=screenshot_base64,
  379. page_url=self.page.url,
  380. status='success'
  381. )
  382. except Exception as e:
  383. current_url = self.page.url
  384. if "https://channels.weixin.qq.com/platform/post/list" in current_url:
  385. self.report_progress(100, "发布成功")
  386. print(f"[{self.platform_name}] 视频发布成功!")
  387. screenshot_base64 = await self.capture_screenshot()
  388. return PublishResult(
  389. success=True,
  390. platform=self.platform_name,
  391. message="发布成功",
  392. screenshot_base64=screenshot_base64,
  393. page_url=current_url,
  394. status='success'
  395. )
  396. else:
  397. print(f"[{self.platform_name}] 视频正在发布中... {i+1}/30, URL: {current_url}")
  398. await asyncio.sleep(1)
  399. # 发布超时
  400. screenshot_base64 = await self.capture_screenshot()
  401. page_url = await self.get_page_url()
  402. return PublishResult(
  403. success=False,
  404. platform=self.platform_name,
  405. error="发布超时,请检查发布状态",
  406. screenshot_base64=screenshot_base64,
  407. page_url=page_url,
  408. status='need_action'
  409. )
  410. async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
  411. """获取视频号作品列表"""
  412. print(f"\n{'='*60}")
  413. print(f"[{self.platform_name}] 获取作品列表")
  414. print(f"[{self.platform_name}] page={page}, page_size={page_size}")
  415. print(f"{'='*60}")
  416. works: List[WorkItem] = []
  417. total = 0
  418. has_more = False
  419. try:
  420. await self.init_browser()
  421. cookie_list = self.parse_cookies(cookies)
  422. await self.set_cookies(cookie_list)
  423. if not self.page:
  424. raise Exception("Page not initialized")
  425. # 访问视频号创作者中心
  426. await self.page.goto("https://channels.weixin.qq.com/platform/post/list")
  427. await asyncio.sleep(5)
  428. # 检查登录状态
  429. current_url = self.page.url
  430. if "login" in current_url:
  431. raise Exception("Cookie 已过期,请重新登录")
  432. # 视频号使用页面爬取方式获取作品列表
  433. # 等待作品列表加载
  434. await self.page.wait_for_selector('div.post-feed-wrap', timeout=10000)
  435. # 获取所有作品项
  436. post_items = self.page.locator('div.post-feed-item')
  437. item_count = await post_items.count()
  438. print(f"[{self.platform_name}] 找到 {item_count} 个作品项")
  439. for i in range(min(item_count, page_size)):
  440. try:
  441. item = post_items.nth(i)
  442. # 获取封面
  443. cover_el = item.locator('div.cover-wrap img').first
  444. cover_url = ''
  445. if await cover_el.count() > 0:
  446. cover_url = await cover_el.get_attribute('src') or ''
  447. # 获取标题
  448. title_el = item.locator('div.content').first
  449. title = ''
  450. if await title_el.count() > 0:
  451. title = await title_el.text_content() or ''
  452. title = title.strip()[:50]
  453. # 获取统计数据
  454. stats_el = item.locator('div.post-data')
  455. play_count = 0
  456. like_count = 0
  457. comment_count = 0
  458. if await stats_el.count() > 0:
  459. stats_text = await stats_el.text_content() or ''
  460. # 解析统计数据(格式可能是: 播放 100 点赞 50 评论 10)
  461. import re
  462. play_match = re.search(r'播放[\s]*([\d.]+[万]?)', stats_text)
  463. like_match = re.search(r'点赞[\s]*([\d.]+[万]?)', stats_text)
  464. comment_match = re.search(r'评论[\s]*([\d.]+[万]?)', stats_text)
  465. def parse_count(match):
  466. if not match:
  467. return 0
  468. val = match.group(1)
  469. if '万' in val:
  470. return int(float(val.replace('万', '')) * 10000)
  471. return int(val)
  472. play_count = parse_count(play_match)
  473. like_count = parse_count(like_match)
  474. comment_count = parse_count(comment_match)
  475. # 获取发布时间
  476. time_el = item.locator('div.time')
  477. publish_time = ''
  478. if await time_el.count() > 0:
  479. publish_time = await time_el.text_content() or ''
  480. publish_time = publish_time.strip()
  481. # 生成临时 work_id(视频号可能需要从详情页获取)
  482. work_id = f"weixin_{i}_{hash(title)}"
  483. works.append(WorkItem(
  484. work_id=work_id,
  485. title=title or '无标题',
  486. cover_url=cover_url,
  487. duration=0,
  488. status='published',
  489. publish_time=publish_time,
  490. play_count=play_count,
  491. like_count=like_count,
  492. comment_count=comment_count,
  493. ))
  494. except Exception as e:
  495. print(f"[{self.platform_name}] 解析作品 {i} 失败: {e}")
  496. continue
  497. total = len(works)
  498. has_more = item_count > page_size
  499. print(f"[{self.platform_name}] 获取到 {total} 个作品")
  500. except Exception as e:
  501. import traceback
  502. traceback.print_exc()
  503. return WorksResult(success=False, platform=self.platform_name, error=str(e))
  504. return WorksResult(success=True, platform=self.platform_name, works=works, total=total, has_more=has_more)
  505. async def get_comments(self, cookies: str, work_id: str, cursor: str = "") -> CommentsResult:
  506. """获取视频号作品评论"""
  507. print(f"\n{'='*60}")
  508. print(f"[{self.platform_name}] 获取作品评论")
  509. print(f"[{self.platform_name}] work_id={work_id}")
  510. print(f"{'='*60}")
  511. comments: List[CommentItem] = []
  512. total = 0
  513. has_more = False
  514. try:
  515. await self.init_browser()
  516. cookie_list = self.parse_cookies(cookies)
  517. await self.set_cookies(cookie_list)
  518. if not self.page:
  519. raise Exception("Page not initialized")
  520. # 访问评论管理页面
  521. await self.page.goto("https://channels.weixin.qq.com/platform/comment/index")
  522. await asyncio.sleep(5)
  523. # 检查登录状态
  524. current_url = self.page.url
  525. if "login" in current_url:
  526. raise Exception("Cookie 已过期,请重新登录")
  527. # 等待评论列表加载
  528. try:
  529. await self.page.wait_for_selector('div.comment-list', timeout=10000)
  530. except:
  531. print(f"[{self.platform_name}] 未找到评论列表")
  532. return CommentsResult(success=True, platform=self.platform_name, work_id=work_id, comments=[], total=0, has_more=False)
  533. # 获取所有评论项
  534. comment_items = self.page.locator('div.comment-item')
  535. item_count = await comment_items.count()
  536. print(f"[{self.platform_name}] 找到 {item_count} 个评论项")
  537. for i in range(item_count):
  538. try:
  539. item = comment_items.nth(i)
  540. # 获取作者信息
  541. author_name = ''
  542. author_avatar = ''
  543. name_el = item.locator('div.nick-name')
  544. if await name_el.count() > 0:
  545. author_name = await name_el.text_content() or ''
  546. author_name = author_name.strip()
  547. avatar_el = item.locator('img.avatar')
  548. if await avatar_el.count() > 0:
  549. author_avatar = await avatar_el.get_attribute('src') or ''
  550. # 获取评论内容
  551. content = ''
  552. content_el = item.locator('div.comment-content')
  553. if await content_el.count() > 0:
  554. content = await content_el.text_content() or ''
  555. content = content.strip()
  556. # 获取时间
  557. create_time = ''
  558. time_el = item.locator('div.time')
  559. if await time_el.count() > 0:
  560. create_time = await time_el.text_content() or ''
  561. create_time = create_time.strip()
  562. # 生成评论 ID
  563. comment_id = f"weixin_comment_{i}_{hash(content)}"
  564. comments.append(CommentItem(
  565. comment_id=comment_id,
  566. work_id=work_id,
  567. content=content,
  568. author_id='',
  569. author_name=author_name,
  570. author_avatar=author_avatar,
  571. like_count=0,
  572. reply_count=0,
  573. create_time=create_time,
  574. ))
  575. except Exception as e:
  576. print(f"[{self.platform_name}] 解析评论 {i} 失败: {e}")
  577. continue
  578. total = len(comments)
  579. print(f"[{self.platform_name}] 获取到 {total} 条评论")
  580. except Exception as e:
  581. import traceback
  582. traceback.print_exc()
  583. return CommentsResult(success=False, platform=self.platform_name, work_id=work_id, error=str(e))
  584. return CommentsResult(success=True, platform=self.platform_name, work_id=work_id, comments=comments, total=total, has_more=has_more)