user_reply_poc.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. import json
  2. import time
  3. import uiautomator2 as u2
  4. import yaml
  5. import httpx
  6. from datetime import datetime, timedelta
  7. import subprocess
  8. from tools import loggerKit
  9. from difflib import SequenceMatcher
  10. # 读取 YAML 文件
  11. with open('config.yaml', 'r') as file:
  12. config = yaml.load(file, Loader=yaml.FullLoader)
  13. extern_domain = config['bmp-cp']['extern_domain']
  14. post_ai_gc_url = config['bmp-cp']['post_ai_gc_url']
  15. class shell_status_content:
  16. def __init__(self, execute_status, error_code, error_msg, result, content, nickname):
  17. """
  18. :rtype: object
  19. """
  20. # 0失败 1成功
  21. self.execute_status = execute_status
  22. self.error_code = error_code
  23. self.error_msg = error_msg
  24. self.result = result
  25. self.content = content
  26. self.nickname = nickname
  27. def to_dict(self):
  28. return {
  29. 'execute_status': self.execute_status,
  30. 'error_code': self.error_code,
  31. 'error_msg': self.error_msg,
  32. 'result': self.result,
  33. 'content': self.content,
  34. 'nickname': self.nickname
  35. }
  36. # 回复详情
  37. class content_detail:
  38. def __init__(self, keyword, title, reply, account_name):
  39. """
  40. :rtype: object
  41. """
  42. # 搜索关键词
  43. self.keyword = keyword
  44. self.title = title
  45. self.reply = reply
  46. self.account_name = account_name
  47. def to_dict(self):
  48. return {
  49. 'keyword': self.keyword,
  50. 'title': self.title,
  51. 'reply': self.reply,
  52. 'accountName': self.account_name
  53. }
  54. # 懂车帝 v7.8.3
  55. # 3-5 天的互动操作
  56. def simulated_operation(device_serial, keyword, task_id, media_channel, sub_resource_name):
  57. cmd_conn = f"adb connect {device_serial}"
  58. conn_output = subprocess.check_output(cmd_conn, shell=True)
  59. print(f"{conn_output}")
  60. d = u2.connect(device_serial)
  61. d.healthcheck()
  62. d.screen_on()
  63. d.unlock()
  64. d.debug = False
  65. # 跳过开屏广告
  66. d.watcher.when("跳过广告").click()
  67. # 跳过升级弹框
  68. d.watcher.when("以后再说").click()
  69. # 位置授权
  70. d.watcher.when("本次运行允许").click()
  71. # 稍后
  72. d.watcher.when("稍后").click()
  73. # 开始后台监控
  74. # 默认监控间隔2.0s
  75. d.watcher.start(2.0)
  76. d.app_stop("com.ss.android.auto")
  77. d.app_start('com.ss.android.auto', use_monkey=True)
  78. loggerKit.info("懂车帝养号" + d.device_info['serial'] + ", 开始操作:{0}, {1},{2},{3}", device_serial, keyword,
  79. task_id, media_channel)
  80. time.sleep(5)
  81. # 进入我的获取账号信息
  82. d.xpath('//*[@resource-id="android:id/tabs"]/android.widget.RelativeLayout[5]').click()
  83. time.sleep(2)
  84. account_name = d.xpath('//*[@resource-id="com.ss.android.auto:id/goi"]').get_text()
  85. # 点击到首页
  86. d.xpath('//*[@resource-id="android:id/tabs"]/android.widget.RelativeLayout[1]').click()
  87. # 点击开始搜索
  88. d.xpath('//*[@resource-id="com.ss.android.auto:id/d0r"]').click(timeout=0.25)
  89. time.sleep(2)
  90. replace_word = keyword.replace('|', '')
  91. # 输入框赋值
  92. d.xpath('//*[@resource-id="com.ss.android.auto:id/h1k"]').set_text(replace_word)
  93. time.sleep(1)
  94. # 点击搜索
  95. d.xpath('//*[@resource-id="com.ss.android.auto:id/g61"]').click()
  96. # 等待元素加载
  97. time.sleep(5)
  98. xpath = '//*[@text="{}"]'
  99. # d.xpath(xpath.format(keyword)).all()[1].click()
  100. if d.xpath(xpath.format(keyword)).all().__len__() > 0:
  101. if d.xpath(xpath.format(keyword)).all().__len__() == 1:
  102. d.xpath(xpath.format(keyword)).click()
  103. else:
  104. d.xpath(xpath.format(keyword)).all()[1].click()
  105. else:
  106. # 第一次存在元素加载问题
  107. if d.xpath(xpath.format(keyword)).all().__len__() == 1:
  108. d.xpath(xpath.format(keyword)).click()
  109. elif d.xpath(xpath.format(keyword)).all().__len__() >= 1:
  110. d.xpath(xpath.format(keyword)).all()[1].click()
  111. else:
  112. # 返回未找到帖子
  113. if sub_resource_name is None or sub_resource_name == '':
  114. status = shell_status_content(0, 501, '该帖子未获取到,且无对应发帖人,无法进行深层次搜索', '', '', '')
  115. d.app_stop("com.ss.android.auto")
  116. return status
  117. else:
  118. return find_user(d, sub_resource_name, keyword, account_name, task_id)
  119. # if not d.xpath('//*[@resource-id="com.ss.android.auto:id/hzw"]').exists and not d.xpath(
  120. # '//*[@resource-id="com.ss.android.auto:id/iv_more"]').exists and (not d.xpath(
  121. # '//*[@resource-id="com.ss.android.auto:id/ff6"]').exists or not d.xpath(
  122. # '//*[@resource-id="com.ss.android.auto:id/d6l"]').exists):
  123. if not d.xpath('//*[@resource-id="com.ss.android.auto:id/hzw"]').exists and not d.xpath(
  124. '//*[@resource-id="com.ss.android.auto:id/iv_more"]').exists and not d.xpath(
  125. '//*[@resource-id="com.ss.android.auto:id/ff6"]').exists and not d.xpath(
  126. '//*[@resource-id="com.ss.android.auto:id/d6l"]').exists:
  127. # 返回未找到帖子
  128. if sub_resource_name is None or sub_resource_name == '':
  129. status = shell_status_content(0, 501, '该帖子未获取到,且无对应发帖人,无法进行深层次搜索', '', '', '')
  130. d.app_stop("com.ss.android.auto")
  131. return status
  132. else:
  133. return find_user(d, sub_resource_name, keyword, account_name, task_id)
  134. return title_action(d, keyword, account_name, task_id)
  135. # 查询到用户主页寻找帖子
  136. def find_user(d, user_name, keyword, account_name, task_id):
  137. time.sleep(3)
  138. if not d.xpath('//*[@resource-id="com.ss.android.auto:id/h1k"]').exists:
  139. status = shell_status_content(0, 501, '该帖子类型不支持/搜索用户页面错误', '', '', '')
  140. d.app_stop("com.ss.android.auto")
  141. return status
  142. # 赋值搜索框 用户名称
  143. d.xpath('//*[@resource-id="com.ss.android.auto:id/h1k"]').set_text(user_name)
  144. # 点击搜索
  145. d.xpath('//*[@resource-id="com.ss.android.auto:id/g61"]').click()
  146. time.sleep(3)
  147. # time.sleep(2)
  148. # d.xpath('//*[@text="小视频"]').click_exists()
  149. if d.xpath('//*[@text="二手车"]').exists:
  150. d(text='二手车').drag_to(text='综合', timeout=3)
  151. else:
  152. d(text='图片').drag_to(text='综合', timeout=3)
  153. # time.sleep(1)
  154. # d.xpath('//*[@text="车友圈"]').click_exists()
  155. #
  156. # time.sleep(1)
  157. # d.xpath('//*[@text="问答"]').click_exists()
  158. time.sleep(1)
  159. d.xpath('//*[@text="用户"]').click()
  160. time.sleep(3)
  161. xpath = '//*[@text="{}"]'
  162. user_name_list = d.xpath(xpath.format(user_name)).all()
  163. # 根据用户名获取元素 搜索框一定有一个 所以<=1 则表示未搜索到用户
  164. if user_name_list.__len__() <= 1:
  165. # 当前用户没有获取到任何帖子
  166. status = shell_status_content(0, 501, '未搜索到当前用户', '', '', '')
  167. return status
  168. else:
  169. # 懂车帝可能根据搜索算法 推出 是否仍要搜索目标选项, 这种情况对下标为2的进行点击。 正常情况对下标为1的进行点击
  170. if not d.xpath('//*[@text="”的搜索结果。仍然搜索:"]').exists:
  171. user_name_list[1].click()
  172. elif user_name_list.__len__() >= 3:
  173. user_name_list[2].click()
  174. # if user_name_list.__len__() > 1:
  175. # user_name_list[user_name_list.__len__() - 1].click()
  176. # else:
  177. # d.xpath(xpath.format(user_name)).click()
  178. # 点击第一个用户进入首页
  179. # d.xpath('//*[@resource-id="root"]/android.view.View[2]/android.view.View[1]/android.view.View[1]').click_exists()
  180. time.sleep(2)
  181. # 如果存在关注用户标签 进行关闭
  182. check_follow_button(d)
  183. time.sleep(1)
  184. # 获取帖子发布时间
  185. if not d.xpath('//*[@resource-id="com.ss.android.auto:id/tv_time"]').exists:
  186. # 当前用户没有获取到任何帖子
  187. status = shell_status_content(0, 501, '当前用户没有获取到任何帖子', '', '', '')
  188. return status
  189. # 预期时间
  190. transfer_date = datetime.strptime('2023-10-01', '%Y-%m-%d').date()
  191. while 1:
  192. # 如果存在关注用户标签 进行关闭
  193. check_follow_button(d)
  194. publish_time = d.xpath('//*[@resource-id="com.ss.android.auto:id/tv_time"]').get_text()
  195. publish_time = __formate_time(publish_time)
  196. # 发布时间
  197. publish_date = datetime.strptime(publish_time, '%Y-%m-%d').date()
  198. # 获取到所有帖子标题进行模糊匹配
  199. text_list = d.xpath('//*[@resource-id="com.ss.android.auto:id/s"]').all()
  200. for text in text_list:
  201. matcher = SequenceMatcher(None, text.text, keyword)
  202. similarity_ratio = matcher.ratio()
  203. if similarity_ratio >= 0.8:
  204. text.click()
  205. loggerKit.info("任务{0},用户{1}下搜索到{2}对应帖子 进行点击并操作", task_id, account_name, keyword)
  206. return title_action(d, keyword, account_name, task_id)
  207. # 如果能够根据标题搜索到帖子
  208. # if d.xpath(xpath.format(keyword)).exists:
  209. # d.xpath(xpath.format(keyword)).click()
  210. # return title_action(d, keyword, account_name)
  211. # 先判断是否超过10月1号
  212. if transfer_date > publish_date:
  213. # 已经抓取到10月1号帖子 仍未获取到该帖子
  214. print('已经获取到10月1号帖子')
  215. status = shell_status_content(0, 501, '已经抓取到10月1号帖子 仍未获取到该帖子,不进行下一步获取', '', '', '')
  216. return status
  217. elif d.xpath('//*[@text="没有更多了"]').exists:
  218. # 没有更多帖子
  219. print('没有更多了展示')
  220. status = shell_status_content(0, 501, "用户拉取到'没有更多了'标识,仍未获取到帖子", '', '', '')
  221. return status
  222. else:
  223. d.swipe_ext("up", 1)
  224. def check_follow_button(d):
  225. # 如果存在关注用户标签 进行关闭
  226. if d.xpath('//*[@resource-id="com.ss.android.auto:id/byg"]').exists:
  227. d.xpath('//*[@resource-id="com.ss.android.auto:id/byg"]').click()
  228. def __formate_time(time_str):
  229. now = datetime.now()
  230. if '刚刚' in time_str:
  231. p_time = now
  232. elif '分钟' in time_str:
  233. minutes = int(time_str[:time_str.index('分')])
  234. p_time = now - timedelta(minutes=minutes)
  235. elif '小时' in time_str:
  236. hours = int(time_str[:time_str.index('小')])
  237. p_time = now - timedelta(hours=hours)
  238. elif '昨天' in time_str:
  239. p_time = now - timedelta(days=1)
  240. elif '前天' in time_str:
  241. p_time = now - timedelta(days=2)
  242. elif '天' in time_str:
  243. days = int(time_str[:time_str.index('天')])
  244. p_time = now - timedelta(days=days)
  245. elif '周' in time_str:
  246. weeks = int(time_str[:time_str.index('周') - 1])
  247. p_time = now - timedelta(weeks=weeks)
  248. elif '年' not in time_str and '月' in time_str and '日' in time_str:
  249. p_time = datetime.strptime(f'{now.year}年{time_str}', "%Y年%m月%d日").date()
  250. else:
  251. time_str = time_str.split()[0]
  252. items = time_str.split("-")
  253. if len(items) == 2:
  254. p_time = datetime.strptime(f'{now.year}年{items[0]}月{items[1]}日', "%Y年%m月%d日").date()
  255. elif len(items) == 3:
  256. p_time = datetime.strptime(f'{items[0]}年{items[1]}月{items[2]}日', "%Y年%m月%d日").date()
  257. else:
  258. return None
  259. #
  260. p_time = p_time.strftime('%Y-%m-%d')
  261. return p_time
  262. # 进入帖子进行操作
  263. def title_action(d, keyword, account_name, task_id):
  264. time.sleep(3)
  265. # 目前只支持对帖子进行操作
  266. if not d.xpath('//*[@resource-id="com.ss.android.auto:id/ff6"]').exists and not d.xpath(
  267. '//*[@resource-id="com.ss.android.auto:id/d6l"]').exists:
  268. # 可能受网络/帖子内容影响 元素未加载出来 等待三秒再重试加载
  269. time.sleep(3)
  270. if not retry(lambda: d.xpath('//*[@resource-id="com.tencent.wework:id/ff6"]').exists, task_id):
  271. # if not d.xpath('//*[@resource-id="com.ss.android.auto:id/ff6"]').exists:
  272. # 获取评论内容,首先获取原文内容如果没有获取到,则获取标题
  273. status = shell_status_content(0, 501, "该文章类型不支持操作", '', '', '')
  274. d.app_stop("com.ss.android.auto")
  275. return status
  276. if d.xpath('//*[@text="视频"]').exists:
  277. # 返回未找到帖子
  278. status = shell_status_content(0, 501, '该帖子为视频类型不支持评论', '', '', '')
  279. d.app_stop("com.ss.android.auto")
  280. return status
  281. comment_request_text = ''
  282. if d.xpath('//*[@resource-id="com.ss.android.auto:id/s"]').exists:
  283. comment_request_text = d.xpath('//*[@resource-id="com.ss.android.auto:id/s"]').get_text()
  284. elif d.xpath('//*[@resource-id="com.ss.android.auto:id/izg"]').exists:
  285. comment_request_text = d.xpath('//*[@resource-id="com.ss.android.auto:id/izg"]').get_text()
  286. elif d.xpath('//*[@resource-id="com.ss.android.auto:id/p"]').exists:
  287. comment_request_text = d.xpath('//*[@resource-id="com.ss.android.auto:id/p"]').get_text()
  288. else:
  289. d.swipe_ext("up", 0.3)
  290. text_view_text = ''
  291. view_view_text = ''
  292. # 获取文章
  293. article_list = d.xpath('//android.widget.TextView').all()
  294. if article_list is None or article_list.__len__() == 0:
  295. text_view_text = keyword
  296. else:
  297. # if article_list.__len__() > 3:
  298. # article_list = article_list[3:]
  299. for article in article_list:
  300. # if article.text.contains('说点什么'):
  301. # break
  302. text_view_text += article.text
  303. # 获取文章
  304. article_list = d.xpath('//android.view.View').all()
  305. if article_list is None or article_list.__len__() == 0:
  306. view_view_text = keyword
  307. else:
  308. # if article_list.__len__() > 3:
  309. # article_list = article_list[3:]
  310. for article in article_list:
  311. # if article.text.contains('说点什么'):
  312. # break
  313. view_view_text += article.text
  314. if len(view_view_text) > len(text_view_text):
  315. comment_request_text = view_view_text
  316. else:
  317. comment_request_text = text_view_text
  318. request_data = {
  319. "templateId": "dcd_comment",
  320. "content": comment_request_text
  321. }
  322. # response = httpx.post('http://47.116.62.124:3000/comment', json=request_data, timeout=120)
  323. #
  324. # if not response.is_success:
  325. # # 调用AIGC获取评论失败
  326. # status = shell_status_content(0, 500, "调用AIGC获取评论失败", '', '','')
  327. # d.app_stop("com.ss.android.auto")
  328. # return status
  329. #
  330. # data = json.loads(response.text)
  331. #
  332. # reply_text = data.get('text')
  333. #
  334. # reply_text_json = json.loads(reply_text)
  335. #
  336. # reply = reply_text_json.get('comment')
  337. response = httpx.post(post_ai_gc_url, json=request_data, timeout=120)
  338. if not response.is_success:
  339. # 调用AIGC获取评论失败
  340. status = shell_status_content(0, 500, "调用AIGC获取评论失败", '', '', '')
  341. d.app_stop("com.ss.android.auto")
  342. return status
  343. response_body = json.loads(response.text)
  344. data = response_body.get('data')
  345. reply_text = data.get('answer')
  346. reply_text_json = json.loads(reply_text)
  347. reply = reply_text_json.get('comment')
  348. if '内容太少' in reply or '对不起' in reply or reply is None or reply == '':
  349. # 返回未获取合适回复内容
  350. status = shell_status_content(0, 500, "无效评论,请求为:" + comment_request_text + "回复为:" + reply, '', '', '')
  351. d.app_stop("com.ss.android.auto")
  352. return status
  353. # 点赞
  354. # if d.xpath('//*[@resource-id="com.ss.android.auto:id/d6l"]').exists:
  355. # like_list = d.xpath('//*[@resource-id="com.ss.android.auto:id/d6l"]').all()
  356. # if like_list.__len__() == 1:
  357. # like_list[0].click()
  358. # else:
  359. # like_list[like_list.__len__() - 1].click()
  360. # else:
  361. # like_list = d.xpath('//*[@resource-id="com.ss.android.auto:id/ff6"]').all()
  362. # if like_list.__len__() == 1:
  363. # like_list[0].click()
  364. # else:
  365. # like_list[like_list.__len__() - 1].click()
  366. #
  367. # time.sleep(2)
  368. # 点击对话框
  369. if d.xpath('//*[@resource-id="com.ss.android.auto:id/kl2"]').exists:
  370. d.xpath('//*[@resource-id="com.ss.android.auto:id/kl2"]').click()
  371. elif d.xpath('//*[@resource-id="com.ss.android.auto:id/dib"]').exists:
  372. d.xpath('//*[@resource-id="com.ss.android.auto:id/dib"]').click()
  373. time.sleep(2)
  374. d.xpath('//*[@resource-id="com.ss.android.auto:id/iit"]').click()
  375. elif d.xpath('//*[@resource-id="com.ss.android.auto:id/die"]').exists:
  376. d.xpath('//*[@resource-id="com.ss.android.auto:id/die"]').click()
  377. else:
  378. d.xpath('//*[@resource-id="com.ss.android.auto:id/dvq"]').click()
  379. time.sleep(2)
  380. # 评论框赋值
  381. d.xpath('//*[@resource-id="com.ss.android.auto:id/daj"]').set_text(reply)
  382. time.sleep(2)
  383. # 发布
  384. d.xpath('//*[@resource-id="com.ss.android.auto:id/fx_"]').click()
  385. time.sleep(2)
  386. if d.xpath('//*[@text="发布评论"]').exists:
  387. d.xpath('//*[@text="发布评论"]').click()
  388. content_result = content_detail(keyword, comment_request_text, reply, account_name)
  389. status = shell_status_content(1, 0, "", '', json.dumps(content_result.to_dict(), ensure_ascii=False), account_name)
  390. return status
  391. # 机器人重复查找元素 针对网络不顺畅的情况 或手机性能问题
  392. def retry(operation, device_serial, retries=11, retry_interval=3):
  393. for i in range(retries):
  394. loggerKit.info("懂车帝互动获取多次获取元素" + device_serial + "开始操作")
  395. if operation():
  396. return True
  397. if i == retries - 1:
  398. return False
  399. time.sleep(retry_interval) # 等待2秒再重试
  400. return False
  401. if __name__ == "__main__":
  402. # search_test('QXNUT21905001550','新车 售18.99万元,定位纯电中大型轿车,飞凡F7都市版正式上市')
  403. # while 1:
  404. #
  405. json_str = '''{
  406. "data": [
  407. {
  408. "taskId": 1163119,
  409. "mediaChannel": "dongchedi",
  410. "taskType": "testPOC",
  411. "taskSubType": "dongchediAppointInteraction",
  412. "taskSequenceId": "",
  413. "taskDesc": "懂车帝指定账号互动",
  414. "actionType": "content",
  415. "resourceName": "为何飞凡广州车展带来的两道“开胃菜”,让人越吃越“舒适”?",
  416. "subResourceName": null,
  417. "executeRobotAccount": "testRobotAccount",
  418. "executeRobotName": "testRobotAccount",
  419. "deviceId": "1",
  420. "content": "",
  421. "answerType": null,
  422. "materialUri": null,
  423. "spiderType": null,
  424. "sequence": null,
  425. "demoTask": false
  426. },
  427. {
  428. "taskId": 1163119,
  429. "mediaChannel": "dongchedi",
  430. "taskType": "testPOC",
  431. "taskSubType": "dongchediAppointInteraction",
  432. "taskSequenceId": "",
  433. "taskDesc": "懂车帝指定账号互动",
  434. "actionType": "content",
  435. "resourceName": "为何飞凡广州车展带来的两道“开胃菜”,让人越吃越“舒适”?",
  436. "subResourceName": null,
  437. "executeRobotAccount": "testRobotAccount",
  438. "executeRobotName": "testRobotAccount",
  439. "deviceId": "1",
  440. "content": "",
  441. "answerType": null,
  442. "materialUri": null,
  443. "spiderType": null,
  444. "sequence": null,
  445. "demoTask": false
  446. }
  447. ],
  448. "code": "00000000",
  449. "message": "success"
  450. }'''
  451. data_list = json.loads(json_str).get('data')
  452. for data in data_list:
  453. print(data.get('taskId'))
  454. status = simulated_operation("7HX5T19911019170", "感受飞凡F7最快的一次充电", 199, 'dongchedi', 'Lightman317')
  455. print(json.dumps(status, default=lambda o: o.__dict__, indent=4, ensure_ascii=False))